mirror of
https://github.com/limo-app/limo.git
synced 2025-12-23 14:57:56 -05:00
initial release
This commit is contained in:
79
.gitignore
vendored
Normal file
79
.gitignore
vendored
Normal 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
246
CMakeLists.txt
Normal 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
674
LICENSE
Normal 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
98
README.md
Normal 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
|
||||
```
|
||||
72
resources/filter_accept.svg
Normal file
72
resources/filter_accept.svg
Normal 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 |
61
resources/filter_reject.svg
Normal file
61
resources/filter_reject.svg
Normal 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
7
resources/icons.qrc
Normal 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
BIN
resources/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
BIN
resources/logo_small.png
Normal file
BIN
resources/logo_small.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
BIN
resources/showcase.png
Normal file
BIN
resources/showcase.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 95 KiB |
42
src/core/addmodinfo.h
Normal file
42
src/core/addmodinfo.h
Normal 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
72
src/core/appinfo.h
Normal 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
110
src/core/autotag.cpp
Normal 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
191
src/core/autotag.h
Normal 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
397
src/core/backupmanager.cpp
Normal 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
196
src/core/backupmanager.h
Normal 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
21
src/core/backuptarget.cpp
Normal 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
46
src/core/backuptarget.h
Normal 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;
|
||||
};
|
||||
145
src/core/casematchingdeployer.cpp
Normal file
145
src/core/casematchingdeployer.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
61
src/core/casematchingdeployer.h
Normal file
61
src/core/casematchingdeployer.h
Normal 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;
|
||||
};
|
||||
25
src/core/compressionerror.h
Normal file
25
src/core/compressionerror.h
Normal 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
31
src/core/conflictinfo.h
Normal 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
121
src/core/cryptography.cpp
Normal 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
58
src/core/cryptography.h
Normal 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
696
src/core/deployer.cpp
Normal 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
349
src/core/deployer.h
Normal 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;
|
||||
};
|
||||
21
src/core/deployerfactory.cpp
Normal file
21
src/core/deployerfactory.cpp
Normal 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 + "\"!");
|
||||
}
|
||||
61
src/core/deployerfactory.h
Normal file
61
src/core/deployerfactory.h
Normal 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
32
src/core/deployerinfo.h
Normal 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;
|
||||
};
|
||||
38
src/core/editapplicationinfo.h
Normal file
38
src/core/editapplicationinfo.h
Normal 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;
|
||||
};
|
||||
49
src/core/editautotagaction.cpp
Normal file
49
src/core/editautotagaction.cpp
Normal 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_;
|
||||
}
|
||||
92
src/core/editautotagaction.h
Normal file
92
src/core/editautotagaction.h
Normal 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_;
|
||||
};
|
||||
27
src/core/editdeployerinfo.h
Normal file
27
src/core/editdeployerinfo.h
Normal 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 = "";
|
||||
};
|
||||
22
src/core/editmanualtagaction.cpp
Normal file
22
src/core/editmanualtagaction.cpp
Normal 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_;
|
||||
}
|
||||
60
src/core/editmanualtagaction.h
Normal file
60
src/core/editmanualtagaction.h
Normal 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_;
|
||||
};
|
||||
24
src/core/editprofileinfo.h
Normal file
24
src/core/editprofileinfo.h
Normal 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;
|
||||
};
|
||||
139
src/core/fomod/dependency.cpp
Normal file
139
src/core/fomod/dependency.cpp
Normal 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 + " )";
|
||||
}
|
||||
}
|
||||
83
src/core/fomod/dependency.h
Normal file
83
src/core/fomod/dependency.h
Normal 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
49
src/core/fomod/file.h
Normal 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; }
|
||||
};
|
||||
}
|
||||
411
src/core/fomod/fomodinstaller.cpp
Normal file
411
src/core/fomod/fomodinstaller.cpp
Normal 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 };
|
||||
}
|
||||
169
src/core/fomod/fomodinstaller.h
Normal file
169
src/core/fomod/fomodinstaller.h
Normal 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; });
|
||||
}
|
||||
};
|
||||
}
|
||||
29
src/core/fomod/installstep.h
Normal file
29
src/core/fomod/installstep.h
Normal 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
67
src/core/fomod/plugin.h
Normal 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
26
src/core/fomod/plugindependency.h
Normal file
26
src/core/fomod/plugindependency.h
Normal 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;
|
||||
};
|
||||
}
|
||||
44
src/core/fomod/plugingroup.h
Normal file
44
src/core/fomod/plugingroup.h
Normal 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;
|
||||
};
|
||||
}
|
||||
38
src/core/fomod/plugintype.h
Normal file
38
src/core/fomod/plugintype.h
Normal 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
57
src/core/importmodinfo.h
Normal 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
431
src/core/installer.cpp
Normal 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
176
src/core/installer.h
Normal 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
65
src/core/log.cpp
Normal 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
61
src/core/log.h
Normal 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
650
src/core/lootdeployer.cpp
Normal 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
276
src/core/lootdeployer.h
Normal 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
61
src/core/manualtag.cpp
Normal 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
65
src/core/manualtag.h
Normal 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
53
src/core/mod.cpp
Normal 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
78
src/core/mod.h
Normal 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;
|
||||
};
|
||||
1993
src/core/moddedapplication.cpp
Normal file
1993
src/core/moddedapplication.cpp
Normal file
File diff suppressed because it is too large
Load Diff
729
src/core/moddedapplication.h
Normal file
729
src/core/moddedapplication.h
Normal 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
84
src/core/modinfo.h
Normal 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
334
src/core/nexus/api.cpp
Normal 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
152
src/core/nexus/api.h
Normal 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
44
src/core/nexus/file.cpp
Normal 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
86
src/core/nexus/file.h
Normal 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
52
src/core/nexus/mod.cpp
Normal 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
100
src/core/nexus/mod.h
Normal 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
27
src/core/parseerror.h
Normal 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
201
src/core/pathutils.cpp
Normal 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
110
src/core/pathutils.h
Normal 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
107
src/core/progressnode.cpp
Normal 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
131
src/core/progressnode.h
Normal 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
29
src/core/tag.cpp
Normal 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
57
src/core/tag.h
Normal 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
34
src/core/tagcondition.h
Normal 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;
|
||||
};
|
||||
442
src/core/tagconditionnode.cpp
Normal file
442
src/core/tagconditionnode.cpp
Normal 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
156
src/core/tagconditionnode.h
Normal 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
27
src/cspell.json
Normal 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
2737
src/lmm_Doxyfile
Normal file
File diff suppressed because it is too large
Load Diff
127
src/main.cpp
Normal file
127
src/main.cpp
Normal 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();
|
||||
}
|
||||
52
src/ui/addapikeydialog.cpp
Normal file
52
src/ui/addapikeydialog.cpp
Normal 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
59
src/ui/addapikeydialog.h
Normal 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
75
src/ui/addapikeydialog.ui
Normal 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
217
src/ui/addappdialog.cpp
Normal 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
139
src/ui/addappdialog.h
Normal 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
231
src/ui/addappdialog.ui
Normal 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>
|
||||
46
src/ui/addautotagdialog.cpp
Normal file
46
src/ui/addautotagdialog.cpp
Normal 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
53
src/ui/addautotagdialog.h
Normal 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();
|
||||
};
|
||||
94
src/ui/addautotagdialog.ui
Normal file
94
src/ui/addautotagdialog.ui
Normal 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>
|
||||
44
src/ui/addbackupdialog.cpp
Normal file
44
src/ui/addbackupdialog.cpp
Normal 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
79
src/ui/addbackupdialog.h
Normal 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
110
src/ui/addbackupdialog.ui
Normal 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>
|
||||
113
src/ui/addbackuptargetdialog.cpp
Normal file
113
src/ui/addbackuptargetdialog.cpp
Normal 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);
|
||||
}
|
||||
103
src/ui/addbackuptargetdialog.h
Normal file
103
src/ui/addbackuptargetdialog.h
Normal 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);
|
||||
};
|
||||
135
src/ui/addbackuptargetdialog.ui
Normal file
135
src/ui/addbackuptargetdialog.ui
Normal 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>
|
||||
213
src/ui/adddeployerdialog.cpp
Normal file
213
src/ui/adddeployerdialog.cpp
Normal 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
123
src/ui/adddeployerdialog.h
Normal 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
192
src/ui/adddeployerdialog.ui
Normal 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><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
|
||||
<html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css">
|
||||
p, li { white-space: pre-wrap; }
|
||||
hr { height: 1px; border-width: 0; }
|
||||
li.unchecked::marker { content: "\2610"; }
|
||||
li.checked::marker { content: "\2612"; }
|
||||
</style></head><body style=" font-family:'Sans Serif'; font-size:9pt; font-weight:400; font-style:normal;">
|
||||
<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-weight:700; color:#ff0000;">Warning: Enabling copy deployment will double the disc space required per mod and may drastically increase deployment time!</span></p></body></html></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
424
src/ui/addmoddialog.cpp
Normal 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
207
src/ui/addmoddialog.h
Normal 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
270
src/ui/addmoddialog.ui
Normal 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
Reference in New Issue
Block a user