Compare commits

...

26 Commits

Author SHA1 Message Date
Sefa Eyeoglu
6e65cd4405
Merge pull request #1118 from flowln/backport_major_version_filter_fix 2022-09-08 18:54:56 +02:00
flow
98fbb3613d
refactor: create mod pages and filter widget by factory methods
This takes most expensive operations out of the constructors.

Signed-off-by: flow <flowlnlnln@gmail.com>
2022-09-08 12:47:37 -03:00
flow
2666beafb1
fix: use more robust method of finding matches for major version
This uses the proper version list to find all MC versions matching the
major number (_don't say anything about SemVer_ 🔫).

Signed-off-by: flow <flowlnlnln@gmail.com>
2022-09-08 12:37:11 -03:00
Sefa Eyeoglu
cf8a76be6b
chore: bump version
Signed-off-by: Sefa Eyeoglu <contact@scrumplex.net>
2022-09-08 15:24:48 +02:00
Sefa Eyeoglu
e96f482544
Merge pull request #965 from flowln/fat_files_in_memory
Refactor a bit EnsureMetadataTask and calculate hashes in a incremental manner
2022-09-08 14:35:59 +02:00
LennyMcLennington
dc32f0671b
Merge pull request #941 from Scrumplex/bump-cxx-standard
Bump to C++17
2022-09-08 14:35:30 +02:00
Sefa Eyeoglu
d6d7794cd0
Merge pull request #1107 from DioEgizio/smaller-about 2022-09-08 13:11:06 +02:00
Sefa Eyeoglu
94d95d45d4
Merge pull request #1095 from flowln/ensure_file_path_in_override 2022-09-08 13:11:06 +02:00
flow
5e767a91d9
Merge pull request #1080 from flowln/eternal_cache
Never invalidate libraries cache entries by time elapsed
2022-09-08 13:11:06 +02:00
flow
c89f8b4657
Merge pull request #1087 from DioEgizio/fix-ftblegacy-url
fix: fix urls on ftb legacy
2022-09-08 13:11:06 +02:00
flow
d7fc0f53d3
Merge pull request #1073 from DioEgizio/update-copying
fix(COPYING): fix COPYING.md by adding some missing copyright notices
2022-09-08 13:11:04 +02:00
flow
e81cf8a845
Merge pull request #1035 from Scrumplex/fix-coremods
Make Coremods / Mods seperation more clear
2022-09-08 13:10:37 +02:00
Sefa Eyeoglu
c116885ab1
Merge pull request #1044 from flowln/better_orphan_fix 2022-09-08 13:10:37 +02:00
Sefa Eyeoglu
d68d5ca23f
Merge pull request #1007 from Gingeh/disable-update-button 2022-09-08 13:10:37 +02:00
dada513
6d94338a56
Merge pull request #1058 from DioEgizio/fix-update-org.polymc.PolyMC.metainfo.xml.in 2022-09-08 13:10:37 +02:00
Sefa Eyeoglu
1e1a1cef05
Merge pull request #1049 from flowln/waiting_for_news_-_- 2022-09-08 13:10:37 +02:00
Sefa Eyeoglu
34bab3e1b2
Merge pull request #968 from magneticflux-/utf8-logging
Decode process lines as UTF-8
2022-09-08 13:10:37 +02:00
timoreo
be6d6501e8
Merge pull request #1039 from budak7273/fix-world-safety-nag-title-text 2022-09-08 13:10:37 +02:00
timoreo
10a70732ce
Merge pull request #920 from flowln/metacache_fix 2022-09-08 13:10:37 +02:00
timoreo
a725dc82a7
Merge pull request #1018 from Scrumplex/fix-infinite-auth-loop 2022-09-08 13:10:37 +02:00
flow
97ce8a94e9
Merge pull request #1017 from flowln/kill_orphan_metadata
Remove orphaned metadata to avoid problems with auto-updating instances
2022-09-08 13:10:37 +02:00
flow
85f0904872
Merge pull request #1014 from DioEgizio/downgrade-qt-macos
chore: downgrade to Qt 6.3.0 on macos
2022-09-08 13:10:37 +02:00
flow
cb0a5e42df
Merge pull request #1006 from DioEgizio/appimage-ubuntu-moment
fix: work around ubuntu 22.04 openssl appimage issues by copying openssl libs
2022-09-08 13:10:36 +02:00
Sefa Eyeoglu
a0c7fa30c0
Merge pull request #1019 from Scrumplex/fix-openbsd-root
Add root path detection on OpenBSD
2022-09-08 13:10:36 +02:00
LennyMcLennington
b7490b479c
Merge pull request #970 from PolyMC/Bump-1.4.1
bump version to 1.4.1
2022-07-28 09:42:56 +01:00
LennyMcLennington
0d35edbbf3
bump version 2022-07-25 06:44:34 +01:00
64 changed files with 866 additions and 947 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
//----------------------------------------------------------------------------- //-----------------------------------------------------------------------------

View File

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

View File

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

View File

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

View File

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

View File

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