commit 984a660eed17bf71c223db0df1350cab91641e5c Author: Limo Date: Mon Aug 12 19:12:41 2024 +0200 initial release diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f2b4809 --- /dev/null +++ b/.gitignore @@ -0,0 +1,79 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + +# Builds +build +tests/build + +# docs +doc diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..5844377 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,246 @@ +cmake_minimum_required(VERSION 3.25) + +project(Limo VERSION 1.0 LANGUAGES CXX) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# jsoncpp +find_package(PkgConfig REQUIRED) +pkg_check_modules(JSONCPP jsoncpp) + +# libarchive +find_package(LibArchive REQUIRED) + +# pugixml +pkg_check_modules(PUGIXML pugixml) +find_package(pugixml REQUIRED) + +# cpr +find_package(cpr REQUIRED) + +# OpenSSL +find_package(OpenSSL REQUIRED) + +# Qt +find_package(QT NAMES Qt5 REQUIRED COMPONENTS Widgets) +find_package(Qt5 REQUIRED COMPONENTS Widgets Svg Network) + +set(PROJECT_SOURCES + resources/icons.qrc + src/core/addmodinfo.h + src/core/appinfo.h + src/core/autotag.cpp + src/core/autotag.h + src/core/backupmanager.cpp + src/core/backupmanager.h + src/core/backuptarget.cpp + src/core/backuptarget.h + src/core/casematchingdeployer.cpp + src/core/casematchingdeployer.h + src/core/compressionerror.h + src/core/conflictinfo.h + src/core/cryptography.cpp + src/core/cryptography.h + src/core/deployer.cpp + src/core/deployer.h + src/core/deployerfactory.cpp + src/core/deployerfactory.h + src/core/deployerinfo.h + src/core/editapplicationinfo.h + src/core/editautotagaction.cpp + src/core/editautotagaction.h + src/core/editdeployerinfo.h + src/core/editmanualtagaction.cpp + src/core/editmanualtagaction.h + src/core/editprofileinfo.h + src/core/fomod/dependency.cpp + src/core/fomod/dependency.h + src/core/fomod/file.h + src/core/fomod/fomodinstaller.cpp + src/core/fomod/fomodinstaller.h + src/core/fomod/plugin.h + src/core/fomod/plugindependency.h + src/core/fomod/plugingroup.h + src/core/fomod/plugintype.h + src/core/importmodinfo.h + src/core/installer.cpp + src/core/installer.h + src/core/log.cpp + src/core/log.h + src/core/lootdeployer.cpp + src/core/lootdeployer.h + src/core/manualtag.cpp + src/core/manualtag.h + src/core/mod.cpp + src/core/mod.h + src/core/moddedapplication.cpp + src/core/moddedapplication.h + src/core/modinfo.h + src/core/nexus/api.cpp + src/core/nexus/api.h + src/core/nexus/file.cpp + src/core/nexus/file.h + src/core/nexus/mod.cpp + src/core/nexus/mod.h + src/core/parseerror.h + src/core/pathutils.cpp + src/core/pathutils.h + src/core/progressnode.cpp + src/core/progressnode.h + src/core/tag.cpp + src/core/tag.h + src/core/tagcondition.h + src/core/tagconditionnode.cpp + src/core/tagconditionnode.h + src/main.cpp + src/ui/addapikeydialog.cpp + src/ui/addapikeydialog.h + src/ui/addapikeydialog.ui + src/ui/addappdialog.cpp + src/ui/addappdialog.h + src/ui/addappdialog.ui + src/ui/addautotagdialog.cpp + src/ui/addautotagdialog.h + src/ui/addautotagdialog.ui + src/ui/addbackupdialog.cpp + src/ui/addbackupdialog.h + src/ui/addbackupdialog.ui + src/ui/addbackuptargetdialog.cpp + src/ui/addbackuptargetdialog.h + src/ui/addbackuptargetdialog.ui + src/ui/adddeployerdialog.cpp + src/ui/adddeployerdialog.h + src/ui/adddeployerdialog.ui + src/ui/addmoddialog.cpp + src/ui/addmoddialog.h + src/ui/addmoddialog.ui + src/ui/addprofiledialog.cpp + src/ui/addprofiledialog.h + src/ui/addprofiledialog.ui + src/ui/addtodeployerdialog.cpp + src/ui/addtodeployerdialog.h + src/ui/addtodeployerdialog.ui + src/ui/addtogroupdialog.cpp + src/ui/addtogroupdialog.h + src/ui/addtogroupdialog.ui + src/ui/addtooldialog.cpp + src/ui/addtooldialog.h + src/ui/addtooldialog.ui + src/ui/applicationmanager.cpp + src/ui/applicationmanager.h + src/ui/backuplistmodel.cpp + src/ui/backuplistmodel.h + src/ui/backuplistview.cpp + src/ui/backuplistview.h + src/ui/backupnamedelegate.cpp + src/ui/backupnamedelegate.h + src/ui/changeapipwdialog.cpp + src/ui/changeapipwdialog.h + src/ui/changeapipwdialog.ui + src/ui/colors.h + src/ui/conflictsmodel.cpp + src/ui/conflictsmodel.h + src/ui/deployerlistmodel.cpp + src/ui/deployerlistmodel.h + src/ui/deployerlistproxymodel.cpp + src/ui/deployerlistproxymodel.h + src/ui/deployerlistview.cpp + src/ui/deployerlistview.h + src/ui/editautotagsdialog.cpp + src/ui/editautotagsdialog.h + src/ui/editautotagsdialog.ui + src/ui/editmanualtagsdialog.cpp + src/ui/editmanualtagsdialog.h + src/ui/editmanualtagsdialog.ui + src/ui/editmodsourcesdialog.cpp + src/ui/editmodsourcesdialog.h + src/ui/editmodsourcesdialog.ui + src/ui/enterapipwdialog.cpp + src/ui/enterapipwdialog.h + src/ui/enterapipwdialog.ui + src/ui/fomodcheckbox.cpp + src/ui/fomodcheckbox.h + src/ui/fomoddialog.cpp + src/ui/fomoddialog.h + src/ui/fomoddialog.ui + src/ui/fomodradiobutton.cpp + src/ui/fomodradiobutton.h + src/ui/importfromsteamdialog.cpp + src/ui/importfromsteamdialog.h + src/ui/importfromsteamdialog.ui + src/ui/ipcclient.cpp + src/ui/ipcclient.h + src/ui/ipcserver.cpp + src/ui/ipcserver.h + src/ui/mainwindow.cpp + src/ui/mainwindow.h + src/ui/mainwindow.ui + src/ui/managemodtagsdialog.cpp + src/ui/managemodtagsdialog.h + src/ui/managemodtagsdialog.ui + src/ui/modlistmodel.cpp + src/ui/modlistmodel.h + src/ui/modlistproxymodel.cpp + src/ui/modlistproxymodel.h + src/ui/modlistview.cpp + src/ui/modlistview.h + src/ui/modnamedelegate.cpp + src/ui/modnamedelegate.h + src/ui/movemoddialog.cpp + src/ui/movemoddialog.h + src/ui/movemoddialog.ui + src/ui/nexusmoddialog.cpp + src/ui/nexusmoddialog.h + src/ui/nexusmoddialog.ui + src/ui/overwritebackupdialog.cpp + src/ui/overwritebackupdialog.h + src/ui/overwritebackupdialog.ui + src/ui/passwordfield.cpp + src/ui/passwordfield.h + src/ui/settingsdialog.cpp + src/ui/settingsdialog.h + src/ui/settingsdialog.ui + src/ui/tablecelldelegate.cpp + src/ui/tablecelldelegate.h + src/ui/tablepushbutton.cpp + src/ui/tablepushbutton.h + src/ui/tabletoolbutton.cpp + src/ui/tabletoolbutton.h + src/ui/tagcheckbox.cpp + src/ui/tagcheckbox.h + src/ui/validatinglineedit.cpp + src/ui/validatinglineedit.h + src/ui/versionboxdelegate.cpp + src/ui/versionboxdelegate.h +) + +add_executable(Limo + ${PROJECT_SOURCES}) + +target_include_directories(Limo + PRIVATE "${PROJECT_SOURCE_DIR}/src" + PRIVATE ${LibArchive_INCLUDE_DIRS} + PRIVATE /usr/include/loot + PRIVATE ${JSONCPP_INCLUDE_DIRS}) + +target_link_libraries(Limo + PRIVATE Qt${QT_VERSION_MAJOR}::Widgets + PRIVATE ${JSONCPP_LIBRARIES} + PRIVATE ${LibArchive_LIBRARIES} + PRIVATE ${PUGIXML_LIBRARIES} + PRIVATE libloot.so + PRIVATE Qt${QT_VERSION_MAJOR}::Svg + PRIVATE cpr::cpr + PRIVATE OpenSSL::SSL + PRIVATE Qt${QT_VERSION_MAJOR}::Network) + +install(TARGETS Limo + BUNDLE DESTINATION . + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/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/README.md b/README.md new file mode 100644 index 0000000..059bac7 --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +# Limo logo +--- + +General purpose mod manager primarily developed for Linux with support for the [NexusMods](https://www.nexusmods.com/) API and [LOOT](https://loot.github.io/). + +

+logo +

+## Features +--- + +- Multiple target directories per application +- Automatic adaptation of mod file names to prevent issues with case mismatches +- Auto-Tagging system for filtering +- Sort load order according to conflicts +- Import installed games from Steam +- Simple backup system +- LOOT integration: + - Manage installed plugins + - Automatically sort the load order + - Check for issues with installed plugins +- NexusMods API support: + - Check for mod updates + - View description, changelogs and available files + - Download mods through Limo + +***For a guide on how to use Limo, refer to the wiki.*** + +## Installation +--- + +### Build from source + +#### Install the dependencies + + - [Qt5](https://doc.qt.io/qt-5/index.html) + - [JsonCpp](https://github.com/open-source-parsers/jsoncpp) + - [libarchive](https://github.com/libarchive/libarchive) + - [pugixml](https://github.com/zeux/pugixml) + - [OpenSSL](https://github.com/openssl/openssl) + - [cpr](https://github.com/libcpr/cpr) + - [libloot](https://github.com/loot/libloot) + - (Optional, for tests) [Catch2](https://github.com/catchorg/Catch2) + - (Optional, for docs) [doxygen](https://github.com/doxygen/doxygen) + +On Debian based systems most dependencies, with the exception of cpr and libloot, can be installed with the following command: + +``` +sudo apt install \ + build-essential \ + cmake \ + git \ + libpugixml-dev \ + libjsoncpp-dev \ + libarchive-dev \ + pkg-config \ + libssl-dev \ + qtbase5-dev \ + qtchooser \ + qt5-qmake \ + qtbase5-dev-tools \ + libqt5svg5-dev \ + libbost-all-dev \ + libtbb-dev \ + cargo \ + cbindgen \ + catch2 \ + doxygen +``` + +#### Clone this repository: + +``` +git clone https://github.com/limo-app/limo.git +``` + +#### Build Limo: + +``` +mkdir build +cmake -DCMAKE_BUILD_TYPE=Release -S . -B build +cmake --build build +``` + +#### (Optional) Run the tests: + +``` +mkdir tests/build +cmake -DCMAKE_BUILD_TYPE=Release -S tests -B tests/build +cmake --build tests/build +tests/build/tests +``` + +#### (Optional) Build the documentation: + +``` +doxygen src/lmm_Doxyfile +``` diff --git a/resources/filter_accept.svg b/resources/filter_accept.svg new file mode 100644 index 0000000..e6303aa --- /dev/null +++ b/resources/filter_accept.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + diff --git a/resources/filter_reject.svg b/resources/filter_reject.svg new file mode 100644 index 0000000..d1ec05b --- /dev/null +++ b/resources/filter_reject.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + diff --git a/resources/icons.qrc b/resources/icons.qrc new file mode 100644 index 0000000..58401c6 --- /dev/null +++ b/resources/icons.qrc @@ -0,0 +1,7 @@ + + + filter_accept.svg + filter_reject.svg + logo.png + + diff --git a/resources/logo.png b/resources/logo.png new file mode 100644 index 0000000..2058f3d Binary files /dev/null and b/resources/logo.png differ diff --git a/resources/logo_small.png b/resources/logo_small.png new file mode 100644 index 0000000..1df0d8c Binary files /dev/null and b/resources/logo_small.png differ diff --git a/resources/showcase.png b/resources/showcase.png new file mode 100644 index 0000000..759cb9e Binary files /dev/null and b/resources/showcase.png differ diff --git a/src/core/addmodinfo.h b/src/core/addmodinfo.h new file mode 100644 index 0000000..53e0a2d --- /dev/null +++ b/src/core/addmodinfo.h @@ -0,0 +1,42 @@ +/*! + * \file addmodinfo.h + * \brief Contains the AddModInfo struct. + */ + +#pragma once + +#include +#include +#include + + +/*! + * \brief Stores data needed to install a new mod. + */ +struct AddModInfo +{ + /*! \brief Name of the new mod. */ + std::string name; + /*! \brief Version of the new mod. */ + std::string version; + /*! \brief Installer type to be used. */ + std::string installer; + /*! \brief Path to the mods files. */ + std::string source_path; + /*! \brief Ids of deployers to which the new mod will be added. */ + std::vector deployers; + /*! \brief Id of the mod the group of which the new mod will be added to, or -1 for no group. */ + int group; + /*! \brief Flags for the installer. */ + int installer_flags; + /*! \brief If > 0: Remove path components with depth < root_level. */ + int root_level; + /*! \brief Contains pairs of source and destination paths for installation files. */ + std::vector> files; + /*! \brief If true: The newly installed mod will replace the mod specified in group. */ + bool replace_mod = false; + /*! \brief Path to the local archive or directory used to install this mod. */ + std::filesystem::path local_source = ""; + /*! \brief URL from where the mod was downloaded. */ + std::string remote_source = ""; +}; diff --git a/src/core/appinfo.h b/src/core/appinfo.h new file mode 100644 index 0000000..56879e8 --- /dev/null +++ b/src/core/appinfo.h @@ -0,0 +1,72 @@ +/*! + * \file appinfo.h + * \brief Contains the AppInfo struct. + */ + +#pragma once + +#include "tagcondition.h" +#include +#include +#include + + +/*! + * \brief Stores information about a ModdedApplication. + */ +struct AppInfo +{ + /*! \brief The \ref ModdedApplication "application's" name. */ + std::string name = ""; + /*! \brief The \ref ModdedApplication "application's" staging directory. */ + std::string staging_dir = ""; + /*! \brief Command used to run the \ref ModdedApplication "application". */ + std::string command = ""; + /*! \brief Number of installed mods of the \ref ModdedApplication "application". */ + int num_mods = 0; + /*! + * \brief Names of \ref Deployer "deployers" belonging to the + * \ref ModdedApplication "application". + */ + std::vector deployers{}; + /*! + * \brief Types of \ref Deployer "deployers" belonging to the + * \ref ModdedApplication "application". + */ + std::vector deployer_types{}; + /*! + * \brief Staging directory of \ref Deployer "deployers" belonging to the + * \ref ModdedApplication "application". + */ + std::vector target_dirs{}; + /*! + * \brief Number of mods for each \ref Deployer "deployer" belonging to the + * \ref ModdedApplication "application". + */ + std::vector deployer_mods{}; + /*! + * \brief One bool per deployer indicating whether file are copied for deployment. + */ + std::vector uses_copy_deployment{}; + /*! + * \brief Name and command for each tool belonging to the + * \ref ModdedApplication "application". + */ + std::vector> tools{}; + /*! + * \brief Maps the names of all manual tags to the number of mods with that tag in the + * \ref ModdedApplication "application". + */ + std::map num_mods_per_manual_tag; + /*! + * \brief Maps the names of all auto tags to the number of mods with that tag in the + * \ref ModdedApplication "application". + */ + std::map num_mods_per_auto_tag; + /*! + * \brief Maps all auto tag names to a pair of the expression used and a vector of Tagconditions. + */ + std::map>> auto_tags; + /*! \brief Version of the target application. */ + std::string app_version = ""; +}; diff --git a/src/core/autotag.cpp b/src/core/autotag.cpp new file mode 100644 index 0000000..3cf307b --- /dev/null +++ b/src/core/autotag.cpp @@ -0,0 +1,110 @@ +#include "autotag.h" +#include "parseerror.h" +#include +#include + +namespace sfs = std::filesystem; +namespace str = std::ranges; + + +AutoTag::AutoTag(const std::string& name, + const std::string& expression, + const std::vector& conditions) : + expression_(expression), conditions_(conditions), evaluator_(expression, conditions) +{ + name_ = name; +} + +AutoTag::AutoTag(const Json::Value& json) +{ + if(!json.isMember("name")) + throw ParseError("Tag name is missing."); + name_ = json["name"].asString(); + + if(json.isMember("mod_ids")) + { + for(const auto& mod : json["mod_ids"]) + mods_.push_back(mod.asInt()); + } + + if(!json.isMember("expression")) + throw ParseError("Auto-Tag expression is missing."); + expression_ = json["expression"].asString(); + + if(!json.isMember("conditions")) + throw ParseError("Auto-Tag conditions are missing."); + for(const auto& json_condition : json["conditions"]) + { + TagCondition condition; + if(!json_condition.isMember("invert")) + throw ParseError("Auto-Tag condition invert flag is missing."); + condition.invert = json_condition["invert"].asBool(); + + if(!json_condition.isMember("use_regex")) + throw ParseError("Auto-Tag condition use_regex flag is missing."); + condition.use_regex = json_condition["use_regex"].asBool(); + + if(!json_condition.isMember("search_string")) + throw ParseError("Auto-Tag search_string is missing."); + condition.search_string = json_condition["search_string"].asString(); + + if(!json_condition.isMember("condition_type")) + throw ParseError("Auto-Tag condition_type is missing."); + condition.condition_type = json_condition["condition_type"].asString() == "file_name" + ? TagCondition::Type::file_name + : TagCondition::Type::path; + conditions_.push_back(condition); + } + if(!TagConditionNode::expressionIsValid(expression_, conditions_.size())) + throw ParseError(std::format("Invalid auto tag expression \"{}\".", expression_)); + evaluator_ = TagConditionNode(expression_, conditions_); +} + +void AutoTag::setEvaluator(const std::string& expression, + const std::vector& conditions) +{ + expression_ = expression; + conditions_ = conditions; + evaluator_ = TagConditionNode(expression, conditions_); +} + +Json::Value AutoTag::toJson() const +{ + Json::Value json; + for(int i = 0; i < mods_.size(); i++) + json["mod_ids"][i] = mods_[i]; + + json["expression"] = expression_; + json["name"] = name_; + + for(const auto& [index, condition] : str::enumerate_view(conditions_)) + { + const int i = index; + json["conditions"][i]["invert"] = condition.invert; + json["conditions"][i]["use_regex"] = condition.use_regex; + json["conditions"][i]["search_string"] = condition.search_string; + json["conditions"][i]["condition_type"] = + condition.condition_type == TagCondition::Type::file_name ? "file_name" : "path"; + } + return json; +} + +bool AutoTag::operator==(const std::string& name) const +{ + return name_ == name; +} + +std::string AutoTag::getExpression() const +{ + return expression_; +} + +std::vector AutoTag::getConditions() const +{ + return conditions_; +} + +int AutoTag::getNumConditions() const +{ + return conditions_.size(); +} diff --git a/src/core/autotag.h b/src/core/autotag.h new file mode 100644 index 0000000..8c43de5 --- /dev/null +++ b/src/core/autotag.h @@ -0,0 +1,191 @@ +/*! + * \file autotag.h + * \brief Header for the AutoTag class. + */ + +#pragma once + +#include "pathutils.h" +#include "progressnode.h" +#include "tag.h" +#include "tagconditionnode.h" +#include +#include +#include +#include +#include + + +/*! + * \brief Tag which is automatically added to a mod when its files fulfill the tags conditions. + * Conditions are managed by a TagConditionNode object. + */ +class AutoTag : public Tag +{ +public: + /*! + * \brief Constructor. + * \param name Name of the new tag. + * \param expression Boolean expression used to combine the given conditions. The tag is applied + * to a mod when this evaluates to true. + * \param conditions Vector of conditions used to decide if this tag is to be applied. These + * act as variables in the tags expression. + */ + AutoTag(const std::string& name, + const std::string& expression, + const std::vector& conditions); + /*! + * \brief Deserializes an AutoTag from the given json object. + * \param json Source json object. + * \throws ParseError when the json object is invalid. + */ + AutoTag(const Json::Value& json); + + /*! + * \brief Removes this tag from all mods, then applies it to all given mods which + * fulfill its conditions. + * \param files Maps mod ids to a vector of pairs of paths and file names for that mod. + * \param mods Iterable container containing int ids of all mods to be checked. + * \param progress_node Used to inform about progress. + */ + template + void reapplyMods(const std::map>>& files, + const View& mods, + std::optional progress_node = {}) + { + mods_.clear(); + for(int mod : mods) + { + if(evaluator_.evaluate(files.at(mod))) + mods_.push_back(mod); + if(progress_node) + (*progress_node)->advance(); + } + } + /*! + * \brief Removes this tag from all mods, then applies it to all given mods which + * fulfill its conditions. + * \param staging_dir Directory containing the mods. + * \param mods Iterable container containing int ids of all mods to be checked. + * \param progress_node Used to inform about progress. + */ + template + void reapplyMods(const std::filesystem::path& staging_dir, + const View& mods, + std::optional progress_node = {}) + { + reapplyMods(readModFiles(staging_dir, mods), mods, progress_node); + } + /*! + * \brief Reevaluates if the given mods should have this tag. Adds/ removes the tag + * from all given mods when needed. + * \param files Maps mod ids to a vector of pairs of paths and file names for that mod. + * \param mods Iterable container containing int ids of all mods to be checked. + * \param progress_node Used to inform about progress. + */ + template + void updateMods(const std::map>>& files, + const View& mods, + std::optional progress_node = {}) + { + for(int mod : mods) + { + auto iter = std::ranges::find(mods_, mod); + if(iter != mods_.end()) + mods_.erase(iter); + if(evaluator_.evaluate(files.at(mod))) + mods_.push_back(mod); + if(progress_node) + (*progress_node)->advance(); + } + } + /*! + * \brief Reevaluates if the given mods should have this tag. Adds/ removes the tag + * from all given mods when needed. + * \param staging_dir Directory containing the mods. + * \param mods Iterable container containing int ids of all mods to be checked. + * \param progress_node Used to inform about progress. + */ + template + void updateMods(const std::filesystem::path& staging_dir, + const View& mods, + std::optional progress_node = {}) + { + updateMods(readModFiles(staging_dir, mods), mods, progress_node); + } + /*! + * \brief Changes the conditions and expression used by this tag. + * \param expression The new expression. + * \param conditions The new conditions. + */ + void setEvaluator(const std::string& expression, const std::vector& conditions); + /*! + * \brief Serializes this tag to a json object. + * \return The json object. + */ + Json::Value toJson() const; + /*! + * \brief Compares this tag by name to the given name. + * \param name Name to compare to. + * \return True if the names are identical. + */ + bool operator==(const std::string& name) const; + /*! + * \brief Getter for this tags expression. + * \return The expression. + */ + std::string getExpression() const; + /*! + * \brief Getter for this tags conditions. + * \return The conditions. + */ + std::vector getConditions() const; + /*! + * \brief Returns the number of conditions for this tag. + * \return The number of conditions. + */ + int getNumConditions() const; + /*! + * \brief Recursively iterates over all files for all mods with given ids and creates a + * a map of mod ids to a vector containing pairs of path and file name. + * This vector is used as input for the reapplyMods and updateMods functions. + * \param staging_dir Staging directory for the given mods. + * \param mods Iterable container containing int ids of all mods to be checked. + * \param progress_node Used to inform about progress. + * \return The map. + */ + template + static std::map>> readModFiles( + const std::filesystem::path& staging_dir, + View mods, + std::optional progress_node = {}) + { + std::map>> files; + for(int mod : mods) + { + files[mod] = {}; + const std::filesystem::path mod_path = staging_dir / std::to_string(mod); + for(const auto& dir_entry : std::filesystem::recursive_directory_iterator(mod_path)) + { + std::string path = path_utils::getRelativePath(dir_entry.path(), mod_path); + if(path.front() == '/') + path.erase(0, 1); + files[mod].emplace_back(path, dir_entry.path().filename().string()); + } + if(progress_node) + (*progress_node)->advance(); + } + return files; + } + +private: + /*! \brief Expression used by the TagConditionNode. */ + std::string expression_; + /*! \brief Conditions used by the TagConditionNode. */ + std::vector conditions_; + /*! + * \brief This tag is applied to a mod if this nodes evaluate function returns true for + * the mods installation directory + */ + TagConditionNode evaluator_; +}; diff --git a/src/core/backupmanager.cpp b/src/core/backupmanager.cpp new file mode 100644 index 0000000..cef847c --- /dev/null +++ b/src/core/backupmanager.cpp @@ -0,0 +1,397 @@ +#include "backupmanager.h" +#include "parseerror.h" +#include "pathutils.h" +#include +#include + +namespace sfs = std::filesystem; +namespace pu = path_utils; + + +void BackupManager::addTarget(const sfs::path& path, + const std::string& name, + const std::vector& backup_names) +{ + if(!sfs::exists(path)) + throw std::runtime_error(std::format("Path \"{}\" does not exist", path.string())); + sfs::path trimmed_path = path; + if(path.string().ends_with(sfs::path::preferred_separator)) + trimmed_path = path.string().erase(path.string().size() - 1, 1); + for(const auto& target : targets_) + { + if(target.path == trimmed_path) + throw std::runtime_error(std::format( + "\"{}\" is already managed as \"{}\" by BackupManager", path.string(), target.target_name)); + } + if(sfs::exists(getConfigPath(trimmed_path))) + addTarget(trimmed_path); + else + { + if(backup_names.empty()) + throw std::runtime_error("At least one backup name must be provided"); + targets_.emplace_back(trimmed_path, + name, + std::vector{ backup_names[0] }, + std::vector(num_profiles_, 0)); + for(int i = 1; i < backup_names.size(); i++) + addBackup(targets_.size() - 1, backup_names[i]); + } + updateSettings(); +} + +void BackupManager::addTarget(const sfs::path& path) +{ + if(!sfs::exists(path)) + throw std::runtime_error(std::format("Path \"{}\" does not exist", path.string())); + if(!sfs::exists(getConfigPath(path))) + throw std::runtime_error( + std::format("Could not find settings file at \"{}\"", getConfigPath(path).string())); + for(const auto& target : targets_) + { + if(target.path == path) + throw std::runtime_error(std::format( + "\"{}\" is already managed as \"{}\" by BackupManager", path.string(), target.target_name)); + } + targets_.push_back({ path, "", {}, {} }); + updateState(); +} + +void BackupManager::addBackup(int target_id, const std::string& name, int source) +{ + if(target_id < 0 || target_id >= targets_.size()) + throw std::runtime_error(std::format("Invalid target id: {}", target_id)); + updateDirectories(target_id); + auto& target = targets_[target_id]; + sfs::path source_path; + if(source >= 0 && source < targets_[target_id].backup_names.size()) + source_path = getBackupPath(target_id, source); + else + source_path = getBackupPath(target_id, target.active_members[cur_profile_]); + sfs::copy(source_path, + getBackupPath(target.path, target.backup_names.size()), + sfs::copy_options::recursive | sfs::copy_options::copy_symlinks); + target.backup_names.push_back(name); + updateSettings(); +} + +void BackupManager::removeTarget(int target_id) +{ + for(int backup = 0; backup < targets_[target_id].backup_names.size(); backup++) + { + if(backup == targets_[target_id].active_members[cur_profile_]) + continue; + const auto path = getBackupPath(target_id, backup); + if(sfs::exists(path)) + sfs::remove_all(path); + } + const auto config_file = getConfigPath(targets_[target_id].path); + if(sfs::exists(config_file)) + sfs::remove(config_file); + targets_.erase(targets_.begin() + target_id); +} + +void BackupManager::removeBackup(int target_id, int backup_id, bool update_dirs) +{ + if(target_id < 0 || target_id >= targets_.size()) + throw std::runtime_error(std::format("Invalid target id: {}", target_id)); + if(update_dirs) + updateDirectories(target_id); + if(targets_[target_id].backup_names.size() == 1) + throw std::runtime_error( + std::format("No backups to remove for \"{}\"", targets_[target_id].target_name)); + auto& target = targets_[target_id]; + if(backup_id < 0 || backup_id >= target.backup_names.size()) + throw std::runtime_error( + std::format("Invalid backup id: {} for target: {}", backup_id, target_id)); + + if(target.active_members[cur_profile_] == backup_id) + setActiveBackup(target_id, backup_id == 0 ? 1 : 0); + for(int prof = 0; prof < num_profiles_; prof++) + target.active_members[prof] = 0; + sfs::path backup_path = getBackupPath(target.path, backup_id); + if(sfs::exists(backup_path)) + sfs::remove_all(backup_path); + for(int i = backup_id + 1; i < target.backup_names.size(); i++) + { + sfs::path cur_path = getBackupPath(target.path, i); + if(sfs::exists(cur_path)) + sfs::rename(cur_path, getBackupPath(target.path, i - 1)); + } + target.backup_names.erase(target.backup_names.begin() + backup_id); + if(update_dirs) + updateSettings(); +} + +void BackupManager::setActiveBackup(int target_id, int backup_id) +{ + if(target_id < 0 || target_id >= targets_.size()) + throw std::runtime_error(std::format("Invalid target id: {}", target_id)); + updateDirectories(target_id); + auto& target = targets_[target_id]; + if(backup_id < 0 || backup_id >= target.backup_names.size()) + throw std::runtime_error( + std::format("Invalid backup id: {} for target: \"{}\"", target.target_name, backup_id)); + int active_id = target.active_members[cur_profile_]; + if(backup_id == active_id) + return; + sfs::rename(target.path, getBackupPath(target.path, active_id)); + sfs::rename(getBackupPath(target.path, backup_id), target.path); + target.active_members[cur_profile_] = backup_id; + target.cur_active_member = backup_id; + updateSettings(); +} + +void BackupManager::setProfile(int profile) +{ + if(profile == cur_profile_) + return; + for(int target_id = 0; target_id < targets_.size(); target_id++) + { + auto& target = targets_[target_id]; + int old_id = target.active_members[cur_profile_]; + int new_id = target.active_members[profile]; + if(old_id == new_id) + continue; + setActiveBackup(target_id, new_id); + target.active_members[cur_profile_] = old_id; + } + cur_profile_ = profile; +} + +void BackupManager::addProfile(int source) +{ + num_profiles_++; + if(cur_profile_ < 0 || cur_profile_ >= num_profiles_) + cur_profile_ = 0; + for(auto& target : targets_) + { + int active_id = source >= 0 && source < num_profiles_ ? target.active_members[source] : 0; + target.active_members.push_back(active_id); + } + updateSettings(); +} + +void BackupManager::removeProfile(int profile) +{ + num_profiles_--; + for(auto& target : targets_) + target.active_members.erase(target.active_members.begin() + profile); + if(profile == cur_profile_) + setProfile(0); + else if(cur_profile_ > profile) + cur_profile_--; + updateSettings(); +} + +std::vector BackupManager::getTargets() const +{ + auto ret_targets = targets_; + for(auto& target : ret_targets) + target.cur_active_member = target.active_members[cur_profile_]; + return ret_targets; +} + +void BackupManager::reset() +{ + targets_.clear(); + num_profiles_ = 0; +} + +int BackupManager::getNumTargets() +{ + return targets_.size(); +} + +int BackupManager::getNumBackups(int target_id) +{ + return targets_[target_id].backup_names.size(); +} + +void BackupManager::setBackupName(int target_id, int backup_id, const std::string& name) +{ + targets_[target_id].backup_names[backup_id] = name; + updateSettings(); +} + +void BackupManager::setBackupTargetName(int target_id, const std::string& name) +{ + targets_[target_id].target_name = name; + updateSettings(); +} + +void BackupManager::overwriteBackup(int target_id, int source_backup, int dest_backup) +{ + if(source_backup == dest_backup) + return; + const auto source_path = getBackupPath(target_id, source_backup); + const auto dest_path = getBackupPath(target_id, dest_backup); + sfs::remove_all(dest_path); + sfs::copy( + source_path, dest_path, sfs::copy_options::recursive | sfs::copy_options::overwrite_existing); +} + +void BackupManager::setLog(const std::function& new_log) +{ + log_ = new_log; +} + +void BackupManager::updateDirectories(int target_id) +{ + std::vector missing_dirs; + for(int backup_id = 0; backup_id < targets_[target_id].backup_names.size(); backup_id++) + { + if(!sfs::exists(getBackupPath(targets_[target_id].path, backup_id)) && + backup_id != targets_[target_id].active_members[cur_profile_]) + missing_dirs.push_back(backup_id); + } + + for(int j = missing_dirs.size() - 1; j >= 0; j--) + { + log_(Log::LOG_WARNING, + std::format("Could not find backup \"{}\" for target \"{}\".", + targets_[target_id].backup_names[missing_dirs[j]], + targets_[target_id].target_name)); + removeBackup(target_id, missing_dirs[j], false); + } + + std::vector extra_dirs; + for(const auto& dir_entry : sfs::directory_iterator(targets_[target_id].path.parent_path())) + { + const auto file_name = dir_entry.path().filename(); + if(!file_name.has_extension() || file_name.extension().string() != BAK_EXTENSION) + continue; + if(!file_name.stem().has_extension()) + continue; + std::string extension = file_name.stem().extension(); + if(sfs::path(file_name).stem().stem() != targets_[target_id].path.filename()) + continue; + if(extension.starts_with(".")) + extension.replace(0, 1, ""); + if(extension.find_first_not_of("0123456789") != extension.npos) + continue; + int id = std::stoi(extension); + if(id >= targets_[target_id].backup_names.size() || + id == targets_[target_id].active_members[cur_profile_]) + extra_dirs.push_back(dir_entry.path()); + } + + for(const auto& path : extra_dirs) + { + sfs::path new_path = path.string() + "OLD"; + int i = 0; + while(sfs::exists(new_path)) + new_path = path.string() + "OLD" + std::to_string(i++); + log_(Log::LOG_WARNING, + std::format( + "Unknown backup found at \"{}\". Moving to \"{}\".", path.string(), new_path.string())); + sfs::rename(path, new_path); + } + updateSettings(); +} + +void BackupManager::updateDirectories() +{ + for(int target_id = 0; target_id < targets_.size(); target_id++) + updateDirectories(target_id); + updateSettings(); +} + +void BackupManager::updateState() +{ + for(auto& target : targets_) + { + const auto settings = readSettings(getConfigPath(target.path)); + auto keys = { "path", "target_name", "backup_names", "active_members" }; + for(const auto& key : keys) + { + if(!settings.isMember(key)) + throw ParseError(std::format("\"{}\" is missing in \"{}\"", key, target.path.string())); + } + if(settings["path"].asString() != target.path.string()) + throw ParseError(std::format( + "Invalid path \"{}\" in \"{}\"", settings["path"].asString(), target.path.string())); + std::vector new_names; + auto names = settings["backup_names"]; + if(names.empty()) + throw ParseError(std::format("No backups found for \"{}\"", target.path.string())); + for(int i = 0; i < names.size(); i++) + new_names.push_back(names[i].asString()); + std::vector new_active_members; + auto active_members = settings["active_members"]; + for(int i = 0; i < active_members.size(); i++) + { + int member = active_members[i].asInt(); + if(member < 0 || member >= new_names.size()) + throw ParseError( + std::format("Invalid active member\"{}\" in \"{}\"", member, target.path.string())); + new_active_members.push_back(member); + } + if(active_members.size() != num_profiles_) + throw ParseError( + std::format("Failed to parse active_members in \"{}\"", target.path.string())); + target.target_name = settings["target_name"].asString(); + target.backup_names = new_names; + target.active_members = new_active_members; + } + updateDirectories(); +} + +void BackupManager::updateSettings() +{ + for(const auto& target : targets_) + { + Json::Value settings; + settings["path"] = target.path.string(); + settings["target_name"] = target.target_name; + for(int i = 0; i < target.backup_names.size(); i++) + settings["backup_names"][i] = target.backup_names[i]; + for(int i = 0; i < target.active_members.size(); i++) + settings["active_members"][i] = target.active_members[i]; + writeSettings(getConfigPath(target.path), settings); + } +} + +void BackupManager::writeSettings(const sfs::path& path, const Json::Value& settings) const +{ + std::ofstream file(path, std::fstream::binary); + if(!file.is_open()) + throw std::runtime_error("Error: Could not write to \"" + path.string() + "\"."); + file << settings; + file.close(); +} + +Json::Value BackupManager::readSettings(const sfs::path& path) const +{ + Json::Value settings; + std::ifstream file(path, std::fstream::binary); + if(!file.is_open()) + throw std::runtime_error("Error: Could not read from \"" + path.string() + "\"."); + file >> settings; + file.close(); + return settings; +} + +sfs::path BackupManager::getConfigPath(const sfs::path& path) const +{ + if(!path.has_parent_path()) + throw std::runtime_error("Creating backups of the filesystem root is not supported"); + + sfs::path dest = path; + if(path.string().ends_with("/")) + dest = dest.parent_path(); + return dest.parent_path() / + ("." + pu::getRelativePath(dest, dest.parent_path()) + JSON_EXTENSION); +} + +sfs::path BackupManager::getBackupPath(const sfs::path& path, int backup) const +{ + return path.string() + "." + std::to_string(backup) + BAK_EXTENSION; +} + +sfs::path BackupManager::getBackupPath(int target, int backup) const +{ + sfs::path file_path = targets_[target].path; + if(targets_[target].active_members[cur_profile_] == backup) + return file_path; + return getBackupPath(file_path, backup); +} diff --git a/src/core/backupmanager.h b/src/core/backupmanager.h new file mode 100644 index 0000000..38daa2d --- /dev/null +++ b/src/core/backupmanager.h @@ -0,0 +1,196 @@ +/*! + * \file backupmanager.h + * \brief Header for the BackupManager class. + */ + +#pragma once + +#include "backuptarget.h" +#include "log.h" +#include +#include +#include +#include + + +/*! + * \brief Handles creation of, deletion of and switching between, bachups. + */ +class BackupManager +{ +public: + /*! \brief Empty default constructor. */ + BackupManager() = default; + + /*! + * \brief Adds a new target file or directory to be managed. + * \param path Path to the target file or directory. + * \param name Display name for this target. + * \param backup_names Display names for initial backups. Must contain at least one. + */ + void addTarget(const std::filesystem::path& path, + const std::string& name, + const std::vector& backup_names); + /*! + * \brief Adds a backup target which was previously managed by a BackupManager. + * \param path Path to the target file or directory. + */ + void addTarget(const std::filesystem::path& path); + /*! + * \brief Removes the given target by deleting all relevant backups and config files. + * \param target_id Target to remove. + */ + void removeTarget(int target_id); + /*! + * \brief Adds a new backup for the given target by copying the currently active backup. + * \param target_id Target for which to create a new backup. + * \param name Display name for the new backup. + * \param source Backup from which to copy files to create the new backup. If -1: + * copy currently active backup. + */ + void addBackup(int target_id, const std::string& name, int source = -1); + /*! + * \brief Deletes the given backup for given target. + * \param target_id Target from which to delete a backup. + * \param backup_id Backup to remove. + * \param update_dirs If true: Repair the target if it is in an invalid state, e.g. if + * a backup has been manually deleted. + */ + void removeBackup(int target_id, int backup_id, bool update_dirs = true); + /*! + * \brief Changes the currently active backup for the given target. + * \param target_id Target for which to change the active backup. + * \param backup_id New active backup. + */ + void setActiveBackup(int target_id, int backup_id); + /*! + * \brief Sets the active profile to the new profile and changes all active backups if + * needed. + * \param profile New active profile. + */ + void setProfile(int profile); + /*! + * \brief Adds a new profile. + * \param source If this refers to an existing backup: Copy the active backups from that + * profile. + */ + void addProfile(int source = -1); + /*! + * \brief Removes the given profile. + * \param profile Profile to be removed. + */ + void removeProfile(int profile); + /*! + * \brief Returns a vector containing information about all managed backup targets. + * \return The vector. + */ + std::vector getTargets() const; + /*! \brief Deletes all entries in targets_ as well as all profiles. */ + void reset(); + /*! \brief Returns the number of backup targets. */ + int getNumTargets(); + /*! + * \brief Returns the number of backups for the given target. + * \param target_id Backup target. + * \return The number of backups. + */ + int getNumBackups(int target_id); + /*! + * \brief Setter for the name of a backup belonging to the given target. + * \param target_id Backup target. + * \param backup_id Backup to be edited. + * \param name The new name. + */ + void setBackupName(int target_id, int backup_id, const std::string& name); + /*! + * \brief Setter for the name of a backup target. + * \param target_id Backup target. + * \param name The new name. + */ + void setBackupTargetName(int target_id, const std::string& name); + /*! + * \brief Deletes all files in the dest backup and replaces them with the files + * from the source backup. + * \param target_id Backup target. + * \param source_backup Backup from which to copy files. + * \param dest_backup Target for data deletion. + */ + void overwriteBackup(int target_id, int source_backup, int dest_backup); + /*! + * \brief Setter for log callback. + * \param new_log New log callback + */ + void setLog(const std::function& new_log); + +private: + /*! \brief File extension used for backups. */ + static inline const std::string BAK_EXTENSION = ".lmmbakman"; + /*! \brief File extension used for the files used to store a targets state. */ + static inline const std::string JSON_EXTENSION = BAK_EXTENSION + ".json"; + /*! \brief Contains all managed targets. */ + std::vector targets_{}; + /*! \brief Number of profiles. */ + int num_profiles_ = 0; + /*! \brief Currently active profile. */ + int cur_profile_ = -1; + /*! \brief Callback for logging. */ + std::function log_ = [](Log::LogLevel a, + const std::string& b) {}; + + /*! + * \brief Ensures consistency with the data on disk. + * + * This is accomplished by deleting backups for which + * no file exists and files on disk which should by filename and extension be a + * backup but have an invalid id. This is done for all files matching the filename + * and path of any target. + */ + void updateDirectories(); + /*! + * \brief Ensures consistency with the data on disk. + * + * This is accomplished by deleting backups for which + * no file exists and renaming files on disk which should by filename and extension be a + * backup but have an invalid id. This is done for all files matching the filename + * and path of the given target. + * \param target_id Target to check. + */ + void updateDirectories(int target_id); + /*! \brief Updates internal state by parsing every targets state file. */ + void updateState(); + /*! \brief Updates every targets state file with the internal state. */ + void updateSettings(); + /*! + * \brief Writes the given json object to disk. + * \param path Path to write to. + * \param settings The json object. + */ + void writeSettings(const std::filesystem::path& path, const Json::Value& settings) const; + /*! + * \brief Reads the given file and creates a json object from the files data. + * \param path File to read. + * \return The json object created from the file. + */ + Json::Value readSettings(const std::filesystem::path& path) const; + /*! + * \brief Returns the path to the file which contains state data for the given file + * or directory. + * \param path File or directory for which to generate the path. + * \return The path. + */ + std::filesystem::path getConfigPath(const std::filesystem::path& path) const; + /*! + * \brief Returns the path to the given backup for the given file or directory. + * \param path Path to a backup target. + * \param backup Backup id for the given target. + * \return The path. + */ + std::filesystem::path getBackupPath(const std::filesystem::path& path, int backup) const; + /*! + * \brief Returns the path to the given existing backup for the given target. + * \param path target Target for which to find the path. + * \param backup Backup id for the given target. + * \return The path. + */ + std::filesystem::path getBackupPath(int target, int backup) const; +}; diff --git a/src/core/backuptarget.cpp b/src/core/backuptarget.cpp new file mode 100644 index 0000000..b504eba --- /dev/null +++ b/src/core/backuptarget.cpp @@ -0,0 +1,21 @@ +#include "backuptarget.h" + +BackupTarget::BackupTarget(const std::filesystem::path& path, + const std::string& target_name, + const std::vector& backup_names, + const std::vector& active_members) : + path(path), target_name(target_name), backup_names(backup_names), active_members(active_members) +{} + +bool BackupTarget::operator==(const BackupTarget& other) const +{ + for(int i = 0; i < backup_names.size(); i++) + if(backup_names[i] != other.backup_names[i]) + return false; + for(int i = 0; i < active_members.size(); i++) + if(active_members[i] != other.active_members[i]) + return false; + return path == other.path && target_name == other.target_name && + backup_names.size() == other.backup_names.size() && + active_members.size() == other.active_members.size(); +} diff --git a/src/core/backuptarget.h b/src/core/backuptarget.h new file mode 100644 index 0000000..7ee909e --- /dev/null +++ b/src/core/backuptarget.h @@ -0,0 +1,46 @@ +/*! + * \file backuptarget.h + * \brief Header for the BackupTarget struct. + */ + +#pragma once + +#include +#include + + +/*! + * \brief Stores information about a backup target. + */ +struct BackupTarget +{ + /*! \brief Path to the target file or directory. */ + std::filesystem::path path; + /*! \brief Display name for this backup target. */ + std::string target_name; + /*! \brief Contains display names for all backups for this target. */ + std::vector backup_names; + /*! \brief Contains the currently active backup for every profile. */ + std::vector active_members; + /*! \brief Active member for current profile. */ + int cur_active_member = 0; + + /*! + * \brief Constructor. + * \param path Path to the target file or directory. + * \param target_name Display name for this backup target. + * \param backup_names Contains display names for all backups for this target. + * \param active_members Contains the currently active backup for every profile. + */ + BackupTarget(const std::filesystem::path& path, + const std::string& target_name, + const std::vector& backup_names, + const std::vector& active_members); + + /*! + * \brief Tests every member of this and other for equality. + * \param other BackupTarget to compare this to. + * \return True only if every member of this is equal to the respective member in other. + */ + bool operator==(const BackupTarget& other) const; +}; diff --git a/src/core/casematchingdeployer.cpp b/src/core/casematchingdeployer.cpp new file mode 100644 index 0000000..9f461bb --- /dev/null +++ b/src/core/casematchingdeployer.cpp @@ -0,0 +1,145 @@ +#include "casematchingdeployer.h" +#include "pathutils.h" +#include +#include + +namespace sfs = std::filesystem; +namespace pu = path_utils; + + +CaseMatchingDeployer::CaseMatchingDeployer(const sfs::path& source_path, + const sfs::path& dest_path, + const std::string& name, + bool use_copy_deployment) : + Deployer(source_path, dest_path, name, use_copy_deployment) +{ + type_ = "Case Matching Deployer"; +} + +std::map CaseMatchingDeployer::deploy( + const std::vector& loadorder, + std::optional progress_node) +{ + if(progress_node) + (*progress_node)->addChildren({ 2, 1, 3 }); + adaptLoadorderFiles(loadorder, + progress_node ? &(*progress_node)->child(0) : std::optional{}); + updateConflictGroups(progress_node ? &(*progress_node)->child(1) : std::optional{}); + return Deployer::deploy( + loadorder, progress_node ? &(*progress_node)->child(2) : std::optional{}); +} + +void CaseMatchingDeployer::adaptDirectoryFiles(const sfs::path& path, + int mod_id, + const sfs::path& target_path) const +{ + std::vector directories; + for(auto const& dir_entry : sfs::directory_iterator(source_path_ / std::to_string(mod_id) / path)) + { + const std::string relative_path = + pu::getRelativePath(dir_entry.path(), source_path_ / std::to_string(mod_id)); + if(sfs::exists(target_path / relative_path)) + { + if(sfs::is_directory(target_path / relative_path)) + directories.push_back(relative_path); + continue; + } + std::string file_name = std::prev(dir_entry.path().end())->string(); + int num_matches = 0; + std::string match_file_name = file_name; + if(!sfs::exists(target_path / path)) + continue; + for(const auto& dest_entry : sfs::directory_iterator(target_path / path)) + { + std::string dest_file_name = std::prev(dest_entry.path().end())->string(); + if(!std::equal(file_name.begin(), + file_name.end(), + dest_file_name.begin(), + dest_file_name.end(), + [](char a, char b) { return std::tolower(a) == std::tolower(b); })) + continue; + num_matches++; + match_file_name = dest_file_name; + if(num_matches > 1) + break; + } + if(num_matches == 1) + { + const auto source = source_path_ / std::to_string(mod_id) / path / file_name; + const auto target = source_path_ / std::to_string(mod_id) / path / match_file_name; + if(!sfs::exists(target)) + sfs::rename(source, target); + else if(sfs::is_directory(target)) + pu::moveFilesToDirectory(source, target); + else + throw std::runtime_error(std::format("Could not rename file '{}' to '{}' " + "because the target already exists", + source.string(), + target.string())); + } + if(sfs::is_directory(source_path_ / std::to_string(mod_id) / path / match_file_name)) + directories.push_back(path / match_file_name); + } + for(const auto& dir : directories) + adaptDirectoryFiles(dir, mod_id, target_path); +} + +void CaseMatchingDeployer::adaptLoadorderFiles(const std::vector& loadorder, + std::optional progress_node) const +{ + log_(Log::LOG_INFO, std::format("Deployer '{}': Matching file names...", name_)); + if(progress_node) + { + (*progress_node)->addChildren({ 2, 1 }); + (*progress_node)->child(0).setTotalSteps(loadorder.size()); + (*progress_node)->child(1).setTotalSteps(loadorder.size()); + } + for(int mod_id : loadorder) + { + if(checkModPathExistsAndMaybeLogError(mod_id)) + adaptDirectoryFiles("", mod_id, dest_path_); + if(progress_node) + (*progress_node)->child(0).advance(); + } + + std::map file_name_map; + for(int mod_id : loadorder) + { + const sfs::path mod_path = source_path_ / std::to_string(mod_id); + std::vector mod_paths; + for(const auto& dir_entry : sfs::recursive_directory_iterator(mod_path)) + mod_paths.push_back(dir_entry.path()); + std::sort(mod_paths.begin(), + mod_paths.end(), + [](const std::string& a, const std::string& b) { return a.size() > b.size(); }); + for(const auto& path : mod_paths) + { + const std::string relative_path = pu::getRelativePath(path, mod_path); + const std::string file_name = std::prev(sfs::path(relative_path).end())->string(); + std::string lower_case_path = pu::toLowerCase(relative_path); + if(file_name_map.contains(lower_case_path)) + { + const sfs::path target_file_name = + std::prev(sfs::path(file_name_map[lower_case_path]).end())->string(); + if(file_name == target_file_name) + continue; + const sfs::path source = mod_path / relative_path; + const sfs::path target = + mod_path / sfs::path(relative_path).parent_path() / target_file_name; + if(!sfs::exists(target)) + sfs::rename(source, target); + else if(sfs::is_directory(target)) + pu::moveFilesToDirectory(source, target); + else + throw std::runtime_error(std::format("Could not rename file '{}' to '{}' " + "because the target already exists", + source.string(), + target.string())); + } + else + file_name_map[lower_case_path] = relative_path; + } + if(progress_node) + (*progress_node)->child(1).advance(); + } +} diff --git a/src/core/casematchingdeployer.h b/src/core/casematchingdeployer.h new file mode 100644 index 0000000..c91c50d --- /dev/null +++ b/src/core/casematchingdeployer.h @@ -0,0 +1,61 @@ +/*! + * \file casematchingdeployer.h + * \brief Header for the CaseMatchingDeployer class + */ + +#pragma once + +#include "deployer.h" + +/*! + * \brief Automatically renames mod files to match the case of target files. + */ +class CaseMatchingDeployer : public Deployer +{ +public: + /*! + * \brief Passes arguments to base class constructor. + * \param source_path Path to directory containing mods installed using the Installer class. + * \param dest_path Path to target directory for mod deployment. + * \param name A custom name for this instance. + * \param use_copy_deployment If True: copy files during deployment, else use hard links. + */ + CaseMatchingDeployer(const std::filesystem::path& source_path, + const std::filesystem::path& dest_path, + const std::string& name, + bool use_copy_deployment = false); + /*! + * \brief Iterates over every file and directory contained in the mods in the given load order. + * If any name case insensitively matches the name of a file in the target directory, the source + * is renamed to be identical to the target. Then calls + * \ref Deployer.deploy() "Deployer::deploy(loadorder)". + * \param loadorder A vector of mod ids representing the load order. + * \param progress_node Used to inform about the current progress of deployment. + * \return A map from deployed mod ids to their respective mods total size on disk. + */ + virtual std::map deploy( + const std::vector& loadorder, + std::optional progress_node = {}) override; + /*! \brief Use base class implementation of overloaded function. */ + using Deployer::deploy; + +private: + /*! + * \brief Recursively renames every file in source_path_/mod_id/path to the name of a file + * in dest_path_, if both match case insensitively. + * \param path Path relative to the mods root directory. + * \param mod_id Id of the mod containing the source files. + * \param target_path Path used for file comparisons. + */ + void adaptDirectoryFiles(const std::filesystem::path& path, + int mod_id, + const std::filesystem::path& target_path) const; + /*! + * \brief Renames every file in every mod in the given load order + * such that all paths are case invariant and match the case of files in \ref dest_path_. + * \param loadorder Contains ids of mods the files of which will be adapted. + * \param progress_node Used to inform about the current progress of deployment. + */ + void adaptLoadorderFiles(const std::vector& loadorder, + std::optional progress_node = {}) const; +}; diff --git a/src/core/compressionerror.h b/src/core/compressionerror.h new file mode 100644 index 0000000..45c7666 --- /dev/null +++ b/src/core/compressionerror.h @@ -0,0 +1,25 @@ +/*! + * \file compressionerror.h + * \brief Contains the CompressionError class + */ +#pragma once + + +/*! + * \brief Exception used for errors during archive extraction. + */ +#include +class CompressionError : public std::runtime_error +{ +public: + /*! + * \brief Constructor accepts an error message. + * \param message Exception message. + */ + CompressionError(const char* message) : std::runtime_error(message){}; + /*! + * \brief Returns the message of this exception. + * \return The message. + */ + const char* what() const throw() { return std::runtime_error::what(); }; +}; diff --git a/src/core/conflictinfo.h b/src/core/conflictinfo.h new file mode 100644 index 0000000..feae8f9 --- /dev/null +++ b/src/core/conflictinfo.h @@ -0,0 +1,31 @@ +/*! + * \file conflictinfo.h + * \brief Contains the ConflictInfo struct. + */ + +#pragma once + +#include + + +/*! + * \brief Stores information about a file conflict. + */ +struct ConflictInfo +{ + /*! \brief Name of the conflicting file. */ + std::string file; + /*! \brief Id of the conflicts winning mod. */ + int mod_id; + /*! \brief Name of the conflicts winning mod. */ + std::string mod_name; + /*! + * \brief Constructor. Simply initializes members. + * \param file Name of the conflicting file. + * \param mod_id Id of the conflicts winning mod. + * \param mod_name Name of the conflicts winning mod. + */ + ConflictInfo(std::string file, int mod_id, std::string mod_name) : + file(std::move(file)), mod_id(mod_id), mod_name(std::move(mod_name)) + {} +}; diff --git a/src/core/cryptography.cpp b/src/core/cryptography.cpp new file mode 100644 index 0000000..e842006 --- /dev/null +++ b/src/core/cryptography.cpp @@ -0,0 +1,121 @@ +#include "cryptography.h" +#include +#include +#include +#include +#include + + +void throwError(const std::string& step) +{ + std::string error = "Error during " + step + ".\n"; + auto code = ERR_get_error(); + char buffer[256]; + while(code) + { + ERR_error_string(code, buffer); + error.append(std::string(buffer)); + error.append("\n"); + code = ERR_get_error(); + } + ERR_free_strings(); + throw CryptographyError(error); +} + +namespace cryptography +{ +std::tuple encrypt(const std::string& plain_text, + const std::string& key) +{ + auto ctx = EVP_CIPHER_CTX_new(); + if(!ctx) + throwError("encryption"); + + if(EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) != 1) + throwError("encryption"); + + constexpr int nonce_size = 12; + unsigned char nonce[nonce_size]; + if(RAND_bytes(nonce, nonce_size) != 1) + throwError("encryption"); + + std::string actual_key = key.empty() ? default_key : key; + constexpr int key_size = 32; + unsigned char key_padded[key_size]; + for(int i = 0; i < key_size; i++) + key_padded[i] = actual_key[i % actual_key.size()]; + if(EVP_EncryptInit_ex(ctx, NULL, NULL, key_padded, nonce) != 1) + throwError("encryption"); + + const int buffer_size = exp2((int)(log(plain_text.size() + 16) / log(2)) + 1); + unsigned char cipher_text[buffer_size]; + int cur_length = 0; + unsigned char plain_array[plain_text.size()]; + for(int i = 0; i < plain_text.size(); i++) + plain_array[i] = plain_text[i]; + if(EVP_EncryptUpdate(ctx, cipher_text, &cur_length, plain_array, plain_text.size()) != 1) + throwError("encryption"); + + int cipher_length = cur_length; + if(EVP_EncryptFinal_ex(ctx, cipher_text + cur_length, &cur_length) != 1) + throwError("encryption"); + cipher_length += cur_length; + + unsigned char tag[16]; + if(EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, tag) != 1) + throwError("encryption"); + + EVP_CIPHER_CTX_free(ctx); + + const std::string cipher_str(reinterpret_cast(cipher_text), cipher_length); + const std::string nonce_str(reinterpret_cast(nonce), nonce_size); + const std::string tag_str(reinterpret_cast(tag), 16); + + return { cipher_str, nonce_str, tag_str }; +} + +std::string decrypt(const std::string& cipher_text, + const std::string& key, + const std::string& nonce, + const std::string& tag) +{ + auto ctx = EVP_CIPHER_CTX_new(); + if(!ctx) + throwError("decryption"); + + if(EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) != 1) + throwError("decryption"); + + std::string actual_key = key.empty() ? default_key : key; + constexpr int key_size = 32; + unsigned char key_arr[key_size]; + for(int i = 0; i < key_size; i++) + key_arr[i] = actual_key[i % actual_key.size()]; + unsigned char nonce_arr[nonce.size()]; + for(int i = 0; i < nonce.size(); i++) + nonce_arr[i] = nonce[i]; + if(EVP_DecryptInit_ex(ctx, NULL, NULL, key_arr, nonce_arr) != 1) + throwError("decryption"); + + unsigned char cipher_arr[cipher_text.size()]; + for(int i = 0; i < cipher_text.size(); i++) + cipher_arr[i] = cipher_text[i]; + unsigned char plain_text[(int)exp2((int)(log(cipher_text.size()) / log(2)) + 1)]; + int cur_length = 0; + if(EVP_DecryptUpdate(ctx, plain_text, &cur_length, cipher_arr, cipher_text.size()) != 1) + throwError("decryption"); + int plain_text_length = cur_length; + + unsigned char tag_arr[tag.size()]; + for(int i = 0; i < tag.size(); i++) + tag_arr[i] = tag[i]; + if(EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, tag_arr) != 1) + throwError("decryption"); + + if(EVP_DecryptFinal_ex(ctx, plain_text + cur_length, &cur_length) <= 0) + throwError("decryption"); + plain_text_length += cur_length; + + return std::string(reinterpret_cast(plain_text), plain_text_length); +} +} diff --git a/src/core/cryptography.h b/src/core/cryptography.h new file mode 100644 index 0000000..ec31229 --- /dev/null +++ b/src/core/cryptography.h @@ -0,0 +1,58 @@ +/*! + * \file cryptography.h + * \brief Header for the cryptography namespace. + */ + +#pragma once + +#include +#include + + +/*! + * \brief Exception indicating an error during a cryptographic operation. + */ +class CryptographyError : public std::runtime_error +{ +public: + /*! + * \brief Constructor. + * \param message Message for the exception. + */ + CryptographyError(const char* message) : std::runtime_error(message) {} + /*! + * \brief Constructor. + * \param message Message for the exception. + */ + CryptographyError(const std::string& message) : std::runtime_error(message) {} +}; + + +namespace cryptography +{ +/*! + * \brief Encrypts the given string using AES-GCM with the given key. + * \param plain_text Text to be encrapted. + * \param key Key to use for encryption. + * \return The cipher text, the random nonce(IV) used, the authentication tag. + * \throws CryptographyError When an OpenSSL internal error occurs. + */ +std::tuple encrypt(const std::string& plain_text, + const std::string& key); +/*! + * \brief Decrypts the given cipher text using AES-GCM. + * \param cipher_text Text to be decrypted. + * \param key Key used for decryption. + * \param nonce Nonce (IV) used during enryption. + * \param tag Authentication tag. + * \return The plain text. + * \throws CryptographyError When an OpenSSL internal error occurs. + */ +std::string decrypt(const std::string& cipher_text, + const std::string& key, + const std::string& nonce, + const std::string& tag); + +/*! \brief A default encryption key used in case no key was specified. */ +constexpr char default_key[] = "rWnYJVdtxz8Iu62GSJy0OPlOat7imMb8"; +}; diff --git a/src/core/deployer.cpp b/src/core/deployer.cpp new file mode 100644 index 0000000..42d338b --- /dev/null +++ b/src/core/deployer.cpp @@ -0,0 +1,696 @@ +#include "deployer.h" +#include "pathutils.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace str = std::ranges; +namespace sfs = std::filesystem; +namespace pu = path_utils; + + +Deployer::Deployer(const sfs::path& source_path, + const sfs::path& dest_path, + const std::string& name, + bool use_copy_deployment) : + source_path_(source_path), dest_path_(dest_path), name_(name), + use_copy_deployment_(use_copy_deployment) +{} + +std::string Deployer::getDestPath() const +{ + return dest_path_; +} + +std::string Deployer::getName() const +{ + return name_; +} + +void Deployer::setName(const std::string& name) +{ + name_ = name; +} + +std::map Deployer::deploy(const std::vector& loadorder, + std::optional progress_node) +{ + auto [source_files, mod_sizes] = getDeploymentSourceFilesAndModSizes(loadorder); + log_(Log::LOG_INFO, + std::format("Deployer '{}': Deploying {} files for {} mods...", + name_, + source_files.size(), + loadorder.size())); + if(progress_node) + (*progress_node)->addChildren({ 2, 5, 1 }); + std::map dest_files = + loadDeployedFiles(progress_node ? &(*progress_node)->child(0) : std::optional{}); + backupOrRestoreFiles(source_files, dest_files); + deployFiles(source_files, + progress_node ? &(*progress_node)->child(1) : std::optional{}); + saveDeployedFiles(source_files, + progress_node ? &(*progress_node)->child(2) : std::optional{}); + return mod_sizes; +} + +std::map Deployer::deploy(std::optional progress_node) +{ + std::vector loadorder; + for(auto const& [id, enabled] : loadorders_[current_profile_]) + { + if(enabled) + loadorder.push_back(id); + } + return deploy(loadorder, progress_node); +} + +void Deployer::setLoadorder(const std::vector>& loadorder) +{ + loadorders_[current_profile_] = loadorder; +} + +std::vector> Deployer::getLoadorder() const +{ + if(loadorders_.empty() || current_profile_ < 0 || current_profile_ >= loadorders_.size() || + loadorders_[current_profile_].empty()) + return std::vector>{}; + return loadorders_[current_profile_]; +} + +std::string Deployer::getType() const +{ + return type_; +} + +void Deployer::changeLoadorder(int from_index, int to_index) +{ + if(to_index == from_index) + return; + if(to_index < 0 || to_index >= loadorders_[current_profile_].size()) + return; + if(to_index < from_index) + { + std::rotate(loadorders_[current_profile_].begin() + to_index, + loadorders_[current_profile_].begin() + from_index, + loadorders_[current_profile_].begin() + from_index + 1); + } + else + { + std::rotate(loadorders_[current_profile_].begin() + from_index, + loadorders_[current_profile_].begin() + from_index + 1, + loadorders_[current_profile_].begin() + to_index + 1); + } +} + +bool Deployer::addMod(int mod_id, bool enabled, bool update_conflicts) +{ + if(hasMod(mod_id)) + return false; + loadorders_[current_profile_].emplace_back(mod_id, enabled); + if(update_conflicts && auto_update_conflict_groups_) + updateConflictGroups(); + return true; +} + +bool Deployer::removeMod(int mod_id) +{ + auto iter = std::find_if(loadorders_[current_profile_].begin(), + loadorders_[current_profile_].end(), + [mod_id](auto elem) { return std::get<0>(elem) == mod_id; }); + if(iter == loadorders_[current_profile_].end()) + return false; + loadorders_[current_profile_].erase(iter); + if(auto_update_conflict_groups_) + updateConflictGroups(); + return true; +} + +void Deployer::setModStatus(int mod_id, bool status) +{ + auto iter = std::find_if(loadorders_[current_profile_].begin(), + loadorders_[current_profile_].end(), + [mod_id, status](const auto& t) { return std::get<0>(t) == mod_id; }); + std::get<1>(*iter) = status; + return; +} + +bool Deployer::hasMod(int mod_id) const +{ + return std::find_if(loadorders_[current_profile_].begin(), + loadorders_[current_profile_].end(), + [mod_id](const auto& tuple) { return std::get<0>(tuple) == mod_id; }) != + loadorders_[current_profile_].end(); +} + +std::vector Deployer::getFileConflicts( + int mod_id, + bool show_disabled, + std::optional progress_node) const +{ + std::vector conflicts; + std::unordered_set unique_files; + std::unordered_set mod_files = getModFiles(mod_id, false); + if(!checkModPathExistsAndMaybeLogError(mod_id)) + return conflicts; + sfs::path mod_base_path = source_path_ / std::to_string(mod_id); + std::vector loadorder; + for(auto const& [id, enabled] : loadorders_[current_profile_]) + { + if(enabled || show_disabled) + loadorder.push_back(id); + } + + if(progress_node) + (*progress_node)->setTotalSteps(loadorder.size()); + bool mod_found = false; + for(int cur_id : loadorder) + { + if(cur_id == mod_id) + { + mod_found = true; + continue; + } + if(!checkModPathExistsAndMaybeLogError(cur_id)) + continue; + mod_base_path = source_path_ / std::to_string(cur_id); + for(const auto& dir_entry : sfs::recursive_directory_iterator(mod_base_path)) + { + const auto relative_path = pu::getRelativePath(dir_entry.path(), mod_base_path); + if(mod_files.contains(relative_path) && !unique_files.contains(relative_path)) + { + unique_files.insert(relative_path); + if(mod_found) + conflicts.emplace_back(relative_path, cur_id, ""); + else + conflicts.emplace_back(relative_path, mod_id, ""); + } + } + if(progress_node) + (*progress_node)->advance(); + } + return conflicts; +} + +int Deployer::getNumMods() const +{ + return loadorders_[current_profile_].size(); +} + +const std::filesystem::path& Deployer::destPath() const +{ + return dest_path_; +} + +void Deployer::setDestPath(const sfs::path& path) +{ + dest_path_ = path; +} + +std::unordered_set Deployer::getModConflicts(int mod_id, + std::optional progress_node) +{ + std::unordered_set conflicts{ mod_id }; + std::unordered_set mod_files = getModFiles(mod_id, false); + if(!checkModPathExistsAndMaybeLogError(mod_id)) + return conflicts; + sfs::path mod_base_path = source_path_ / std::to_string(mod_id); + if(progress_node) + (*progress_node)->setTotalSteps(loadorders_[current_profile_].size()); + for(const auto [cur_id, _] : loadorders_[current_profile_]) + { + if(!checkModPathExistsAndMaybeLogError(cur_id)) + continue; + mod_base_path = source_path_ / std::to_string(cur_id); + for(const auto& dir_entry : sfs::recursive_directory_iterator(mod_base_path)) + { + const auto relative_path = pu::getRelativePath(dir_entry.path(), mod_base_path); + if(mod_files.contains(relative_path)) + { + conflicts.insert(cur_id); + break; + } + } + if(progress_node) + (*progress_node)->advance(); + } + return conflicts; +} + +void Deployer::addProfile(int source) +{ + if(source < 0 || source >= loadorders_.size()) + { + loadorders_.push_back(std::vector>{}); + conflict_groups_.push_back(std::vector>{}); + } + else + { + loadorders_.push_back(loadorders_[source]); + conflict_groups_.push_back(conflict_groups_[source]); + } +} + +void Deployer::removeProfile(int profile) +{ + loadorders_.erase(loadorders_.begin() + profile); + conflict_groups_.erase(conflict_groups_.begin() + profile); + if(profile == current_profile_) + setProfile(0); +} + +void Deployer::setProfile(int profile) +{ + current_profile_ = profile; +} + +int Deployer::getProfile() const +{ + return current_profile_; +} + +int Deployer::verifyDirectories() +{ + std::string file_name = "_lmm_write_test_file_"; + try + { + std::ofstream file(source_path_ / file_name); + if(file.is_open()) + file << "test"; + } + catch(const std::ios_base::failure& f) + { + return 1; + } + try + { + if(sfs::exists(dest_path_ / file_name)) + sfs::remove(dest_path_ / file_name); + if(use_copy_deployment_) + sfs::copy_file(source_path_ / file_name, dest_path_ / file_name); + else + sfs::create_hard_link(source_path_ / file_name, dest_path_ / file_name); + } + catch(sfs::filesystem_error& e) + { + sfs::remove(source_path_ / file_name); + if(use_copy_deployment_) + return 3; + else + return 2; + } + sfs::remove(source_path_ / file_name); + sfs::remove(dest_path_ / file_name); + return 0; +} + +bool Deployer::swapMod(int old_id, int new_id) +{ + auto iter = std::find_if(loadorders_[current_profile_].begin(), + loadorders_[current_profile_].end(), + [old_id](auto elem) { return std::get<0>(elem) == old_id; }); + if(iter == loadorders_[current_profile_].end() || std::get<0>(*iter) == new_id) + return false; + *iter = { new_id, std::get<1>(*iter) }; + if(auto_update_conflict_groups_) + updateConflictGroups(); + return true; +} + +void Deployer::sortModsByConflicts(std::optional progress_node) +{ + updateConflictGroups(progress_node); + std::vector> new_loadorder; + new_loadorder.reserve(loadorders_[current_profile_].size()); + int i = 0; + for(const auto& group : conflict_groups_[current_profile_]) + { + for(int mod_id : group) + { + auto entry = str::find_if(loadorders_[current_profile_], + [mod_id](auto t) { return std::get<0>(t) == mod_id; }); + new_loadorder.emplace_back(mod_id, std::get<1>(*entry)); + } + i++; + } + loadorders_[current_profile_] = new_loadorder; +} + +std::vector> Deployer::getConflictGroups() const +{ + return conflict_groups_[current_profile_]; +} + +void Deployer::setConflictGroups(const std::vector>& newConflict_groups) +{ + conflict_groups_[current_profile_] = newConflict_groups; +} + +bool Deployer::usesCopyDeployment() const +{ + return use_copy_deployment_; +} + +void Deployer::setUseCopyDeployment(bool new_use_copy_deployment) +{ + use_copy_deployment_ = new_use_copy_deployment; +} + +bool Deployer::isAutonomous() +{ + return is_autonomous_; +} + +std::vector Deployer::getModNames() const +{ + return {}; +} + +std::filesystem::path Deployer::sourcePath() const +{ + return source_path_; +} + +void Deployer::setSourcePath(const sfs::path& newSourcePath) +{ + source_path_ = newSourcePath; +} + +std::pair, std::map> +Deployer::getDeploymentSourceFilesAndModSizes(const std::vector& loadorder) const +{ + std::map source_files{}; + std::map mod_sizes{}; + for(int i = loadorder.size() - 1; i >= 0; i--) + { + if(!checkModPathExistsAndMaybeLogError(loadorder[i])) + continue; + sfs::path mod_base_path = source_path_ / std::to_string(loadorder[i]); + unsigned long mod_size = 0; + for(auto const& dir_entry : sfs::recursive_directory_iterator(mod_base_path)) + { + const bool is_regular_file = dir_entry.is_regular_file(); + if(is_regular_file) + mod_size += dir_entry.file_size(); + if(is_regular_file || dir_entry.is_directory()) + source_files.insert({ pu::getRelativePath(dir_entry.path(), mod_base_path), loadorder[i] }); + } + mod_sizes[loadorder[i]] = mod_size; + } + return { source_files, mod_sizes }; +} + +void Deployer::backupOrRestoreFiles(const std::map& source_files, + const std::map& dest_files) const +{ + std::map restore_targets; + std::map backup_targets; + std::set_difference(dest_files.begin(), + dest_files.end(), + source_files.begin(), + source_files.end(), + std::inserter(restore_targets, restore_targets.begin()), + dest_files.value_comp()); + std::set_difference(source_files.begin(), + source_files.end(), + dest_files.begin(), + dest_files.end(), + std::inserter(backup_targets, backup_targets.begin()), + source_files.value_comp()); + + std::map restore_directories; + for(const auto& [path, id] : restore_targets) + { + sfs::path absolute_path = dest_path_ / path; + if(!sfs::exists(absolute_path)) + continue; + if(sfs::is_directory(absolute_path)) + { + restore_directories[path] = id; + continue; + } + sfs::path backup_name = absolute_path.string() + backup_extension_; + sfs::remove(absolute_path); + if(sfs::exists(backup_name)) + sfs::rename(backup_name, absolute_path); + } + for(const auto& [path, id] : restore_directories) + { + sfs::path absolute_path = dest_path_ / path; + if(pu::directoryIsEmpty(absolute_path)) + sfs::remove_all(absolute_path); + } + + for(const auto& [path, id] : backup_targets) + { + sfs::path absolute_path = dest_path_ / path; + sfs::path backup_name = absolute_path.string() + backup_extension_; + if(sfs::exists(absolute_path) && !sfs::is_directory(absolute_path)) + sfs::rename(absolute_path, backup_name); + } +} + +void Deployer::deployFiles(const std::map& source_files, + std::optional progress_node) const +{ + if(progress_node) + (*progress_node)->setTotalSteps(source_files.size()); + for(const auto& [path, id] : source_files) + { + sfs::path dest_path = dest_path_ / path; + if(!checkModPathExistsAndMaybeLogError(id)) + continue; + sfs::path source_path = source_path_ / std::to_string(id) / path; + if(sfs::is_directory(source_path) || + sfs::exists(dest_path) && sfs::equivalent(source_path, dest_path)) + { + if(progress_node) + (*progress_node)->advance(); + continue; + } + sfs::create_directories(dest_path.parent_path()); + sfs::remove(dest_path); + if(use_copy_deployment_) + sfs::copy_file(source_path, dest_path); + else + sfs::create_hard_link(source_path, dest_path); + if(progress_node) + (*progress_node)->advance(); + } +} + +std::map Deployer::loadDeployedFiles( + std::optional progress_node) const +{ + if(progress_node) + { + (*progress_node)->addChildren({ 1, 2 }); + (*progress_node)->child(0).setTotalSteps(1); + } + std::map deployed_files; + sfs::path deployed_files_path = dest_path_ / deployed_files_name_; + if(!sfs::exists(deployed_files_path)) + return deployed_files; + std::ifstream file(deployed_files_path, std::fstream::binary); + if(!file.is_open()) + throw std::runtime_error("Could not read \"" + deployed_files_path.string() + "\""); + Json::Value json_object; + file >> json_object; + if(progress_node) + { + (*progress_node)->child(0).advance(); + (*progress_node)->child(1).setTotalSteps(json_object["files"].size()); + } + for(int i = 0; i < json_object["files"].size(); i++) + { + deployed_files[json_object["files"][i]["path"].asString()] = + json_object["files"][i]["mod_id"].asInt(); + if(progress_node) + (*progress_node)->child(1).advance(); + } + return deployed_files; +} + +void Deployer::saveDeployedFiles(const std::map& deployed_files, + std::optional progress_node) const +{ + if(progress_node) + { + (*progress_node)->addChildren({ 1, 1 }); + (*progress_node)->child(0).setTotalSteps(deployed_files.size()); + (*progress_node)->child(1).setTotalSteps(1); + } + sfs::path deployed_files_path = dest_path_ / deployed_files_name_; + std::ofstream file(deployed_files_path, std::fstream::binary); + if(!file.is_open()) + throw std::runtime_error("Could not write \"" + deployed_files_path.string() + "\""); + Json::Value json_object; + int i = 0; + for(auto const& [path, id] : deployed_files) + { + json_object["files"][i]["path"] = path.c_str(); + json_object["files"][i]["mod_id"] = id; + i++; + if(progress_node) + (*progress_node)->child(0).advance(); + } + file << json_object; + file.close(); + if(progress_node) + (*progress_node)->child(1).advance(); +} + +std::unordered_set Deployer::getModFiles(int mod_id, bool include_directories) const +{ + std::unordered_set mod_files; + if(!checkModPathExistsAndMaybeLogError(mod_id)) + return mod_files; + sfs::path mod_base_path = source_path_ / std::to_string(mod_id); + for(const auto& dir_entry : sfs::recursive_directory_iterator(mod_base_path)) + { + if(!dir_entry.is_directory() || include_directories) + mod_files.insert(pu::getRelativePath(dir_entry.path(), mod_base_path)); + } + return mod_files; +} + +bool Deployer::checkModPathExistsAndMaybeLogError(int mod_id) const +{ + if(sfs::exists(source_path_ / std::to_string(mod_id))) + return true; + + log_(Log::LOG_ERROR, std::format("No installation directory exists for mod with id {}", mod_id)); + return false; +} + +void Deployer::updateConflictGroups(std::optional progress_node) +{ + log_(Log::LOG_INFO, std::format("Deployer '{}': Updating conflict groups...", name_)); + std::map file_map; + std::vector> groups; + std::vector non_conflicting; + // create groups + if(progress_node) + (*progress_node)->setTotalSteps(loadorders_[current_profile_].size()); + for(const auto& [mod_id, _] : loadorders_[current_profile_]) + { + if(!checkModPathExistsAndMaybeLogError(mod_id)) + continue; + std::string base_path = (source_path_ / std::to_string(mod_id)).string(); + for(const auto& dir_entry : sfs::recursive_directory_iterator(base_path)) + { + if(dir_entry.is_directory()) + continue; + const auto relative_path = pu::getRelativePath(dir_entry.path(), base_path); + if(!file_map.contains(relative_path)) + file_map[relative_path] = mod_id; + else + { + int other_id = file_map[relative_path]; + auto contains_id = [other_id](const auto& s) { return str::find(s, other_id) != s.end(); }; + auto group_iter = str::find_if(groups, contains_id); + if(group_iter != groups.end()) + group_iter->insert(mod_id); + else + groups.push_back({ other_id, mod_id }); + } + } + if(progress_node) + (*progress_node)->advance(); + } + std::vector> merged_groups; + // merge groups + for(int i = 0; i < groups.size(); i++) + { + if(groups[i].empty()) + continue; + std::set new_group = groups[i]; + bool found_intersection = true; + while(found_intersection) + { + found_intersection = false; + for(int j = i + 1; j < groups.size(); j++) + { + if(groups[j].empty()) + continue; + std::vector intersection; + std::set_intersection(new_group.begin(), + new_group.end(), + groups[j].begin(), + groups[j].end(), + std::back_inserter(intersection)); + if(!intersection.empty()) + { + found_intersection = true; + new_group.merge(groups[j]); + groups[j].clear(); + } + } + } + merged_groups.push_back(std::move(new_group)); + } + std::vector> sorted_groups(merged_groups.size() + 1, std::vector()); + // sort mods + for(const auto& [mod_id, _] : loadorders_[current_profile_]) + { + bool is_in_group = false; + for(int i = 0; i < merged_groups.size(); i++) + { + if(merged_groups[i].contains(mod_id)) + { + sorted_groups[i].push_back(mod_id); + is_in_group = true; + break; + } + } + if(!is_in_group) + sorted_groups[sorted_groups.size() - 1].push_back(mod_id); + } + conflict_groups_[current_profile_] = sorted_groups; + log_(Log::LOG_INFO, std::format("Deployer '{}': Conflict groups updated", name_)); +} + +void Deployer::setLog(const std::function& newLog) +{ + log_ = newLog; +} + +void Deployer::cleanup() +{ + deploy(std::vector{}); + if(sfs::exists(dest_path_ / deployed_files_name_)) + sfs::remove(dest_path_ / deployed_files_name_); +} + +bool Deployer::autoUpdateConflictGroups() const +{ + return auto_update_conflict_groups_; +} + +void Deployer::setAutoUpdateConflictGroups(bool status) +{ + auto_update_conflict_groups_ = status; +} + +std::optional Deployer::getModStatus(int mod_id) +{ + auto iter = str::find_if(loadorders_[current_profile_], + [mod_id](auto t) { return std::get<0>(t) == mod_id; }); + if(iter == loadorders_[current_profile_].end()) + return {}; + return { std::get<1>(*iter) }; +} + +std::vector> Deployer::getAutoTags() +{ + return {}; +} + +std::map Deployer::getAutoTagMap() +{ + return {}; +} diff --git a/src/core/deployer.h b/src/core/deployer.h new file mode 100644 index 0000000..173bee7 --- /dev/null +++ b/src/core/deployer.h @@ -0,0 +1,349 @@ +/*! + * \file deployer.h + * \brief Header for the Deployer class. + */ + +#pragma once + +#include "conflictinfo.h" +#include "log.h" +#include "progressnode.h" +#include +#include +#include +#include +#include + + +/*! + * \brief Handles deployment of mods to target directory. + */ +class Deployer +{ +public: + /*! + * \brief Constructor. + * \param source_path Path to directory containing mods installed using the Installer class. + * \param dest_path Path to target directory for mod deployment. + * \param name A custom name for this instance. + * \param use_copy_deployment If True: copy files during deployment, else use hard links. + */ + Deployer(const std::filesystem::path& source_path, + const std::filesystem::path& dest_path, + const std::string& name, + bool use_copy_deployment = false); + + /*! + * \brief Getter for path to deployment target directory. + * \return The path. + */ + std::string getDestPath() const; + /*! + * \brief Getter for deployer name. + * \return The name. + */ + std::string getName() const; + /*! + * \brief Setter for deployer name. + * \param name The new name. + */ + void setName(const std::string& name); + /*! + * \brief Deploys all mods to the target directory using hard links. + * If any file already exists in the target directory, a backup for that file is created. + * Previously backed up files are automatically restored if no mod in the current load order + * overwrites them. Conflicts are handled by overwriting mods earlier in the load order + * with later mods. + * \param loadorder A vector of mod ids representing the load order. + * \param progress_node Used to inform about the current progress of deployment. + * \return A map from deployed mod ids to their respective mods total size on disk. + */ + virtual std::map deploy(const std::vector& loadorder, + std::optional progress_node = {}); + /*! + * \brief Deploys all mods to the target directory using hard links. + * If any file already exists in the target directory, a backup for that file is created. + * Previously backed up files are automatically restored if no mod in the current load order + * overwrites them. Conflicts are handled by overwriting mods earlier in the load order + * with later mods. This function uses the internal load order. + * \param progress_node Used to inform about the current progress of deployment. + * \return A map from deployed mod ids to their respective mods total size on disk. + */ + virtual std::map deploy(std::optional progress_node = {}); + /*! + * \brief Setter for the load order used for deployment. + * \param loadorder The new load order. + */ + void setLoadorder(const std::vector>& loadorder); + /*! + * \brief Getter for the current mod load order. + * \return The load order. + */ + virtual std::vector> getLoadorder() const; + /*! + * \brief Returns the type of this deployer, i.e. SIMPLEDEPLOYER + * \return The type. + */ + std::string getType() const; + /*! + * \brief Moves a mod from one position in the load order to another. + * \param from_index Index of mod to be moved. + * \param to_index Destination index. + */ + virtual void changeLoadorder(int from_index, int to_index); + /*! + * \brief Appends a new mod to the load order. + * \param mod_id Id of the mod to be added. + * \param enabled Controls if the new mod will be enabled. + * \param update_conflicts If true: Update mod conflict groups. + * \return True iff the mod has been added. + */ + virtual bool addMod(int mod_id, bool enabled = true, bool update_conflicts = true); + /*! + * \brief Removes a mod from the load order. + * \param mod_id Id of the mod to be removed. + * \return True iff the mod has been removed. + */ + virtual bool removeMod(int mod_id); + /*! + * \brief Enables or disables the given mod in the load order. + * \param mod_id Mod to be edited. + * \param status The new status. + */ + virtual void setModStatus(int mod_id, bool status); + /*! + * \brief Checks if given mod id is part of the load order. + * \param mod_id Mod to be checked. + * \return True is mod is in load order, else false. + */ + virtual bool hasMod(int mod_id) const; + /*! + * \brief Checks for file conflicts of given mod with all other mods in the load order. + * \param mod_id Mod to be checked. + * \param show_disabled If true: Also check for conflicts with disabled mods. + * \param progress_node Used to inform about the current progress. + * \return A vector with information about conflicts with every other mod. + */ + virtual std::vector getFileConflicts( + int mod_id, + bool show_disabled = false, + std::optional progress_node = {}) const; + /*! + * \brief Returns the number of mods in the load order. + * \return The number of mods. + */ + virtual int getNumMods() const; + /*! + * \brief Getter for path to deployment target directory. + * \return The path. + */ + const std::filesystem::path& destPath() const; + /*! + * \brief Setter for path to deployment target directory. + * \param newDest_path the new path. + */ + void setDestPath(const std::filesystem::path& path); + /*! + * \brief Checks for conflicts with other mods. + * Two mods are conflicting if they share at least one file. + * \param mod_id The mod to be checked. + * \param progress_node Used to inform about the current progress. + * \return A set of mod ids which conflict with the given mod. + */ + virtual std::unordered_set getModConflicts(int mod_id, + std::optional progress_node = {}); + /*! + * \brief Adds a new profile and optionally copies it's load order from an existing profile. + * \param source The profile to be copied. A value of -1 indicates no copy. + */ + virtual void addProfile(int source = -1); + /*! + * \brief Removes a profile. + * \param profile The profile to be removed. + */ + virtual void removeProfile(int profile); + /*! + * \brief Setter for the active profile. + * \param profile The new profile. + */ + virtual void setProfile(int profile); + /*! + * \brief Getter for the active profile. + * \return The profile. + */ + int getProfile() const; + /*! + * \brief Checks if writing to the deployment directory is possible. + * \return A code indicating success(0), an IO error(1) or an error during link creation(2). + */ + int verifyDirectories(); + /*! + * \brief Replaces the given id in the load order with a new id. + * \param old_id The mod to be replaced. + * \param new_id The new mod. + * \return True iff the mod has been swapped. + */ + virtual bool swapMod(int old_id, int new_id); + /*! + * \brief Sorts the load order by grouping mods which contain conflicting files. + * \param progress_node Used to inform about the current progress. + */ + virtual void sortModsByConflicts(std::optional progress_node = {}); + /*! + * \brief Getter for the conflict groups of the current profile. + * \return The conflict groups. + */ + virtual std::vector> getConflictGroups() const; + /*! + * \brief Setter for the conflict groups of the current profile. + * \param newConflict_groups The new conflict groups. + */ + virtual void setConflictGroups(const std::vector>& newConflict_groups); + /*! + * \brief Getter for use_copy_deployment_. + * \return True if this copies files during deployment, else use hard links. + */ + bool usesCopyDeployment() const; + /*! + * \brief Sets the copy deployment. + * \param newUse_copy_deployment If true: copy files during deployment, else use hard links. + */ + void setUseCopyDeployment(bool newUse_copy_deployment); + /*! \brief Getter for is_autonomous_. */ + bool isAutonomous(); + /*! + * \brief Autonomous deployers override this tho provide names for their mods. + * Non Autonomous deployers return an empty vector. + * \return The mod name vector. + */ + virtual std::vector getModNames() const; + /*! \brief Getter for mod source path. */ + std::filesystem::path sourcePath() const; + /*! + * \brief Setter for mod source path. + * \param New source path. + */ + void setSourcePath(const std::filesystem::path& newSourcePath); + /*! + * \brief Setter for log callback. + * \param newLog New log callback + */ + void setLog(const std::function& newLog); + /*! + * \brief Removes all deployed mods from the target directory and deletes the file + * which stores the state of this deployer. + */ + virtual void cleanup(); + /*! + * \brief Updates conflict_groups_ for the current profile. + * \param progress_node Used to inform about the current progress. + */ + void updateConflictGroups(std::optional progress_node = {}); + /*! \brief Getter for \ref auto_update_conflict_groups_. */ + bool autoUpdateConflictGroups() const; + /*! \brief Setter for \ref auto_update_conflict_groups_. */ + void setAutoUpdateConflictGroups(bool status); + /*! + * \brief Searches the load order for the given mod id and returns the corresponding mods + * activation status, if found. + * \param mod_id Mod to be found. + * \return The activation status, if found. + */ + std::optional getModStatus(int mod_id); + /*! + * \brief Getter for auto tags. + * Only implemented in autonomous deployers. + * \return For every mod: A vector of auto tags added to that mod. + */ + virtual std::vector> getAutoTags(); + /*! + * \brief Returns all available auto tag names mapped to the number of mods for that tag. + * Only implemented in autonomous deployers. + * \return The tag map. + */ + virtual std::map getAutoTagMap(); + +protected: + /*! \brief Type of this deployer, e.g. Simple Deployer. */ + std::string type_ = "Simple Deployer"; + /*! \brief Path to the directory containing all mods which are to be deployed. */ + std::filesystem::path source_path_; + /*! \brief Path to the directory where all mods are deployed to. */ + std::filesystem::path dest_path_; + /*! \brief The file extension appended to backed up files. */ + const std::string backup_extension_ = ".lmmbak"; + /*! \brief The file name for a file in the target directory containing names of deployed files*/ + const std::string deployed_files_name_ = ".lmmfiles"; + /*! \brief The name of this deployer. */ + std::string name_; + /*! \brief The currently active profile. */ + int current_profile_ = 0; + /*! \brief One load order per profile consisting of tuples of mod ids and their enabled status. */ + std::vector>> loadorders_; + /*! + * \brief For every profile: Groups of mods which conflict with each other. The last + * group contains mods with no conflicts. + */ + std::vector>> conflict_groups_; + /*! \brief If false: Use hard links to deploy mods, else: copy files. */ + bool use_copy_deployment_ = false; + /*! \brief Autonomous deployers manage their own mods and do not rely on ModdedApplication. */ + bool is_autonomous_ = false; + /*! \brief If true: Automatically update conflict groups when necessary. */ + bool auto_update_conflict_groups_ = false; + + /*! + * \brief Creates a pair of maps. One maps relative file paths to the mod id from which that + * file is to be deployed. The other maps mod ids to their total file size on disk. + * \param loadorder The load order used for file checks. + * \return The generated maps. + */ + std::pair, std::map> + getDeploymentSourceFilesAndModSizes(const std::vector& loadorder) const; + /*! + * \brief Backs up all files which would be overwritten during deployment and restores all + * files backed up during previous deployments files which are no longer overwritten. + * \param source_files A map of files to be deployed to their source mods. + * \param dest_files A map of files currently deployed to their source mods. + */ + void backupOrRestoreFiles(const std::map& source_files, + const std::map& dest_files) const; + /*! + * \brief Hard links all given files to target directory. + * \param source_files A map of files to be deployed to their source mods. + * \param progress_node Used to inform about the current progress of deployment. + */ + void deployFiles(const std::map& source_files, + std::optional progress_node = {}) const; + /*! + * \brief Creates a map of currently deployed files to their source mods. + * \param progress_node Used to inform about the current progress. + * \return The map. + */ + std::map loadDeployedFiles( + std::optional progress_node = {}) const; + /*! + * \brief Creates a file containing information about currently deployed files. + * \param deployed_files The currently deployed files. + * \param progress_node Used to inform about the current progress. + */ + void saveDeployedFiles(const std::map& deployed_files, + std::optional progress_node = {}) const; + /*! + * \brief Creates a set containing every file contained in one mod. Files are + * represented as paths relative to the mods root directory. + * \param mod_id Target mod. + * \param include_directories If true: Also include all directories in the mod. + * \return The set of files. + */ + std::unordered_set getModFiles(int mod_id, bool include_directories = false) const; + /*! \brief Callback for logging. */ + std::function log_ = [](Log::LogLevel a, + const std::string& b) {}; + /*! + * \brief Checks if the directory containing the given mod exists, if not logs an error. + * \param mod_id If of the mod to check. + * \return True if the directory exists, else false. + */ + bool checkModPathExistsAndMaybeLogError(int mod_id) const; +}; diff --git a/src/core/deployerfactory.cpp b/src/core/deployerfactory.cpp new file mode 100644 index 0000000..ec388d9 --- /dev/null +++ b/src/core/deployerfactory.cpp @@ -0,0 +1,21 @@ +#include "deployerfactory.h" +#include "casematchingdeployer.h" +#include "lootdeployer.h" + + +std::unique_ptr DeployerFactory::makeDeployer(const std::string& type, + const std::filesystem::path& source_path, + const std::filesystem::path& dest_path, + const std::string& name, + bool use_copy_deployment) +{ + if(type == SIMPLEDEPLOYER) + return std::make_unique(source_path, dest_path, name, use_copy_deployment); + else if(type == CASEMATCHINGDEPLOYER) + return std::make_unique( + source_path, dest_path, name, use_copy_deployment); + else if(type == LOOTDEPLOYER) + return std::make_unique(source_path, dest_path, name); + else + throw std::runtime_error("Unknown deployer type \"" + type + "\"!"); +} diff --git a/src/core/deployerfactory.h b/src/core/deployerfactory.h new file mode 100644 index 0000000..2fbc750 --- /dev/null +++ b/src/core/deployerfactory.h @@ -0,0 +1,61 @@ +#pragma once + +#include "deployer.h" + + +class DeployerFactory +{ +public: + DeployerFactory() = delete; + + /*! \brief Performs no additional actions. */ + inline static const std::string SIMPLEDEPLOYER{ "Simple Deployer" }; + /*! + * \brief Uses case insensitive string matching when comparing + * mod file names with target file names. + */ + inline static const std::string CASEMATCHINGDEPLOYER{ "Case Matching Deployer" }; + inline static const std::string LOOTDEPLOYER{ "Loot Deployer" }; + /*! + * \brief Returns a vector of available deployer types. + * \return The vector of deployer types. + */ + /*! \brief Contains all available deployer types. */ + inline static const std::vector DEPLOYER_TYPES{ CASEMATCHINGDEPLOYER, + SIMPLEDEPLOYER, + LOOTDEPLOYER }; + /*! \brief Maps deployer types to a description of what they do. */ + inline static const std::map DEPLOYER_DESCRIPTIONS{ + { SIMPLEDEPLOYER, + "Links/ copies all files from enabled mods in its loadorder into " + "target directory. Backs up and restores existing files when needed." }, + { CASEMATCHINGDEPLOYER, + "When the target directory contains a file with the same name " + "but different case as a mods file name, renames the mods name to " + "match the target file. Then deploys as normal." }, + { LOOTDEPLOYER, + "Uses LOOT to manage plugins for games like Skyrim. Source path " + "should point to the directory which plugins are installed into." + "Target path should point to the directory containing plugins.txt " + "and loadorder.txt" } + }; + /*! \brief Maps deployer types to a bool indicating + * if the type refers to an autonomous deployer. */ + inline static const std::map AUTONOMOUS_DEPLOYERS{ { SIMPLEDEPLOYER, false }, + { CASEMATCHINGDEPLOYER, + false }, + { LOOTDEPLOYER, true } }; + /*! + * \brief Constructs a unique pointer to a new deployer of given type. + * \param type Deployer type to be constructed. + * \param source_path Path to directory containing mods installed using the Installer class. + * \param dest_path Path to target directory for mod deployment. + * \param name A custom name for this instance. + * \return The constructed unique pointer. + */ + static std::unique_ptr makeDeployer(const std::string& type, + const std::filesystem::path& source_path, + const std::filesystem::path& dest_path, + const std::string& name, + bool use_copy_deployment = false); +}; diff --git a/src/core/deployerinfo.h b/src/core/deployerinfo.h new file mode 100644 index 0000000..6ed913f --- /dev/null +++ b/src/core/deployerinfo.h @@ -0,0 +1,32 @@ +/*! + * \file deployerinfo.h + * \brief Contains the DeployerInfo struct. + */ + +#pragma once + +#include +#include +#include + + +/*! + * \brief Stores a \ref Deployer "deployer's" installed mods and load order. + */ +struct DeployerInfo +{ + /*! \brief Names of the mods managed by this deployer, in their load order. */ + std::vector mod_names; + /*! \brief The \ref Deployer "deployer's" load order. */ + std::vector> loadorder; + /*! \brief Contains groups of mods which conflict with each other. */ + std::vector> conflict_groups; + /*! \brief If true: Deployer manages its own mods and does not rely on ModdedApplication. */ + bool is_autonomous = false; + /*! \brief For every mod: A vector of manual tags added to that mod. */ + std::vector> manual_tags; + /*! \brief For every mod: A vector of auto tags added to that mod. */ + std::vector> auto_tags; + /*! \brief Maps tag names to the number of mods for that tag. */ + std::map mods_per_tag; +}; diff --git a/src/core/editapplicationinfo.h b/src/core/editapplicationinfo.h new file mode 100644 index 0000000..3eb2ed3 --- /dev/null +++ b/src/core/editapplicationinfo.h @@ -0,0 +1,38 @@ +/*! + * \file editapplicationinfo.h + * \brief Contains the EditApplicationInfo struct. + */ + +#pragma once + +#include +#include + + +/*! + * \brief Stores data needed to either create a new or edit an existing + * \ref ModdedApplication "application". + */ +struct EditApplicationInfo +{ + /*! \brief New name of the application. */ + std::string name; + /*! \brief Path to the staging directory. */ + std::string staging_dir; + /*! \brief Command used to run the application. */ + std::string command; + /*! + * \brief When creating a new application, this contains names and target paths + * for initial deployers. + */ + std::vector> deployers; + /*! + * \brief When editing an application, this indicates whether to move the existing + * staging directory to the new path specified in staging_dir. + */ + bool move_staging_dir = false; + /*! \brief Path to the applications icon. */ + std::string icon_path; + /*! \brief Version of the app. This is used for FOMOD conditions. */ + std::string app_version; +}; diff --git a/src/core/editautotagaction.cpp b/src/core/editautotagaction.cpp new file mode 100644 index 0000000..13e534c --- /dev/null +++ b/src/core/editautotagaction.cpp @@ -0,0 +1,49 @@ +#include "editautotagaction.h" + +EditAutoTagAction::EditAutoTagAction(const std::string& name, ActionType type) +{ + name_ = name; + type_ = type; +} + +EditAutoTagAction::EditAutoTagAction(const std::string& name, const std::string& new_name) +{ + name_ = name; + new_name_ = new_name; + type_ = ActionType::rename; +} + +EditAutoTagAction::EditAutoTagAction(const std::string& name, + const std::string& expression, + const std::vector& conditions) +{ + name_ = name; + expression_ = expression; + conditions_ = conditions; + type_ = ActionType::change_evaluator; +} + +std::string EditAutoTagAction::getName() const +{ + return name_; +} + +std::string EditAutoTagAction::getNewName() const +{ + return new_name_; +} + +EditAutoTagAction::ActionType EditAutoTagAction::getType() const +{ + return type_; +} + +std::string EditAutoTagAction::getExpression() const +{ + return expression_; +} + +std::vector EditAutoTagAction::getConditions() const +{ + return conditions_; +} diff --git a/src/core/editautotagaction.h b/src/core/editautotagaction.h new file mode 100644 index 0000000..fcdda99 --- /dev/null +++ b/src/core/editautotagaction.h @@ -0,0 +1,92 @@ +/*! + * \file editautotagaction.h + * \brief Header for the EditAutoTagAction class. + */ + +#pragma once + +#include "tagcondition.h" +#include +#include + + +/*! + * \brief Contains data relevent for the action of editing an auto tag. + */ +class EditAutoTagAction +{ +public: + /*! \brief Represents the type of action performed. */ + enum class ActionType + { + /*! \brief Add a new tag. */ + add, + /*! \brief Remove an existing tag. */ + remove, + /*! \brief Rename a tag. */ + rename, + /*! \brief Create a new evaluator. */ + change_evaluator + }; + + /*! + * \brief Constructor for an add or remove action. + * \param name Name of the tag to be added/ removed. + * \param type Action type. + */ + EditAutoTagAction(const std::string& name, ActionType type); + /*! + * \brief Constructor for a rename action. + * \param name Name of the tag to be renamed. + * \param new_name New name for the tag. + */ + EditAutoTagAction(const std::string& name, const std::string& new_name); + /*! + * \brief Constructor for a change_evaluator action. + * \param name Name of the tag the evaluator of which is to be updated. + * \param expression New evaluator expression. + * \param conditions New evaluator conditions. + */ + EditAutoTagAction(const std::string& name, + const std::string& expression, + const std::vector& conditions); + + + /*! + * \brief Getter for the target tags name. + * \return The name. + */ + std::string getName() const; + /*! + * \brief Getter for the new name. + * \return The new name. + */ + std::string getNewName() const; + /*! + * \brief Getter for the ActionType to be performed. + * \return The ActionType. + */ + ActionType getType() const; + /*! + * \brief Getter for the expression of the updated evaluator. + * \return The expression. + */ + std::string getExpression() const; + /*! + * \brief Getter for the conditions of the updated evaluator. + * \return The conditions. + */ + std::vector getConditions() const; + +private: + /*! \brief The target tags name. */ + std::string name_; + /*! \brief The target tags new name, if ActionType == rename. */ + std::string new_name_; + /*! \brief The type of action to be performed. */ + ActionType type_; + /*! \brief Expression used to generate a new evaluator. */ + std::string expression_; + /*! \brief Conditions used to generate a new evaluator. */ + std::vector conditions_; +}; diff --git a/src/core/editdeployerinfo.h b/src/core/editdeployerinfo.h new file mode 100644 index 0000000..e1fff9d --- /dev/null +++ b/src/core/editdeployerinfo.h @@ -0,0 +1,27 @@ +/*! + * \file editdeployerinfo.h + * \brief Contains the EditDeployerInfo struct. + */ + +#pragma once + +#include + + +/*! + * \brief Stores data needed to either create a new or edit an existing + * \ref Deployer "deployer". + */ +struct EditDeployerInfo +{ + /*! \brief Type of the deployer. */ + std::string type; + /*! \brief Name of the deployer */ + std::string name; + /*! \brief This is where the deployer will deploy to. */ + std::string target_dir; + /*! \brief If true: Copy mods to target directory, else: use hard links. */ + bool use_copy_deployment; + /*! \brief The deployers mod source directory. Only used by autonomous deployers. */ + std::string source_dir = ""; +}; diff --git a/src/core/editmanualtagaction.cpp b/src/core/editmanualtagaction.cpp new file mode 100644 index 0000000..de9d9c4 --- /dev/null +++ b/src/core/editmanualtagaction.cpp @@ -0,0 +1,22 @@ +#include "editmanualtagaction.h" + +EditManualTagAction::EditManualTagAction(const std::string& name, + ActionType type, + const std::string& new_name) : + name_(name), type_(type), new_name_(new_name) +{} + +std::string EditManualTagAction::getName() const +{ + return name_; +} + +std::string EditManualTagAction::getNewName() const +{ + return new_name_; +} + +EditManualTagAction::ActionType EditManualTagAction::getType() const +{ + return type_; +} diff --git a/src/core/editmanualtagaction.h b/src/core/editmanualtagaction.h new file mode 100644 index 0000000..ae025a6 --- /dev/null +++ b/src/core/editmanualtagaction.h @@ -0,0 +1,60 @@ +/*! + * \file editmanualtagaction.h + * \brief Header for the EditManualTagAction class. + */ + +#pragma once + +#include + + +/*! + * \brief Contains data relevent for the action of editing a manual tag. + */ +class EditManualTagAction +{ +public: + /*! \brief Represents the type of action performed. */ + enum class ActionType + { + /*! \brief Add a new tag. */ + add, + /*! \brief Remove an existing tag. */ + remove, + /*! \brief Rename a tag. */ + rename + }; + + /*! + * \brief Constructor. + * \param name Name of the tag to be edited. + * \param type Type of editing action to be performed. + * \param new_name Contains the tags new name, if action is of type Rename. + */ + EditManualTagAction(const std::string& name, ActionType type, const std::string& new_name = ""); + + + /*! + * \brief Getter for the target tags name. + * \return The name. + */ + std::string getName() const; + /*! + * \brief Getter for the new name. + * \return The new name. + */ + std::string getNewName() const; + /*! + * \brief Getter for the ActionType to be performed. + * \return The ActionType. + */ + ActionType getType() const; + +private: + /*! \brief The target tags name. */ + std::string name_; + /*! \brief The target tags new name, if ActionType == rename. */ + std::string new_name_; + /*! \brief The type of action to be performed. */ + ActionType type_; +}; diff --git a/src/core/editprofileinfo.h b/src/core/editprofileinfo.h new file mode 100644 index 0000000..5665391 --- /dev/null +++ b/src/core/editprofileinfo.h @@ -0,0 +1,24 @@ +/*! + * \file editprofileinfo.h + * \brief Contains the EditProfileInfo struct. + */ + +#pragma once + +#include + + +/*! + * \brief Stores data needed to either create a new or edit an existing + * profile of a \ref ModdedApplication "application". + */ +struct EditProfileInfo +{ + /*! \brief The new name of the profile. */ + std::string name; + /*! \brief The new app version of the profile. Used for FOMOD conditions. */ + std::string app_version; + /*! \brief If a new profile is created and this is != -1: Copy all settings from source profile. + */ + int source = -1; +}; diff --git a/src/core/fomod/dependency.cpp b/src/core/fomod/dependency.cpp new file mode 100644 index 0000000..4187d3c --- /dev/null +++ b/src/core/fomod/dependency.cpp @@ -0,0 +1,139 @@ +#include "dependency.h" +#include "../pathutils.h" + +using namespace fomod; +namespace sfs = std::filesystem; +namespace pu = path_utils; + + +Dependency::Dependency(pugi::xml_node source) +{ + if(!source) + { + type_ = dummy_node; + return; + } + const std::string name(source.name()); + if(name == "dependencies" || name == "moduleDependencies") + { + type_ = source.attribute("operator").value() == std::string("Or") ? or_node : and_node; + std::map> file_dependencies; + for(auto child : source.children()) + { + if(type_ == or_node && child && std::string(child.name()) == "fileDependency") + { + const std::string target = child.attribute("file").value(); + if(!file_dependencies.contains(target)) + file_dependencies[target] = {child.attribute("state").value(), child}; + else + { + const std::string child_state = child.attribute("state").value(); + if(child_state == "Active" && file_dependencies[target].first != "Active") + file_dependencies[target] = {"Active", child}; + } + } + else + children_.emplace_back(child); + } + for(const auto& [target, pair]: file_dependencies) + children_.emplace_back(pair.second); + } + else if(name == "fileDependency") + { + type_ = file_leaf; + target_ = source.attribute("file").value(); + state_ = source.attribute("state").value(); + } + else if(name == "flagDependency") + { + type_ = flag_leaf; + target_ = source.attribute("flag").value(); + state_ = source.attribute("value").value(); + } + else if(name == "gameDependency") + { + type_ = game_version_leaf; + target_ = source.attribute("version").value(); + } + else if(name == "fommDependency") + { + type_ = fomm_version_leaf; + target_ = source.attribute("version").value(); + } +} + +Dependency::Dependency() +{ + type_ = dummy_node; +} + +bool Dependency::evaluate(const sfs::path& target_path, + const std::map& flags, + std::function eval_game_version, + std::function eval_fomm_version) const +{ + if(type_ == and_node) + { + if(children_.empty()) + return true; + for(const auto& child : children_) + { + if(!child.evaluate(target_path, flags, eval_game_version, eval_fomm_version)) + return false; + } + return true; + } + else if(type_ == or_node) + { + if(children_.empty()) + return true; + for(const auto& child : children_) + { + if(child.evaluate(target_path, flags, eval_game_version, eval_fomm_version)) + return true; + } + return false; + } + else if(type_ == file_leaf) + { + const bool exists = pu::pathExists(target_, target_path) ? true : false; + if(state_ == "Active") + return exists; + return !exists; + } + else if(type_ == flag_leaf) + { + if(!flags.contains(target_)) + return false; + return flags.at(target_) == state_; + } + else if(type_ == game_version_leaf) + return eval_game_version(target_); + else if(type_ == fomm_version_leaf) + return eval_fomm_version(target_); + return true; +} + +std::string Dependency::toString() const +{ + if(type_ == file_leaf) + return "(File '" + target_ + "' is '" + state_ + "')"; + else if(type_ == flag_leaf) + return "(Flag '" + target_ + "' is '" + state_ + "')"; + else if(type_ == game_version_leaf) + return "(Game version == '" + target_ + "')"; + else if(type_ == fomm_version_leaf) + return "(Fomm version == '" + target_ + "')"; + else + { + std::string op = type_ == or_node ? "OR" : "AND"; + std::string chain = "( "; + for(int i = 0; i < children_.size(); i++) + { + chain += children_.at(i).toString(); + if(i < children_.size() - 1) + chain += " " + op + " "; + } + return chain + " )"; + } +} diff --git a/src/core/fomod/dependency.h b/src/core/fomod/dependency.h new file mode 100644 index 0000000..bd1011e --- /dev/null +++ b/src/core/fomod/dependency.h @@ -0,0 +1,83 @@ +/*! + * \file fomoddependency.h + * \brief Header for the FomodDependency class and FomodFile struct. + */ + +#pragma once + +#include "pugixml.hpp" +#include +#include +#include +#include +#include +#include + + +/*! + * \brief The fomod namespace contains classes used for parsing a FOMOD xml file and for + * creating an installer. + */ +namespace fomod +{ +/*! + * \brief Represents a fomod dependency tree node. + */ +class Dependency +{ + /*! \brief Represents different dependency types. */ + enum Type + { + /*! \brief Always evaluates to true. */ + dummy_node, + /*! \brief True if all children evaluate to true. */ + and_node, + /*! \brief True if at least one child evaluates to true. */ + or_node, + /*! \brief File must exist. */ + file_leaf, + /*! \brief Flag must be set. */ + flag_leaf, + /*! \brief Game version must be == some version. */ + game_version_leaf, + /*! \brief Fomm version must be == some version. */ + fomm_version_leaf + }; + + +public: + /*! + * \brief Recursively builds a dependency tree from given fomod node. + * \param source Source fomod node. + */ + Dependency(pugi::xml_node source); + /*! \brief Constructs a dummy node. */ + Dependency(); + + /*! + * \brief Checks given flags, files, game version and fomm version fulfill the condition + * represented by this tree. + * \param target_path Path to target files. + * \param flags Flags to be checked. + * \param eval_game_version Used to check if this nodes game version is valid. + * \param eval_fomm_version Used to check if this nodes fomm version is valid. + * \return True if conditions are met, else false. + */ + bool evaluate( + const std::filesystem::path& target_path, + const std::map& flags, + std::function eval_game_version, + std::function eval_fomm_version = [](auto s) { return true; }) const; + std::string toString() const; + +private: + /*! \brief Type of this dependency. */ + Type type_; + /*! \brief Value for comparison, e.g. file path for a file dependency. */ + std::string target_; + /*! \brief State of file or flag. */ + std::string state_; + /*! \brief Children of this node. */ + std::vector children_; +}; +} diff --git a/src/core/fomod/file.h b/src/core/fomod/file.h new file mode 100644 index 0000000..cd7bd48 --- /dev/null +++ b/src/core/fomod/file.h @@ -0,0 +1,49 @@ +/*! + * \file file.h + * \brief Header for the File struct. + */ + +#pragma once + +#include + + +/*! + * \brief The fomod namespace contains classes used for parsing a FOMOD xml file and for + * creating an installer. + */ +namespace fomod +{ +/*! + * \brief Holds data regarding the installation of a single file in a fomod configuration. + */ +struct File +{ + /*! \brief Source path, relative to mods root directory. */ + std::filesystem::path source; + /*! \brief Destination path, relative to target root.*/ + std::filesystem::path destination = ""; + /*! \brief If True: Always install, regardless of selection. */ + bool always_install = false; + /*! \brief If True: Always install if dependencies are fulfilled. */ + bool install_if_usable = false; + /*! \brief If two files share a destination, the higher priority file gets installed. */ + int priority = -std::numeric_limits::max(); + + /*! + * \brief Compares two File objects by their destination. + * \param other Other File. + * \return True if destinations are equal. + */ + bool operator==(const File& other) const + { + return destination.string() == other.destination.string(); + } + /*! + * \brief Compares two File objects by their priority. + * \param other Other File. + * \return True if this has lower priority. + */ + bool operator<(const File& other) const { return priority < other.priority; } +}; +} diff --git a/src/core/fomod/fomodinstaller.cpp b/src/core/fomod/fomodinstaller.cpp new file mode 100644 index 0000000..2f4e52a --- /dev/null +++ b/src/core/fomod/fomodinstaller.cpp @@ -0,0 +1,411 @@ +#include "fomodinstaller.h" +#include "../log.h" +#include "../pathutils.h" +#include +#include +#include +#include + +using namespace fomod; +namespace sfs = std::filesystem; +namespace pu = path_utils; + + +void FomodInstaller::init(const sfs::path& config_file, + const sfs::path& target_path, + const std::string& app_version) +{ + if(!app_version.empty()) + version_eval_fun_ = [app_version](std::string version) { return app_version == version; }; + cur_step_ = -1; + config_file_.reset(); + files_.clear(); + steps_.clear(); + int cur_step_ = -1; + flags_.clear(); + prev_selections_.clear(); + target_path_ = target_path; + if(sfs::is_directory(config_file)) + { + mod_base_path_ = config_file; + auto [fomod_dir_name, config_file_name] = getFomodPath(config_file); + config_file_.load_file((config_file / fomod_dir_name / config_file_name).c_str()); + } + else + { + mod_base_path_ = config_file.parent_path().parent_path(); + config_file_.load_file(config_file.c_str()); + } + config_ = config_file_.child("config"); + auto file_list = config_.child("requiredInstallFiles"); + if(file_list) + parseFileList(file_list, files_); + auto steps = config_.child("installSteps"); + if(steps) + parseInstallSteps(steps); +} + +std::optional FomodInstaller::step(const std::vector>& selection) +{ + updateState(selection); + for(int i = cur_step_ + 1; i < steps_.size(); i++) + { + if(steps_[i].dependencies.evaluate(target_path_, flags_, version_eval_fun_, fomm_eval_fun_)) + { + for(auto& group : steps_[i].groups) + { + for(auto& plugin : group.plugins) + plugin.updateType(target_path_, flags_, version_eval_fun_, fomm_eval_fun_); + } + if(cur_step_ > -1) + prev_selections_.push_back(selection); + cur_step_ = i; + return steps_[i]; + } + } + return {}; +} + +std::optional>, InstallStep>> FomodInstaller::stepBack() +{ + if(cur_step_ < 1) + return {}; + files_.clear(); + flags_.clear(); + for(auto& step : steps_) + { + for(auto& group : step.groups) + { + for(auto& plugin : group.plugins) + plugin.updateType(target_path_, flags_, version_eval_fun_, fomm_eval_fun_); + } + } + if(prev_selections_.size() == 1) + { + cur_step_ = -1; + auto old_selection = prev_selections_[0]; + prev_selections_.clear(); + return { { old_selection, *step() } }; + } + cur_step_ = -1; + auto old_selections = prev_selections_; + prev_selections_.clear(); + step(); + for(int i = 0; i < old_selections.size() - 2; i++) + step(old_selections[i]); + int idx = old_selections.size() - 2; + return { { old_selections[idx + 1], *step(old_selections[idx]) } }; +} + +bool FomodInstaller::hasNextStep(const std::vector>& selection) const +{ + if(cur_step_ == steps_.size() - 1) + return false; + std::map cur_flags = flags_; + int group_idx = 0; + if(!selection.empty()) + { + for(auto& group : steps_[cur_step_].groups) + { + int plugin_idx = 0; + for(auto& plugin : group.plugins) + { + if(!selection[group_idx][plugin_idx]) + { + plugin_idx++; + continue; + } + for(const auto& [key, value] : plugin.flags) + cur_flags[key] = value; + plugin_idx++; + } + group_idx++; + } + } + for(int i = cur_step_ + 1; i < steps_.size(); i++) + { + if(steps_[i].dependencies.evaluate(target_path_, cur_flags, version_eval_fun_, fomm_eval_fun_)) + return true; + } + return false; +} + +bool FomodInstaller::hasNoSteps() const +{ + return steps_.empty(); +} + +std::pair FomodInstaller::getMetaData(const sfs::path& path) +{ + pugi::xml_document doc; + auto [dir_name, file_name] = getFomodPath(path, "info.xml"); + doc.load_file((path / dir_name / file_name).c_str()); + return { doc.child("fomod").child_value("Name"), doc.child("fomod").child_value("Version") }; +} + +std::vector> FomodInstaller::getInstallationFiles( + const std::vector>& selection) +{ + updateState(selection); + parseInstallList(); + std::vector> files; + for(const auto& file : files_) + files.emplace_back(file.source, file.destination); + return files; +} + +bool FomodInstaller::hasPreviousStep() const +{ + return cur_step_ > 0; +} + +void FomodInstaller::parseFileList(const pugi::xml_node& file_list, + std::vector& target_vector, + bool warn_missing) +{ + for(auto file : file_list.children()) + { + File new_file; + const auto source_path = pu::normalizePath(file.attribute("source").value()); + auto source_path_optional = pu::pathExists(source_path, mod_base_path_); + if(!source_path_optional) + { + if(warn_missing) + Log::warning(std::format("Fomod requires installation of non existent file '{}'", + (mod_base_path_ / source_path).string())); + continue; + } + new_file.source = *source_path_optional; + auto dest = file.attribute("destination"); + if(dest) + new_file.destination = pu::normalizePath(dest.value()); + else + new_file.destination = new_file.source; + auto always_install = file.attribute("alwaysInstall"); + if(always_install) + new_file.always_install = always_install.as_bool(); + auto install_if_usable = file.attribute("installIfUsable"); + if(install_if_usable) + new_file.install_if_usable = install_if_usable.as_bool(); + auto priority = file.attribute("priority"); + if(priority) + new_file.priority = priority.as_int(); + target_vector.push_back(new_file); + } +} + +void FomodInstaller::parseInstallSteps(const pugi::xml_node& steps) +{ + for(const auto& step : steps.children()) + { + InstallStep cur_step; + cur_step.name = step.attribute("name").value(); + if(step.child("visible")) + cur_step.dependencies = *(step.child("visible").children().begin()); + for(const auto& group : step.child("optionalFileGroups").children()) + { + PluginGroup cur_group; + cur_group.name = group.attribute("name").value(); + cur_group.type = parseGroupType(group.attribute("type").value()); + for(const auto& plugin : group.child("plugins").children()) + { + Plugin cur_plugin; + initPlugin(plugin, cur_plugin); + cur_group.plugins.push_back(cur_plugin); + } + sortVector(cur_group.plugins, group.child("plugins").attribute("order").value()); + cur_step.groups.push_back(cur_group); + } + sortVector(cur_step.groups, step.child("optionalFileGroups").attribute("order").value()); + steps_.push_back(cur_step); + } + sortVector(steps_, steps.attribute("order").value()); +} + +PluginGroup::Type FomodInstaller::parseGroupType(const std::string& type) +{ + if(type == "SelectAtLeastOne") + return PluginGroup::at_least_one; + else if(type == "SelectAtMostOne") + return PluginGroup::at_most_one; + else if(type == "SelectExactlyOne") + return PluginGroup::exactly_one; + else if(type == "SelectAll") + return PluginGroup::all; + return PluginGroup::any; +} + +void FomodInstaller::parseInstallList() +{ + auto root = config_.child("conditionalFileInstalls"); + if(!root) + return; + for(const auto& pattern : root.child("patterns").children()) + { + if(!Dependency(pattern.child("dependencies")) + .evaluate(target_path_, flags_, version_eval_fun_, fomm_eval_fun_)) + continue; + std::vector cur_files; + parseFileList(pattern.child("files"), cur_files); + for(const auto& file : cur_files) + { + auto duplicate_iter = + std::ranges::find_if(files_, + [file = file](const File& other) + { + return file.source.string() == other.source.string() && + file.destination.string() == other.destination.string(); + }); + if(duplicate_iter != files_.end()) + continue; + auto iter = std::ranges::find(files_, file); + if(iter == files_.end() || file.destination.empty() || + file.destination.string().ends_with("/") || + sfs::is_directory(mod_base_path_ / file.source) && + sfs::is_directory(mod_base_path_ / iter->source)) + files_.push_back(file); + else if(*(iter) < file) + *iter = file; + } + } +} + +void FomodInstaller::initPlugin(const pugi::xml_node& xml_node, Plugin& plugin) +{ + plugin.name = xml_node.attribute("name").value(); + plugin.description = xml_node.child_value("description"); + std::string image_path = xml_node.child("image").attribute("path").value(); + if(image_path.empty()) + plugin.image_path = ""; + else + plugin.image_path = mod_base_path_ / pu::normalizePath(image_path); + if(xml_node.child("files")) + parseFileList(xml_node.child("files"), plugin.files, false); + for(const auto& flag : xml_node.child("conditionFlags").children()) + plugin.flags[flag.attribute("name").value()] = flag.text().as_string(); + if(xml_node.child("typeDescriptor").child("type")) + { + auto type = + parsePluginType(xml_node.child("typeDescriptor").child("type").attribute("name").value()); + plugin.type = type; + plugin.default_type = type; + } + else + { + auto type = parsePluginType(xml_node.child("typeDescriptor") + .child("dependencyType") + .child("defaultType") + .attribute("name") + .value()); + plugin.type = type; + plugin.default_type = type; + for(const auto& pattern : + xml_node.child("typeDescriptor").child("dependencyType").child("patterns").children()) + { + PluginDependency dependency; + dependency.type = parsePluginType(pattern.child("type").attribute("name").value()); + dependency.dependencies = pattern.child("dependencies"); + plugin.potential_types.push_back(dependency); + } + } +} + +PluginType FomodInstaller::parsePluginType(const std::string& type) +{ + if(type == "Required") + return PluginType::required; + else if(type == "Optional") + return PluginType::optional; + else if(type == "Recommended") + return PluginType::recommended; + else if(type == "NotUsable") + return PluginType::not_usable; + return PluginType::could_be_usable; +} + +void FomodInstaller::updateState(const std::vector>& selection) +{ + if(cur_step_ < 0 || selection.empty()) + return; + for(int group_idx = 0; auto& group : steps_[cur_step_].groups) + { + for(int plugin_idx = 0; auto& plugin : group.plugins) + { + if(!selection[group_idx][plugin_idx]) + { + plugin_idx++; + continue; + } + for(const auto& [key, value] : plugin.flags) + flags_[key] = value; + for(const auto& file : plugin.files) + { + auto duplicate_iter = + std::ranges::find_if(files_, + [file = file](const File& other) + { + return file.source.string() == other.source.string() && + file.destination.string() == other.destination.string(); + }); + if(duplicate_iter != files_.end()) + continue; + auto iter = std::ranges::find(files_, file); + if(iter == files_.end() || file.destination.empty() || + file.destination.string().ends_with("/") || + sfs::is_directory(mod_base_path_ / file.source) && + sfs::is_directory(mod_base_path_ / iter->source)) + files_.push_back(file); + else if(*(iter) < file) + *iter = file; + else + Log::warning( + std::format("Ignoring file '{}' because '{}' points to the same destination '{}'", + file.source.string(), + iter->source.string(), + file.destination.string())); + } + plugin_idx++; + } + group_idx++; + } +} + +std::pair FomodInstaller::getFomodPath(const sfs::path& source, + const std::string& file_name) +{ + std::string fomod_dir_name = "fomod"; + auto str_equals = [](const std::string& a, const std::string& b) + { + return std::equal(a.begin(), + a.end(), + b.begin(), + b.end(), + [](char c1, char c2) { return tolower(c1) == tolower(c2); }); + }; + for(const auto& dir_entry : sfs::directory_iterator(source)) + { + if(!dir_entry.is_directory()) + continue; + const std::string cur_dir = std::prev(dir_entry.path().end())->string(); + if(str_equals(cur_dir, fomod_dir_name)) + { + fomod_dir_name = cur_dir; + break; + } + } + if(!sfs::exists(source / fomod_dir_name)) + return { fomod_dir_name, file_name }; + std::string actual_name = file_name; + for(const auto& dir_entry : sfs::directory_iterator(source / fomod_dir_name)) + { + if(dir_entry.is_directory()) + continue; + const std::string cur_file = dir_entry.path().filename(); + if(str_equals(cur_file, file_name)) + { + actual_name = cur_file; + break; + } + } + return { fomod_dir_name, actual_name }; +} diff --git a/src/core/fomod/fomodinstaller.h b/src/core/fomod/fomodinstaller.h new file mode 100644 index 0000000..8aeaf6b --- /dev/null +++ b/src/core/fomod/fomodinstaller.h @@ -0,0 +1,169 @@ +/*! + * \file fomodinstaller.h + * \brief Header for the FomodInstaller class. + */ + +#pragma once + +#include "installstep.h" +#include +#include +#include +#include +#include + + +/*! + * \brief The fomod namespace contains classes used for parsing a FOMOD xml file and for + * creating an installer. + */ +namespace fomod +{ +/*! + * \brief Holds data and functions needed to pass a fomod file. + */ +class FomodInstaller +{ +public: + /*! \brief Default constructor. */ + FomodInstaller() = default; + + /*! + * \brief Initializes the installer. + * \param config_file Fomod file to be parsed. + * \param target_path Installation target, this is only used to check file dependencies. + */ + void init(const std::filesystem::path& config_file, + const std::filesystem::path& target_path = "", + const std::string& app_version = ""); + /*! + * \brief Advances installation process by one step. + * \param selection For every group: for every plugin: True if selected. + * \return The next installation step, if one exists. + */ + std::optional step(const std::vector>& selection = {}); + /*! + * \brief Returns a pair of the previous installation step and the + * selections made at that step. + * \return The step, if one exists. + */ + std::optional>, InstallStep>> stepBack(); + /*! + * \brief Checks if there is at least one more valid installation step. + * \param selection Current plugin selection. + * \return True if more steps exist. + */ + bool hasNextStep(const std::vector>& selection) const; + /*! + * \brief Returns all files to be installed with current selection. + * \param selection For every group: for every plugin: True if selected. + * \return Pair or source, destination paths for every file. + */ + std::vector> getInstallationFiles( + const std::vector>& selection = {}); + /*! + * \brief Checks if there is a previous installation step. + * \return True if there is one. + */ + bool hasPreviousStep() const; + /*! + * \brief Checks if installation has not steps. + * \return True if no steps where found. + */ + bool hasNoSteps() const; + /*! + * \brief Extracts mod name and version from a fomod info file in path/fomod/info.xml + * \param path Mod root directory. + * \return Mod name and version. + */ + static std::pair getMetaData(const std::filesystem::path& path); + +private: + /*! \brief Source fomod config file. */ + pugi::xml_document config_file_; + /*! \brief Root node of the config file. */ + pugi::xml_node config_; + /*! \brief Path used to check for file dependencies. */ + std::filesystem::path target_path_; + /*! \brief Contains all files extracted from the config file. */ + std::vector files_; + /*! \brief Steps performed during installation. */ + std::vector steps_; + /*! \brief Current installation step. */ + int cur_step_ = -1; + /*! \brief Maps flags to their value. */ + std::map flags_; + /*! \brief Base path of the mod to be installed. */ + std::filesystem::path mod_base_path_; + /*! \brief Previous selections made during installation process. */ + std::vector>> prev_selections_; + /*! \brief Used to evaluate game version conditions. */ + std::function version_eval_fun_ = [](auto s) { return true; }; + /*! \brief Used to evaluate fomm version conditions. */ + std::function fomm_eval_fun_ = [](auto s) { return true; }; + + /*! + * \brief Extracts all files from given file list node and appends them to given vector. + * \param file_list Source file list. + * \param target_list Extracted files will be appended to this vector. + * \param warn_missing If true: Warn if a file is missing. + */ + void parseFileList(const pugi::xml_node& file_list, + std::vector& target_vector, + bool warn_missing = true); + /*! + * \brief Extracts all install steps from given node and stores them in steps_. + * \param steps Source node. + */ + void parseInstallSteps(const pugi::xml_node& steps); + /*! + * \brief Determines group type from given string. + * \param type Source string. + * \return The type. + */ + PluginGroup::Type parseGroupType(const std::string& type); + /*! \brief Updates files_ according to the fomod files conditionalFileInstalls node. */ + void parseInstallList(); + /*! + * \brief Initializes given plugin plugin from fomod node. + * \param xml_node Source node. + * \param plugin Target plugin. + */ + void initPlugin(const pugi::xml_node& xml_node, Plugin& plugin); + /*! + * \brief Determines plugin type from given string. + * \param type Source string. + * \return The type. + */ + PluginType parsePluginType(const std::string& type); + /*! + * \brief Updates flags_ and files_ with selection. + * \param selection For every group: for every plugin: True if selected. + */ + void updateState(const std::vector>& selection); + /*! + * \brief Tries to find fomod/file_name in the given path. + * \param source Path to check. + * \param file_name File name to search for. + * \return Name of fomod directory and file, adapted to the actual capitalization. + */ + static std::pair getFomodPath( + const std::filesystem::path& source, + const std::string& file_name = "ModuleConfig.xml"); + /*! + * \brief Sorts given vector according to given ordering type. + * \param source Vector to be sorted. + * \param order Ordering type. + */ + template + void sortVector(std::vector& source, std::string order) + { + if(order == "Explicit") + return; + else if(order == "Descending") + std::ranges::sort(source, [](auto a, auto b) { return a.name > b.name; }); + else + std::ranges::sort(source, [](auto a, auto b) { return a.name < b.name; }); + } +}; +} diff --git a/src/core/fomod/installstep.h b/src/core/fomod/installstep.h new file mode 100644 index 0000000..89ded61 --- /dev/null +++ b/src/core/fomod/installstep.h @@ -0,0 +1,29 @@ +/*! + * \file installstep.h + * \brief Header for the InstallStep struct. + */ + +#pragma once + +#include "dependency.h" +#include "plugingroup.h" +#include + + +/*! + * \brief The fomod namespace contains classes used for parsing a FOMOD xml file and for + * creating an installer. + */ +namespace fomod +{ +/*! \brief A step during installation. */ +struct InstallStep +{ + /*! \brief Step name. */ + std::string name; + /*! \brief Step description. */ + Dependency dependencies; + /*! \brief Sets of choices displayed during this step. */ + std::vector groups; +}; +} diff --git a/src/core/fomod/plugin.h b/src/core/fomod/plugin.h new file mode 100644 index 0000000..525d0b3 --- /dev/null +++ b/src/core/fomod/plugin.h @@ -0,0 +1,67 @@ +/*! + * \file plugin.h + * \brief Header for the Plugin struct. + */ + +#pragma once + +#include "file.h" +#include "plugindependency.h" +#include "plugintype.h" +#include +#include +#include + + +/*! + * \brief The fomod namespace contains classes used for parsing a FOMOD xml file and for + * creating an installer. + */ +namespace fomod +{ +/*! \brief Represents one selectable option during installation. */ +struct Plugin +{ + /*! \brief Plugin name. */ + std::string name; + /*! \brief Plugin description. */ + std::string description; + /*! \brief Path to an image representing this plugin. */ + std::filesystem::path image_path; + /*! \brief Affects how this plugin is displayed. */ + PluginType type; + /*! \brief Fallback type if this has potential types but none are valid. */ + PluginType default_type; + /*! \brief Plugin takes the first type for which the condition is fulfilled. */ + std::vector potential_types; + /*! \brief Flags to be set when this is selected. */ + std::map flags; + /*! \brief Files to be installed when this is selected. */ + std::vector files; + + /*! + * \brief Updates type according to potential_types + * \param target_path Path file conditions. + * \param current_flags Flags to check. + * \param version_eval_fun Used to evaluate game version conditions. + * \param fomm_eval_fun Used to evaluate game fromm conditions. + */ + void updateType( + const std::filesystem::path& target_path, + const std::map& current_flags, + std::function version_eval_fun, + std::function fomm_eval_fun = [](auto s) { return true; }) + { + for(const auto& cur_type : potential_types) + { + if(cur_type.dependencies.evaluate( + target_path, current_flags, version_eval_fun, fomm_eval_fun)) + { + type = cur_type.type; + return; + } + } + type = default_type; + } +}; +} diff --git a/src/core/fomod/plugindependency.h b/src/core/fomod/plugindependency.h new file mode 100644 index 0000000..abbcf6a --- /dev/null +++ b/src/core/fomod/plugindependency.h @@ -0,0 +1,26 @@ +/*! + * \file plugindependency.h + * \brief Header for the PluginDependency struct. + */ + +#pragma once + +#include "dependency.h" +#include "plugintype.h" + + +/*! + * \brief The fomod namespace contains classes used for parsing a FOMOD xml file and for + * creating an installer. + */ +namespace fomod +{ +/*! \brief Represents a possible plugin type. */ +struct PluginDependency +{ + /*! \brief Possible type. */ + PluginType type; + /*! \brief Conditions which must be fulfilled for a plugin to take this type. */ + Dependency dependencies; +}; +} diff --git a/src/core/fomod/plugingroup.h b/src/core/fomod/plugingroup.h new file mode 100644 index 0000000..d55b4bb --- /dev/null +++ b/src/core/fomod/plugingroup.h @@ -0,0 +1,44 @@ +/*! + * \file plugingroup.h + * \brief Header for the PluginGroup struct. + */ + +#pragma once + +#include "plugin.h" +#include +#include + + +/*! + * \brief The fomod namespace contains classes used for parsing a FOMOD xml file and for + * creating an installer. + */ +namespace fomod +{ +/*! \brief Represents a set of options which can be selected during installation. */ +struct PluginGroup +{ + /*! \brief Describes restriction on how plugins in a group can be selected. */ + enum Type + { + /*! \brief At least one plugin must be selected. */ + at_least_one, + /*! \brief At most one plugin must be selected. */ + at_most_one, + /*! \brief Exactly one plugin must be selected. */ + exactly_one, + /*! \brief All plugins must be selected. */ + all, + /*! \brief No restrictions on selection. */ + any + }; + + /*! \brief Group name. */ + std::string name; + /*! \brief Selection restrictions. */ + Type type; + /*! \brief Selectable plugins in this group. */ + std::vector plugins; +}; +} diff --git a/src/core/fomod/plugintype.h b/src/core/fomod/plugintype.h new file mode 100644 index 0000000..37f8c92 --- /dev/null +++ b/src/core/fomod/plugintype.h @@ -0,0 +1,38 @@ +/*! + * \file plugintype.h + * \brief Header for the PluginType enum. + */ + +#pragma once + +#include +#include + + +/*! + * \brief The fomod namespace contains classes used for parsing a FOMOD xml file and for + * creating an installer. + */ +namespace fomod +{ +/*! \brief Describes how a plugin is presented. */ +enum PluginType +{ + /*! \brief Always installed. */ + required, + /*! \brief Can be installed. */ + optional, + /*! \brief Should be installed. */ + recommended, + /*! \brief Cannot be installed. */ + not_usable, + /*! \brief Usage unclear, will be treated like optional. */ + could_be_usable +}; + +const std::vector PLUGIN_TYPE_NAMES{ "Required", + "Optional", + "Recommended", + "Not Available", + "Could be usable" }; +} diff --git a/src/core/importmodinfo.h b/src/core/importmodinfo.h new file mode 100644 index 0000000..540cafd --- /dev/null +++ b/src/core/importmodinfo.h @@ -0,0 +1,57 @@ +/*! + * \file importmodinfo.h + * \brief Contains the ImportModInfo struct. + */ + +#pragma once + +#include +#include +#include + + +/*! + * \brief Stores data needed to download or extract a mod. + */ +struct ImportModInfo +{ + /*! \brief Describes what import action should be taken. */ + enum Type + { + download = 0, + extract = 1 + }; + /*! \brief Target ModdedApplication */ + int app_id; + /*! \brief Type of action to be performed. */ + Type type; + /*! \brief Path to the local file used for extraction or empty if type == download. */ + std::filesystem::path local_source; + /*! + * \brief URL used to download the mod. Can be either a URL pointing to the mod itself or + * a NexusMods nxm URL. + */ + std::string remote_source = ""; + /*! \brief This is where the mod should be stored after extraction/ download. */ + std::filesystem::path target_path; + /*! \brief If remote_source is a NexusMods mod page: The id of the file to be downloaded, else: + * Not set. */ + int nexus_file_id = -1; + /*! \brief If !=-1: The mod should be added to this mods group after installation. */ + int mod_id = -1; + /*! \brief Time at which this object was added to the queue. Used for sorting. */ + std::chrono::time_point queue_time = + std::chrono::high_resolution_clock::now(); + + /*! + * \brief Compares with another ImportModInfo object by their type. + * \param other Object to compare to. + * \return True if only this object has type extract, else false. + */ + bool operator<(const ImportModInfo& other) const + { + if(type == other.type) + return queue_time > other.queue_time; + return type < other.type; + } +}; diff --git a/src/core/installer.cpp b/src/core/installer.cpp new file mode 100644 index 0000000..f79778e --- /dev/null +++ b/src/core/installer.cpp @@ -0,0 +1,431 @@ +#include "installer.h" +#include "compressionerror.h" +#include "pathutils.h" +#include +#include +#include +#include +#include +#include + +namespace sfs = std::filesystem; +namespace pu = path_utils; + + +void Installer::extract(const sfs::path& source_path, + const sfs::path& dest_path, + std::optional progress_node) +{ + if(sfs::is_directory(source_path)) + { + sfs::create_directories(dest_path); + if(source_path.parent_path() == dest_path.parent_path()) + sfs::rename(source_path, dest_path); + else + sfs::copy(source_path, dest_path, sfs::copy_options::recursive); + return; + } + + try + { + extractWithProgress(source_path, dest_path, progress_node); + } + catch(CompressionError& error) + { + if(source_path.extension().string() == ".rar") + { + if(sfs::exists(dest_path)) + sfs::remove_all(dest_path); + extractBrokenRarArchive(source_path, dest_path); + } + else + throw error; + } + for(const auto& dir_entry : sfs::recursive_directory_iterator(dest_path)) + { + auto permissions = sfs::perms::owner_read | sfs::perms::owner_write | sfs::perms::group_read | + sfs::perms::group_write | sfs::perms::others_read; + if(dir_entry.is_directory()) + permissions |= sfs::perms::owner_exec | sfs::perms::group_exec | sfs::perms::others_exec; + sfs::permissions(dir_entry.path(), permissions); + } +} + +unsigned long Installer::install(const sfs::path& source, + const sfs::path& destination, + int options, + const std::string& type, + int root_level, + const std::vector> fomod_files) +{ + if(type != SIMPLEINSTALLER && type != FOMODINSTALLER) + throw std::runtime_error("Error: Unknown Installer type \"" + type + "\"!"); + unsigned tmp_id = 0; + sfs::path tmp_dir; + do + tmp_dir = destination.parent_path() / (EXTRACT_TMP_DIR + std::to_string(tmp_id)); + while(sfs::exists(tmp_dir) && tmp_id++ < std::numeric_limits::max()); + if(tmp_id == std::numeric_limits::max()) + throw std::runtime_error("Could not create directory!"); + try + { + extract(source, tmp_dir, {}); + } + catch(CompressionError& error) + { + sfs::remove_all(tmp_dir); + throw error; + } + + if(type == FOMODINSTALLER) + { + if(fomod_files.empty()) + { + sfs::remove_all(tmp_dir); + throw std::runtime_error("No files to install."); + } + if(root_level > 0) + { + auto tmp_move_dir = tmp_dir.string() + "." + MOVE_EXTENSION; + pu::moveFilesWithDepth(tmp_dir, tmp_move_dir, root_level); + sfs::rename(tmp_move_dir, tmp_dir); + } + // for(const auto& [source_file, dest_file] : fomod_files) + // { + // std::cout << std::format("'{}'\n -> '{}'", source_file.string(), dest_file.string()) + // << std::endl; + // } + for(auto iter = fomod_files.begin(); iter != fomod_files.end(); iter++) + { + const auto& [source_file, dest_file] = *iter; + sfs::create_directories(destination / dest_file.parent_path()); + if(!sfs::exists(tmp_dir / source_file)) + { + sfs::remove_all(destination); + sfs::remove_all(tmp_dir); + throw std::runtime_error("Could not find '" + source_file.string() + "'"); + } + const bool contains_no_duplicates = std::find_if(std::next(iter), + fomod_files.end(), + [source_file](auto pair) { + return pair.first == source_file; + }) == fomod_files.end(); + if(sfs::is_directory(tmp_dir / source_file)) + { + if(sfs::exists(destination / dest_file)) + pu::moveFilesToDirectory( + tmp_dir / source_file, destination / dest_file, contains_no_duplicates); + else + pu::copyOrMoveFiles( + tmp_dir / source_file, destination / dest_file, contains_no_duplicates); + } + else + { + if(sfs::exists(destination / dest_file) && !sfs::is_directory(destination / dest_file)) + sfs::remove(destination / dest_file); + if(dest_file.empty()) + pu::copyOrMoveFiles( + tmp_dir / source_file, destination / source_file.filename(), contains_no_duplicates); + else + pu::copyOrMoveFiles( + tmp_dir / source_file, destination / dest_file, contains_no_duplicates); + } + } + sfs::remove_all(tmp_dir); + } + else + { + if(options & lower_case) + pu::renameFiles(tmp_dir, tmp_dir, [](unsigned char c) { return std::tolower(c); }); + else if(options & upper_case) + pu::renameFiles(tmp_dir, tmp_dir, [](unsigned char c) { return std::toupper(c); }); + if(options & single_directory) + { + std::vector directories; + for(const auto& dir_entry : sfs::recursive_directory_iterator(tmp_dir)) + { + if(!dir_entry.is_directory()) + sfs::rename(dir_entry.path(), tmp_dir / dir_entry.path().filename()); + else + directories.push_back(dir_entry.path()); + } + for(const auto& dir : directories) + { + if(sfs::exists(dir)) + sfs::remove_all(dir); + } + } + + sfs::create_directories(destination); + try + { + if(root_level == 0) + sfs::rename(tmp_dir, destination); + else + pu::moveFilesWithDepth(tmp_dir, destination, root_level); + } + catch(sfs::filesystem_error& error) + { + sfs::remove_all(tmp_dir); + sfs::remove_all(destination); + throw error; + } + catch(std::runtime_error& error) + { + sfs::remove_all(tmp_dir); + sfs::remove_all(destination); + throw error; + } + } + unsigned long size = 0; + for(const auto& dir_entry : sfs::recursive_directory_iterator(destination)) + if(dir_entry.is_regular_file()) + size += dir_entry.file_size(); + return size; +} + +void Installer::uninstall(const sfs::path& mod_path, const std::string& type) +{ + sfs::remove_all(mod_path); +} + +std::vector Installer::getArchiveFileNames(const sfs::path& path) +{ + std::vector file_names; + if(sfs::is_directory(path)) + { + for(const auto& dir_entry : sfs::recursive_directory_iterator(path)) + file_names.push_back(pu::getRelativePath(dir_entry.path(), path)); + return file_names; + } + struct archive* source; + struct archive_entry* entry; + source = archive_read_new(); + archive_read_support_filter_all(source); + archive_read_support_format_all(source); + if(archive_read_open_filename(source, path.string().c_str(), 10240) != ARCHIVE_OK) + throw CompressionError("Could not open archive file."); + while(archive_read_next_header(source, &entry) == ARCHIVE_OK) + file_names.push_back(archive_entry_pathname(entry)); + if(archive_read_free(source) != ARCHIVE_OK) + throw CompressionError("Parsing of archive failed."); + return file_names; +} + +std::tuple Installer::detectInstallerSignature( + const sfs::path& source) +{ + const auto path = (sfs::path("fomod") / "ModuleConfig.xml"); + auto str_equals = [](const std::string& a, const std::string& b) + { + return std::equal(a.begin(), + a.end(), + b.begin(), + b.end(), + [](char c1, char c2) { return tolower(c1) == tolower(c2); }); + }; + const auto files = getArchiveFileNames(source); + int max_length = 0; + for(const auto& file : files) + max_length = std::max(max_length, pu::getPathLength(file)); + for(int root_level = 0; root_level < max_length; root_level++) + { + for(const auto& file : files) + { + const auto [head, tail] = pu::removePathComponents(file, root_level); + if(str_equals(path, tail)) + return { root_level, head.string(), FOMODINSTALLER }; + } + } + return { 0, {}, SIMPLEINSTALLER }; +} + +void Installer::cleanupFailedInstallation(const sfs::path& staging_dir, int mod_id) +{ + if(mod_id >= 0) + { + if(sfs::exists(staging_dir / std::to_string(mod_id))) + sfs::remove_all(staging_dir / std::to_string(mod_id)); + } + for(const auto& dir_entry : sfs::directory_iterator(staging_dir)) + { + if(!dir_entry.is_directory()) + continue; + if(dir_entry.path().extension() == MOVE_EXTENSION) + sfs::remove_all(dir_entry.path()); + std::regex tmp_dir_regex(EXTRACT_TMP_DIR + R"(\d+)"); + if(std::regex_search(dir_entry.path().filename().string(), tmp_dir_regex)) + sfs::remove_all(dir_entry.path()); + } +} + +void Installer::setIsAFlatpak(bool is_a_flatpak) +{ + is_a_flatpak_ = is_a_flatpak; +} + +void Installer::throwCompressionError(struct archive* source) +{ + throw CompressionError("Error during archive extraction."); + + // The following code sometimes crashes during execution of archive_error_string: + + // throw CompressionError( + // ("Error during archive extraction: " + std::string(archive_error_string(source))).c_str()); +} + +void Installer::copyArchive(struct archive* source, struct archive* dest) +{ + int return_code; + const void* buffer; + size_t size; + la_int64_t offset; + + while(true) + { + return_code = archive_read_data_block(source, &buffer, &size, &offset); + if(return_code == ARCHIVE_EOF) + return; + if(return_code < ARCHIVE_OK) + throwCompressionError(source); + if(archive_write_data_block(dest, buffer, size, offset) < ARCHIVE_OK) + throwCompressionError(dest); + } +} + +void Installer::extractWithProgress(const sfs::path& source_path, + const sfs::path& dest_path, + std::optional progress_node) +{ + constexpr int buffer_size = 10240; + struct archive* source; + struct archive* dest; + struct archive_entry* entry; + int return_code; + const char* file_name = source_path.c_str(); + int flags = ARCHIVE_EXTRACT_TIME; + sfs::path working_dir = "/tmp"; + try + { + working_dir = sfs::current_path(); + } + catch(std::filesystem::filesystem_error& error) + {} + if(!sfs::exists(dest_path)) + sfs::create_directories(dest_path); + sfs::current_path(dest_path); + source = archive_read_new(); + archive_read_support_format_all(source); + archive_read_support_filter_all(source); + dest = archive_write_disk_new(); + archive_write_disk_set_options(dest, flags); + archive_write_disk_set_standard_lookup(dest); + if(archive_read_open_filename(source, file_name, buffer_size)) + { + sfs::current_path(working_dir); + throw CompressionError("Could not open archive file."); + } + uint64_t total_size = 0; + while(true) + { + return_code = archive_read_next_header(source, &entry); + if(return_code == ARCHIVE_EOF) + break; + if(return_code < ARCHIVE_OK) + { + sfs::current_path(working_dir); + throwCompressionError(source); + } + total_size += archive_entry_size(entry); + } + if(progress_node) + (*progress_node)->setTotalSteps(total_size); + archive_read_close(source); + archive_read_free(source); + source = archive_read_new(); + archive_read_support_format_all(source); + archive_read_support_filter_all(source); + if(archive_read_open_filename(source, file_name, buffer_size)) + { + sfs::current_path(working_dir); + throw CompressionError("Could not open archive file."); + } + + while(true) + { + return_code = archive_read_next_header(source, &entry); + if(return_code == ARCHIVE_EOF) + break; + if(return_code < ARCHIVE_OK) + { + sfs::current_path(working_dir); + throwCompressionError(source); + } + archive_entry_set_pathname(entry, archive_entry_pathname(entry)); + if(archive_write_header(dest, entry) < ARCHIVE_OK) + { + sfs::current_path(working_dir); + throwCompressionError(dest); + } + const void* buff; + size_t size; + int64_t offset; + + while(true) + { + return_code = archive_read_data_block(source, &buff, &size, &offset); + if(return_code == ARCHIVE_EOF) + break; + if(return_code < ARCHIVE_OK) + { + sfs::current_path(working_dir); + throwCompressionError(source); + } + if(archive_write_data_block(dest, buff, size, offset) != ARCHIVE_OK) + { + sfs::current_path(working_dir); + throwCompressionError(dest); + } + if(progress_node) + (*progress_node)->advance(size); + } + if(archive_write_finish_entry(dest) < ARCHIVE_OK) + { + sfs::current_path(working_dir); + throwCompressionError(dest); + } + } + archive_read_close(source); + archive_read_free(source); + archive_write_close(dest); + archive_write_free(dest); + sfs::current_path(working_dir); +} + +void Installer::extractBrokenRarArchive(const sfs::path& source_path, const sfs::path& dest_path) +{ + sfs::path working_dir = sfs::current_path(); + if(!sfs::exists(dest_path)) + sfs::create_directories(dest_path); + sfs::current_path(dest_path); + std::string output; + std::array buffer; + std::string command = "\"" + UNRAR_PATH.string() + "\" x \"" + source_path.string() + "\""; + if(is_a_flatpak_) + command = "flatpak-spawn --host " + command; + auto pipe = + popen(command.c_str(), "r"); + while(!feof(pipe)) + { + if(fgets(buffer.data(), buffer.size(), pipe) != nullptr) + output += buffer.data(); + } + int ret_code = pclose(pipe); + sfs::current_path(working_dir); + if(ret_code == 127) + throw std::runtime_error( + "Invalid path to unrar. Try setting a different path in the settings."); + if(ret_code != 0) + throw std::runtime_error("Failed to extract archive using unrar. " + "Try setting a different path in the settings."); +} diff --git a/src/core/installer.h b/src/core/installer.h new file mode 100644 index 0000000..ac7f4a9 --- /dev/null +++ b/src/core/installer.h @@ -0,0 +1,176 @@ +/*! + * \file installer.h + * \brief Header for the Installer class + */ + +#pragma once + +#include "progressnode.h" +#include +#include +#include +#include +#include + + +/*! + * \brief Holds static functions to install and uninstall mods. + */ +class Installer +{ +public: + /*! \brief Flags used for installation options. */ + enum Flag + { + preserve_case = 0, + lower_case = 1 << 0, + upper_case = 1 << 1, + preserve_directories = 1 << 2, + single_directory = 1 << 3 + }; + /*! \brief Every vector represents an exclusive group of flags. */ + inline static const std::vector> OPTION_GROUPS{ + { preserve_case, lower_case, upper_case }, + { preserve_directories, single_directory } + }; + /*! \brief Maps installer flags to descriptive names. */ + inline static const std::map OPTION_NAMES{ + { preserve_case, "Preserve file names" }, + { lower_case, "Convert to lower case" }, + { upper_case, "Convert to upper case" }, + { preserve_directories, "Preserve directories" }, + { single_directory, "Root directory only" } + }; + /*! \brief Maps installer flags to brief descriptions of what they do. */ + inline static const std::map OPTION_DESCRIPTIONS{ + { preserve_case, "Do not alter file names" }, + { lower_case, "Convert file and directory names to lower case (FiLe -> file)" }, + { upper_case, "Convert file and directory names to upper case (FiLe -> FILE)" }, + { preserve_directories, "Do not alter directory structure" }, + { single_directory, "Move files from all sub directories to the mods root directory" } + }; + /*! \brief Simply extracts files */ + inline static const std::string SIMPLEINSTALLER{ "Simple Installer" }; + /*! + * \brief Takes a vector of files created by fomod::FomodInstaller and + * moves them to their target. + */ + inline static const std::string FOMODINSTALLER{ "Fomod Installer" }; + /*! + * \brief Contains all available installer types. + */ + inline static const std::vector INSTALLER_TYPES{ SIMPLEINSTALLER, FOMODINSTALLER }; + /*! + * \brief Path to the unrar binary. When set, this is used to extract certain rar + * archives. + */ + inline static std::filesystem::path UNRAR_PATH = "/bin/unrar"; + + /*! + * \brief Extracts the given archive to the given directory. + * \param source Path to the archive. + * \param destination Destination directory for extraction. + * \param progress_node Used to inform about extraction progress. + * \return Int indicating success(0), a filesystem error(-2) or an error + * during extraction(-1). + */ + static void extract(const std::filesystem::path& source, + const std::filesystem::path& destination, + std::optional progress_node = {}); + /*! + * \brief Extracts the archive, performs any actions specified by the installer type, + * then copies all files to given destination. + * \param path Path to the archive. + * \param destination Destination directory for the installation. + * \param options Sum of installation flags + * \param installer Installer type to use. + * \param root_level If > 0: Ignore all mod files and path components with depth < + * root_level. + * \return The total file size of the installed mod on disk. + */ + static unsigned long install( + const std::filesystem::path& source, + const std::filesystem::path& destination, + int options, + const std::string& type = SIMPLEINSTALLER, + int root_level = 0, + const std::vector> fomod_files = {}); + /*! + * \brief Uninstalls the mod at given directory using the given installer type. + * \param path Path to the mod. + * \param installer Installer type to use. + */ + static void uninstall(const std::filesystem::path& mod_path, + const std::string& type = SIMPLEINSTALLER); + /*! + * \brief Recursively reads all file and directory names from given archive. + * \param path Path to given archive. + * \return Vector of paths within the archive. + */ + static std::vector getArchiveFileNames(const std::filesystem::path& path); + /*! + * \brief Identifies the appropriate installer type from given source archive or + * directory. + * \param source Path to mod source. + * \return Required root level and type of the installer. + */ + static std::tuple detectInstallerSignature( + const std::filesystem::path& source); + /*! + * \brief Deletes all temporary files created during a previous installation attempt. + * \param staging_dir Directory containing temporary files. + * \param mod_id Id of the mod whose installation failed. + */ + static void cleanupFailedInstallation(const std::filesystem::path& staging_dir, int mod_id); + /*! + * \brief Sets whether this application is running as a flatpak. + * \param is_a_flatpak If true: The application is running as a flatpak. + */ + static void setIsAFlatpak(bool is_a_flatpak); + +private: + /*! \brief Directory name used to temporary storage of files during installation. */ + static inline std::string EXTRACT_TMP_DIR = "lmm_tmp_extract"; + /*! \brief Extension used for temporary storage during file movement. */ + static inline std::string MOVE_EXTENSION = "tmpmove"; + /*! \brief If true: The application is running as a flatpak. */ + static inline bool is_a_flatpak_ = false; + + /*! + * \brief Throws a CompressionError containing the error message of given archive. + * \param source Archive containing the error message. + */ + static void throwCompressionError(struct archive* source); + /*! + * \brief Copies data from given source archive to given destination archive. + * Throws CompressionError when an reading or writing fails. + * \param source Source archive. + * \param dest Destination archive. + */ + static void copyArchive(struct archive* source, struct archive* dest); + + /*! + * \brief Extracts the given archive to the given directory. Informs about + * extraction progress using the provided node. + * \param source Path to the archive. + * \param destination Destination directory for extraction. + * \param progress_node Used to inform about extraction progress. + * \return Int indicating success(0), a filesystem error(-2) or an error + * during extraction(-1). + */ + static void extractWithProgress(const std::filesystem::path& source_path, + const std::filesystem::path& dest_path, + std::optional progress_node = {}); + /*! + * \brief Libarchive sometime fails to extract certain rar archives when + * using the method implemented in \ref extractWithProgress. This function + * uses the unrar binary instead of libarchive to extract a given rar archive. + * \param source Path to the archive. + * \param destination Destination directory for extraction. + * \param progress_node Used to inform about extraction progress. + * \return Int indicating success(0), a filesystem error(-2) or an error + * during extraction(-1). + */ + static void extractBrokenRarArchive(const std::filesystem::path& source_path, + const std::filesystem::path& dest_path); +}; diff --git a/src/core/log.cpp b/src/core/log.cpp new file mode 100644 index 0000000..4ef11f8 --- /dev/null +++ b/src/core/log.cpp @@ -0,0 +1,65 @@ +#include "log.h" +#include +#include + + +std::string getTimestamp(Log::LogLevel log_level) +{ + const auto now = std::chrono::system_clock::now(); + auto cur_time = std::chrono::system_clock::to_time_t(now); + std::stringstream ss; + ss << std::put_time(std::localtime(&cur_time), "%F %T"); + if(log_level == Log::LOG_DEBUG) + ss << "." + << std::chrono::time_point_cast(now).time_since_epoch().count() % + 1000; + return ss.str(); +} + +namespace Log +{ +void error(const std::string& message) +{ + if(log_level >= LOG_ERROR) + log_printer(getTimestamp(Log::LOG_ERROR) + " [Error]: " + message, LOG_ERROR); +} + +void warning(const std::string& message) +{ + if(log_level >= LOG_WARNING) + log_printer(getTimestamp(Log::LOG_WARNING) + " [Warning]: " + message, LOG_WARNING); +} + +void info(const std::string& message) +{ + if(log_level >= LOG_INFO) + log_printer(getTimestamp(Log::LOG_INFO) + " [Info]: " + message, LOG_INFO); +} + +void debug(const std::string& message) +{ + if(log_level >= LOG_DEBUG) + log_printer(getTimestamp(Log::LOG_DEBUG) + " [Debug]: " + message, LOG_DEBUG); +} + +void log(LogLevel level, const std::string& message) +{ + switch(level) + { + case LOG_DEBUG: + debug(message); + break; + case LOG_INFO: + info(message); + break; + case LOG_WARNING: + warning(message); + break; + case LOG_ERROR: + error(message); + break; + default: + break; + } +} +} diff --git a/src/core/log.h b/src/core/log.h new file mode 100644 index 0000000..0e37f73 --- /dev/null +++ b/src/core/log.h @@ -0,0 +1,61 @@ +/*! + * \file log.h + * \brief Header for the Log namespace + */ +#pragma once + +#include +#include +#include + + +/*! + * \brief Contains functions for logging. + */ +namespace Log +{ +/*! \brief Represents the importance of a log message. */ +enum LogLevel +{ + LOG_ERROR = 0, + LOG_WARNING = 1, + LOG_INFO = 2, + LOG_DEBUG = 3 +}; + +/*! + * \brief Current log level. Messages with a log level less important than + * this will be ignored. + */ +inline LogLevel log_level = LOG_INFO; +/*! \brief Callback function used to output log messages. */ +inline std::function log_printer = [](std::string, LogLevel) {}; + + +/*! + * \brief Prints the current time and date followed by a debug message. + * \param message Message to be printed. + */ +void debug(const std::string& message); +/*! + * \brief Prints the current time and date followed by an info message. + * \param message Message to be printed. + */ +void info(const std::string& message); +/*! + * \brief Prints the current time and date followed by a warning message. + * \param message Message to be printed. + */ +void warning(const std::string& message); +/*! + * \brief Prints the current time and date followed by an error message. + * \param message Message to be printed. + */ +void error(const std::string& message); +/*! + * \brief Calls the appropriate logging function for the given log level with the given message. + * \param level Log level for the message. + * \param message Message to be printed. + */ +void log(LogLevel level, const std::string& message); +} diff --git a/src/core/lootdeployer.cpp b/src/core/lootdeployer.cpp new file mode 100644 index 0000000..e3c507f --- /dev/null +++ b/src/core/lootdeployer.cpp @@ -0,0 +1,650 @@ +#include "lootdeployer.h" +#include "pathutils.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sfs = std::filesystem; +namespace str = std::ranges; +namespace pu = path_utils; + + +LootDeployer::LootDeployer(const sfs::path& source_path, + const sfs::path& dest_path, + const std::string& name, + bool init_tags) : Deployer(source_path, dest_path, name) +{ + LIST_URLS = DEFAULT_LIST_URLS; + type_ = "Loot Deployer"; + is_autonomous_ = true; + updateAppType(); + setupPluginFiles(); + loadPlugins(); + updatePlugins(); + if(sfs::exists(dest_path_ / CONFIG_FILE_NAME)) + loadSettings(); + if(init_tags) + readPluginTags(); +} + +std::map LootDeployer::deploy(std::optional progress_node) +{ + log_(Log::LOG_INFO, std::format("Deployer '{}': Updating plugins...", name_)); + updatePlugins(); + updatePluginTags(); + return {}; +} + +std::map LootDeployer::deploy(const std::vector& loadorder, + std::optional progress_node) +{ + log_(Log::LOG_INFO, std::format("Deployer '{}': Updating plugins...", name_)); + updatePlugins(); + updatePluginTags(); + return {}; +} + +void LootDeployer::changeLoadorder(int from_index, int to_index) +{ + if(to_index == from_index) + return; + if(to_index < 0 || to_index >= plugins_.size()) + return; + if(to_index < from_index) + std::rotate(plugins_.begin() + to_index, + plugins_.begin() + from_index, + plugins_.begin() + from_index + 1); + else + std::rotate(plugins_.begin() + from_index, + plugins_.begin() + from_index + 1, + plugins_.begin() + to_index + 1); + writePlugins(); +} + +void LootDeployer::setModStatus(int mod_id, bool status) +{ + if(mod_id >= plugins_.size() || mod_id < 0) + return; + plugins_[mod_id].second = status; + writePlugins(); +} + +std::vector> LootDeployer::getConflictGroups() const +{ + std::vector group(plugins_.size()); + std::iota(group.begin(), group.end(), 0); + return { group }; +} + +std::vector LootDeployer::getModNames() const +{ + std::vector names{}; + names.reserve(plugins_.size()); + for(int i = 0; i < plugins_.size(); i++) + names.push_back(plugins_[i].first); + return names; +} + +void LootDeployer::addProfile(int source) +{ + if(num_profiles_ == 0) + { + num_profiles_++; + saveSettings(); + return; + } + if(source >= 0 && source <= num_profiles_ && num_profiles_ > 1) + { + sfs::copy(dest_path_ / ("." + PLUGIN_FILE_NAME + EXTENSION + std::to_string(source)), + dest_path_ / ("." + PLUGIN_FILE_NAME + EXTENSION + std::to_string(num_profiles_))); + sfs::copy(dest_path_ / ("." + LOADORDER_FILE_NAME + EXTENSION + std::to_string(source)), + dest_path_ / ("." + LOADORDER_FILE_NAME + EXTENSION + std::to_string(num_profiles_))); + } + else + { + sfs::copy(dest_path_ / PLUGIN_FILE_NAME, + dest_path_ / ("." + PLUGIN_FILE_NAME + EXTENSION + std::to_string(num_profiles_))); + sfs::copy(dest_path_ / LOADORDER_FILE_NAME, + dest_path_ / ("." + LOADORDER_FILE_NAME + EXTENSION + std::to_string(num_profiles_))); + } + num_profiles_++; + saveSettings(); +} + +void LootDeployer::removeProfile(int profile) +{ + if(profile >= num_profiles_ || profile < 0) + return; + std::string plugin_file = "." + PLUGIN_FILE_NAME + EXTENSION + std::to_string(profile); + std::string loadorder_file = "." + LOADORDER_FILE_NAME + EXTENSION + std::to_string(profile); + if(profile == current_profile_) + setProfile(profile == 0 ? num_profiles_ - 2 : 0); + if(sfs::exists(dest_path_ / plugin_file)) + sfs::remove(dest_path_ / plugin_file); + if(sfs::exists(dest_path_ / loadorder_file)) + sfs::remove(dest_path_ / loadorder_file); + num_profiles_--; + saveSettings(); +} + +void LootDeployer::setProfile(int profile) +{ + if(profile >= num_profiles_ || profile < 0 || profile == current_profile_) + return; + if(!sfs::exists(dest_path_ / PLUGIN_FILE_NAME) || + !sfs::exists(dest_path_ / LOADORDER_FILE_NAME) || + !sfs::exists(dest_path_ / ("." + PLUGIN_FILE_NAME + EXTENSION + std::to_string(profile))) || + !sfs::exists(dest_path_ / ("." + LOADORDER_FILE_NAME + EXTENSION + std::to_string(profile)))) + { + resetSettings(); + return; + } + sfs::rename(dest_path_ / PLUGIN_FILE_NAME, + dest_path_ / ("." + PLUGIN_FILE_NAME + EXTENSION + std::to_string(current_profile_))); + sfs::rename(dest_path_ / LOADORDER_FILE_NAME, + dest_path_ / + ("." + LOADORDER_FILE_NAME + EXTENSION + std::to_string(current_profile_))); + sfs::rename(dest_path_ / ("." + PLUGIN_FILE_NAME + EXTENSION + std::to_string(profile)), + dest_path_ / PLUGIN_FILE_NAME); + sfs::rename(dest_path_ / ("." + LOADORDER_FILE_NAME + EXTENSION + std::to_string(profile)), + dest_path_ / LOADORDER_FILE_NAME); + current_profile_ = profile; + saveSettings(); + loadPlugins(); + updatePlugins(); +} + +void LootDeployer::setConflictGroups(const std::vector>& newConflict_groups) +{ + log_(Log::LOG_DEBUG, + "WARNING: You are trying to set a load order for an autonomous" + " deployer. This will have no effect"); +} + +int LootDeployer::getNumMods() const +{ + return plugins_.size(); +} + +std::vector> LootDeployer::getLoadorder() const +{ + std::vector> loadorder; + loadorder.reserve(plugins_.size()); + for(int i = 0; i < plugins_.size(); i++) + loadorder.emplace_back(i, plugins_[i].second); + return loadorder; +} + +bool LootDeployer::addMod(int mod_id, bool enabled, bool update_conflicts) +{ + log_(Log::LOG_DEBUG, + "WARNING: You are trying to add a mod to an autonomous" + " deployer. This will have no effect"); + return false; +} + +bool LootDeployer::removeMod(int mod_id) +{ + log_(Log::LOG_DEBUG, + "WARNING: You are trying to remove a mod from an autonomous" + " deployer. This will have no effect"); + return false; +} + +bool LootDeployer::hasMod(int mod_id) const +{ + return false; +} + +bool LootDeployer::swapMod(int old_id, int new_id) +{ + log_(Log::LOG_DEBUG, + "WARNING: You are trying to swap a mod in an autonomous" + " deployer. This will have no effect"); + return false; +} + +std::vector LootDeployer::getFileConflicts( + int mod_id, + bool show_disabled, + std::optional progress_node) const +{ + if(progress_node) + { + (*progress_node)->setTotalSteps(1); + (*progress_node)->advance(); + } + return {}; +} + +std::unordered_set LootDeployer::getModConflicts(int mod_id, + std::optional progress_node) +{ + std::unordered_set conflicts{ mod_id }; + auto loot_handle = loot::CreateGameHandle(app_type_, source_path_, dest_path_); + std::vector plugin_paths; + plugin_paths.reserve(plugins_.size()); + for(const auto& [path, s] : plugins_) + plugin_paths.emplace_back(source_path_ / path); + loot_handle->LoadPlugins(plugin_paths, false); + auto plugin = loot_handle->GetPlugin(plugins_[mod_id].first); + for(int i = 0; i < plugins_.size(); i++) + { + if(i == mod_id) + continue; + if(loot_handle->GetPlugin(plugins_[i].first)->DoRecordsOverlap(*plugin)) + conflicts.insert(i); + } + return conflicts; +} + +void LootDeployer::sortModsByConflicts(std::optional progress_node) +{ + if(progress_node) + { + (*progress_node)->addChildren({ 1, 2, 5, 0.2f }); + (*progress_node)->child(0).setTotalSteps(1); + (*progress_node)->child(1).setTotalSteps(1); + (*progress_node)->child(2).setTotalSteps(1); + (*progress_node)->child(3).setTotalSteps(1); + } + updateMasterList(); + if(progress_node) + (*progress_node)->child(0).advance(); + sfs::path master_list_path = dest_path_ / "masterlist.yaml"; + if(!sfs::exists(master_list_path)) + throw std::runtime_error("Could not find masterlist.yaml at '" + master_list_path.string() + + "'\n.Try to update the URL in the " + + "settings. Alternatively, you can manually download the " + + "file and place it in '" + dest_path_.string() + "'.\nYou can " + + "disable auto updates in '" + + (dest_path_ / CONFIG_FILE_NAME).string() + "'."); + auto loot_handle = loot::CreateGameHandle(app_type_, source_path_, dest_path_); + sfs::path user_list_path(""); + if(sfs::exists(dest_path_ / "userlist.yaml")) + user_list_path = dest_path_ / "userlist.yaml"; + loot_handle->GetDatabase().LoadLists(master_list_path, user_list_path); + if(progress_node) + (*progress_node)->child(1).advance(); + std::vector plugin_paths; + plugin_paths.reserve(plugins_.size() + prefix_plugins_.size()); + for(const auto& plugin : prefix_plugins_) + plugin_paths.emplace_back(source_path_ / plugin); + for(const auto& [path, s] : plugins_) + plugin_paths.emplace_back(source_path_ / path); + auto sorted_plugins = loot_handle->SortPlugins(plugin_paths); + if(progress_node) + (*progress_node)->child(2).advance(); + std::vector> new_plugins; + std::set conflicting; + int num_light_plugins = 0; + int num_master_plugins = 0; + int num_standard_plugins = 0; + for(const auto& plugin : sorted_plugins) + { + if(str::find(prefix_plugins_, plugin) != prefix_plugins_.end()) + continue; + auto iter = str::find_if(plugins_, [plugin](const auto& p) { return p.first == plugin; }); + bool enabled = true; + if(iter != plugins_.end()) + enabled = iter->second; + const auto cur_plugin = loot_handle->GetPlugin(plugin); + if(cur_plugin->IsLightPlugin()) + num_light_plugins++; + else if(cur_plugin->IsMaster()) + num_master_plugins++; + else + num_standard_plugins++; + new_plugins.emplace_back(plugin, enabled); + auto masters = cur_plugin->GetMasters(); + for(const auto& master : masters) + { + if(!pu::pathExists(master, source_path_) && enabled) + log_(Log::LOG_WARNING, + "LOOT: Plugin '" + master + "' is missing but required" + " for '" + plugin + "'"); + } + auto meta_data = loot_handle->GetDatabase().GetPluginMetadata(plugin); + if(!meta_data) + continue; + auto requirements = meta_data->GetRequirements(); + for(const auto& req : requirements) + { + std::string file = static_cast(req.GetName()); + if(!pu::pathExists(file, source_path_)) + log_(Log::LOG_WARNING, "LOOT: Requirement '" + file + "' not met for '" + plugin + "'"); + } + } + log_(Log::LOG_INFO, + std::format("LOOT: Total Plugins: {}, Master: {}, Standard: {}, Light: {}", + new_plugins.size(), + num_master_plugins, + num_standard_plugins, + num_light_plugins)); + plugins_ = new_plugins; + writePlugins(); + if(progress_node) + (*progress_node)->child(3).advance(); +} + +void LootDeployer::cleanup() +{ + for(int i = 0; i < num_profiles_; i++) + { + sfs::path plugin_path = dest_path_ / ("." + PLUGIN_FILE_NAME + EXTENSION + std::to_string(i)); + sfs::path load_order_path = + dest_path_ / ("." + LOADORDER_FILE_NAME + EXTENSION + std::to_string(i)); + if(sfs::exists(plugin_path)) + sfs::remove(plugin_path); + if(sfs::exists(load_order_path)) + sfs::remove(load_order_path); + } + current_profile_ = 0; + num_profiles_ = 1; + if(sfs::exists(dest_path_ / CONFIG_FILE_NAME)) + sfs::remove(dest_path_ / CONFIG_FILE_NAME); +} + +std::vector> LootDeployer::getAutoTags() +{ + return tags_; +} + +std::map LootDeployer::getAutoTagMap() +{ + return { { LIGHT_PLUGIN, num_light_plugins_ }, + { MASTER_PLUGIN, num_master_plugins_ }, + { STANDARD_PLUGIN, num_standard_plugins_ } }; +} + +void LootDeployer::updatePlugins() +{ + std::vector plugin_files; + std::vector> new_plugins; + for(const auto& dir_entry : sfs::directory_iterator(source_path_)) + { + if(dir_entry.is_directory()) + continue; + const std::string file_name = dir_entry.path().filename().string(); + if(str::find(prefix_plugins_, file_name) != prefix_plugins_.end()) + continue; + if(std::regex_match(file_name, std::regex(R"(.*\.[eE][sS][pPlLmM]$)"))) + plugin_files.push_back(file_name); + } + for(auto it = plugins_.begin(); it != plugins_.end(); it++) + { + if(str::find_if(plugin_files, [&it](const auto& s) { return it->first == s; }) != + plugin_files.end()) + new_plugins.emplace_back(*it); + } + for(auto it = plugin_files.begin(); it != plugin_files.end(); it++) + { + if(str::find_if(new_plugins, [&it](auto& p) { return p.first == *it; }) == new_plugins.end()) + new_plugins.emplace_back(*it, true); + } + plugins_ = new_plugins; + writePlugins(); +} + +void LootDeployer::loadPlugins() +{ + plugins_.clear(); + prefix_plugins_.clear(); + std::string line; + std::ifstream plugin_file; + plugin_file.open(dest_path_ / PLUGIN_FILE_NAME); + if(!plugin_file.is_open()) + throw std::runtime_error("Could not open " + PLUGIN_FILE_NAME + + "!\nMake sure you have launched the game at least once."); + while(getline(plugin_file, line)) + { + std::smatch match; + if(std::regex_match(line, match, std::regex(R"(^\s*(\*?)([^#]*\.es[plm])(\r?))"))) + plugins_.emplace_back(match[2], match[1] == "*"); + } + plugin_file.close(); + std::ifstream loadorder_file; + loadorder_file.open(dest_path_ / LOADORDER_FILE_NAME); + if(!loadorder_file.is_open()) + throw std::runtime_error("Could not open " + LOADORDER_FILE_NAME + + "!\nMake sure you have launched the game at least once."); + while(getline(loadorder_file, line)) + { + std::smatch match; + if(std::regex_match(line, match, std::regex(R"(^\s*([^#]*\.es[plm])(\r)?)"))) + { + if((plugins_.empty() || plugins_[0].first != match[1])) + prefix_plugins_.push_back(match[1]); + else if(plugins_[0].first == match[1]) + break; + } + } + loadorder_file.close(); +} + +void LootDeployer::writePlugins() const +{ + std::ofstream plugin_file; + plugin_file.open(dest_path_ / PLUGIN_FILE_NAME); + if(!plugin_file.is_open()) + throw std::runtime_error("Could not open " + PLUGIN_FILE_NAME + "!"); + for(const auto& [name, status] : plugins_) + plugin_file << (status ? "*" : "") << name << "\n"; + plugin_file.close(); + std::ofstream loadorder_file; + loadorder_file.open(dest_path_ / LOADORDER_FILE_NAME); + if(!loadorder_file.is_open()) + throw std::runtime_error("Could not open " + LOADORDER_FILE_NAME + "!"); + for(const auto& name : prefix_plugins_) + loadorder_file << name << "\n"; + for(const auto& [name, status] : plugins_) + loadorder_file << name << "\n"; + loadorder_file.close(); +} + +void LootDeployer::saveSettings() const +{ + Json::Value settings; + settings["num_profiles"] = num_profiles_; + settings["current_profile"] = current_profile_; + settings["list_download_time"] = list_download_time_; + settings["auto_update_master_list"] = auto_update_lists_; + sfs::path settings_file_path = dest_path_ / CONFIG_FILE_NAME; + std::ofstream file(settings_file_path, std::fstream::binary); + if(!file.is_open()) + throw std::runtime_error("Error: Could not write to \"" + settings_file_path.string() + "\"."); + file << settings; + file.close(); +} + +void LootDeployer::loadSettings() +{ + Json::Value settings; + sfs::path settings_file_path = dest_path_ / CONFIG_FILE_NAME; + if(!sfs::exists(settings_file_path)) + { + resetSettings(); + return; + } + std::ifstream file(settings_file_path, std::fstream::binary); + if(!file.is_open()) + { + resetSettings(); + return; + } + file >> settings; + file.close(); + if(!settings.isMember("num_profiles") || !settings.isMember("current_profile") || + !settings.isMember("list_download_time") || !settings.isMember("auto_update_master_list")) + { + resetSettings(); + return; + } + num_profiles_ = settings["num_profiles"].asInt(); + current_profile_ = settings["current_profile"].asInt(); + list_download_time_ = settings["list_download_time"].asInt64(); + auto_update_lists_ = settings["auto_update_master_list"].asBool(); +} + +void LootDeployer::updateAppType() +{ + for(const auto& [type, file] : TYPE_IDENTIFIERS) + { + if(pu::pathExists(source_path_ / file, "")) + { + app_type_ = type; + return; + } + } + throw std::runtime_error("Could not identify game type in '" + source_path_.string() + "'"); +} + +void LootDeployer::updateMasterList() +{ + if(!auto_update_lists_) + return; + const auto cur_time = std::chrono::system_clock::now(); + const std::chrono::time_point update_time( + (std::chrono::seconds(list_download_time_))); + const auto one_day_ago = cur_time - std::chrono::days(1); + if(update_time >= one_day_ago && sfs::exists(dest_path_ / "masterlist.yaml")) + return; + + std::ofstream fstream(dest_path_ / "masterlist.yaml.tmp", std::ios::binary); + if(!fstream.is_open()) + throw std::runtime_error("Failed to update masterlist.yaml: Could not write to: \"" + + dest_path_.string() + "\"."); + + std::string url = LIST_URLS.at(app_type_); + auto pos = url.find(" "); + while(pos != std::string::npos) + { + url.replace(pos, 1, "%20"); + pos = url.find(" "); + } + cpr::Response response = cpr::Download(fstream, cpr::Url{ url }); + if(response.status_code != 200) + { + if(sfs::exists(dest_path_ / "masterlist.yaml.tmp")) + sfs::remove(dest_path_ / "masterlist.yaml.tmp"); + throw std::runtime_error("Could not download masterlist.yaml from '" + LIST_URLS.at(app_type_) + + "'.\nTry to update the URL in the " + + "settings. Alternatively, you can manually download the " + + "file and place it in '" + dest_path_.string() + + "'. You can disable auto updates in '" + + (dest_path_ / CONFIG_FILE_NAME).string() + "'."); + } + if(sfs::exists(dest_path_ / "masterlist.yaml")) + sfs::remove(dest_path_ / "masterlist.yaml"); + sfs::rename(dest_path_ / "masterlist.yaml.tmp", dest_path_ / "masterlist.yaml"); + list_download_time_ = + std::chrono::duration_cast(cur_time.time_since_epoch()).count(); + saveSettings(); +} + +void LootDeployer::resetSettings() +{ + num_profiles_ = 1; + current_profile_ = 0; + auto_update_lists_ = true; + list_download_time_ = 0; +} + +void LootDeployer::setupPluginFiles() +{ + if(sfs::exists(dest_path_ / PLUGIN_FILE_NAME) && sfs::exists(dest_path_ / LOADORDER_FILE_NAME)) + return; + updatePlugins(); +} + +void LootDeployer::updatePluginTags() +{ + tags_.clear(); + auto loot_handle = loot::CreateGameHandle(app_type_, source_path_, dest_path_); + std::vector plugin_paths; + plugin_paths.reserve(plugins_.size()); + for(const auto& [path, s] : plugins_) + plugin_paths.emplace_back(source_path_ / path); + loot_handle->LoadPlugins(plugin_paths, false); + num_light_plugins_ = 0; + num_master_plugins_ = 0; + num_standard_plugins_ = 0; + for(int i = 0; i < plugins_.size(); i++) + { + auto plugin = loot_handle->GetPlugin(plugins_[i].first); + if(plugin->IsLightPlugin()) + { + num_light_plugins_++; + tags_.push_back({ LIGHT_PLUGIN }); + } + else if(plugin->IsMaster()) + { + num_master_plugins_++; + tags_.push_back({ MASTER_PLUGIN }); + } + else + { + num_standard_plugins_++; + tags_.push_back({ STANDARD_PLUGIN }); + } + } + writePluginTags(); +} + +void LootDeployer::writePluginTags() const +{ + Json::Value json; + for(int i = 0; i < tags_.size(); i++) + { + for(int j = 0; j < tags_[i].size(); j++) + json[i][j] = tags_.at(i).at(j); + } + + const sfs::path tag_file_path = dest_path_ / TAGS_FILE_NAME; + std::ofstream file(tag_file_path, std::fstream::binary); + if(!file.is_open()) + throw std::runtime_error("Error: Could not write to \"" + tag_file_path.string() + "\"."); + file << json; + file.close(); +} + +void LootDeployer::readPluginTags() +{ + const sfs::path tag_file_path = dest_path_ / TAGS_FILE_NAME; + if(!sfs::exists(tag_file_path)) + { + updatePluginTags(); + return; + } + tags_.clear(); + num_light_plugins_ = 0; + num_master_plugins_ = 0; + num_standard_plugins_ = 0; + std::ifstream file(tag_file_path, std::fstream::binary); + if(!file.is_open()) + throw std::runtime_error("Error: Could not read from \"" + tag_file_path.string() + "\"."); + Json::Value json; + file >> json; + file.close(); + for(int i = 0; i < json.size(); i++) + { + tags_.push_back({}); + for(int j = 0; j < json[i].size(); j++) + { + const std::string tag = json[i][j].asString(); + tags_[i].push_back(tag); + if(tag == LIGHT_PLUGIN) + num_light_plugins_++; + else if(tag == MASTER_PLUGIN) + num_master_plugins_++; + else if(tag == STANDARD_PLUGIN) + num_standard_plugins_++; + } + } + if(tags_.size() != plugins_.size()) + updatePluginTags(); +} diff --git a/src/core/lootdeployer.h b/src/core/lootdeployer.h new file mode 100644 index 0000000..6e7eb5b --- /dev/null +++ b/src/core/lootdeployer.h @@ -0,0 +1,276 @@ +/*! + * \file lootdeployer.h + * \brief Header for the LootDeployer class + */ +#pragma once + +#include "deployer.h" +#include "loot/api.h" +#include + + +/*! + * \brief Autonomous Deployer which handles plugins for Fallout 3, Fallout 4, + * Fallout New Vegas, Fallout 4 VR, Starfield, Morrowind, Oblivion, Skyrim, + * Skyrim SE and Skyrim VR. + */ +class LootDeployer : public Deployer +{ +public: + /*! + * \brief Loads plugins and identifies the app type to be managed. + * \param source_path Path to the directory containing installed plugins. + * \param dest_path Path to the directory containing plugins.txt and loadorder.txt. + * \param name A custom name for this instance. + * \param init_tags If true: Initializes plugin tags. Disable this for testing purposes + * with invalid plugin files + */ + LootDeployer(const std::filesystem::path& source_path, + const std::filesystem::path& dest_path, + const std::string& name, + bool init_tags = true); + + /*! \brief Maps game type to a URL pointing to the masterlist.yaml for that type. */ + static inline const std::map DEFAULT_LIST_URLS = { + { loot::GameType::fo3, + "https://raw.githubusercontent.com/loot/fallout3/master/masterlist.yaml" }, + { loot::GameType::fo4, + "https://raw.githubusercontent.com/loot/fallout4/master/masterlist.yaml" }, + { loot::GameType::fo4vr, + "https://raw.githubusercontent.com/loot/fallout4vr/master/masterlist.yaml" }, + { loot::GameType::fonv, + "https://raw.githubusercontent.com/loot/falloutnv/master/masterlist.yaml" }, + { loot::GameType::starfield, + "https://raw.githubusercontent.com/loot/starfield/master/masterlist.yaml" }, + { loot::GameType::tes3, + "https://raw.githubusercontent.com/loot/morrowind/master/masterlist.yaml" }, + { loot::GameType::tes4, + "https://raw.githubusercontent.com/loot/oblivion/master/masterlist.yaml" }, + { loot::GameType::tes5, + "https://raw.githubusercontent.com/loot/skyrim/master/masterlist.yaml" }, + { loot::GameType::tes5se, + "https://raw.githubusercontent.com/loot/skyrimse/master/masterlist.yaml" }, + { loot::GameType::tes5vr, + "https://raw.githubusercontent.com/loot/skyrimvr/master/masterlist.yaml" } + }; + + static inline std::map LIST_URLS; + + /*! + * \brief Reloads all deployed plugins. Does NOT save current load order to disk. + * \param progress_node Used to inform about the current progress of deployment. + * \return Since this is an autonomous deployer, the returned map is always empty. + */ + std::map deploy(std::optional progress_node = {}) override; + /*! + * \brief Reloads all deployed plugins. Does NOT save current load order to disk. + * \param loadorder Ignored. + * \param progress_node Used to inform about the current progress of deployment. + * \return Since this is an autonomous deployer, the returned map is always empty. + */ + std::map deploy(const std::vector& loadorder, + std::optional progress_node = {}) override; + /*! + * \brief Moves a mod from one position in the load order to another. Saves changes to disk. + * \param from_index Index of mod to be moved. + * \param to_index Destination index. + */ + void changeLoadorder(int from_index, int to_index) override; + /*! + * \brief Enables or disables the given mod in the load order. Saves changes to disk. + * \param mod_id Mod to be edited. + * \param status The new status. + */ + void setModStatus(int mod_id, bool status) override; + /*! + * \brief Conflict groups are not supported by this type. + * \return All plugins in the non conflicting group. + */ + std::vector> getConflictGroups() const override; + /*! + * \brief Generates a vector of names for every plugin. + * \return The name vector. + */ + std::vector getModNames() const override; + /*! + * \brief Adds a new profile and optionally copies it's load order from an existing profile. + * Profiles are stored in the target directory. + * \param source The profile to be copied. A value of -1 indicates no copy. + */ + void addProfile(int source = -1) override; + /*! + * \brief Removes a profile. + * \param profile The profile to be removed. + */ + void removeProfile(int profile) override; + /*! + * \brief Setter for the active profile. Changes the currently active loadorder.txt + * and plugin.txt to the ones saved in the new profile. + * \param profile The new profile. + */ + void setProfile(int profile) override; + /*! + * \brief Does nothing. + * \param newConflict_groups Ignored. + */ + void setConflictGroups(const std::vector>& newConflict_groups) override; + /*! + * \brief Returns the number of plugins on the load order. + * \return The number of plugins. + */ + int getNumMods() const override; + /*! + * \brief Getter for the current plugin load order. + * \return The load order. + */ + std::vector> getLoadorder() const override; + /*! + * \brief Does nothing since this deployer manages its own mods. + * \param mod_id Ignored. + * \param enabled Ignored. + * \param update_conflicts Ignored. + * \return False. + */ + bool addMod(int mod_id, bool enabled = true, bool update_conflicts = true) override; + /*! + * \brief Does nothing. + * \param mod_id Ignored. + * \return False. + */ + bool removeMod(int mod_id) override; + /*! + * \brief Since this deployer uses its own internal mod ids, this function always + * returns false. + * \param mod_id Ignores + * \return False. + */ + bool hasMod(int mod_id) const override; + /*! + * \brief swapMod Does nothing since this deployer manages its own mods. + * \param old_id Ignored. + * \param new_id Ignored + * \return False. + */ + bool swapMod(int old_id, int new_id) override; + /*! + * \brief Not supported. + * \param mod_id Ignored. + * \param show_disabled Ignored. + * \param progress_node Set to 100%. + * \return An empty vector. + */ + std::vector getFileConflicts( + int mod_id, + bool show_disabled = false, + std::optional progress_node = {}) const override; + /*! + * \brief Checks for conflicts with other mods. + * Two mods are conflicting if they share at least one record. + * \param mod_id The mod to be checked. + * \param progress_node Used to inform about the current progress. + * \return A set of mod ids which conflict with the given mod. + */ + std::unordered_set getModConflicts(int mod_id, + std::optional progress_node = {}) override; + /*! + * \brief Sorts the current load order using LOOT. Uses a masterlist.yaml appropriate + * for the game managed by this deployer and optionally a userlist.yaml in the target + * directory. Saves the new load order to disk after sorting. + * \param progress_node Used to inform about the current progress. + */ + void sortModsByConflicts(std::optional progress_node = {}) override; + /*! \brief Deletes the config file and all profile files. */ + void cleanup() override; + /*! + * \brief Getter for mod tags. + * \return For every mod: A vector of auto tags added to that mod. + */ + virtual std::vector> getAutoTags() override; + /*! + * \brief Returns all available auto tag names. + * \return The tag names. + */ + virtual std::map getAutoTagMap() override; + +private: + /*! \brief Name of the file containing plugin activation status. */ + static constexpr std::string PLUGIN_FILE_NAME = "plugins.txt"; + /*! \brief Name of the file containing plugin load order. */ + static constexpr std::string LOADORDER_FILE_NAME = "loadorder.txt"; + /*! \brief Appended to profile file names. */ + static constexpr std::string EXTENSION = ".lmmprof"; + /*! \brief Name of the file containing settings. */ + static constexpr std::string CONFIG_FILE_NAME = ".lmmconfig"; + /*! \brief Name of the file containing loot tags. */ + static constexpr std::string TAGS_FILE_NAME = ".loot_tags"; + /*! \brief Maps supported game type to a path to a file unique to that type. */ + static inline const std::map TYPE_IDENTIFIERS = { + { loot::GameType::fo3, "Fallout3.esm" }, + { loot::GameType::fo4, "Fallout4.esm" }, + { loot::GameType::fo4vr, "Fallout4_VR.esm" }, + { loot::GameType::fonv, "FalloutNV.esm" }, + { loot::GameType::starfield, "Starfield.esm" }, + { loot::GameType::tes3, "Morrowind.esm" }, + { loot::GameType::tes4, "Oblivion.esm" }, + { loot::GameType::tes5, std::filesystem::path("..") / "TESV.exe" }, + { loot::GameType::tes5se, std::filesystem::path("..") / "SkyrimSE.exe" }, + { loot::GameType::tes5vr, "SkyrimVR.esm" } + }; + /*! \brief Name of a light plugin tag. */ + static constexpr std::string LIGHT_PLUGIN = "Light"; + /*! \brief Name of a master plugin tag. */ + static constexpr std::string MASTER_PLUGIN = "Master"; + /*! \brief Name of a standard plugin tag. */ + static constexpr std::string STANDARD_PLUGIN = "Standard"; + /*! \brief Contains names of all plugins and their activation status. */ + std::vector> plugins_; + /*! \brief Contains names of plugins which are in loadorder.txt but not in plugins.txt. */ + std::vector prefix_plugins_; + /*! \brief Current number of profiles. */ + int num_profiles_ = 0; + /*! \brief Type of game to be managed. */ + loot::GameType app_type_; + /*! \brief Timestamp representing the last time the masterlist.yaml was updated. */ + long list_download_time_ = 0; + /*! \brief If true: Automatically download new master lists. */ + bool auto_update_lists_ = true; + /*! \brief Current number of light plugins. */ + int num_light_plugins_ = 0; + /*! \brief Current number of master plugins. */ + int num_master_plugins_ = 0; + /*! \brief Current number of standard plugins. */ + int num_standard_plugins_ = 0; + /*! \brief For every plugin: Every loot tag associated with that plugin. */ + std::vector> tags_; + + /*! \brief Updates current plugins to reflect plugins actually in the source directory. */ + void updatePlugins(); + /*! \brief Load plugins from plugins.txt and loadorder.txt. */ + void loadPlugins(); + /*! \brief Writes current load order to plugins.txt and loadorder.txt. */ + void writePlugins() const; + /*! + * \brief Saves number of profiles, active profile, list_download_time_ and + * auto_update_lists_ to the config file. + */ + void saveSettings() const; + /*! + * \brief Loads number of profiles, active profile, list_download_time_ and + * auto_update_lists_ from the config file. + */ + void loadSettings(); + /*! \brief Identifies the type of game in the source directory using signature files. */ + void updateAppType(); + /*! \brief Downloads a new masterlist.yaml, if the current one is older than a day. */ + void updateMasterList(); + /*! \brief Resets all settings to default values. */ + void resetSettings(); + /*! \brief Creates plugin.txt and loadorder.txt files if they do not exist. */ + void setupPluginFiles(); + /*! \brief Updates the loot plugin tags for every currently loaded plugin. */ + void updatePluginTags(); + /*! \brief Writes the current tags_ to disk. */ + void writePluginTags() const; + /*! \brief Reads tags_ from disk. */ + void readPluginTags(); +}; diff --git a/src/core/manualtag.cpp b/src/core/manualtag.cpp new file mode 100644 index 0000000..7e8fa04 --- /dev/null +++ b/src/core/manualtag.cpp @@ -0,0 +1,61 @@ +#include "manualtag.h" +#include "parseerror.h" + +namespace str = std::ranges; + + +ManualTag::ManualTag(std::string name) +{ + name_ = name; +} + +ManualTag::ManualTag(const Json::Value& json) +{ + if(!json.isMember("name")) + throw ParseError("Tag name is missing."); + name_ = json["name"].asString(); + + if(json.isMember("mod_ids")) + { + for(const auto& mod : json["mod_ids"]) + mods_.push_back(mod.asInt()); + } +} + +void ManualTag::addMod(int mod_id) +{ + auto iter = str::find(mods_, mod_id); + if(iter == mods_.end()) + mods_.push_back(mod_id); +} + +void ManualTag::removeMod(int mod_id) +{ + auto iter = str::find(mods_, mod_id); + if(iter != mods_.end()) + mods_.erase(iter); +} + +void ManualTag::setMods(const std::vector mods) +{ + mods_ = mods; +} + +Json::Value ManualTag::toJson() const +{ + Json::Value json; + json["name"] = name_; + for(int i = 0; i < mods_.size(); i++) + json["mod_ids"][i] = mods_[i]; + return json; +} + +bool ManualTag::operator==(const std::string& name) const +{ + return this->name_ == name; +} + +bool ManualTag::operator==(const ManualTag& other) const +{ + return this->name_ == other.name_; +} diff --git a/src/core/manualtag.h b/src/core/manualtag.h new file mode 100644 index 0000000..3dc091c --- /dev/null +++ b/src/core/manualtag.h @@ -0,0 +1,65 @@ +/*! + * \file manualtag.h + * \brief Header for the ManualTag class. + */ + +#pragma once + +#include "tag.h" +#include +#include +#include + + +/*! + * \brief Tag which has to be manually added to mods. + */ +class ManualTag : public Tag +{ +public: + /*! + * \brief Constructs a new tag with the given name. + * \param name The tags name. + */ + ManualTag(std::string name); + /*! + * \brief Deserializes a ManualTag from the given json object. + * \param json Source json object. + * \param json_path Path to the json object. Used is exception messaged. + * \throws ParseError when the json object is invalid. + */ + ManualTag(const Json::Value& json); + + /*! + * \brief Adds this tag to the given mod. + * \param mod_id Id if the mod to which this tag is to be added. + */ + void addMod(int mod_id); + /*! + * \brief Removes this tag from the given mod. + * \param mod_id Id if the mod from which this tag is to be removed. + */ + void removeMod(int mod_id); + /*! + * \brief Removes this tag from all mods and adds it only to the given mods. + * \param mods Mods to which this tag is to be added. + */ + void setMods(const std::vector mods); + /*! + * \brief Serializes this tag to a json object. + * \return The json object. + */ + Json::Value toJson() const; + /*! + * \brief Compares this tag by name to the given name. + * \param name Name to compare to. + * \return True if the names are identical. + */ + bool operator==(const std::string& name) const; + /*! + * \brief Compares this tag by name to the given tag. + * \param other Tag to compare to. + * \return True if the names are identical. + */ + bool operator==(const ManualTag& other) const; +}; diff --git a/src/core/mod.cpp b/src/core/mod.cpp new file mode 100644 index 0000000..80a0e29 --- /dev/null +++ b/src/core/mod.cpp @@ -0,0 +1,53 @@ +#include "mod.h" + +Mod::Mod(int id, + const std::string& name, + const std::string& version, + const std::time_t& time, + const std::filesystem::path& source_l, + const std::string& source_r, + const std::time_t& time_r, + unsigned long size, + const std::time_t& suppress_time) : + id(id), name(std::move(name)), version(std::move(version)), install_time(time), + local_source(source_l), remote_source(source_r), remote_update_time(time_r), size_on_disk(size), + suppress_update_time(suppress_time) +{} + +Mod::Mod(const Json::Value& json) +{ + Mod(json["id"].asInt(), + json["name"].asString(), + json["version"].asString(), + json["install_time"].asInt64(), + json["local_source"].asString(), + json["remote_source"].asString(), + json["remote_update_time"].asInt64(), + json["size_on_disk"].asInt64(), + json["suppress_update_time"].asInt64()); +} + +Json::Value Mod::toJson() const +{ + Json::Value json; + json["id"] = id; + json["name"] = name; + json["version"] = version; + json["install_time"] = install_time; + json["local_source"] = local_source.string(); + json["remote_source"] = remote_source; + json["remote_update_time"] = remote_update_time; + json["size_on_disk"] = size_on_disk; + json["suppress_update_time"] = suppress_update_time; + return json; +} + +bool Mod::operator==(const Mod& other) const +{ + return id == other.id; +} + +bool Mod::operator<(const Mod& other) const +{ + return id < other.id; +} diff --git a/src/core/mod.h b/src/core/mod.h new file mode 100644 index 0000000..50dfe77 --- /dev/null +++ b/src/core/mod.h @@ -0,0 +1,78 @@ +/*! + * \file mod.h + * \brief Contains the Mod struct. + */ + +#pragma once + +#include +#include +#include + + +/*! + * \brief Stores information about an installed mod. + */ +struct Mod +{ + /*! \brief The mod's id. */ + int id; + /*! \brief The mod's name. */ + std::string name; + /*! \brief The mod's version. */ + std::string version; + /*! \brief The mod's installation time. */ + std::time_t install_time; + /*! \brief Path to the local archive or directory used to install this mod. */ + std::filesystem::path local_source; + /*! \brief URL from where the mod was downloaded. */ + std::string remote_source; + /*! \brief Timestamp for when the mod was updated at the remote source. */ + std::time_t remote_update_time; + /*! \brief Total size of the installed mod on disk. */ + unsigned long size_on_disk; + /*! \brief Timestamp for when the user requested to suppress current update notifications. */ + std::time_t suppress_update_time; + + /*! + * \brief Constructor. Simply initializes members. + * \param id The mod's id. + * \param name The mod's name. + * \param version The mod's version. + * \param time The mod's installation time. + * \param source_l Path to the local archive or directory used to install this mod. + * \param source_r URL from where the mod was downloaded. + * \param time_r Timestamp for when the mod was updated at the remote source. + * \param size Total size of the installed mod on disk. + * \param suppress_time Timestamp for when the user requested to suppress current update + * notifications. + */ + Mod(int id, + const std::string& name, + const std::string& version, + const std::time_t& time, + const std::filesystem::path& source_l, + const std::string& source_r, + const std::time_t& time_r, + unsigned long size, + const std::time_t& suppress_time); + /*! + * \brief Initializes all members from a JSON object. + * \param json The source for member values. + */ + Mod(const Json::Value& json); + + Json::Value toJson() const; + /*! + * \brief Compares to another mod by id. + * \param other Mod to compare to. + * \return True if both share the same id, else false. + */ + bool operator==(const Mod& other) const; + /*! + * \brief Compares mods by their id. + * \param Other mod for comparison. + * \return True only if this.id < other.id + */ + bool operator<(const Mod& other) const; +}; diff --git a/src/core/moddedapplication.cpp b/src/core/moddedapplication.cpp new file mode 100644 index 0000000..26af439 --- /dev/null +++ b/src/core/moddedapplication.cpp @@ -0,0 +1,1993 @@ +#include "moddedapplication.h" +#include "deployerfactory.h" +#include "installer.h" +#include "parseerror.h" +#include "pathutils.h" +#include +#include +#include +#include +#include + +namespace sfs = std::filesystem; +namespace str = std::ranges; +namespace pu = path_utils; + + +ModdedApplication::ModdedApplication(sfs::path staging_dir, + std::string name, + std::string command, + std::filesystem::path icon_path, + std::string app_version) : + staging_dir_(staging_dir), name_(name), command_(command), icon_path_(icon_path) +{ + if(sfs::exists(staging_dir / CONFIG_FILE_NAME)) + updateState(true); + else + { + addProfile({ "Default", app_version, -1 }); + updateSettings(true); + } + sfs::copy(staging_dir_ / CONFIG_FILE_NAME, + staging_dir_ / ("." + CONFIG_FILE_NAME + ".bak"), + sfs::copy_options::overwrite_existing); +} + +void ModdedApplication::deployMods() +{ + std::vector deployers; + for(int i = 0; i < deployers_.size(); i++) + deployers.push_back(i); + deployModsFor(deployers); +} + +void ModdedApplication::deployModsFor(const std::vector& deployers) +{ + std::vector weights; + for(int i : deployers) + { + const int num_mods = deployers_[i]->getNumMods(); + if(deployers_[i]->isAutonomous() || num_mods == 0) + weights.push_back(1); + else + weights.push_back(num_mods); + } + + // always deploy normal deployers first, since some autonomous deployers + // may depend on their output + ProgressNode node(progress_callback_, weights); + for(int i : deployers) + { + if(!deployers_[i]->isAutonomous()) + { + const auto mod_sizes = deployers_[i]->deploy(&(node.child(i))); + for(const auto [mod_id, mod_size] : mod_sizes) + { + auto mod_iter = + str::find_if(installed_mods_, [id = mod_id](const Mod& m) { return m.id == id; }); + if(mod_iter != installed_mods_.end()) + mod_iter->size_on_disk = mod_size; + } + } + } + + for(int i : deployers) + { + if(deployers_[i]->isAutonomous()) + deployers_[i]->deploy(&(node.child(i))); + } + + updateSettings(true); +} + +void ModdedApplication::installMod(const AddModInfo& info) +{ + if(info.replace_mod && info.group != -1) + { + replaceMod(info); + return; + } + ProgressNode progress_node(progress_callback_); + if(info.group >= 0 && !info.deployers.empty()) + progress_node.addChildren({ 1.0f, 10.0f, info.deployers.size() > 1 ? 10.0f : 1.0f }); + else if(info.group >= 0 || !info.deployers.empty()) + progress_node.addChildren({ 1, 10 }); + else + progress_node.addChildren({ 1 }); + progress_node.child(0).setTotalSteps(1); + int mod_id = 0; + if(!installed_mods_.empty()) + mod_id = std::max_element(installed_mods_.begin(), installed_mods_.end())->id + 1; + while(sfs::exists(staging_dir_ / std::to_string(mod_id)) && + mod_id < std::numeric_limits().max()) + mod_id++; + if(mod_id == std::numeric_limits().max()) + throw std::runtime_error("Error: Could not generate new mod id."); + last_mod_id_ = mod_id; + const auto mod_size = Installer::install(info.source_path, + staging_dir_ / std::to_string(mod_id), + info.installer_flags, + info.installer, + info.root_level, + info.files); + const auto time_now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + installed_mods_.emplace_back(mod_id, + info.name, + info.version, + time_now, + info.local_source, + info.remote_source, + time_now, + mod_size, + time_now); + installer_map_[mod_id] = info.installer; + progress_node.child(0).advance(); + if(info.group >= 0) + { + if(modHasGroup(info.group)) + addModToGroup(mod_id, group_map_[info.group], &progress_node.child(1)); + else + createGroup(mod_id, info.group, &progress_node.child(1)); + } + + for(int deployer : info.deployers) + addModToDeployer(deployer, mod_id, true, &progress_node.child(info.group >= 0 ? 2 : 1)); + + for(auto& tag : auto_tags_) + tag.updateMods(staging_dir_, std::vector{ mod_id }); + updateAutoTagMap(); + + updateSettings(true); +} + +void ModdedApplication::uninstallMods(const std::vector& mod_ids, + const std::string& installer_type) +{ + std::vector weights; + std::vector> update_targets; + for(int depl = 0; depl < deployers_.size(); depl++) + update_targets.push_back({}); + for(int mod_id : mod_ids) + { + if(group_map_.contains(mod_id)) + removeModFromGroup(mod_id, false); + auto mod_iter = std::find_if( + installed_mods_.begin(), installed_mods_.end(), [mod_id](Mod m) { return m.id == mod_id; }); + if(mod_iter == installed_mods_.end()) + continue; + for(int depl = 0; depl < deployers_.size(); depl++) + { + if(deployers_[depl]->isAutonomous()) + continue; + for(int prof = 0; prof < profile_names_.size(); prof++) + { + deployers_[depl]->setProfile(prof); + if(deployers_[depl]->removeMod(mod_id) && + str::find(update_targets[depl], prof) == update_targets[depl].end()) + { + update_targets[depl].push_back(prof); + weights.push_back(deployers_[depl]->getNumMods()); + } + } + deployers_[depl]->setProfile(current_profile_); + } + + installed_mods_.erase(mod_iter); + std::string installer = Installer::SIMPLEINSTALLER; + if(installer_type == "" && installer_map_.contains(mod_id)) + installer = installer_map_[mod_id]; + Installer::uninstall(staging_dir_ / std::to_string(mod_id), installer); + + for(auto& tag : manual_tags_) + tag.removeMod(mod_id); + } + + ProgressNode node(progress_callback_, weights); + int i = 0; + for(int depl = 0; depl < update_targets.size(); depl++) + { + for(int prof : update_targets[depl]) + { + deployers_[depl]->setProfile(prof); + deployers_[depl]->updateConflictGroups(&node.child(i)); + i++; + } + deployers_[depl]->setProfile(current_profile_); + } + + updateSettings(true); +} + +void ModdedApplication::changeLoadorder(int deployer, int from_index, int to_index) +{ + deployers_[deployer]->changeLoadorder(from_index, to_index); + updateSettings(true); +} + +void ModdedApplication::addModToDeployer(int deployer, + int mod_id, + bool update_conflicts, + std::optional progress_node) +{ + if(!deployers_[deployer]->isAutonomous()) + { + const bool was_added = deployers_[deployer]->addMod(mod_id); + ProgressNode node(progress_callback_); + if(update_conflicts && was_added) + deployers_[deployer]->updateConflictGroups(progress_node ? progress_node : &node); + else if(progress_node) + { + (*progress_node)->setTotalSteps(1); + (*progress_node)->advance(); + } + splitMod(mod_id, deployer); + updateSettings(true); + } +} + +void ModdedApplication::removeModFromDeployer(int deployer, + int mod_id, + bool update_conflicts, + std::optional progress_node) +{ + if(!deployers_[deployer]->isAutonomous()) + { + const bool was_removed = deployers_[deployer]->removeMod(mod_id); + ProgressNode node(progress_callback_); + if(update_conflicts && was_removed) + deployers_[deployer]->updateConflictGroups(progress_node ? progress_node : &node); + else if(progress_node) + { + (*progress_node)->setTotalSteps(1); + (*progress_node)->advance(); + } + updateSettings(true); + } +} + +void ModdedApplication::setModStatus(int deployer, int mod_id, bool status) +{ + deployers_[deployer]->setModStatus(mod_id, status); + updateSettings(true); +} + +void ModdedApplication::addDeployer(const EditDeployerInfo& info) +{ + std::string source_dir = staging_dir_; + if(DeployerFactory::AUTONOMOUS_DEPLOYERS.at(info.type)) + source_dir = info.source_dir; + deployers_.push_back(DeployerFactory::makeDeployer( + info.type, source_dir, info.target_dir, info.name, info.use_copy_deployment)); + for(int i = 0; i < profile_names_.size(); i++) + deployers_[deployers_.size() - 1]->addProfile(); + deployers_[deployers_.size() - 1]->setProfile(current_profile_); + for(int i = 0; i < installed_mods_.size(); i++) + { + for(int depl = 0; depl < deployers_.size(); depl++) + { + if(deployers_[depl]->hasMod(installed_mods_[i].id)) + splitMod(installed_mods_[i].id, depl); + } + } + updateSettings(true); +} + +void ModdedApplication::removeDeployer(int deployer, bool cleanup) +{ + if(cleanup) + deployers_[deployer]->cleanup(); + deployers_.erase(deployers_.begin() + deployer); + updateSettings(true); +} + +std::vector ModdedApplication::getDeployerNames() const +{ + std::vector names; + for(const auto& deployer : deployers_) + names.push_back(deployer->getName()); + return names; +} + +std::vector ModdedApplication::getModInfo() const +{ + std::vector mod_info{}; + for(const auto& mod : installed_mods_) + { + std::vector deployer_names; + std::vector deployer_ids; + std::vector statuses; + for(int i = 0; i < deployers_.size(); i++) + { + if(deployers_[i]->isAutonomous()) + continue; + auto status = deployers_[i]->getModStatus(mod.id); + if(status) + { + deployer_names.push_back(deployers_[i]->getName()); + deployer_ids.push_back(i); + statuses.push_back(*status); + } + } + + int group = -1; + bool is_active = false; + if(group_map_.contains(mod.id)) + { + group = group_map_.at(mod.id); + is_active = active_group_members_[group] == mod.id; + } + + mod_info.emplace_back( + mod.id, + mod.name, + mod.version, + mod.install_time, + mod.local_source, + mod.remote_source, + mod.remote_update_time, + mod.size_on_disk, + mod.suppress_update_time, + deployer_names, + deployer_ids, + statuses, + group, + is_active, + manual_tag_map_.contains(mod.id) ? manual_tag_map_.at(mod.id) : std::vector{}, + auto_tag_map_.contains(mod.id) ? auto_tag_map_.at(mod.id) : std::vector{}); + } + return mod_info; +} + +std::vector> ModdedApplication::getLoadorder(int deployer) const +{ + return deployers_[deployer]->getLoadorder(); +} + +const sfs::path& ModdedApplication::getStagingDir() const +{ + return staging_dir_; +} + +void ModdedApplication::setStagingDir(std::string staging_dir, bool move_existing) +{ + if(staging_dir == staging_dir_) + return; + if(move_existing) + { + for(const auto& mod : installed_mods_) + { + std::string mod_dir = std::to_string(mod.id); + sfs::rename(staging_dir_ / mod_dir, sfs::path(staging_dir) / mod_dir); + } + sfs::rename(staging_dir_ / CONFIG_FILE_NAME, sfs::path(staging_dir) / CONFIG_FILE_NAME); + } + staging_dir_ = staging_dir; + updateState(true); +} + +const std::string& ModdedApplication::name() const +{ + return name_; +} + +void ModdedApplication::setName(const std::string& newName) +{ + name_ = newName; + updateSettings(true); +} + +int ModdedApplication::getNumDeployers() const +{ + return deployers_.size(); +} + +const std::string& ModdedApplication::getConfigFileName() const +{ + return CONFIG_FILE_NAME; +} + +void ModdedApplication::changeModName(int mod_id, const std::string& new_name) +{ + auto iter = std::find_if( + installed_mods_.begin(), installed_mods_.end(), [mod_id](Mod m) { return m.id == mod_id; }); + if(iter == installed_mods_.end()) + throw std::runtime_error("Error: Unknown mod id: " + std::to_string(mod_id)); + iter->name = new_name; + updateSettings(true); +} + +std::vector ModdedApplication::getFileConflicts(int deployer, + int mod_id, + bool show_disabled) const +{ + ProgressNode node(progress_callback_); + auto conflicts = deployers_[deployer]->getFileConflicts(mod_id, show_disabled, &node); + for(auto& [_, id, name] : conflicts) + name = getModName(id); + return conflicts; +} + +AppInfo ModdedApplication::getAppInfo() const +{ + AppInfo info; + info.name = name_; + info.staging_dir = staging_dir_.string(); + info.command = command_; + info.num_mods = installed_mods_.size(); + info.app_version = app_versions_[current_profile_]; + for(const auto& deployer : deployers_) + { + info.deployers.push_back(deployer->getName()); + info.deployer_types.push_back(deployer->getType()); + info.target_dirs.push_back(deployer->getDestPath()); + info.deployer_mods.push_back(deployer->getNumMods()); + info.uses_copy_deployment.push_back(deployer->usesCopyDeployment()); + } + info.tools = tools_; + for(const auto& tag : manual_tags_) + info.num_mods_per_manual_tag[tag.getName()] = tag.getNumMods(); + for(const auto& tag : auto_tags_) + { + info.num_mods_per_auto_tag[tag.getName()] = tag.getNumMods(); + info.auto_tags[tag.getName()] = { tag.getExpression(), tag.getConditions() }; + } + return info; +} + +void ModdedApplication::addTool(std::string name, std::string command) +{ + tools_.emplace_back(name, command); + updateSettings(true); +} + +void ModdedApplication::removeTool(int tool_id) +{ + if(tool_id < tools_.size() && tool_id >= 0) + { + tools_.erase(tools_.begin() + tool_id); + updateSettings(true); + } +} + +const std::vector>& ModdedApplication::getTools() const +{ + return tools_; +} + +const std::string& ModdedApplication::command() const +{ + return command_; +} + +void ModdedApplication::setCommand(const std::string& newCommand) +{ + command_ = newCommand; + updateSettings(true); +} + +void ModdedApplication::editDeployer(int deployer, const EditDeployerInfo& info) +{ + if(deployers_[deployer]->getType() == info.type) + { + deployers_[deployer]->setName(info.name); + deployers_[deployer]->setDestPath(info.target_dir); + deployers_[deployer]->setUseCopyDeployment(info.use_copy_deployment); + } + else + { + json_settings_["deployers"][deployer]["source_path"] = info.source_dir; + json_settings_["deployers"][deployer]["name"] = info.name; + json_settings_["deployers"][deployer]["dest_path"] = info.target_dir; + json_settings_["deployers"][deployer]["type"] = info.type; + json_settings_["deployers"][deployer]["use_copy_deployment"] = info.use_copy_deployment; + updateState(); + } + if(deployers_[deployer]->isAutonomous()) + deployers_[deployer]->setSourcePath(info.source_dir); + updateSettings(true); +} + +std::unordered_set ModdedApplication::getModConflicts(int deployer, int mod_id) +{ + ProgressNode node(progress_callback_); + return deployers_[deployer]->getModConflicts(mod_id, &node); +} + +void ModdedApplication::setProfile(int profile) +{ + if(profile < 0 || profile >= profile_names_.size()) + return; + bak_man_.setProfile(profile); + for(const auto& deployer : deployers_) + deployer->setProfile(profile); + current_profile_ = profile; +} + +void ModdedApplication::addProfile(const EditProfileInfo& info) +{ + profile_names_.push_back(info.name); + app_versions_.push_back(info.app_version); + for(const auto& deployer : deployers_) + deployer->addProfile(info.source); + bak_man_.addProfile(info.source); + updateSettings(true); +} + +void ModdedApplication::removeProfile(int profile) +{ + if(profile < 0 || profile >= profile_names_.size()) + return; + for(const auto& deployer : deployers_) + deployer->removeProfile(profile); + profile_names_.erase(profile_names_.begin() + profile); + app_versions_.erase(app_versions_.begin() + profile); + bak_man_.removeProfile(profile); + if(profile == current_profile_) + setProfile(0); + updateSettings(true); +} + +std::vector ModdedApplication::getProfileNames() const +{ + return profile_names_; +} + +void ModdedApplication::editProfile(int profile, const EditProfileInfo& info) +{ + if(profile < 0 || profile >= profile_names_.size()) + return; + profile_names_[profile] = info.name; + app_versions_[profile] = info.app_version; + updateSettings(true); +} + +void ModdedApplication::editTool(int tool, std::string name, std::string command) +{ + if(tool >= 0 && tool < tools_.size()) + { + std::get<0>(tools_[tool]) = name; + std::get<1>(tools_[tool]) = command; + } + updateSettings(true); +} + +std::tuple ModdedApplication::verifyDeployerDirectories() +{ + std::tuple ret{ 0, "" }; + for(const auto& depl : deployers_) + { + int cur_code = depl->verifyDirectories(); + if(cur_code) + { + ret = std::tuple{ cur_code, depl->destPath() }; + } + } + return ret; +} + +void ModdedApplication::addModToGroup(int mod_id, + int group, + std::optional progress_node) +{ + if(group < 0 || group >= groups_.size() || group_map_.contains(mod_id)) + return; + groups_[group].push_back(mod_id); + group_map_[mod_id] = group; + active_group_members_[group] = mod_id; + ProgressNode node(progress_callback_); + updateDeployerGroups(progress_node ? progress_node : &node); + updateSettings(true); +} + +void ModdedApplication::removeModFromGroup(int mod_id, + bool update_conflicts, + std::optional progress_node) +{ + if(!group_map_.contains(mod_id)) + return; + int group = group_map_[mod_id]; + groups_[group].erase(std::find(groups_[group].begin(), groups_[group].end(), mod_id)); + + if(!groups_[group].empty()) + { + active_group_members_[group] = groups_[group][0]; + std::vector> update_targets; + std::vector weights; + for(int depl = 0; depl < deployers_.size(); depl++) + { + update_targets.push_back({}); + if(deployers_[depl]->isAutonomous()) + continue; + for(int prof = 0; prof < profile_names_.size(); prof++) + { + deployers_[depl]->setProfile(prof); + auto loadorder = deployers_[depl]->getLoadorder(); + auto iter = str::find_if( + loadorder, [mod_id](const auto& tuple) { return std::get<0>(tuple) == mod_id; }); + if(iter != loadorder.end()) + { + deployers_[depl]->addMod(active_group_members_[group], std::get<1>(*iter), false); + deployers_[depl]->changeLoadorder(loadorder.size(), iter - loadorder.begin()); + update_targets[depl].push_back(prof); + weights.push_back(loadorder.size()); + } + } + deployers_[depl]->setProfile(current_profile_); + } + + ProgressNode node = progress_node ? **progress_node : ProgressNode(progress_callback_); + if(!update_conflicts) + { + node.setTotalSteps(1); + node.advance(); + } + else + { + node.addChildren(weights); + int i = 0; + for(int depl = 0; depl < update_targets.size(); depl++) + { + for(int prof : update_targets[depl]) + { + deployers_[depl]->setProfile(prof); + deployers_[depl]->updateConflictGroups(&node.child(i)); + i++; + } + deployers_[depl]->setProfile(current_profile_); + } + } + } + + if(groups_[group].size() == 1) + group_map_.erase(groups_[group][0]); + if(groups_[group].size() < 2) + { + groups_.erase(groups_.begin() + group); + active_group_members_.erase(active_group_members_.begin() + group); + for(auto& pair : group_map_) + { + if(pair.second > group) + pair.second--; + } + } + group_map_.erase(mod_id); + updateSettings(true); +} + +void ModdedApplication::createGroup(int first_mod_id, + int second_mod_id, + std::optional progress_node) +{ + if(group_map_.contains(first_mod_id)) + { + addModToGroup(second_mod_id, group_map_[first_mod_id]); + return; + } + if(group_map_.contains(second_mod_id)) + { + addModToGroup(first_mod_id, group_map_[second_mod_id]); + return; + } + groups_.push_back({ first_mod_id, second_mod_id }); + int group = groups_.size() - 1; + group_map_[first_mod_id] = group; + group_map_[second_mod_id] = group; + active_group_members_.push_back(first_mod_id); + ProgressNode node(progress_callback_); + updateDeployerGroups(progress_node ? progress_node : &node); + updateSettings(true); +} + +void ModdedApplication::changeActiveGroupMember(int group, + int mod_id, + std::optional progress_node) +{ + if(group < 0 || group >= groups_.size() || + std::find(groups_[group].begin(), groups_[group].end(), mod_id) == groups_[group].end()) + return; + active_group_members_[group] = mod_id; + ProgressNode node(progress_callback_); + updateDeployerGroups(progress_node ? progress_node : &node); + updateSettings(true); +} + +void ModdedApplication::changeModVersion(int mod_id, const std::string& new_version) +{ + auto iter = std::find_if( + installed_mods_.begin(), installed_mods_.end(), [mod_id](Mod m) { return m.id == mod_id; }); + if(iter == installed_mods_.end()) + throw std::runtime_error("Error: Unknown mod id: " + std::to_string(mod_id)); + iter->version = new_version; + updateSettings(true); +} + +int ModdedApplication::getNumGroups() +{ + return groups_.size(); +} + +bool ModdedApplication::modHasGroup(int mod_id) +{ + return group_map_.contains(mod_id); +} + +int ModdedApplication::getModGroup(int mod_id) +{ + if(!group_map_.contains(mod_id)) + return -1; + return group_map_[mod_id]; +} + +void ModdedApplication::sortModsByConflicts(int deployer) +{ + ProgressNode node(progress_callback_); + deployers_[deployer]->sortModsByConflicts(&node); + updateSettings(true); +} + +std::vector> ModdedApplication::getConflictGroups(int deployer) +{ + return deployers_[deployer]->getConflictGroups(); +} + +void ModdedApplication::updateModDeployers(const std::vector& mod_ids, + const std::vector& deployers) +{ + std::vector weights; + for(const auto& depl : deployers_) + weights.push_back(depl->isAutonomous() ? 1 : depl->getNumMods()); + ProgressNode node(progress_callback_, weights); + std::optional dummy_node{}; + for(int i = 0; i < mod_ids.size(); i++) + { + const int mod_id = mod_ids[i]; + const bool is_last_mod = i == (mod_ids.size() - 1); + for(int depl = 0; depl < deployers.size(); depl++) + { + if(deployers_[depl]->isAutonomous()) + continue; + if(deployers[depl]) + addModToDeployer(depl, mod_id, is_last_mod, is_last_mod ? &node.child(depl) : dummy_node); + else + removeModFromDeployer( + depl, mod_id, is_last_mod, is_last_mod ? &node.child(depl) : dummy_node); + } + } +} + +int ModdedApplication::verifyStagingDir(sfs::path staging_dir) +{ + try + { + Json::Value val; + std::ifstream file(staging_dir / CONFIG_FILE_NAME, std::fstream::binary); + if(file.is_open()) + file >> val; + file.close(); + } + catch(std::ios_base::failure& f) + { + return 1; + } + catch(Json::RuntimeError& e) + { + return 2; + } + return 0; +} + +void ModdedApplication::extractArchive(const sfs::path& source, const sfs::path& target) +{ + ProgressNode node(progress_callback_); + Installer::extract(source, target, &node); +} + +DeployerInfo ModdedApplication::getDeployerInfo(int deployer) +{ + if(!(deployers_[deployer]->isAutonomous())) + { + std::map mods_per_tag; + for(const auto& tag : manual_tags_) + mods_per_tag[tag.getName()] = tag.getNumMods(); + + const auto loadorder = deployers_[deployer]->getLoadorder(); + std::vector mod_names; + mod_names.reserve(loadorder.size()); + std::vector> manual_tags; + manual_tags.reserve(loadorder.size()); + std::vector> auto_tags; + manual_tags.reserve(loadorder.size()); + for(const auto& [id, e] : loadorder) + { + mod_names.push_back( + std::ranges::find_if(installed_mods_, [id = id](auto& mod) { return mod.id == id; })->name); + if(manual_tag_map_.contains(id)) + manual_tags.push_back(manual_tag_map_.at(id)); + else + manual_tags.push_back({}); + + if(auto_tag_map_.contains(id)) + auto_tags.push_back(auto_tag_map_.at(id)); + else + auto_tags.push_back({}); + } + for(const auto& tag : auto_tags_) + { + if(mods_per_tag.contains(tag.getName())) + mods_per_tag[tag.getName()] += tag.getNumMods(); + else + mods_per_tag[tag.getName()] = tag.getNumMods(); + } + return { mod_names, loadorder, deployers_[deployer]->getConflictGroups(), false, manual_tags, + auto_tags, mods_per_tag }; + } + else + { + return { deployers_[deployer]->getModNames(), + deployers_[deployer]->getLoadorder(), + deployers_[deployer]->getConflictGroups(), + true, + {}, + deployers_[deployer]->getAutoTags(), + deployers_[deployer]->getAutoTagMap() }; + } +} + +void ModdedApplication::setLog(const std::function& newLog) +{ + log_ = newLog; + for(auto& deployer : deployers_) + deployer->setLog(newLog); +} + +void ModdedApplication::addBackupTarget(const sfs::path& path, + const std::string& name, + const std::vector& backup_names) +{ + bak_man_.addTarget(path, name, backup_names); + updateSettings(true); +} + +void ModdedApplication::removeBackupTarget(int target_id) +{ + if(target_id < 0 || target_id >= bak_man_.getNumTargets()) + return; + bak_man_.removeTarget(target_id); + updateSettings(true); +} + +void ModdedApplication::removeAllBackupTargets() +{ + for(int target = 0; target < bak_man_.getNumTargets(); target++) + removeBackupTarget(target); +} + +void ModdedApplication::addBackup(int target_id, const std::string& name, int source) +{ + if(target_id < 0 || target_id >= bak_man_.getNumTargets()) + return; + bak_man_.addBackup(target_id, name, source); +} + +void ModdedApplication::removeBackup(int target_id, int backup_id) +{ + if(target_id < 0 || target_id >= bak_man_.getNumTargets() || backup_id < 0 || + backup_id >= bak_man_.getNumBackups(target_id)) + return; + bak_man_.removeBackup(target_id, backup_id); +} + +void ModdedApplication::setActiveBackup(int target_id, int backup_id) +{ + if(target_id < 0 || target_id >= bak_man_.getNumTargets() || backup_id < 0 || + backup_id >= bak_man_.getNumBackups(target_id)) + return; + bak_man_.setActiveBackup(target_id, backup_id); +} + +std::vector ModdedApplication::getBackupTargets() const +{ + return bak_man_.getTargets(); +} + +void ModdedApplication::setBackupName(int target_id, int backup_id, const std::string& name) +{ + if(target_id < 0 || target_id >= bak_man_.getNumTargets() || backup_id < 0 || + backup_id >= bak_man_.getNumBackups(target_id)) + return; + bak_man_.setBackupName(target_id, backup_id, name); +} + +void ModdedApplication::setBackupTargetName(int target_id, const std::string& name) +{ + if(target_id < 0 || target_id >= bak_man_.getNumTargets()) + return; + bak_man_.setBackupTargetName(target_id, name); +} + +void ModdedApplication::overwriteBackup(int target_id, int source_backup, int dest_backup) +{ + if(target_id < 0 || target_id >= bak_man_.getNumTargets()) + return; + bak_man_.overwriteBackup(target_id, source_backup, dest_backup); +} + +void ModdedApplication::cleanupFailedInstallation() +{ + Installer::cleanupFailedInstallation(staging_dir_, last_mod_id_); + auto iter = std::find_if(installed_mods_.begin(), + installed_mods_.end(), + [this](const Mod& m) { return m.id == this->last_mod_id_; }); + if(iter != installed_mods_.end()) + uninstallMods({ last_mod_id_ }); + last_mod_id_ = -1; +} + +void ModdedApplication::setProgressCallback(const std::function& progress_callback) +{ + progress_callback_ = progress_callback; +} + +void ModdedApplication::uninstallGroupMembers(const std::vector& mod_ids) +{ + std::vector uninstall_targets; + for(int active_id : mod_ids) + { + if(!group_map_.contains(active_id)) + continue; + for(int mod_id : groups_[group_map_[active_id]]) + { + if(mod_id != active_id) + uninstall_targets.push_back(mod_id); + } + } + uninstallMods(uninstall_targets); +} + +void ModdedApplication::addManualTag(const std::string& tag_name) +{ + if(str::find(manual_tags_, tag_name) != manual_tags_.end()) + throw std::runtime_error( + std::format("Error: A tag with the name '{}' already exists.", tag_name)); + manual_tags_.emplace_back(tag_name); + updateSettings(true); +} + +void ModdedApplication::removeManualTag(const std::string& tag_name, bool update_map) +{ + auto iter = str::find(manual_tags_, tag_name); + if(iter != manual_tags_.end()) + manual_tags_.erase(iter); + if(update_map) + updateManualTagMap(); + updateSettings(true); +} + +void ModdedApplication::changeManualTagName(const std::string& old_name, + const std::string& new_name, + bool update_map) +{ + auto old_iter = str::find(manual_tags_, old_name); + if(old_iter == manual_tags_.end()) + return; + auto new_iter = str::find(manual_tags_, new_name); + if(new_iter != manual_tags_.end()) + throw std::runtime_error( + std::format("Error: Cannot rename tag '{}', because a tag with the name '{}' already exists.", + old_name, + new_name)); + old_iter->setName(new_name); + if(update_map) + updateManualTagMap(); + updateSettings(true); +} + +void ModdedApplication::addTagsToMods(const std::vector& tag_names, + const std::vector& mod_ids) +{ + for(const auto& tag_name : tag_names) + { + auto tag = str::find(manual_tags_, tag_name); + if(tag == manual_tags_.end()) + return; + for(int mod : mod_ids) + tag->addMod(mod); + } + updateManualTagMap(); + updateSettings(true); +} + +void ModdedApplication::removeTagsFromMods(const std::vector& tag_names, + const std::vector& mod_ids) +{ + for(const auto& tag_name : tag_names) + { + auto tag = str::find(manual_tags_, tag_name); + if(tag == manual_tags_.end()) + return; + for(int mod : mod_ids) + tag->removeMod(mod); + } + updateManualTagMap(); + updateSettings(true); +} + +void ModdedApplication::setTagsForMods(const std::vector& tag_names, + const std::vector mod_ids) +{ + for(auto& tag : manual_tags_) + { + if(str::find(tag_names, tag) != tag_names.end()) + { + for(int mod : mod_ids) + tag.addMod(mod); + } + else + { + for(int mod : mod_ids) + tag.removeMod(mod); + } + } + updateManualTagMap(); + updateSettings(true); +} + +void ModdedApplication::editManualTags(const std::vector& actions) +{ + auto old_tags = manual_tags_; + try + { + for(const auto& action : actions) + { + if(action.getType() == EditManualTagAction::ActionType::add) + addManualTag(action.getName()); + else if(action.getType() == EditManualTagAction::ActionType::remove) + removeManualTag(action.getName(), false); + else if(action.getType() == EditManualTagAction::ActionType::rename) + changeManualTagName(action.getName(), action.getNewName(), false); + } + } + catch(std::runtime_error& e) + { + manual_tags_ = old_tags; + throw e; + } + updateManualTagMap(); + updateSettings(true); +} + +void ModdedApplication::addAutoTag(const std::string& tag_name, + const std::string& expression, + const std::vector& conditions, + bool update) +{ + if(std::find(auto_tags_.begin(), auto_tags_.end(), tag_name) != auto_tags_.end()) + throw std::runtime_error( + std::format("Error: A tag with the name '{}' already exists.", tag_name)); + + auto_tags_.emplace_back(tag_name, expression, conditions); + auto select_id = [](const auto& mod) { return mod.id; }; + if(expression != "") + auto_tags_.back().reapplyMods(staging_dir_, str::transform_view(installed_mods_, select_id)); + if(update) + { + updateAutoTagMap(); + updateSettings(true); + } +} + +void ModdedApplication::removeAutoTag(const std::string& tag_name, bool update) +{ + auto iter = std::find(auto_tags_.begin(), auto_tags_.end(), tag_name); + if(iter == auto_tags_.end()) + return; + auto_tags_.erase(iter); + if(update) + { + updateAutoTagMap(); + updateSettings(true); + } +} + +void ModdedApplication::renameAutoTag(const std::string& old_name, + const std::string& new_name, + bool update) +{ + auto iter = std::find(auto_tags_.begin(), auto_tags_.end(), old_name); + if(iter == auto_tags_.end()) + return; + if(std::find(auto_tags_.begin(), auto_tags_.end(), new_name) != auto_tags_.end()) + throw std::runtime_error( + std::format("Error: Cannot rename tag '{}', because a tag with the name '{}' already exists.", + old_name, + new_name)); + + iter->setName(new_name); + if(update) + { + updateAutoTagMap(); + updateSettings(true); + } +} + +void ModdedApplication::changeAutoTagEvaluator(const std::string& tag_name, + const std::string& expression, + const std::vector& conditions, + bool update) +{ + auto iter = std::find(auto_tags_.begin(), auto_tags_.end(), tag_name); + if(iter == auto_tags_.end()) + return; + + iter->setEvaluator(expression, conditions); + auto select_id = [](const auto& mod) { return mod.id; }; + if(update) + { + iter->reapplyMods(staging_dir_, str::transform_view(installed_mods_, select_id)); + updateAutoTagMap(); + updateSettings(true); + } +} + +void ModdedApplication::editAutoTags(const std::vector& actions) +{ + auto old_tags = auto_tags_; + try + { + std::vector reapply_targets; + for(const auto& action : actions) + { + if(action.getType() == EditAutoTagAction::ActionType::add) + addAutoTag(action.getName(), action.getExpression(), action.getConditions(), false); + else if(action.getType() == EditAutoTagAction::ActionType::remove) + removeAutoTag(action.getName(), false); + else if(action.getType() == EditAutoTagAction::ActionType::rename) + renameAutoTag(action.getName(), action.getNewName(), false); + else if(action.getType() == EditAutoTagAction::ActionType::change_evaluator) + { + changeAutoTagEvaluator( + action.getName(), action.getExpression(), action.getConditions(), false); + reapply_targets.push_back(action.getName()); + } + } + if(!reapply_targets.empty()) + { + log_(Log::LOG_INFO, "Reapplying auto tags with edited conditions to all mods..."); + ProgressNode node(progress_callback_); + node.addChildren({ 1.0f, std::min(8.0f, (float)reapply_targets.size()) }); + node.child(0).setTotalSteps(installed_mods_.size()); + std::vector weights; + for(const auto& tag : reapply_targets) + { + auto iter = std::find(auto_tags_.begin(), auto_tags_.end(), tag); + if(iter != auto_tags_.end()) + weights.push_back(iter->getNumConditions()); + } + node.child(1).addChildren(weights); + for(int i = 0; i < weights.size(); i++) + node.child(1).child(i).setTotalSteps(installed_mods_.size()); + + auto select_id = [](const auto& mod) { return mod.id; }; + auto mods = str::transform_view(installed_mods_, select_id); + const auto files = AutoTag::readModFiles(staging_dir_, mods, &node.child(0)); + for(int i = 0; i < reapply_targets.size(); i++) + { + auto iter = std::find(auto_tags_.begin(), auto_tags_.end(), reapply_targets[i]); + if(iter != auto_tags_.end()) + iter->reapplyMods(files, mods, &node.child(1).child(i)); + } + } + } + catch(std::runtime_error& e) + { + auto_tags_ = old_tags; + throw e; + } + updateAutoTagMap(); + updateSettings(true); +} + +void ModdedApplication::reapplyAutoTags() +{ + log_(Log::LOG_INFO, "Reapplying auto tags to all mods..."); + ProgressNode node(progress_callback_); + node.addChildren({ 1.0f, 8.0f }); + node.child(0).setTotalSteps(installed_mods_.size()); + std::vector weights; + for(auto& tag : auto_tags_) + weights.push_back(tag.getNumConditions()); + node.child(1).addChildren(weights); + for(int i = 0; i < weights.size(); i++) + node.child(1).child(i).setTotalSteps(installed_mods_.size()); + auto select_id = [](const auto& mod) { return mod.id; }; + auto mods = str::transform_view(installed_mods_, select_id); + const auto files = AutoTag::readModFiles(staging_dir_, mods, &node.child(0)); + for(int i = 0; i < auto_tags_.size(); i++) + auto_tags_[i].reapplyMods(files, mods, &node.child(1).child(i)); + updateAutoTagMap(); + updateSettings(true); +} + +void ModdedApplication::updateAutoTags(const std::vector mod_ids) +{ + log_(Log::LOG_INFO, std::format("Reapplying auto tags to {} mods...", mod_ids.size())); + ProgressNode node(progress_callback_); + node.addChildren( + { 1.0f, std::max(1.0f, 8.0f * (float)mod_ids.size() / (float)installed_mods_.size()) }); + node.child(0).setTotalSteps(mod_ids.size()); + std::vector weights; + for(auto& tag : auto_tags_) + weights.push_back(tag.getNumConditions()); + node.child(1).addChildren(weights); + for(int i = 0; i < weights.size(); i++) + node.child(1).child(i).setTotalSteps(mod_ids.size()); + const auto files = AutoTag::readModFiles(staging_dir_, mod_ids, &node.child(0)); + for(int i = 0; i < auto_tags_.size(); i++) + auto_tags_[i].updateMods(files, mod_ids, &node.child(1).child(i)); + updateAutoTagMap(); + updateSettings(true); +} + +void ModdedApplication::deleteAllData() +{ + for(int i = 0; i < deployers_.size(); i++) + removeDeployer(i, true); + for(auto mod : installed_mods_) + { + const auto path = staging_dir_ / std::to_string(mod.id); + if(sfs::exists(path)) + sfs::remove_all(path); + } + const auto path = staging_dir_ / CONFIG_FILE_NAME; + if(sfs::exists(path)) + sfs::remove(path); + + if(sfs::exists(staging_dir_ / download_dir_)) + sfs::remove_all(staging_dir_ / download_dir_); +} + +void ModdedApplication::setAppVersion(const std::string& app_version) +{ + app_versions_[current_profile_] = app_version; + updateSettings(true); +} + +void ModdedApplication::setModSources(int mod_id, + const std::string& local_source, + const std::string& remote_source) +{ + auto iter = std::find_if( + installed_mods_.begin(), installed_mods_.end(), [mod_id](Mod m) { return m.id == mod_id; }); + if(iter == installed_mods_.end()) + throw std::runtime_error("Error: Unknown mod id: " + std::to_string(mod_id)); + iter->local_source = local_source; + iter->remote_source = remote_source; + updateSettings(true); +} + +nexus::Page ModdedApplication::getNexusPage(int mod_id) +{ + auto iter = std::find_if( + installed_mods_.begin(), installed_mods_.end(), [mod_id](Mod m) { return m.id == mod_id; }); + if(iter == installed_mods_.end()) + throw std::runtime_error("Error: Unknown mod id: " + std::to_string(mod_id)); + return nexus::Api::getNexusPage(iter->remote_source); +} + +void ModdedApplication::checkForModUpdates() +{ + std::vector target_mod_indices; + for(const auto& [i, mod] : str::enumerate_view(installed_mods_)) + { + if(nexus::Api::modUrlIsValid(mod.remote_source) && mod.remote_update_time <= mod.install_time) + target_mod_indices.push_back(i); + } + performUpdateCheck(target_mod_indices); +} + +void ModdedApplication::checkModsForUpdates(const std::vector& mod_ids) +{ + std::vector target_mod_indices; + for(const auto& [i, mod] : str::enumerate_view(installed_mods_)) + { + if(str::find(mod_ids, mod.id) != mod_ids.end() && + nexus::Api::modUrlIsValid(mod.remote_source) && mod.remote_update_time <= mod.install_time) + target_mod_indices.push_back(i); + } + performUpdateCheck(target_mod_indices); +} + +void ModdedApplication::suppressUpdateNotification(const std::vector& mod_ids) +{ + for(int mod_id : mod_ids) + { + auto iter = std::find_if(installed_mods_.begin(), + installed_mods_.end(), + [mod_id](const Mod& mod) { return mod.id == mod_id; }); + if(iter != installed_mods_.end() && iter->remote_update_time > iter->install_time) + iter->suppress_update_time = + std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + } + updateSettings(true); +} + +std::string ModdedApplication::getDownloadUrl(const std::string& nxm_url) +{ + return nexus::Api::getDownloadUrl(nxm_url); +} + +std::string ModdedApplication::getDownloadUrlForFile(int nexus_file_id, const std::string& mod_url) +{ + return nexus::Api::getDownloadUrl(mod_url, nexus_file_id); +} + +std::string ModdedApplication::getNexusPageUrl(const std::string& nxm_url) +{ + return nexus::Api::getNexusPageUrl(nxm_url); +} + +std::string ModdedApplication::downloadMod(const std::string& url, + std::function progress_callback) +{ + log_(Log::LOG_DEBUG, "Download URL: " + url); + std::regex url_regex(R"(.*/(.*)\?.*)"); + std::smatch match; + if(!std::regex_match(url, match, url_regex)) + throw std::runtime_error(std::format("Invalid download URL \"{}\"", url)); + sfs::path download_path = staging_dir_ / download_dir_; + if(!sfs::exists(download_path)) + sfs::create_directories(download_path); + sfs::path file_name = match[1].str(); + const std::string file_name_prefix = file_name.stem(); + const std::string extension = file_name.extension(); + int suffix = 1; + while(sfs::exists(download_path / file_name)) + { + file_name = file_name_prefix + "(" + std::to_string(suffix) + ")" + extension; + suffix++; + } + std::string file_name_str = file_name.string(); + auto pos = file_name_str.find("%20"); + while(pos != std::string::npos) + { + file_name_str.replace(pos, 3, " "); + pos = file_name_str.find("%20"); + } + file_name = file_name_str; + + std::ofstream fstream(download_path / file_name, std::ios::binary); + if(!fstream.is_open()) + throw std::runtime_error("Failed to write to disk."); + bool message_sent = false; + cpr::Response response = cpr::Download( + fstream, + cpr::Url(url), + cpr::ProgressCallback( + [app = this, &message_sent, &file_name, progress_callback](auto download_total, + auto download_now, + auto upload_total, + auto upload_now, + intptr_t user_data) + { + if(!message_sent && download_total > 0) + { + std::string size_string; + long last_size = 0; + long size = download_total; + int exp = 0; + const std::vector units{ "B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB" }; + while(size > 1024 && exp < units.size()) + { + last_size = size; + size /= 1024; + exp++; + } + last_size /= 1.024; + size_string = std::to_string(size); + const int first_digit = (last_size / 100) % 10; + const int second_digit = (last_size / 10) % 10; + if(first_digit != 0 || second_digit != 0) + size_string += "." + std::to_string(first_digit); + if(second_digit != 0) + size_string += std::to_string(second_digit); + size_string += units[exp]; + + app->log_(Log::LOG_INFO, + ("Downloading \"" + file_name.string() + "\" with size: ").c_str() + + size_string + "..."); + message_sent = true; + } + if(download_total != 0) + progress_callback((float)download_now / (float)download_total); + return true; + })); + if(response.status_code != 200) + { + if(sfs::exists(download_path / file_name)) + sfs::remove(download_path / file_name); + throw std::runtime_error("Download failed with response: \"" + response.status_line + + "\" (code " + std::to_string(response.status_code) + ")."); + } + fstream.close(); + return (download_path / file_name).string(); +} + +sfs::path ModdedApplication::iconPath() const +{ + return icon_path_; +} + +void ModdedApplication::setIconPath(const sfs::path& icon_path) +{ + icon_path_ = icon_path; + updateSettings(true); +} + +void ModdedApplication::updateSettings(bool write) +{ + json_settings_.clear(); + json_settings_["name"] = name_; + json_settings_["command"] = command_; + json_settings_["icon_path"] = icon_path_.string(); + for(int group = 0; group < groups_.size(); group++) + { + json_settings_["groups"][group]["active_member"] = active_group_members_[group]; + for(int i = 0; i < groups_[group].size(); i++) + { + json_settings_["groups"][group]["members"][i] = groups_[group][i]; + } + } + + for(int i = 0; i < profile_names_.size(); i++) + json_settings_["profiles"][i]["name"] = profile_names_[i]; + + for(int i = 0; i < app_versions_.size(); i++) + json_settings_["profiles"][i]["app_version"] = app_versions_[i]; + + for(int i = 0; i < installed_mods_.size(); i++) + { + json_settings_["installed_mods"][i]["id"] = installed_mods_[i].id; + json_settings_["installed_mods"][i]["name"] = installed_mods_[i].name; + json_settings_["installed_mods"][i]["version"] = installed_mods_[i].version; + json_settings_["installed_mods"][i]["installer"] = installer_map_[installed_mods_[i].id]; + json_settings_["installed_mods"][i]["install_time"] = installed_mods_[i].install_time; + json_settings_["installed_mods"][i]["local_source"] = installed_mods_[i].local_source.string(); + json_settings_["installed_mods"][i]["remote_source"] = installed_mods_[i].remote_source; + json_settings_["installed_mods"][i]["remote_update_time"] = + installed_mods_[i].remote_update_time; + json_settings_["installed_mods"][i]["size_on_disk"] = installed_mods_[i].size_on_disk; + json_settings_["installed_mods"][i]["suppress_update_time"] = + installed_mods_[i].suppress_update_time; + } + + for(int depl = 0; depl < deployers_.size(); depl++) + { + json_settings_["deployers"][depl]["dest_path"] = deployers_[depl]->getDestPath(); + json_settings_["deployers"][depl]["source_path"] = deployers_[depl]->sourcePath().string(); + json_settings_["deployers"][depl]["name"] = deployers_[depl]->getName(); + json_settings_["deployers"][depl]["type"] = deployers_[depl]->getType(); + json_settings_["deployers"][depl]["use_copy_deployment"] = + deployers_[depl]->usesCopyDeployment(); + + if(!deployers_[depl]->isAutonomous()) + { + for(int prof = 0; prof < profile_names_.size(); prof++) + { + deployers_[depl]->setProfile(prof); + json_settings_["deployers"][depl]["profiles"][prof]["name"] = profile_names_[prof]; + auto loadorder = deployers_[depl]->getLoadorder(); + for(int mod = 0; mod < loadorder.size(); mod++) + { + json_settings_["deployers"][depl]["profiles"][prof]["loadorder"][mod]["id"] = + std::get<0>(loadorder[mod]); + json_settings_["deployers"][depl]["profiles"][prof]["loadorder"][mod]["enabled"] = + std::get<1>(loadorder[mod]); + } + auto conflict_groups = deployers_[depl]->getConflictGroups(); + for(int group = 0; group < conflict_groups.size(); group++) + { + for(int i = 0; i < conflict_groups[group].size(); i++) + json_settings_["deployers"][depl]["profiles"][prof]["conflict_groups"][group][i] = + conflict_groups[group][i]; + } + } + } + deployers_[depl]->setProfile(current_profile_); + } + + for(int tool = 0; tool < tools_.size(); tool++) + { + json_settings_["tools"][tool]["name"] = std::get<0>(tools_[tool]); + json_settings_["tools"][tool]["command"] = std::get<1>(tools_[tool]); + } + + const auto targets = bak_man_.getTargets(); + for(int i = 0; i < targets.size(); i++) + json_settings_["backup_targets"][i]["path"] = targets[i].path.string(); + + for(int i = 0; i < manual_tags_.size(); i++) + json_settings_["manual_tags"][i] = manual_tags_[i].toJson(); + + for(int i = 0; i < auto_tags_.size(); i++) + { + if(!auto_tags_[i].getExpression().empty()) + json_settings_["auto_tags"][i] = auto_tags_[i].toJson(); + } + + if(write) + writeSettings(); +} + +void ModdedApplication::writeSettings() const +{ + sfs::path settings_file_path = staging_dir_ / (CONFIG_FILE_NAME + ".tmp"); + std::ofstream file(settings_file_path, std::fstream::binary); + if(!file.is_open()) + throw std::runtime_error("Error: Could not write to \"" + settings_file_path.string() + "\"."); + file << json_settings_; + file.close(); + sfs::rename(settings_file_path, staging_dir_ / CONFIG_FILE_NAME); +} + +void ModdedApplication::readSettings() +{ + json_settings_.clear(); + sfs::path settings_file_path = staging_dir_ / CONFIG_FILE_NAME; + std::ifstream file(settings_file_path, std::fstream::binary); + if(!file.is_open()) + throw std::runtime_error("Error: Could not read from \"" + settings_file_path.string() + "\"."); + file >> json_settings_; + file.close(); +} + +void ModdedApplication::updateState(bool read) +{ + installed_mods_.clear(); + deployers_.clear(); + groups_.clear(); + group_map_.clear(); + active_group_members_.clear(); + profile_names_.clear(); + bak_man_.reset(); + tools_.clear(); + profile_names_.clear(); + app_versions_.clear(); + manual_tags_.clear(); + manual_tag_map_.clear(); + auto_tags_.clear(); + auto_tag_map_.clear(); + installer_map_.clear(); + + if(read) + { + if(!sfs::exists(staging_dir_ / CONFIG_FILE_NAME)) + return; + readSettings(); + } + + if(!json_settings_.isMember("name")) + throw ParseError("Name is missing in \"" + (staging_dir_ / CONFIG_FILE_NAME).string() + "\""); + name_ = json_settings_["name"].asString(); + + if(!json_settings_.isMember("command")) + throw ParseError("Command is missing in \"" + (staging_dir_ / CONFIG_FILE_NAME).string() + + "\""); + command_ = json_settings_["command"].asString(); + + if(!json_settings_.isMember("icon_path")) + throw ParseError("Icon path is missing in \"" + (staging_dir_ / CONFIG_FILE_NAME).string() + + "\""); + icon_path_ = json_settings_["icon_path"].asString(); + + if(!json_settings_.isMember("profiles")) + throw ParseError("Profiles are missing in \"" + (staging_dir_ / CONFIG_FILE_NAME).string() + + "\""); + + Json::Value profiles = json_settings_["profiles"]; + for(int i = 0; i < profiles.size(); i++) + { + profile_names_.push_back(profiles[i]["name"].asString()); + app_versions_.push_back(profiles[i]["app_version"].asString()); + } + + Json::Value installed_mods = json_settings_["installed_mods"]; + for(int i = 0; i < installed_mods.size(); i++) + { + installed_mods_.emplace_back(installed_mods[i]["id"].asInt(), + installed_mods[i]["name"].asString(), + installed_mods[i]["version"].asString(), + installed_mods[i]["install_time"].asInt64(), + installed_mods[i]["local_source"].asString(), + installed_mods[i]["remote_source"].asString(), + installed_mods[i]["remote_update_time"].asInt64(), + installed_mods[i]["size_on_disk"].asInt64(), + installed_mods[i]["suppress_update_time"].asInt64()); + std::string installer = installed_mods[i]["installer"].asString(); + std::vector types = Installer::INSTALLER_TYPES; + if(std::find(types.begin(), types.end(), installer) == types.end()) + throw ParseError("Unknown installer type: " + installer + " in \"" + + (staging_dir_ / CONFIG_FILE_NAME).string() + "\""); + installer_map_[installed_mods[i]["id"].asInt()] = installer; + } + Json::Value groups = json_settings_["groups"]; + for(int group = 0; group < groups.size(); group++) + { + groups_.push_back(std::vector{}); + for(int i = 0; i < groups[group]["members"].size(); i++) + { + int mod_id = groups[group]["members"][i].asInt(); + if(std::find_if(installed_mods_.begin(), + installed_mods_.end(), + [mod_id](const Mod& m) { return m.id == mod_id; }) == installed_mods_.end()) + throw ParseError("Unknown mod id in group " + std::to_string(group) + ": " + + std::to_string(mod_id) + " in \"" + + (staging_dir_ / CONFIG_FILE_NAME).string() + "\""); + if(std::find(groups_[group].begin(), groups_[group].end(), mod_id) != groups_[group].end()) + throw ParseError("Duplicate mod id in group " + std::to_string(group) + ": " + + std::to_string(mod_id) + " in \"" + + (staging_dir_ / CONFIG_FILE_NAME).string() + "\""); + group_map_[mod_id] = group; + groups_[group].push_back(mod_id); + } + int active_member = groups[group]["active_member"].asInt(); + if(std::find(groups_[group].begin(), groups_[group].end(), active_member) == + groups_[group].end() || + !groups[group].isMember("active_member")) + throw ParseError("Invalid active group member: " + std::to_string(active_member) + " in \"" + + (staging_dir_ / CONFIG_FILE_NAME).string() + "\""); + active_group_members_.push_back(groups[group]["active_member"].asInt()); + } + Json::Value deployers = json_settings_["deployers"]; + for(int depl = 0; depl < deployers.size(); depl++) + { + std::vector types = DeployerFactory::DEPLOYER_TYPES; + std::string type = deployers[depl]["type"].asString(); + if(std::find(types.begin(), types.end(), type) == types.end()) + throw ParseError("Unknown deployer type: " + type + " in \"" + + (staging_dir_ / CONFIG_FILE_NAME).string() + "\""); + deployers_.push_back( + DeployerFactory::makeDeployer(type, + sfs::path(deployers[depl]["source_path"].asString()), + sfs::path(deployers[depl]["dest_path"].asString()), + deployers[depl]["name"].asString(), + deployers[depl]["use_copy_deployment"].asBool())); + if(!deployers_[depl]->isAutonomous()) + { + for(int prof = 0; prof < profile_names_.size(); prof++) + { + deployers_[depl]->addProfile(); + deployers_[depl]->setProfile(prof); + Json::Value loadorder = deployers[depl]["profiles"][prof]["loadorder"]; + for(int mod = 0; mod < loadorder.size(); mod++) + { + int mod_id = loadorder[mod]["id"].asInt(); + if(std::find_if(installed_mods_.begin(), + installed_mods_.end(), + [mod_id](const Mod& m) + { return m.id == mod_id; }) == installed_mods_.end()) + throw ParseError("Unknown mod id in deployers: " + std::to_string(mod_id) + " in \"" + + (staging_dir_ / CONFIG_FILE_NAME).string() + "\""); + if(!group_map_.contains(mod_id) || active_group_members_[group_map_[mod_id]] == mod_id && + !(deployers_[depl]->isAutonomous())) + deployers_[depl]->addMod(mod_id, loadorder[mod]["enabled"].asBool(), false); + } + Json::Value conflict_groups_json = deployers[depl]["profiles"][prof]["conflict_groups"]; + std::vector> conflict_groups; + for(int group = 0; group < conflict_groups_json.size(); group++) + { + std::vector new_group; + for(int mod = 0; mod < conflict_groups_json[group].size(); mod++) + new_group.push_back(conflict_groups_json[group][mod].asInt()); + conflict_groups.push_back(std::move(new_group)); + } + deployers_[depl]->setConflictGroups(conflict_groups); + } + } + deployers_[depl]->setProfile(current_profile_); + } + Json::Value tools = json_settings_["tools"]; + for(int tool = 0; tool < tools.size(); tool++) + tools_.emplace_back(tools[tool]["name"].asString(), tools[tool]["command"].asString()); + + for(int prof = 0; prof < profile_names_.size(); prof++) + bak_man_.addProfile(); + bak_man_.setProfile(current_profile_); + Json::Value backup_targets = json_settings_["backup_targets"]; + for(int target = 0; target < backup_targets.size(); target++) + bak_man_.addTarget(backup_targets[target]["path"].asString()); + bak_man_.setLog(log_); + + if(json_settings_.isMember("manual_tags")) + { + for(auto& tag_entry : json_settings_["manual_tags"]) + { + if(str::find_if(manual_tags_, + [name = tag_entry["name"].asString()](auto tag) + { return tag.getName() == name; }) != manual_tags_.end()) + throw ParseError( + std::format("Manual tag \"{}\" found more than once.", tag_entry["name"].asString())); + manual_tags_.emplace_back(tag_entry); + } + updateManualTagMap(); + } + + if(json_settings_.isMember("auto_tags")) + { + for(auto& tag_entry : json_settings_["auto_tags"]) + { + if(str::find_if(auto_tags_, + [name = tag_entry["name"].asString()](auto tag) + { return tag.getName() == name; }) != auto_tags_.end()) + throw ParseError( + std::format("Auto tag \"{}\" found more than once.", tag_entry["name"].asString())); + auto_tags_.emplace_back(tag_entry); + } + updateAutoTagMap(); + } +} + +std::string ModdedApplication::getModName(int mod_id) const +{ + auto iter = std::find_if( + installed_mods_.begin(), installed_mods_.end(), [mod_id](Mod m) { return m.id == mod_id; }); + if(iter == installed_mods_.end()) + return ""; + return iter->name; +} + +void ModdedApplication::updateDeployerGroups(std::optional progress_node) +{ + std::vector> update_targets; + for(int depl = 0; depl < deployers_.size(); depl++) + { + update_targets.push_back({}); + if(deployers_[depl]->isAutonomous()) + continue; + for(int profile = 0; profile < profile_names_.size(); profile++) + { + deployers_[depl]->setProfile(profile); + std::vector completed_groups(active_group_members_.size()); + std::fill(completed_groups.begin(), completed_groups.end(), false); + for(const auto [mod_id, _] : deployers_[depl]->getLoadorder()) + { + if(!group_map_.contains(mod_id)) + continue; + const int group = group_map_[mod_id]; + if(!completed_groups[group]) + { + completed_groups[group] = true; + if(deployers_[depl]->swapMod(mod_id, active_group_members_[group])) + update_targets[depl].push_back(profile); + } + else if(deployers_[depl]->removeMod(mod_id)) + update_targets[depl].push_back(profile); + } + } + deployers_[depl]->setProfile(current_profile_); + } + if(progress_node) + { + std::vector weights; + for(int depl = 0; depl < update_targets.size(); depl++) + { + for(int profile : update_targets[depl]) + { + deployers_[depl]->setProfile(profile); + weights.push_back(deployers_[depl]->getNumMods()); + } + deployers_[depl]->setProfile(current_profile_); + } + (*progress_node)->addChildren(weights); + } + int i = 0; + for(int depl = 0; depl < update_targets.size(); depl++) + { + for(int profile : update_targets[depl]) + { + deployers_[depl]->setProfile(profile); + deployers_[depl]->updateConflictGroups(progress_node ? &(*progress_node)->child(i) + : std::optional{}); + i++; + } + deployers_[depl]->setProfile(current_profile_); + } +} + +void ModdedApplication::splitMod(int mod_id, int deployer) +{ + if(deployers_[deployer]->isAutonomous()) + return; + + std::map managed_sub_dirs; + for(int i = 0; i < deployers_.size(); i++) + { + if(i == deployer || deployers_[i]->isAutonomous()) + continue; + auto cur_depl_path = deployers_[i]->getDestPath(); + if(!cur_depl_path.ends_with("/")) + cur_depl_path += "/"; + auto target_depl_path = deployers_[deployer]->getDestPath(); + if(!target_depl_path.ends_with("/")) + target_depl_path += "/"; + const auto pos = cur_depl_path.find(target_depl_path); + if(pos != std::string::npos) + { + std::string sub_dir = cur_depl_path.substr(pos + target_depl_path.size()); + if(sub_dir.starts_with("/")) + sub_dir = sub_dir.substr(1); + managed_sub_dirs[i] = sub_dir; + } + } + if(managed_sub_dirs.empty()) + return; + + for(const auto& [depl, dir] : managed_sub_dirs) + { + const auto mod_dir_optional = + pu::pathExists(dir, + staging_dir_ / std::to_string(mod_id), + deployers_[deployer]->getType() == DeployerFactory::CASEMATCHINGDEPLOYER); + if(!mod_dir_optional) + continue; + const auto mod_dir = staging_dir_ / std::to_string(mod_id) / mod_dir_optional->string(); + + AddModInfo info; + info.deployers = { depl }; + info.group = -1; + auto iter = + str::find_if(installed_mods_, [mod_id](const auto& mod) { return mod.id == mod_id; }); + if(iter == installed_mods_.end()) + throw std::runtime_error(std::format("Invalid mod id {}", mod_id)); + info.name = iter->name + " [" + deployers_[depl]->getName() + "]"; + info.version = iter->version; + info.installer = Installer::SIMPLEINSTALLER; + info.installer_flags = Installer::Flag::preserve_case | Installer::Flag::preserve_directories; + info.files = {}; + info.root_level = 0; + info.source_path = mod_dir; + log_(Log::LOG_WARNING, + std::format("Mod '{}' has been split because it contains" + " a sub-directory managed by deployer '{}'.", + iter->name, + deployers_[depl]->getName())); + installMod(info); + if(sfs::exists(mod_dir)) + sfs::remove_all(mod_dir); + } +} + +void ModdedApplication::replaceMod(const AddModInfo& info) +{ + if(!info.replace_mod || info.group == -1) + { + installMod(info); + return; + } + auto index = + str::find_if(installed_mods_, [group = info.group](const Mod& m) { return m.id == group; }); + if(index == installed_mods_.end()) + throw std::runtime_error(std::format("Invalid group '{}' for mod '{}'", info.group, info.name)); + + int mod_id = 0; + if(!installed_mods_.empty()) + mod_id = std::max_element(installed_mods_.begin(), installed_mods_.end())->id + 1; + while(sfs::exists(staging_dir_ / std::to_string(mod_id)) && + mod_id < std::numeric_limits().max()) + mod_id++; + if(mod_id == std::numeric_limits().max()) + throw std::runtime_error("Error: Could not generate new mod id."); + const sfs::path tmp_replace_dir = + staging_dir_ / (std::string("tmp_replace_") + std::to_string(mod_id)); + + const auto mod_size = Installer::install(info.source_path, + tmp_replace_dir, + info.installer_flags, + info.installer, + info.root_level, + info.files); + const sfs::path old_mod_path = staging_dir_ / std::to_string(info.group); + if(sfs::exists(old_mod_path)) + sfs::remove_all(old_mod_path); + sfs::rename(tmp_replace_dir, old_mod_path); + + index->name = info.name; + index->version = info.version; + index->remote_source = info.remote_source; + index->local_source = info.local_source; + index->install_time = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + index->remote_update_time = index->install_time; + index->size_on_disk = mod_size; + + std::vector weights; + std::vector> update_targets; + for(int depl = 0; depl < deployers_.size(); depl++) + { + update_targets.push_back({}); + if(deployers_[depl]->isAutonomous()) + continue; + for(int prof = 0; prof < profile_names_.size(); prof++) + { + deployers_[depl]->setProfile(prof); + if(deployers_[depl]->hasMod(info.group)) + { + update_targets[depl].push_back(prof); + weights.push_back(deployers_[depl]->getNumMods()); + } + } + deployers_[depl]->setProfile(current_profile_); + } + ProgressNode node(progress_callback_, weights); + int i = 0; + for(int depl = 0; depl < update_targets.size(); depl++) + { + for(int prof : update_targets[depl]) + { + deployers_[depl]->setProfile(prof); + deployers_[depl]->updateConflictGroups(&node.child(i)); + i++; + } + deployers_[depl]->setProfile(current_profile_); + } + + for(auto& tag : auto_tags_) + tag.updateMods(staging_dir_, std::vector{ info.group }); + updateAutoTagMap(); + + updateSettings(true); +} + +void ModdedApplication::updateManualTagMap() +{ + manual_tag_map_.clear(); + for(const auto& mod : installed_mods_) + manual_tag_map_[mod.id] = {}; + for(const auto& tag : manual_tags_) + { + for(int mod_id : tag.getMods()) + manual_tag_map_[mod_id].push_back(tag.getName()); + } +} + +void ModdedApplication::updateAutoTagMap() +{ + auto_tag_map_.clear(); + for(const auto& mod : installed_mods_) + auto_tag_map_[mod.id] = {}; + for(const auto& tag : auto_tags_) + { + for(int mod_id : tag.getMods()) + auto_tag_map_[mod_id].push_back(tag.getName()); + } +} + +void ModdedApplication::performUpdateCheck(const std::vector& target_mod_indices) +{ + if(target_mod_indices.empty()) + { + log_(Log::LOG_INFO, "None of the selected mods has a valid remote source."); + return; + } + log_(Log::LOG_INFO, + std::format("Checking for updates for {} mod{}...", + target_mod_indices.size(), + target_mod_indices.size() > 1 ? "s" : "")); + ProgressNode node(progress_callback_); + node.setTotalSteps(target_mod_indices.size()); + int num_available_updates = 0; + for(int i : target_mod_indices) + { + installed_mods_[i].remote_update_time = + nexus::Api::getNexusPage(installed_mods_[i].remote_source).mod.updated_time; + if(installed_mods_[i].remote_update_time > installed_mods_[i].install_time) + num_available_updates++; + node.advance(); + } + if(num_available_updates > 0) + log_(Log::LOG_INFO, + std::format("Found updates for {} mod{}.", + num_available_updates, + num_available_updates == 1 ? "" : "s")); + else + log_(Log::LOG_INFO, "No mod updates found."); + updateSettings(true); +} diff --git a/src/core/moddedapplication.h b/src/core/moddedapplication.h new file mode 100644 index 0000000..fecf41c --- /dev/null +++ b/src/core/moddedapplication.h @@ -0,0 +1,729 @@ +/*! + * \file moddedapplication.h + * \brief Header for the ModdedApplication class. + */ + +#pragma once + +#include "addmodinfo.h" +#include "appinfo.h" +#include "autotag.h" +#include "backupmanager.h" +#include "deployer.h" +#include "deployerinfo.h" +#include "editautotagaction.h" +#include "editdeployerinfo.h" +#include "editmanualtagaction.h" +#include "editprofileinfo.h" +#include "log.h" +#include "manualtag.h" +#include "modinfo.h" +#include "nexus/api.h" +#include +#include +#include +#include + + +/*! + * \brief Contains all mods and Deployer objects used for one target application. + * Stores internal state in a JSON file. + */ +class ModdedApplication +{ +public: + /*! + * \brief If a JSON settings file already exists in app_mod_dir, it is + * used to construct this object. + * \param staging_dir Path to staging directory where all installed mods are stored. + * \param name Name of target application. + * \param command Command used to run target application. + * \param icon_path Path to an icon for this application. + * \throws Json::LogicError Indicates a logic error, e.g. trying to convert "123" to a bool, + * while parsing. + * \throws Json::RuntimeError Indicates a syntax error in the JSON file. + * \throws ParseError Indicates a semantic error while parsing the JSON file, e.g. + * the active member of a group is not part of that group. + */ + ModdedApplication(std::filesystem::path staging_dir, + std::string name = "", + std::string command = "", + std::filesystem::path icon_path = "", + std::string app_version = ""); + + /*! \brief Name of the file used to store this objects internal state. */ + inline static const std::string CONFIG_FILE_NAME = "lmm_mods.json"; + + /*! \brief Deploys mods using all Deployer objects of this application. */ + void deployMods(); + /*! + * \brief Deploys mods using Deployer objects with given ids. + * \param deployers The Deployer ids used for deployment. + */ + void deployModsFor(const std::vector& deployers); + /*! + * \brief Installs a new mod using the given Installer type. + * \param info Contains all data needed to install the mod. + */ + void installMod(const AddModInfo& info); + /*! + * \brief Uninstalls the given mods, this includes deleting all installed files. + * \param mod_id Ids of the mods to be uninstalled. + * \param installer_type The Installer type used. If an empty string is given, the Installer + * used during installation is used. + */ + void uninstallMods(const std::vector& mod_ids, const std::string& installer_type = ""); + /*! + * \brief Moves a mod from one position in the load order to another for given Deployer. + * \param deployer The target Deployer. + * \param from_index Index of mod to be moved. + * \param to_index Destination index. + */ + void changeLoadorder(int deployer, int from_index, int to_index); + /*! + * \brief Appends a new mod to the load order for given Deployer. + * \param deployer The target Deployer + * \param mod_id Id of the mod to be added. + * \param update_conflicts Updates the target deployers conflict groups only if this is true. + * \param progress_node Used to inform about the current progress. + */ + void addModToDeployer(int deployer, + int mod_id, + bool update_conflicts = true, + std::optional progress_node = {}); + /*! + * \brief Removes a mod from the load order for given Deployer. + * \param deployer The target Deployer + * \param mod_id Id of the mod to be removed. + * \param update_conflicts Updates the target deployers conflict groups only if this is true. + * \param progress_node Used to inform about the current progress. + */ + void removeModFromDeployer(int deployer, + int mod_id, + bool update_conflicts = true, + std::optional progress_node = {}); + /*! + * \brief Enables or disables the given mod in the load order for given Deployer. + * \param deployer The target Deployer + * \param mod_id Mod to be edited. + * \param status The new status. + */ + void setModStatus(int deployer, int mod_id, bool status); + /*! + * \brief Adds a new Deployer of given type. + * \param info Contains all data needed to create a deployer, e.g. its name. + */ + void addDeployer(const EditDeployerInfo& info); + /*! + * \brief Removes a Deployer. + * \param deployer The Deployer. + * \param cleanup If true: Remove all currently deployed files and restore backups. + */ + void removeDeployer(int deployer, bool cleanup); + /*! + * \brief Creates a vector containing the names of all Deployer objects. + * \return The vector. + */ + std::vector getDeployerNames() const; + /*! + * \brief Creates a vector containing information about all installed mods, stored in ModInfo + * objects. + * \return The vector. + */ + std::vector getModInfo() const; + /*! + * \brief Getter for the current mod load order of one Deployer. + * \param deployer The target Deployer. + * \return The load order. + */ + std::vector> getLoadorder(int deployer) const; + /*! + * \brief Getter for the path to the staging directory. This is where all installed + * mods are stored. + * \return The path. + */ + const std::filesystem::path& getStagingDir() const; + /*! + * \brief Setter for the path to the staging directory. This is where all installed + * mods are stored. + * \param staging_dir The new staging directory path. + * \param move_existing If true: Move all installed mods to the new directory. + * \throws Json::LogicError Indicates a logic error, e.g. trying to convert "123" to a bool, + * while parsing. + * \throws Json::RuntimeError Indicates a syntax error in the JSON file. + * \throws ParseError Indicates a semantic error while parsing the JSON file, e.g. + * the active member of a group is not part of that group. + */ + void setStagingDir(std::string staging_dir, bool move_existing); + /*! + * \brief Getter for the name of this application. + * \return The name. + */ + const std::string& name() const; + /*! + * \brief Setter for the name of this application. + * \param newName The new name. + */ + void setName(const std::string& newName); + /*! + * \brief Returns the number of Deployer objects for this application. + * \return The number of Deployers. + */ + int getNumDeployers() const; + /*! + * \brief Getter for the name of the file used to store this objects internal state. + * \return The name. + */ + const std::string& getConfigFileName() const; + /*! + * \brief Changes the name of an installed mod. + * \param mod_id Id of the target mod. + * \param new_name The new name. + */ + void changeModName(int mod_id, const std::string& new_name); + /*! + * \brief Checks for file conflicts of given mod with all other mods in the load order for + * one Deployer. + * \param deployer The target Deployer + * \param mod_id Mod to be checked. + * \param show_disabled If true: Also check for conflicts with disabled mods. + * \return A vector with information about conflicts with every other mod. + */ + std::vector getFileConflicts(int deployer, int mod_id, bool show_disabled) const; + /*! + * \brief Fills an AppInfo object with information about this object. + * \return The AppInfo object. + */ + AppInfo getAppInfo() const; + /*! + * \brief Adds a new tool to this application. + * \param name The tool's name. + * \param command The tool's command. + */ + void addTool(std::string name, std::string command); + /*! + * \brief Removes a tool. + * \param tool_id The tool's id. + */ + void removeTool(int tool_id); + /*! + * \brief Getter for the tools of this application. The tuples contain the name (index 0) + * and the command (index 1). + * \return The vector of tuples. + */ + const std::vector>& getTools() const; + /*! + * \brief Getter for the command used to run this application. + * \return The command. + */ + const std::string& command() const; + /*! + * \brief Setter for the command used to run this application. + * \param newCommand The new command. + */ + void setCommand(const std::string& newCommand); + /*! + * \brief Used to set type, name and target directory for one deployer. + * \param deployer Target Deployer. + * \param info Contains all data needed to edit a deployer, e.g. its new name. + */ + void editDeployer(int deployer, const EditDeployerInfo& info); + /*! + * \brief Checks for conflicts with other mods for one Deployer. + * Two mods are conflicting if they share at least one file. + * \param deployer Target Deployer. + * \param mod_id The mod to be checked. + * \return A set of mod ids which conflict with the given mod. + */ + std::unordered_set getModConflicts(int deployer, int mod_id); + /*! + * \brief Sets the currently active profile. + * \param profile The new profile. + */ + void setProfile(int profile); + /*! + * \brief Adds a new profile and optionally copies it's load order from an existing profile. + * \param info Contains the data for the new profile. + */ + void addProfile(const EditProfileInfo& info); + /*! + * \brief Removes a profile. + * \param profile The profile to be removed. + */ + void removeProfile(int profile); + /*! + * \brief Returns a vector containing the names of all profiles. + * \return The vector. + */ + std::vector getProfileNames() const; + /*! + * \brief Used to set the name of a profile. + * \param profile Target Profile + * \param info Contains the new profile data. + */ + void editProfile(int profile, const EditProfileInfo& info); + /*! + * \brief Used to set name and command for one tool. + * \param tool Target tool. + * \param name the new name. + * \param command The new command. + */ + void editTool(int tool, std::string name, std::string command); + /*! + * \brief Checks if writing to the deployment directory is possible for every Deployer. + * Creates a vector of tuples containing a code (index 0) indicating success(0), + * an IO error(1) or an error during link creation(2) and the deployers name (index 1). + * \return The vector. + */ + std::tuple verifyDeployerDirectories(); + /*! + * \brief Adds a mod to an existing group and makes the mod the active member of that group. + * \param mod_id The mod's id. + * \param group The target group. + * \param progress_node Used to inform about the current progress. + */ + void addModToGroup(int mod_id, int group, std::optional progress_node = {}); + /*! + * \brief Removes a mod from it's group. + * \param mod_id Target mod. + * \param update_conflicts If true: Update relevant conflict groups. + * \param progress_node Used to inform about the current progress. + */ + void removeModFromGroup(int mod_id, + bool update_conflicts = true, + std::optional progress_node = {}); + /*! + * \brief Creates a new group containing the two given mods. A group is a set of mods + * where only one member, the active member, will be deployed. + * \param first_mod_id First mod. This will be the active member of the new group. + * \param second_mod_id Second mod. + * \param progress_node Used to inform about the current progress. + */ + void createGroup(int first_mod_id, + int second_mod_id, + std::optional progress_node = {}); + /*! + * \brief Changes the active member of given group to given mod. + * \param group Target group. + * \param mod_id The new active member. + * \param progress_node Used to inform about the current progress. + */ + void changeActiveGroupMember(int group, + int mod_id, + std::optional progress_node = {}); + /*! + * \brief Sets the given mod's version to the given new version. + * \param mod_id Target mod. + * \param new_version The new version. + */ + void changeModVersion(int mod_id, const std::string& new_version); + /*! + * \brief Returns the number of groups. + * \return The number of groups. + */ + int getNumGroups(); + /*! + * \brief Checks if given mod belongs to any group. + * \param mod_id Target mod. + * \return True if mod belongs to a group, else: False. + */ + bool modHasGroup(int mod_id); + /*! + * \brief Returns the group to which the given mod belongs. + * \param mod_id Target mod. + * \return The group, or -1 if the mod has no group. + */ + int getModGroup(int mod_id); + /*! + * \brief Sorts the load order by grouping mods which contain conflicting files. + * \param deployer Deployer for which the currently active load order is to be sorted. + */ + void sortModsByConflicts(int deployer); + /*! + * \brief Returns the conflicts groups for the current profile of given deployer. + * \param deployer Target Deployer. + * \return The conflict info. + */ + std::vector> getConflictGroups(int deployer); + /*! + * \brief Updates which \ref Deployer "deployer" should manage given mods. + * \param mod_id Vector of mod ids to be added. + * \param deployers Bool for every deployer, indicating if the mods should be managed + * by that deployer. + */ + void updateModDeployers(const std::vector& mod_ids, const std::vector& deployers); + /*! \brief Getter for icon_path_. */ + std::filesystem::path iconPath() const; + /*! + * \brief Setter for icon_path_. + * \param icon_path The new icon path + */ + void setIconPath(const std::filesystem::path& icon_path); + + /*! + * \brief Verifies if reading/ writing to the staging directory is possible and if the + * JSON file containing information about installed mods can be parsed. + * \param staging_dir Path to the staging directory. + * \return A code indicating success(0), an IO error(1) or an error during JSON parsing(2). + */ + static int verifyStagingDir(std::filesystem::path staging_dir); + /*! + * \brief Extracts the given archive to the given location. + * \param source Source path. + * \param target Extraction target path. + */ + void extractArchive(const std::filesystem::path& source, const std::filesystem::path& target); + /*! + * \brief Creates DeployerInfo for one Deployer. + * \param deployer Target deployer. + */ + DeployerInfo getDeployerInfo(int deployer); + /*! \brief Setter for log callback. */ + void setLog(const std::function& newLog); + /*! + * \brief Adds a new target file or directory to be managed by the BackupManager. + * \param path Path to the target file or directory. + * \param name Display name for this target. + * \param backup_names Display names for initial backups. Must contain at least one. + */ + void addBackupTarget(const std::filesystem::path& path, + const std::string& name, + const std::vector& backup_names); + /*! + * \brief Removes the given backup target by deleting all backups, except for the active one, + * and all config files. + * \param target_id Target to remove. + */ + void removeBackupTarget(int target_id); + /*! + * \brief Removes all targets by deleting all backups, except for the active ones, + * and all config files. + */ + void removeAllBackupTargets(); + /*! + * \brief Adds a new backup for the given target by copying the currently active backup. + * \param target_id Target for which to create a new backup. + * \param name Display name for the new backup. + * \param source Backup from which to copy files to create the new backup. If -1: + * copy currently active backup. + */ + void addBackup(int target_id, const std::string& name, int source); + /*! + * \brief Deletes the given backup for given target. + * \param target_id Target from which to delete a backup. + * \param backup_id Backup to remove. + */ + void removeBackup(int target_id, int backup_id); + /*! + * \brief Changes the currently active backup for the given target. + * \param target_id Target for which to change the active backup. + * \param backup_id New active backup. + */ + void setActiveBackup(int target_id, int backup_id); + /*! + * \brief Returns a vector containing information about all managed backup targets. + * \return The vector. + */ + std::vector getBackupTargets() const; + /*! + * \brief Changes the name of the given backup for the given target + * \param target_id Backup target. + * \param backup_id Backup to be edited. + * \param name The new name. + */ + void setBackupName(int target_id, int backup_id, const std::string& name); + /*! + * \brief Changes the name of the given backup target + * \param target_id Backup target. + * \param name The new name. + */ + void setBackupTargetName(int target_id, const std::string& name); + /*! + * \brief Deletes all files in the dest backup and replaces them with the files + * from the source backup. + * \param target_id Backup target. + * \param source_backup Backup from which to copy files. + * \param dest_backup Target for data deletion. + */ + void overwriteBackup(int target_id, int source_backup, int dest_backup); + /*! \brief Performs a cleanup for the previous installation. */ + void cleanupFailedInstallation(); + /*! + * \brief Sets the callback function used to inform about the current task's progress. + * \param progress_callback The function. + */ + void setProgressCallback(const std::function& progress_callback); + /*! + * \brief Uninstalls all mods which are inactive group members of any group which contains + * any of the given mods. + * \param mod_ids Ids of the mods for which to uninstall group members. + */ + void uninstallGroupMembers(const std::vector& mod_ids); + /*! + * \brief Adds a new tag with the given name. Fails if a tag by that name already exists. + * \param tag_name Name for the new tag. + * \throw std::runtime_error If a tag by that name exists. + */ + void addManualTag(const std::string& tag_name); + /*! + * \brief Removes the tag with the given name, if it exists. + * \param tag_name Tag to be removed. + * \param update_map If true: Update the manual tag map. + */ + void removeManualTag(const std::string& tag_name, bool update_map = true); + /*! + * \brief Changes the name of the given tag to the given new name. + * Fails if a tag by the given name exists. + * \param old_name Name of the target tag. + * \param new_name Target tags new name. + * \param update_map If true: Update the manual tag map. + * \throw std::runtime_error If a tag with the given new_name exists. + */ + void changeManualTagName(const std::string& old_name, + const std::string& new_name, + bool update_map = true); + /*! + * \brief Adds the given tags to all given mods. + * \param tag_name Target tags name. + * \param mod_ids Target mod ids. + */ + void addTagsToMods(const std::vector& tag_names, const std::vector& mod_ids); + /*! + * \brief Removes the given tags from the given mods. + * \param tag_name Target tags name. + * \param mod_ids Target mod ids. + */ + void removeTagsFromMods(const std::vector& tag_names, + const std::vector& mod_ids); + /*! + * \brief Sets the tags for all given mods to the given tags. + * \param tag_names Names of the new tags. + * \param mod_ids Target mod ids. + */ + void setTagsForMods(const std::vector& tag_names, const std::vector mod_ids); + /*! + * \brief Performs the given editing actions on the manual tags. + * \param actions Editing actions. + */ + void editManualTags(const std::vector& actions); + /*! + * \brief Adds a new auto tag. + * \param name The new tags name. + * \param expression Expression used for the new tags evaluator. + * \param conditions Conditions used for the new tags evaluator. + * \param update If true: Update the auto tag map and the settings. + * \throw std::runtime_error If a tag by that name exists. + */ + void addAutoTag(const std::string& tag_name, + const std::string& expression, + const std::vector& conditions, + bool update); + /*! + * \brief Removes the given auto tag. + * \param name Tag to be removed. + * \param update If true: Update the auto tag map and the settings. + */ + void removeAutoTag(const std::string& tag_name, bool update); + /*! + * \brief Changes the name of the given auto tag to the given new name. + * Fails if a tag by the given name exists. + * \param old_name Name of the target tag. + * \param new_name Target tags new name. + * \param update If true: Update the auto tag map. + * \throw std::runtime_error If a tag with the given new_name exists. + */ + void renameAutoTag(const std::string& old_name, const std::string& new_name, bool update); + /*! + * \brief Changes the given tags evaluator according to the given expression and conditions. + * \param tag_name Target auto tag. + * \param expression New expression to be used. + * \param conditions Conditions for the new expression. + * \param update If true: Update the auto tag map. + */ + void changeAutoTagEvaluator(const std::string& tag_name, + const std::string& expression, + const std::vector& conditions, + bool update); + /*! + * \brief Performs the given editing actions on the auto tags. + * \param actions Editing actions. + */ + void editAutoTags(const std::vector& actions); + /*! \brief Reapply all auto tags to all mods. */ + void reapplyAutoTags(); + /*! + * \brief Reapplies auto tags to the specified mods. + * \param mod_ids Mods to which auto tags are to be reapplied. + */ + void updateAutoTags(const std::vector mod_ids); + /*! \brief Deletes all data for this app. */ + void deleteAllData(); + /*! + * \brief Sets the app version of the currently active profile to the given version. + * \param app_version The new app version. + */ + void setAppVersion(const std::string& app_version); + /*! + * \brief Sets the given mods local and remote sources to the given paths. + * \param mod_id Target mod id. + * \param local_source Path to a local archive or directory used for mod installation. + * \param remote_source Remote URL from which the mod was downloaded. + */ + void setModSources(int mod_id, const std::string& local_source, const std::string& remote_source); + /*! + * \brief Fetches data from NexusMods for the given mod. + * \param mod_id Target mod id. + * \return A Mod object containing all data from NexusMods regarding that mod. + */ + nexus::Page getNexusPage(int mod_id); + /*! \brief Checks for updates for all mods. */ + void checkForModUpdates(); + /*! + * \brief Checks for updates for mods with the given ids. + * \param mod_ids Ids of the mods for which to check for updates. + */ + void checkModsForUpdates(const std::vector& mod_ids); + /*! + * \brief Temporarily disables update notifications for the given mods. This is done + * by setting the mods remote_update_time to the installation_time. + * \param mod_ids Ids of the mods for which update notifications are to be disabled. + */ + void suppressUpdateNotification(const std::vector& mod_ids); + /*! + * \brief Generates a download URL from the given NexusMods nxm Url. + * \param nxm_url The nxm URL used. + * \return The download URL. + */ + std::string getDownloadUrl(const std::string& nxm_url); + /*! + * \brief Generates a download URL from the given NexusMods mod id and file id. + * \param nexus_file_id File id of the mod. + * \param mod_url Url to the mod page on NexusMods. + * \return The download URL. + */ + std::string getDownloadUrlForFile(int nexus_file_id, const std::string& mod_url); + /*! + * \brief Generates a NexusMods mod page URL from the given nxm URL. + * \param nxm_url The nxm Url used. This is usually generated through the NexusMods website. + * \return The NexusMods mod page URL. + */ + std::string getNexusPageUrl(const std::string& nxm_url); + /*! + * \brief Downloads the file from the given url to staging_dir_ / _download. + * \param url Url from which to download the file. + * \return The path to the downloaded file. + */ + std::string downloadMod(const std::string& url, std::function progress_callback); + +private: + /*! \brief The name of this application. */ + std::string name_; + /*! \brief Contains the internal state of this object. */ + Json::Value json_settings_; + /*! \brief The path to the staging directory containing all installed mods. */ + std::filesystem::path staging_dir_; + /*! \brief Contains all currently installed mods. */ + std::vector installed_mods_; + /*! \brief Contains every Deployer used by this application. */ + std::vector> deployers_; + /*! \brief Contains names and commands for every tool. */ + std::vector> tools_; + /*! \brief The command used to run this application. */ + std::string command_ = ""; + /*! \brief The currently active profile id. */ + int current_profile_ = 0; + /*! \brief Contains names of all profiles. */ + std::vector profile_names_; + /*! \brief For every group: A vector containing every mod in that group. */ + std::vector> groups_; + /*! \brief Maps mods to their groups. */ + std::map group_map_; + /*! \brief Contains the active member of every group. */ + std::vector active_group_members_; + /*! \brief Maps mods to the installer used during their installation. */ + std::map installer_map_; + /*! \brief Path to this applications icon. */ + std::filesystem::path icon_path_; + /*! \brief Callback for logging. */ + std::function log_ = [](Log::LogLevel a, + const std::string& b) {}; + /*! \brief Manages all backups for this application. */ + BackupManager bak_man_; + /*! \brief Id of the most recently installed mod. */ + int last_mod_id_ = -1; + /*! \brief Contains all known manually managed tags. */ + std::vector manual_tags_; + /*! \brief Maps mod ids to a vector of manual tags associated with that mod. */ + std::map> manual_tag_map_; + /*! \brief Contains all known auto tags. */ + std::vector auto_tags_; + /*! \brief Maps mod ids to a vector of auto tags associated with that mod. */ + std::map> auto_tag_map_; + /*! + * \brief For every profile: The version of the app managed by that profile. + * + * This does not refer to a ModdedApplication object but rather the actually + * modded application. + */ + std::vector app_versions_; + /*! \brief Callback used to inform about the current task's progress. */ + std::function progress_callback_ = [](float f) {}; + /*! \brief The subdirectory used to store downloads. */ + std::string download_dir_ = "_download"; + + /*! + * \brief Updates json_settings_ with the current state of this object. + * \param write If true: write json_settings_ to a file after updating. + */ + void updateSettings(bool write = false); + /*! + * \brief Writes json_settings_ to a file at app_mod_dir_/CONFIG_FILE_NAME. + */ + void writeSettings() const; + /*! + * \brief Reads json_settings_ from a file at app_mod_dir_/CONFIG_FILE_NAME. + */ + void readSettings(); + /*! + * \brief Updates the internal state of this object to the state stored in json_settings_. + * \param read If true: Read json_settings_ from a file before updating. + */ + void updateState(bool read = false); + /*! + * \brief Returns the name of a mod. + * \param mod_id The mod. + * \return The name. + * \throws Json::LogicError Indicates a logic error, e.g. trying to convert "123" to a bool, + * while parsing. + * \throws Json::RuntimeError Indicates a syntax error in the JSON file. + * \throws ParseError Indicates a semantic error while parsing the JSON file, e.g. + * the active member of a group is not part of that group. + */ + std::string getModName(int mod_id) const; + /*! + * \brief Updates the load order for every Deployer to reflect the current mod groups. + * \param progress_node Used to inform about the current progress. + */ + void updateDeployerGroups(std::optional progress_node = {}); + /*! + * \brief If given mod contains a sub-directory managed by a deployer that is not the given + * deployer, creates a new mod which contains that sub-directory. + * \param mod_id Mod to check. + * \param deployer Deployer which currently manages the given mod. + */ + void splitMod(int mod_id, int deployer); + /*! + * \brief Replaces an existing mod with the mod specified by the given argument. + * \param info Contains all data needed to install the mod. + */ + void replaceMod(const AddModInfo& info); + /*! \brief Updates manual_tag_map_ with the information contained in manual_tags_. */ + void updateManualTagMap(); + /*! \brief Updates auto_tag_map_ with the information contained in auto_tags_. */ + void updateAutoTagMap(); + /*! + * \brief Checks for available updates for mods with the given index in installed_mods_. + * \param target_mod_indices Target mod indices. + */ + void performUpdateCheck(const std::vector& target_mod_indices); +}; diff --git a/src/core/modinfo.h b/src/core/modinfo.h new file mode 100644 index 0000000..07e4f1d --- /dev/null +++ b/src/core/modinfo.h @@ -0,0 +1,84 @@ +/*! + * \file modinfo.h + * \brief Contains the ModInfo struct. + */ + +#pragma once + +#include "mod.h" +#include +#include + + +/*! + * \brief Stores information about a mod as well as the group and + * \ref Deployer "deployers" it belongs to. + */ +struct ModInfo +{ + /*! \brief Contains information about the mod itself. */ + Mod mod; + /*! \brief Names of all \ref Deployer "deployers" the mod belongs to. */ + std::vector deployers; + /*! \brief Ids of all \ref Deployer "deployers" the mod belongs to. */ + std::vector deployer_ids; + /*! \brief The mods activation status for every \ref Deployer "deployer" it belongs to. */ + std::vector deployer_statuses; + /*! \brief Group this mod belongs to. If == -1: Mod belongs to no group. */ + int group = -1; + /*! \brief If true: Mod is the active member of its group. */ + bool is_active_group_member = false; + /*! \brief Contains the names of all manual tags added to this mod. */ + std::vector manual_tags; + /*! \brief Contains the names of all auto tags added to this mod. */ + std::vector auto_tags; + + /*! + * \brief Constructor. Simply initializes members. + * \param id The mod's id. + * \param name The mod's name. + * \param version The mod's version. + * \param install_time Timestamp indicating when the mod was installed. + * \param local_source Source archive for the mod. + * \param remote_source URL from where the mod was downloaded. + * \param remote_update_time Timestamp for when the mod was updated at the remote source. + * \param size Total size of the installed mod on disk. + * \param suppress_time Timestamp for when the user requested to suppress current update + * notifications. + * \param deployer_names Names of all \ref Deployer "deployers" the mod belongs to. + * \param deployer_ids Ids of all \ref Deployer "deployers" the mod belongs to. + * \param statuses The mods activation status for every \ref Deployer "deployer" it belongs to. + * \param group Group this mod belongs to. If == -1: Mod belongs to no group. + * \param is_active_member If true: Mod is the active member of it's group. + * \param man_tags The names of all manual tags for this mod. + */ + ModInfo(int id, + const std::string& name, + const std::string& version, + const std::time_t& install_time, + const std::filesystem::path& local_source, + const std::string& remote_source, + const std::time_t& remote_update_time, + unsigned long size, + const std::time_t& suppress_time, + const std::vector& deployer_names, + const std::vector& deployer_ids, + const std::vector& statuses, + int group, + bool is_active_member, + const std::vector& man_tags, + const std::vector& au_tags) : + mod(id, + name, + version, + install_time, + local_source, + remote_source, + remote_update_time, + size, + suppress_time), + deployers(std::move(deployer_names)), deployer_ids(std::move(deployer_ids)), + deployer_statuses(statuses), group(group), is_active_group_member(is_active_member), + manual_tags(man_tags), auto_tags(au_tags) + {} +}; diff --git a/src/core/nexus/api.cpp b/src/core/nexus/api.cpp new file mode 100644 index 0000000..d93d191 --- /dev/null +++ b/src/core/nexus/api.cpp @@ -0,0 +1,334 @@ +#include "api.h" +#include "../parseerror.h" +#include +#include +#include + +using namespace nexus; +namespace str = std::ranges; + + +void Api::setApiKey(const std::string& api_key) +{ + api_key_ = api_key; +} + +bool Api::isInitialized() +{ + return !api_key_.empty(); +} + +Mod Api::getMod(const std::string& mod_url) +{ + auto domain_and_mod = extractDomainAndModId(mod_url); + if(!domain_and_mod) + throw std::runtime_error(std::format("Could not parse mod URL: \"{}\".", mod_url)); + return getMod(domain_and_mod->first, domain_and_mod->second); +} + +Mod Api::getMod(const std::string& domain_name, long mod_id) +{ + cpr::Response response = + cpr::Get(cpr::Url(std::format( + "https://api.nexusmods.com/v1/games/{}/mods/{}.json", domain_name, mod_id)), + cpr::Header{ { "apikey", api_key_ } }); + if(response.status_code != 200) + throw std::runtime_error( + std::format("Failed to get data for mod with id {} from NexusMods. Response code was {}", + mod_id, + response.status_code)); + return { response.text }; +} + +void Api::trackMod(const std::string& mod_url) +{ + auto domain_and_mod = extractDomainAndModId(mod_url); + if(!domain_and_mod) + throw std::runtime_error(std::format("Could not parse mod URL: \"{}\".", mod_url)); + const cpr::Response response = + cpr::Post(cpr::Url("https://api.nexusmods.com/v1/user/tracked_mods.json"), + cpr::Header{ { "apikey", api_key_ } }, + cpr::Parameters{ { "domain_name", domain_and_mod->first }, + { "mod_id", std::to_string(domain_and_mod->second) } }); +} + +void Api::untrackMod(const std::string& mod_url) +{ + auto domain_and_mod = extractDomainAndModId(mod_url); + if(!domain_and_mod) + throw std::runtime_error(std::format("Could not parse mod URL: \"{}\".", mod_url)); + const cpr::Response response = + cpr::Delete(cpr::Url("https://api.nexusmods.com/v1/user/tracked_mods.json"), + cpr::Header{ { "apikey", api_key_ } }, + cpr::Parameters{ { "domain_name", domain_and_mod->first }, + { "mod_id", std::to_string(domain_and_mod->second) } }); +} + +std::vector Api::getTrackedMods() +{ + cpr::Response response = cpr::Get(cpr::Url("https://api.nexusmods.com/v1/user/tracked_mods.json"), + cpr::Header{ { "apikey", api_key_ } }); + if(response.status_code != 200) + throw std::runtime_error(std::format( + "Failed to get tracked mods from NexusMods. Response code was: {}", response.status_code)); + + Json::Value json_body; + Json::Reader reader; + bool success = reader.parse(response.text.c_str(), json_body); + if(!success) + throw ParseError("Failed to parse response from NexusMods."); + + std::vector mods; + for(int i = 0; i < json_body.size(); i++) + mods.push_back( + getMod(json_body[i]["domain_name"].asString(), json_body[i]["mod_id"].asInt64())); + return mods; +} + +std::vector Api::getModFiles(const std::string& mod_url) +{ + auto domain_and_mod = extractDomainAndModId(mod_url); + if(!domain_and_mod) + throw std::runtime_error(std::format("Could not parse mod URL: \"{}\".", mod_url)); + + const auto [domain_name, mod_id] = *domain_and_mod; + cpr::Response response = + cpr::Get(cpr::Url(std::format( + "https://api.nexusmods.com/v1/games/{}/mods/{}/files.json", domain_name, mod_id)), + cpr::Header{ { "apikey", api_key_ } }); + if(response.status_code != 200) + throw std::runtime_error( + std::format("Failed to get mod files for mod with id {} from NexusMods. Response code was {}", + mod_id, + response.status_code)); + + Json::Value json_body; + Json::Reader reader; + bool success = reader.parse(response.text.c_str(), json_body); + if(!success) + throw ParseError("Failed to parse response from NexusMods."); + + std::vector files; + for(int i = 0; i < json_body["files"].size(); i++) + files.emplace_back(json_body["files"][i]); + return files; +} + +std::string Api::getDownloadUrl(const std::string& mod_url, long file_id) +{ + auto domain_and_mod = extractDomainAndModId(mod_url); + if(!domain_and_mod) + throw std::runtime_error(std::format("Could not parse mod URL: \"{}\".", mod_url)); + + const auto [domain_name, mod_id] = *domain_and_mod; + cpr::Response response = + cpr::Get(cpr::Url(std::format( + "https://api.nexusmods.com/v1/games/{}/mods/{}/files/{}/download_link.json", + domain_name, + mod_id, + file_id)), + cpr::Header{ { "apikey", api_key_ } }); + if(response.status_code == 403) + throw std::runtime_error( + "Generation of download links for NexusMods is restricted to premium accounts." + "You can download the mod on the website here:\n" + + std::format( + "https://www.nexusmods.com/{}/mods/{}?tab=files&file_id={}", domain_name, mod_id, file_id)); + else if(response.status_code == 404) + throw std::runtime_error("The requested file does not exist in NexusMods."); + else if(response.status_code != 200) + throw std::runtime_error(std::format("Failed to generate a download link for \"{}\"", mod_url)); + + Json::Value json_body; + Json::Reader reader; + bool success = reader.parse(response.text.c_str(), json_body); + if(!success) + throw ParseError("Failed to parse response from NexusMods."); + + return json_body[0]["URI"].asString(); +} + +std::string Api::getDownloadUrl(const std::string& nxm_url) +{ + const std::regex regex( + R"(nxm:\/\/(.+)\/mods\/(\d+)\/files\/(\d+)\?key=(.+)&expires=(\d+)&user_id=(\d+))"); + std::smatch match; + if(!std::regex_match(nxm_url, match, regex)) + throw std::runtime_error(std::format("Invalid NXM URL: \"{}\"", nxm_url)); + const std::string domain_name = match[1]; + const std::string mod_id = match[2]; + const std::string file_id = match[3]; + const std::string key = match[4]; + const std::string expires = match[5]; + + cpr::Response response = + cpr::Get(cpr::Url(std::format( + "https://api.nexusmods.com/v1/games/{}/mods/{}/files/{}/download_link.json", + domain_name, + mod_id, + file_id)), + cpr::Header{ { "apikey", api_key_ } }, + cpr::Parameters{ { "game_domain_name", domain_name }, + { "id", file_id }, + { "mod_id", mod_id }, + { "key", key }, + { "expires", expires } }); + if(response.status_code == 400) + throw std::runtime_error("Failed to generate download link. Check if the account used on " + "NexusMods matches the one for the API key in Limo."); + else if(response.status_code == 404) + throw std::runtime_error(std::format("File with id {} for mod with id {} for application" + "\"{}\" not found on NexusMods.", + file_id, + mod_id, + domain_name)); + else if(response.status_code == 410) + throw std::runtime_error("The NexusMods download link has expired."); + else if(response.status_code != 200) + throw std::runtime_error(std::format("Failed to generate download link for file with id {} " + "for mod with id {} for application {}.", + file_id, + mod_id, + domain_name)); + + Json::Value json_body; + Json::Reader reader; + bool success = reader.parse(response.text.c_str(), json_body); + if(!success) + throw ParseError("Failed to parse response from NexusMods."); + + return json_body[0]["URI"].asString(); +} + +std::vector>> Api::getChangelogs( + const std::string& mod_url) +{ + std::vector>> changelogs; + auto domain_and_mod = extractDomainAndModId(mod_url); + if(!domain_and_mod) + throw std::runtime_error(std::format("Could not parse mod URL: \"{}\".", mod_url)); + + const auto [domain_name, mod_id] = *domain_and_mod; + cpr::Response response = cpr::Get( + cpr::Url(std::format( + "https://api.nexusmods.com/v1/games/{}/mods/{}/changelogs.json", domain_name, mod_id)), + cpr::Header{ { "apikey", api_key_ } }); + if(response.status_code != 200) + throw std::runtime_error(std::format( + "Failed to get changelogs for mod with id {} from NexusMods. Response code was {}", + mod_id, + response.status_code)); + + Json::Value json_body; + Json::Reader reader; + bool success = reader.parse(response.text.c_str(), json_body); + if(!success) + throw ParseError("Failed to parse response from NexusMods."); + + std::string text = response.text; + if(text.starts_with('\"')) + text.erase(0, 1); + if(text.ends_with('\"')) + text.erase(text.size() - 1, 1); + + for(const auto& key : json_body.getMemberNames()) + { + std::vector changes; + auto log = json_body[key]; + for(int i = 0; i < log.size(); i++) + changes.push_back(log[i].asString()); + changelogs.emplace_back(key, changes); + } + // Jsoncpp uses a std::map to store key, value pairs. This messes up the order of the keys, so + // they have be re-sorted by version number + std::sort(changelogs.begin(), + changelogs.end(), + [](auto a, auto b) + { + std::regex regex(R"(.*?(\d+)\.?(.*))"); + std::smatch match; + std::vector a_parts; + std::vector b_parts; + std::string target = a.first; + bool found = false; + while(std::regex_search(target, match, regex)) + { + found = true; + a_parts.push_back(std::stoi(match[1])); + target = match[2]; + } + if(!found) + return a > b; + + found = false; + target = b.first; + while(std::regex_search(target, match, regex)) + { + found = true; + b_parts.push_back(std::stoi(match[1])); + target = match[2]; + } + if(!found) + return a > b; + + for(auto [a_num, b_num] : str::zip_view(a_parts, b_parts)) + { + if(a_num != b_num) + return a_num > b_num; + } + return a > b; + }); + return changelogs; +} + +bool Api::modUrlIsValid(const std::string& url) +{ + if(url.empty()) + return false; + const std::regex regex(R"((?:https:\/\/)?www\.nexusmods\.com\/(.+)\/mods\/(\d+).*)"); + return std::regex_match(url, regex); +} + +Page Api::getNexusPage(const std::string& mod_url) +{ + return { mod_url, getMod(mod_url), getChangelogs(mod_url), getModFiles(mod_url) }; +} + +std::optional> Api::validateKey(const std::string& api_key) +{ + cpr::Response response = cpr::Get(cpr::Url("https://api.nexusmods.com/v1/users/validate.json"), + cpr::Header{ { "apikey", api_key } }); + if(response.status_code != 200) + return {}; + + Json::Value json_body; + Json::Reader reader; + bool success = reader.parse(response.text.c_str(), json_body); + if(!success) + throw ParseError("Failed to parse response from NexusMods."); + + return { { json_body["name"].asString(), json_body["is_premium"].asBool() } }; +} + +std::string Api::getNexusPageUrl(const std::string& nxm_url) +{ + std::regex nxm_regex(R"(nxm:\/\/(.*)\/mods\/(\d+)\/files\/\d+\?.*)"); + std::smatch match; + if(!std::regex_match(nxm_url, match, nxm_regex)) + throw std::runtime_error("Invalid nxm url: \"" + nxm_url + "\"."); + return std::format("https://www.nexusmods.com/{}/mods/{}", match[1].str(), match[2].str()); +} + +std::string Api::getApiKey() +{ + return api_key_; +} + +std::optional> Api::extractDomainAndModId(const std::string& mod_url) +{ + const std::regex regex(R"((?:https:\/\/)?www\.nexusmods\.com\/(.+)\/mods\/(\d+).*)"); + std::smatch match; + if(std::regex_match(mod_url, match, regex)) + return { { match[1], std::stoi(match[2]) } }; + return {}; +} diff --git a/src/core/nexus/api.h b/src/core/nexus/api.h new file mode 100644 index 0000000..a01a887 --- /dev/null +++ b/src/core/nexus/api.h @@ -0,0 +1,152 @@ +/*! + * \file Api.h + * \brief Header for the nexus::Api class. + */ + +#pragma once + +#include "file.h" +#include "mod.h" +#include +#include + + +/*! + * \brief The nexus namespace contains structs and functions needed for accessing the NexusMods API. + */ +namespace nexus +{ +/*! + * \brief Contains all data for a mod available through the NexusMods api. + */ +struct Page +{ + /*! \brief URL of the mod page on NexusMods. */ + std::string url; + /*! \brief Contains an overview of of the mod page, like a description and summary. */ + Mod mod; + /*! \brief For every Version of the mod: A vector of changes in that version. */ + std::vector>> changelog; + /*! \brief Contains data on all available files for the mod. */ + std::vector files; +}; + +/*! + * \brief Provides functions for accessing the NexusMods API. + */ +class Api +{ +public: + /*! \brief This is an abstract class, so the constructor is deleted. */ + Api() = delete; + + /*! + * \brief Sets the API key to use for all operations. + * \param api_key The new API key. + */ + static void setApiKey(const std::string& api_key); + /*! + * \brief Checks if this class has been initialized with an API key. + * Does NOT check if the key works. + * \return True if an API key exists. + */ + static bool isInitialized(); + /*! + * \brief Fetches data for the mod accessible by the given NexusMods URL. + * \param mod_url URL to the mod on NexusMods. + * \return A Mod object containing all received data. + */ + static Mod getMod(const std::string& mod_url); + /*! + * \brief Fetches data for the mod specified by the NexusMods domain and mod id. + * \param domain_name The NexusMods domain containing the mod. + * \param mod_id Target mod id. + * \return A Mod object containing all received data. + */ + static Mod getMod(const std::string& domain_name, long mod_id); + /*! + * \brief Tracks the mod for the NexusMods account belonging to the API key. + * \param mod_url URL to the mod on NexusMods. + */ + static void trackMod(const std::string& mod_url); + /*! + * \brief Tracks the mod for the NexusMods account belonging to the API key. + * \param mod_url URL to the mod on NexusMods. + */ + static void untrackMod(const std::string& mod_url); + /*! + * \brief Fetches data for all mods tracked by the account belonging to the API key. + * \return A vector of Mod objects with the received data. + */ + static std::vector getTrackedMods(); + /*! + * \brief Fetches data for all available files for the given mod. + * \param mod_url URL to the mod on NexusMods. + * \return A vector of File objects containing the received data. + */ + static std::vector getModFiles(const std::string& mod_url); + /*! + * \brief Generates a download URL for the given mod file. This only works for premium accounts. + * \param mod_url URL to the mod on NexusMods. + * \param file_id Id of the file for which a link is to be generated. + * \return The download URL. + */ + static std::string getDownloadUrl(const std::string& mod_url, long file_id); + /*! + * \brief Generates a download URL from the given nxm Url. + * \param nxm_url The nxm Url used. This is usually generated through the NexusMods website. + * \return The download URL. + */ + static std::string getDownloadUrl(const std::string& nxm_url); + /*! + * \brief Fetches changelogs for the given mod. + * \param mod_url URL to the mod on NexusMods. + * \return For every Version of the mod: A vector of changes in that version. + */ + static std::vector>> getChangelogs( + const std::string& mod_url); + /*! + * \brief Checks if the given URL is a valid NexusMods mod page URL. + * Only verifies if the URL is semantically correct, not if the target exists. + * \param url URL to check. + * \return True if the URL points to a NexusMods page. + */ + static bool modUrlIsValid(const std::string& url); + /*! + * \brief Fetches data to fill a Page object for the given mod. + * \param mod_url URL to the mod on NexusMods. + * \return The generated Page object. + */ + static Page getNexusPage(const std::string& mod_url); + /*! + * \brief Checks if the NexusMods API can be accessed with the given API key. + * \param api_key API key to validate. + * \return If the key works: The account name and a bool indicating if the account is premium. + * Else: An empty std::optional. + */ + static std::optional> validateKey(const std::string& api_key); + /*! + * \brief Generates a NexusMods mod page URL from the given nxm URL. + * \param nxm_url The nxm Url used. This is usually generated through the NexusMods website. + * \return The NexusMods mod page URL. + */ + static std::string getNexusPageUrl(const std::string& nxm_url); + /*! + * \brief Getter for the API key. + * \return The API key. + */ + static std::string getApiKey(); + +private: + /*! \brief The API key used for all operations. */ + inline static std::string api_key_ = ""; + + /*! + * \brief Extracts the NexusMods domain and mod id from the given mod page URL. + * \param url URL to the mod on NexusMods. + * \return If the given URL is valid: The domain and mod id. Else an empty std::optional. + */ + static std::optional> extractDomainAndModId( + const std::string& mod_url); +}; +} diff --git a/src/core/nexus/file.cpp b/src/core/nexus/file.cpp new file mode 100644 index 0000000..1203a81 --- /dev/null +++ b/src/core/nexus/file.cpp @@ -0,0 +1,44 @@ +#include "file.h" +#include "../parseerror.h" + +using namespace nexus; + + +File::File(const std::string& http_body) +{ + Json::Value json_body; + Json::Reader reader; + bool success = reader.parse(http_body.c_str(), json_body); + if(!success) + throw ParseError("Failed to parse response from NexusMods."); + + init(json_body); +} + +File::File(const Json::Value& json_body) +{ + init(json_body); +} + +void File::init(const Json::Value& json_body) +{ + id_0 = json_body["id"][0].asInt64(); + id_1 = json_body["id"][1].asInt64(); + uid = json_body["uid"].asInt64(); + file_id = json_body["file_id"].asInt64(); + name = json_body["name"].asString(); + version = json_body["version"].asString(); + category_id = json_body["category_id"].asInt64(); + category_name = json_body["category_name"].asString(); + is_primary = json_body["is_primary"].asBool(); + size = json_body["size"].asInt64(); + file_name = json_body["file_name"].asString(); + uploaded_time = json_body["uploaded_timestamp"].asInt64(); + mod_version = json_body["mod_version"].asString(); + external_virus_scan_url = json_body["external_virus_scan_url"].asString(); + description = json_body["description"].asString(); + size_kb = json_body["size_kb"].asInt64(); + size_in_bytes = json_body["size_in_bytes"].asInt64(); + changelog_html = json_body["changelog_html"].asString(); + content_preview_link = json_body["content_preview_link"].asString(); +} diff --git a/src/core/nexus/file.h b/src/core/nexus/file.h new file mode 100644 index 0000000..7c3cff2 --- /dev/null +++ b/src/core/nexus/file.h @@ -0,0 +1,86 @@ +/*! + * \file file.h + * \brief Header for the nexus::File class. + */ + +#pragma once + +#include +#include +#include + + +/*! + * \brief The nexus namespace contains structs and functions needed for accessing the NexusMods API. + */ +namespace nexus +{ +/*! + * \brief Contains data for a file on NexusMods. + */ +class File +{ +public: + /*! + * \brief Constructor. Initializes all members from the given http response body generated + * through an API request. + * \param http_body The http response body. + */ + File(const std::string& http_body); + /*! + * \brief Constructor. Initializes all members from the given http response body in json form + * generated through an API request. + * \param http_body The http response body in json form. + */ + File(const Json::Value& json_body); + /*! \brief Default constructor. */ + File() = default; + + /*! \brief The file id. */ + long id_0; + /*! \brief The id of the domain containing mod to which the file belongs. */ + long id_1; + /*! \brief Purpose unknown. */ + long uid; + /*! \brief The file id. */ + long file_id; + /*! \brief The name of the actual file on disk. */ + std::string name; + /*! \brief The files version. */ + std::string version; + /*! \brief Id of the category to which the file belongs. */ + long category_id; + /*! \brief Name of the category to which the file belongs, e.g. MAIN. */ + std::string category_name; + /*! \brief Purpose unknown. */ + bool is_primary; + /*! \brief Size of the file in KibiBytes. */ + long size; + /*! \brief The files display name- */ + std::string file_name; + /*! \brief Timestamp for when the file was uploaded to NexusMods. */ + std::time_t uploaded_time; + /*! \brief Mod version to which the file belongs. */ + std::string mod_version; + /*! \brief Optional: The URL of a virus scanning website (like virustotal.com) for this file. */ + std::string external_virus_scan_url; + /*! \brief The description if the file. */ + std::string description; + /*! \brief Size of the file in KibiBytes. */ + long size_kb; + /*! \brief Size of the file in Bytes. */ + long size_in_bytes; + /*! \brief The changelog if the file. */ + std::string changelog_html; + /*! \brief A URL of a NexusMods site showing a preview of the files contents. */ + std::string content_preview_link; + +private: + /*! + * \brief Initializes all members from the given http response body in json form + * generated through an API request. + * \param http_body The http response body in json form. + */ + void init(const Json::Value& json_body); +}; +} diff --git a/src/core/nexus/mod.cpp b/src/core/nexus/mod.cpp new file mode 100644 index 0000000..b9d6c1b --- /dev/null +++ b/src/core/nexus/mod.cpp @@ -0,0 +1,52 @@ +#include "mod.h" +#include "../parseerror.h" +#include + +using namespace nexus; + + +Mod::Mod(const std::string& http_body) +{ + Json::Value json_body; + Json::Reader reader; + bool success = reader.parse(http_body.c_str(), json_body); + if(!success) + throw ParseError("Failed to parse response from NexusMods."); + + init(json_body); +} + +Mod::Mod(const Json::Value& json_body) +{ + init(json_body); +} + +void Mod::init(const Json::Value& json_body) +{ + name = json_body["name"].asString(); + summary = json_body["summary"].asString(); + description = json_body["description"].asString(); + picture_url = json_body["picture_url"].asString(); + mod_downloads = json_body["mod_downloads"].asInt64(); + mod_unique_downloads = json_body["mod_unique_downloads"].asInt64(); + uid = json_body["uid"].asInt64(); + mod_id = json_body["mod_id"].asInt64(); + game_id = json_body["game_id"].asInt64(); + allow_rating = json_body["allow_rating"].asBool(); + domain_name = json_body["domain_name"].asString(); + category_id = json_body["category_id"].asInt64(); + version = json_body["version"].asString(); + endorsement_count = json_body["endorsement_count"].asInt64(); + created_time = json_body["created_timestamp"].asInt64(); + updated_time = json_body["updated_timestamp"].asInt64(); + author = json_body["author"].asString(); + uploaded_by = json_body["uploaded_by"].asString(); + uploaded_users_profile_url = json_body["uploaded_users_profile_url"].asString(); + contains_adult_content = json_body["contains_adult_content"].asBool(); + status = json_body["status"].asString(); + available = json_body["available"].asBool(); + user_member_id = json_body["user"]["member_id"].asInt64(); + user_member_group_id = json_body["user"]["member_group_id"].asInt64(); + user_name = json_body["user"]["name"].asString(); + endorsement_status = json_body["endorsement"]["endorse_status"].asString(); +} diff --git a/src/core/nexus/mod.h b/src/core/nexus/mod.h new file mode 100644 index 0000000..17cb276 --- /dev/null +++ b/src/core/nexus/mod.h @@ -0,0 +1,100 @@ +/*! + * \file mod.h + * \brief Header for the nexus::Mod class. + */ + +#pragma once + +#include +#include +#include + + +/*! + * \brief The nexus namespace contains structs and functions needed for accessing the NexusMods API. + */ +namespace nexus +{ +/*! + * \brief Contains data for a mod on NexusMods. + */ +class Mod +{ +public: + /*! \brief Default constructor. */ + Mod() = default; + /*! + * \brief Constructor. Initializes all members from the given http response body generated + * through an API request. + * \param http_body The http response body. + */ + Mod(const std::string& http_body); + /*! + * \brief Constructor. Initializes all members from the given http response body in json form + * generated through an API request. + * \param http_body The http response body in json form. + */ + Mod(const Json::Value& json_body); + + /*! \brief Name of the mod. */ + std::string name; + /*! \brief A summary of the mods contents. */ + std::string summary; + /*! \brief The long form description of the mod. */ + std::string description; + /*! \brief URL of the main image representing the mod. */ + std::string picture_url; + /*! \brief Total number of downloads for the mod. */ + long mod_downloads; + /*! \brief Total number of unique downloads for the mod. */ + long mod_unique_downloads; + /*! \brief Purpose unknown. */ + long uid; + /*! \brief NexusMods mod id. */ + long mod_id; + /*! \brief Id of the NexusMods domain containing the mod. */ + long game_id; + /*! \brief If true: Mod can be rated. */ + bool allow_rating; + /*! \brief Name of the NexusMods domain containing the mod. */ + std::string domain_name; + /*! \brief Id of the NexusMods mod category for the mod. */ + long category_id; + /*! \brief Most recent mod version. */ + std::string version; + /*! \brief Number of endorsements of the mod. */ + long endorsement_count; + /*! \brief Timestamp for when the mod was first uploaded to NexusMods. */ + std::time_t created_time; + /*! \brief Timestamp for when the mod was first last updated. */ + std::time_t updated_time; + /*! \brief Name of the mods author. */ + std::string author; + /*! \brief Name of the mod uploader. */ + std::string uploaded_by; + /*! \brief URL to the NexusMods account which uploaded the mod. */ + std::string uploaded_users_profile_url; + /*! \brief True if the mod contains adult content. */ + bool contains_adult_content; + /*! \brief The current status of the mod, e.g. Published. */ + std::string status; + /*! \brief True if the mod is available........ */ + bool available; + /*! \brief User id of the uploader. */ + long user_member_id; + /*! \brief A group id for the uploader. */ + long user_member_group_id; + /*! \brief Name of the uploader. */ + std::string user_name; + /*! \brief Endorsement status of the mod for the account used to fetch the mod data. */ + std::string endorsement_status; + +private: + /*! + * \brief Initializes all members from the given http response body in json form + * generated through an API request. + * \param http_body The http response body in json form. + */ + void init(const Json::Value& json_body); +}; +} diff --git a/src/core/parseerror.h b/src/core/parseerror.h new file mode 100644 index 0000000..2a66ac0 --- /dev/null +++ b/src/core/parseerror.h @@ -0,0 +1,27 @@ +/*! + * \file parseerror.h + * \brief Contains the ParseError class. + */ + +#pragma once + +#include + + +/*! + * \brief Exception indicating an error while parsing a JSON file. + */ +class ParseError : public std::runtime_error +{ +public: + /*! + * \brief Constructor. + * \param message Message for the exception. + */ + ParseError(const char* message) : std::runtime_error(message) {} + /*! + * \brief Constructor. + * \param message Message for the exception. + */ + ParseError(const std::string& message) : std::runtime_error(message) {} +}; diff --git a/src/core/pathutils.cpp b/src/core/pathutils.cpp new file mode 100644 index 0000000..c6b89e3 --- /dev/null +++ b/src/core/pathutils.cpp @@ -0,0 +1,201 @@ +#include "pathutils.h" +#include +#include +#include + +namespace sfs = std::filesystem; + + +namespace path_utils +{ +std::optional pathExists(const sfs::path& path_to_check, + const sfs::path& base_path, + bool case_insensitive) +{ + if(sfs::exists(base_path / path_to_check)) + return path_to_check; + if(!case_insensitive) + return {}; + const sfs::path target = + path_to_check.string().ends_with("/") ? path_to_check.parent_path() : path_to_check; + + sfs::path actual_path; + int i = 0; + for(auto iter = target.begin(); iter != target.end(); iter++) + { + if(sfs::exists(base_path / actual_path / *iter)) + { + actual_path /= *iter; + continue; + } + + std::string lower_part = toLowerCase(*iter); + bool found = false; + for(const auto& dir_entry : sfs::directory_iterator(base_path / actual_path)) + { + const sfs::path path_end = *(std::prev(dir_entry.path().end())); + std::string lower_case_path_end = toLowerCase(path_end); + std::string actual_case_path_end = path_end.string(); + if(lower_case_path_end == lower_part) + { + actual_path /= actual_case_path_end; + found = true; + break; + } + } + if(!found) + return {}; + } + return actual_path; +} + +std::string toLowerCase(const sfs::path& path) +{ + auto path_string = path.string(); + std::transform(path_string.begin(), + path_string.end(), + path_string.begin(), + [](unsigned char c) { return std::tolower(c); }); + return path_string; +} + +void moveFilesToDirectory(const sfs::path& source, const sfs::path& destination, bool move) +{ + if(!sfs::exists(destination)) + sfs::create_directories(destination); + for(const auto& dir_entry : sfs::directory_iterator(source)) + { + const auto relative_path = getRelativePath(dir_entry.path(), source); + if(sfs::exists(destination / relative_path)) + { + if(sfs::is_directory(destination / relative_path)) + moveFilesToDirectory(dir_entry.path(), destination / relative_path, move); + else + { + sfs::remove(destination / relative_path); + copyOrMoveFiles(dir_entry.path(), destination / relative_path, move); + } + continue; + } + copyOrMoveFiles(dir_entry.path(), destination / relative_path, move); + } + if(sfs::exists(source) && move) + sfs::remove_all(source); +} + +std::string normalizePath(const std::string& path) +{ + return std::regex_replace(path, std::regex(R"(\\)"), "/"); +} + +std::string getRelativePath(sfs::path target, sfs::path source) +{ + std::string relative_path = target.string(); + relative_path.erase(0, source.string().size() + 1); + return relative_path; +} + +bool directoryIsEmpty(const sfs::path& directory) +{ + if(!sfs::is_directory(directory)) + return false; + for(const auto& dir_entry : sfs::recursive_directory_iterator(directory)) + { + if(!dir_entry.is_directory()) + return false; + } + return true; +} + +int getPathLength(const sfs::path& path) +{ + int length = 0; + for(const auto& e : path) + length++; + return length; +} + +std::pair removePathComponents(const sfs::path& path, int depth) +{ + sfs::path short_path; + sfs::path head; + int cur_depth = 0; + for(auto it = path.begin(); it != path.end(); it++, cur_depth++) + { + if(cur_depth >= depth) + short_path /= *it; + else + head /= *it; + } + return { head, short_path }; +} + +void renameFiles(const sfs::path& destination, + const sfs::path& source, + std::function converter) +{ + std::vector old_directories; + for(const auto& dir_entry : sfs::recursive_directory_iterator(source)) + { + auto relative_path = getRelativePath(dir_entry.path(), source); + std::string old_path = relative_path; + std::transform(relative_path.begin(), relative_path.end(), relative_path.begin(), converter); + if(dir_entry.is_directory()) + { + if(old_path != relative_path) + old_directories.push_back(dir_entry.path()); + continue; + } + if(!sfs::exists((destination / relative_path).parent_path())) + sfs::create_directories((destination / relative_path).parent_path()); + sfs::rename(dir_entry.path(), destination / relative_path); + } + if(source == destination) + { + for(const auto& dir : old_directories) + { + if(sfs::exists(dir)) + sfs::remove_all(dir); + } + } + else + sfs::remove_all(source); +} + +void moveFilesWithDepth(const sfs::path& source, const sfs::path& destination, int depth) +{ + std::set> files_to_move; + for(const auto& dir_entry : sfs::recursive_directory_iterator(source)) + { + const auto [head, short_path] = + removePathComponents(getRelativePath(dir_entry.path(), source), depth); + if(short_path != "") + files_to_move.emplace(dir_entry.path(), destination / short_path); + } + + for(const auto& [cur_source, cur_dest] : files_to_move) + { + if(sfs::is_directory(cur_source)) + sfs::create_directories(cur_dest); + else + { + if(sfs::exists(cur_dest)) + throw std::runtime_error("Error: Duplicate file detected: \"" + + getRelativePath(cur_source, source) + "\"!"); + if(cur_dest.has_parent_path()) + sfs::create_directories(cur_dest.parent_path()); + sfs::rename(cur_source, cur_dest); + } + } + sfs::remove_all(source); +} + +void copyOrMoveFiles(const sfs::path& source, const sfs::path& destination, bool move) +{ + if(move) + sfs::rename(source, destination); + else + sfs::copy(source, destination, sfs::copy_options(sfs::copy_options::recursive)); +} + +} diff --git a/src/core/pathutils.h b/src/core/pathutils.h new file mode 100644 index 0000000..ad77fe5 --- /dev/null +++ b/src/core/pathutils.h @@ -0,0 +1,110 @@ +/*! + * \file pathutils.h + * \brief Header for the path_utils namespace. + */ + +#pragma once + +#include +#include +#include + + +/*! + * \brief Contains utility functions for dealing with std::filesystem::path objects. + */ +namespace path_utils +{ +/*! + * \brief Checks if the target path exists. + * \param target Path to check. + * \param base_path If specified, target path is appended to this path during the search. + * \param case_insensitive If true: Ignore case mismatch for path search. + * \return The target path in its actual case, if found. + */ +std::optional pathExists(const std::filesystem::path& path_to_check, + const std::filesystem::path& base_path, + bool case_insensitive = true); + +/*! + * \brief Returns a string containing the given path in lower case. + * \param path Path to be converted. + * \return The lower case path. + */ +std::string toLowerCase(const std::filesystem::path& path); +/*! + * \brief Recursively moves all files from the source directory to the target directory. + * \param source Source directory. + * \param destination Target directory. + * \param move If false: Copy files instead of moving them. + */ +void moveFilesToDirectory(const std::filesystem::path& source, + const std::filesystem::path& destination, + bool move = true); +/*! + * \brief Replaces all double backslash path separators with a forward slash. + * \return The normalized path. + */ +std::string normalizePath(const std::string& path); +/*! + * \brief Determines the relative path from source to target. Only works if source.string() + * is a sub-string of target.string(). + * \param target Target path. + * \param source Source path. + * \return The relative path. + */ +std::string getRelativePath(std::filesystem::path target, std::filesystem::path source); +/*! + * \brief Returns true if directory is empty or contains only empty directories. + * \param directory Directory to check. + * \return True if empty, else false. + */ +bool directoryIsEmpty(const std::filesystem::path& directory); +/*! + * \brief Returns the number of elements in given path. + * \param path Path to be checked. + * \return The length. + */ +int getPathLength(const std::filesystem::path& path); +/*! + * \brief Removes the first components of a given path. + * \param path Source path. + * \param depth Components with depth < this will be removed. + * \return A pair of the removed components and the shortened path. + */ +std::pair removePathComponents( + const std::filesystem::path& path, + int depth); +/*! + * \brief Recursively renames all files at given source directory using given converter, + * then copies the result to given destination directory. + * \param destination Path to destination directory for renamed files. + * \param source Path to source files to be renamed. + * \param converter Function which converts one char to another, e.g. converting to + * upper case. + */ +void renameFiles(const std::filesystem::path& destination, + const std::filesystem::path& source, + std::function converter); +/*! + * \brief Recursively moves all files from source to destination, removes all + * path components with depth < root_level. + * \param source Source path. + * \param destination Destination path. + * \param depth Minimum depth for path components to keep. + */ +void moveFilesWithDepth(const std::filesystem::path& source, + const std::filesystem::path& destination, + int depth); + +/*! + * \brief Copies or moves files from source to dest. + * \param source Copy/ move source path. + * \param destination Copy/ move target path. + * \param move If true: Move files, else: Recursively copy files. + */ +void copyOrMoveFiles(const std::filesystem::path& source, + const std::filesystem::path& destination, + bool move); + +} diff --git a/src/core/progressnode.cpp b/src/core/progressnode.cpp new file mode 100644 index 0000000..494ce43 --- /dev/null +++ b/src/core/progressnode.cpp @@ -0,0 +1,107 @@ +#include "progressnode.h" +#include +#include + + +ProgressNode::ProgressNode(int id, + const std::vector& weights, + std::optional parent) : id_(id), parent_(parent) +{ + addChildren(weights); +} + +ProgressNode::ProgressNode(std::function progress_callback, + const std::vector& weights) +{ + addChildren(weights); + setProgressCallback(progress_callback); +} + +void ProgressNode::advance(uint64_t num_steps) +{ + if(!children_.empty()) + throw std::runtime_error("Cannot advance progress for a node with children."); + cur_step_ += num_steps; + if(total_steps_ == 0) + progress_ = 1.0f; + else + progress_ = std::min(static_cast(cur_step_) / total_steps_, 1.0f); + propagateProgress(); +} + +int ProgressNode::totalSteps() const +{ + return total_steps_; +} + +void ProgressNode::setTotalSteps(uint64_t total_steps) +{ + if(!children_.empty()) + throw std::runtime_error("Cannot set total steps for a node with children."); + total_steps_ = total_steps; +} + +int ProgressNode::id() const +{ + return id_; +} + +void ProgressNode::addChildren(const std::vector& weights) +{ + weights_ = weights; + float sum = std::accumulate(weights_.begin(), weights_.end(), 0.0f); + if(sum == 0.0f) + sum = 1.0f; + for(auto& weight : weights_) + weight = std::abs(weight / sum); + for(int i = 0; i < weights_.size(); i++) + children_.push_back({ i, {}, this }); +} + +ProgressNode& ProgressNode::child(int id) +{ + int* i; + return children_[id]; +} + +void ProgressNode::setProgressCallback(std::function progress_callback) +{ + set_progress_ = progress_callback; + set_progress_(progress_); +} + +float ProgressNode::updateStepSize() const +{ + return update_step_size_; +} + +void ProgressNode::setUpdateStepSize(float step_size) +{ + update_step_size_ = step_size; +} + +float ProgressNode::getProgress() const +{ + return progress_; +} + +void ProgressNode::updateProgress() +{ + progress_ = 0.0f; + for(int i = 0; i < weights_.size(); i++) + progress_ += weights_[i] * children_[i].progress_; + propagateProgress(); +} + +void ProgressNode::propagateProgress() +{ + if(parent_) + (*parent_)->updateProgress(); + else if(progress_ - prev_progress_ > update_step_size_ || + std::abs(1.0f - progress_) <= std::numeric_limits::epsilon() && + std::abs(1.0f - prev_progress_) > std::numeric_limits::epsilon()) + { + set_progress_(progress_); + prev_progress_ = progress_; + } +} diff --git a/src/core/progressnode.h b/src/core/progressnode.h new file mode 100644 index 0000000..c09b3e3 --- /dev/null +++ b/src/core/progressnode.h @@ -0,0 +1,131 @@ +/*! + * \file progressnode.h + * \brief Header for the ProgressNode class. + */ + +#pragma once + +#include +#include +#include +#include + + +/*! + * \brief Represents a node in a tree used to track the progress of a task. + * + * Each node in the tree represents the progress in a sub-task. Each sub-task has + * a weight associated to it, which should be proportional to the time this task takes + * to be completed. + */ +class ProgressNode +{ +public: + /*! + * \brief Constructor. + * \param id Id of this node. Used to index weights and children of parent. + * \param weights If not empty: Weights of sub-tasks. + * \param parent Parent of this node. If empty: This is a root node. + */ + ProgressNode(int id, const std::vector& weights, std::optional parent); + /*! + * \brief Constructor for a root node. + * \param progress_callback a callback function used by the root node to inform about + * changes in the task progress. + * \param weights If not empty: Weights of sub-tasks. + */ + ProgressNode(std::function progress_callback, + const std::vector& weights = {}); + + /*! + * \brief Advances the current progress of this node by the given amount of steps. + * This must be a leaf node. + * \param num_steps Number steps to advance. + */ + void advance(uint64_t num_steps = 1); + /*! + * \brief Returns the total number of steps in this task. + * \return The number of steps. + */ + int totalSteps() const; + /*! + * \brief Sets the total number of steps in this task. + * \param total_steps The number of steps. + */ + void setTotalSteps(uint64_t total_steps); + /*! + * \brief Returns the id of this node. + * \return The id. + */ + int id() const; + /*! + * \brief Adds new child nodes with given weights to this node. + * \param weights The child weights. + */ + void addChildren(const std::vector& weights); + /*! + * \brief Returns a reference to the child with the given id. + * \param id Target child id. + * \return The child. + */ + ProgressNode& child(int id); + /*! + * \brief Sets a callback function used by the root node to inform about changes in the + * task progress. + * \param set_progress The callback function. + */ + void setProgressCallback(std::function progress_callback); + /*! + * \brief Returns the minimal progress interval after which the progress callback is called. + * \return The interval. + */ + float updateStepSize() const; + /*! + * \brief Sets the minimal progress interval after which the progress callback is called. + * \param step_size The interval. + */ + void setUpdateStepSize(float step_size); + /*! + * \brief Returns the current progress. + * \return The progress. + */ + float getProgress() const; + +private: + /*! \brief This nodes id. */ + int id_; + /*! \brief Current step in this task. Only used for leaf nodes. */ + uint64_t cur_step_ = 0; + /*! \brief Number of total steps in this task. Only used for leaf nodes. */ + uint64_t total_steps_; + /*! \brief Current progress in this task. */ + float progress_ = 0.0f; + /*! \brief Progress at the time of the last call to \ref set_progress_. */ + float prev_progress_ = 0.0f; + /*! \brief minimal progress interval after which \ref set_progress_ is called. */ + float update_step_size_ = 0.01f; + /*! \brief The parent of this, if this is not the root. */ + std::optional parent_; + /*! \brief Weights of children. */ + std::vector weights_; + /*! \brief Children representing sub-tasks of this task. */ + std::vector children_; + + /*! + * \brief Callback function used by the root node to inform about changes in the + * task progress. + */ + std::function set_progress_ = [](float f) {}; + /*! + * \brief Sets the current progress of this node to the weighted sum of the current + * progresses of its children. + */ + void updateProgress(); + /*! + * \brief Informs this nodes parent of a change in progress. + * + * If this is a root node and the change of progress since the last update exceeds + * \ref update_step_size_ : Call \ref set_progress_. + */ + void propagateProgress(); +}; diff --git a/src/core/tag.cpp b/src/core/tag.cpp new file mode 100644 index 0000000..0ff688d --- /dev/null +++ b/src/core/tag.cpp @@ -0,0 +1,29 @@ +#include "tag.h" + +namespace str = std::ranges; + + +std::string Tag::getName() const +{ + return name_; +} + +void Tag::setName(const std::string& name) +{ + name_ = name; +} + +std::vector Tag::getMods() const +{ + return mods_; +} + +int Tag::getNumMods() const +{ + return mods_.size(); +} + +bool Tag::hasMod(int mod_id) const +{ + return str::find(mods_, mod_id) != mods_.end(); +} diff --git a/src/core/tag.h b/src/core/tag.h new file mode 100644 index 0000000..d91496c --- /dev/null +++ b/src/core/tag.h @@ -0,0 +1,57 @@ +/*! + * \file tag.h + * \brief Header for the Tag class. + */ + +#pragma once + +#include +#include +#include + + +/*! + * \brief Abstract base class for a tag assigned to a set of mods. + */ +class Tag +{ +public: + /*! + * \brief Getter for the tags name. + * \return The name. + */ + std::string getName() const; + /*! + * \brief Setter for the tags name. + * \param name The new name. + */ + void setName(const std::string& name); + /*! + * \brief Returns all mods to which this tag has been added. + * \return A vector of mods ids. + */ + std::vector getMods() const; + /*! + * \brief Returns the number of mods to which this tag has been added. + * \return The number of mods. + */ + int getNumMods() const; + /*! + * \brief Checks if this tag has been added to the given mod. + * \param mod_id Mod to be checked. + * \return True if the given mod has this tag. + */ + bool hasMod(int mod_id) const; + /*! + * \brief Serializes this tag to a json object. + * This function must be implemented by derived classes. + * \return The json object. + */ + virtual Json::Value toJson() const = 0; + +protected: + /*! \brief Name of this tag. */ + std::string name_; + /*! \brief Contains ids of all mods to which this tag has been added. */ + std::vector mods_{}; +}; diff --git a/src/core/tagcondition.h b/src/core/tagcondition.h new file mode 100644 index 0000000..1a217cc --- /dev/null +++ b/src/core/tagcondition.h @@ -0,0 +1,34 @@ +/*! + * \file tagcondition.h + * \brief Contains the TagCondition struct. + */ + +#pragma once + +#include + + +/*! + * \brief Contains data relevant to describing a single condition used for the application + * of auto tags. This is used to construct a TagConditionNode. + */ +struct TagCondition +{ + /*! \brief Represents what should be compared to the search string. */ + enum class Type + { + /*! \brief Match against relative path, including file name. */ + path, + /*! \brief Match against file name only. */ + file_name + }; + + /*! \brief If true: Matches only if condition is NOT met. */ + bool invert; + /*! \brief Describes against what the search string should be matched. */ + Type condition_type; + /*! \brief If true: Use regex matching, else use case insensitive matching with wildcards. */ + bool use_regex; + /*! \brief This string will be matched against a given path. */ + std::string search_string; +}; diff --git a/src/core/tagconditionnode.cpp b/src/core/tagconditionnode.cpp new file mode 100644 index 0000000..982c2a4 --- /dev/null +++ b/src/core/tagconditionnode.cpp @@ -0,0 +1,442 @@ +#include "tagconditionnode.h" +#include +#include +#include +#include + +namespace str = std::ranges; +namespace sfs = std::filesystem; + + +TagConditionNode::TagConditionNode() +{ + expression_ = ""; + invert_ = false; + children_ = {}; + type_ = Type::empty; + condition_ = ""; + condition_strings_ = {}; + condition_id_ = -1; + use_regex_ = false; +} + +TagConditionNode::TagConditionNode(std::string expression, + const std::vector& conditions) : + expression_(expression) +{ + if(expression == "") + { + type_ = Type::empty; + return; + } + if(!expressionIsValid(expression, conditions.size())) + throw std::runtime_error(std::format("Invalid expression '{}'", expression)); + + std::transform(expression.begin(), + expression.end(), + expression.begin(), + [](unsigned char c) { return std::tolower(c); }); + removeWhitespaces(expression); + removeEnclosingParentheses(expression); + auto tokens = tokenize(expression); + while(tokens.size() == 1 && expression.compare(0, 3, "not") == 0) + { + invert_ = !invert_; + expression.erase(expression.begin(), expression.begin() + 3); + removeEnclosingParentheses(expression); + tokens = tokenize(expression); + } + if(tokens.size() == 1) + { + if(expression.find_first_not_of("0123456789") != std::string::npos) + throw std::runtime_error( + std::format("Error: Could not parse condition in expression '{}'", expression)); + int condition_index = std::stoi(expression); + if(condition_index >= conditions.size()) + throw std::runtime_error(std::format( + "Error: Condition index {} out of range in expression '{}'", condition_index, expression)); + condition_id_ = condition_index; + type_ = conditions[condition_index].condition_type == TagCondition::Type::path + ? Type::path_matcher + : Type::file_matcher; + condition_ = conditions[condition_index].search_string; + condition_strings_ = splitString(condition_); + invert_ = conditions[condition_index].invert ? !invert_ : invert_; + use_regex_ = conditions[condition_index].use_regex; + if(!use_regex_) + std::transform(condition_.begin(), + condition_.end(), + condition_.begin(), + [](unsigned char c) { return std::tolower(c); }); + } + else + { + type_ = containsOperator(expression, "or") ? Type::or_connector : Type::and_connector; + for(auto [start, size] : tokens) + children_.emplace_back(expression.substr(start, size), conditions); + } +} + +bool TagConditionNode::evaluate(const std::vector>& files) const +{ + if(type_ == Type::empty) + return false; + std::map results; + return evaluateOnce(files, results); +} + +bool TagConditionNode::evaluateOnce(const std::vector>& files, + std::map& results) const +{ + return invert_ ? !evaluateWithoutInversion(files, results) + : evaluateWithoutInversion(files, results); +} + +bool TagConditionNode::evaluateWithoutInversion( + const std::vector>& files, + std::map& results) const +{ + if(type_ == Type::file_matcher || type_ == Type::path_matcher) + { + if(results.contains(condition_id_)) + return results[condition_id_]; + + bool result = false; + for(const auto& [path, file_name] : files) + { + std::string target = type_ == Type::file_matcher ? file_name : path; + if(use_regex_) + result = std::regex_match(target, std::regex(condition_)); + else + { + std::transform(target.begin(), + target.end(), + target.begin(), + [](unsigned char c) { return std::tolower(c); }); + result = wildcardMatch(target); + } + if(result) + break; + } + results[condition_id_] = result; + return result; + } + else if(type_ == Type::or_connector) + { + for(const auto& child : children_) + if(child.evaluateOnce(files, results)) + return true; + return false; + } + else + { + for(const auto& child : children_) + if(!child.evaluateOnce(files, results)) + return false; + return true; + } +} + +void TagConditionNode::removeEnclosingParentheses(std::string& expression) +{ + while(expression.front() == '(' && expression.back() == ')') + { + int level = 0; + for(auto [i, c] : str::enumerate_view(expression)) + { + if(c == '(') + level++; + else if(c == ')') + level--; + if(i != expression.size() - 1 && level == 0) + return; + } + expression.erase(expression.begin()); + expression.erase(expression.end() - 1); + } +} + +bool TagConditionNode::expressionIsValid(std::string expression, int num_conditions) +{ + if(expression.empty()) + return false; + std::transform(expression.begin(), + expression.end(), + expression.begin(), + [](unsigned char c) { return std::tolower(c); }); + // check for invalid operators + if(expression.find_first_not_of("notadr0123456789() ") != std::string::npos) + return false; + std::string expression_2 = expression; + removeSubstring(expression_2, "and"); + removeSubstring(expression_2, "or"); + removeSubstring(expression_2, "not"); + if(std::regex_search(expression_2, std::regex("[a-zA-Z]"))) + return false; + + removeSubstring(expression, " "); + // check for invalid parentheses + char last_c = ' '; + int level = 0; + for(auto c : expression) + { + if(c == '(') + level++; + else if(c == ')') + { + if(last_c == '(') + return false; + level--; + } + last_c = c; + } + if(level != 0) + return false; + + // check if variables exist + const std::regex num_regex(R"(\d+)"); + auto first = std::sregex_iterator(expression.begin(), expression.end(), num_regex); + auto last = std::sregex_iterator(); + for(auto iter = first; iter != last; iter++) + { + if(std::stoi(iter->str()) >= num_conditions) + return false; + } + + return operatorOrderIsValid(expression); +} + +bool TagConditionNode::containsOperator(const std::string& expression, const std::string& op) const +{ + int level = 0; + for(auto [i, c] : str::enumerate_view(expression)) + { + if(c == '(') + { + level++; + continue; + } + if(level > 0) + { + if(c == ')') + level--; + continue; + } + if(expression.compare(i, op.size(), op) == 0) + return true; + } + return false; +} + +std::vector> TagConditionNode::tokenize(const std::string& expression) const +{ + bool or_has_priority = containsOperator(expression, "or"); + bool and_has_priority = !or_has_priority && containsOperator(expression, "and"); + std::vector> tokens; + int level = 0; + int token_start = 0; + for(auto [i, c] : str::enumerate_view(expression)) + { + if(c == '(') + { + level++; + continue; + } + if(level > 0) + { + if(c == ')') + level--; + continue; + } + if(or_has_priority && expression.compare(i, 2, "or") == 0) + { + tokens.emplace_back(token_start, i - token_start); + token_start = i + 2; + } + else if(and_has_priority && expression.compare(i, 3, "and") == 0) + { + tokens.emplace_back(token_start, i - token_start); + token_start = i + 3; + } + } + tokens.emplace_back(token_start, expression.size() - token_start); + return tokens; +} + +void TagConditionNode::removeWhitespaces(std::string& expression) const +{ + auto pos = expression.find(" "); + while(pos != std::string::npos) + { + expression.erase(pos, 1); + pos = expression.find(" "); + } +} + +bool TagConditionNode::wildcardMatch(const std::string& target) const +{ + if(condition_.empty()) + return false; + if(condition_.find_first_not_of("*") == std::string::npos) + return true; + + auto condition_strings_ = splitString(condition_); + if(condition_.front() != '*' && !target.starts_with(condition_strings_[0]) || + condition_.back() != '*' && !target.ends_with(condition_strings_.back())) + return false; + + size_t target_pos = 0; + for(const auto& search_string : condition_strings_) + { + if(target_pos >= target.size()) + return false; + target_pos = target.find(search_string, target_pos); + if(target_pos == std::string::npos) + return false; + target_pos += search_string.size(); + } + return true; +} + +std::vector TagConditionNode::splitString(const std::string& input) const +{ + std::vector splits; + size_t pos = 0; + size_t old_pos = 0; + while(old_pos != input.size()) + { + pos = input.find('*', old_pos); + if(pos == std::string::npos) + { + splits.push_back(input.substr(old_pos)); + break; + } + if(pos - old_pos > 0) + splits.push_back(input.substr(old_pos, pos - old_pos)); + old_pos = pos + 1; + } + return splits; +} + +bool TagConditionNode::operatorOrderIsValid(std::string expression) +{ + constexpr int type_var = 0; + constexpr int type_op = 1; + constexpr int type_group = 2; + constexpr int type_not = 3; + + TagConditionNode::removeEnclosingParentheses(expression); + std::vector token_types; + std::vector> token_borders; + int level = 0; + int token_start = 0; + bool is_in_group = false; + bool is_in_var = false; + int i = 0; + while(i < expression.size()) + { + char c = expression[i]; + if(is_in_var) + { + if(c < '0' || c > '9') + { + token_types.push_back(type_var); + token_borders.emplace_back(token_start, i - token_start); + is_in_var = false; + } + else + { + i++; + continue; + } + } + if(is_in_group) + { + if(c == '(') + level++; + else if(c == ')') + { + level--; + if(level == 0) + { + is_in_group = false; + token_types.push_back(type_group); + token_borders.emplace_back(token_start, i - token_start + 1); + } + } + i++; + } + else if(c == '(') + { + is_in_group = true; + token_start = i; + level++; + i++; + } + else if(c == 'a') + { + token_borders.emplace_back(i, 3); + token_types.push_back(type_op); + i += 3; + } + else if(c == 'o') + { + token_borders.emplace_back(i, 2); + token_types.push_back(type_op); + i += 2; + } + else if(c == 'n') + { + token_borders.emplace_back(i, i + 2); + token_types.push_back(type_not); + i += 3; + } + else if('0' <= c && c <= '9') + { + if(!is_in_var) + token_start = i; + is_in_var = true; + i++; + } + else + i++; + } + if(is_in_var) + { + token_types.push_back(type_var); + token_borders.emplace_back(token_start, i - token_start); + } + + int prev_token = type_op; + for(int token : token_types) + { + if(token == type_op && (prev_token == type_not || prev_token == type_op)) + return false; + if(token == type_var && (prev_token == type_var || prev_token == type_group)) + return false; + if(token == type_group && (prev_token == type_var || prev_token == type_group)) + return false; + if(token == type_not && ((prev_token == type_var || prev_token == type_group))) + return false; + prev_token = token; + } + if(token_types.back() == type_not || token_types.back() == type_op) + return false; + + for(const auto& [token, borders] : str::zip_view(token_types, token_borders)) + { + if(token != type_group) + continue; + const auto [start, len] = borders; + if(!operatorOrderIsValid(expression.substr(start, len))) + return false; + } + return true; +} + +void TagConditionNode::removeSubstring(std::string& string, std::string substring) +{ + const size_t length = substring.length(); + for(auto pos = string.find(substring); pos != std::string::npos; pos = string.find(substring)) + string.erase(pos, length); +} diff --git a/src/core/tagconditionnode.h b/src/core/tagconditionnode.h new file mode 100644 index 0000000..c2bc4af --- /dev/null +++ b/src/core/tagconditionnode.h @@ -0,0 +1,156 @@ +/*! + * \file tagconditionnode.h + * \brief Header for the TagConditionNode class. + */ + +#pragma once + +#include "tagcondition.h" +#include +#include +#include + + +/*! + * \brief Represents a node in a tree used to model a boolean expression for + * evaluating if the files in a directory match a set of conditions. + */ +class TagConditionNode +{ +public: + /*! \brief Type of this node. */ + enum class Type + { + /*! \brief Node evaluates to true only if all children evaluate to true. */ + and_connector, + /*! \brief Node evaluates to true if at least one child evaluates to true. */ + or_connector, + /*! \brief Leaf node. Evaluates to true if a file name matches a pattern. */ + file_matcher, + /*! \brief Leaf node. Evaluates to true if a file path matches a pattern. */ + path_matcher, + /*! \brief Dummy node. Always evaluates to false. */ + empty + }; + + /*! \brief Constructs a node of type empty. */ + TagConditionNode(); + /*! + * \brief Constructs a new node from the given boolean expression and conditions. + * Recursively constructs children as needed. Node types are deduced from the expression. + * \param expression Expression used to construct the tree. + * \param conditions Conditions which serve as variables in the expression. + */ + TagConditionNode(std::string expression, const std::vector& conditions); + + /*! + * \brief Checks if files in the given vector satisfy + * the boolean expression modeled by this tree node. + * \param files Contains pairs of path and file names for all files of a mod. + * \return True if the directory satisfies the expression. + */ + bool evaluate(const std::vector>& files) const; + /*! + * \brief Removes all outer parentheses that serve no semantic purpose in the given expression. + * \param expression Expression to be modified. + */ + static void removeEnclosingParentheses(std::string& expression); + /*! + * \brief Checks if the given string is a syntactically valid boolean expression. + * \param exppression String to validate. + * \param num_conditions Number of conditions available in the expression. + * \return True if the expression is valid. + */ + static bool expressionIsValid(std::string expression, int num_conditions); + +private: + /*! \brief The boolean expression modeled by this tree. */ + std::string expression_; + /*! \brief If true: Invert the evaluation result. */ + bool invert_ = false; + /*! \brief Child nodes of this node. */ + std::vector children_; + /*! \brief Type of this node. */ + Type type_; + /*! \brief String used to comparisons in leaf nodes. */ + std::string condition_; + /*! \brief Used to store substrings of the expression. Split by the * wildcard. */ + std::vector condition_strings_; + /*! + * \brief If this is a leaf: Represents the condition in the tree. Used to avoid + * evaluating conditions multiple times. + */ + int condition_id_; + /*! + * \brief If true: Use regex to compare against the condition string. + * Else: Use a simple string matcher with * as a wildcard. + */ + bool use_regex_; + + /*! + * \brief Checks if files in the given vector satisfy + * the boolean expression modeled by this tree node. This check is skipped if the given + * results map contains this nodes id. + * \param files Contains pairs of path and file names for all files of a mod. + * \param results Contains results of previous evaluations. + * \return True if the directory satisfies the expression. + */ + bool evaluateOnce(const std::vector>& files, + std::map& results) const; + /*! + * \brief Checks if files in the given vector satisfy + * the boolean expression modeled by this tree node. This check is skipped if the given + * results map contains this nodes id. + * Does not invert the result even is invert_ is true. + * \param files Contains pairs of path and file names for all files of a mod. + * \param results Contains results of previous evaluations. + * \return True if the directory satisfies the expression. + */ + bool evaluateWithoutInversion(const std::vector>& files, + std::map& results) const; + /*! + * \brief Checks if the given expression contains the given boolean operator. + * Only checks the top level part of the expression. + * \param expression Expression to check. + * \param op Operator used for comparison. + * \return True if expression contains operator. + */ + bool containsOperator(const std::string& expression, const std::string& op) const; + /*! + * \brief Splits the given expression into tokens. Tokens are either condition ids, + * boolean operators or a subexpression in parentheses. + * \param expression Expression to split. + * \return Contains pairs of index and length of tokens in the given expression. + */ + std::vector> tokenize(const std::string& expression) const; + /*! + * \brief Removes all whitespaces in the given string. + * \param expression Expression to modify. + */ + void removeWhitespaces(std::string& expression) const; + /*! + * \brief Checks if the given string matches this nodes condition_ string. + * Uses * as a wildcard. + * \param target String to compare to. + * \return True if both match. + */ + bool wildcardMatch(const std::string& target) const; + /*! + * \brief Splits the given string into substrings seperated by the * wildcard. + * \param input String to split. + * \return All substrings without the * wildcard. + */ + std::vector splitString(const std::string& input) const; + /*! + * \brief Checks if the order of operators in the given boolean expression is valid. + * \param expression Expression to check. + * \return True if the order is valid. + */ + static bool operatorOrderIsValid(std::string expression); + /*! + * \brief Removes all occurrences of substring from string. + * \param string String from which to remove. + * \param substring Substring to remove. + */ + static void removeSubstring(std::string& string, std::string substring); +}; diff --git a/src/cspell.json b/src/cspell.json new file mode 100644 index 0000000..8e0125e --- /dev/null +++ b/src/cspell.json @@ -0,0 +1,27 @@ +{ + "version": "0.2", + "language": "en", + "words": [ + "deployer", + "deployers", + "Deployer", + "Deployers", + "flatpak", + "unrar", + "fomod", + "Fomod", + "loadorder", + "LOADORDER", + "Skyrim", + "Morrowind", + "Fallout", + "fomm", + "SIMPLEDEPLOYER", + "CASEMATCHINGDEPLOYER", + "LOOTDEPLOYER", + "Kibi", + "depl", + "pugi" + ], + "allowCompoundWords": true +} diff --git a/src/lmm_Doxyfile b/src/lmm_Doxyfile new file mode 100644 index 0000000..fa82c3e --- /dev/null +++ b/src/lmm_Doxyfile @@ -0,0 +1,2737 @@ +# Doxyfile 1.9.4 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. +# +# All text after a double hash (##) is considered a comment and is placed in +# front of the TAG it is preceding. +# +# All text after a single hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists, items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (\" \"). +# +# Note: +# +# Use doxygen to compare the used configuration file with the template +# configuration file: +# doxygen -x [configFile] +# Use doxygen to compare the used configuration file with the template +# configuration file without replacing the environment variables: +# doxygen -x_noenv [configFile] + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the configuration +# file that follow. The default is UTF-8 which is also the encoding used for all +# text before the first occurrence of this tag. Doxygen uses libiconv (or the +# iconv built into libc) for the transcoding. See +# https://www.gnu.org/software/libiconv/ for the list of possible encodings. +# The default value is: UTF-8. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by +# double-quotes, unless you are using Doxywizard) that should identify the +# project for which the documentation is generated. This name is used in the +# title of most generated pages and in a few other places. +# The default value is: My Project. + +PROJECT_NAME = "Limo" + +# The PROJECT_NUMBER tag can be used to enter a project or revision number. This +# could be handy for archiving the generated documentation or if some version +# control system is used. + +PROJECT_NUMBER = + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewer a +# quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = "A simple mod manager" + +# With the PROJECT_LOGO tag one can specify a logo or an icon that is included +# in the documentation. The maximum height of the logo should not exceed 55 +# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy +# the logo to the output directory. + +PROJECT_LOGO = resources/logo_small.png + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path +# into which the generated documentation will be written. If a relative path is +# entered, it will be relative to the location where doxygen was started. If +# left blank the current directory will be used. + +OUTPUT_DIRECTORY = doc/ + +# If the CREATE_SUBDIRS tag is set to YES then doxygen will create up to 4096 +# sub-directories (in 2 levels) under the output directory of each output format +# and will distribute the generated files over these directories. Enabling this +# option can be useful when feeding doxygen a huge amount of source files, where +# putting all generated files in the same directory would otherwise causes +# performance problems for the file system. Adapt CREATE_SUBDIRS_LEVEL to +# control the number of sub-directories. +# The default value is: NO. + +CREATE_SUBDIRS = NO + +# Controls the number of sub-directories that will be created when +# CREATE_SUBDIRS tag is set to YES. Level 0 represents 16 directories, and every +# level increment doubles the number of directories, resulting in 4096 +# directories at level 8 which is the default and also the maximum value. The +# sub-directories are organized in 2 levels, the first level always has a fixed +# numer of 16 directories. +# Minimum value: 0, maximum value: 8, default value: 8. +# This tag requires that the tag CREATE_SUBDIRS is set to YES. + +CREATE_SUBDIRS_LEVEL = 8 + +# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII +# characters to appear in the names of generated files. If set to NO, non-ASCII +# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode +# U+3044. +# The default value is: NO. + +ALLOW_UNICODE_NAMES = NO + +# The OUTPUT_LANGUAGE tag is used to specify the language in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all constant output in the proper language. +# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Bulgarian, +# Catalan, Chinese, Chinese-Traditional, Croatian, Czech, Danish, Dutch, English +# (United States), Esperanto, Farsi (Persian), Finnish, French, German, Greek, +# Hindi, Hungarian, Indonesian, Italian, Japanese, Japanese-en (Japanese with +# English messages), Korean, Korean-en (Korean with English messages), Latvian, +# Lithuanian, Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, +# Romanian, Russian, Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, +# Swedish, Turkish, Ukrainian and Vietnamese. +# The default value is: English. + +OUTPUT_LANGUAGE = English + +# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member +# descriptions after the members that are listed in the file and class +# documentation (similar to Javadoc). Set to NO to disable this. +# The default value is: YES. + +BRIEF_MEMBER_DESC = YES + +# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief +# description of a member or function before the detailed description +# +# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the +# brief descriptions will be completely suppressed. +# The default value is: YES. + +REPEAT_BRIEF = YES + +# This tag implements a quasi-intelligent brief description abbreviator that is +# used to form the text in various listings. Each string in this list, if found +# as the leading text of the brief description, will be stripped from the text +# and the result, after processing the whole list, is used as the annotated +# text. Otherwise, the brief description is used as-is. If left blank, the +# following values are used ($name is automatically replaced with the name of +# the entity):The $name class, The $name widget, The $name file, is, provides, +# specifies, contains, represents, a, an and the. + +ABBREVIATE_BRIEF = "The $name class" \ + "The $name widget" \ + "The $name file" \ + is \ + provides \ + specifies \ + contains \ + represents \ + a \ + an \ + the + +# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then +# doxygen will generate a detailed section even if there is only a brief +# description. +# The default value is: NO. + +ALWAYS_DETAILED_SEC = NO + +# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all +# inherited members of a class in the documentation of that class as if those +# members were ordinary class members. Constructors, destructors and assignment +# operators of the base classes will not be shown. +# The default value is: NO. + +INLINE_INHERITED_MEMB = NO + +# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path +# before files name in the file list and in the header files. If set to NO the +# shortest path that makes the file name unique will be used +# The default value is: YES. + +FULL_PATH_NAMES = YES + +# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. +# Stripping is only done if one of the specified strings matches the left-hand +# part of the path. The tag can be used to show relative paths in the file list. +# If left blank the directory from which doxygen is run is used as the path to +# strip. +# +# Note that you can specify absolute paths here, but also relative paths, which +# will be relative from the directory where doxygen is started. +# This tag requires that the tag FULL_PATH_NAMES is set to YES. + +STRIP_FROM_PATH = + +# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the +# path mentioned in the documentation of a class, which tells the reader which +# header file to include in order to use a class. If left blank only the name of +# the header file containing the class definition is used. Otherwise one should +# specify the list of include paths that are normally passed to the compiler +# using the -I flag. + +STRIP_FROM_INC_PATH = + +# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but +# less readable) file names. This can be useful is your file systems doesn't +# support long names like on DOS, Mac, or CD-ROM. +# The default value is: NO. + +SHORT_NAMES = NO + +# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the +# first line (until the first dot) of a Javadoc-style comment as the brief +# description. If set to NO, the Javadoc-style will behave just like regular Qt- +# style comments (thus requiring an explicit @brief command for a brief +# description.) +# The default value is: NO. + +JAVADOC_AUTOBRIEF = NO + +# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line +# such as +# /*************** +# as being the beginning of a Javadoc-style comment "banner". If set to NO, the +# Javadoc-style will behave just like regular comments and it will not be +# interpreted by doxygen. +# The default value is: NO. + +JAVADOC_BANNER = NO + +# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first +# line (until the first dot) of a Qt-style comment as the brief description. If +# set to NO, the Qt-style will behave just like regular Qt-style comments (thus +# requiring an explicit \brief command for a brief description.) +# The default value is: NO. + +QT_AUTOBRIEF = NO + +# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a +# multi-line C++ special comment block (i.e. a block of //! or /// comments) as +# a brief description. This used to be the default behavior. The new default is +# to treat a multi-line C++ comment block as a detailed description. Set this +# tag to YES if you prefer the old behavior instead. +# +# Note that setting this tag to YES also means that rational rose comments are +# not recognized any more. +# The default value is: NO. + +MULTILINE_CPP_IS_BRIEF = NO + +# By default Python docstrings are displayed as preformatted text and doxygen's +# special commands cannot be used. By setting PYTHON_DOCSTRING to NO the +# doxygen's special commands can be used and the contents of the docstring +# documentation blocks is shown as doxygen documentation. +# The default value is: YES. + +PYTHON_DOCSTRING = YES + +# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the +# documentation from any documented member that it re-implements. +# The default value is: YES. + +INHERIT_DOCS = YES + +# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new +# page for each member. If set to NO, the documentation of a member will be part +# of the file/class/namespace that contains it. +# The default value is: NO. + +SEPARATE_MEMBER_PAGES = NO + +# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen +# uses this value to replace tabs by spaces in code fragments. +# Minimum value: 1, maximum value: 16, default value: 4. + +TAB_SIZE = 4 + +# This tag can be used to specify a number of aliases that act as commands in +# the documentation. An alias has the form: +# name=value +# For example adding +# "sideeffect=@par Side Effects:^^" +# will allow you to put the command \sideeffect (or @sideeffect) in the +# documentation, which will result in a user-defined paragraph with heading +# "Side Effects:". Note that you cannot put \n's in the value part of an alias +# to insert newlines (in the resulting output). You can put ^^ in the value part +# of an alias to insert a newline as if a physical newline was in the original +# file. When you need a literal { or } or , in the value part of an alias you +# have to escape them by means of a backslash (\), this can lead to conflicts +# with the commands \{ and \} for these it is advised to use the version @{ and +# @} or use a double escape (\\{ and \\}) + +ALIASES = + +# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources +# only. Doxygen will then generate output that is more tailored for C. For +# instance, some of the names that are used will be different. The list of all +# members will be omitted, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_FOR_C = NO + +# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or +# Python sources only. Doxygen will then generate output that is more tailored +# for that language. For instance, namespaces will be presented as packages, +# qualified scopes will look different, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_JAVA = NO + +# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran +# sources. Doxygen will then generate output that is tailored for Fortran. +# The default value is: NO. + +OPTIMIZE_FOR_FORTRAN = NO + +# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL +# sources. Doxygen will then generate output that is tailored for VHDL. +# The default value is: NO. + +OPTIMIZE_OUTPUT_VHDL = NO + +# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice +# sources only. Doxygen will then generate output that is more tailored for that +# language. For instance, namespaces will be presented as modules, types will be +# separated into more groups, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_SLICE = NO + +# Doxygen selects the parser to use depending on the extension of the files it +# parses. With this tag you can assign which parser to use for a given +# extension. Doxygen has a built-in mapping, but you can override or extend it +# using this tag. The format is ext=language, where ext is a file extension, and +# language is one of the parsers supported by doxygen: IDL, Java, JavaScript, +# Csharp (C#), C, C++, Lex, D, PHP, md (Markdown), Objective-C, Python, Slice, +# VHDL, Fortran (fixed format Fortran: FortranFixed, free formatted Fortran: +# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser +# tries to guess whether the code is fixed or free formatted code, this is the +# default for Fortran type files). For instance to make doxygen treat .inc files +# as Fortran files (default is PHP), and .f files as C (default is Fortran), +# use: inc=Fortran f=C. +# +# Note: For files without extension you can use no_extension as a placeholder. +# +# Note that for custom extensions you also need to set FILE_PATTERNS otherwise +# the files are not read by doxygen. When specifying no_extension you should add +# * to the FILE_PATTERNS. +# +# Note see also the list of default file extension mappings. + +EXTENSION_MAPPING = + +# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments +# according to the Markdown format, which allows for more readable +# documentation. See https://daringfireball.net/projects/markdown/ for details. +# The output of markdown processing is further processed by doxygen, so you can +# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in +# case of backward compatibilities issues. +# The default value is: YES. + +MARKDOWN_SUPPORT = YES + +# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up +# to that level are automatically included in the table of contents, even if +# they do not have an id attribute. +# Note: This feature currently applies only to Markdown headings. +# Minimum value: 0, maximum value: 99, default value: 5. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +TOC_INCLUDE_HEADINGS = 5 + +# When enabled doxygen tries to link words that correspond to documented +# classes, or namespaces to their corresponding documentation. Such a link can +# be prevented in individual cases by putting a % sign in front of the word or +# globally by setting AUTOLINK_SUPPORT to NO. +# The default value is: YES. + +AUTOLINK_SUPPORT = YES + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should set this +# tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); +# versus func(std::string) {}). This also make the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. +# The default value is: NO. + +BUILTIN_STL_SUPPORT = NO + +# If you use Microsoft's C++/CLI language, you should set this option to YES to +# enable parsing support. +# The default value is: NO. + +CPP_CLI_SUPPORT = NO + +# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: +# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen +# will parse them like normal C++ but will assume all classes use public instead +# of private inheritance when no explicit protection keyword is present. +# The default value is: NO. + +SIP_SUPPORT = NO + +# For Microsoft's IDL there are propget and propput attributes to indicate +# getter and setter methods for a property. Setting this option to YES will make +# doxygen to replace the get and set methods by a property in the documentation. +# This will only work if the methods are indeed getting or setting a simple +# type. If this is not the case, or you want to show the methods anyway, you +# should set this option to NO. +# The default value is: YES. + +IDL_PROPERTY_SUPPORT = YES + +# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC +# tag is set to YES then doxygen will reuse the documentation of the first +# member in the group (if any) for the other members of the group. By default +# all members of a group must be documented explicitly. +# The default value is: NO. + +DISTRIBUTE_GROUP_DOC = NO + +# If one adds a struct or class to a group and this option is enabled, then also +# any nested class or struct is added to the same group. By default this option +# is disabled and one has to add nested compounds explicitly via \ingroup. +# The default value is: NO. + +GROUP_NESTED_COMPOUNDS = NO + +# Set the SUBGROUPING tag to YES to allow class member groups of the same type +# (for instance a group of public functions) to be put as a subgroup of that +# type (e.g. under the Public Functions section). Set it to NO to prevent +# subgrouping. Alternatively, this can be done per class using the +# \nosubgrouping command. +# The default value is: YES. + +SUBGROUPING = YES + +# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions +# are shown inside the group in which they are included (e.g. using \ingroup) +# instead of on a separate page (for HTML and Man pages) or section (for LaTeX +# and RTF). +# +# Note that this feature does not work in combination with +# SEPARATE_MEMBER_PAGES. +# The default value is: NO. + +INLINE_GROUPED_CLASSES = NO + +# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions +# with only public data fields or simple typedef fields will be shown inline in +# the documentation of the scope in which they are defined (i.e. file, +# namespace, or group documentation), provided this scope is documented. If set +# to NO, structs, classes, and unions are shown on a separate page (for HTML and +# Man pages) or section (for LaTeX and RTF). +# The default value is: NO. + +INLINE_SIMPLE_STRUCTS = NO + +# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or +# enum is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically be +# useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. +# The default value is: NO. + +TYPEDEF_HIDES_STRUCT = NO + +# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This +# cache is used to resolve symbols given their name and scope. Since this can be +# an expensive process and often the same symbol appears multiple times in the +# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small +# doxygen will become slower. If the cache is too large, memory is wasted. The +# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range +# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 +# symbols. At the end of a run doxygen will report the cache usage and suggest +# the optimal cache size from a speed point of view. +# Minimum value: 0, maximum value: 9, default value: 0. + +LOOKUP_CACHE_SIZE = 0 + +# The NUM_PROC_THREADS specifies the number of threads doxygen is allowed to use +# during processing. When set to 0 doxygen will based this on the number of +# cores available in the system. You can set it explicitly to a value larger +# than 0 to get more control over the balance between CPU load and processing +# speed. At this moment only the input processing can be done using multiple +# threads. Since this is still an experimental feature the default is set to 1, +# which effectively disables parallel processing. Please report any issues you +# encounter. Generating dot graphs in parallel is controlled by the +# DOT_NUM_THREADS setting. +# Minimum value: 0, maximum value: 32, default value: 1. + +NUM_PROC_THREADS = 1 + +#--------------------------------------------------------------------------- +# Build related configuration options +#--------------------------------------------------------------------------- + +# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in +# documentation are documented, even if no documentation was available. Private +# class members and static file members will be hidden unless the +# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. +# Note: This will also disable the warnings about undocumented members that are +# normally produced when WARNINGS is set to YES. +# The default value is: NO. + +EXTRACT_ALL = NO + +# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will +# be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIVATE = YES + +# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual +# methods of a class will be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIV_VIRTUAL = NO + +# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal +# scope will be included in the documentation. +# The default value is: NO. + +EXTRACT_PACKAGE = NO + +# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be +# included in the documentation. +# The default value is: NO. + +EXTRACT_STATIC = YES + +# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined +# locally in source files will be included in the documentation. If set to NO, +# only classes defined in header files are included. Does not have any effect +# for Java sources. +# The default value is: YES. + +EXTRACT_LOCAL_CLASSES = YES + +# This flag is only useful for Objective-C code. If set to YES, local methods, +# which are defined in the implementation section but not in the interface are +# included in the documentation. If set to NO, only methods in the interface are +# included. +# The default value is: NO. + +EXTRACT_LOCAL_METHODS = NO + +# If this flag is set to YES, the members of anonymous namespaces will be +# extracted and appear in the documentation as a namespace called +# 'anonymous_namespace{file}', where file will be replaced with the base name of +# the file that contains the anonymous namespace. By default anonymous namespace +# are hidden. +# The default value is: NO. + +EXTRACT_ANON_NSPACES = NO + +# If this flag is set to YES, the name of an unnamed parameter in a declaration +# will be determined by the corresponding definition. By default unnamed +# parameters remain unnamed in the output. +# The default value is: YES. + +RESOLVE_UNNAMED_PARAMS = YES + +# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all +# undocumented members inside documented classes or files. If set to NO these +# members will be included in the various overviews, but no documentation +# section is generated. This option has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_MEMBERS = NO + +# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all +# undocumented classes that are normally visible in the class hierarchy. If set +# to NO, these classes will be included in the various overviews. This option +# has no effect if EXTRACT_ALL is enabled. +# The default value is: NO. + +HIDE_UNDOC_CLASSES = NO + +# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend +# declarations. If set to NO, these declarations will be included in the +# documentation. +# The default value is: NO. + +HIDE_FRIEND_COMPOUNDS = NO + +# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any +# documentation blocks found inside the body of a function. If set to NO, these +# blocks will be appended to the function's detailed documentation block. +# The default value is: NO. + +HIDE_IN_BODY_DOCS = NO + +# The INTERNAL_DOCS tag determines if documentation that is typed after a +# \internal command is included. If the tag is set to NO then the documentation +# will be excluded. Set it to YES to include the internal documentation. +# The default value is: NO. + +INTERNAL_DOCS = NO + +# With the correct setting of option CASE_SENSE_NAMES doxygen will better be +# able to match the capabilities of the underlying filesystem. In case the +# filesystem is case sensitive (i.e. it supports files in the same directory +# whose names only differ in casing), the option must be set to YES to properly +# deal with such files in case they appear in the input. For filesystems that +# are not case sensitive the option should be set to NO to properly deal with +# output files written for symbols that only differ in casing, such as for two +# classes, one named CLASS and the other named Class, and to also support +# references to files without having to specify the exact matching casing. On +# Windows (including Cygwin) and MacOS, users should typically set this option +# to NO, whereas on Linux or other Unix flavors it should typically be set to +# YES. +# The default value is: system dependent. + +CASE_SENSE_NAMES = NO + +# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with +# their full class and namespace scopes in the documentation. If set to YES, the +# scope will be hidden. +# The default value is: NO. + +HIDE_SCOPE_NAMES = NO + +# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will +# append additional text to a page's title, such as Class Reference. If set to +# YES the compound reference will be hidden. +# The default value is: NO. + +HIDE_COMPOUND_REFERENCE= NO + +# If the SHOW_HEADERFILE tag is set to YES then the documentation for a class +# will show which file needs to be included to use the class. +# The default value is: YES. + +SHOW_HEADERFILE = YES + +# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of +# the files that are included by a file in the documentation of that file. +# The default value is: YES. + +SHOW_INCLUDE_FILES = YES + +# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each +# grouped member an include statement to the documentation, telling the reader +# which file to include in order to use the member. +# The default value is: NO. + +SHOW_GROUPED_MEMB_INC = NO + +# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include +# files with double quotes in the documentation rather than with sharp brackets. +# The default value is: NO. + +FORCE_LOCAL_INCLUDES = NO + +# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the +# documentation for inline members. +# The default value is: YES. + +INLINE_INFO = YES + +# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the +# (detailed) documentation of file and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. +# The default value is: YES. + +SORT_MEMBER_DOCS = YES + +# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief +# descriptions of file, namespace and class members alphabetically by member +# name. If set to NO, the members will appear in declaration order. Note that +# this will also influence the order of the classes in the class list. +# The default value is: NO. + +SORT_BRIEF_DOCS = NO + +# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the +# (brief and detailed) documentation of class members so that constructors and +# destructors are listed first. If set to NO the constructors will appear in the +# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. +# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief +# member documentation. +# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting +# detailed member documentation. +# The default value is: NO. + +SORT_MEMBERS_CTORS_1ST = NO + +# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy +# of group names into alphabetical order. If set to NO the group names will +# appear in their defined order. +# The default value is: NO. + +SORT_GROUP_NAMES = NO + +# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by +# fully-qualified names, including namespaces. If set to NO, the class list will +# be sorted only by class name, not including the namespace part. +# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. +# Note: This option applies only to the class list, not to the alphabetical +# list. +# The default value is: NO. + +SORT_BY_SCOPE_NAME = NO + +# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper +# type resolution of all parameters of a function it will reject a match between +# the prototype and the implementation of a member function even if there is +# only one candidate or it is obvious which candidate to choose by doing a +# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still +# accept a match between prototype and implementation in such cases. +# The default value is: NO. + +STRICT_PROTO_MATCHING = NO + +# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo +# list. This list is created by putting \todo commands in the documentation. +# The default value is: YES. + +GENERATE_TODOLIST = YES + +# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test +# list. This list is created by putting \test commands in the documentation. +# The default value is: YES. + +GENERATE_TESTLIST = YES + +# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug +# list. This list is created by putting \bug commands in the documentation. +# The default value is: YES. + +GENERATE_BUGLIST = YES + +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO) +# the deprecated list. This list is created by putting \deprecated commands in +# the documentation. +# The default value is: YES. + +GENERATE_DEPRECATEDLIST= YES + +# The ENABLED_SECTIONS tag can be used to enable conditional documentation +# sections, marked by \if ... \endif and \cond +# ... \endcond blocks. + +ENABLED_SECTIONS = + +# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the +# initial value of a variable or macro / define can have for it to appear in the +# documentation. If the initializer consists of more lines than specified here +# it will be hidden. Use a value of 0 to hide initializers completely. The +# appearance of the value of individual variables and macros / defines can be +# controlled using \showinitializer or \hideinitializer command in the +# documentation regardless of this setting. +# Minimum value: 0, maximum value: 10000, default value: 30. + +MAX_INITIALIZER_LINES = 30 + +# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at +# the bottom of the documentation of classes and structs. If set to YES, the +# list will mention the files that were used to generate the documentation. +# The default value is: YES. + +SHOW_USED_FILES = YES + +# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This +# will remove the Files entry from the Quick Index and from the Folder Tree View +# (if specified). +# The default value is: YES. + +SHOW_FILES = YES + +# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces +# page. This will remove the Namespaces entry from the Quick Index and from the +# Folder Tree View (if specified). +# The default value is: YES. + +SHOW_NAMESPACES = YES + +# The FILE_VERSION_FILTER tag can be used to specify a program or script that +# doxygen should invoke to get the current version for each file (typically from +# the version control system). Doxygen will invoke the program by executing (via +# popen()) the command command input-file, where command is the value of the +# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided +# by doxygen. Whatever the program writes to standard output is used as the file +# version. For an example see the documentation. + +FILE_VERSION_FILTER = + +# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed +# by doxygen. The layout file controls the global structure of the generated +# output files in an output format independent way. To create the layout file +# that represents doxygen's defaults, run doxygen with the -l option. You can +# optionally specify a file name after the option, if omitted DoxygenLayout.xml +# will be used as the name of the layout file. See also section "Changing the +# layout of pages" for information. +# +# Note that if you run doxygen from a directory containing a file called +# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE +# tag is left empty. + +LAYOUT_FILE = + +# The CITE_BIB_FILES tag can be used to specify one or more bib files containing +# the reference definitions. This must be a list of .bib files. The .bib +# extension is automatically appended if omitted. This requires the bibtex tool +# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info. +# For LaTeX the style of the bibliography can be controlled using +# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the +# search path. See also \cite for info how to create references. + +CITE_BIB_FILES = + +#--------------------------------------------------------------------------- +# Configuration options related to warning and progress messages +#--------------------------------------------------------------------------- + +# The QUIET tag can be used to turn on/off the messages that are generated to +# standard output by doxygen. If QUIET is set to YES this implies that the +# messages are off. +# The default value is: NO. + +QUIET = NO + +# The WARNINGS tag can be used to turn on/off the warning messages that are +# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES +# this implies that the warnings are on. +# +# Tip: Turn warnings on while writing the documentation. +# The default value is: YES. + +WARNINGS = YES + +# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate +# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag +# will automatically be disabled. +# The default value is: YES. + +WARN_IF_UNDOCUMENTED = YES + +# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for +# potential errors in the documentation, such as documenting some parameters in +# a documented function twice, or documenting parameters that don't exist or +# using markup commands wrongly. +# The default value is: YES. + +WARN_IF_DOC_ERROR = YES + +# If WARN_IF_INCOMPLETE_DOC is set to YES, doxygen will warn about incomplete +# function parameter documentation. If set to NO, doxygen will accept that some +# parameters have no documentation without warning. +# The default value is: YES. + +WARN_IF_INCOMPLETE_DOC = YES + +# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that +# are documented, but have no documentation for their parameters or return +# value. If set to NO, doxygen will only warn about wrong parameter +# documentation, but not about the absence of documentation. If EXTRACT_ALL is +# set to YES then this flag will automatically be disabled. See also +# WARN_IF_INCOMPLETE_DOC +# The default value is: NO. + +WARN_NO_PARAMDOC = NO + +# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when +# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS +# then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but +# at the end of the doxygen process doxygen will return with a non-zero status. +# Possible values are: NO, YES and FAIL_ON_WARNINGS. +# The default value is: NO. + +WARN_AS_ERROR = NO + +# The WARN_FORMAT tag determines the format of the warning messages that doxygen +# can produce. The string should contain the $file, $line, and $text tags, which +# will be replaced by the file and line number from which the warning originated +# and the warning text. Optionally the format may contain $version, which will +# be replaced by the version of the file (if it could be obtained via +# FILE_VERSION_FILTER) +# See also: WARN_LINE_FORMAT +# The default value is: $file:$line: $text. + +WARN_FORMAT = "$file:$line: $text" + +# In the $text part of the WARN_FORMAT command it is possible that a reference +# to a more specific place is given. To make it easier to jump to this place +# (outside of doxygen) the user can define a custom "cut" / "paste" string. +# Example: +# WARN_LINE_FORMAT = "'vi $file +$line'" +# See also: WARN_FORMAT +# The default value is: at line $line of file $file. + +WARN_LINE_FORMAT = "at line $line of file $file" + +# The WARN_LOGFILE tag can be used to specify a file to which warning and error +# messages should be written. If left blank the output is written to standard +# error (stderr). In case the file specified cannot be opened for writing the +# warning and error messages are written to standard error. When as file - is +# specified the warning and error messages are written to standard output +# (stdout). + +WARN_LOGFILE = + +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag is used to specify the files and/or directories that contain +# documented source files. You may enter file names like myfile.cpp or +# directories like /usr/src/myproject. Separate the files or directories with +# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING +# Note: If this tag is empty the current directory is searched. + +INPUT = src/ + +# This tag can be used to specify the character encoding of the source files +# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses +# libiconv (or the iconv built into libc) for the transcoding. See the libiconv +# documentation (see: +# https://www.gnu.org/software/libiconv/) for the list of possible encodings. +# The default value is: UTF-8. + +INPUT_ENCODING = UTF-8 + +# If the value of the INPUT tag contains directories, you can use the +# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and +# *.h) to filter out the source-files in the directories. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# read by doxygen. +# +# Note the list of default checked file patterns might differ from the list of +# default file extension mappings. +# +# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, +# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, +# *.hh, *.hxx, *.hpp, *.h++, *.l, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, +# *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C +# comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, +# *.vhdl, *.ucf, *.qsf and *.ice. + +FILE_PATTERNS = *.c \ + *.cc \ + *.cxx \ + *.cpp \ + *.c++ \ + *.java \ + *.ii \ + *.ixx \ + *.ipp \ + *.i++ \ + *.inl \ + *.idl \ + *.ddl \ + *.odl \ + *.h \ + *.hh \ + *.hxx \ + *.hpp \ + *.h++ \ + *.l \ + *.cs \ + *.d \ + *.php \ + *.php4 \ + *.php5 \ + *.phtml \ + *.inc \ + *.m \ + *.markdown \ + *.md \ + *.mm \ + *.dox \ + *.py \ + *.pyw \ + *.f90 \ + *.f95 \ + *.f03 \ + *.f08 \ + *.f18 \ + *.f \ + *.for \ + *.vhd \ + *.vhdl \ + *.ucf \ + *.qsf \ + *.ice + +# The RECURSIVE tag can be used to specify whether or not subdirectories should +# be searched for input files as well. +# The default value is: NO. + +RECURSIVE = YES + +# The EXCLUDE tag can be used to specify files and/or directories that should be +# excluded from the INPUT source files. This way you can easily exclude a +# subdirectory from a directory tree whose root is specified with the INPUT tag. +# +# Note that relative paths are relative to the directory from which doxygen is +# run. + +EXCLUDE = + +# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or +# directories that are symbolic links (a Unix file system feature) are excluded +# from the input. +# The default value is: NO. + +EXCLUDE_SYMLINKS = NO + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories for example use the pattern */test/* + +EXCLUDE_PATTERNS = + +# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names +# (namespaces, classes, functions, etc.) that should be excluded from the +# output. The symbol name can be a fully qualified name, a word, or if the +# wildcard * is used, a substring. Examples: ANamespace, AClass, +# ANamespace::AClass, ANamespace::*Test +# +# Note that the wildcards are matched against the file with absolute path, so to +# exclude all test directories use the pattern */test/* + +EXCLUDE_SYMBOLS = + +# The EXAMPLE_PATH tag can be used to specify one or more files or directories +# that contain example code fragments that are included (see the \include +# command). + +EXAMPLE_PATH = + +# If the value of the EXAMPLE_PATH tag contains directories, you can use the +# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and +# *.h) to filter out the source-files in the directories. If left blank all +# files are included. + +EXAMPLE_PATTERNS = * + +# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be +# searched for input files to be used with the \include or \dontinclude commands +# irrespective of the value of the RECURSIVE tag. +# The default value is: NO. + +EXAMPLE_RECURSIVE = NO + +# The IMAGE_PATH tag can be used to specify one or more files or directories +# that contain images that are to be included in the documentation (see the +# \image command). + +IMAGE_PATH = + +# The INPUT_FILTER tag can be used to specify a program that doxygen should +# invoke to filter for each input file. Doxygen will invoke the filter program +# by executing (via popen()) the command: +# +# +# +# where is the value of the INPUT_FILTER tag, and is the +# name of an input file. Doxygen will then use the output that the filter +# program writes to standard output. If FILTER_PATTERNS is specified, this tag +# will be ignored. +# +# Note that the filter must not add or remove lines; it is applied before the +# code is scanned, but not when the output code is generated. If lines are added +# or removed, the anchors will not be placed correctly. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. + +INPUT_FILTER = + +# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern +# basis. Doxygen will compare the file name with each pattern and apply the +# filter if there is a match. The filters are a list of the form: pattern=filter +# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how +# filters are used. If the FILTER_PATTERNS tag is empty or if none of the +# patterns match the file name, INPUT_FILTER is applied. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. + +FILTER_PATTERNS = + +# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using +# INPUT_FILTER) will also be used to filter the input files that are used for +# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). +# The default value is: NO. + +FILTER_SOURCE_FILES = NO + +# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file +# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and +# it is also possible to disable source filtering for a specific pattern using +# *.ext= (so without naming a filter). +# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. + +FILTER_SOURCE_PATTERNS = + +# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page +# (index.html). This can be useful if you have a project on for instance GitHub +# and want to reuse the introduction page also for the doxygen output. + +USE_MDFILE_AS_MAINPAGE = + +#--------------------------------------------------------------------------- +# Configuration options related to source browsing +#--------------------------------------------------------------------------- + +# If the SOURCE_BROWSER tag is set to YES then a list of source files will be +# generated. Documented entities will be cross-referenced with these sources. +# +# Note: To get rid of all source code in the generated output, make sure that +# also VERBATIM_HEADERS is set to NO. +# The default value is: NO. + +SOURCE_BROWSER = NO + +# Setting the INLINE_SOURCES tag to YES will include the body of functions, +# classes and enums directly into the documentation. +# The default value is: NO. + +INLINE_SOURCES = NO + +# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any +# special comment blocks from generated source code fragments. Normal C, C++ and +# Fortran comments will always remain visible. +# The default value is: YES. + +STRIP_CODE_COMMENTS = YES + +# If the REFERENCED_BY_RELATION tag is set to YES then for each documented +# entity all documented functions referencing it will be listed. +# The default value is: NO. + +REFERENCED_BY_RELATION = NO + +# If the REFERENCES_RELATION tag is set to YES then for each documented function +# all documented entities called/used by that function will be listed. +# The default value is: NO. + +REFERENCES_RELATION = NO + +# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set +# to YES then the hyperlinks from functions in REFERENCES_RELATION and +# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will +# link to the documentation. +# The default value is: YES. + +REFERENCES_LINK_SOURCE = YES + +# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the +# source code will show a tooltip with additional information such as prototype, +# brief description and links to the definition and documentation. Since this +# will make the HTML file larger and loading of large files a bit slower, you +# can opt to disable this feature. +# The default value is: YES. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +SOURCE_TOOLTIPS = YES + +# If the USE_HTAGS tag is set to YES then the references to source code will +# point to the HTML generated by the htags(1) tool instead of doxygen built-in +# source browser. The htags tool is part of GNU's global source tagging system +# (see https://www.gnu.org/software/global/global.html). You will need version +# 4.8.6 or higher. +# +# To use it do the following: +# - Install the latest version of global +# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file +# - Make sure the INPUT points to the root of the source tree +# - Run doxygen as normal +# +# Doxygen will invoke htags (and that will in turn invoke gtags), so these +# tools must be available from the command line (i.e. in the search path). +# +# The result: instead of the source browser generated by doxygen, the links to +# source code will now point to the output of htags. +# The default value is: NO. +# This tag requires that the tag SOURCE_BROWSER is set to YES. + +USE_HTAGS = NO + +# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a +# verbatim copy of the header file for each class for which an include is +# specified. Set to NO to disable this. +# See also: Section \class. +# The default value is: YES. + +VERBATIM_HEADERS = YES + +# If the CLANG_ASSISTED_PARSING tag is set to YES then doxygen will use the +# clang parser (see: +# http://clang.llvm.org/) for more accurate parsing at the cost of reduced +# performance. This can be particularly helpful with template rich C++ code for +# which doxygen's built-in parser lacks the necessary type information. +# Note: The availability of this option depends on whether or not doxygen was +# generated with the -Duse_libclang=ON option for CMake. +# The default value is: NO. + +CLANG_ASSISTED_PARSING = NO + +# If the CLANG_ASSISTED_PARSING tag is set to YES and the CLANG_ADD_INC_PATHS +# tag is set to YES then doxygen will add the directory of each input to the +# include path. +# The default value is: YES. +# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. + +CLANG_ADD_INC_PATHS = YES + +# If clang assisted parsing is enabled you can provide the compiler with command +# line options that you would normally use when invoking the compiler. Note that +# the include paths will already be set by doxygen for the files and directories +# specified with INPUT and INCLUDE_PATH. +# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. + +CLANG_OPTIONS = + +# If clang assisted parsing is enabled you can provide the clang parser with the +# path to the directory containing a file called compile_commands.json. This +# file is the compilation database (see: +# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) containing the +# options used when the source files were built. This is equivalent to +# specifying the -p option to a clang tool, such as clang-check. These options +# will then be passed to the parser. Any options specified with CLANG_OPTIONS +# will be added as well. +# Note: The availability of this option depends on whether or not doxygen was +# generated with the -Duse_libclang=ON option for CMake. + +CLANG_DATABASE_PATH = + +#--------------------------------------------------------------------------- +# Configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- + +# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all +# compounds will be generated. Enable this if the project contains a lot of +# classes, structs, unions or interfaces. +# The default value is: YES. + +ALPHABETICAL_INDEX = YES + +# In case all classes in a project start with a common prefix, all classes will +# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag +# can be used to specify a prefix (or a list of prefixes) that should be ignored +# while generating the index headers. +# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. + +IGNORE_PREFIX = + +#--------------------------------------------------------------------------- +# Configuration options related to the HTML output +#--------------------------------------------------------------------------- + +# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output +# The default value is: YES. + +GENERATE_HTML = YES + +# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a +# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of +# it. +# The default directory is: html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_OUTPUT = html + +# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each +# generated HTML page (for example: .htm, .php, .asp). +# The default value is: .html. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FILE_EXTENSION = .html + +# The HTML_HEADER tag can be used to specify a user-defined HTML header file for +# each generated HTML page. If the tag is left blank doxygen will generate a +# standard header. +# +# To get valid HTML the header file that includes any scripts and style sheets +# that doxygen needs, which is dependent on the configuration options used (e.g. +# the setting GENERATE_TREEVIEW). It is highly recommended to start with a +# default header using +# doxygen -w html new_header.html new_footer.html new_stylesheet.css +# YourConfigFile +# and then modify the file new_header.html. See also section "Doxygen usage" +# for information on how to generate the default header that doxygen normally +# uses. +# Note: The header is subject to change so you typically have to regenerate the +# default header when upgrading to a newer version of doxygen. For a description +# of the possible markers and block names see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_HEADER = + +# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each +# generated HTML page. If the tag is left blank doxygen will generate a standard +# footer. See HTML_HEADER for more information on how to generate a default +# footer and what special commands can be used inside the footer. See also +# section "Doxygen usage" for information on how to generate the default footer +# that doxygen normally uses. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FOOTER = + +# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style +# sheet that is used by each HTML page. It can be used to fine-tune the look of +# the HTML output. If left blank doxygen will generate a default style sheet. +# See also section "Doxygen usage" for information on how to generate the style +# sheet that doxygen normally uses. +# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as +# it is more robust and this tag (HTML_STYLESHEET) will in the future become +# obsolete. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_STYLESHEET = + +# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined +# cascading style sheets that are included after the standard style sheets +# created by doxygen. Using this option one can overrule certain style aspects. +# This is preferred over using HTML_STYLESHEET since it does not replace the +# standard style sheet and is therefore more robust against future updates. +# Doxygen will copy the style sheet files to the output directory. +# Note: The order of the extra style sheet files is of importance (e.g. the last +# style sheet in the list overrules the setting of the previous ones in the +# list). For an example see the documentation. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_STYLESHEET = + +# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or +# other source files which should be copied to the HTML output directory. Note +# that these files will be copied to the base HTML output directory. Use the +# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these +# files. In the HTML_STYLESHEET file, use the file name only. Also note that the +# files will be copied as-is; there are no commands or markers available. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_EXTRA_FILES = + +# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen +# will adjust the colors in the style sheet and background images according to +# this color. Hue is specified as an angle on a color-wheel, see +# https://en.wikipedia.org/wiki/Hue for more information. For instance the value +# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 +# purple, and 360 is red again. +# Minimum value: 0, maximum value: 359, default value: 220. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_HUE = 220 + +# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors +# in the HTML output. For a value of 0 the output will use gray-scales only. A +# value of 255 will produce the most vivid colors. +# Minimum value: 0, maximum value: 255, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_SAT = 100 + +# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the +# luminance component of the colors in the HTML output. Values below 100 +# gradually make the output lighter, whereas values above 100 make the output +# darker. The value divided by 100 is the actual gamma applied, so 80 represents +# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not +# change the gamma. +# Minimum value: 40, maximum value: 240, default value: 80. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_COLORSTYLE_GAMMA = 80 + +# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML +# page will contain the date and time when the page was generated. Setting this +# to YES can help to show when doxygen was last run and thus if the +# documentation is up to date. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_TIMESTAMP = NO + +# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML +# documentation will contain a main index with vertical navigation menus that +# are dynamically created via JavaScript. If disabled, the navigation index will +# consists of multiple levels of tabs that are statically embedded in every HTML +# page. Disable this option to support browsers that do not have JavaScript, +# like the Qt help browser. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_MENUS = YES + +# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML +# documentation will contain sections that can be hidden and shown after the +# page has loaded. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_DYNAMIC_SECTIONS = NO + +# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries +# shown in the various tree structured indices initially; the user can expand +# and collapse entries dynamically later on. Doxygen will expand the tree to +# such a level that at most the specified number of entries are visible (unless +# a fully collapsed tree already exceeds this amount). So setting the number of +# entries 1 will produce a full collapsed tree by default. 0 is a special value +# representing an infinite number of entries and will result in a full expanded +# tree by default. +# Minimum value: 0, maximum value: 9999, default value: 100. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_INDEX_NUM_ENTRIES = 100 + +# If the GENERATE_DOCSET tag is set to YES, additional index files will be +# generated that can be used as input for Apple's Xcode 3 integrated development +# environment (see: +# https://developer.apple.com/xcode/), introduced with OSX 10.5 (Leopard). To +# create a documentation set, doxygen will generate a Makefile in the HTML +# output directory. Running make will produce the docset in that directory and +# running make install will install the docset in +# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at +# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy +# genXcode/_index.html for more information. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_DOCSET = NO + +# This tag determines the name of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# The default value is: Doxygen generated docs. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDNAME = "Doxygen generated docs" + +# This tag determines the URL of the docset feed. A documentation feed provides +# an umbrella under which multiple documentation sets from a single provider +# (such as a company or product suite) can be grouped. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_FEEDURL = + +# This tag specifies a string that should uniquely identify the documentation +# set bundle. This should be a reverse domain-name style string, e.g. +# com.mycompany.MyDocSet. Doxygen will append .docset to the name. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_BUNDLE_ID = org.doxygen.Project + +# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify +# the documentation publisher. This should be a reverse domain-name style +# string, e.g. com.mycompany.MyDocSet.documentation. +# The default value is: org.doxygen.Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_ID = org.doxygen.Publisher + +# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. +# The default value is: Publisher. +# This tag requires that the tag GENERATE_DOCSET is set to YES. + +DOCSET_PUBLISHER_NAME = Publisher + +# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three +# additional HTML index files: index.hhp, index.hhc, and index.hhk. The +# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop +# on Windows. In the beginning of 2021 Microsoft took the original page, with +# a.o. the download links, offline the HTML help workshop was already many years +# in maintenance mode). You can download the HTML help workshop from the web +# archives at Installation executable (see: +# http://web.archive.org/web/20160201063255/http://download.microsoft.com/downlo +# ad/0/A/9/0A939EF6-E31C-430F-A3DF-DFAE7960D564/htmlhelp.exe). +# +# The HTML Help Workshop contains a compiler that can convert all HTML output +# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML +# files are now used as the Windows 98 help format, and will replace the old +# Windows help format (.hlp) on all Windows platforms in the future. Compressed +# HTML files also contain an index, a table of contents, and you can search for +# words in the documentation. The HTML workshop also contains a viewer for +# compressed HTML files. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_HTMLHELP = NO + +# The CHM_FILE tag can be used to specify the file name of the resulting .chm +# file. You can add a path in front of the file if the result should not be +# written to the html output directory. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_FILE = + +# The HHC_LOCATION tag can be used to specify the location (absolute path +# including file name) of the HTML help compiler (hhc.exe). If non-empty, +# doxygen will try to run the HTML help compiler on the generated index.hhp. +# The file has to be specified with full path. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +HHC_LOCATION = + +# The GENERATE_CHI flag controls if a separate .chi index file is generated +# (YES) or that it should be included in the main .chm file (NO). +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +GENERATE_CHI = NO + +# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc) +# and project file content. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +CHM_INDEX_ENCODING = + +# The BINARY_TOC flag controls whether a binary table of contents is generated +# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it +# enables the Previous and Next buttons. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +BINARY_TOC = NO + +# The TOC_EXPAND flag can be set to YES to add extra items for group members to +# the table of contents of the HTML help documentation and to the tree view. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTMLHELP is set to YES. + +TOC_EXPAND = NO + +# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and +# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that +# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help +# (.qch) of the generated HTML documentation. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_QHP = NO + +# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify +# the file name of the resulting .qch file. The path specified is relative to +# the HTML output folder. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QCH_FILE = + +# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help +# Project output. For more information please see Qt Help Project / Namespace +# (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace). +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_NAMESPACE = org.doxygen.Project + +# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt +# Help Project output. For more information please see Qt Help Project / Virtual +# Folders (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-folders). +# The default value is: doc. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_VIRTUAL_FOLDER = doc + +# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom +# filter to add. For more information please see Qt Help Project / Custom +# Filters (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_NAME = + +# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the +# custom filter to add. For more information please see Qt Help Project / Custom +# Filters (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_CUST_FILTER_ATTRS = + +# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this +# project's filter section matches. Qt Help Project / Filter Attributes (see: +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes). +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHP_SECT_FILTER_ATTRS = + +# The QHG_LOCATION tag can be used to specify the location (absolute path +# including file name) of Qt's qhelpgenerator. If non-empty doxygen will try to +# run qhelpgenerator on the generated .qhp file. +# This tag requires that the tag GENERATE_QHP is set to YES. + +QHG_LOCATION = + +# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be +# generated, together with the HTML files, they form an Eclipse help plugin. To +# install this plugin and make it available under the help contents menu in +# Eclipse, the contents of the directory containing the HTML and XML files needs +# to be copied into the plugins directory of eclipse. The name of the directory +# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. +# After copying Eclipse needs to be restarted before the help appears. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_ECLIPSEHELP = NO + +# A unique identifier for the Eclipse help plugin. When installing the plugin +# the directory name containing the HTML and XML files should also have this +# name. Each documentation set should have its own identifier. +# The default value is: org.doxygen.Project. +# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. + +ECLIPSE_DOC_ID = org.doxygen.Project + +# If you want full control over the layout of the generated HTML pages it might +# be necessary to disable the index and replace it with your own. The +# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top +# of each HTML page. A value of NO enables the index and the value YES disables +# it. Since the tabs in the index contain the same information as the navigation +# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +DISABLE_INDEX = NO + +# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index +# structure should be generated to display hierarchical information. If the tag +# value is set to YES, a side panel will be generated containing a tree-like +# index structure (just like the one that is generated for HTML Help). For this +# to work a browser that supports JavaScript, DHTML, CSS and frames is required +# (i.e. any modern browser). Windows users are probably better off using the +# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can +# further fine tune the look of the index (see "Fine-tuning the output"). As an +# example, the default style sheet generated by doxygen has an example that +# shows how to put an image at the root of the tree instead of the PROJECT_NAME. +# Since the tree basically has the same information as the tab index, you could +# consider setting DISABLE_INDEX to YES when enabling this option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +GENERATE_TREEVIEW = YES + +# When both GENERATE_TREEVIEW and DISABLE_INDEX are set to YES, then the +# FULL_SIDEBAR option determines if the side bar is limited to only the treeview +# area (value NO) or if it should extend to the full height of the window (value +# YES). Setting this to YES gives a layout similar to +# https://docs.readthedocs.io with more room for contents, but less room for the +# project logo, title, and description. If either GENERATE_TREEVIEW or +# DISABLE_INDEX is set to NO, this option has no effect. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FULL_SIDEBAR = NO + +# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that +# doxygen will group on one line in the generated HTML documentation. +# +# Note that a value of 0 will completely suppress the enum values from appearing +# in the overview section. +# Minimum value: 0, maximum value: 20, default value: 4. +# This tag requires that the tag GENERATE_HTML is set to YES. + +ENUM_VALUES_PER_LINE = 4 + +# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used +# to set the initial width (in pixels) of the frame in which the tree is shown. +# Minimum value: 0, maximum value: 1500, default value: 250. +# This tag requires that the tag GENERATE_HTML is set to YES. + +TREEVIEW_WIDTH = 250 + +# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to +# external symbols imported via tag files in a separate window. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +EXT_LINKS_IN_WINDOW = NO + +# If the OBFUSCATE_EMAILS tag is set to YES, doxygen will obfuscate email +# addresses. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +OBFUSCATE_EMAILS = YES + +# If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg +# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see +# https://inkscape.org) to generate formulas as SVG images instead of PNGs for +# the HTML output. These images will generally look nicer at scaled resolutions. +# Possible values are: png (the default) and svg (looks nicer but requires the +# pdf2svg or inkscape tool). +# The default value is: png. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FORMULA_FORMAT = png + +# Use this tag to change the font size of LaTeX formulas included as images in +# the HTML documentation. When you change the font size after a successful +# doxygen run you need to manually remove any form_*.png images from the HTML +# output directory to force them to be regenerated. +# Minimum value: 8, maximum value: 50, default value: 10. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_FONTSIZE = 10 + +# Use the FORMULA_TRANSPARENT tag to determine whether or not the images +# generated for formulas are transparent PNGs. Transparent PNGs are not +# supported properly for IE 6.0, but are supported on all modern browsers. +# +# Note that when changing this option you need to delete any form_*.png files in +# the HTML output directory before the changes have effect. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +FORMULA_TRANSPARENT = YES + +# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands +# to create new LaTeX commands to be used in formulas as building blocks. See +# the section "Including formulas" for details. + +FORMULA_MACROFILE = + +# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see +# https://www.mathjax.org) which uses client side JavaScript for the rendering +# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX +# installed or if you want to formulas look prettier in the HTML output. When +# enabled you may also need to install MathJax separately and configure the path +# to it using the MATHJAX_RELPATH option. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +USE_MATHJAX = NO + +# With MATHJAX_VERSION it is possible to specify the MathJax version to be used. +# Note that the different versions of MathJax have different requirements with +# regards to the different settings, so it is possible that also other MathJax +# settings have to be changed when switching between the different MathJax +# versions. +# Possible values are: MathJax_2 and MathJax_3. +# The default value is: MathJax_2. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_VERSION = MathJax_2 + +# When MathJax is enabled you can set the default output format to be used for +# the MathJax output. For more details about the output format see MathJax +# version 2 (see: +# http://docs.mathjax.org/en/v2.7-latest/output.html) and MathJax version 3 +# (see: +# http://docs.mathjax.org/en/latest/web/components/output.html). +# Possible values are: HTML-CSS (which is slower, but has the best +# compatibility. This is the name for Mathjax version 2, for MathJax version 3 +# this will be translated into chtml), NativeMML (i.e. MathML. Only supported +# for NathJax 2. For MathJax version 3 chtml will be used instead.), chtml (This +# is the name for Mathjax version 3, for MathJax version 2 this will be +# translated into HTML-CSS) and SVG. +# The default value is: HTML-CSS. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_FORMAT = HTML-CSS + +# When MathJax is enabled you need to specify the location relative to the HTML +# output directory using the MATHJAX_RELPATH option. The destination directory +# should contain the MathJax.js script. For instance, if the mathjax directory +# is located at the same level as the HTML output directory, then +# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax +# Content Delivery Network so you can quickly see the result without installing +# MathJax. However, it is strongly recommended to install a local copy of +# MathJax from https://www.mathjax.org before deployment. The default value is: +# - in case of MathJax version 2: https://cdn.jsdelivr.net/npm/mathjax@2 +# - in case of MathJax version 3: https://cdn.jsdelivr.net/npm/mathjax@3 +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_RELPATH = + +# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax +# extension names that should be enabled during MathJax rendering. For example +# for MathJax version 2 (see +# https://docs.mathjax.org/en/v2.7-latest/tex.html#tex-and-latex-extensions): +# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols +# For example for MathJax version 3 (see +# http://docs.mathjax.org/en/latest/input/tex/extensions/index.html): +# MATHJAX_EXTENSIONS = ams +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_EXTENSIONS = + +# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces +# of code that will be used on startup of the MathJax code. See the MathJax site +# (see: +# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. For an +# example see the documentation. +# This tag requires that the tag USE_MATHJAX is set to YES. + +MATHJAX_CODEFILE = + +# When the SEARCHENGINE tag is enabled doxygen will generate a search box for +# the HTML output. The underlying search engine uses javascript and DHTML and +# should work on any modern browser. Note that when using HTML help +# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) +# there is already a search function so this one should typically be disabled. +# For large projects the javascript based search engine can be slow, then +# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to +# search using the keyboard; to jump to the search box use + S +# (what the is depends on the OS and browser, but it is typically +# , /