initial release

This commit is contained in:
Limo
2024-08-12 19:12:41 +02:00
commit 984a660eed
607 changed files with 37936 additions and 0 deletions

79
.gitignore vendored Normal file
View File

@@ -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

246
CMakeLists.txt Normal file
View File

@@ -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})

674
LICENSE Normal file
View File

@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
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:
<program> Copyright (C) <year> <name of author>
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
<https://www.gnu.org/licenses/>.
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
<https://www.gnu.org/licenses/why-not-lgpl.html>.

98
README.md Normal file
View File

@@ -0,0 +1,98 @@
# Limo <img align="right" src="resources/logo.png" alt="logo" width="40"/>
---
General purpose mod manager primarily developed for Linux with support for the [NexusMods](https://www.nexusmods.com/) API and [LOOT](https://loot.github.io/).
<p align="center">
<img src="resources/showcase.png" alt="logo" width="800"/>
</p>
## 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
```

View File

@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="22.0px"
height="22.0px"
viewBox="0 0 22.0 22.0"
version="1.1"
id="SVGRoot"
sodipodi:docname="filter_accept.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview261"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="28.963094"
inkscape:cx="11.773604"
inkscape:cy="10.030006"
inkscape:window-width="1920"
inkscape:window-height="1022"
inkscape:window-x="0"
inkscape:window-y="30"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid384" />
</sodipodi:namedview>
<defs
id="defs256" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<circle
style="fill:none;fill-opacity:1;stroke:#3da433;stroke-linecap:round;stroke-opacity:1;paint-order:markers fill stroke;stop-color:#000000;stroke-width:1.37952756;stroke-dasharray:none"
id="path11098"
cx="11"
cy="11"
r="10.078342" />
<g
id="g18575"
transform="rotate(-45,11.38053,6.1422914)">
<rect
style="fill:none;fill-opacity:1;stroke:#3da433;stroke-width:0.817151;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke;stop-color:#000000"
id="rect15572"
width="13.101018"
height="0.66756999"
x="2.8178916"
y="10.666215" />
<rect
style="fill:#3da433;fill-opacity:1;stroke:#3da433;stroke-width:0.635809;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke;stop-color:#000000"
id="rect15572-6"
width="6.2371912"
height="0.84891129"
x="3.8080847"
y="-3.5761318"
transform="rotate(90)" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="22.0px"
height="22.0px"
viewBox="0 0 22.0 22.0"
version="1.1"
id="SVGRoot"
sodipodi:docname="filter_reject.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview261"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="28.963094"
inkscape:cx="7.6304003"
inkscape:cy="10.064532"
inkscape:window-width="1920"
inkscape:window-height="1022"
inkscape:window-x="0"
inkscape:window-y="30"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
type="xygrid"
id="grid384" />
</sodipodi:namedview>
<defs
id="defs256" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<circle
style="fill:none;fill-opacity:1;stroke:#dc2222;stroke-linecap:round;stroke-opacity:1;paint-order:markers fill stroke;stop-color:#000000;stroke-width:1.37952756;stroke-dasharray:none"
id="path11098"
cx="11"
cy="11"
r="10.078342" />
<rect
style="fill:none;fill-opacity:1;stroke:#dc2222;stroke-width:0.929238;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke;stop-color:#000000"
id="rect15572"
width="20.349926"
height="0.55576187"
x="-25.731312"
y="-0.27788076"
transform="rotate(-135)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

7
resources/icons.qrc Normal file
View File

@@ -0,0 +1,7 @@
<RCC>
<qresource prefix="/">
<file>filter_accept.svg</file>
<file>filter_reject.svg</file>
<file>logo.png</file>
</qresource>
</RCC>

BIN
resources/logo.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
resources/logo_small.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
resources/showcase.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

42
src/core/addmodinfo.h Normal file
View File

@@ -0,0 +1,42 @@
/*!
* \file addmodinfo.h
* \brief Contains the AddModInfo struct.
*/
#pragma once
#include <filesystem>
#include <string>
#include <vector>
/*!
* \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<int> 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<std::pair<std::filesystem::path, std::filesystem::path>> 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 = "";
};

72
src/core/appinfo.h Normal file
View File

@@ -0,0 +1,72 @@
/*!
* \file appinfo.h
* \brief Contains the AppInfo struct.
*/
#pragma once
#include "tagcondition.h"
#include <map>
#include <string>
#include <vector>
/*!
* \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<std::string> deployers{};
/*!
* \brief Types of \ref Deployer "deployers" belonging to the
* \ref ModdedApplication "application".
*/
std::vector<std::string> deployer_types{};
/*!
* \brief Staging directory of \ref Deployer "deployers" belonging to the
* \ref ModdedApplication "application".
*/
std::vector<std::string> target_dirs{};
/*!
* \brief Number of mods for each \ref Deployer "deployer" belonging to the
* \ref ModdedApplication "application".
*/
std::vector<int> deployer_mods{};
/*!
* \brief One bool per deployer indicating whether file are copied for deployment.
*/
std::vector<bool> uses_copy_deployment{};
/*!
* \brief Name and command for each tool belonging to the
* \ref ModdedApplication "application".
*/
std::vector<std::tuple<std::string, std::string>> tools{};
/*!
* \brief Maps the names of all manual tags to the number of mods with that tag in the
* \ref ModdedApplication "application".
*/
std::map<std::string, int> 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<std::string, int> 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<std::string, std::pair<std::string, std::vector<TagCondition>>> auto_tags;
/*! \brief Version of the target application. */
std::string app_version = "";
};

110
src/core/autotag.cpp Normal file
View File

@@ -0,0 +1,110 @@
#include "autotag.h"
#include "parseerror.h"
#include <format>
#include <ranges>
namespace sfs = std::filesystem;
namespace str = std::ranges;
AutoTag::AutoTag(const std::string& name,
const std::string& expression,
const std::vector<TagCondition>& 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<TagCondition>& 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<TagCondition> AutoTag::getConditions() const
{
return conditions_;
}
int AutoTag::getNumConditions() const
{
return conditions_.size();
}

191
src/core/autotag.h Normal file
View File

@@ -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 <filesystem>
#include <json/json.h>
#include <optional>
#include <string>
#include <vector>
/*!
* \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<TagCondition>& 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<typename View>
void reapplyMods(const std::map<int, std::vector<std::pair<std::string, std::string>>>& files,
const View& mods,
std::optional<ProgressNode*> 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<typename View>
void reapplyMods(const std::filesystem::path& staging_dir,
const View& mods,
std::optional<ProgressNode*> 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<typename View>
void updateMods(const std::map<int, std::vector<std::pair<std::string, std::string>>>& files,
const View& mods,
std::optional<ProgressNode*> 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<typename View>
void updateMods(const std::filesystem::path& staging_dir,
const View& mods,
std::optional<ProgressNode*> 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<TagCondition>& 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<TagCondition> 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<typename View>
static std::map<int, std::vector<std::pair<std::string, std::string>>> readModFiles(
const std::filesystem::path& staging_dir,
View mods,
std::optional<ProgressNode*> progress_node = {})
{
std::map<int, std::vector<std::pair<std::string, std::string>>> 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<TagCondition> conditions_;
/*!
* \brief This tag is applied to a mod if this nodes evaluate function returns true for
* the mods installation directory
*/
TagConditionNode evaluator_;
};

397
src/core/backupmanager.cpp Normal file
View File

@@ -0,0 +1,397 @@
#include "backupmanager.h"
#include "parseerror.h"
#include "pathutils.h"
#include <format>
#include <fstream>
namespace sfs = std::filesystem;
namespace pu = path_utils;
void BackupManager::addTarget(const sfs::path& path,
const std::string& name,
const std::vector<std::string>& 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<std::string>{ backup_names[0] },
std::vector<int>(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<BackupTarget> 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<void(Log::LogLevel, const std::string&)>& new_log)
{
log_ = new_log;
}
void BackupManager::updateDirectories(int target_id)
{
std::vector<int> 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<sfs::path> 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<std::string> 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<int> 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);
}

196
src/core/backupmanager.h Normal file
View File

@@ -0,0 +1,196 @@
/*!
* \file backupmanager.h
* \brief Header for the BackupManager class.
*/
#pragma once
#include "backuptarget.h"
#include "log.h"
#include <filesystem>
#include <functional>
#include <json/json.h>
#include <vector>
/*!
* \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<std::string>& 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<BackupTarget> 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<void(Log::LogLevel, const std::string&)>& 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<BackupTarget> targets_{};
/*! \brief Number of profiles. */
int num_profiles_ = 0;
/*! \brief Currently active profile. */
int cur_profile_ = -1;
/*! \brief Callback for logging. */
std::function<void(Log::LogLevel, const std::string&)> 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;
};

21
src/core/backuptarget.cpp Normal file
View File

@@ -0,0 +1,21 @@
#include "backuptarget.h"
BackupTarget::BackupTarget(const std::filesystem::path& path,
const std::string& target_name,
const std::vector<std::string>& backup_names,
const std::vector<int>& 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();
}

46
src/core/backuptarget.h Normal file
View File

@@ -0,0 +1,46 @@
/*!
* \file backuptarget.h
* \brief Header for the BackupTarget struct.
*/
#pragma once
#include <filesystem>
#include <vector>
/*!
* \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<std::string> backup_names;
/*! \brief Contains the currently active backup for every profile. */
std::vector<int> 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<std::string>& backup_names,
const std::vector<int>& 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;
};

View File

@@ -0,0 +1,145 @@
#include "casematchingdeployer.h"
#include "pathutils.h"
#include <algorithm>
#include <format>
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<int, unsigned long> CaseMatchingDeployer::deploy(
const std::vector<int>& loadorder,
std::optional<ProgressNode*> progress_node)
{
if(progress_node)
(*progress_node)->addChildren({ 2, 1, 3 });
adaptLoadorderFiles(loadorder,
progress_node ? &(*progress_node)->child(0) : std::optional<ProgressNode*>{});
updateConflictGroups(progress_node ? &(*progress_node)->child(1) : std::optional<ProgressNode*>{});
return Deployer::deploy(
loadorder, progress_node ? &(*progress_node)->child(2) : std::optional<ProgressNode*>{});
}
void CaseMatchingDeployer::adaptDirectoryFiles(const sfs::path& path,
int mod_id,
const sfs::path& target_path) const
{
std::vector<sfs::path> 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<int>& loadorder,
std::optional<ProgressNode*> 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<std::string, std::string> file_name_map;
for(int mod_id : loadorder)
{
const sfs::path mod_path = source_path_ / std::to_string(mod_id);
std::vector<sfs::path> 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();
}
}

View File

@@ -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<int, unsigned long> deploy(
const std::vector<int>& loadorder,
std::optional<ProgressNode*> 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<int>& loadorder,
std::optional<ProgressNode*> progress_node = {}) const;
};

View File

@@ -0,0 +1,25 @@
/*!
* \file compressionerror.h
* \brief Contains the CompressionError class
*/
#pragma once
/*!
* \brief Exception used for errors during archive extraction.
*/
#include <stdexcept>
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(); };
};

31
src/core/conflictinfo.h Normal file
View File

@@ -0,0 +1,31 @@
/*!
* \file conflictinfo.h
* \brief Contains the ConflictInfo struct.
*/
#pragma once
#include <string>
/*!
* \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))
{}
};

121
src/core/cryptography.cpp Normal file
View File

@@ -0,0 +1,121 @@
#include "cryptography.h"
#include <cmath>
#include <openssl/aes.h>
#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/rand.h>
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<std::string, std::string, std::string> 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<const char*>(cipher_text), cipher_length);
const std::string nonce_str(reinterpret_cast<const char*>(nonce), nonce_size);
const std::string tag_str(reinterpret_cast<const char*>(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<const char*>(plain_text), plain_text_length);
}
}

58
src/core/cryptography.h Normal file
View File

@@ -0,0 +1,58 @@
/*!
* \file cryptography.h
* \brief Header for the cryptography namespace.
*/
#pragma once
#include <stdexcept>
#include <string>
/*!
* \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<std::string, std::string, std::string> 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";
};

696
src/core/deployer.cpp Normal file
View File

@@ -0,0 +1,696 @@
#include "deployer.h"
#include "pathutils.h"
#include <algorithm>
#include <format>
#include <fstream>
#include <iostream>
#include <json/json.h>
#include <ranges>
#include <set>
#include <unordered_set>
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<int, unsigned long> Deployer::deploy(const std::vector<int>& loadorder,
std::optional<ProgressNode*> 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<sfs::path, int> dest_files =
loadDeployedFiles(progress_node ? &(*progress_node)->child(0) : std::optional<ProgressNode*>{});
backupOrRestoreFiles(source_files, dest_files);
deployFiles(source_files,
progress_node ? &(*progress_node)->child(1) : std::optional<ProgressNode*>{});
saveDeployedFiles(source_files,
progress_node ? &(*progress_node)->child(2) : std::optional<ProgressNode*>{});
return mod_sizes;
}
std::map<int, unsigned long> Deployer::deploy(std::optional<ProgressNode*> progress_node)
{
std::vector<int> 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<std::tuple<int, bool>>& loadorder)
{
loadorders_[current_profile_] = loadorder;
}
std::vector<std::tuple<int, bool>> Deployer::getLoadorder() const
{
if(loadorders_.empty() || current_profile_ < 0 || current_profile_ >= loadorders_.size() ||
loadorders_[current_profile_].empty())
return std::vector<std::tuple<int, bool>>{};
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<ConflictInfo> Deployer::getFileConflicts(
int mod_id,
bool show_disabled,
std::optional<ProgressNode*> progress_node) const
{
std::vector<ConflictInfo> conflicts;
std::unordered_set<std::string> unique_files;
std::unordered_set<std::string> 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<int> 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<int> Deployer::getModConflicts(int mod_id,
std::optional<ProgressNode*> progress_node)
{
std::unordered_set<int> conflicts{ mod_id };
std::unordered_set<std::string> 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<std::tuple<int, bool>>{});
conflict_groups_.push_back(std::vector<std::vector<int>>{});
}
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<ProgressNode*> progress_node)
{
updateConflictGroups(progress_node);
std::vector<std::tuple<int, bool>> 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<std::vector<int>> Deployer::getConflictGroups() const
{
return conflict_groups_[current_profile_];
}
void Deployer::setConflictGroups(const std::vector<std::vector<int>>& 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<std::string> 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<std::filesystem::path, int>, std::map<int, unsigned long>>
Deployer::getDeploymentSourceFilesAndModSizes(const std::vector<int>& loadorder) const
{
std::map<sfs::path, int> source_files{};
std::map<int, unsigned long> 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<sfs::path, int>& source_files,
const std::map<sfs::path, int>& dest_files) const
{
std::map<sfs::path, int> restore_targets;
std::map<sfs::path, int> 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<sfs::path, int> 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<sfs::path, int>& source_files,
std::optional<ProgressNode*> 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<sfs::path, int> Deployer::loadDeployedFiles(
std::optional<ProgressNode*> progress_node) const
{
if(progress_node)
{
(*progress_node)->addChildren({ 1, 2 });
(*progress_node)->child(0).setTotalSteps(1);
}
std::map<sfs::path, int> 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<sfs::path, int>& deployed_files,
std::optional<ProgressNode*> 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<std::string> Deployer::getModFiles(int mod_id, bool include_directories) const
{
std::unordered_set<std::string> 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<ProgressNode*> progress_node)
{
log_(Log::LOG_INFO, std::format("Deployer '{}': Updating conflict groups...", name_));
std::map<std::string, int> file_map;
std::vector<std::set<int>> groups;
std::vector<int> 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<std::set<int>> merged_groups;
// merge groups
for(int i = 0; i < groups.size(); i++)
{
if(groups[i].empty())
continue;
std::set<int> 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<int> 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<std::vector<int>> sorted_groups(merged_groups.size() + 1, std::vector<int>());
// 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<void(Log::LogLevel, const std::string&)>& newLog)
{
log_ = newLog;
}
void Deployer::cleanup()
{
deploy(std::vector<int>{});
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<bool> 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<std::vector<std::string>> Deployer::getAutoTags()
{
return {};
}
std::map<std::string, int> Deployer::getAutoTagMap()
{
return {};
}

349
src/core/deployer.h Normal file
View File

@@ -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 <filesystem>
#include <map>
#include <optional>
#include <unordered_set>
#include <vector>
/*!
* \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<int, unsigned long> deploy(const std::vector<int>& loadorder,
std::optional<ProgressNode*> 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<int, unsigned long> deploy(std::optional<ProgressNode*> progress_node = {});
/*!
* \brief Setter for the load order used for deployment.
* \param loadorder The new load order.
*/
void setLoadorder(const std::vector<std::tuple<int, bool>>& loadorder);
/*!
* \brief Getter for the current mod load order.
* \return The load order.
*/
virtual std::vector<std::tuple<int, bool>> 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<ConflictInfo> getFileConflicts(
int mod_id,
bool show_disabled = false,
std::optional<ProgressNode*> 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<int> getModConflicts(int mod_id,
std::optional<ProgressNode*> 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<ProgressNode*> progress_node = {});
/*!
* \brief Getter for the conflict groups of the current profile.
* \return The conflict groups.
*/
virtual std::vector<std::vector<int>> 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<std::vector<int>>& 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<std::string> 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<void(Log::LogLevel, const std::string&)>& 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<ProgressNode*> 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<bool> 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<std::vector<std::string>> 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<std::string, int> 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<std::vector<std::tuple<int, bool>>> loadorders_;
/*!
* \brief For every profile: Groups of mods which conflict with each other. The last
* group contains mods with no conflicts.
*/
std::vector<std::vector<std::vector<int>>> 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<std::filesystem::path, int>, std::map<int, unsigned long>>
getDeploymentSourceFilesAndModSizes(const std::vector<int>& 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<std::filesystem::path, int>& source_files,
const std::map<std::filesystem::path, int>& 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<std::filesystem::path, int>& source_files,
std::optional<ProgressNode*> 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<std::filesystem::path, int> loadDeployedFiles(
std::optional<ProgressNode*> 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<std::filesystem::path, int>& deployed_files,
std::optional<ProgressNode*> 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<std::string> getModFiles(int mod_id, bool include_directories = false) const;
/*! \brief Callback for logging. */
std::function<void(Log::LogLevel, const std::string&)> 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;
};

View File

@@ -0,0 +1,21 @@
#include "deployerfactory.h"
#include "casematchingdeployer.h"
#include "lootdeployer.h"
std::unique_ptr<Deployer> 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<Deployer>(source_path, dest_path, name, use_copy_deployment);
else if(type == CASEMATCHINGDEPLOYER)
return std::make_unique<CaseMatchingDeployer>(
source_path, dest_path, name, use_copy_deployment);
else if(type == LOOTDEPLOYER)
return std::make_unique<LootDeployer>(source_path, dest_path, name);
else
throw std::runtime_error("Unknown deployer type \"" + type + "\"!");
}

View File

@@ -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<std::string> DEPLOYER_TYPES{ CASEMATCHINGDEPLOYER,
SIMPLEDEPLOYER,
LOOTDEPLOYER };
/*! \brief Maps deployer types to a description of what they do. */
inline static const std::map<std::string, std::string> 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<std::string, bool> 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<Deployer> 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);
};

32
src/core/deployerinfo.h Normal file
View File

@@ -0,0 +1,32 @@
/*!
* \file deployerinfo.h
* \brief Contains the DeployerInfo struct.
*/
#pragma once
#include <map>
#include <string>
#include <vector>
/*!
* \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<std::string> mod_names;
/*! \brief The \ref Deployer "deployer's" load order. */
std::vector<std::tuple<int, bool>> loadorder;
/*! \brief Contains groups of mods which conflict with each other. */
std::vector<std::vector<int>> 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<std::vector<std::string>> manual_tags;
/*! \brief For every mod: A vector of auto tags added to that mod. */
std::vector<std::vector<std::string>> auto_tags;
/*! \brief Maps tag names to the number of mods for that tag. */
std::map<std::string, int> mods_per_tag;
};

View File

@@ -0,0 +1,38 @@
/*!
* \file editapplicationinfo.h
* \brief Contains the EditApplicationInfo struct.
*/
#pragma once
#include <string>
#include <vector>
/*!
* \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<std::pair<std::string, std::string>> 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;
};

View File

@@ -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<TagCondition>& 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<TagCondition> EditAutoTagAction::getConditions() const
{
return conditions_;
}

View File

@@ -0,0 +1,92 @@
/*!
* \file editautotagaction.h
* \brief Header for the EditAutoTagAction class.
*/
#pragma once
#include "tagcondition.h"
#include <string>
#include <vector>
/*!
* \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<TagCondition>& 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<TagCondition> 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<TagCondition> conditions_;
};

View File

@@ -0,0 +1,27 @@
/*!
* \file editdeployerinfo.h
* \brief Contains the EditDeployerInfo struct.
*/
#pragma once
#include <string>
/*!
* \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 = "";
};

View File

@@ -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_;
}

View File

@@ -0,0 +1,60 @@
/*!
* \file editmanualtagaction.h
* \brief Header for the EditManualTagAction class.
*/
#pragma once
#include <string>
/*!
* \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_;
};

View File

@@ -0,0 +1,24 @@
/*!
* \file editprofileinfo.h
* \brief Contains the EditProfileInfo struct.
*/
#pragma once
#include <string>
/*!
* \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;
};

View File

@@ -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<std::string, std::pair<std::string, pugi::xml_node>> 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<std::string, std::string>& flags,
std::function<bool(std::string)> eval_game_version,
std::function<bool(std::string)> 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 + " )";
}
}

View File

@@ -0,0 +1,83 @@
/*!
* \file fomoddependency.h
* \brief Header for the FomodDependency class and FomodFile struct.
*/
#pragma once
#include "pugixml.hpp"
#include <filesystem>
#include <functional>
#include <map>
#include <optional>
#include <string>
#include <vector>
/*!
* \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<std::string, std::string>& flags,
std::function<bool(std::string)> eval_game_version,
std::function<bool(std::string)> 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<Dependency> children_;
};
}

49
src/core/fomod/file.h Normal file
View File

@@ -0,0 +1,49 @@
/*!
* \file file.h
* \brief Header for the File struct.
*/
#pragma once
#include <filesystem>
/*!
* \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<int>::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; }
};
}

View File

@@ -0,0 +1,411 @@
#include "fomodinstaller.h"
#include "../log.h"
#include "../pathutils.h"
#include <algorithm>
#include <format>
#include <ranges>
#include <regex>
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<InstallStep> FomodInstaller::step(const std::vector<std::vector<bool>>& 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<std::pair<std::vector<std::vector<bool>>, 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<std::vector<bool>>& selection) const
{
if(cur_step_ == steps_.size() - 1)
return false;
std::map<std::string, std::string> 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<std::string, std::string> 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<std::pair<sfs::path, sfs::path>> FomodInstaller::getInstallationFiles(
const std::vector<std::vector<bool>>& selection)
{
updateState(selection);
parseInstallList();
std::vector<std::pair<sfs::path, sfs::path>> 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<File>& 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<File> 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<std::vector<bool>>& 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<std::string, std::string> 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 };
}

View File

@@ -0,0 +1,169 @@
/*!
* \file fomodinstaller.h
* \brief Header for the FomodInstaller class.
*/
#pragma once
#include "installstep.h"
#include <algorithm>
#include <filesystem>
#include <pugixml.hpp>
#include <ranges>
#include <vector>
/*!
* \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<InstallStep> step(const std::vector<std::vector<bool>>& 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<std::pair<std::vector<std::vector<bool>>, 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<std::vector<bool>>& 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<std::pair<std::filesystem::path, std::filesystem::path>> getInstallationFiles(
const std::vector<std::vector<bool>>& 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<std::string, std::string> 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<File> files_;
/*! \brief Steps performed during installation. */
std::vector<InstallStep> steps_;
/*! \brief Current installation step. */
int cur_step_ = -1;
/*! \brief Maps flags to their value. */
std::map<std::string, std::string> 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<std::vector<std::vector<bool>>> prev_selections_;
/*! \brief Used to evaluate game version conditions. */
std::function<bool(std::string)> version_eval_fun_ = [](auto s) { return true; };
/*! \brief Used to evaluate fomm version conditions. */
std::function<bool(std::string)> 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<File>& 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<std::vector<bool>>& 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<std::string, std::string> 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<typename T>
void sortVector(std::vector<T>& 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; });
}
};
}

View File

@@ -0,0 +1,29 @@
/*!
* \file installstep.h
* \brief Header for the InstallStep struct.
*/
#pragma once
#include "dependency.h"
#include "plugingroup.h"
#include <vector>
/*!
* \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<PluginGroup> groups;
};
}

67
src/core/fomod/plugin.h Normal file
View File

@@ -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 <filesystem>
#include <map>
#include <vector>
/*!
* \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<PluginDependency> potential_types;
/*! \brief Flags to be set when this is selected. */
std::map<std::string, std::string> flags;
/*! \brief Files to be installed when this is selected. */
std::vector<File> 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<std::string, std::string>& current_flags,
std::function<bool(std::string)> version_eval_fun,
std::function<bool(std::string)> 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;
}
};
}

View File

@@ -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;
};
}

View File

@@ -0,0 +1,44 @@
/*!
* \file plugingroup.h
* \brief Header for the PluginGroup struct.
*/
#pragma once
#include "plugin.h"
#include <string>
#include <vector>
/*!
* \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<Plugin> plugins;
};
}

View File

@@ -0,0 +1,38 @@
/*!
* \file plugintype.h
* \brief Header for the PluginType enum.
*/
#pragma once
#include <string>
#include <vector>
/*!
* \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<std::string> PLUGIN_TYPE_NAMES{ "Required",
"Optional",
"Recommended",
"Not Available",
"Could be usable" };
}

57
src/core/importmodinfo.h Normal file
View File

@@ -0,0 +1,57 @@
/*!
* \file importmodinfo.h
* \brief Contains the ImportModInfo struct.
*/
#pragma once
#include <chrono>
#include <filesystem>
#include <string>
/*!
* \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<std::chrono::high_resolution_clock> 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;
}
};

431
src/core/installer.cpp Normal file
View File

@@ -0,0 +1,431 @@
#include "installer.h"
#include "compressionerror.h"
#include "pathutils.h"
#include <archive.h>
#include <archive_entry.h>
#include <filesystem>
#include <iostream>
#include <ranges>
#include <regex>
namespace sfs = std::filesystem;
namespace pu = path_utils;
void Installer::extract(const sfs::path& source_path,
const sfs::path& dest_path,
std::optional<ProgressNode*> 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<std::pair<sfs::path, sfs::path>> 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<unsigned>::max());
if(tmp_id == std::numeric_limits<unsigned>::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<sfs::path> 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<sfs::path> Installer::getArchiveFileNames(const sfs::path& path)
{
std::vector<sfs::path> 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<int, std::string, std::string> 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<ProgressNode*> 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<char, 128> 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.");
}

176
src/core/installer.h Normal file
View File

@@ -0,0 +1,176 @@
/*!
* \file installer.h
* \brief Header for the Installer class
*/
#pragma once
#include "progressnode.h"
#include <filesystem>
#include <functional>
#include <map>
#include <optional>
#include <vector>
/*!
* \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<std::vector<Flag>> OPTION_GROUPS{
{ preserve_case, lower_case, upper_case },
{ preserve_directories, single_directory }
};
/*! \brief Maps installer flags to descriptive names. */
inline static const std::map<Flag, std::string> 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<Flag, std::string> 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<std::string> 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<ProgressNode*> 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<std::pair<std::filesystem::path, std::filesystem::path>> 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<std::filesystem::path> 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<int, std::string, std::string> 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<ProgressNode*> 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);
};

65
src/core/log.cpp Normal file
View File

@@ -0,0 +1,65 @@
#include "log.h"
#include <chrono>
#include <iomanip>
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<std::chrono::milliseconds>(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;
}
}
}

61
src/core/log.h Normal file
View File

@@ -0,0 +1,61 @@
/*!
* \file log.h
* \brief Header for the Log namespace
*/
#pragma once
#include <functional>
#include <iostream>
#include <string>
/*!
* \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<void(std::string, LogLevel)> 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);
}

650
src/core/lootdeployer.cpp Normal file
View File

@@ -0,0 +1,650 @@
#include "lootdeployer.h"
#include "pathutils.h"
#include <chrono>
#include <cpr/cpr.h>
#include <fstream>
#include <iostream>
#include <numeric>
#include <ranges>
#include <regex>
#include <set>
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<int, unsigned long> LootDeployer::deploy(std::optional<ProgressNode*> progress_node)
{
log_(Log::LOG_INFO, std::format("Deployer '{}': Updating plugins...", name_));
updatePlugins();
updatePluginTags();
return {};
}
std::map<int, unsigned long> LootDeployer::deploy(const std::vector<int>& loadorder,
std::optional<ProgressNode*> 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<std::vector<int>> LootDeployer::getConflictGroups() const
{
std::vector<int> group(plugins_.size());
std::iota(group.begin(), group.end(), 0);
return { group };
}
std::vector<std::string> LootDeployer::getModNames() const
{
std::vector<std::string> 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<std::vector<int>>& 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<std::tuple<int, bool>> LootDeployer::getLoadorder() const
{
std::vector<std::tuple<int, bool>> 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<ConflictInfo> LootDeployer::getFileConflicts(
int mod_id,
bool show_disabled,
std::optional<ProgressNode*> progress_node) const
{
if(progress_node)
{
(*progress_node)->setTotalSteps(1);
(*progress_node)->advance();
}
return {};
}
std::unordered_set<int> LootDeployer::getModConflicts(int mod_id,
std::optional<ProgressNode*> progress_node)
{
std::unordered_set<int> conflicts{ mod_id };
auto loot_handle = loot::CreateGameHandle(app_type_, source_path_, dest_path_);
std::vector<sfs::path> 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<ProgressNode*> 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<sfs::path> 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<std::pair<std::string, bool>> new_plugins;
std::set<std::string> 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<std::string>(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<std::vector<std::string>> LootDeployer::getAutoTags()
{
return tags_;
}
std::map<std::string, int> LootDeployer::getAutoTagMap()
{
return { { LIGHT_PLUGIN, num_light_plugins_ },
{ MASTER_PLUGIN, num_master_plugins_ },
{ STANDARD_PLUGIN, num_standard_plugins_ } };
}
void LootDeployer::updatePlugins()
{
std::vector<std::string> plugin_files;
std::vector<std::pair<std::string, bool>> 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<std::chrono::system_clock> 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<std::chrono::seconds>(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<sfs::path> 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();
}

276
src/core/lootdeployer.h Normal file
View File

@@ -0,0 +1,276 @@
/*!
* \file lootdeployer.h
* \brief Header for the LootDeployer class
*/
#pragma once
#include "deployer.h"
#include "loot/api.h"
#include <json/json.h>
/*!
* \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<loot::GameType, std::string> 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<loot::GameType, std::string> 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<int, unsigned long> deploy(std::optional<ProgressNode*> 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<int, unsigned long> deploy(const std::vector<int>& loadorder,
std::optional<ProgressNode*> 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<std::vector<int>> getConflictGroups() const override;
/*!
* \brief Generates a vector of names for every plugin.
* \return The name vector.
*/
std::vector<std::string> 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<std::vector<int>>& 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<std::tuple<int, bool>> 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<ConflictInfo> getFileConflicts(
int mod_id,
bool show_disabled = false,
std::optional<ProgressNode*> 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<int> getModConflicts(int mod_id,
std::optional<ProgressNode*> 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<ProgressNode*> 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<std::vector<std::string>> getAutoTags() override;
/*!
* \brief Returns all available auto tag names.
* \return The tag names.
*/
virtual std::map<std::string, int> 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<loot::GameType, std::filesystem::path> 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<std::pair<std::string, bool>> plugins_;
/*! \brief Contains names of plugins which are in loadorder.txt but not in plugins.txt. */
std::vector<std::string> 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<std::vector<std::string>> 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();
};

61
src/core/manualtag.cpp Normal file
View File

@@ -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<int> 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_;
}

65
src/core/manualtag.h Normal file
View File

@@ -0,0 +1,65 @@
/*!
* \file manualtag.h
* \brief Header for the ManualTag class.
*/
#pragma once
#include "tag.h"
#include <json/json.h>
#include <string>
#include <vector>
/*!
* \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<int> 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;
};

53
src/core/mod.cpp Normal file
View File

@@ -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;
}

78
src/core/mod.h Normal file
View File

@@ -0,0 +1,78 @@
/*!
* \file mod.h
* \brief Contains the Mod struct.
*/
#pragma once
#include <filesystem>
#include <json/json.h>
#include <string>
/*!
* \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;
};

View File

File diff suppressed because it is too large Load Diff

View File

@@ -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 <filesystem>
#include <json/json.h>
#include <string>
#include <vector>
/*!
* \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<int>& 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<int>& 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<ProgressNode*> 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<ProgressNode*> 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<std::string> getDeployerNames() const;
/*!
* \brief Creates a vector containing information about all installed mods, stored in ModInfo
* objects.
* \return The vector.
*/
std::vector<ModInfo> getModInfo() const;
/*!
* \brief Getter for the current mod load order of one Deployer.
* \param deployer The target Deployer.
* \return The load order.
*/
std::vector<std::tuple<int, bool>> 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<ConflictInfo> 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<std::tuple<std::string, std::string>>& 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<int> 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<std::string> 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<int, std::string> 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<ProgressNode*> 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<ProgressNode*> 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<ProgressNode*> 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<ProgressNode*> 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<std::vector<int>> 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<int>& mod_ids, const std::vector<bool>& 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<void(Log::LogLevel, const std::string&)>& 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<std::string>& 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<BackupTarget> 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<void(float)>& 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<int>& 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<std::string>& tag_names, const std::vector<int>& 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<std::string>& tag_names,
const std::vector<int>& 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<std::string>& tag_names, const std::vector<int> mod_ids);
/*!
* \brief Performs the given editing actions on the manual tags.
* \param actions Editing actions.
*/
void editManualTags(const std::vector<EditManualTagAction>& 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<TagCondition>& 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<TagCondition>& conditions,
bool update);
/*!
* \brief Performs the given editing actions on the auto tags.
* \param actions Editing actions.
*/
void editAutoTags(const std::vector<EditAutoTagAction>& 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<int> 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<int>& 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<int>& 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<void(float)> 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<Mod> installed_mods_;
/*! \brief Contains every Deployer used by this application. */
std::vector<std::unique_ptr<Deployer>> deployers_;
/*! \brief Contains names and commands for every tool. */
std::vector<std::tuple<std::string, std::string>> 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<std::string> profile_names_;
/*! \brief For every group: A vector containing every mod in that group. */
std::vector<std::vector<int>> groups_;
/*! \brief Maps mods to their groups. */
std::map<int, int> group_map_;
/*! \brief Contains the active member of every group. */
std::vector<int> active_group_members_;
/*! \brief Maps mods to the installer used during their installation. */
std::map<int, std::string> installer_map_;
/*! \brief Path to this applications icon. */
std::filesystem::path icon_path_;
/*! \brief Callback for logging. */
std::function<void(Log::LogLevel, const std::string&)> 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<ManualTag> manual_tags_;
/*! \brief Maps mod ids to a vector of manual tags associated with that mod. */
std::map<int, std::vector<std::string>> manual_tag_map_;
/*! \brief Contains all known auto tags. */
std::vector<AutoTag> auto_tags_;
/*! \brief Maps mod ids to a vector of auto tags associated with that mod. */
std::map<int, std::vector<std::string>> 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<std::string> app_versions_;
/*! \brief Callback used to inform about the current task's progress. */
std::function<void(float)> 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<ProgressNode*> 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<int>& target_mod_indices);
};

84
src/core/modinfo.h Normal file
View File

@@ -0,0 +1,84 @@
/*!
* \file modinfo.h
* \brief Contains the ModInfo struct.
*/
#pragma once
#include "mod.h"
#include <filesystem>
#include <string>
/*!
* \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<std::string> deployers;
/*! \brief Ids of all \ref Deployer "deployers" the mod belongs to. */
std::vector<int> deployer_ids;
/*! \brief The mods activation status for every \ref Deployer "deployer" it belongs to. */
std::vector<bool> 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<std::string> manual_tags;
/*! \brief Contains the names of all auto tags added to this mod. */
std::vector<std::string> 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<std::string>& deployer_names,
const std::vector<int>& deployer_ids,
const std::vector<bool>& statuses,
int group,
bool is_active_member,
const std::vector<std::string>& man_tags,
const std::vector<std::string>& 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)
{}
};

334
src/core/nexus/api.cpp Normal file
View File

@@ -0,0 +1,334 @@
#include "api.h"
#include "../parseerror.h"
#include <json/json.h>
#include <ranges>
#include <regex>
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<Mod> 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<Mod> 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<File> 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<File> 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<std::pair<std::string, std::vector<std::string>>> Api::getChangelogs(
const std::string& mod_url)
{
std::vector<std::pair<std::string, std::vector<std::string>>> 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<std::string> 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<int> a_parts;
std::vector<int> 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<std::pair<std::string, bool>> 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<std::pair<std::string, int>> 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 {};
}

152
src/core/nexus/api.h Normal file
View File

@@ -0,0 +1,152 @@
/*!
* \file Api.h
* \brief Header for the nexus::Api class.
*/
#pragma once
#include "file.h"
#include "mod.h"
#include <cpr/cpr.h>
#include <string>
/*!
* \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<std::pair<std::string, std::vector<std::string>>> changelog;
/*! \brief Contains data on all available files for the mod. */
std::vector<File> 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<Mod> 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<File> 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<std::pair<std::string, std::vector<std::string>>> 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<std::pair<std::string, bool>> 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<std::pair<std::string, int>> extractDomainAndModId(
const std::string& mod_url);
};
}

44
src/core/nexus/file.cpp Normal file
View File

@@ -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();
}

86
src/core/nexus/file.h Normal file
View File

@@ -0,0 +1,86 @@
/*!
* \file file.h
* \brief Header for the nexus::File class.
*/
#pragma once
#include <chrono>
#include <json/json.h>
#include <string>
/*!
* \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);
};
}

52
src/core/nexus/mod.cpp Normal file
View File

@@ -0,0 +1,52 @@
#include "mod.h"
#include "../parseerror.h"
#include <json/json.h>
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();
}

100
src/core/nexus/mod.h Normal file
View File

@@ -0,0 +1,100 @@
/*!
* \file mod.h
* \brief Header for the nexus::Mod class.
*/
#pragma once
#include <chrono>
#include <json/json.h>
#include <string>
/*!
* \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);
};
}

27
src/core/parseerror.h Normal file
View File

@@ -0,0 +1,27 @@
/*!
* \file parseerror.h
* \brief Contains the ParseError class.
*/
#pragma once
#include <stdexcept>
/*!
* \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) {}
};

201
src/core/pathutils.cpp Normal file
View File

@@ -0,0 +1,201 @@
#include "pathutils.h"
#include <algorithm>
#include <regex>
#include <set>
namespace sfs = std::filesystem;
namespace path_utils
{
std::optional<sfs::path> 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<sfs::path, sfs::path> 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<unsigned char(unsigned char)> converter)
{
std::vector<sfs::path> 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<std::pair<sfs::path, sfs::path>> 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));
}
}

110
src/core/pathutils.h Normal file
View File

@@ -0,0 +1,110 @@
/*!
* \file pathutils.h
* \brief Header for the path_utils namespace.
*/
#pragma once
#include <filesystem>
#include <functional>
#include <optional>
/*!
* \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<std::filesystem::path> 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<std::filesystem::path, std::filesystem::path> 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<unsigned char(unsigned char)> 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);
}

107
src/core/progressnode.cpp Normal file
View File

@@ -0,0 +1,107 @@
#include "progressnode.h"
#include <limits>
#include <numeric>
ProgressNode::ProgressNode(int id,
const std::vector<float>& weights,
std::optional<ProgressNode*> parent) : id_(id), parent_(parent)
{
addChildren(weights);
}
ProgressNode::ProgressNode(std::function<void(float)> progress_callback,
const std::vector<float>& 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<float>(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<float>& 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<void(float)> 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<float>::epsilon() &&
std::abs(1.0f - prev_progress_) > std::numeric_limits<float>::epsilon())
{
set_progress_(progress_);
prev_progress_ = progress_;
}
}

131
src/core/progressnode.h Normal file
View File

@@ -0,0 +1,131 @@
/*!
* \file progressnode.h
* \brief Header for the ProgressNode class.
*/
#pragma once
#include <functional>
#include <memory>
#include <optional>
#include <vector>
/*!
* \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<float>& weights, std::optional<ProgressNode*> 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<void(float)> progress_callback,
const std::vector<float>& 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<float>& 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<void(float)> 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<ProgressNode*> parent_;
/*! \brief Weights of children. */
std::vector<float> weights_;
/*! \brief Children representing sub-tasks of this task. */
std::vector<ProgressNode> children_;
/*!
* \brief Callback function used by the root node to inform about changes in the
* task progress.
*/
std::function<void(float)> 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();
};

29
src/core/tag.cpp Normal file
View File

@@ -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<int> 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();
}

57
src/core/tag.h Normal file
View File

@@ -0,0 +1,57 @@
/*!
* \file tag.h
* \brief Header for the Tag class.
*/
#pragma once
#include <json/json.h>
#include <string>
#include <vector>
/*!
* \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<int> 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<int> mods_{};
};

34
src/core/tagcondition.h Normal file
View File

@@ -0,0 +1,34 @@
/*!
* \file tagcondition.h
* \brief Contains the TagCondition struct.
*/
#pragma once
#include <string>
/*!
* \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;
};

View File

@@ -0,0 +1,442 @@
#include "tagconditionnode.h"
#include <algorithm>
#include <format>
#include <ranges>
#include <regex>
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<TagCondition>& 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<std::pair<std::string, std::string>>& files) const
{
if(type_ == Type::empty)
return false;
std::map<int, bool> results;
return evaluateOnce(files, results);
}
bool TagConditionNode::evaluateOnce(const std::vector<std::pair<std::string, std::string>>& files,
std::map<int, bool>& results) const
{
return invert_ ? !evaluateWithoutInversion(files, results)
: evaluateWithoutInversion(files, results);
}
bool TagConditionNode::evaluateWithoutInversion(
const std::vector<std::pair<std::string, std::string>>& files,
std::map<int, bool>& 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<std::pair<int, int>> 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<std::pair<int, int>> 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<std::string> TagConditionNode::splitString(const std::string& input) const
{
std::vector<std::string> 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<int> token_types;
std::vector<std::pair<int, int>> 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);
}

156
src/core/tagconditionnode.h Normal file
View File

@@ -0,0 +1,156 @@
/*!
* \file tagconditionnode.h
* \brief Header for the TagConditionNode class.
*/
#pragma once
#include "tagcondition.h"
#include <filesystem>
#include <map>
#include <vector>
/*!
* \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<TagCondition>& 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<std::pair<std::string, std::string>>& 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<TagConditionNode> 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<std::string> 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<std::pair<std::string, std::string>>& files,
std::map<int, bool>& 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<std::pair<std::string, std::string>>& files,
std::map<int, bool>& 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<std::pair<int, int>> 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<std::string> 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);
};

27
src/cspell.json Normal file
View File

@@ -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
}

2737
src/lmm_Doxyfile Normal file
View File

File diff suppressed because it is too large Load Diff

127
src/main.cpp Normal file
View File

@@ -0,0 +1,127 @@
/**
* \file main.cpp
* \brief Contains the main function
*/
#include "ui/ipcclient.h"
#include "ui/mainwindow.h"
#include <QApplication>
#include <filesystem>
#include <iostream>
/*!
* \brief Main function of Limo.
* \param argc Number of arguments passed to the application.
* \param argv Array of arguments passed to the application.
* \return 0: Application exited normally. 1: An error occurred while parsing arguments.
* 2: Execution canceled, another Limo instance is already running.
*/
int main(int argc, char* argv[])
{
QCoreApplication::setApplicationName("Limo");
QApplication app(argc, argv);
QIcon::setFallbackSearchPaths(
QIcon::fallbackSearchPaths()
<< (std::filesystem::path(__FILE__).parent_path().parent_path() / "resources").c_str());
QCommandLineParser parser;
parser.setApplicationDescription("A simple tool for managing mods.");
parser.addHelpOption();
QCommandLineOption list_option(QStringList() << "l" << "list",
"List all applications and their profiles.");
QCommandLineOption deploy_option(QStringList() << "d" << "deploy",
"Deploy all mods for given <application>. Requires setting "
"a profile",
"application");
QCommandLineOption profile_option(
QStringList() << "p" << "profile", "Set a <profile> to use for deployment.", "profile");
QCommandLineOption debug_option(QStringList() << "D" << "debug" << "Show debug log messages.");
parser.addOption(list_option);
parser.addOption(deploy_option);
parser.addOption(profile_option);
parser.addOption(debug_option);
parser.addPositionalArgument("url", "Imports the mod at this URL.");
parser.process(app);
const bool debug_mode = parser.isSet(debug_option);
if(parser.isSet(list_option))
{
ApplicationManager app_man;
app_man.enableExceptions(true);
app_man.init();
std::cout << app_man.toString();
return 0;
}
if(parser.isSet(deploy_option))
{
bool is_int;
QString input = parser.value(deploy_option);
auto app_id = input.toInt(&is_int);
if(!is_int)
{
std::cout << "Error: Specify the application id, '" << input.toStdString()
<< "' is not a number." << std::endl;
return 1;
}
if(!parser.isSet(profile_option))
{
std::cout << "Error: Missing profile id." << std::endl;
return 1;
}
input = parser.value(profile_option);
int profile_id = input.toInt(&is_int);
if(!is_int)
{
std::cout << "Error: Specify the profile id, '" << input.toStdString() << "' is not a number."
<< std::endl;
return 1;
}
ApplicationManager app_man;
app_man.enableExceptions(true);
app_man.init();
if(app_id < 0 || app_id >= app_man.getNumApplications())
{
std::cout << "Error: Application index out of bounds." << std::endl;
return 1;
}
if(profile_id < 0 || profile_id >= app_man.getNumProfiles(app_id))
{
std::cout << "Error: Profile index out of bounds." << std::endl;
return 1;
}
app_man.setProfile(app_id, profile_id);
app_man.deployMods(app_id);
return 0;
}
const auto pos_args = parser.positionalArguments();
std::string argument = "";
if(!pos_args.empty())
argument = pos_args[0].toStdString();
if(argument.starts_with('\"'))
argument.erase(0, 1);
if(argument.ends_with('\"'))
argument.erase(argument.size() - 1, 1);
IpcClient client;
if(client.connect())
{
if(pos_args.empty())
{
client.sendString("Started");
return 2;
}
std::regex nxm_regex(R"(nxm:\/\/.*\mods\/\d+\/files\/\d+\?.*)");
std::smatch match;
if(std::regex_match(argument, match, nxm_regex))
client.sendString(argument);
return 0;
}
app.setWindowIcon(QIcon(":/logo.png"));
MainWindow w;
w.setDebugMode(debug_mode);
if(!pos_args.empty())
w.setCmdArgument(argument);
emit w.getApplicationNames(false);
w.show();
return app.exec();
}

View File

@@ -0,0 +1,52 @@
#include "addapikeydialog.h"
#include "ui_addapikeydialog.h"
#include <QPushButton>
AddApiKeyDialog::AddApiKeyDialog(QWidget* parent) : QDialog(parent), ui(new Ui::AddApiKeyDialog)
{
ui->setupUi(this);
ui->pw_field->setPartnerField(ui->pw_repeat_field, PasswordField::repeat);
connect(ui->pw_repeat_field,
&PasswordField::passwordValidityChanged,
this,
&AddApiKeyDialog::onPasswordValidityChanged);
}
AddApiKeyDialog::~AddApiKeyDialog()
{
delete ui;
}
QString AddApiKeyDialog::getApiKey() const
{
return ui->key_field->text();
}
QString AddApiKeyDialog::getPassword() const
{
return ui->pw_field->getPassword();
}
void AddApiKeyDialog::on_buttonBox_rejected()
{
if(dialog_completed_)
return;
dialog_completed_ = true;
reject();
}
void AddApiKeyDialog::on_buttonBox_accepted()
{
if(dialog_completed_)
return;
dialog_completed_ = true;
accept();
}
void AddApiKeyDialog::onPasswordValidityChanged(bool is_valid)
{
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(is_valid);
}

59
src/ui/addapikeydialog.h Normal file
View File

@@ -0,0 +1,59 @@
/*!
* \file addapikeydialog.h
* \brief Header for the AddApiKeyDialog class.
*/
#pragma once
#include <QDialog>
namespace Ui
{
class AddApiKeyDialog;
}
/*!
* \brief Dialog used to adding a new NexusMods API key and setting an encryption password.
*/
class AddApiKeyDialog : public QDialog
{
Q_OBJECT
public:
/*!
* \brief Initializes the UI.
* \param parent Parent for this widget, this is passed to the constructor of QDialog.
*/
explicit AddApiKeyDialog(QWidget* parent = nullptr);
/*! \brief Deletes the UI. */
~AddApiKeyDialog();
/*!
* \brief Returns the API key entered in the dialog.
* \return The API key.
*/
QString getApiKey() const;
/*!
* \brief Returns the password entered in the dialog.
* \return The password.
*/
QString getPassword() const;
private slots:
/*! \brief Closes the dialog */
void on_buttonBox_rejected();
/*! \brief Closes the dialog */
void on_buttonBox_accepted();
/*!
* \brief Disables/ enables the OK button, depending on if the entered passwords match.
* \param is_valid True if both passwords match.
*/
void onPasswordValidityChanged(bool is_valid);
private:
/*! \brief Contains auto-generated UI elements. */
Ui::AddApiKeyDialog* ui;
/*! \brief Indicates whether the dialog has been completed. */
bool dialog_completed_ = false;
};

75
src/ui/addapikeydialog.ui Normal file
View File

@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AddApiKeyDialog</class>
<widget class="QDialog" name="AddApiKeyDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>284</height>
</rect>
</property>
<property name="windowTitle">
<string>App Nexus API Key</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Enter a NexusMods API key:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="key_field"/>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Chose a password for key encryption. If you leave this empty the key will be encrypted using a default password and you will not be prompted to enter a password when accessing the API. Not chosing a password might allow someone with access to this device to decrypt your API key.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="PasswordField" name="pw_field" native="true"/>
</item>
<item>
<widget class="PasswordField" name="pw_repeat_field" native="true"/>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>5</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>PasswordField</class>
<extends>QWidget</extends>
<header>ui/passwordfield.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

217
src/ui/addappdialog.cpp Normal file
View File

@@ -0,0 +1,217 @@
#include "addappdialog.h"
#include "importfromsteamdialog.h"
#include "ui_addappdialog.h"
#include <QDebug>
#include <QDir>
#include <QFileDialog>
#include <QMessageBox>
#include <QStandardPaths>
#include <filesystem>
namespace sfs = std::filesystem;
AddAppDialog::AddAppDialog(QWidget* parent) : QDialog(parent), ui(new Ui::AddAppDialog)
{
ui->setupUi(this);
ui->move_dir_box->setVisible(false);
ui->import_checkbox->setVisible(false);
enableOkButton(false);
ui->path_field->setValidationMode(ValidatingLineEdit::VALID_PATH_EXISTS);
dialog_completed_ = false;
}
AddAppDialog::~AddAppDialog()
{
delete ui;
}
void AddAppDialog::on_file_picker_button_clicked()
{
QString starting_dir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
if(pathIsValid())
starting_dir = ui->path_field->text();
auto dialog = new QFileDialog;
dialog->setWindowTitle("Select Staging Directory");
dialog->setFilter(QDir::AllDirs | QDir::Hidden);
dialog->setFileMode(QFileDialog::Directory);
dialog->setDirectory(starting_dir);
connect(dialog, &QFileDialog::fileSelected, this, &AddAppDialog::onFileDialogAccepted);
dialog->exec();
}
void AddAppDialog::on_name_field_textChanged(const QString& text)
{
if(text.isEmpty())
enableOkButton(false);
else if(pathIsValid())
enableOkButton(true);
}
void AddAppDialog::on_path_field_textChanged(const QString& text)
{
if(!pathIsValid())
enableOkButton(false);
else if(!ui->name_field->text().isEmpty())
enableOkButton(true);
}
void AddAppDialog::enableOkButton(bool state)
{
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(state);
}
bool AddAppDialog::pathIsValid()
{
QString path = ui->path_field->text();
if(path.isEmpty())
return false;
return std::filesystem::exists(path.toStdString());
}
bool AddAppDialog::iconIsValid(const QString& path)
{
QString icon_path = path.isEmpty() ? ui->icon_field->text() : path;
return QIcon(icon_path).availableSizes().size() > 0;
}
void AddAppDialog::setEditMode(const QString& name,
const QString& app_version,
const QString& path,
const QString& command,
const QString& icon_path,
int app_id)
{
steam_prefix_path_ = "";
steam_install_path_ = "";
ui->import_checkbox->setVisible(false);
ui->import_button->setEnabled(false);
ui->import_button->setHidden(true);
ui->move_dir_box->setCheckState(Qt::Unchecked);
name_ = name;
path_ = path;
command_ = command;
app_id_ = app_id;
enableOkButton(true);
edit_mode_ = true;
ui->move_dir_box->setVisible(true);
setWindowTitle("Edit " + name_);
ui->name_field->setText(name);
ui->version_field->setText(app_version);
ui->icon_field->setText(icon_path);
if(iconIsValid(icon_path))
ui->icon_picker_button->setIcon(QIcon(icon_path));
else
ui->icon_picker_button->setIcon(QIcon::fromTheme("folder-open"));
ui->path_field->setText(path);
ui->command_field->setText(command);
dialog_completed_ = false;
}
void AddAppDialog::setAddMode()
{
steam_prefix_path_ = "";
steam_install_path_ = "";
ui->import_checkbox->setVisible(false);
ui->import_button->setEnabled(true);
ui->import_button->setHidden(false);
setWindowTitle("New Application");
ui->name_field->setText("");
ui->version_field->setText("");
ui->icon_field->setText("");
ui->icon_picker_button->setIcon(QIcon::fromTheme("folder-open"));
ui->path_field->setText("");
ui->command_field->setText("");
enableOkButton(false);
edit_mode_ = false;
ui->move_dir_box->setVisible(false);
dialog_completed_ = false;
}
void AddAppDialog::on_buttonBox_accepted()
{
if(dialog_completed_)
return;
dialog_completed_ = true;
EditApplicationInfo info;
info.name = ui->name_field->text().toStdString();
info.app_version = ui->version_field->text().toStdString();
info.staging_dir = ui->path_field->text().toStdString();
info.command = ui->command_field->text().toStdString();
info.icon_path = ui->icon_field->text().toStdString();
if(edit_mode_)
{
info.move_staging_dir = ui->move_dir_box->checkState() == Qt::Checked;
emit applicationEdited(info, app_id_);
}
else
{
info.deployers = std::vector<std::pair<std::string, std::string>>{};
if(ui->import_checkbox->isChecked())
{
if(steam_install_path_ != "")
info.deployers.push_back({ "Install", steam_install_path_.toStdString() });
if(steam_prefix_path_ != "")
info.deployers.push_back({ "Prefix", steam_prefix_path_.toStdString() });
}
emit applicationAdded(info);
}
}
void AddAppDialog::on_import_button_clicked()
{
auto dialog = new ImportFromSteamDialog(this);
connect(dialog,
&ImportFromSteamDialog::applicationImported,
this,
&AddAppDialog::onApplicationImported);
dialog->exec();
}
void AddAppDialog::onApplicationImported(QString name,
QString app_id,
QString install_dir,
QString prefix_path,
QString icon_path)
{
ui->name_field->setText(name);
ui->command_field->setText("steam -applaunch " + app_id);
ui->import_checkbox->setVisible(true);
steam_install_path_ = install_dir;
steam_prefix_path_ = prefix_path;
ui->icon_field->setText(icon_path);
ui->icon_picker_button->setIcon(QIcon(icon_path));
}
void AddAppDialog::onFileDialogAccepted(const QString& path)
{
if(!path.isEmpty())
ui->path_field->setText(path);
}
void AddAppDialog::on_icon_picker_button_clicked()
{
QString starting_dir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
QString path = ui->icon_field->text();
if(!path.isEmpty() && std::filesystem::exists(path.toStdString()))
starting_dir = std::filesystem::path(path.toStdString()).parent_path().string().c_str();
auto dialog = new QFileDialog;
dialog->setWindowTitle("Select Icon");
dialog->setFilter(QDir::AllDirs | QDir::Hidden);
dialog->setDirectory(starting_dir);
connect(dialog, &QFileDialog::fileSelected, this, &AddAppDialog::onIconPathDialogComplete);
dialog->exec();
}
void AddAppDialog::onIconPathDialogComplete(const QString& path)
{
if(!iconIsValid(path))
{
QMessageBox* error_box =
new QMessageBox(QMessageBox::Critical, "Error", "Invalid icon!", QMessageBox::Ok);
error_box->exec();
return;
}
ui->icon_field->setText(path);
ui->icon_picker_button->setIcon(QIcon(path));
}

139
src/ui/addappdialog.h Normal file
View File

@@ -0,0 +1,139 @@
/*!
* \file addappdialog.h
* \brief Header for the AddAppDialog class.
*/
#pragma once
#include "../core/editapplicationinfo.h"
#include <QDialog>
namespace Ui
{
class AddAppDialog;
}
/*!
* \brief Dialog for creating and editing \ref ModdedApplication "applications".
*/
class AddAppDialog : public QDialog
{
Q_OBJECT
public:
/*!
* \brief Initializes the UI.
* \param parent Parent for this widget, this is passed to the constructor of QDialog.
*/
explicit AddAppDialog(QWidget* parent = nullptr);
/*! \brief Deletes the UI. */
~AddAppDialog();
private:
/*! \brief Contains auto-generated UI elements. */
Ui::AddAppDialog* ui;
/*! \brief If true: Dialog is used to edit, else: Dialog is used to create. */
bool edit_mode_ = false;
/*! \brief Current name of the edited \ref ModdedApplication "application". */
QString name_;
/*! \brief Current staging directory path of the edited \ref ModdedApplication "application". */
QString path_;
/*! \brief Current command to run the edited \ref ModdedApplication "application". */
QString command_;
/*! \brief Id of the edited \ref ModdedApplication "application". */
int app_id_;
/*! \brief Path to imported steam applications installation directory. */
QString steam_install_path_ = "";
/*! \brief Path to imported steam applications prefix directory. */
QString steam_prefix_path_ = "";
/*! \brief Indicates whether the dialog has been completed. */
bool dialog_completed_ = false;
/*!
* \brief Set the enabled state of this dialogs OK button.
* \param state
*/
void enableOkButton(bool state);
/*! \brief Checks whether the currently entered path exists. */
bool pathIsValid();
/*!
* \brief Checks whether the currently entered icon path refers to a valid icon file.
* \param Path to an icon. If this checked instead of ui->icon_field if this is not empty.
*/
bool iconIsValid(const QString& path = "");
public:
/*!
* \brief Initializes this dialog to allow editing of an existing
* \ref ModdedApplication "application".
* \param name Current name of the edited \ref ModdedApplication "application".
* \param app_version Current app app_version.
* \param path Current staging directory path of the edited
* \ref ModdedApplication "application".
* \param command Current command to run the edited \ref ModdedApplication "application".
* \param app_id Id of the edited \ref ModdedApplication "application".
*/
void setEditMode(const QString& name,
const QString& app_version,
const QString& path,
const QString& command,
const QString& icon_path,
int app_id);
/*!
* \brief Initializes this dialog to allow creating a new
* \ref ModdedApplication "application".
*/
void setAddMode();
private slots:
/*! \brief Shows a file dialog for the staging directory path. */
void on_file_picker_button_clicked();
/*! \brief Only enable the OK button if a name has been entered. */
void on_name_field_textChanged(const QString& text);
/*! \brief Only enable the OK button if a valid staging directory path has been entered. */
void on_path_field_textChanged(const QString& text);
/*! \brief Closes the dialog and emits a signal for completion. */
void on_buttonBox_accepted();
/*! \brief Opens a dialog to import currently installed steam app. */
void on_import_button_clicked();
/*!
* \brief Called when the import steam application dialog has been completed.
* \param name Name of the imported application.
* \param app_id Steam app_id of the imported application.
* \param install_dir Name of the directory under steamapps which contains the
* new applications files.
* \param prefix_path Path to the applications Proton prefix, or empty if none exists.
* \param icon_path Path to the applications icon.
*/
void onApplicationImported(QString name,
QString app_id,
QString install_dir,
QString prefix_path,
QString icon_path);
/*!
* \brief Updates the staging directory path to given path.
* \param path The new path.
*/
void onFileDialogAccepted(const QString& path);
/*! \brief Called when icon path picker button is clicked. */
void on_icon_picker_button_clicked();
/*!
* \brief Updates the icon path to the given path if the given path refers to a valid icon.
* \param path The new path.
*/
void onIconPathDialogComplete(const QString& path);
signals:
/*!
* \brief Signals completion of the dialog in add mode.
* \param info Contains all data entered in the dialog.
*/
void applicationAdded(EditApplicationInfo edit_app_info);
/*!
* \brief Signals completion of the dialog in edit mode.
* \param info Contains all data entered in the dialog.
* \param app_id Id of the edited \ref ModdedApplication "application".
*/
void applicationEdited(EditApplicationInfo edit_app_info, int app_id);
};

231
src/ui/addappdialog.ui Normal file
View File

@@ -0,0 +1,231 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AddAppDialog</class>
<widget class="QDialog" name="AddAppDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>525</width>
<height>260</height>
</rect>
</property>
<property name="windowTitle">
<string>Add Application</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="2">
<widget class="QPushButton" name="icon_picker_button">
<property name="toolTip">
<string>Pick staging directory</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="folder-open">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="toolTip">
<string>Application name</string>
</property>
<property name="text">
<string>Name:</string>
</property>
</widget>
</item>
<item row="3" column="0" rowspan="2">
<widget class="QLabel" name="label_2">
<property name="toolTip">
<string>All installed mods are stored here. For hard link deployment, this has to be on the same partition as the application</string>
</property>
<property name="text">
<string>Staging directory:</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Icon path</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_4">
<property name="toolTip">
<string>Command used to run the application</string>
</property>
<property name="text">
<string>Command:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="ValidatingLineEdit" name="path_field"/>
</item>
<item row="0" column="1">
<widget class="ValidatingLineEdit" name="name_field"/>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="icon_field"/>
</item>
<item row="3" column="2" rowspan="2">
<widget class="QPushButton" name="file_picker_button">
<property name="toolTip">
<string>Pick staging directory</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="folder-open">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLineEdit" name="command_field"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="toolTip">
<string>This will be used for FOMOD conditions</string>
</property>
<property name="text">
<string>Version:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="version_field"/>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="move_dir_box">
<property name="enabled">
<bool>true</bool>
</property>
<property name="toolTip">
<string>Move old staging directory to new location</string>
</property>
<property name="text">
<string>Move staging directory</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="import_checkbox">
<property name="toolTip">
<string>Create Deployers targeting the apps installation and prefix directories</string>
</property>
<property name="text">
<string>Import deployers</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="import_button">
<property name="toolTip">
<string>Import an application from steam</string>
</property>
<property name="text">
<string>Import from Steam</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>21</height>
</size>
</property>
</spacer>
</item>
<item row="5" column="0">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ValidatingLineEdit</class>
<extends>QLineEdit</extends>
<header>ui/validatinglineedit.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>AddAppDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>AddAppDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -0,0 +1,46 @@
#include "addautotagdialog.h"
#include "ui_addautotagdialog.h"
#include <QPushButton>
AddAutoTagDialog::AddAutoTagDialog(const QStringList& existing_tags, QWidget* parent) :
QDialog(parent), ui(new Ui::AddAutoTagDialog)
{
ui->setupUi(this);
ui->name_field->setCustomValidator([existing_tags](const auto& name)
{ return !name.isEmpty() && !existing_tags.contains(name); });
ui->name_field->setValidationMode(ValidatingLineEdit::VALID_CUSTOM);
connect(ui->name_field, &QLineEdit::textEdited, this, &AddAutoTagDialog::onTagNameEdited);
setWindowTitle("Add Auto Tag");
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
}
AddAutoTagDialog::AddAutoTagDialog(QStringList existing_tags,
const QString& tag_name,
QWidget* parent) : QDialog(parent), ui(new Ui::AddAutoTagDialog)
{
ui->setupUi(this);
existing_tags.removeAll(tag_name);
ui->name_field->setCustomValidator([existing_tags](const auto& name)
{ return !name.isEmpty() && !existing_tags.contains(name); });
ui->name_field->setValidationMode(ValidatingLineEdit::VALID_CUSTOM);
ui->name_field->setText(tag_name);
connect(ui->name_field, &QLineEdit::textEdited, this, &AddAutoTagDialog::onTagNameEdited);
setWindowTitle("Rename Auto Tag");
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
}
AddAutoTagDialog::~AddAutoTagDialog()
{
delete ui;
}
QString AddAutoTagDialog::getName() const
{
return ui->name_field->text();
}
void AddAutoTagDialog::onTagNameEdited()
{
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(ui->name_field->hasValidText());
}

53
src/ui/addautotagdialog.h Normal file
View File

@@ -0,0 +1,53 @@
/*!
* \file addautotagdialog.h
* \brief Header for the AddAutoTagDialog class.
*/
#pragma once
#include <QDialog>
namespace Ui
{
class AddAutoTagDialog;
}
/*!
* \brief Dialog for adding a new auto tag or renaming an existing one.
*/
class AddAutoTagDialog : public QDialog
{
Q_OBJECT
public:
/*!
* \brief Initializes the ui for adding a new auto tag.
* \param existing_tags Contains names of all currently existing auto tags.
* \param parent Parent of this widget.
*/
explicit AddAutoTagDialog(const QStringList& existing_tags, QWidget* parent = nullptr);
/*!
* \brief Initializes the ui for renaming an existing auto tag.
* \param existing_tags Contains names of all currently existing auto tags.
* \param tag_name Name of the tag to be renamed.
* \param parent Parent of this widget.
*/
AddAutoTagDialog(QStringList existing_tags, const QString& tag_name, QWidget* parent = nullptr);
/*! \brief Deletes the ui. */
~AddAutoTagDialog();
/*!
* \brief Resturns the name entered in the ui.
* \return The name.
*/
QString getName() const;
private:
/*! \brief Auto generated ui elements. */
Ui::AddAutoTagDialog* ui;
private slots:
/*! \brief Updates the OK button to only be enabled when a unique name has been entered. */
void onTagNameEdited();
};

View File

@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AddAutoTagDialog</class>
<widget class="QDialog" name="AddAutoTagDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>322</width>
<height>114</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="info_label">
<property name="text">
<string>Enter a unique tag name:</string>
</property>
</widget>
</item>
<item>
<widget class="ValidatingLineEdit" name="name_field"/>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>15</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ValidatingLineEdit</class>
<extends>QLineEdit</extends>
<header>ui/validatinglineedit.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>AddAutoTagDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>AddAutoTagDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -0,0 +1,44 @@
#include "addbackupdialog.h"
#include "ui_addbackupdialog.h"
#include <QDialogButtonBox>
#include <QPushButton>
AddBackupDialog::AddBackupDialog(QWidget* parent) : QDialog(parent), ui(new Ui::AddBackupDialog)
{
ui->setupUi(this);
}
AddBackupDialog::~AddBackupDialog()
{
delete ui;
}
void AddBackupDialog::setupDialog(int app_id,
int target_id,
const QString& target_name,
const QStringList& existing_backups)
{
target_name_ = target_name;
app_id_ = app_id;
target_id_ = target_id;
this->setWindowTitle("Add backup to " + target_name);
ui->name_field->clear();
ui->copy_from_box->clear();
ui->copy_from_box->addItems(existing_backups);
dialog_completed_ = false;
}
void AddBackupDialog::on_name_field_textChanged(const QString& text)
{
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!text.isEmpty());
}
void AddBackupDialog::on_buttonBox_accepted()
{
if(dialog_completed_)
return;
dialog_completed_ = true;
emit addBackupDialogAccepted(
app_id_, target_id_, ui->name_field->text(), target_name_, ui->copy_from_box->currentIndex());
}

79
src/ui/addbackupdialog.h Normal file
View File

@@ -0,0 +1,79 @@
/*!
* \file addbackupdialog.h
* \brief Header for the AddBackupDialog class.
*/
#pragma once
#include <QDialog>
namespace Ui
{
class AddBackupDialog;
}
/*!
* \brief Dialog for adding new backups.
*/
class AddBackupDialog : public QDialog
{
Q_OBJECT
public:
/*!
* \brief Initializes the UI.
* \param parent Parent for this widget, this is passed to the constructor of QDialog.
*/
explicit AddBackupDialog(QWidget* parent = nullptr);
/*! \brief Deletes the UI. */
~AddBackupDialog();
/*!
* \brief Initializes this dialog with data needed for backup creation.
* \param Application to which the new backup is to be added.
* \param target_id Id of the target for which to create a new backup.
* \param target_name Name of the target for which to create a new backup.
* \param existing_backups List of all existing backup names for the selected target.
*/
void setupDialog(int app_id,
int target_id,
const QString& target_name,
const QStringList& existing_backups);
private slots:
/*!
* \brief Called when the user edits the backup name field. Updates the Ok button.
* \param text New text.
*/
void on_name_field_textChanged(const QString& text);
/*! \brief Emits \ref addBackupDialogAccepted with the data entered in the Ui. */
void on_buttonBox_accepted();
private:
/*! \brief Contains auto-generated UI elements. */
Ui::AddBackupDialog* ui;
/*! \brief Id of the target for which to create a new backup. */
int target_id_;
/*! \brief Application to which the new backup is to be added. */
int app_id_;
/*! \brief Name of the target for which to create a new backup. */
QString target_name_;
/*! \brief Indicates whether the dialog has been completed. */
bool dialog_completed_ = false;
signals:
/*!
* \brief Signals completion of this dialog.
* \param app_id Application to which the new backup is to be added.
* \param target_id Id of the target for which to create a new backup.
* \param name Name of the target for which to create a new backup.
* \param target_name Name of the target for which to create a new backup.
* \param source_backup Data for the new backup will be copied from this backup.
*/
void addBackupDialogAccepted(int app_id,
int target_id,
QString name,
QString target_name,
int source_backup);
};

110
src/ui/addbackupdialog.ui Normal file
View File

@@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AddBackupDialog</class>
<widget class="QDialog" name="AddBackupDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>134</height>
</rect>
</property>
<property name="windowTitle">
<string>Add Backup</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="toolTip">
<string>Name for the new backup</string>
</property>
<property name="text">
<string>Backup name:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="ValidatingLineEdit" name="name_field"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="toolTip">
<string>Data from this backup will be copied to create the new backup</string>
</property>
<property name="text">
<string>Copy from:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="copy_from_box"/>
</item>
<item row="2" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>295</width>
<height>27</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="1">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ValidatingLineEdit</class>
<extends>QLineEdit</extends>
<header>ui/validatinglineedit.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>AddBackupDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>AddBackupDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -0,0 +1,113 @@
#include "addbackuptargetdialog.h"
#include "ui_addbackuptargetdialog.h"
#include <QDialogButtonBox>
#include <QDir>
#include <QFileDialog>
#include <QPushButton>
#include <QStandardPaths>
AddBackupTargetDialog::AddBackupTargetDialog(QWidget* parent) :
QDialog(parent), ui(new Ui::AddBackupTargetDialog)
{
ui->setupUi(this);
file_dialog_ = std::make_unique<QFileDialog>();
file_dialog_->setOption(QFileDialog::DontUseNativeDialog);
file_dialog_->setWindowTitle("Select Backup Target");
connect(file_dialog_.get(),
&QFileDialog::fileSelected,
this,
&AddBackupTargetDialog::onFileDialogAccepted);
connect(file_dialog_.get(),
&QFileDialog::currentChanged,
this,
&AddBackupTargetDialog::onFileDialogSelectionChanged);
updateOkButton();
ui->target_path_field->setValidationMode(ValidatingLineEdit::VALID_PATH_EXISTS);
dialog_completed_ = false;
}
AddBackupTargetDialog::~AddBackupTargetDialog()
{
delete ui;
}
void AddBackupTargetDialog::resetDialog(int app_id)
{
app_id_ = app_id;
ui->target_name_field->setText("");
ui->target_path_field->setText("");
ui->default_backup_field->setText("");
ui->first_backup_field->setText("");
updateOkButton();
dialog_completed_ = false;
}
void AddBackupTargetDialog::updateOkButton()
{
ui->buttonBox->button(QDialogButtonBox::Ok)
->setEnabled(!ui->target_name_field->text().isEmpty() && pathIsValid() &&
!ui->default_backup_field->text().isEmpty());
}
bool AddBackupTargetDialog::pathIsValid()
{
const QString path = ui->target_path_field->text();
if(path.isEmpty())
return false;
return QFileInfo::exists(path);
}
void AddBackupTargetDialog::on_target_name_field_textEdited(const QString& text)
{
updateOkButton();
}
void AddBackupTargetDialog::on_target_path_field_textEdited(const QString& text)
{
updateOkButton();
}
void AddBackupTargetDialog::on_default_backup_field_textEdited(const QString& text)
{
updateOkButton();
}
void AddBackupTargetDialog::on_buttonBox_accepted()
{
if(dialog_completed_)
return;
dialog_completed_ = true;
emit backupTargetAdded(app_id_,
ui->target_name_field->text(),
ui->target_path_field->text(),
ui->default_backup_field->text(),
ui->first_backup_field->text());
}
void AddBackupTargetDialog::on_path_picker_button_clicked()
{
QString starting_dir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
if(pathIsValid())
starting_dir = ui->target_path_field->text();
file_dialog_->setFileMode(QFileDialog::ExistingFile);
file_dialog_->setDirectory(starting_dir);
file_dialog_->exec();
}
void AddBackupTargetDialog::onFileDialogAccepted(const QString& path)
{
if(!path.isEmpty())
ui->target_path_field->setText(path);
updateOkButton();
}
void AddBackupTargetDialog::onFileDialogSelectionChanged(const QString& path)
{
QFileInfo info(path);
if(info.isFile())
file_dialog_->setFileMode(QFileDialog::ExistingFile);
else if(info.isDir())
file_dialog_->setFileMode(QFileDialog::Directory);
}

View File

@@ -0,0 +1,103 @@
/*!
* \file addbackuptargetdialog.h
* \brief Header for the AddBackupTargetDialog class.
*/
#pragma once
#include <QDialog>
#include <QFileDialog>
namespace Ui
{
class AddBackupTargetDialog;
}
/*!
* \brief Dialog for adding new backup targets.
*/
class AddBackupTargetDialog : public QDialog
{
Q_OBJECT
public:
/*!
* \brief Initializes the UI.
* \param parent Parent for this widget, this is passed to the constructor of QDialog.
*/
explicit AddBackupTargetDialog(QWidget* parent = nullptr);
/*! \brief Deletes the UI. */
~AddBackupTargetDialog();
/*!
* \brief Removes the text from all input fields.
* \param app_id Application to which the new backup target is to be added.
*/
void resetDialog(int app_id);
private:
/*! \brief Contains auto-generated UI elements. */
Ui::AddBackupTargetDialog* ui;
/*! \brief File dialog used to select a backup target. */
std::unique_ptr<QFileDialog> file_dialog_;
/*! \brief Application to which the new backup target is to be added. */
int app_id_;
/*! \brief Indicates whether the dialog has been completed. */
bool dialog_completed_ = false;
/*!
* \brief Updates the Ok button to only be enabled if the target path, name and default
* backup fields are filled.
*/
void updateOkButton();
/*! \brief Verifies if the target path field refers to an existing file or directory. */
bool pathIsValid();
private slots:
/*!
* \brief Calls \ref updateOkButton.
* \param text Ignored.
*/
void on_target_name_field_textEdited(const QString& text);
/*!
* \brief Calls \ref updateOkButton.
* \param text Ignored.
*/
void on_target_path_field_textEdited(const QString& text);
/*!
* \brief Calls \ref updateOkButton.
* \param text Ignored.
*/
void on_default_backup_field_textEdited(const QString& text);
/*! \brief Signals dialog completion by emitting \ref backupTargetAdded. */
void on_buttonBox_accepted();
/*! \brief Opens a file dialog to pick a target path. */
void on_path_picker_button_clicked();
/*!
* \brief Updates the target path field with the new path.
* \param path The selected path.
*/
void onFileDialogAccepted(const QString& path);
/*!
* \brief Updates the file mode of file_dialog_ to allow selection of both files and
* directories.
* \param path Currently selected item.
*/
void onFileDialogSelectionChanged(const QString& path);
signals:
/*!
* \brief Signals dialog has been accepted.
* \param app_id Application to which the new backup target is to be added.
* \param target_name Name of the new backup target.
* \param target_path Path to the file or directory to be managed.
* \param default_backup Name of the currently active version of the target.
* \param first_backup If not empty: Name of the first backup.
*/
void backupTargetAdded(int app_id,
QString target_name,
QString target_path,
QString default_backup,
QString first_backup);
};

View File

@@ -0,0 +1,135 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AddBackupTargetDialog</class>
<widget class="QDialog" name="AddBackupTargetDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>494</width>
<height>180</height>
</rect>
</property>
<property name="windowTitle">
<string>Add Backup Target</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Target Name:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="ValidatingLineEdit" name="target_name_field"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Target Path:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="ValidatingLineEdit" name="target_path_field"/>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="path_picker_button">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="folder-open">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Default Backup Name:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="ValidatingLineEdit" name="default_backup_field"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>First Backup Name:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="first_backup_field"/>
</item>
<item row="4" column="1" colspan="2">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>345</width>
<height>17</height>
</size>
</property>
</spacer>
</item>
<item row="5" column="1" colspan="2">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ValidatingLineEdit</class>
<extends>QLineEdit</extends>
<header>ui/validatinglineedit.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>AddBackupTargetDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>AddBackupTargetDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -0,0 +1,213 @@
#include "adddeployerdialog.h"
#include "../core/deployerfactory.h"
#include "ui_adddeployerdialog.h"
#include <QDebug>
#include <QDir>
#include <QFileDialog>
#include <QStandardPaths>
AddDeployerDialog::AddDeployerDialog(QWidget* parent) :
QDialog(parent), ui(new Ui::AddDeployerDialog)
{
ui->setupUi(this);
setupTypeBox();
enableOkButton(false);
ui->path_field->setValidationMode(ValidatingLineEdit::VALID_PATH_EXISTS);
ui->source_path_field->setValidationMode(ValidatingLineEdit::VALID_PATH_EXISTS);
}
AddDeployerDialog::~AddDeployerDialog()
{
delete ui;
}
void AddDeployerDialog::enableOkButton(bool state)
{
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(state);
}
bool AddDeployerDialog::pathIsValid()
{
QString path = ui->path_field->text();
if(path.isEmpty())
return false;
return QDir(path).exists();
}
void AddDeployerDialog::setupTypeBox()
{
ui->type_box->clear();
for(const auto& type : DeployerFactory::DEPLOYER_TYPES)
{
ui->type_box->addItem(type.c_str());
ui->type_box->setItemData(ui->type_box->count() - 1,
DeployerFactory::DEPLOYER_DESCRIPTIONS.at(type).c_str(),
Qt::ToolTipRole);
}
}
void AddDeployerDialog::updateOkButton()
{
enableOkButton(ui->name_field->hasValidText() && ui->path_field->hasValidText() &&
ui->source_path_field->hasValidText());
}
void AddDeployerDialog::setAddMode(int app_id)
{
app_id_ = app_id;
setWindowTitle("New Deployer");
ui->name_field->clear();
ui->path_field->clear();
ui->copy_box->setCheckState(Qt::Unchecked);
ui->warning_label->setHidden(true);
edit_mode_ = false;
setupTypeBox();
enableOkButton(false);
updateSourceFields();
ui->name_field->updateValidation();
ui->path_field->updateValidation();
ui->source_path_field->updateValidation();
dialog_completed_ = false;
}
void AddDeployerDialog::setEditMode(QString type,
QString name,
QString path,
bool use_copy_deployment,
int app_id,
int deployer_id)
{
name_ = name;
path_ = path;
type_ = type;
app_id_ = app_id;
deployer_id_ = deployer_id;
ui->copy_box->setCheckState(use_copy_deployment ? Qt::Checked : Qt::Unchecked);
if(use_copy_deployment)
ui->warning_label->setHidden(false);
else
ui->warning_label->setHidden(true);
setupTypeBox();
setWindowTitle("Edit " + name);
edit_mode_ = true;
ui->name_field->setText(name);
ui->path_field->setText(path);
for(int i = 0; i < ui->type_box->count(); i++)
{
if(ui->type_box->itemText(i) == type)
ui->type_box->setCurrentIndex(i);
}
updateSourceFields();
ui->name_field->updateValidation();
ui->path_field->updateValidation();
ui->source_path_field->updateValidation();
dialog_completed_ = false;
}
void AddDeployerDialog::updateSourceFields()
{
std::string cur_text = ui->type_box->currentText().toStdString();
if(cur_text.empty())
return;
bool hidden = !DeployerFactory::AUTONOMOUS_DEPLOYERS.at(cur_text);
ui->source_path_field->setHidden(hidden);
ui->source_dir_label->setHidden(hidden);
ui->source_picker_button->setHidden(hidden);
ui->source_path_field->updateValidation();
updateOkButton();
}
void AddDeployerDialog::on_file_picker_button_clicked()
{
QString starting_dir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
if(pathIsValid())
starting_dir = ui->path_field->text();
auto dialog = new QFileDialog;
dialog->setWindowTitle("Select Target Directory");
dialog->setFilter(QDir::AllDirs | QDir::Hidden);
dialog->setFileMode(QFileDialog::Directory);
dialog->setDirectory(starting_dir);
connect(dialog, &QFileDialog::fileSelected, this, &AddDeployerDialog::onFileDialogAccepted);
dialog->exec();
}
void AddDeployerDialog::on_name_field_textChanged(const QString& text)
{
updateOkButton();
}
void AddDeployerDialog::on_path_field_textChanged(const QString& text)
{
updateOkButton();
}
void AddDeployerDialog::on_buttonBox_accepted()
{
if(dialog_completed_)
return;
dialog_completed_ = true;
EditDeployerInfo info;
info.type = ui->type_box->currentText().toStdString();
info.name = ui->name_field->text().toStdString();
info.target_dir = ui->path_field->text().toStdString();
info.source_dir = ui->source_path_field->text().toStdString();
info.use_copy_deployment = ui->copy_box->checkState() == Qt::Checked;
if(edit_mode_)
emit deployerEdited(info, app_id_, deployer_id_);
else
emit deployerAdded(info, app_id_);
}
void AddDeployerDialog::onFileDialogAccepted(const QString& path)
{
if(!path.isEmpty())
ui->path_field->setText(path);
}
void AddDeployerDialog::on_copy_box_stateChanged(int state)
{
if(state == Qt::Checked)
ui->warning_label->setHidden(false);
else
ui->warning_label->setHidden(true);
}
void AddDeployerDialog::on_type_box_currentIndexChanged(int index)
{
updateSourceFields();
}
void AddDeployerDialog::on_source_picker_button_clicked()
{
QString starting_dir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation);
QString path = ui->source_path_field->text();
if(!path.isEmpty() && QDir(path).exists())
starting_dir = path;
auto dialog = new QFileDialog;
dialog->setWindowTitle("Select Source Directory");
dialog->setFilter(QDir::AllDirs | QDir::Hidden);
dialog->setFileMode(QFileDialog::Directory);
dialog->setDirectory(starting_dir);
connect(dialog, &QFileDialog::fileSelected, this, &AddDeployerDialog::onSourceDialogAccepted);
dialog->exec();
}
void AddDeployerDialog::onSourceDialogAccepted(const QString& path)
{
if(!path.isEmpty())
ui->source_path_field->setText(path);
}
void AddDeployerDialog::on_source_path_field_textChanged(const QString& path)
{
updateOkButton();
}

123
src/ui/adddeployerdialog.h Normal file
View File

@@ -0,0 +1,123 @@
/*!
* \file adddeployerdialog.h
* \brief Header for the AddDeployerDialog class.
*/
#pragma once
#include "../core/editdeployerinfo.h"
#include <QDialog>
namespace Ui
{
class AddDeployerDialog;
}
/*!
* \brief Dialog for creating and editing \ref Deployer "deployers".
*/
class AddDeployerDialog : public QDialog
{
Q_OBJECT
public:
/*!
* \brief Initializes the UI.
* \param parent Parent for this widget, this is passed to the constructor of QDialog.
*/
explicit AddDeployerDialog(QWidget* parent = nullptr);
/*! \brief Deletes the UI. */
~AddDeployerDialog();
private:
/*! \brief Contains auto-generated UI elements. */
Ui::AddDeployerDialog* ui;
/*! \brief If true: Dialog is used to edit, else: Dialog is used to create. */
bool edit_mode_ = false;
/*! \brief Current name of the edited Deployer. */
QString name_;
/*! \brief Current target directory of the edited Deployer. */
QString path_;
/*! \brief Current type of the edited Deployer. */
QString type_;
/*! \brief Id of the ModdedApplication owning the edited Deployer. */
int app_id_;
/*! \brief Id of the edited Deployer. */
int deployer_id_;
/*! \brief Indicates whether the dialog has been completed. */
bool dialog_completed_ = false;
/*!
* \brief Set the enabled state of this dialog's OK button.
* \param state
*/
void enableOkButton(bool state);
/*! \brief Checks whether the currently entered path exists. */
bool pathIsValid();
/*! \brief Adds all available Deployer types to the type combo box. */
void setupTypeBox();
/*! \brief Updates the state of this dialog's OK button to only be enabled when all inputs are
* valid. */
void updateOkButton();
public:
/*!
* \brief Initializes this dialog to allow creating a new Deployer.
* \param app_id Id of the ModdedApplication owning the edited Deployer.
*/
void setAddMode(int app_id);
/*!
* \brief setEditMode Initializes this dialog to allow editing an existing Deployer.
* \param type Current type of the edited Deployer.
* \param name Current name of the edited Deployer.
* \param path Current target directory of the edited Deployer.
* \param app_id Id of the ModdedApplication owning the edited Deployer.
* \param deployer_id Id of the edited Deployer.
*/
void setEditMode(QString type,
QString name,
QString path,
bool use_copy_deployment,
int app_id,
int deployer_id);
/*! \brief Enables/ Disables the ui elements responsible for setting a source directory. */
void updateSourceFields();
private slots:
/*! \brief Shows a file dialog for the target directory path. */
void on_file_picker_button_clicked();
/*! \brief Only enable the OK button if a name has been entered. */
void on_name_field_textChanged(const QString& text);
/*! \brief Only enable the OK button if a valid target directory path has been entered. */
void on_path_field_textChanged(const QString& text);
/*! \brief Closes the dialog and emits a signal for completion. */
void on_buttonBox_accepted();
/*! \brief Updates the target path with given path. */
void onFileDialogAccepted(const QString& path);
/*! \brief Updates the warning label. */
void on_copy_box_stateChanged(int arg1);
/*! \brief Updates the source path widgets enabled status. */
void on_type_box_currentIndexChanged(int index);
/*! \brief Shows a file dialog for the source directory path. */
void on_source_picker_button_clicked();
/*! \brief Updates the source path with given path. */
void onSourceDialogAccepted(const QString& path);
/*! \brief Only enable the OK button if a valid source directory path has been entered. */
void on_source_path_field_textChanged(const QString& path);
signals:
/*!
* \brief Signals completion of the dialog in add mode.
* \param info Contains all data entered in this dialog.
* \param app_id Id of the ModdedApplication owning the edited Deployer.
*/
void deployerAdded(EditDeployerInfo info, int app_id);
/*!
* \brief Signals completion of the dialog in edit mode.
* \param info Contains all data entered in this dialog.
* \param app_id Id of the ModdedApplication owning the edited Deployer.
* \param deployer_id Id of the edited Deployer.
*/
void deployerEdited(EditDeployerInfo info, int app_id, int deployer_id);
};

192
src/ui/adddeployerdialog.ui Normal file
View File

@@ -0,0 +1,192 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AddDeployerDialog</class>
<widget class="QDialog" name="AddDeployerDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>533</width>
<height>226</height>
</rect>
</property>
<property name="windowTitle">
<string>New Deployer</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="0">
<widget class="QCheckBox" name="copy_box">
<property name="toolTip">
<string>Copy all files instead of using hard links</string>
</property>
<property name="text">
<string>Use copy deployment</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="3">
<widget class="QLabel" name="warning_label">
<property name="text">
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;meta charset=&quot;utf-8&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
hr { height: 1px; border-width: 0; }
li.unchecked::marker { content: &quot;\2610&quot;; }
li.checked::marker { content: &quot;\2612&quot;; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-weight:700; color:#ff0000;&quot;&gt;Warning: Enabling copy deployment will double the disc space required per mod and may drastically increase deployment time!&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse</set>
</property>
</widget>
</item>
<item row="3" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>14</height>
</size>
</property>
</spacer>
</item>
<item row="4" column="2">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
<item row="0" column="0" colspan="3">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="toolTip">
<string>Name of the deployer</string>
</property>
<property name="text">
<string>Name:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="ValidatingLineEdit" name="name_field"/>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="type_box"/>
</item>
<item row="1" column="1">
<widget class="ValidatingLineEdit" name="path_field"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="toolTip">
<string>This is where installed mods will be deployed to</string>
</property>
<property name="text">
<string>Target directory:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="ValidatingLineEdit" name="source_path_field"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="source_dir_label">
<property name="text">
<string>Source directory:</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="file_picker_button">
<property name="toolTip">
<string>Pick target directory</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="folder-open">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_4">
<property name="toolTip">
<string>Determines how mods will be deployed</string>
</property>
<property name="text">
<string>Deployer type:</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QPushButton" name="source_picker_button">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="folder-open">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ValidatingLineEdit</class>
<extends>QLineEdit</extends>
<header>ui/validatinglineedit.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>AddDeployerDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>AddDeployerDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

424
src/ui/addmoddialog.cpp Normal file
View File

@@ -0,0 +1,424 @@
#include "addmoddialog.h"
#include "../core/installer.h"
#include "../core/log.h"
#include "fomoddialog.h"
#include "qdebug.h"
#include "ui_addmoddialog.h"
#include <QGroupBox>
#include <QMessageBox>
#include <QPushButton>
#include <QRadioButton>
#include <QSettings>
#include <QTreeWidget>
#include <ranges>
#include <regex>
#include <set>
namespace sfs = std::filesystem;
namespace str = std::ranges;
AddModDialog::AddModDialog(QWidget* parent) : QDialog(parent), ui(new Ui::AddModDialog)
{
ui->setupUi(this);
fomod_dialog_ = std::make_unique<FomodDialog>();
connect(
fomod_dialog_.get(), &FomodDialog::addModAccepted, this, &AddModDialog::onFomodDialogComplete);
connect(fomod_dialog_.get(), &FomodDialog::addModAborted, this, &AddModDialog::onFomodDialogAborted);
auto options_frame = new QFrame();
auto grid = new QGridLayout();
options_frame->setLayout(grid);
ui->options_container->setWidget(options_frame);
for(int i = 0; i < Installer::OPTION_GROUPS.size(); i++)
{
auto button_group = new QButtonGroup;
option_groups_.append(button_group);
auto box = new QGroupBox();
auto layout = new QVBoxLayout();
bool is_first = true;
for(const auto option : Installer::OPTION_GROUPS[i])
{
auto button = new QRadioButton(Installer::OPTION_NAMES.at(option).c_str());
button->setToolTip(Installer::OPTION_DESCRIPTIONS.at(option).c_str());
if(is_first)
{
button->setChecked(true);
is_first = false;
}
button_group->addButton(button, option);
layout->addWidget(button);
}
box->setLayout(layout);
grid->addWidget(box, i / 2, i % 2);
}
auto group_validator = [groups = &groups_](QString s) { return groups->contains(s); };
ui->group_field->setCustomValidator(group_validator);
ui->group_field->setValidationMode(ValidatingLineEdit::VALID_CUSTOM);
}
AddModDialog::~AddModDialog()
{
delete ui;
}
void AddModDialog::updateOkButton()
{
ui->buttonBox->button(QDialogButtonBox::Ok)
->setEnabled(ui->name_text->hasValidText() && ui->version_text->hasValidText() &&
ui->group_field->hasValidText());
}
void AddModDialog::colorTreeNodes(QTreeWidgetItem* node, int cur_depth, int root_level)
{
auto color = cur_depth < root_level ? COLOR_REMOVE_ : COLOR_KEEP_;
node->setForeground(0, color);
for(int i = 0; i < node->childCount(); i++)
colorTreeNodes(node->child(i), cur_depth + 1, root_level);
}
void AddModDialog::showError(const std::runtime_error& error)
{
std::string message = std::string("Could not open source files: ") + error.what();
Log::error(message);
QMessageBox* error_box =
new QMessageBox(QMessageBox::Critical, "Error", message.c_str(), QMessageBox::Ok);
error_box->exec();
}
bool AddModDialog::setupDialog(const QString& name,
const QStringList& deployers,
int cur_deployer,
const QStringList& groups,
const std::vector<int>& mod_ids,
const QString& path,
const QStringList& deployer_paths,
int app_id,
const std::vector<bool>& autonomous_deployers,
const QString& app_version,
const QString& local_source,
const QString& remote_source,
int mod_id)
{
ui->name_text->setFocus();
app_id_ = app_id;
mod_ids_ = mod_ids;
mod_path_ = path;
deployer_paths_ = deployer_paths;
groups_ = groups;
app_version_ = app_version;
local_source_ = local_source;
remote_source_ = remote_source;
ui->group_combo_box->setCurrentIndex(ADD_TO_GROUP_INDEX);
ui->deployer_list->setEnabled(true);
ui->group_field->clear();
ui->group_field->setEnabled(false);
ui->group_field->updateValidation();
completer_ = std::make_unique<QCompleter>(groups);
completer_->setCaseSensitivity(Qt::CaseInsensitive);
completer_->setFilterMode(Qt::MatchContains);
ui->group_field->setCompleter(completer_.get());
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
ui->group_check->setCheckState(Qt::Unchecked);
if(mod_id != -1)
{
auto iter = str::find(mod_ids, mod_id);
if(iter != mod_ids.end())
{
ui->group_field->setText(groups[iter - mod_ids.begin()]);
ui->group_check->setCheckState(Qt::Checked);
}
}
std::regex name_regex(R"(-\d+((?:-[\dA-Za-z]+)+)-\d+\.(?:zip|7z|rar)$)");
std::smatch match;
std::string name_str = name.toStdString();
if(std::regex_search(name_str, match, name_regex))
{
ui->name_text->setText(match.prefix().str().c_str());
std::string version_str = match[1].str();
if(!version_str.empty())
version_str.erase(version_str.begin());
std::replace(version_str.begin(), version_str.end(), '-', '.');
ui->version_text->setText(version_str.c_str());
}
else
{
ui->version_text->setText("1.0");
ui->name_text->setText(name);
}
ui->installer_box->clear();
int root_level = 0;
std::string prefix;
std::string detected_type;
try
{
auto signature = Installer::detectInstallerSignature(path.toStdString());
std::tie(root_level, prefix, detected_type) = signature;
}
catch(std::runtime_error& error)
{
showError(error);
emit addModAborted(mod_path_);
return false;
}
if(detected_type == Installer::FOMODINSTALLER)
{
auto [name, version] =
fomod::FomodInstaller::getMetaData(sfs::path(mod_path_.toStdString()) / prefix);
if(!name.empty())
ui->name_text->setText(name.c_str());
if(!version.empty())
ui->version_text->setText(version.c_str());
}
path_prefix_ = prefix.c_str();
int target_idx = 0;
for(int i = 0; const auto& installer : Installer::INSTALLER_TYPES)
{
if(installer == detected_type)
{
target_idx = i;
ui->installer_box->addItem(("[Auto detected] " + installer).c_str());
}
else
ui->installer_box->addItem(installer.c_str());
i++;
}
ui->installer_box->setCurrentIndex(target_idx);
ui->deployer_list->clear();
std::set<int> selected_deployers;
auto settings = QSettings(QCoreApplication::applicationName());
settings.beginGroup(QString::number(app_id));
int size = settings.beginReadArray("selected_deployers");
for(int i = 0; i < size; i++)
{
settings.setArrayIndex(i);
selected_deployers.insert(settings.value("selected").toInt());
}
settings.endArray();
ui->fomod_deployer_box->clear();
for(int i = 0; i < deployers.size(); i++)
{
if(!autonomous_deployers[i])
ui->fomod_deployer_box->addItem(deployers[i]);
auto item = new QListWidgetItem(deployers[i], ui->deployer_list);
item->setCheckState((selected_deployers.contains(i) | (i == cur_deployer)) ? Qt::Checked
: Qt::Unchecked);
item->setHidden(autonomous_deployers[i]);
}
int fomod_target_deployer = settings.value("fomod_target_deployer", -1).toInt();
if(fomod_target_deployer > 0 && fomod_target_deployer < ui->fomod_deployer_box->count())
ui->fomod_deployer_box->setCurrentIndex(fomod_target_deployer);
else if(cur_deployer > 0 && cur_deployer < ui->fomod_deployer_box->count())
ui->fomod_deployer_box->setCurrentIndex(cur_deployer);
settings.endGroup();
try
{
auto paths = Installer::getArchiveFileNames(path.toStdString());
int max_depth = 0;
ui->content_tree->clear();
for(const auto& path : paths)
max_depth = std::max(addTreeNode(ui->content_tree, path), max_depth);
ui->root_level_box->setMaximum(std::max(max_depth - 1, 0));
ui->root_level_box->setValue(root_level);
on_root_level_box_valueChanged(root_level);
}
catch(std::runtime_error& error)
{
showError(error);
emit addModAborted(mod_path_);
return false;
}
dialog_completed_ = false;
return true;
}
void AddModDialog::closeEvent(QCloseEvent* event)
{
if(dialog_completed_)
return;
dialog_completed_ = true;
emit addModAborted(mod_path_);
QDialog::closeEvent(event);
}
sfs::path AddModDialog::removeRoot(const sfs::path& source)
{
sfs::path result;
bool is_first = true;
for(auto it = source.begin(); it != source.end(); it++)
{
if(!is_first)
result /= *it;
is_first = false;
}
return result;
}
int AddModDialog::addTreeNode(QTreeWidgetItem* parent, const sfs::path& cur_path)
{
if(cur_path.empty())
return 0;
QString cur_text{ (*(cur_path.begin())).c_str() };
for(int i = 0; i < parent->childCount(); i++)
{
auto cur_child = parent->child(i);
if(cur_child->text(0) == cur_text)
return addTreeNode(cur_child, removeRoot(cur_path)) + 1;
}
auto child = new QTreeWidgetItem(parent);
child->setText(0, cur_text);
child->setForeground(0, COLOR_KEEP_);
return addTreeNode(child, removeRoot(cur_path)) + 1;
}
int AddModDialog::addTreeNode(QTreeWidget* tree, const sfs::path& cur_path)
{
if(cur_path.empty())
return 0;
QString cur_text{ (*(cur_path.begin())).c_str() };
for(int i = 0; i < tree->topLevelItemCount(); i++)
{
auto cur_item = tree->topLevelItem(i);
if(cur_item->text(0) == cur_text)
return addTreeNode(cur_item, removeRoot(cur_path)) + 1;
}
auto item = new QTreeWidgetItem(tree);
item->setText(0, cur_text);
item->setForeground(0, COLOR_KEEP_);
return addTreeNode(item, removeRoot(cur_path)) + 1;
}
void AddModDialog::on_buttonBox_accepted()
{
if(dialog_completed_)
return;
dialog_completed_ = true;
int options = 0;
for(const auto group : static_cast<const QList<QButtonGroup*>>(option_groups_))
options |= group->checkedId();
const bool replace_mod = ui->group_combo_box->currentIndex() == REPLACE_MOD_INDEX;
int group = -1;
const QString group_name = ui->group_field->text();
if(ui->group_check->isChecked() && groups_.contains(group_name))
group = mod_ids_[groups_.indexOf(group_name)];
std::vector<int> deployers;
auto settings = QSettings(QCoreApplication::applicationName());
settings.beginGroup(QString::number(app_id_));
settings.beginWriteArray("selected_deployers");
int settings_index = 0;
for(int i = 0; i < ui->deployer_list->count(); i++)
{
if(ui->deployer_list->item(i)->checkState() == Qt::Checked)
{
settings.setArrayIndex(settings_index++);
settings.setValue("selected", i);
deployers.push_back(i);
}
}
settings.endArray();
settings.setValue("fomod_target_deployer", ui->fomod_deployer_box->currentIndex());
settings.endGroup();
std::vector<std::pair<sfs::path, sfs::path>> fomod_files{};
AddModInfo info{ ui->name_text->text().toStdString(),
ui->version_text->text().toStdString(),
Installer::INSTALLER_TYPES[ui->installer_box->currentIndex()],
mod_path_.toStdString(),
deployers,
group,
options,
ui->root_level_box->value(),
fomod_files,
replace_mod,
local_source_.toStdString(),
remote_source_.toStdString() };
if(Installer::INSTALLER_TYPES[ui->installer_box->currentIndex()] == Installer::FOMODINSTALLER)
{
fomod_dialog_->setupDialog(
sfs::path(mod_path_.toStdString()) / path_prefix_.toStdString(),
deployer_paths_[ui->fomod_deployer_box->currentIndex()].toStdString(),
app_version_,
info,
app_id_);
if(!fomod_dialog_->hasSteps())
{
info.files = fomod_dialog_->getResult();
emit addModAccepted(app_id_, info);
}
fomod_dialog_->show();
}
else
emit addModAccepted(app_id_, info);
}
void AddModDialog::on_group_check_stateChanged(int state)
{
ui->group_field->setEnabled(state == Qt::Checked);
ui->deployer_list->setEnabled(ui->group_combo_box->currentIndex() == ADD_TO_GROUP_INDEX ||
state == Qt::Unchecked);
ui->group_field->updateValidation();
updateOkButton();
}
void AddModDialog::on_buttonBox_rejected()
{
if(dialog_completed_)
return;
dialog_completed_ = true;
emit addModAborted(mod_path_);
}
void AddModDialog::on_name_text_textChanged(const QString& text)
{
updateOkButton();
}
void AddModDialog::on_version_text_textChanged(const QString& text)
{
updateOkButton();
}
void AddModDialog::on_root_level_box_valueChanged(int value)
{
for(int i = 0; i < ui->content_tree->topLevelItemCount(); i++)
colorTreeNodes(ui->content_tree->topLevelItem(i), 0, value);
}
void AddModDialog::on_installer_box_currentIndexChanged(int index)
{
if(ui->installer_box->count() > 0 &&
Installer::INSTALLER_TYPES[ui->installer_box->currentIndex()] == Installer::FOMODINSTALLER)
{
ui->fomod_deployer_box->setVisible(true);
ui->fomod_label->setVisible(true);
ui->options_container->setEnabled(false);
}
else
{
ui->fomod_deployer_box->setVisible(false);
ui->fomod_label->setVisible(false);
ui->options_container->setEnabled(true);
}
}
void AddModDialog::on_group_field_textChanged(const QString& arg1)
{
updateOkButton();
}
void AddModDialog::on_group_combo_box_currentIndexChanged(int index)
{
ui->deployer_list->setEnabled(index == ADD_TO_GROUP_INDEX ||
ui->group_check->checkState() == Qt::Unchecked);
}
void AddModDialog::onFomodDialogComplete(int app_id, AddModInfo info)
{
emit addModAccepted(app_id, info);
}
void AddModDialog::onFomodDialogAborted()
{
emit addModAborted(mod_path_);
}

207
src/ui/addmoddialog.h Normal file
View File

@@ -0,0 +1,207 @@
/*!
* \file addmoddialog.h
* \brief Header for the AddModDialog class.
*/
#pragma once
#include "ui/fomoddialog.h"
#include <QButtonGroup>
#include <QCompleter>
#include <QDialog>
#include <QFrame>
#include <QTreeWidgetItem>
#include <QVBoxLayout>
#include <filesystem>
namespace Ui
{
class AddModDialog;
}
/*!
* \brief Dialog for installing new mods.
*/
class AddModDialog : public QDialog
{
Q_OBJECT
public:
/*!
* \brief Initializes the UI.
* \param parent Parent for this widget, this is passed to the constructor of QDialog.
*/
explicit AddModDialog(QWidget* parent = nullptr);
/*! \brief Deletes the UI. */
~AddModDialog();
/*!
* \brief Initializes this dialog with data needed for mod installation.
* \param name Default mod name.
* \param deployers Contains all available \ref Deployer "deployers".
* \param cur_deployer The currently active Deployer.
* \param groups Contains all mod names which act as targets for groups.
* \param mod_ids Ids of all currently installed mods.
* \param path Source path for the new mod.
* \param deployer_paths Contains target paths for all non autonomous deployers.
* \param app_id Id of currently active application.
* \param autonomous_deployers Vector of bools indicating for each deployer
* if that deployer is autonomous.
* \param local_source Source archive for the mod.
* \param remote_source URL from where the mod was downloaded.
* \param mod_id If =! -1: Id of the mod to the group of which the new mod should be added by default.
* \return True if dialog creation was successful.
*/
bool setupDialog(const QString& name,
const QStringList& deployers,
int cur_deployer,
const QStringList& groups,
const std::vector<int>& mod_ids,
const QString& path,
const QStringList& deployer_paths,
int app_id,
const std::vector<bool>& autonomous_deployers,
const QString& app_version,
const QString& local_source,
const QString& remote_source,
int mod_id);
/*!
* \brief Closes the dialog and emits a signal indicating installation has been canceled.
* \param event The close even sent upon closing the application.
*/
void closeEvent(QCloseEvent* event) override;
private:
/*! \brief Contains auto-generated UI elements. */
Ui::AddModDialog* ui;
/*! \brief Contains mod ids corresponding to entries in the field. */
std::vector<int> mod_ids_;
/*! \brief Source path for the new mod data. */
QString mod_path_;
/*! \brief Stores the id of the currently active \ref ModdedApplication "application". */
int app_id_;
/*! \brief Holds radio button groups used to select installation options. */
QList<QButtonGroup*> option_groups_;
/*! \brief Used to color tree nodes which will not be removed. */
const QColor COLOR_KEEP_{ 0x2ca02c };
/*! \brief Used to color tree nodes which will be removed. */
const QColor COLOR_REMOVE_{ 0xd62728 };
/*! \brief Contains target paths for all deployers. */
QStringList deployer_paths_;
/*! \brief Prefix for fomod installer source path. */
QString path_prefix_;
/*! \brief Contains names of all available groups. */
QStringList groups_;
/*! \brief Completer used for group names. */
std::unique_ptr<QCompleter> completer_;
/*! \brief Dialog for fomod installation. */
std::unique_ptr<FomodDialog> fomod_dialog_;
/*! \brief Index in ui->group_combo_box representing the option of adding a mod to a group. */
static constexpr int ADD_TO_GROUP_INDEX = 0;
/*! \brief Index in ui->group_combo_box representing the option of replacing an existing mod. */
static constexpr int REPLACE_MOD_INDEX = 1;
/*! \brief App version used for fomod conditions. */
QString app_version_;
/*! \brief Path to the source archive for the mod. */
QString local_source_;
/*! \brief URL from where the mod was downloaded. */
QString remote_source_;
/*! \brief Indicates whether the dialog has been completed. */
bool dialog_completed_ = false;
/*!
* \brief Updates the enabled state of this dialog's OK button to only be enabled when
* both a name and a version has been entered and an existing group or no group has been
* selected.
*/
void updateOkButton();
/*!
* \brief Adds the root path element of given path as a root node to the given tree.
* Then adds all subsequent path components as children to the new node.
* \param tree Target QTreeWidget.
* \param cur_path Source path.
*/
int addTreeNode(QTreeWidget* tree, const std::filesystem::path& cur_path);
/*!
* \brief Adds the root path element of given path as a root node to the given parent node.
* Then adds all subsequent path components as children to the new node.
* \param tree Target QTreeWidget.
* \param cur_path Source path.
* \return The depth of the given path.
*/
int addTreeNode(QTreeWidgetItem* parent, const std::filesystem::path& cur_path);
/*!
* \brief Removes the root path component from the given path.
* \param source Source path.
* \return source without its root component.
* \return The depth of the given path.
*/
std::filesystem::path removeRoot(const std::filesystem::path& source);
/*!
* \brief Changes the color of the given node and its children, depending on
* whether or not the nodes depth is less than the given root level.
* \param node Node to be colored.
* \param cur_depth Depth of current node.
* \param root_level Target depth.
*/
void colorTreeNodes(QTreeWidgetItem* node, int cur_depth, int root_level);
/*!
* \brief Shows a message box with a message constructed from given exception.
* \param error Source of error.
*/
void showError(const std::runtime_error& error);
private slots:
/*! \brief Closes the dialog and emits a signal for completion. */
void on_buttonBox_accepted();
/*! \brief Enables or disables the group combo box depending on the check box state. */
void on_group_check_stateChanged(int state);
/*! \brief Closes the dialog and emits a signal indicating installation has been canceled. */
void on_buttonBox_rejected();
/*! \brief Only enable the OK button if a name has been entered. */
void on_name_text_textChanged(const QString& text);
/*! \brief Only enable the OK button if a version has been entered. */
void on_version_text_textChanged(const QString& text);
/*!
* \brief Called when the value of the root level box has been changed by a user.
* \param The new value.
*/
void on_root_level_box_valueChanged(int value);
/*!
* \brief Enables/ disables ui elements based on chosen installer.
* \param index New index.
*/
void on_installer_box_currentIndexChanged(int index);
/*!
* \brief Updates the Ok buttons enabled state.
* \param arg1 Ignored.
*/
void on_group_field_textChanged(const QString& arg1);
/*!
* \brief Disables the deployer list if the new mod is to replace an existing mod.
* \param index The new index.
*/
void on_group_combo_box_currentIndexChanged(int index);
/*!
* \brief Called when the fomod dialog has been completed. Emits addModAccepted.
* \param app_id Application for which the new mod is to be installed.
* \param info Contains all data needed to install the mod.
*/
void onFomodDialogComplete(int app_id, AddModInfo info);
/*! \brief Called when fomod dialog has been canceled. Emits addModAborted */
void onFomodDialogAborted();
signals:
/*!
* \brief Signals dialog completion.
* \param app_id Application for which the new mod is to be installed.
* \param info Contains all data needed to install the mod.
*/
void addModAccepted(int app_id, AddModInfo info);
/*!
* \brief Signals mod installation has been aborted.
* \param temp_dir Directory used for mod extraction.
*/
void addModAborted(QString temp_dir);
};

270
src/ui/addmoddialog.ui Normal file
View File

@@ -0,0 +1,270 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AddModDialog</class>
<widget class="QDialog" name="AddModDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>615</width>
<height>556</height>
</rect>
</property>
<property name="windowTitle">
<string>Install Mod</string>
</property>
<layout class="QGridLayout" name="gridLayout_6">
<item row="5" column="0" colspan="3">
<widget class="QScrollArea" name="options_container">
<property name="minimumSize">
<size>
<width>0</width>
<height>120</height>
</size>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>595</width>
<height>118</height>
</rect>
</property>
</widget>
</widget>
</item>
<item row="3" column="0" colspan="3">
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="0">
<widget class="QLabel" name="fomod_label">
<property name="toolTip">
<string>This deployer's target directory is used by the fomod installer to check for file dependencies</string>
</property>
<property name="text">
<string>Fomod target:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="fomod_deployer_box"/>
</item>
</layout>
</item>
<item row="8" column="2">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_5">
<property name="toolTip">
<string>Options for the installer</string>
</property>
<property name="text">
<string>Options:</string>
</property>
</widget>
</item>
<item row="6" column="2">
<widget class="QLabel" name="label_4">
<property name="toolTip">
<string>The new mod will be added to these installers</string>
</property>
<property name="text">
<string>Add to deployers:</string>
</property>
</widget>
</item>
<item row="7" column="2">
<widget class="QListWidget" name="deployer_list">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::NoSelection</enum>
</property>
</widget>
</item>
<item row="0" column="0" colspan="3">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="toolTip">
<string>Mod name</string>
</property>
<property name="text">
<string>Name:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="ValidatingLineEdit" name="name_text">
<property name="clearButtonEnabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="toolTip">
<string>Mod version</string>
</property>
<property name="text">
<string>Version:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="ValidatingLineEdit" name="version_text">
<property name="acceptDrops">
<bool>true</bool>
</property>
<property name="text">
<string>1.0</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="7" column="0">
<widget class="QTreeWidget" name="content_tree">
<property name="toolTip">
<string>Mod contents</string>
</property>
<property name="headerHidden">
<bool>true</bool>
</property>
<column>
<property name="text">
<string notr="true">1</string>
</property>
</column>
</widget>
</item>
<item row="6" column="0">
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0">
<widget class="QLabel" name="label_6">
<property name="toolTip">
<string>Removes the first n directories from every path</string>
</property>
<property name="text">
<string>Root level:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="root_level_box">
<property name="suffix">
<string/>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0" colspan="3">
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QCheckBox" name="group_check">
<property name="toolTip">
<string>Only one mod in a group can be active at a time</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="ValidatingLineEdit" name="group_field">
<property name="placeholderText">
<string>enter mod name</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="group_combo_box">
<item>
<property name="text">
<string>Add to Group</string>
</property>
</item>
<item>
<property name="text">
<string>Replace Mod</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item row="1" column="0" colspan="3">
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="1">
<widget class="QComboBox" name="installer_box"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_3">
<property name="toolTip">
<string>Determines how mods are installed</string>
</property>
<property name="text">
<string>Installer:</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ValidatingLineEdit</class>
<extends>QLineEdit</extends>
<header>ui/validatinglineedit.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>AddModDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>AddModDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

Some files were not shown because too many files have changed in this diff Show More