Compare commits
26 Commits
develop
...
release-1.
Author | SHA1 | Date | |
---|---|---|---|
|
6e65cd4405 | ||
|
98fbb3613d | ||
|
2666beafb1 | ||
|
cf8a76be6b | ||
|
e96f482544 | ||
|
dc32f0671b | ||
|
d6d7794cd0 | ||
|
94d95d45d4 | ||
|
5e767a91d9 | ||
|
c89f8b4657 | ||
|
d7fc0f53d3 | ||
|
e81cf8a845 | ||
|
c116885ab1 | ||
|
d68d5ca23f | ||
|
6d94338a56 | ||
|
1e1a1cef05 | ||
|
34bab3e1b2 | ||
|
be6d6501e8 | ||
|
10a70732ce | ||
|
a725dc82a7 | ||
|
97ce8a94e9 | ||
|
85f0904872 | ||
|
cb0a5e42df | ||
|
a0c7fa30c0 | ||
|
b7490b479c | ||
|
0d35edbbf3 |
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
@ -43,7 +43,7 @@ jobs:
|
|||||||
macosx_deployment_target: 10.14
|
macosx_deployment_target: 10.14
|
||||||
qt_ver: 6
|
qt_ver: 6
|
||||||
qt_host: mac
|
qt_host: mac
|
||||||
qt_version: '6.3.1'
|
qt_version: '6.3.0'
|
||||||
qt_modules: 'qt5compat qtimageformats'
|
qt_modules: 'qt5compat qtimageformats'
|
||||||
qt_path: /Users/runner/work/PolyMC/Qt
|
qt_path: /Users/runner/work/PolyMC/Qt
|
||||||
|
|
||||||
@ -315,6 +315,9 @@ jobs:
|
|||||||
|
|
||||||
cp -r /home/runner/work/PolyMC/Qt/${{ matrix.qt_version }}/gcc_64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines
|
cp -r /home/runner/work/PolyMC/Qt/${{ matrix.qt_version }}/gcc_64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines
|
||||||
|
|
||||||
|
cp /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
|
||||||
|
cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}//usr/lib/
|
||||||
|
|
||||||
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib"
|
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib"
|
||||||
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-8-openjdk/lib/amd64/server"
|
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-8-openjdk/lib/amd64/server"
|
||||||
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-8-openjdk/lib/amd64"
|
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-8-openjdk/lib/amd64"
|
||||||
|
@ -29,10 +29,10 @@ set(CMAKE_JAVA_TARGET_OUTPUT_DIR ${PROJECT_BINARY_DIR}/jars)
|
|||||||
######## Set compiler flags ########
|
######## Set compiler flags ########
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED true)
|
set(CMAKE_CXX_STANDARD_REQUIRED true)
|
||||||
set(CMAKE_C_STANDARD_REQUIRED true)
|
set(CMAKE_C_STANDARD_REQUIRED true)
|
||||||
set(CMAKE_CXX_STANDARD 11)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
set(CMAKE_C_STANDARD 11)
|
set(CMAKE_C_STANDARD 11)
|
||||||
include(GenerateExportHeader)
|
include(GenerateExportHeader)
|
||||||
set(CMAKE_CXX_FLAGS "-Wall -pedantic -D_GLIBCXX_USE_CXX11_ABI=0 -fstack-protector-strong --param=ssp-buffer-size=4 ${CMAKE_CXX_FLAGS}")
|
set(CMAKE_CXX_FLAGS "-Wall -pedantic -fstack-protector-strong --param=ssp-buffer-size=4 ${CMAKE_CXX_FLAGS}")
|
||||||
if(UNIX AND APPLE)
|
if(UNIX AND APPLE)
|
||||||
set(CMAKE_CXX_FLAGS "-stdlib=libc++ ${CMAKE_CXX_FLAGS}")
|
set(CMAKE_CXX_FLAGS "-stdlib=libc++ ${CMAKE_CXX_FLAGS}")
|
||||||
endif()
|
endif()
|
||||||
@ -81,7 +81,7 @@ set(Launcher_HELP_URL "https://polymc.org/wiki/help-pages/%1" CACHE STRING "URL
|
|||||||
######## Set version numbers ########
|
######## Set version numbers ########
|
||||||
set(Launcher_VERSION_MAJOR 1)
|
set(Launcher_VERSION_MAJOR 1)
|
||||||
set(Launcher_VERSION_MINOR 4)
|
set(Launcher_VERSION_MINOR 4)
|
||||||
set(Launcher_VERSION_HOTFIX 0)
|
set(Launcher_VERSION_HOTFIX 2)
|
||||||
|
|
||||||
# Build number
|
# Build number
|
||||||
set(Launcher_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.")
|
set(Launcher_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.")
|
||||||
@ -319,7 +319,6 @@ endif()
|
|||||||
add_subdirectory(libraries/rainbow) # Qt extension for colors
|
add_subdirectory(libraries/rainbow) # Qt extension for colors
|
||||||
add_subdirectory(libraries/LocalPeer) # fork of a library from Qt solutions
|
add_subdirectory(libraries/LocalPeer) # fork of a library from Qt solutions
|
||||||
add_subdirectory(libraries/classparser) # class parser library
|
add_subdirectory(libraries/classparser) # class parser library
|
||||||
add_subdirectory(libraries/optional-bare)
|
|
||||||
add_subdirectory(libraries/tomlc99) # toml parser
|
add_subdirectory(libraries/tomlc99) # toml parser
|
||||||
add_subdirectory(libraries/katabasis) # An OAuth2 library that tried to do too much
|
add_subdirectory(libraries/katabasis) # An OAuth2 library that tried to do too much
|
||||||
add_subdirectory(libraries/gamemode)
|
add_subdirectory(libraries/gamemode)
|
||||||
|
111
COPYING.md
111
COPYING.md
@ -32,27 +32,47 @@
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
|
|
||||||
# MinGW runtime (Windows)
|
# MinGW-w64 runtime (Windows)
|
||||||
|
|
||||||
Copyright (c) 2012 MinGW.org project
|
Copyright (c) 2009, 2010, 2011, 2012, 2013 by the mingw-w64 project
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a
|
This license has been certified as open source. It has also been designated
|
||||||
copy of this software and associated documentation files (the "Software"),
|
as GPL compatible by the Free Software Foundation (FSF).
|
||||||
to deal in the Software without restriction, including without limitation
|
|
||||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
||||||
and/or sell copies of the Software, and to permit persons to whom the
|
|
||||||
Software is furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice, this permission notice and the below disclaimer
|
Redistribution and use in source and binary forms, with or without
|
||||||
shall be included in all copies or substantial portions of the Software.
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
1. Redistributions in source code must retain the accompanying copyright
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
notice, this list of conditions, and the following disclaimer.
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
2. Redistributions in binary form must reproduce the accompanying
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
copyright notice, this list of conditions, and the following disclaimer
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
in the documentation and/or other materials provided with the
|
||||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
distribution.
|
||||||
DEALINGS IN THE SOFTWARE.
|
3. Names of the copyright holders must not be used to endorse or promote
|
||||||
|
products derived from this software without prior written permission
|
||||||
|
from the copyright holders.
|
||||||
|
4. The right to distribute this software or to use it for any purpose does
|
||||||
|
not give you the right to use Servicemarks (sm) or Trademarks (tm) of
|
||||||
|
the copyright holders. Use of them is covered by separate agreement
|
||||||
|
with the copyright holders.
|
||||||
|
5. If any files are modified, you must cause the modified files to carry
|
||||||
|
prominent notices stating that you changed the files and the date of
|
||||||
|
any change.
|
||||||
|
|
||||||
|
Disclaimer
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED
|
||||||
|
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
||||||
|
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
||||||
|
EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||||
|
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
|
||||||
|
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
||||||
|
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
Information on third party licenses used in MinGW-w64 can be found in its COPYING.MinGW-w64-runtime.txt.
|
||||||
|
|
||||||
# Qt 5/6
|
# Qt 5/6
|
||||||
|
|
||||||
@ -295,34 +315,6 @@
|
|||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|
||||||
# optional-bare
|
|
||||||
|
|
||||||
Code from https://github.com/martinmoene/optional-bare/
|
|
||||||
|
|
||||||
Boost Software License - Version 1.0 - August 17th, 2003
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person or organization
|
|
||||||
obtaining a copy of the software and accompanying documentation covered by
|
|
||||||
this license (the "Software") to use, reproduce, display, distribute,
|
|
||||||
execute, and transmit the Software, and to prepare derivative works of the
|
|
||||||
Software, and to permit third-parties to whom the Software is furnished to
|
|
||||||
do so, all subject to the following:
|
|
||||||
|
|
||||||
The copyright notices in the Software and this entire statement, including
|
|
||||||
the above license grant, this restriction and the following disclaimer,
|
|
||||||
must be included in all copies of the Software, in whole or in part, and
|
|
||||||
all derivative works of the Software, unless such copies or derivative
|
|
||||||
works are solely in the form of machine-executable object code generated by
|
|
||||||
a source language processor.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
|
||||||
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
|
||||||
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
|
||||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
||||||
DEALINGS IN THE SOFTWARE.
|
|
||||||
|
|
||||||
# tomlc99
|
# tomlc99
|
||||||
|
|
||||||
MIT License
|
MIT License
|
||||||
@ -373,3 +365,32 @@
|
|||||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
## Gamemode
|
||||||
|
|
||||||
|
Copyright (c) 2017-2022, Feral Interactive
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
* Neither the name of Feral Interactive nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this software
|
||||||
|
without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||||
|
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
@ -321,7 +321,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
|
|||||||
|
|
||||||
{
|
{
|
||||||
// Root path is used for updates and portable data
|
// Root path is used for updates and portable data
|
||||||
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
|
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
|
||||||
QDir foo(FS::PathCombine(binPath, "..")); // typically portable-root or /usr
|
QDir foo(FS::PathCombine(binPath, "..")); // typically portable-root or /usr
|
||||||
m_rootPath = foo.absolutePath();
|
m_rootPath = foo.absolutePath();
|
||||||
#elif defined(Q_OS_WIN32)
|
#elif defined(Q_OS_WIN32)
|
||||||
@ -864,6 +864,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
|
|||||||
m_metacache->addBase("ModpacksCHPacks", QDir("cache/ModpacksCHPacks").absolutePath());
|
m_metacache->addBase("ModpacksCHPacks", QDir("cache/ModpacksCHPacks").absolutePath());
|
||||||
m_metacache->addBase("TechnicPacks", QDir("cache/TechnicPacks").absolutePath());
|
m_metacache->addBase("TechnicPacks", QDir("cache/TechnicPacks").absolutePath());
|
||||||
m_metacache->addBase("FlamePacks", QDir("cache/FlamePacks").absolutePath());
|
m_metacache->addBase("FlamePacks", QDir("cache/FlamePacks").absolutePath());
|
||||||
|
m_metacache->addBase("FlameMods", QDir("cache/FlameMods").absolutePath());
|
||||||
m_metacache->addBase("ModrinthPacks", QDir("cache/ModrinthPacks").absolutePath());
|
m_metacache->addBase("ModrinthPacks", QDir("cache/ModrinthPacks").absolutePath());
|
||||||
m_metacache->addBase("root", QDir::currentPath());
|
m_metacache->addBase("root", QDir::currentPath());
|
||||||
m_metacache->addBase("translations", QDir("translations").absolutePath());
|
m_metacache->addBase("translations", QDir("translations").absolutePath());
|
||||||
@ -1258,6 +1259,9 @@ bool Application::launch(
|
|||||||
}
|
}
|
||||||
connect(controller.get(), &LaunchController::succeeded, this, &Application::controllerSucceeded);
|
connect(controller.get(), &LaunchController::succeeded, this, &Application::controllerSucceeded);
|
||||||
connect(controller.get(), &LaunchController::failed, this, &Application::controllerFailed);
|
connect(controller.get(), &LaunchController::failed, this, &Application::controllerFailed);
|
||||||
|
connect(controller.get(), &LaunchController::aborted, this, [this] {
|
||||||
|
controllerFailed(tr("Aborted"));
|
||||||
|
});
|
||||||
addRunningInstance();
|
addRunningInstance();
|
||||||
controller->start();
|
controller->start();
|
||||||
return true;
|
return true;
|
||||||
|
@ -494,6 +494,8 @@ set(API_SOURCES
|
|||||||
modplatform/modrinth/ModrinthAPI.cpp
|
modplatform/modrinth/ModrinthAPI.cpp
|
||||||
modplatform/helpers/NetworkModAPI.h
|
modplatform/helpers/NetworkModAPI.h
|
||||||
modplatform/helpers/NetworkModAPI.cpp
|
modplatform/helpers/NetworkModAPI.cpp
|
||||||
|
modplatform/helpers/HashUtils.h
|
||||||
|
modplatform/helpers/HashUtils.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
set(FTB_SOURCES
|
set(FTB_SOURCES
|
||||||
@ -988,7 +990,6 @@ target_link_libraries(Launcher_logic
|
|||||||
Launcher_murmur2
|
Launcher_murmur2
|
||||||
nbt++
|
nbt++
|
||||||
${ZLIB_LIBRARIES}
|
${ZLIB_LIBRARIES}
|
||||||
optional-bare
|
|
||||||
tomlc99
|
tomlc99
|
||||||
BuildConfig
|
BuildConfig
|
||||||
Katabasis
|
Katabasis
|
||||||
|
@ -516,6 +516,7 @@ bool overrideFolder(QString overwritten_path, QString override_path)
|
|||||||
for (auto file : listFolderPaths(root_override)) {
|
for (auto file : listFolderPaths(root_override)) {
|
||||||
QString destination = file;
|
QString destination = file;
|
||||||
destination.replace(override_path, overwritten_path);
|
destination.replace(override_path, overwritten_path);
|
||||||
|
ensureFilePathExists(destination);
|
||||||
|
|
||||||
qDebug() << QString("Applying override %1 in %2").arg(file, destination);
|
qDebug() << QString("Applying override %1 in %2").arg(file, destination);
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@
|
|||||||
#include "QObjectPtr.h"
|
#include "QObjectPtr.h"
|
||||||
#include "modplatform/flame/PackManifest.h"
|
#include "modplatform/flame/PackManifest.h"
|
||||||
|
|
||||||
#include <nonstd/optional>
|
#include <optional>
|
||||||
|
|
||||||
class QuaZip;
|
class QuaZip;
|
||||||
namespace Flame
|
namespace Flame
|
||||||
@ -90,8 +90,8 @@ private: /* data */
|
|||||||
QString m_archivePath;
|
QString m_archivePath;
|
||||||
bool m_downloadRequired = false;
|
bool m_downloadRequired = false;
|
||||||
std::unique_ptr<QuaZip> m_packZip;
|
std::unique_ptr<QuaZip> m_packZip;
|
||||||
QFuture<nonstd::optional<QStringList>> m_extractFuture;
|
QFuture<std::optional<QStringList>> m_extractFuture;
|
||||||
QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
|
QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
|
||||||
QVector<Flame::File> m_blockedMods;
|
QVector<Flame::File> m_blockedMods;
|
||||||
enum class ModpackType{
|
enum class ModpackType{
|
||||||
Unknown,
|
Unknown,
|
||||||
|
@ -145,16 +145,26 @@ void LaunchController::login() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// we try empty password first :)
|
|
||||||
QString password;
|
|
||||||
// we loop until the user succeeds in logging in or gives up
|
// we loop until the user succeeds in logging in or gives up
|
||||||
bool tryagain = true;
|
bool tryagain = true;
|
||||||
// the failure. the default failure.
|
unsigned int tries = 0;
|
||||||
const QString needLoginAgain = tr("Your account is currently not logged in. Please enter your password to log in again. <br /> <br /> This could be caused by a password change.");
|
|
||||||
QString failReason = needLoginAgain;
|
|
||||||
|
|
||||||
while (tryagain)
|
while (tryagain)
|
||||||
{
|
{
|
||||||
|
if (tries > 0 && tries % 3 == 0) {
|
||||||
|
auto result = QMessageBox::question(
|
||||||
|
m_parentWidget,
|
||||||
|
tr("Continue launch?"),
|
||||||
|
tr("It looks like we couldn't launch after %1 tries. Do you want to continue trying?")
|
||||||
|
.arg(tries)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result == QMessageBox::No) {
|
||||||
|
emitAborted();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tries++;
|
||||||
m_session = std::make_shared<AuthSession>();
|
m_session = std::make_shared<AuthSession>();
|
||||||
m_session->wants_online = m_online;
|
m_session->wants_online = m_online;
|
||||||
m_accountToUse->fillSession(m_session);
|
m_accountToUse->fillSession(m_session);
|
||||||
|
@ -34,8 +34,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "LoggedProcess.h"
|
#include "LoggedProcess.h"
|
||||||
#include "MessageLevel.h"
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QTextDecoder>
|
||||||
|
#include "MessageLevel.h"
|
||||||
|
|
||||||
LoggedProcess::LoggedProcess(QObject *parent) : QProcess(parent)
|
LoggedProcess::LoggedProcess(QObject *parent) : QProcess(parent)
|
||||||
{
|
{
|
||||||
@ -59,25 +60,26 @@ LoggedProcess::~LoggedProcess()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList reprocess(const QByteArray & data, QString & leftover)
|
QStringList reprocess(const QByteArray& data, QTextDecoder& decoder)
|
||||||
{
|
{
|
||||||
QString str = leftover + QString::fromLocal8Bit(data);
|
auto str = decoder.toUnicode(data);
|
||||||
|
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
|
||||||
str.remove('\r');
|
auto lines = str.remove(QChar::CarriageReturn).split(QChar::LineFeed, QString::SkipEmptyParts);
|
||||||
QStringList lines = str.split("\n");
|
#else
|
||||||
leftover = lines.takeLast();
|
auto lines = str.remove(QChar::CarriageReturn).split(QChar::LineFeed, Qt::SkipEmptyParts);
|
||||||
|
#endif
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
void LoggedProcess::on_stdErr()
|
void LoggedProcess::on_stdErr()
|
||||||
{
|
{
|
||||||
auto lines = reprocess(readAllStandardError(), m_err_leftover);
|
auto lines = reprocess(readAllStandardError(), m_err_decoder);
|
||||||
emit log(lines, MessageLevel::StdErr);
|
emit log(lines, MessageLevel::StdErr);
|
||||||
}
|
}
|
||||||
|
|
||||||
void LoggedProcess::on_stdOut()
|
void LoggedProcess::on_stdOut()
|
||||||
{
|
{
|
||||||
auto lines = reprocess(readAllStandardOutput(), m_out_leftover);
|
auto lines = reprocess(readAllStandardOutput(), m_out_decoder);
|
||||||
emit log(lines, MessageLevel::StdOut);
|
emit log(lines, MessageLevel::StdOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,18 +88,6 @@ void LoggedProcess::on_exit(int exit_code, QProcess::ExitStatus status)
|
|||||||
// save the exit code
|
// save the exit code
|
||||||
m_exit_code = exit_code;
|
m_exit_code = exit_code;
|
||||||
|
|
||||||
// Flush console window
|
|
||||||
if (!m_err_leftover.isEmpty())
|
|
||||||
{
|
|
||||||
emit log({m_err_leftover}, MessageLevel::StdErr);
|
|
||||||
m_err_leftover.clear();
|
|
||||||
}
|
|
||||||
if (!m_out_leftover.isEmpty())
|
|
||||||
{
|
|
||||||
emit log({m_err_leftover}, MessageLevel::StdOut);
|
|
||||||
m_out_leftover.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// based on state, send signals
|
// based on state, send signals
|
||||||
if (!m_is_aborting)
|
if (!m_is_aborting)
|
||||||
{
|
{
|
||||||
|
@ -36,6 +36,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QProcess>
|
#include <QProcess>
|
||||||
|
#include <QTextDecoder>
|
||||||
#include "MessageLevel.h"
|
#include "MessageLevel.h"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -88,8 +89,8 @@ private:
|
|||||||
void changeState(LoggedProcess::State state);
|
void changeState(LoggedProcess::State state);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString m_err_leftover;
|
QTextDecoder m_err_decoder = QTextDecoder(QTextCodec::codecForLocale());
|
||||||
QString m_out_leftover;
|
QTextDecoder m_out_decoder = QTextDecoder(QTextCodec::codecForLocale());
|
||||||
bool m_killed = false;
|
bool m_killed = false;
|
||||||
State m_state = NotRunning;
|
State m_state = NotRunning;
|
||||||
int m_exit_code = 0;
|
int m_exit_code = 0;
|
||||||
|
@ -268,7 +268,7 @@ bool MMCZip::findFilesInZip(QuaZip * zip, const QString & what, QStringList & re
|
|||||||
|
|
||||||
|
|
||||||
// ours
|
// ours
|
||||||
nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target)
|
std::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target)
|
||||||
{
|
{
|
||||||
QDir directory(target);
|
QDir directory(target);
|
||||||
QStringList extracted;
|
QStringList extracted;
|
||||||
@ -277,7 +277,7 @@ nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString &
|
|||||||
auto numEntries = zip->getEntriesCount();
|
auto numEntries = zip->getEntriesCount();
|
||||||
if(numEntries < 0) {
|
if(numEntries < 0) {
|
||||||
qWarning() << "Failed to enumerate files in archive";
|
qWarning() << "Failed to enumerate files in archive";
|
||||||
return nonstd::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
else if(numEntries == 0) {
|
else if(numEntries == 0) {
|
||||||
qDebug() << "Extracting empty archives seems odd...";
|
qDebug() << "Extracting empty archives seems odd...";
|
||||||
@ -286,7 +286,7 @@ nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString &
|
|||||||
else if (!zip->goToFirstFile())
|
else if (!zip->goToFirstFile())
|
||||||
{
|
{
|
||||||
qWarning() << "Failed to seek to first file in zip";
|
qWarning() << "Failed to seek to first file in zip";
|
||||||
return nonstd::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
do
|
do
|
||||||
@ -323,7 +323,7 @@ nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString &
|
|||||||
{
|
{
|
||||||
qWarning() << "Failed to extract file" << original_name << "to" << absFilePath;
|
qWarning() << "Failed to extract file" << original_name << "to" << absFilePath;
|
||||||
JlCompress::removeFile(extracted);
|
JlCompress::removeFile(extracted);
|
||||||
return nonstd::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
extracted.append(absFilePath);
|
extracted.append(absFilePath);
|
||||||
@ -341,7 +341,7 @@ bool MMCZip::extractRelFile(QuaZip *zip, const QString &file, const QString &tar
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ours
|
// ours
|
||||||
nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString dir)
|
std::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString dir)
|
||||||
{
|
{
|
||||||
QuaZip zip(fileCompressed);
|
QuaZip zip(fileCompressed);
|
||||||
if (!zip.open(QuaZip::mdUnzip))
|
if (!zip.open(QuaZip::mdUnzip))
|
||||||
@ -352,13 +352,13 @@ nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString
|
|||||||
return QStringList();
|
return QStringList();
|
||||||
}
|
}
|
||||||
qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();;
|
qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();;
|
||||||
return nonstd::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
return MMCZip::extractSubDir(&zip, "", dir);
|
return MMCZip::extractSubDir(&zip, "", dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ours
|
// ours
|
||||||
nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString subdir, QString dir)
|
std::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString subdir, QString dir)
|
||||||
{
|
{
|
||||||
QuaZip zip(fileCompressed);
|
QuaZip zip(fileCompressed);
|
||||||
if (!zip.open(QuaZip::mdUnzip))
|
if (!zip.open(QuaZip::mdUnzip))
|
||||||
@ -369,7 +369,7 @@ nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString
|
|||||||
return QStringList();
|
return QStringList();
|
||||||
}
|
}
|
||||||
qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();;
|
qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();;
|
||||||
return nonstd::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
return MMCZip::extractSubDir(&zip, subdir, dir);
|
return MMCZip::extractSubDir(&zip, subdir, dir);
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
#include <quazip/JlCompress.h>
|
#include <quazip/JlCompress.h>
|
||||||
#include <nonstd/optional>
|
#include <optional>
|
||||||
|
|
||||||
namespace MMCZip
|
namespace MMCZip
|
||||||
{
|
{
|
||||||
@ -95,7 +95,7 @@ namespace MMCZip
|
|||||||
/**
|
/**
|
||||||
* Extract a subdirectory from an archive
|
* Extract a subdirectory from an archive
|
||||||
*/
|
*/
|
||||||
nonstd::optional<QStringList> extractSubDir(QuaZip *zip, const QString & subdir, const QString &target);
|
std::optional<QStringList> extractSubDir(QuaZip *zip, const QString & subdir, const QString &target);
|
||||||
|
|
||||||
bool extractRelFile(QuaZip *zip, const QString & file, const QString &target);
|
bool extractRelFile(QuaZip *zip, const QString & file, const QString &target);
|
||||||
|
|
||||||
@ -106,7 +106,7 @@ namespace MMCZip
|
|||||||
* \param dir The directory to extract to, the current directory if left empty.
|
* \param dir The directory to extract to, the current directory if left empty.
|
||||||
* \return The list of the full paths of the files extracted, empty on failure.
|
* \return The list of the full paths of the files extracted, empty on failure.
|
||||||
*/
|
*/
|
||||||
nonstd::optional<QStringList> extractDir(QString fileCompressed, QString dir);
|
std::optional<QStringList> extractDir(QString fileCompressed, QString dir);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract a subdirectory from an archive
|
* Extract a subdirectory from an archive
|
||||||
@ -116,7 +116,7 @@ namespace MMCZip
|
|||||||
* \param dir The directory to extract to, the current directory if left empty.
|
* \param dir The directory to extract to, the current directory if left empty.
|
||||||
* \return The list of the full paths of the files extracted, empty on failure.
|
* \return The list of the full paths of the files extracted, empty on failure.
|
||||||
*/
|
*/
|
||||||
nonstd::optional<QStringList> extractDir(QString fileCompressed, QString subdir, QString dir);
|
std::optional<QStringList> extractDir(QString fileCompressed, QString subdir, QString dir);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract a single file from an archive into a directory
|
* Extract a single file from an archive into a directory
|
||||||
|
@ -140,6 +140,13 @@ VersionPtr VersionList::getVersion(const QString &version)
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool VersionList::hasVersion(QString version) const
|
||||||
|
{
|
||||||
|
auto ver = std::find_if(m_versions.constBegin(), m_versions.constEnd(),
|
||||||
|
[&](Meta::VersionPtr const& a){ return a->version() == version; });
|
||||||
|
return (ver != m_versions.constEnd());
|
||||||
|
}
|
||||||
|
|
||||||
void VersionList::setName(const QString &name)
|
void VersionList::setName(const QString &name)
|
||||||
{
|
{
|
||||||
m_name = name;
|
m_name = name;
|
||||||
|
@ -66,6 +66,7 @@ public:
|
|||||||
QString humanReadable() const;
|
QString humanReadable() const;
|
||||||
|
|
||||||
VersionPtr getVersion(const QString &version);
|
VersionPtr getVersion(const QString &version);
|
||||||
|
bool hasVersion(QString version) const;
|
||||||
|
|
||||||
QVector<VersionPtr> versions() const
|
QVector<VersionPtr> versions() const
|
||||||
{
|
{
|
||||||
|
@ -88,6 +88,9 @@ QList<NetAction::Ptr> Library::getDownloads(
|
|||||||
options |= Net::Download::Option::AcceptLocalFiles;
|
options |= Net::Download::Option::AcceptLocalFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't add a time limit for the libraries cache entry validity
|
||||||
|
options |= Net::Download::Option::MakeEternal;
|
||||||
|
|
||||||
if(sha1.size())
|
if(sha1.size())
|
||||||
{
|
{
|
||||||
auto rawSha1 = QByteArray::fromHex(sha1.toLatin1());
|
auto rawSha1 = QByteArray::fromHex(sha1.toLatin1());
|
||||||
|
@ -53,12 +53,12 @@
|
|||||||
|
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
|
|
||||||
#include <nonstd/optional>
|
#include <optional>
|
||||||
|
|
||||||
using nonstd::optional;
|
using std::optional;
|
||||||
using nonstd::nullopt;
|
using std::nullopt;
|
||||||
|
|
||||||
GameType::GameType(nonstd::optional<int> original):
|
GameType::GameType(std::optional<int> original):
|
||||||
original(original)
|
original(original)
|
||||||
{
|
{
|
||||||
if(!original) {
|
if(!original) {
|
||||||
|
@ -16,11 +16,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <nonstd/optional>
|
#include <optional>
|
||||||
|
|
||||||
struct GameType {
|
struct GameType {
|
||||||
GameType() = default;
|
GameType() = default;
|
||||||
GameType (nonstd::optional<int> original);
|
GameType (std::optional<int> original);
|
||||||
|
|
||||||
QString toTranslatedString() const;
|
QString toTranslatedString() const;
|
||||||
QString toLogString() const;
|
QString toLogString() const;
|
||||||
@ -33,7 +33,7 @@ struct GameType {
|
|||||||
Adventure,
|
Adventure,
|
||||||
Spectator
|
Spectator
|
||||||
} type = Unknown;
|
} type = Unknown;
|
||||||
nonstd::optional<int> original;
|
std::optional<int> original;
|
||||||
};
|
};
|
||||||
|
|
||||||
class World
|
class World
|
||||||
|
@ -63,6 +63,9 @@ void ModFolderModel::startWatching()
|
|||||||
if(is_watching)
|
if(is_watching)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Remove orphaned metadata next time
|
||||||
|
m_first_folder_load = true;
|
||||||
|
|
||||||
update();
|
update();
|
||||||
|
|
||||||
// Watch the mods folder
|
// Watch the mods folder
|
||||||
@ -113,7 +116,8 @@ bool ModFolderModel::update()
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto index_dir = indexDir();
|
auto index_dir = indexDir();
|
||||||
auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed);
|
auto task = new ModFolderLoadTask(dir(), index_dir, m_is_indexed, m_first_folder_load);
|
||||||
|
m_first_folder_load = false;
|
||||||
|
|
||||||
m_update = task->result();
|
m_update = task->result();
|
||||||
|
|
||||||
|
@ -172,6 +172,7 @@ protected:
|
|||||||
bool interaction_disabled = false;
|
bool interaction_disabled = false;
|
||||||
QDir m_dir;
|
QDir m_dir;
|
||||||
bool m_is_indexed;
|
bool m_is_indexed;
|
||||||
|
bool m_first_folder_load = true;
|
||||||
QMap<QString, int> modsIndex;
|
QMap<QString, int> modsIndex;
|
||||||
QMap<int, LocalModParseTask::ResultPtr> activeTickets;
|
QMap<int, LocalModParseTask::ResultPtr> activeTickets;
|
||||||
int nextResolutionTicket = 0;
|
int nextResolutionTicket = 0;
|
||||||
|
@ -38,8 +38,8 @@
|
|||||||
|
|
||||||
#include "minecraft/mod/MetadataHandler.h"
|
#include "minecraft/mod/MetadataHandler.h"
|
||||||
|
|
||||||
ModFolderLoadTask::ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed)
|
ModFolderLoadTask::ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed, bool clean_orphan)
|
||||||
: m_mods_dir(mods_dir), m_index_dir(index_dir), m_is_indexed(is_indexed), m_result(new Result())
|
: m_mods_dir(mods_dir), m_index_dir(index_dir), m_is_indexed(is_indexed), m_clean_orphan(clean_orphan), m_result(new Result())
|
||||||
{}
|
{}
|
||||||
|
|
||||||
void ModFolderLoadTask::run()
|
void ModFolderLoadTask::run()
|
||||||
@ -83,6 +83,19 @@ void ModFolderLoadTask::run()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove orphan metadata to prevent issues
|
||||||
|
// See https://github.com/PolyMC/PolyMC/issues/996
|
||||||
|
if (m_clean_orphan) {
|
||||||
|
QMutableMapIterator<QString, Mod::Ptr> iter(m_result->mods);
|
||||||
|
while (iter.hasNext()) {
|
||||||
|
auto mod = iter.next().value();
|
||||||
|
if (mod->status() == ModStatus::NotInstalled) {
|
||||||
|
mod->destroy(m_index_dir, false);
|
||||||
|
iter.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
emit succeeded();
|
emit succeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed);
|
ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed, bool clean_orphan = false);
|
||||||
void run();
|
void run();
|
||||||
signals:
|
signals:
|
||||||
void succeeded();
|
void succeeded();
|
||||||
@ -67,5 +67,6 @@ private:
|
|||||||
private:
|
private:
|
||||||
QDir& m_mods_dir, m_index_dir;
|
QDir& m_mods_dir, m_index_dir;
|
||||||
bool m_is_indexed;
|
bool m_is_indexed;
|
||||||
|
bool m_clean_orphan;
|
||||||
ResultPtr m_result;
|
ResultPtr m_result;
|
||||||
};
|
};
|
||||||
|
@ -63,11 +63,12 @@ void FMLLibrariesTask::executeTask()
|
|||||||
setStatus(tr("Downloading FML libraries..."));
|
setStatus(tr("Downloading FML libraries..."));
|
||||||
auto dljob = new NetJob("FML libraries", APPLICATION->network());
|
auto dljob = new NetJob("FML libraries", APPLICATION->network());
|
||||||
auto metacache = APPLICATION->metacache();
|
auto metacache = APPLICATION->metacache();
|
||||||
|
Net::Download::Options options = Net::Download::Option::MakeEternal;
|
||||||
for (auto &lib : fmlLibsToProcess)
|
for (auto &lib : fmlLibsToProcess)
|
||||||
{
|
{
|
||||||
auto entry = metacache->resolveEntry("fmllibs", lib.filename);
|
auto entry = metacache->resolveEntry("fmllibs", lib.filename);
|
||||||
QString urlString = BuildConfig.FMLLIBS_BASE_URL + lib.filename;
|
QString urlString = BuildConfig.FMLLIBS_BASE_URL + lib.filename;
|
||||||
dljob->addNetAction(Net::Download::makeCached(QUrl(urlString), entry));
|
dljob->addNetAction(Net::Download::makeCached(QUrl(urlString), entry, options));
|
||||||
}
|
}
|
||||||
|
|
||||||
connect(dljob, &NetJob::succeeded, this, &FMLLibrariesTask::fmllibsFinished);
|
connect(dljob, &NetJob::succeeded, this, &FMLLibrariesTask::fmllibsFinished);
|
||||||
|
@ -3,81 +3,73 @@
|
|||||||
#include <MurmurHash2.h>
|
#include <MurmurHash2.h>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
|
||||||
#include "FileSystem.h"
|
|
||||||
#include "Json.h"
|
#include "Json.h"
|
||||||
|
|
||||||
#include "minecraft/mod/Mod.h"
|
#include "minecraft/mod/Mod.h"
|
||||||
#include "minecraft/mod/tasks/LocalModUpdateTask.h"
|
#include "minecraft/mod/tasks/LocalModUpdateTask.h"
|
||||||
|
|
||||||
#include "modplatform/flame/FlameAPI.h"
|
#include "modplatform/flame/FlameAPI.h"
|
||||||
#include "modplatform/flame/FlameModIndex.h"
|
#include "modplatform/flame/FlameModIndex.h"
|
||||||
#include "modplatform/modrinth/ModrinthAPI.h"
|
#include "modplatform/modrinth/ModrinthAPI.h"
|
||||||
#include "modplatform/modrinth/ModrinthPackIndex.h"
|
#include "modplatform/modrinth/ModrinthPackIndex.h"
|
||||||
|
|
||||||
#include "net/NetJob.h"
|
#include "net/NetJob.h"
|
||||||
#include "tasks/MultipleOptionsTask.h"
|
|
||||||
|
|
||||||
static ModPlatform::ProviderCapabilities ProviderCaps;
|
static ModPlatform::ProviderCapabilities ProviderCaps;
|
||||||
|
|
||||||
static ModrinthAPI modrinth_api;
|
static ModrinthAPI modrinth_api;
|
||||||
static FlameAPI flame_api;
|
static FlameAPI flame_api;
|
||||||
|
|
||||||
EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::Provider prov) : Task(nullptr), m_index_dir(dir), m_provider(prov)
|
EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::Provider prov)
|
||||||
|
: Task(nullptr), m_index_dir(dir), m_provider(prov), m_hashing_task(nullptr), m_current_task(nullptr)
|
||||||
{
|
{
|
||||||
auto hash = getHash(mod);
|
auto hash_task = createNewHash(mod);
|
||||||
if (hash.isEmpty())
|
if (!hash_task)
|
||||||
emitFail(mod);
|
return;
|
||||||
else
|
connect(hash_task.get(), &Task::succeeded, [this, hash_task, mod] { m_mods.insert(hash_task->getResult(), mod); });
|
||||||
m_mods.insert(hash, mod);
|
connect(hash_task.get(), &Task::failed, [this, hash_task, mod] { emitFail(mod, "", RemoveFromList::No); });
|
||||||
|
hash_task->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
EnsureMetadataTask::EnsureMetadataTask(QList<Mod*>& mods, QDir dir, ModPlatform::Provider prov)
|
EnsureMetadataTask::EnsureMetadataTask(QList<Mod*>& mods, QDir dir, ModPlatform::Provider prov)
|
||||||
: Task(nullptr), m_index_dir(dir), m_provider(prov)
|
: Task(nullptr), m_index_dir(dir), m_provider(prov), m_current_task(nullptr)
|
||||||
{
|
{
|
||||||
|
m_hashing_task = new ConcurrentTask(this, "MakeHashesTask", 10);
|
||||||
for (auto* mod : mods) {
|
for (auto* mod : mods) {
|
||||||
if (!mod->valid()) {
|
auto hash_task = createNewHash(mod);
|
||||||
emitFail(mod);
|
if (!hash_task)
|
||||||
continue;
|
continue;
|
||||||
}
|
connect(hash_task.get(), &Task::succeeded, [this, hash_task, mod] { m_mods.insert(hash_task->getResult(), mod); });
|
||||||
|
connect(hash_task.get(), &Task::failed, [this, hash_task, mod] { emitFail(mod, "", RemoveFromList::No); });
|
||||||
auto hash = getHash(mod);
|
m_hashing_task->addTask(hash_task);
|
||||||
if (hash.isEmpty()) {
|
|
||||||
emitFail(mod);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_mods.insert(hash, mod);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QString EnsureMetadataTask::getHash(Mod* mod)
|
Hashing::Hasher::Ptr EnsureMetadataTask::createNewHash(Mod* mod)
|
||||||
{
|
{
|
||||||
/* Here we create a mapping hash -> mod, because we need that relationship to parse the API routes */
|
if (!mod || !mod->valid() || mod->type() == Mod::MOD_FOLDER)
|
||||||
QByteArray jar_data;
|
return nullptr;
|
||||||
try {
|
|
||||||
jar_data = FS::read(mod->fileinfo().absoluteFilePath());
|
|
||||||
} catch (FS::FileSystemException& e) {
|
|
||||||
qCritical() << QString("Failed to open / read JAR file of %1").arg(mod->name());
|
|
||||||
qCritical() << QString("Reason: ") << e.cause();
|
|
||||||
|
|
||||||
return {};
|
return Hashing::createHasher(mod->fileinfo().absoluteFilePath(), m_provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString EnsureMetadataTask::getExistingHash(Mod* mod)
|
||||||
|
{
|
||||||
|
// Check for already computed hashes
|
||||||
|
// (linear on the number of mods vs. linear on the size of the mod's JAR)
|
||||||
|
auto it = m_mods.keyValueBegin();
|
||||||
|
while (it != m_mods.keyValueEnd()) {
|
||||||
|
if ((*it).second == mod)
|
||||||
|
break;
|
||||||
|
it++;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (m_provider) {
|
// We already have the hash computed
|
||||||
case ModPlatform::Provider::MODRINTH: {
|
if (it != m_mods.keyValueEnd()) {
|
||||||
auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
|
return (*it).first;
|
||||||
|
|
||||||
return QString(ProviderCaps.hash(ModPlatform::Provider::MODRINTH, jar_data, hash_type).toHex());
|
|
||||||
}
|
|
||||||
case ModPlatform::Provider::FLAME: {
|
|
||||||
QByteArray jar_data_treated;
|
|
||||||
for (char c : jar_data) {
|
|
||||||
// CF-specific
|
|
||||||
if (!(c == 9 || c == 10 || c == 13 || c == 32))
|
|
||||||
jar_data_treated.push_back(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
return QString::number(MurmurHash2(jar_data_treated, jar_data_treated.length()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No existing hash
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,11 +119,9 @@ void EnsureMetadataTask::executeTask()
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto invalidade_leftover = [this] {
|
auto invalidade_leftover = [this] {
|
||||||
QMutableHashIterator<QString, Mod*> mods_iter(m_mods);
|
for (auto mod = m_mods.constBegin(); mod != m_mods.constEnd(); mod++)
|
||||||
while (mods_iter.hasNext()) {
|
emitFail(mod.value(), mod.key(), RemoveFromList::No);
|
||||||
auto mod = mods_iter.next();
|
m_mods.clear();
|
||||||
emitFail(mod.value());
|
|
||||||
}
|
|
||||||
|
|
||||||
emitSucceeded();
|
emitSucceeded();
|
||||||
};
|
};
|
||||||
@ -178,20 +168,44 @@ void EnsureMetadataTask::executeTask()
|
|||||||
version_task->start();
|
version_task->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
void EnsureMetadataTask::emitReady(Mod* m)
|
void EnsureMetadataTask::emitReady(Mod* m, QString key, RemoveFromList remove)
|
||||||
{
|
{
|
||||||
|
if (!m) {
|
||||||
|
qCritical() << "Tried to mark a null mod as ready.";
|
||||||
|
if (!key.isEmpty())
|
||||||
|
m_mods.remove(key);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
qDebug() << QString("Generated metadata for %1").arg(m->name());
|
qDebug() << QString("Generated metadata for %1").arg(m->name());
|
||||||
emit metadataReady(m);
|
emit metadataReady(m);
|
||||||
|
|
||||||
m_mods.remove(getHash(m));
|
if (remove == RemoveFromList::Yes) {
|
||||||
|
if (key.isEmpty())
|
||||||
|
key = getExistingHash(m);
|
||||||
|
m_mods.remove(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void EnsureMetadataTask::emitFail(Mod* m)
|
void EnsureMetadataTask::emitFail(Mod* m, QString key, RemoveFromList remove)
|
||||||
{
|
{
|
||||||
|
if (!m) {
|
||||||
|
qCritical() << "Tried to mark a null mod as failed.";
|
||||||
|
if (!key.isEmpty())
|
||||||
|
m_mods.remove(key);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
qDebug() << QString("Failed to generate metadata for %1").arg(m->name());
|
qDebug() << QString("Failed to generate metadata for %1").arg(m->name());
|
||||||
emit metadataFailed(m);
|
emit metadataFailed(m);
|
||||||
|
|
||||||
m_mods.remove(getHash(m));
|
if (remove == RemoveFromList::Yes) {
|
||||||
|
if (key.isEmpty())
|
||||||
|
key = getExistingHash(m);
|
||||||
|
m_mods.remove(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modrinth
|
// Modrinth
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "ModIndex.h"
|
#include "ModIndex.h"
|
||||||
#include "tasks/SequentialTask.h"
|
|
||||||
#include "net/NetJob.h"
|
#include "net/NetJob.h"
|
||||||
|
|
||||||
|
#include "modplatform/helpers/HashUtils.h"
|
||||||
|
|
||||||
|
#include "tasks/ConcurrentTask.h"
|
||||||
|
|
||||||
class Mod;
|
class Mod;
|
||||||
class QDir;
|
class QDir;
|
||||||
class MultipleOptionsTask;
|
|
||||||
|
|
||||||
class EnsureMetadataTask : public Task {
|
class EnsureMetadataTask : public Task {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@ -17,6 +19,8 @@ class EnsureMetadataTask : public Task {
|
|||||||
|
|
||||||
~EnsureMetadataTask() = default;
|
~EnsureMetadataTask() = default;
|
||||||
|
|
||||||
|
Task::Ptr getHashingTask() { return m_hashing_task; }
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
bool abort() override;
|
bool abort() override;
|
||||||
protected slots:
|
protected slots:
|
||||||
@ -31,10 +35,16 @@ class EnsureMetadataTask : public Task {
|
|||||||
auto flameProjectsTask() -> NetJob::Ptr;
|
auto flameProjectsTask() -> NetJob::Ptr;
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
void emitReady(Mod*);
|
enum class RemoveFromList {
|
||||||
void emitFail(Mod*);
|
Yes,
|
||||||
|
No
|
||||||
|
};
|
||||||
|
void emitReady(Mod*, QString key = {}, RemoveFromList = RemoveFromList::Yes);
|
||||||
|
void emitFail(Mod*, QString key = {}, RemoveFromList = RemoveFromList::Yes);
|
||||||
|
|
||||||
auto getHash(Mod*) -> QString;
|
// Hashes and stuff
|
||||||
|
auto createNewHash(Mod*) -> Hashing::Hasher::Ptr;
|
||||||
|
auto getExistingHash(Mod*) -> QString;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod*);
|
void modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod*);
|
||||||
@ -50,5 +60,6 @@ class EnsureMetadataTask : public Task {
|
|||||||
ModPlatform::Provider m_provider;
|
ModPlatform::Provider m_provider;
|
||||||
|
|
||||||
QHash<QString, ModPlatform::IndexedVersion> m_temp_versions;
|
QHash<QString, ModPlatform::IndexedVersion> m_temp_versions;
|
||||||
|
ConcurrentTask* m_hashing_task;
|
||||||
NetJob* m_current_task;
|
NetJob* m_current_task;
|
||||||
};
|
};
|
||||||
|
@ -19,6 +19,8 @@
|
|||||||
#include "modplatform/ModIndex.h"
|
#include "modplatform/ModIndex.h"
|
||||||
|
|
||||||
#include <QCryptographicHash>
|
#include <QCryptographicHash>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QIODevice>
|
||||||
|
|
||||||
namespace ModPlatform {
|
namespace ModPlatform {
|
||||||
|
|
||||||
@ -53,34 +55,26 @@ auto ProviderCapabilities::hashType(Provider p) -> QStringList
|
|||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
auto ProviderCapabilities::hash(Provider p, QByteArray& data, QString type) -> QByteArray
|
|
||||||
|
auto ProviderCapabilities::hash(Provider p, QIODevice* device, QString type) -> QString
|
||||||
{
|
{
|
||||||
|
QCryptographicHash::Algorithm algo = QCryptographicHash::Sha1;
|
||||||
switch (p) {
|
switch (p) {
|
||||||
case Provider::MODRINTH: {
|
case Provider::MODRINTH: {
|
||||||
// NOTE: Data is the result of reading the entire JAR file!
|
algo = (type == "sha1") ? QCryptographicHash::Sha1 : QCryptographicHash::Sha512;
|
||||||
|
|
||||||
// If 'type' was specified, we use that
|
|
||||||
if (!type.isEmpty() && hashType(p).contains(type)) {
|
|
||||||
if (type == "sha512")
|
|
||||||
return QCryptographicHash::hash(data, QCryptographicHash::Sha512);
|
|
||||||
else if (type == "sha1")
|
|
||||||
return QCryptographicHash::hash(data, QCryptographicHash::Sha1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return QCryptographicHash::hash(data, QCryptographicHash::Sha512);
|
|
||||||
}
|
|
||||||
case Provider::FLAME:
|
|
||||||
// If 'type' was specified, we use that
|
|
||||||
if (!type.isEmpty() && hashType(p).contains(type)) {
|
|
||||||
if(type == "sha1")
|
|
||||||
return QCryptographicHash::hash(data, QCryptographicHash::Sha1);
|
|
||||||
else if (type == "md5")
|
|
||||||
return QCryptographicHash::hash(data, QCryptographicHash::Md5);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return {};
|
case Provider::FLAME:
|
||||||
|
algo = (type == "sha1") ? QCryptographicHash::Sha1 : QCryptographicHash::Md5;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
QCryptographicHash hash(algo);
|
||||||
|
if(!hash.addData(device))
|
||||||
|
qCritical() << "Failed to read JAR to create hash!";
|
||||||
|
|
||||||
|
Q_ASSERT(hash.result().length() == hash.hashLength(algo));
|
||||||
|
return { hash.result().toHex() };
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace ModPlatform
|
} // namespace ModPlatform
|
||||||
|
@ -24,6 +24,8 @@
|
|||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
#include <QVector>
|
#include <QVector>
|
||||||
|
|
||||||
|
class QIODevice;
|
||||||
|
|
||||||
namespace ModPlatform {
|
namespace ModPlatform {
|
||||||
|
|
||||||
enum class Provider {
|
enum class Provider {
|
||||||
@ -36,7 +38,7 @@ class ProviderCapabilities {
|
|||||||
auto name(Provider) -> const char*;
|
auto name(Provider) -> const char*;
|
||||||
auto readableName(Provider) -> QString;
|
auto readableName(Provider) -> QString;
|
||||||
auto hashType(Provider) -> QStringList;
|
auto hashType(Provider) -> QStringList;
|
||||||
auto hash(Provider, QByteArray&, QString type = "") -> QByteArray;
|
auto hash(Provider, QIODevice*, QString type = "") -> QString;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ModpackAuthor {
|
struct ModpackAuthor {
|
||||||
|
@ -46,7 +46,7 @@
|
|||||||
#include "minecraft/PackProfile.h"
|
#include "minecraft/PackProfile.h"
|
||||||
#include "meta/Version.h"
|
#include "meta/Version.h"
|
||||||
|
|
||||||
#include <nonstd/optional>
|
#include <optional>
|
||||||
|
|
||||||
namespace ATLauncher {
|
namespace ATLauncher {
|
||||||
|
|
||||||
@ -131,8 +131,8 @@ private:
|
|||||||
Meta::VersionPtr minecraftVersion;
|
Meta::VersionPtr minecraftVersion;
|
||||||
QMap<QString, Meta::VersionPtr> componentsToInstall;
|
QMap<QString, Meta::VersionPtr> componentsToInstall;
|
||||||
|
|
||||||
QFuture<nonstd::optional<QStringList>> m_extractFuture;
|
QFuture<std::optional<QStringList>> m_extractFuture;
|
||||||
QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
|
QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
|
||||||
|
|
||||||
QFuture<bool> m_modExtractFuture;
|
QFuture<bool> m_modExtractFuture;
|
||||||
QFutureWatcher<bool> m_modExtractFutureWatcher;
|
QFutureWatcher<bool> m_modExtractFutureWatcher;
|
||||||
|
81
launcher/modplatform/helpers/HashUtils.cpp
Normal file
81
launcher/modplatform/helpers/HashUtils.cpp
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
#include "HashUtils.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QFile>
|
||||||
|
|
||||||
|
#include "FileSystem.h"
|
||||||
|
|
||||||
|
#include <MurmurHash2.h>
|
||||||
|
|
||||||
|
namespace Hashing {
|
||||||
|
|
||||||
|
static ModPlatform::ProviderCapabilities ProviderCaps;
|
||||||
|
|
||||||
|
Hasher::Ptr createHasher(QString file_path, ModPlatform::Provider provider)
|
||||||
|
{
|
||||||
|
switch (provider) {
|
||||||
|
case ModPlatform::Provider::MODRINTH:
|
||||||
|
return createModrinthHasher(file_path);
|
||||||
|
case ModPlatform::Provider::FLAME:
|
||||||
|
return createFlameHasher(file_path);
|
||||||
|
default:
|
||||||
|
qCritical() << "[Hashing]"
|
||||||
|
<< "Unrecognized mod platform!";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Hasher::Ptr createModrinthHasher(QString file_path)
|
||||||
|
{
|
||||||
|
return new ModrinthHasher(file_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
Hasher::Ptr createFlameHasher(QString file_path)
|
||||||
|
{
|
||||||
|
return new FlameHasher(file_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ModrinthHasher::executeTask()
|
||||||
|
{
|
||||||
|
QFile file(m_path);
|
||||||
|
|
||||||
|
try {
|
||||||
|
file.open(QFile::ReadOnly);
|
||||||
|
} catch (FS::FileSystemException& e) {
|
||||||
|
qCritical() << QString("Failed to open JAR file in %1").arg(m_path);
|
||||||
|
qCritical() << QString("Reason: ") << e.cause();
|
||||||
|
|
||||||
|
emitFailed("Failed to open file for hashing.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
|
||||||
|
m_hash = ProviderCaps.hash(ModPlatform::Provider::MODRINTH, &file, hash_type);
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
if (m_hash.isEmpty()) {
|
||||||
|
emitFailed("Empty hash!");
|
||||||
|
} else {
|
||||||
|
emitSucceeded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FlameHasher::executeTask()
|
||||||
|
{
|
||||||
|
// CF-specific
|
||||||
|
auto should_filter_out = [](char c) { return (c == 9 || c == 10 || c == 13 || c == 32); };
|
||||||
|
|
||||||
|
std::ifstream file_stream(m_path.toStdString(), std::ifstream::binary);
|
||||||
|
// TODO: This is very heavy work, but apparently QtConcurrent can't use move semantics, so we can't boop this to another thread.
|
||||||
|
// How do we make this non-blocking then?
|
||||||
|
m_hash = QString::number(MurmurHash2(std::move(file_stream), 4 * MiB, should_filter_out));
|
||||||
|
|
||||||
|
if (m_hash.isEmpty()) {
|
||||||
|
emitFailed("Empty hash!");
|
||||||
|
} else {
|
||||||
|
emitSucceeded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Hashing
|
47
launcher/modplatform/helpers/HashUtils.h
Normal file
47
launcher/modplatform/helpers/HashUtils.h
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "modplatform/ModIndex.h"
|
||||||
|
#include "tasks/Task.h"
|
||||||
|
|
||||||
|
namespace Hashing {
|
||||||
|
|
||||||
|
class Hasher : public Task {
|
||||||
|
public:
|
||||||
|
using Ptr = shared_qobject_ptr<Hasher>;
|
||||||
|
|
||||||
|
Hasher(QString file_path) : m_path(std::move(file_path)) {}
|
||||||
|
|
||||||
|
/* We can't really abort this task, but we can say we aborted and finish our thing quickly :) */
|
||||||
|
bool abort() override { return true; }
|
||||||
|
|
||||||
|
void executeTask() override = 0;
|
||||||
|
|
||||||
|
QString getResult() const { return m_hash; };
|
||||||
|
QString getPath() const { return m_path; };
|
||||||
|
|
||||||
|
protected:
|
||||||
|
QString m_hash;
|
||||||
|
QString m_path;
|
||||||
|
};
|
||||||
|
|
||||||
|
class FlameHasher : public Hasher {
|
||||||
|
public:
|
||||||
|
FlameHasher(QString file_path) : Hasher(file_path) { setObjectName(QString("FlameHasher: %1").arg(file_path)); }
|
||||||
|
|
||||||
|
void executeTask() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ModrinthHasher : public Hasher {
|
||||||
|
public:
|
||||||
|
ModrinthHasher(QString file_path) : Hasher(file_path) { setObjectName(QString("ModrinthHasher: %1").arg(file_path)); }
|
||||||
|
|
||||||
|
void executeTask() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
Hasher::Ptr createHasher(QString file_path, ModPlatform::Provider provider);
|
||||||
|
Hasher::Ptr createFlameHasher(QString file_path);
|
||||||
|
Hasher::Ptr createModrinthHasher(QString file_path);
|
||||||
|
|
||||||
|
} // namespace Hashing
|
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
#include "net/NetJob.h"
|
#include "net/NetJob.h"
|
||||||
|
|
||||||
#include <nonstd/optional>
|
#include <optional>
|
||||||
|
|
||||||
namespace LegacyFTB {
|
namespace LegacyFTB {
|
||||||
|
|
||||||
@ -46,8 +46,8 @@ private: /* data */
|
|||||||
shared_qobject_ptr<QNetworkAccessManager> m_network;
|
shared_qobject_ptr<QNetworkAccessManager> m_network;
|
||||||
bool abortable = false;
|
bool abortable = false;
|
||||||
std::unique_ptr<QuaZip> m_packZip;
|
std::unique_ptr<QuaZip> m_packZip;
|
||||||
QFuture<nonstd::optional<QStringList>> m_extractFuture;
|
QFuture<std::optional<QStringList>> m_extractFuture;
|
||||||
QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
|
QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
|
||||||
NetJob::Ptr netJobContainer;
|
NetJob::Ptr netJobContainer;
|
||||||
QString archivePath;
|
QString archivePath;
|
||||||
|
|
||||||
|
@ -2,11 +2,14 @@
|
|||||||
#include "ModrinthAPI.h"
|
#include "ModrinthAPI.h"
|
||||||
#include "ModrinthPackIndex.h"
|
#include "ModrinthPackIndex.h"
|
||||||
|
|
||||||
#include "FileSystem.h"
|
|
||||||
#include "Json.h"
|
#include "Json.h"
|
||||||
|
|
||||||
#include "ModDownloadTask.h"
|
#include "ModDownloadTask.h"
|
||||||
|
|
||||||
|
#include "modplatform/helpers/HashUtils.h"
|
||||||
|
|
||||||
|
#include "tasks/ConcurrentTask.h"
|
||||||
|
|
||||||
static ModrinthAPI api;
|
static ModrinthAPI api;
|
||||||
static ModPlatform::ProviderCapabilities ProviderCaps;
|
static ModPlatform::ProviderCapabilities ProviderCaps;
|
||||||
|
|
||||||
@ -32,6 +35,8 @@ void ModrinthCheckUpdate::executeTask()
|
|||||||
// Create all hashes
|
// Create all hashes
|
||||||
QStringList hashes;
|
QStringList hashes;
|
||||||
auto best_hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
|
auto best_hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
|
||||||
|
|
||||||
|
ConcurrentTask hashing_task(this, "MakeModrinthHashesTask", 10);
|
||||||
for (auto* mod : m_mods) {
|
for (auto* mod : m_mods) {
|
||||||
if (!mod->enabled()) {
|
if (!mod->enabled()) {
|
||||||
emit checkFailed(mod, tr("Disabled mods won't be updated, to prevent mod duplication issues!"));
|
emit checkFailed(mod, tr("Disabled mods won't be updated, to prevent mod duplication issues!"));
|
||||||
@ -44,24 +49,24 @@ void ModrinthCheckUpdate::executeTask()
|
|||||||
// need to generate a new hash if the current one is innadequate
|
// need to generate a new hash if the current one is innadequate
|
||||||
// (though it will rarely happen, if at all)
|
// (though it will rarely happen, if at all)
|
||||||
if (mod->metadata()->hash_format != best_hash_type) {
|
if (mod->metadata()->hash_format != best_hash_type) {
|
||||||
QByteArray jar_data;
|
auto hash_task = Hashing::createModrinthHasher(mod->fileinfo().absoluteFilePath());
|
||||||
|
connect(hash_task.get(), &Task::succeeded, [&] {
|
||||||
try {
|
QString hash (hash_task->getResult());
|
||||||
jar_data = FS::read(mod->fileinfo().absoluteFilePath());
|
hashes.append(hash);
|
||||||
} catch (FS::FileSystemException& e) {
|
mappings.insert(hash, mod);
|
||||||
qCritical() << QString("Failed to open / read JAR file of %1").arg(mod->name());
|
});
|
||||||
qCritical() << QString("Reason: ") << e.cause();
|
connect(hash_task.get(), &Task::failed, [this, hash_task] { failed("Failed to generate hash"); });
|
||||||
|
hashing_task.addTask(hash_task);
|
||||||
failed(e.what());
|
} else {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
hash = QString(ProviderCaps.hash(ModPlatform::Provider::MODRINTH, jar_data, best_hash_type).toHex());
|
|
||||||
}
|
|
||||||
|
|
||||||
hashes.append(hash);
|
hashes.append(hash);
|
||||||
mappings.insert(hash, mod);
|
mappings.insert(hash, mod);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QEventLoop loop;
|
||||||
|
connect(&hashing_task, &Task::finished, [&loop]{ loop.quit(); });
|
||||||
|
hashing_task.start();
|
||||||
|
loop.exec();
|
||||||
|
|
||||||
auto* response = new QByteArray();
|
auto* response = new QByteArray();
|
||||||
auto job = api.latestVersions(hashes, best_hash_type, m_game_versions, m_loaders, response);
|
auto job = api.latestVersions(hashes, best_hash_type, m_game_versions, m_loaders, response);
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
|
||||||
#include <nonstd/optional>
|
#include <optional>
|
||||||
|
|
||||||
namespace Technic {
|
namespace Technic {
|
||||||
|
|
||||||
@ -57,8 +57,8 @@ private:
|
|||||||
QString m_archivePath;
|
QString m_archivePath;
|
||||||
NetJob::Ptr m_filesNetJob;
|
NetJob::Ptr m_filesNetJob;
|
||||||
std::unique_ptr<QuaZip> m_packZip;
|
std::unique_ptr<QuaZip> m_packZip;
|
||||||
QFuture<nonstd::optional<QStringList>> m_extractFuture;
|
QFuture<std::optional<QStringList>> m_extractFuture;
|
||||||
QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
|
QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Technic
|
} // namespace Technic
|
||||||
|
@ -60,7 +60,7 @@ auto Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) -> Down
|
|||||||
dl->m_url = url;
|
dl->m_url = url;
|
||||||
dl->m_options = options;
|
dl->m_options = options;
|
||||||
auto md5Node = new ChecksumValidator(QCryptographicHash::Md5);
|
auto md5Node = new ChecksumValidator(QCryptographicHash::Md5);
|
||||||
auto cachedNode = new MetaCacheSink(entry, md5Node);
|
auto cachedNode = new MetaCacheSink(entry, md5Node, options.testFlag(Option::MakeEternal));
|
||||||
dl->m_sink.reset(cachedNode);
|
dl->m_sink.reset(cachedNode);
|
||||||
return dl;
|
return dl;
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ class Download : public NetAction {
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
using Ptr = shared_qobject_ptr<class Download>;
|
using Ptr = shared_qobject_ptr<class Download>;
|
||||||
enum class Option { NoOptions = 0, AcceptLocalFiles = 1 };
|
enum class Option { NoOptions = 0, AcceptLocalFiles = 1, MakeEternal = 2 };
|
||||||
Q_DECLARE_FLAGS(Options, Option)
|
Q_DECLARE_FLAGS(Options, Option)
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
@ -121,6 +121,14 @@ auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString ex
|
|||||||
SaveEventually();
|
SaveEventually();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get rid of old entries, to prevent cache problems
|
||||||
|
auto current_time = QDateTime::currentSecsSinceEpoch();
|
||||||
|
if (entry->isExpired(current_time - ( file_last_changed / 1000 ))) {
|
||||||
|
qWarning() << "Removing cache entry because of old age!";
|
||||||
|
selected_base.entry_list.remove(resource_path);
|
||||||
|
return staleEntry(base, resource_path);
|
||||||
|
}
|
||||||
|
|
||||||
// entry passed all the checks we cared about.
|
// entry passed all the checks we cared about.
|
||||||
entry->basePath = getBasePath(base);
|
entry->basePath = getBasePath(base);
|
||||||
return entry;
|
return entry;
|
||||||
@ -221,6 +229,13 @@ void HttpMetaCache::Load()
|
|||||||
foo->etag = Json::ensureString(element_obj, "etag");
|
foo->etag = Json::ensureString(element_obj, "etag");
|
||||||
foo->local_changed_timestamp = Json::ensureDouble(element_obj, "last_changed_timestamp");
|
foo->local_changed_timestamp = Json::ensureDouble(element_obj, "last_changed_timestamp");
|
||||||
foo->remote_changed_timestamp = Json::ensureString(element_obj, "remote_changed_timestamp");
|
foo->remote_changed_timestamp = Json::ensureString(element_obj, "remote_changed_timestamp");
|
||||||
|
|
||||||
|
foo->makeEternal(Json::ensureBoolean(element_obj, "eternal", false));
|
||||||
|
if (!foo->isEternal()) {
|
||||||
|
foo->current_age = Json::ensureDouble(element_obj, "current_age");
|
||||||
|
foo->max_age = Json::ensureDouble(element_obj, "max_age");
|
||||||
|
}
|
||||||
|
|
||||||
// presumed innocent until closer examination
|
// presumed innocent until closer examination
|
||||||
foo->stale = false;
|
foo->stale = false;
|
||||||
|
|
||||||
@ -240,6 +255,8 @@ void HttpMetaCache::SaveNow()
|
|||||||
if (m_index_file.isNull())
|
if (m_index_file.isNull())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
qDebug() << "[HttpMetaCache]" << "Saving metacache with" << m_entries.size() << "entries";
|
||||||
|
|
||||||
QJsonObject toplevel;
|
QJsonObject toplevel;
|
||||||
Json::writeString(toplevel, "version", "1");
|
Json::writeString(toplevel, "version", "1");
|
||||||
|
|
||||||
@ -259,6 +276,12 @@ void HttpMetaCache::SaveNow()
|
|||||||
entryObj.insert("last_changed_timestamp", QJsonValue(double(entry->local_changed_timestamp)));
|
entryObj.insert("last_changed_timestamp", QJsonValue(double(entry->local_changed_timestamp)));
|
||||||
if (!entry->remote_changed_timestamp.isEmpty())
|
if (!entry->remote_changed_timestamp.isEmpty())
|
||||||
entryObj.insert("remote_changed_timestamp", QJsonValue(entry->remote_changed_timestamp));
|
entryObj.insert("remote_changed_timestamp", QJsonValue(entry->remote_changed_timestamp));
|
||||||
|
if (entry->isEternal()) {
|
||||||
|
entryObj.insert("eternal", true);
|
||||||
|
} else {
|
||||||
|
entryObj.insert("current_age", QJsonValue(double(entry->current_age)));
|
||||||
|
entryObj.insert("max_age", QJsonValue(double(entry->max_age)));
|
||||||
|
}
|
||||||
entriesArr.append(entryObj);
|
entriesArr.append(entryObj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,14 +64,31 @@ class MetaEntry {
|
|||||||
auto getMD5Sum() -> QString { return md5sum; }
|
auto getMD5Sum() -> QString { return md5sum; }
|
||||||
void setMD5Sum(QString md5sum) { this->md5sum = md5sum; }
|
void setMD5Sum(QString md5sum) { this->md5sum = md5sum; }
|
||||||
|
|
||||||
|
/* Whether the entry expires after some time (false) or not (true). */
|
||||||
|
void makeEternal(bool eternal) { is_eternal = eternal; }
|
||||||
|
[[nodiscard]] bool isEternal() const { return is_eternal; }
|
||||||
|
|
||||||
|
auto getCurrentAge() -> qint64 { return current_age; }
|
||||||
|
void setCurrentAge(qint64 age) { current_age = age; }
|
||||||
|
|
||||||
|
auto getMaximumAge() -> qint64 { return max_age; }
|
||||||
|
void setMaximumAge(qint64 age) { max_age = age; }
|
||||||
|
|
||||||
|
bool isExpired(qint64 offset) { return !is_eternal && (current_age >= max_age - offset); };
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
QString baseId;
|
QString baseId;
|
||||||
QString basePath;
|
QString basePath;
|
||||||
QString relativePath;
|
QString relativePath;
|
||||||
QString md5sum;
|
QString md5sum;
|
||||||
QString etag;
|
QString etag;
|
||||||
|
|
||||||
qint64 local_changed_timestamp = 0;
|
qint64 local_changed_timestamp = 0;
|
||||||
QString remote_changed_timestamp; // QString for now, RFC 2822 encoded time
|
QString remote_changed_timestamp; // QString for now, RFC 2822 encoded time
|
||||||
|
qint64 current_age = 0;
|
||||||
|
qint64 max_age = 0;
|
||||||
|
bool is_eternal = false;
|
||||||
|
|
||||||
bool stale = true;
|
bool stale = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -36,13 +36,18 @@
|
|||||||
#include "MetaCacheSink.h"
|
#include "MetaCacheSink.h"
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include "FileSystem.h"
|
|
||||||
#include "Application.h"
|
#include "Application.h"
|
||||||
|
|
||||||
namespace Net {
|
namespace Net {
|
||||||
|
|
||||||
MetaCacheSink::MetaCacheSink(MetaEntryPtr entry, ChecksumValidator * md5sum)
|
/** Maximum time to hold a cache entry
|
||||||
:Net::FileSink(entry->getFullPath()), m_entry(entry), m_md5Node(md5sum)
|
* = 1 week in seconds
|
||||||
|
*/
|
||||||
|
#define MAX_TIME_TO_EXPIRE 1*7*24*60*60
|
||||||
|
|
||||||
|
|
||||||
|
MetaCacheSink::MetaCacheSink(MetaEntryPtr entry, ChecksumValidator * md5sum, bool is_eternal)
|
||||||
|
:Net::FileSink(entry->getFullPath()), m_entry(entry), m_md5Node(md5sum), m_is_eternal(is_eternal)
|
||||||
{
|
{
|
||||||
addValidator(md5sum);
|
addValidator(md5sum);
|
||||||
}
|
}
|
||||||
@ -88,6 +93,40 @@ Task::State MetaCacheSink::finalizeCache(QNetworkReply & reply)
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_entry->setLocalChangedTimestamp(output_file_info.lastModified().toUTC().toMSecsSinceEpoch());
|
m_entry->setLocalChangedTimestamp(output_file_info.lastModified().toUTC().toMSecsSinceEpoch());
|
||||||
|
|
||||||
|
{ // Cache lifetime
|
||||||
|
if (m_is_eternal) {
|
||||||
|
qDebug() << "[MetaCache] Adding eternal cache entry:" << m_entry->getFullPath();
|
||||||
|
m_entry->makeEternal(true);
|
||||||
|
} else if (reply.hasRawHeader("Cache-Control")) {
|
||||||
|
auto cache_control_header = reply.rawHeader("Cache-Control");
|
||||||
|
// qDebug() << "[MetaCache] Parsing 'Cache-Control' header with" << cache_control_header;
|
||||||
|
|
||||||
|
QRegularExpression max_age_expr("max-age=([0-9]+)");
|
||||||
|
qint64 max_age = max_age_expr.match(cache_control_header).captured(1).toLongLong();
|
||||||
|
m_entry->setMaximumAge(max_age);
|
||||||
|
|
||||||
|
} else if (reply.hasRawHeader("Expires")) {
|
||||||
|
auto expires_header = reply.rawHeader("Expires");
|
||||||
|
// qDebug() << "[MetaCache] Parsing 'Expires' header with" << expires_header;
|
||||||
|
|
||||||
|
qint64 max_age = QDateTime::fromString(expires_header).toSecsSinceEpoch() - QDateTime::currentSecsSinceEpoch();
|
||||||
|
m_entry->setMaximumAge(max_age);
|
||||||
|
} else {
|
||||||
|
m_entry->setMaximumAge(MAX_TIME_TO_EXPIRE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reply.hasRawHeader("Age")) {
|
||||||
|
auto age_header = reply.rawHeader("Age");
|
||||||
|
// qDebug() << "[MetaCache] Parsing 'Age' header with" << age_header;
|
||||||
|
|
||||||
|
qint64 current_age = age_header.toLongLong();
|
||||||
|
m_entry->setCurrentAge(current_age);
|
||||||
|
} else {
|
||||||
|
m_entry->setCurrentAge(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
m_entry->setStale(false);
|
m_entry->setStale(false);
|
||||||
APPLICATION->metacache()->updateEntry(m_entry);
|
APPLICATION->metacache()->updateEntry(m_entry);
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
namespace Net {
|
namespace Net {
|
||||||
class MetaCacheSink : public FileSink {
|
class MetaCacheSink : public FileSink {
|
||||||
public:
|
public:
|
||||||
MetaCacheSink(MetaEntryPtr entry, ChecksumValidator* md5sum);
|
MetaCacheSink(MetaEntryPtr entry, ChecksumValidator* md5sum, bool is_eternal = false);
|
||||||
virtual ~MetaCacheSink() = default;
|
virtual ~MetaCacheSink() = default;
|
||||||
|
|
||||||
auto hasLocalData() -> bool override;
|
auto hasLocalData() -> bool override;
|
||||||
@ -54,5 +54,6 @@ class MetaCacheSink : public FileSink {
|
|||||||
private:
|
private:
|
||||||
MetaEntryPtr m_entry;
|
MetaEntryPtr m_entry;
|
||||||
ChecksumValidator* m_md5Node;
|
ChecksumValidator* m_md5Node;
|
||||||
|
bool m_is_eternal;
|
||||||
};
|
};
|
||||||
} // namespace Net
|
} // namespace Net
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
#include "ConcurrentTask.h"
|
#include "ConcurrentTask.h"
|
||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QCoreApplication>
|
||||||
|
|
||||||
ConcurrentTask::ConcurrentTask(QObject* parent, QString task_name, int max_concurrent)
|
ConcurrentTask::ConcurrentTask(QObject* parent, QString task_name, int max_concurrent)
|
||||||
: Task(parent), m_name(task_name), m_total_max_size(max_concurrent)
|
: Task(parent), m_name(task_name), m_total_max_size(max_concurrent)
|
||||||
{}
|
{ setObjectName(task_name); }
|
||||||
|
|
||||||
ConcurrentTask::~ConcurrentTask()
|
ConcurrentTask::~ConcurrentTask()
|
||||||
{
|
{
|
||||||
@ -36,8 +37,9 @@ void ConcurrentTask::executeTask()
|
|||||||
{
|
{
|
||||||
m_total_size = m_queue.size();
|
m_total_size = m_queue.size();
|
||||||
|
|
||||||
for (int i = 0; i < m_total_max_size; i++)
|
for (int i = 0; i < m_total_max_size; i++) {
|
||||||
startNext();
|
QMetaObject::invokeMethod(this, &ConcurrentTask::startNext, Qt::QueuedConnection);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ConcurrentTask::abort()
|
bool ConcurrentTask::abort()
|
||||||
@ -91,6 +93,8 @@ void ConcurrentTask::startNext()
|
|||||||
setStepStatus(next->isMultiStep() ? next->getStepStatus() : next->getStatus());
|
setStepStatus(next->isMultiStep() ? next->getStepStatus() : next->getStatus());
|
||||||
updateState();
|
updateState();
|
||||||
|
|
||||||
|
QCoreApplication::processEvents();
|
||||||
|
|
||||||
next->start();
|
next->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1445,6 +1445,7 @@ void MainWindow::updateNewsLabel()
|
|||||||
{
|
{
|
||||||
newsLabel->setText(tr("Loading news..."));
|
newsLabel->setText(tr("Loading news..."));
|
||||||
newsLabel->setEnabled(false);
|
newsLabel->setEnabled(false);
|
||||||
|
ui->actionMoreNews->setVisible(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -1453,11 +1454,13 @@ void MainWindow::updateNewsLabel()
|
|||||||
{
|
{
|
||||||
newsLabel->setText(entries[0]->title);
|
newsLabel->setText(entries[0]->title);
|
||||||
newsLabel->setEnabled(true);
|
newsLabel->setEnabled(true);
|
||||||
|
ui->actionMoreNews->setVisible(true);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
newsLabel->setText(tr("No news available."));
|
newsLabel->setText(tr("No news available."));
|
||||||
newsLabel->setEnabled(false);
|
newsLabel->setEnabled(false);
|
||||||
|
ui->actionMoreNews->setVisible(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,8 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>783</width>
|
<width>573</width>
|
||||||
<height>843</height>
|
<height>600</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
|
@ -121,9 +121,9 @@ QList<BasePage *> ModDownloadDialog::getPages()
|
|||||||
{
|
{
|
||||||
QList<BasePage *> pages;
|
QList<BasePage *> pages;
|
||||||
|
|
||||||
pages.append(new ModrinthModPage(this, m_instance));
|
pages.append(ModrinthModPage::create(this, m_instance));
|
||||||
if (APPLICATION->currentCapabilities() & Application::SupportsFlame)
|
if (APPLICATION->currentCapabilities() & Application::SupportsFlame)
|
||||||
pages.append(new FlameModPage(this, m_instance));
|
pages.append(FlameModPage::create(this, m_instance));
|
||||||
|
|
||||||
return pages;
|
return pages;
|
||||||
}
|
}
|
||||||
|
@ -270,6 +270,10 @@ auto ModUpdateDialog::ensureMetadata() -> bool
|
|||||||
connect(modrinth_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) {
|
connect(modrinth_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) {
|
||||||
onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::Provider::MODRINTH);
|
onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::Provider::MODRINTH);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (modrinth_task->getHashingTask())
|
||||||
|
seq.addTask(modrinth_task->getHashingTask());
|
||||||
|
|
||||||
seq.addTask(modrinth_task);
|
seq.addTask(modrinth_task);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,6 +283,10 @@ auto ModUpdateDialog::ensureMetadata() -> bool
|
|||||||
connect(flame_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) {
|
connect(flame_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) {
|
||||||
onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::Provider::FLAME);
|
onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::Provider::FLAME);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (flame_task->getHashingTask())
|
||||||
|
seq.addTask(flame_task->getHashingTask());
|
||||||
|
|
||||||
seq.addTask(flame_task);
|
seq.addTask(flame_task);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,7 +101,7 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared
|
|||||||
{
|
{
|
||||||
ui->setupUi(this);
|
ui->setupUi(this);
|
||||||
|
|
||||||
runningStateChanged(m_instance && m_instance->isRunning());
|
ExternalResourcesPage::runningStateChanged(m_instance && m_instance->isRunning());
|
||||||
|
|
||||||
ui->actionsToolbar->insertSpacer(ui->actionViewConfigs);
|
ui->actionsToolbar->insertSpacer(ui->actionViewConfigs);
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ class ExternalResourcesPage : public QMainWindow, public BasePage {
|
|||||||
protected slots:
|
protected slots:
|
||||||
void itemActivated(const QModelIndex& index);
|
void itemActivated(const QModelIndex& index);
|
||||||
void filterTextChanged(const QString& newContents);
|
void filterTextChanged(const QString& newContents);
|
||||||
void runningStateChanged(bool running);
|
virtual void runningStateChanged(bool running);
|
||||||
|
|
||||||
virtual void addItem();
|
virtual void addItem();
|
||||||
virtual void removeItem();
|
virtual void removeItem();
|
||||||
|
@ -84,51 +84,46 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel>
|
|||||||
ui->actionsToolbar->insertActionAfter(ui->actionAddItem, ui->actionUpdateItem);
|
ui->actionsToolbar->insertActionAfter(ui->actionAddItem, ui->actionUpdateItem);
|
||||||
connect(ui->actionUpdateItem, &QAction::triggered, this, &ModFolderPage::updateMods);
|
connect(ui->actionUpdateItem, &QAction::triggered, this, &ModFolderPage::updateMods);
|
||||||
|
|
||||||
connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this,
|
auto check_allow_update = [this] {
|
||||||
[this] { ui->actionUpdateItem->setEnabled(ui->treeView->selectionModel()->hasSelection() || !m_model->empty()); });
|
return (!m_instance || !m_instance->isRunning()) &&
|
||||||
|
(ui->treeView->selectionModel()->hasSelection() || !m_model->empty());
|
||||||
|
};
|
||||||
|
|
||||||
connect(mods.get(), &ModFolderModel::rowsInserted, this,
|
connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [this, check_allow_update] {
|
||||||
[this] { ui->actionUpdateItem->setEnabled(ui->treeView->selectionModel()->hasSelection() || !m_model->empty()); });
|
ui->actionUpdateItem->setEnabled(check_allow_update());
|
||||||
|
});
|
||||||
|
|
||||||
connect(mods.get(), &ModFolderModel::updateFinished, this, [this, mods] {
|
connect(mods.get(), &ModFolderModel::rowsInserted, this, [this, check_allow_update] {
|
||||||
ui->actionUpdateItem->setEnabled(ui->treeView->selectionModel()->hasSelection() || !m_model->empty());
|
ui->actionUpdateItem->setEnabled(check_allow_update());
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(mods.get(), &ModFolderModel::rowsRemoved, this, [this, check_allow_update] {
|
||||||
|
ui->actionUpdateItem->setEnabled(check_allow_update());
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(mods.get(), &ModFolderModel::updateFinished, this, [this, check_allow_update, mods] {
|
||||||
|
ui->actionUpdateItem->setEnabled(check_allow_update());
|
||||||
|
|
||||||
// Prevent a weird crash when trying to open the mods page twice in a session o.O
|
// Prevent a weird crash when trying to open the mods page twice in a session o.O
|
||||||
disconnect(mods.get(), &ModFolderModel::updateFinished, this, 0);
|
disconnect(mods.get(), &ModFolderModel::updateFinished, this, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ModFolderPage::runningStateChanged(m_instance && m_instance->isRunning());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CoreModFolderPage::CoreModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent)
|
void ModFolderPage::runningStateChanged(bool running)
|
||||||
: ModFolderPage(inst, mods, parent)
|
{
|
||||||
{}
|
ExternalResourcesPage::runningStateChanged(running);
|
||||||
|
ui->actionDownloadItem->setEnabled(!running);
|
||||||
|
ui->actionUpdateItem->setEnabled(!running);
|
||||||
|
}
|
||||||
|
|
||||||
bool ModFolderPage::shouldDisplay() const
|
bool ModFolderPage::shouldDisplay() const
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CoreModFolderPage::shouldDisplay() const
|
|
||||||
{
|
|
||||||
if (ModFolderPage::shouldDisplay()) {
|
|
||||||
auto inst = dynamic_cast<MinecraftInstance*>(m_instance);
|
|
||||||
if (!inst)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
auto version = inst->getPackProfile();
|
|
||||||
|
|
||||||
if (!version)
|
|
||||||
return true;
|
|
||||||
if (!version->getComponent("net.minecraftforge"))
|
|
||||||
return false;
|
|
||||||
if (!version->getComponent("net.minecraft"))
|
|
||||||
return false;
|
|
||||||
if (version->getComponent("net.minecraft")->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate)
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ModFolderPage::installMods()
|
void ModFolderPage::installMods()
|
||||||
{
|
{
|
||||||
if (!m_controlsEnabled)
|
if (!m_controlsEnabled)
|
||||||
@ -232,3 +227,28 @@ void ModFolderPage::updateMods()
|
|||||||
m_model->update();
|
m_model->update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CoreModFolderPage::CoreModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent)
|
||||||
|
: ModFolderPage(inst, mods, parent)
|
||||||
|
{}
|
||||||
|
|
||||||
|
bool CoreModFolderPage::shouldDisplay() const
|
||||||
|
{
|
||||||
|
if (ModFolderPage::shouldDisplay()) {
|
||||||
|
auto inst = dynamic_cast<MinecraftInstance*>(m_instance);
|
||||||
|
if (!inst)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
auto version = inst->getPackProfile();
|
||||||
|
|
||||||
|
if (!version)
|
||||||
|
return true;
|
||||||
|
if (!version->getComponent("net.minecraftforge"))
|
||||||
|
return false;
|
||||||
|
if (!version->getComponent("net.minecraft"))
|
||||||
|
return false;
|
||||||
|
if (version->getComponent("net.minecraft")->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
@ -53,6 +53,7 @@ class ModFolderPage : public ExternalResourcesPage {
|
|||||||
virtual QString helpPage() const override { return "Loader-mods"; }
|
virtual QString helpPage() const override { return "Loader-mods"; }
|
||||||
|
|
||||||
virtual bool shouldDisplay() const override;
|
virtual bool shouldDisplay() const override;
|
||||||
|
void runningStateChanged(bool running) override;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void installMods();
|
void installMods();
|
||||||
@ -63,5 +64,11 @@ class CoreModFolderPage : public ModFolderPage {
|
|||||||
public:
|
public:
|
||||||
explicit CoreModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent = 0);
|
explicit CoreModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel> mods, QWidget* parent = 0);
|
||||||
virtual ~CoreModFolderPage() = default;
|
virtual ~CoreModFolderPage() = default;
|
||||||
virtual bool shouldDisplay() const;
|
|
||||||
|
virtual QString displayName() const override { return tr("Core mods"); }
|
||||||
|
virtual QIcon icon() const override { return APPLICATION->getThemedIcon("coremods"); }
|
||||||
|
virtual QString id() const override { return "coremods"; }
|
||||||
|
virtual QString helpPage() const override { return "Core-mods"; }
|
||||||
|
|
||||||
|
virtual bool shouldDisplay() const override;
|
||||||
};
|
};
|
||||||
|
@ -211,7 +211,7 @@ void WorldListPage::on_actionDatapacks_triggered()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!worldSafetyNagQuestion())
|
if(!worldSafetyNagQuestion(tr("Open World Datapacks Folder")))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
auto fullPath = m_worlds->data(index, WorldList::FolderRole).toString();
|
auto fullPath = m_worlds->data(index, WorldList::FolderRole).toString();
|
||||||
@ -269,7 +269,7 @@ void WorldListPage::on_actionMCEdit_triggered()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!worldSafetyNagQuestion())
|
if(!worldSafetyNagQuestion(tr("Open World in MCEdit")))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
auto fullPath = m_worlds->data(index, WorldList::FolderRole).toString();
|
auto fullPath = m_worlds->data(index, WorldList::FolderRole).toString();
|
||||||
@ -373,11 +373,11 @@ bool WorldListPage::isWorldSafe(QModelIndex)
|
|||||||
return !m_inst->isRunning();
|
return !m_inst->isRunning();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool WorldListPage::worldSafetyNagQuestion()
|
bool WorldListPage::worldSafetyNagQuestion(const QString &actionType)
|
||||||
{
|
{
|
||||||
if(!isWorldSafe(getSelectedWorld()))
|
if(!isWorldSafe(getSelectedWorld()))
|
||||||
{
|
{
|
||||||
auto result = QMessageBox::question(this, tr("Copy World"), tr("Changing a world while Minecraft is running is potentially unsafe.\nDo you wish to proceed?"));
|
auto result = QMessageBox::question(this, actionType, tr("Changing a world while Minecraft is running is potentially unsafe.\nDo you wish to proceed?"));
|
||||||
if(result == QMessageBox::No)
|
if(result == QMessageBox::No)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@ -395,7 +395,7 @@ void WorldListPage::on_actionCopy_triggered()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!worldSafetyNagQuestion())
|
if(!worldSafetyNagQuestion(tr("Copy World")))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
auto worldVariant = m_worlds->data(index, WorldList::ObjectRole);
|
auto worldVariant = m_worlds->data(index, WorldList::ObjectRole);
|
||||||
@ -417,7 +417,7 @@ void WorldListPage::on_actionRename_triggered()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!worldSafetyNagQuestion())
|
if(!worldSafetyNagQuestion(tr("Rename World")))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
auto worldVariant = m_worlds->data(index, WorldList::ObjectRole);
|
auto worldVariant = m_worlds->data(index, WorldList::ObjectRole);
|
||||||
|
@ -93,7 +93,7 @@ protected:
|
|||||||
private:
|
private:
|
||||||
QModelIndex getSelectedWorld();
|
QModelIndex getSelectedWorld();
|
||||||
bool isWorldSafe(QModelIndex index);
|
bool isWorldSafe(QModelIndex index);
|
||||||
bool worldSafetyNagQuestion();
|
bool worldSafetyNagQuestion(const QString &actionType);
|
||||||
void mceditError();
|
void mceditError();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
@ -44,12 +44,12 @@
|
|||||||
#include "minecraft/PackProfile.h"
|
#include "minecraft/PackProfile.h"
|
||||||
#include "ui/dialogs/ModDownloadDialog.h"
|
#include "ui/dialogs/ModDownloadDialog.h"
|
||||||
|
|
||||||
|
|
||||||
ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api)
|
ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api)
|
||||||
: QWidget(dialog)
|
: QWidget(dialog)
|
||||||
, m_instance(instance)
|
, m_instance(instance)
|
||||||
, ui(new Ui::ModPage)
|
, ui(new Ui::ModPage)
|
||||||
, dialog(dialog)
|
, dialog(dialog)
|
||||||
, filter_widget(static_cast<MinecraftInstance*>(instance)->getPackProfile()->getComponentVersion("net.minecraft"), this)
|
|
||||||
, api(api)
|
, api(api)
|
||||||
{
|
{
|
||||||
ui->setupUi(this);
|
ui->setupUi(this);
|
||||||
@ -59,18 +59,6 @@ ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api)
|
|||||||
|
|
||||||
ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||||
ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
|
ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
|
||||||
|
|
||||||
ui->gridLayout_3->addWidget(&filter_widget, 0, 0, 1, ui->gridLayout_3->columnCount());
|
|
||||||
|
|
||||||
filter_widget.setInstance(static_cast<MinecraftInstance*>(m_instance));
|
|
||||||
m_filter = filter_widget.getFilter();
|
|
||||||
|
|
||||||
connect(&filter_widget, &ModFilterWidget::filterChanged, this, [&]{
|
|
||||||
ui->searchButton->setStyleSheet("text-decoration: underline");
|
|
||||||
});
|
|
||||||
connect(&filter_widget, &ModFilterWidget::filterUnchanged, this, [&]{
|
|
||||||
ui->searchButton->setStyleSheet("text-decoration: none");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ModPage::~ModPage()
|
ModPage::~ModPage()
|
||||||
@ -78,6 +66,26 @@ ModPage::~ModPage()
|
|||||||
delete ui;
|
delete ui;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ModPage::setFilterWidget(unique_qobject_ptr<ModFilterWidget>& widget)
|
||||||
|
{
|
||||||
|
if (m_filter_widget)
|
||||||
|
disconnect(m_filter_widget.get(), nullptr, nullptr, nullptr);
|
||||||
|
|
||||||
|
m_filter_widget.swap(widget);
|
||||||
|
|
||||||
|
ui->gridLayout_3->addWidget(m_filter_widget.get(), 0, 0, 1, ui->gridLayout_3->columnCount());
|
||||||
|
|
||||||
|
m_filter_widget->setInstance(static_cast<MinecraftInstance*>(m_instance));
|
||||||
|
m_filter = m_filter_widget->getFilter();
|
||||||
|
|
||||||
|
connect(m_filter_widget.get(), &ModFilterWidget::filterChanged, this, [&]{
|
||||||
|
ui->searchButton->setStyleSheet("text-decoration: underline");
|
||||||
|
});
|
||||||
|
connect(m_filter_widget.get(), &ModFilterWidget::filterUnchanged, this, [&]{
|
||||||
|
ui->searchButton->setStyleSheet("text-decoration: none");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/******** Qt things ********/
|
/******** Qt things ********/
|
||||||
|
|
||||||
@ -105,13 +113,13 @@ auto ModPage::eventFilter(QObject* watched, QEvent* event) -> bool
|
|||||||
|
|
||||||
void ModPage::filterMods()
|
void ModPage::filterMods()
|
||||||
{
|
{
|
||||||
filter_widget.setHidden(!filter_widget.isHidden());
|
m_filter_widget->setHidden(!m_filter_widget->isHidden());
|
||||||
}
|
}
|
||||||
|
|
||||||
void ModPage::triggerSearch()
|
void ModPage::triggerSearch()
|
||||||
{
|
{
|
||||||
auto changed = filter_widget.changed();
|
auto changed = m_filter_widget->changed();
|
||||||
m_filter = filter_widget.getFilter();
|
m_filter = m_filter_widget->getFilter();
|
||||||
|
|
||||||
if(changed){
|
if(changed){
|
||||||
ui->packView->clearSelection();
|
ui->packView->clearSelection();
|
||||||
|
@ -20,7 +20,17 @@ class ModPage : public QWidget, public BasePage {
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api);
|
template<typename T>
|
||||||
|
static T* create(ModDownloadDialog* dialog, BaseInstance* instance)
|
||||||
|
{
|
||||||
|
auto page = new T(dialog, instance);
|
||||||
|
|
||||||
|
auto filter_widget = ModFilterWidget::create(static_cast<MinecraftInstance*>(instance)->getPackProfile()->getComponentVersion("net.minecraft"), page);
|
||||||
|
page->setFilterWidget(filter_widget);
|
||||||
|
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
~ModPage() override;
|
~ModPage() override;
|
||||||
|
|
||||||
/* Affects what the user sees */
|
/* Affects what the user sees */
|
||||||
@ -45,6 +55,8 @@ class ModPage : public QWidget, public BasePage {
|
|||||||
auto getFilter() const -> const std::shared_ptr<ModFilterWidget::Filter> { return m_filter; }
|
auto getFilter() const -> const std::shared_ptr<ModFilterWidget::Filter> { return m_filter; }
|
||||||
auto getDialog() const -> const ModDownloadDialog* { return dialog; }
|
auto getDialog() const -> const ModDownloadDialog* { return dialog; }
|
||||||
|
|
||||||
|
void setFilterWidget(unique_qobject_ptr<ModFilterWidget>&);
|
||||||
|
|
||||||
auto getCurrent() -> ModPlatform::IndexedPack& { return current; }
|
auto getCurrent() -> ModPlatform::IndexedPack& { return current; }
|
||||||
void updateModVersions(int prev_count = -1);
|
void updateModVersions(int prev_count = -1);
|
||||||
|
|
||||||
@ -54,6 +66,7 @@ class ModPage : public QWidget, public BasePage {
|
|||||||
BaseInstance* m_instance;
|
BaseInstance* m_instance;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
ModPage(ModDownloadDialog* dialog, BaseInstance* instance, ModAPI* api);
|
||||||
void updateSelectionButton();
|
void updateSelectionButton();
|
||||||
|
|
||||||
protected slots:
|
protected slots:
|
||||||
@ -67,7 +80,7 @@ class ModPage : public QWidget, public BasePage {
|
|||||||
Ui::ModPage* ui = nullptr;
|
Ui::ModPage* ui = nullptr;
|
||||||
ModDownloadDialog* dialog = nullptr;
|
ModDownloadDialog* dialog = nullptr;
|
||||||
|
|
||||||
ModFilterWidget filter_widget;
|
unique_qobject_ptr<ModFilterWidget> m_filter_widget;
|
||||||
std::shared_ptr<ModFilterWidget::Filter> m_filter;
|
std::shared_ptr<ModFilterWidget::Filter> m_filter;
|
||||||
|
|
||||||
ModPlatform::ListModel* listModel = nullptr;
|
ModPlatform::ListModel* listModel = nullptr;
|
||||||
|
@ -44,7 +44,12 @@ class FlameModPage : public ModPage {
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance);
|
static FlameModPage* create(ModDownloadDialog* dialog, BaseInstance* instance)
|
||||||
|
{
|
||||||
|
return ModPage::create<FlameModPage>(dialog, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance);
|
||||||
~FlameModPage() override = default;
|
~FlameModPage() override = default;
|
||||||
|
|
||||||
inline auto displayName() const -> QString override { return "CurseForge"; }
|
inline auto displayName() const -> QString override { return "CurseForge"; }
|
||||||
|
@ -35,7 +35,11 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="1">
|
<item row="0" column="1">
|
||||||
<widget class="QTextBrowser" name="publicPackDescription"/>
|
<widget class="QTextBrowser" name="publicPackDescription">
|
||||||
|
<property name="openExternalLinks">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
@ -45,7 +49,11 @@
|
|||||||
</attribute>
|
</attribute>
|
||||||
<layout class="QGridLayout" name="gridLayout_3">
|
<layout class="QGridLayout" name="gridLayout_3">
|
||||||
<item row="0" column="1">
|
<item row="0" column="1">
|
||||||
<widget class="QTextBrowser" name="thirdPartyPackDescription"/>
|
<widget class="QTextBrowser" name="thirdPartyPackDescription">
|
||||||
|
<property name="openExternalLinks">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="0">
|
<item row="0" column="0">
|
||||||
<widget class="QTreeView" name="thirdPartyPackList">
|
<widget class="QTreeView" name="thirdPartyPackList">
|
||||||
@ -95,7 +103,11 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="1" rowspan="3">
|
<item row="0" column="1" rowspan="3">
|
||||||
<widget class="QTextBrowser" name="privatePackDescription"/>
|
<widget class="QTextBrowser" name="privatePackDescription">
|
||||||
|
<property name="openExternalLinks">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
|
@ -44,7 +44,12 @@ class ModrinthModPage : public ModPage {
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instance);
|
static ModrinthModPage* create(ModDownloadDialog* dialog, BaseInstance* instance)
|
||||||
|
{
|
||||||
|
return ModPage::create<ModrinthModPage>(dialog, instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instance);
|
||||||
~ModrinthModPage() override = default;
|
~ModrinthModPage() override = default;
|
||||||
|
|
||||||
inline auto displayName() const -> QString override { return "Modrinth"; }
|
inline auto displayName() const -> QString override { return "Modrinth"; }
|
||||||
|
@ -1,6 +1,39 @@
|
|||||||
#include "ModFilterWidget.h"
|
#include "ModFilterWidget.h"
|
||||||
#include "ui_ModFilterWidget.h"
|
#include "ui_ModFilterWidget.h"
|
||||||
|
|
||||||
|
#include "Application.h"
|
||||||
|
|
||||||
|
unique_qobject_ptr<ModFilterWidget> ModFilterWidget::create(Version default_version, QWidget* parent)
|
||||||
|
{
|
||||||
|
auto filter_widget = new ModFilterWidget(default_version, parent);
|
||||||
|
|
||||||
|
if (!filter_widget->versionList()->isLoaded()) {
|
||||||
|
QEventLoop load_version_list_loop;
|
||||||
|
|
||||||
|
QTimer time_limit_for_list_load;
|
||||||
|
time_limit_for_list_load.setTimerType(Qt::TimerType::CoarseTimer);
|
||||||
|
time_limit_for_list_load.setSingleShot(true);
|
||||||
|
time_limit_for_list_load.callOnTimeout(&load_version_list_loop, &QEventLoop::quit);
|
||||||
|
time_limit_for_list_load.start(4000);
|
||||||
|
|
||||||
|
auto task = filter_widget->versionList()->getLoadTask();
|
||||||
|
|
||||||
|
connect(task.get(), &Task::failed, [filter_widget]{
|
||||||
|
filter_widget->disableVersionButton(VersionButtonID::Major, tr("failed to get version index"));
|
||||||
|
});
|
||||||
|
connect(task.get(), &Task::finished, &load_version_list_loop, &QEventLoop::quit);
|
||||||
|
|
||||||
|
if (!task->isRunning())
|
||||||
|
task->start();
|
||||||
|
|
||||||
|
load_version_list_loop.exec();
|
||||||
|
if (time_limit_for_list_load.isActive())
|
||||||
|
time_limit_for_list_load.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
return unique_qobject_ptr<ModFilterWidget>(filter_widget);
|
||||||
|
}
|
||||||
|
|
||||||
ModFilterWidget::ModFilterWidget(Version def, QWidget* parent)
|
ModFilterWidget::ModFilterWidget(Version def, QWidget* parent)
|
||||||
: QTabWidget(parent), m_filter(new Filter()), ui(new Ui::ModFilterWidget)
|
: QTabWidget(parent), m_filter(new Filter()), ui(new Ui::ModFilterWidget)
|
||||||
{
|
{
|
||||||
@ -16,6 +49,7 @@ ModFilterWidget::ModFilterWidget(Version def, QWidget* parent)
|
|||||||
|
|
||||||
m_filter->versions.push_front(def);
|
m_filter->versions.push_front(def);
|
||||||
|
|
||||||
|
m_version_list = APPLICATION->metadataIndex()->get("net.minecraft");
|
||||||
setHidden(true);
|
setHidden(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,24 +85,30 @@ auto ModFilterWidget::getFilter() -> std::shared_ptr<Filter>
|
|||||||
return m_filter;
|
return m_filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ModFilterWidget::disableVersionButton(VersionButtonID id)
|
void ModFilterWidget::disableVersionButton(VersionButtonID id, QString reason)
|
||||||
{
|
{
|
||||||
|
QAbstractButton* btn = nullptr;
|
||||||
|
|
||||||
switch(id){
|
switch(id){
|
||||||
case(VersionButtonID::Strict):
|
case(VersionButtonID::Strict):
|
||||||
ui->strictVersionButton->setEnabled(false);
|
btn = ui->strictVersionButton;
|
||||||
break;
|
break;
|
||||||
case(VersionButtonID::Major):
|
case(VersionButtonID::Major):
|
||||||
ui->majorVersionButton->setEnabled(false);
|
btn = ui->majorVersionButton;
|
||||||
break;
|
break;
|
||||||
case(VersionButtonID::All):
|
case(VersionButtonID::All):
|
||||||
ui->allVersionsButton->setEnabled(false);
|
btn = ui->allVersionsButton;
|
||||||
break;
|
break;
|
||||||
case(VersionButtonID::Between):
|
case(VersionButtonID::Between):
|
||||||
// ui->betweenVersionsButton->setEnabled(false);
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (btn) {
|
||||||
|
btn->setEnabled(false);
|
||||||
|
if (!reason.isEmpty())
|
||||||
|
btn->setText(btn->text() + QString(" (%1)").arg(reason));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ModFilterWidget::onVersionFilterChanged(int id)
|
void ModFilterWidget::onVersionFilterChanged(int id)
|
||||||
@ -76,7 +116,7 @@ void ModFilterWidget::onVersionFilterChanged(int id)
|
|||||||
//ui->lowerVersionComboBox->setEnabled(id == VersionButtonID::Between);
|
//ui->lowerVersionComboBox->setEnabled(id == VersionButtonID::Between);
|
||||||
//ui->upperVersionComboBox->setEnabled(id == VersionButtonID::Between);
|
//ui->upperVersionComboBox->setEnabled(id == VersionButtonID::Between);
|
||||||
|
|
||||||
int index = 0;
|
int index = 1;
|
||||||
|
|
||||||
auto cast_id = (VersionButtonID) id;
|
auto cast_id = (VersionButtonID) id;
|
||||||
if (cast_id != m_version_id) {
|
if (cast_id != m_version_id) {
|
||||||
@ -93,10 +133,15 @@ void ModFilterWidget::onVersionFilterChanged(int id)
|
|||||||
break;
|
break;
|
||||||
case(VersionButtonID::Major): {
|
case(VersionButtonID::Major): {
|
||||||
auto versionSplit = mcVersionStr().split(".");
|
auto versionSplit = mcVersionStr().split(".");
|
||||||
for(auto i = Version(QString("%1.%2").arg(versionSplit[0], versionSplit[1])); i <= mcVersion(); index++){
|
|
||||||
m_filter->versions.push_front(i);
|
auto major_version = QString("%1.%2").arg(versionSplit[0], versionSplit[1]);
|
||||||
i = Version(QString("%1.%2.%3").arg(versionSplit[0], versionSplit[1], QString("%1").arg(index)));
|
QString version_str = major_version;
|
||||||
|
|
||||||
|
while (m_version_list->hasVersion(version_str)) {
|
||||||
|
m_filter->versions.emplace_back(version_str);
|
||||||
|
version_str = QString("%1.%2").arg(major_version, QString::number(index++));
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case(VersionButtonID::All):
|
case(VersionButtonID::All):
|
||||||
|
@ -4,6 +4,10 @@
|
|||||||
#include <QButtonGroup>
|
#include <QButtonGroup>
|
||||||
|
|
||||||
#include "Version.h"
|
#include "Version.h"
|
||||||
|
|
||||||
|
#include "meta/Index.h"
|
||||||
|
#include "meta/VersionList.h"
|
||||||
|
|
||||||
#include "minecraft/MinecraftInstance.h"
|
#include "minecraft/MinecraftInstance.h"
|
||||||
#include "minecraft/PackProfile.h"
|
#include "minecraft/PackProfile.h"
|
||||||
|
|
||||||
@ -34,18 +38,22 @@ public:
|
|||||||
std::shared_ptr<Filter> m_filter;
|
std::shared_ptr<Filter> m_filter;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ModFilterWidget(Version def, QWidget* parent = nullptr);
|
static unique_qobject_ptr<ModFilterWidget> create(Version default_version, QWidget* parent = nullptr);
|
||||||
~ModFilterWidget();
|
~ModFilterWidget();
|
||||||
|
|
||||||
void setInstance(MinecraftInstance* instance);
|
void setInstance(MinecraftInstance* instance);
|
||||||
|
|
||||||
/// By default all buttons are enabled
|
/// By default all buttons are enabled
|
||||||
void disableVersionButton(VersionButtonID);
|
void disableVersionButton(VersionButtonID, QString reason = {});
|
||||||
|
|
||||||
auto getFilter() -> std::shared_ptr<Filter>;
|
auto getFilter() -> std::shared_ptr<Filter>;
|
||||||
auto changed() const -> bool { return m_last_version_id != m_version_id; }
|
auto changed() const -> bool { return m_last_version_id != m_version_id; }
|
||||||
|
|
||||||
|
Meta::VersionListPtr versionList() { return m_version_list; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
ModFilterWidget(Version def, QWidget* parent = nullptr);
|
||||||
|
|
||||||
inline auto mcVersionStr() const -> QString { return m_instance ? m_instance->getPackProfile()->getComponentVersion("net.minecraft") : ""; }
|
inline auto mcVersionStr() const -> QString { return m_instance ? m_instance->getPackProfile()->getComponentVersion("net.minecraft") : ""; }
|
||||||
inline auto mcVersion() const -> Version { return { mcVersionStr() }; }
|
inline auto mcVersion() const -> Version { return { mcVersionStr() }; }
|
||||||
|
|
||||||
@ -61,8 +69,12 @@ private:
|
|||||||
|
|
||||||
MinecraftInstance* m_instance = nullptr;
|
MinecraftInstance* m_instance = nullptr;
|
||||||
|
|
||||||
|
|
||||||
|
/* Version stuff */
|
||||||
QButtonGroup m_mcVersion_buttons;
|
QButtonGroup m_mcVersion_buttons;
|
||||||
|
|
||||||
|
Meta::VersionListPtr m_version_list;
|
||||||
|
|
||||||
/* Used to tell if the filter was changed since the last getFilter() call */
|
/* Used to tell if the filter was changed since the last getFilter() call */
|
||||||
VersionButtonID m_last_version_id = VersionButtonID::Strict;
|
VersionButtonID m_last_version_id = VersionButtonID::Strict;
|
||||||
VersionButtonID m_version_id = VersionButtonID::Strict;
|
VersionButtonID m_version_id = VersionButtonID::Strict;
|
||||||
|
@ -15,7 +15,7 @@ A performance optimization daemon.
|
|||||||
|
|
||||||
See [github repo](https://github.com/FeralInteractive/gamemode).
|
See [github repo](https://github.com/FeralInteractive/gamemode).
|
||||||
|
|
||||||
BSD licensed
|
BSD-3-Clause licensed
|
||||||
|
|
||||||
## hoedown
|
## hoedown
|
||||||
Hoedown is a revived fork of Sundown, the Markdown parser based on the original code of the Upskirt library by Natacha Porté.
|
Hoedown is a revived fork of Sundown, the Markdown parser based on the original code of the Upskirt library by Natacha Porté.
|
||||||
@ -155,19 +155,11 @@ Canonical implementation of the murmur2 hash, taken from [SMHasher](https://gith
|
|||||||
|
|
||||||
Public domain (the author disclaimed the copyright).
|
Public domain (the author disclaimed the copyright).
|
||||||
|
|
||||||
## optional-bare
|
|
||||||
|
|
||||||
A simple single-file header-only version of a C++17-like optional for default-constructible, copyable types, for C++98 and later.
|
|
||||||
|
|
||||||
Imported from: https://github.com/martinmoene/optional-bare/commit/0bb1d183bcee1e854c4ea196b533252c51f98b81
|
|
||||||
|
|
||||||
Boost Software License - Version 1.0
|
|
||||||
|
|
||||||
## quazip
|
## quazip
|
||||||
|
|
||||||
A zip manipulation library, forked for MultiMC's use.
|
A zip manipulation library.
|
||||||
|
|
||||||
LGPL 2.1
|
LGPL 2.1 with linking exception.
|
||||||
|
|
||||||
## rainbow
|
## rainbow
|
||||||
Color functions extracted from [KGuiAddons](https://inqlude.org/libraries/kguiaddons.html). Used for adaptive text coloring.
|
Color functions extracted from [KGuiAddons](https://inqlude.org/libraries/kguiaddons.html). Used for adaptive text coloring.
|
||||||
@ -176,7 +168,7 @@ Available either under LGPL version 2.1 or later.
|
|||||||
|
|
||||||
## systeminfo
|
## systeminfo
|
||||||
|
|
||||||
A MultiMC-specific library for probing system information.
|
A PolyMC-specific library for probing system information.
|
||||||
|
|
||||||
Apache 2.0
|
Apache 2.0
|
||||||
|
|
||||||
@ -190,7 +182,7 @@ Licenced under the MIT licence.
|
|||||||
|
|
||||||
## xz-embedded
|
## xz-embedded
|
||||||
|
|
||||||
Tiny implementation of LZMA2 de/compression. This format is only used by Forge to save bandwidth.
|
Tiny implementation of LZMA2 de/compression. This format was only used by Forge to save bandwidth.
|
||||||
|
|
||||||
Public domain.
|
Public domain.
|
||||||
|
|
||||||
|
@ -1,86 +1,110 @@
|
|||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
// MurmurHash2 was written by Austin Appleby, and is placed in the public
|
// MurmurHash2 was written by Austin Appleby, and is placed in the public
|
||||||
// domain. The author hereby disclaims copyright to this source code.
|
// domain. The author hereby disclaims copyright to this source code.
|
||||||
|
//
|
||||||
// Note - This code makes a few assumptions about how your machine behaves -
|
// This was modified as to possibilitate it's usage incrementally.
|
||||||
|
// Those modifications are also placed in the public domain, and the author of
|
||||||
// 1. We can read a 4-byte value from any address without crashing
|
// such modifications hereby disclaims copyright to this source code.
|
||||||
// 2. sizeof(int) == 4
|
|
||||||
|
|
||||||
// And it has a few limitations -
|
|
||||||
|
|
||||||
// 1. It will not work incrementally.
|
|
||||||
// 2. It will not produce the same results on little-endian and big-endian
|
|
||||||
// machines.
|
|
||||||
|
|
||||||
#include "MurmurHash2.h"
|
#include "MurmurHash2.h"
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
// Platform-specific functions and macros
|
|
||||||
|
|
||||||
// Microsoft Visual Studio
|
// 'm' and 'r' are mixing constants generated offline.
|
||||||
|
// They're not really 'magic', they just happen to work well.
|
||||||
|
const uint32_t m = 0x5bd1e995;
|
||||||
|
const int r = 24;
|
||||||
|
|
||||||
#if defined(_MSC_VER)
|
uint32_t MurmurHash2(std::ifstream&& file_stream, std::size_t buffer_size, std::function<bool(char)> filter_out)
|
||||||
|
|
||||||
#define BIG_CONSTANT(x) (x)
|
|
||||||
|
|
||||||
// Other compilers
|
|
||||||
|
|
||||||
#else // defined(_MSC_VER)
|
|
||||||
|
|
||||||
#define BIG_CONSTANT(x) (x##LLU)
|
|
||||||
|
|
||||||
#endif // !defined(_MSC_VER)
|
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
uint64_t MurmurHash2 ( const void* key, int len, uint32_t seed )
|
|
||||||
{
|
{
|
||||||
// 'm' and 'r' are mixing constants generated offline.
|
auto* buffer = new char[buffer_size];
|
||||||
// They're not really 'magic', they just happen to work well.
|
char data[4];
|
||||||
|
|
||||||
const uint32_t m = 0x5bd1e995;
|
int read = 0;
|
||||||
const int r = 24;
|
uint32_t size = 0;
|
||||||
|
|
||||||
// Initialize the hash to a 'random' value
|
// We need the size without the filtered out characters before actually calculating the hash,
|
||||||
|
// to setup the initial value for the hash.
|
||||||
|
do {
|
||||||
|
file_stream.read(buffer, buffer_size);
|
||||||
|
read = file_stream.gcount();
|
||||||
|
for (int i = 0; i < read; i++) {
|
||||||
|
if (!filter_out(buffer[i]))
|
||||||
|
size += 1;
|
||||||
|
}
|
||||||
|
} while (!file_stream.eof());
|
||||||
|
|
||||||
uint32_t h = seed ^ len;
|
file_stream.clear();
|
||||||
|
file_stream.seekg(0, file_stream.beg);
|
||||||
|
|
||||||
|
int index = 0;
|
||||||
|
|
||||||
|
// This forces a seed of 1.
|
||||||
|
IncrementalHashInfo info{ (uint32_t)1 ^ size, (uint32_t)size };
|
||||||
|
do {
|
||||||
|
file_stream.read(buffer, buffer_size);
|
||||||
|
read = file_stream.gcount();
|
||||||
|
for (int i = 0; i < read; i++) {
|
||||||
|
char c = buffer[i];
|
||||||
|
|
||||||
|
if (filter_out(c))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
data[index] = c;
|
||||||
|
index = (index + 1) % 4;
|
||||||
|
|
||||||
// Mix 4 bytes at a time into the hash
|
// Mix 4 bytes at a time into the hash
|
||||||
const auto* data = (const unsigned char*) key;
|
if (index == 0)
|
||||||
while(len >= 4)
|
FourBytes_MurmurHash2((unsigned char*)&data, info);
|
||||||
{
|
}
|
||||||
|
} while (!file_stream.eof());
|
||||||
|
|
||||||
|
// Do one last bit shuffle in the hash
|
||||||
|
FourBytes_MurmurHash2((unsigned char*)&data, info);
|
||||||
|
|
||||||
|
delete[] buffer;
|
||||||
|
|
||||||
|
file_stream.close();
|
||||||
|
return info.h;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FourBytes_MurmurHash2(const unsigned char* data, IncrementalHashInfo& prev)
|
||||||
|
{
|
||||||
|
if (prev.len >= 4) {
|
||||||
|
// Not the final mix
|
||||||
uint32_t k = *(uint32_t*)data;
|
uint32_t k = *(uint32_t*)data;
|
||||||
|
|
||||||
k *= m;
|
k *= m;
|
||||||
k ^= k >> r;
|
k ^= k >> r;
|
||||||
k *= m;
|
k *= m;
|
||||||
|
|
||||||
h *= m;
|
prev.h *= m;
|
||||||
h ^= k;
|
prev.h ^= k;
|
||||||
|
|
||||||
data += 4*sizeof(char);
|
prev.len -= 4;
|
||||||
len -= 4;
|
} else {
|
||||||
}
|
// The final mix
|
||||||
|
|
||||||
// Handle the last few bytes of the input array
|
// Handle the last few bytes of the input array
|
||||||
|
switch (prev.len) {
|
||||||
switch(len)
|
case 3:
|
||||||
{
|
prev.h ^= data[2] << 16;
|
||||||
case 3: h ^= data[2] << 16;
|
case 2:
|
||||||
case 2: h ^= data[1] << 8;
|
prev.h ^= data[1] << 8;
|
||||||
case 1: h ^= data[0];
|
case 1:
|
||||||
h *= m;
|
prev.h ^= data[0];
|
||||||
|
prev.h *= m;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Do a few final mixes of the hash to ensure the last few
|
// Do a few final mixes of the hash to ensure the last few
|
||||||
// bytes are well-incorporated.
|
// bytes are well-incorporated.
|
||||||
|
|
||||||
h ^= h >> 13;
|
prev.h ^= prev.h >> 13;
|
||||||
h *= m;
|
prev.h *= m;
|
||||||
h ^= h >> 15;
|
prev.h ^= prev.h >> 15;
|
||||||
|
|
||||||
return h;
|
prev.len = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
|
@ -1,30 +1,33 @@
|
|||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
// MurmurHash2 was written by Austin Appleby, and is placed in the public
|
// The original MurmurHash2 was written by Austin Appleby, and is placed in the
|
||||||
// domain. The author hereby disclaims copyright to this source code.
|
// public domain. The author hereby disclaims copyright to this source code.
|
||||||
|
//
|
||||||
|
// This was modified as to possibilitate it's usage incrementally.
|
||||||
|
// Those modifications are also placed in the public domain, and the author of
|
||||||
|
// such modifications hereby disclaims copyright to this source code.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------
|
#include <cstdint>
|
||||||
// Platform-specific functions and macros
|
#include <fstream>
|
||||||
|
|
||||||
// Microsoft Visual Studio
|
#include <functional>
|
||||||
|
|
||||||
#if defined(_MSC_VER) && (_MSC_VER < 1600)
|
|
||||||
|
|
||||||
typedef unsigned char uint8_t;
|
|
||||||
typedef unsigned int uint32_t;
|
|
||||||
typedef unsigned __int64 uint64_t;
|
|
||||||
|
|
||||||
// Other compilers
|
|
||||||
|
|
||||||
#else // defined(_MSC_VER)
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
#endif // !defined(_MSC_VER)
|
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
uint64_t MurmurHash2 ( const void* key, int len, uint32_t seed = 1 );
|
#define KiB 1024
|
||||||
|
#define MiB 1024*KiB
|
||||||
|
|
||||||
|
uint32_t MurmurHash2(
|
||||||
|
std::ifstream&& file_stream,
|
||||||
|
std::size_t buffer_size = 4*MiB,
|
||||||
|
std::function<bool(char)> filter_out = [](char) { return false; });
|
||||||
|
|
||||||
|
struct IncrementalHashInfo {
|
||||||
|
uint32_t h;
|
||||||
|
uint32_t len;
|
||||||
|
};
|
||||||
|
|
||||||
|
void FourBytes_MurmurHash2(const unsigned char* data, IncrementalHashInfo& prev);
|
||||||
|
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
cmake_minimum_required(VERSION 3.9.4)
|
|
||||||
project(optional-bare)
|
|
||||||
|
|
||||||
add_library(optional-bare INTERFACE)
|
|
||||||
target_include_directories(optional-bare INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include")
|
|
@ -1,23 +0,0 @@
|
|||||||
Boost Software License - Version 1.0 - August 17th, 2003
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person or organization
|
|
||||||
obtaining a copy of the software and accompanying documentation covered by
|
|
||||||
this license (the "Software") to use, reproduce, display, distribute,
|
|
||||||
execute, and transmit the Software, and to prepare derivative works of the
|
|
||||||
Software, and to permit third-parties to whom the Software is furnished to
|
|
||||||
do so, all subject to the following:
|
|
||||||
|
|
||||||
The copyright notices in the Software and this entire statement, including
|
|
||||||
the above license grant, this restriction and the following disclaimer,
|
|
||||||
must be included in all copies of the Software, in whole or in part, and
|
|
||||||
all derivative works of the Software, unless such copies or derivative
|
|
||||||
works are solely in the form of machine-executable object code generated by
|
|
||||||
a source language processor.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
|
||||||
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
|
||||||
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
|
||||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
||||||
DEALINGS IN THE SOFTWARE.
|
|
@ -1,5 +0,0 @@
|
|||||||
# optional bare
|
|
||||||
|
|
||||||
A simple single-file header-only version of a C++17-like optional for default-constructible, copyable types, for C++98 and later.
|
|
||||||
|
|
||||||
Imported from: https://github.com/martinmoene/optional-bare/commit/0bb1d183bcee1e854c4ea196b533252c51f98b81
|
|
@ -1,508 +0,0 @@
|
|||||||
//
|
|
||||||
// Copyright 2017-2019 by Martin Moene
|
|
||||||
//
|
|
||||||
// https://github.com/martinmoene/optional-bare
|
|
||||||
//
|
|
||||||
// Distributed under the Boost Software License, Version 1.0.
|
|
||||||
// (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
|
||||||
|
|
||||||
#ifndef NONSTD_OPTIONAL_BARE_HPP
|
|
||||||
#define NONSTD_OPTIONAL_BARE_HPP
|
|
||||||
|
|
||||||
#define optional_bare_MAJOR 1
|
|
||||||
#define optional_bare_MINOR 1
|
|
||||||
#define optional_bare_PATCH 0
|
|
||||||
|
|
||||||
#define optional_bare_VERSION optional_STRINGIFY(optional_bare_MAJOR) "." optional_STRINGIFY(optional_bare_MINOR) "." optional_STRINGIFY(optional_bare_PATCH)
|
|
||||||
|
|
||||||
#define optional_STRINGIFY( x ) optional_STRINGIFY_( x )
|
|
||||||
#define optional_STRINGIFY_( x ) #x
|
|
||||||
|
|
||||||
// optional-bare configuration:
|
|
||||||
|
|
||||||
#define optional_OPTIONAL_DEFAULT 0
|
|
||||||
#define optional_OPTIONAL_NONSTD 1
|
|
||||||
#define optional_OPTIONAL_STD 2
|
|
||||||
|
|
||||||
#if !defined( optional_CONFIG_SELECT_OPTIONAL )
|
|
||||||
# define optional_CONFIG_SELECT_OPTIONAL ( optional_HAVE_STD_OPTIONAL ? optional_OPTIONAL_STD : optional_OPTIONAL_NONSTD )
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Control presence of exception handling (try and auto discover):
|
|
||||||
|
|
||||||
#ifndef optional_CONFIG_NO_EXCEPTIONS
|
|
||||||
# if _MSC_VER
|
|
||||||
# include <cstddef> // for _HAS_EXCEPTIONS
|
|
||||||
# endif
|
|
||||||
# if _MSC_VER
|
|
||||||
# include <cstddef> // for _HAS_EXCEPTIONS
|
|
||||||
# endif
|
|
||||||
# if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || (_HAS_EXCEPTIONS)
|
|
||||||
# define optional_CONFIG_NO_EXCEPTIONS 0
|
|
||||||
# else
|
|
||||||
# define optional_CONFIG_NO_EXCEPTIONS 1
|
|
||||||
# endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// C++ language version detection (C++20 is speculative):
|
|
||||||
// Note: VC14.0/1900 (VS2015) lacks too much from C++14.
|
|
||||||
|
|
||||||
#ifndef optional_CPLUSPLUS
|
|
||||||
# if defined(_MSVC_LANG ) && !defined(__clang__)
|
|
||||||
# define optional_CPLUSPLUS (_MSC_VER == 1900 ? 201103L : _MSVC_LANG )
|
|
||||||
# else
|
|
||||||
# define optional_CPLUSPLUS __cplusplus
|
|
||||||
# endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define optional_CPP98_OR_GREATER ( optional_CPLUSPLUS >= 199711L )
|
|
||||||
#define optional_CPP11_OR_GREATER ( optional_CPLUSPLUS >= 201103L )
|
|
||||||
#define optional_CPP14_OR_GREATER ( optional_CPLUSPLUS >= 201402L )
|
|
||||||
#define optional_CPP17_OR_GREATER ( optional_CPLUSPLUS >= 201703L )
|
|
||||||
#define optional_CPP20_OR_GREATER ( optional_CPLUSPLUS >= 202000L )
|
|
||||||
|
|
||||||
// C++ language version (represent 98 as 3):
|
|
||||||
|
|
||||||
#define optional_CPLUSPLUS_V ( optional_CPLUSPLUS / 100 - (optional_CPLUSPLUS > 200000 ? 2000 : 1994) )
|
|
||||||
|
|
||||||
// Use C++17 std::optional if available and requested:
|
|
||||||
|
|
||||||
#if optional_CPP17_OR_GREATER && defined(__has_include )
|
|
||||||
# if __has_include( <optional> )
|
|
||||||
# define optional_HAVE_STD_OPTIONAL 1
|
|
||||||
# else
|
|
||||||
# define optional_HAVE_STD_OPTIONAL 0
|
|
||||||
# endif
|
|
||||||
#else
|
|
||||||
# define optional_HAVE_STD_OPTIONAL 0
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#define optional_USES_STD_OPTIONAL ( (optional_CONFIG_SELECT_OPTIONAL == optional_OPTIONAL_STD) || ((optional_CONFIG_SELECT_OPTIONAL == optional_OPTIONAL_DEFAULT) && optional_HAVE_STD_OPTIONAL) )
|
|
||||||
|
|
||||||
//
|
|
||||||
// Using std::optional:
|
|
||||||
//
|
|
||||||
|
|
||||||
#if optional_USES_STD_OPTIONAL
|
|
||||||
|
|
||||||
#include <optional>
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
namespace nonstd {
|
|
||||||
|
|
||||||
using std::in_place;
|
|
||||||
using std::in_place_type;
|
|
||||||
using std::in_place_index;
|
|
||||||
using std::in_place_t;
|
|
||||||
using std::in_place_type_t;
|
|
||||||
using std::in_place_index_t;
|
|
||||||
|
|
||||||
using std::optional;
|
|
||||||
using std::bad_optional_access;
|
|
||||||
using std::hash;
|
|
||||||
|
|
||||||
using std::nullopt;
|
|
||||||
using std::nullopt_t;
|
|
||||||
|
|
||||||
using std::operator==;
|
|
||||||
using std::operator!=;
|
|
||||||
using std::operator<;
|
|
||||||
using std::operator<=;
|
|
||||||
using std::operator>;
|
|
||||||
using std::operator>=;
|
|
||||||
using std::make_optional;
|
|
||||||
using std::swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
#else // optional_USES_STD_OPTIONAL
|
|
||||||
|
|
||||||
#include <cassert>
|
|
||||||
|
|
||||||
#if ! optional_CONFIG_NO_EXCEPTIONS
|
|
||||||
# include <stdexcept>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace nonstd { namespace optional_bare {
|
|
||||||
|
|
||||||
// type for nullopt
|
|
||||||
|
|
||||||
struct nullopt_t
|
|
||||||
{
|
|
||||||
struct init{};
|
|
||||||
nullopt_t( init ) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
// extra parenthesis to prevent the most vexing parse:
|
|
||||||
|
|
||||||
const nullopt_t nullopt(( nullopt_t::init() ));
|
|
||||||
|
|
||||||
// optional access error.
|
|
||||||
|
|
||||||
#if ! optional_CONFIG_NO_EXCEPTIONS
|
|
||||||
|
|
||||||
class bad_optional_access : public std::logic_error
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
explicit bad_optional_access()
|
|
||||||
: logic_error( "bad optional access" ) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // optional_CONFIG_NO_EXCEPTIONS
|
|
||||||
|
|
||||||
// Simplistic optional: requires T to be default constructible, copyable.
|
|
||||||
|
|
||||||
template< typename T >
|
|
||||||
class optional
|
|
||||||
{
|
|
||||||
private:
|
|
||||||
typedef void (optional::*safe_bool)() const;
|
|
||||||
|
|
||||||
public:
|
|
||||||
typedef T value_type;
|
|
||||||
|
|
||||||
optional()
|
|
||||||
: has_value_( false )
|
|
||||||
{}
|
|
||||||
|
|
||||||
optional( nullopt_t )
|
|
||||||
: has_value_( false )
|
|
||||||
{}
|
|
||||||
|
|
||||||
optional( T const & arg )
|
|
||||||
: has_value_( true )
|
|
||||||
, value_ ( arg )
|
|
||||||
{}
|
|
||||||
|
|
||||||
template< class U >
|
|
||||||
optional( optional<U> const & other )
|
|
||||||
: has_value_( other.has_value() )
|
|
||||||
, value_ ( other.value() )
|
|
||||||
{}
|
|
||||||
|
|
||||||
optional & operator=( nullopt_t )
|
|
||||||
{
|
|
||||||
reset();
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
template< class U >
|
|
||||||
optional & operator=( optional<U> const & other )
|
|
||||||
{
|
|
||||||
has_value_ = other.has_value();
|
|
||||||
value_ = other.value();
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
void swap( optional & rhs )
|
|
||||||
{
|
|
||||||
using std::swap;
|
|
||||||
if ( has_value() == true && rhs.has_value() == true ) { swap( **this, *rhs ); }
|
|
||||||
else if ( has_value() == false && rhs.has_value() == true ) { initialize( *rhs ); rhs.reset(); }
|
|
||||||
else if ( has_value() == true && rhs.has_value() == false ) { rhs.initialize( **this ); reset(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
// observers
|
|
||||||
|
|
||||||
value_type const * operator->() const
|
|
||||||
{
|
|
||||||
return assert( has_value() ),
|
|
||||||
&value_;
|
|
||||||
}
|
|
||||||
|
|
||||||
value_type * operator->()
|
|
||||||
{
|
|
||||||
return assert( has_value() ),
|
|
||||||
&value_;
|
|
||||||
}
|
|
||||||
|
|
||||||
value_type const & operator*() const
|
|
||||||
{
|
|
||||||
return assert( has_value() ),
|
|
||||||
value_;
|
|
||||||
}
|
|
||||||
|
|
||||||
value_type & operator*()
|
|
||||||
{
|
|
||||||
return assert( has_value() ),
|
|
||||||
value_;
|
|
||||||
}
|
|
||||||
|
|
||||||
#if optional_CPP11_OR_GREATER
|
|
||||||
explicit operator bool() const
|
|
||||||
{
|
|
||||||
return has_value();
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
operator safe_bool() const
|
|
||||||
{
|
|
||||||
return has_value() ? &optional::this_type_does_not_support_comparisons : 0;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
bool has_value() const
|
|
||||||
{
|
|
||||||
return has_value_;
|
|
||||||
}
|
|
||||||
|
|
||||||
value_type const & value() const
|
|
||||||
{
|
|
||||||
#if optional_CONFIG_NO_EXCEPTIONS
|
|
||||||
assert( has_value() );
|
|
||||||
#else
|
|
||||||
if ( ! has_value() )
|
|
||||||
throw bad_optional_access();
|
|
||||||
#endif
|
|
||||||
return value_;
|
|
||||||
}
|
|
||||||
|
|
||||||
value_type & value()
|
|
||||||
{
|
|
||||||
#if optional_CONFIG_NO_EXCEPTIONS
|
|
||||||
assert( has_value() );
|
|
||||||
#else
|
|
||||||
if ( ! has_value() )
|
|
||||||
throw bad_optional_access();
|
|
||||||
#endif
|
|
||||||
return value_;
|
|
||||||
}
|
|
||||||
|
|
||||||
template< class U >
|
|
||||||
value_type value_or( U const & v ) const
|
|
||||||
{
|
|
||||||
return has_value() ? value() : static_cast<value_type>( v );
|
|
||||||
}
|
|
||||||
|
|
||||||
// modifiers
|
|
||||||
|
|
||||||
void reset()
|
|
||||||
{
|
|
||||||
has_value_ = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
void this_type_does_not_support_comparisons() const {}
|
|
||||||
|
|
||||||
template< typename V >
|
|
||||||
void initialize( V const & value )
|
|
||||||
{
|
|
||||||
assert( ! has_value() );
|
|
||||||
value_ = value;
|
|
||||||
has_value_ = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
bool has_value_;
|
|
||||||
value_type value_;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Relational operators
|
|
||||||
|
|
||||||
template< typename T, typename U >
|
|
||||||
inline bool operator==( optional<T> const & x, optional<U> const & y )
|
|
||||||
{
|
|
||||||
return bool(x) != bool(y) ? false : bool(x) == false ? true : *x == *y;
|
|
||||||
}
|
|
||||||
|
|
||||||
template< typename T, typename U >
|
|
||||||
inline bool operator!=( optional<T> const & x, optional<U> const & y )
|
|
||||||
{
|
|
||||||
return !(x == y);
|
|
||||||
}
|
|
||||||
|
|
||||||
template< typename T, typename U >
|
|
||||||
inline bool operator<( optional<T> const & x, optional<U> const & y )
|
|
||||||
{
|
|
||||||
return (!y) ? false : (!x) ? true : *x < *y;
|
|
||||||
}
|
|
||||||
|
|
||||||
template< typename T, typename U >
|
|
||||||
inline bool operator>( optional<T> const & x, optional<U> const & y )
|
|
||||||
{
|
|
||||||
return (y < x);
|
|
||||||
}
|
|
||||||
|
|
||||||
template< typename T, typename U >
|
|
||||||
inline bool operator<=( optional<T> const & x, optional<U> const & y )
|
|
||||||
{
|
|
||||||
return !(y < x);
|
|
||||||
}
|
|
||||||
|
|
||||||
template< typename T, typename U >
|
|
||||||
inline bool operator>=( optional<T> const & x, optional<U> const & y )
|
|
||||||
{
|
|
||||||
return !(x < y);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comparison with nullopt
|
|
||||||
|
|
||||||
template< typename T >
|
|
||||||
inline bool operator==( optional<T> const & x, nullopt_t )
|
|
||||||
{
|
|
||||||
return (!x);
|
|
||||||
}
|
|
||||||
|
|
||||||
template< typename T >
|
|
||||||
inline bool operator==( nullopt_t, optional<T> const & x )
|
|
||||||
{
|
|
||||||
return (!x);
|
|
||||||
}
|
|
||||||
|
|
||||||
template< typename T >
|
|
||||||
inline bool operator!=( optional<T> const & x, nullopt_t )
|
|
||||||
{
|
|
||||||
return bool(x);
|
|
||||||
}
|
|
||||||
|
|
||||||
template< typename T >
|
|
||||||
inline bool operator!=( nullopt_t, optional<T> const & x )
|
|
||||||
{
|
|
||||||
return bool(x);
|
|
||||||
}
|
|
||||||
|
|
||||||
template< typename T >
|
|
||||||
inline bool operator<( optional<T> const &, nullopt_t )
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
template< typename T >
|
|
||||||
inline bool operator<( nullopt_t, optional<T> const & x )
|
|
||||||
{
|
|
||||||
return bool(x);
|
|
||||||
}
|
|
||||||
|
|
||||||
template< typename T >
|
|
||||||
inline bool operator<=( optional<T> const & x, nullopt_t )
|
|
||||||
{
|
|
||||||
return (!x);
|
|
||||||
}
|
|
||||||
|
|
||||||
template< typename T >
|
|
||||||
inline bool operator<=( nullopt_t, optional<T> const & )
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
template< typename T >
|
|
||||||
inline bool operator>( optional<T> const & x, nullopt_t )
|
|
||||||
{
|
|
||||||
return bool(x);
|
|
||||||
}
|
|
||||||
|
|
||||||
template< typename T >
|
|
||||||
inline bool operator>( nullopt_t, optional<T> const & )
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
template< typename T >
|
|
||||||
inline bool operator>=( optional<T> const &, nullopt_t )
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
template< typename T >
|
|
||||||
inline bool operator>=( nullopt_t, optional<T> const & x )
|
|
||||||
{
|
|
||||||
return (!x);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comparison with T
|
|
||||||
|
|
||||||
template< typename T, typename U >
|
|
||||||
inline bool operator==( optional<T> const & x, U const & v )
|
|
||||||
{
|
|
||||||
return bool(x) ? *x == v : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
template< typename T, typename U >
|
|
||||||
inline bool operator==( U const & v, optional<T> const & x )
|
|
||||||
{
|
|
||||||
return bool(x) ? v == *x : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
template< typename T, typename U >
|
|
||||||
inline bool operator!=( optional<T> const & x, U const & v )
|
|
||||||
{
|
|
||||||
return bool(x) ? *x != v : true;
|
|
||||||
}
|
|
||||||
|
|
||||||
template< typename T, typename U >
|
|
||||||
inline bool operator!=( U const & v, optional<T> const & x )
|
|
||||||
{
|
|
||||||
return bool(x) ? v != *x : true;
|
|
||||||
}
|
|
||||||
|
|
||||||
template< typename T, typename U >
|
|
||||||
inline bool operator<( optional<T> const & x, U const & v )
|
|
||||||
{
|
|
||||||
return bool(x) ? *x < v : true;
|
|
||||||
}
|
|
||||||
|
|
||||||
template< typename T, typename U >
|
|
||||||
inline bool operator<( U const & v, optional<T> const & x )
|
|
||||||
{
|
|
||||||
return bool(x) ? v < *x : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
template< typename T, typename U >
|
|
||||||
inline bool operator<=( optional<T> const & x, U const & v )
|
|
||||||
{
|
|
||||||
return bool(x) ? *x <= v : true;
|
|
||||||
}
|
|
||||||
|
|
||||||
template< typename T, typename U >
|
|
||||||
inline bool operator<=( U const & v, optional<T> const & x )
|
|
||||||
{
|
|
||||||
return bool(x) ? v <= *x : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
template< typename T, typename U >
|
|
||||||
inline bool operator>( optional<T> const & x, U const & v )
|
|
||||||
{
|
|
||||||
return bool(x) ? *x > v : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
template< typename T, typename U >
|
|
||||||
inline bool operator>( U const & v, optional<T> const & x )
|
|
||||||
{
|
|
||||||
return bool(x) ? v > *x : true;
|
|
||||||
}
|
|
||||||
|
|
||||||
template< typename T, typename U >
|
|
||||||
inline bool operator>=( optional<T> const & x, U const & v )
|
|
||||||
{
|
|
||||||
return bool(x) ? *x >= v : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
template< typename T, typename U >
|
|
||||||
inline bool operator>=( U const & v, optional<T> const & x )
|
|
||||||
{
|
|
||||||
return bool(x) ? v >= *x : true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Specialized algorithms
|
|
||||||
|
|
||||||
template< typename T >
|
|
||||||
void swap( optional<T> & x, optional<T> & y )
|
|
||||||
{
|
|
||||||
x.swap( y );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convenience function to create an optional.
|
|
||||||
|
|
||||||
template< typename T >
|
|
||||||
inline optional<T> make_optional( T const & v )
|
|
||||||
{
|
|
||||||
return optional<T>( v );
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace optional-bare
|
|
||||||
|
|
||||||
using namespace optional_bare;
|
|
||||||
|
|
||||||
} // namespace nonstd
|
|
||||||
|
|
||||||
#endif // optional_USES_STD_OPTIONAL
|
|
||||||
|
|
||||||
#endif // NONSTD_OPTIONAL_BARE_HPP
|
|
@ -6,7 +6,7 @@
|
|||||||
</provides>
|
</provides>
|
||||||
<launchable type="desktop-id">org.polymc.PolyMC.desktop</launchable>
|
<launchable type="desktop-id">org.polymc.PolyMC.desktop</launchable>
|
||||||
<name>PolyMC</name>
|
<name>PolyMC</name>
|
||||||
<developer_name>PolyMC Team</developer_name>
|
<developer_name>PolyMC</developer_name>
|
||||||
<summary>A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once</summary>
|
<summary>A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once</summary>
|
||||||
<metadata_license>CC0-1.0</metadata_license>
|
<metadata_license>CC0-1.0</metadata_license>
|
||||||
<project_license>GPL-3.0-only</project_license>
|
<project_license>GPL-3.0-only</project_license>
|
||||||
@ -16,35 +16,39 @@
|
|||||||
<p>PolyMC is a custom launcher for Minecraft that focuses on predictability, long term stability and simplicity.</p>
|
<p>PolyMC is a custom launcher for Minecraft that focuses on predictability, long term stability and simplicity.</p>
|
||||||
<p>Features:</p>
|
<p>Features:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Easily install game modifications, such as Fabric or Forge</li>
|
<li>Easily install game modifications, such as Fabric, Forge and Quilt</li>
|
||||||
<li>Control your java settings</li>
|
<li>Control your java settings</li>
|
||||||
<li>Manage worlds and resource packs from the launcher</li>
|
<li>Manage worlds and resource packs from the launcher</li>
|
||||||
<li>See logs and other details easily</li>
|
<li>See logs and other details easily</li>
|
||||||
<li>Kill Minecraft in case of a crash/freeze</li>
|
<li>Kill Minecraft in case of a crash/freeze</li>
|
||||||
<li>Isolate minecraft instances to keep everything clean</li>
|
<li>Isolate minecraft instances to keep everything clean</li>
|
||||||
<li>Install mods directly from the launcher</li>
|
<li>Install and update mods directly from the launcher</li>
|
||||||
</ul>
|
</ul>
|
||||||
</description>
|
</description>
|
||||||
<screenshots>
|
<screenshots>
|
||||||
<screenshot type="default">
|
<screenshot type="default">
|
||||||
<caption>The main PolyMC window</caption>
|
<caption>The main PolyMC window</caption>
|
||||||
<image type="source" width="931" height="759">https://polymc.org/img/screenshots/LauncherDark.png</image>
|
<image type="source" width="578" height="452">https://polymc.org/img/screenshots/LauncherDark.png</image>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
<screenshot>
|
<screenshot>
|
||||||
<caption>Modpack installation</caption>
|
<caption>Modpack installation</caption>
|
||||||
<image type="source" width="860" height="848">https://polymc.org/img/screenshots/ModpackInstallDark.png</image>
|
<image type="source" width="523" height="452">https://polymc.org/img/screenshots/ModpackInstallDark.png</image>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
<screenshot>
|
<screenshot>
|
||||||
<caption>Mod installation</caption>
|
<caption>Mod installation</caption>
|
||||||
<image type="source" width="1018" height="858">https://polymc.org/img/screenshots/ModInstallDark.png</image>
|
<image type="source" width="654" height="452">https://polymc.org/img/screenshots/ModInstallDark.png</image>
|
||||||
|
</screenshot>
|
||||||
|
<screenshot>
|
||||||
|
<caption>Mod updating</caption>
|
||||||
|
<image type="source" width="490" height="452">https://polymc.org/img/screenshots/ModUpdateDark.png</image>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
<screenshot>
|
<screenshot>
|
||||||
<caption>Instance management</caption>
|
<caption>Instance management</caption>
|
||||||
<image type="source" width="777" height="693">https://polymc.org/img/screenshots/PropertiesDark.png</image>
|
<image type="source" width="667" height="452">https://polymc.org/img/screenshots/PropertiesDark.png</image>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
<screenshot>
|
<screenshot>
|
||||||
<caption>Cat :)</caption>
|
<caption>Cat :)</caption>
|
||||||
<image type="source" width="931" height="759">https://polymc.org/img/screenshots/LauncherCatDark.png</image>
|
<image type="source" width="555" height="452">https://polymc.org/img/screenshots/LauncherCatDark.png</image>
|
||||||
</screenshot>
|
</screenshot>
|
||||||
</screenshots>
|
</screenshots>
|
||||||
<releases>
|
<releases>
|
||||||
|
Loading…
Reference in New Issue
Block a user