Compare commits

...

45 Commits
1.2.0 ... 1.2.2

Author SHA1 Message Date
da25f3b84e Merge pull request #533 from DioEgizio/patch-5 2022-05-08 21:43:18 +02:00
1e34de98ab Merge pull request #534 from DioEgizio/stable 2022-05-08 21:43:06 +02:00
7b46f50cf1 Merge pull request #530 from ryanccn/cfcore-backport 2022-05-08 21:42:40 +02:00
cab40026f2 Merge pull request #475 from Scrumplex/fix-hide-all-tokens
Hide all tokens for non-Debug builds for log and logfiles
2022-05-08 16:56:26 +02:00
ac66bddeda Merge pull request #482 from TheCodex6824/mojang-auth-fix 2022-05-08 16:52:31 +02:00
f4237be9bd Merge pull request #492 from DioEgizio/appimage-fix 2022-05-08 16:52:14 +02:00
dd11ccb3fd bump to 1.2.2 2022-05-08 16:30:12 +02:00
c4549a5375 Update launcher/modplatform/flame/FlameModIndex.cpp
Co-authored-by: flow <thiagodonato300@gmail.com>
2022-05-08 16:25:51 +02:00
e9b3140d12 Update launcher/modplatform/flame/FlameModIndex.cpp 2022-05-08 16:25:45 +02:00
22f5128e39 adopt changes from #497 remapped 2022-05-08 15:22:50 +08:00
ae1aa6f63e gitignore stuff 2022-05-08 15:02:21 +08:00
5adcc26190 Merge pull request #472 from Scrumplex/fix-download-mods-snapshot
fix: disable major version match for snapshots
2022-04-20 21:59:25 +10:00
db6dae7541 fix: disable major version match for snapshots 2022-04-20 09:56:53 +02:00
4e97f2a6fe Merge pull request #470 from kthchew/fix/ml-selector-crash
Fix crash if no Minecraft version is selected in the new instance screen
2022-04-20 09:08:48 +02:00
4643046989 Merge pull request #465 from DioEgizio/bump-1.2.1
bump to 1.2.1
2022-04-20 16:19:20 +10:00
3c3ce71214 Merge pull request #467 from PolyMC/revert-429-develop
Revert "better FreeBSD support"
2022-04-20 08:17:19 +02:00
0682fe544a Fix crash if no Minecraft version is selected in the new instance screen 2022-04-19 22:20:00 -04:00
27e803e4a4 Merge pull request #463 from txtsd/enter_shenanigans
Only trigger macOS instance rename on KeyDown
2022-04-20 08:34:30 +10:00
405c44c9e1 Merge pull request #462 from Scrumplex/fix-world-size-sort
fix: use size in bytes to sort by world size
2022-04-20 08:32:38 +10:00
4c5f701b05 Revert "better FreeBSD support" 2022-04-19 21:49:54 +02:00
c7563a5f7c Merge pull request #454 from kthchew/feature/hardened-runtime 2022-04-19 21:47:57 +02:00
3ff3c335bc Merge pull request #452 from Scrumplex/fix-download-mods-crash 2022-04-19 21:47:05 +02:00
b9d5e1bbf1 Merge pull request #451 from Scrumplex/fix-modloader-selection 2022-04-19 21:01:43 +02:00
e313b366a0 Merge pull request #444 from Scrumplex/fix-release-workflow
Fix release workflow
2022-04-19 21:01:23 +02:00
027c666265 Merge pull request #450 from Scrumplex/fix-quilt-mod-dl 2022-04-19 20:36:37 +02:00
4a3d94aaf9 fix: fix filename of linux portable 2022-04-19 19:06:17 +02:00
c637e3657c bump to 1.2.1 2022-04-19 18:07:42 +02:00
9462dd3ddc Improve security by enabling hardened runtime for macOS
This change also fixes a bug on recent versions of macOS where Minecraft mods that requested access to the microphone would silently fail.
2022-04-19 11:36:03 -04:00
c3524a9d57 fix: bundle binary tarball as user root 2022-04-19 16:18:11 +02:00
53ff66c317 fix: update files for relase workflow 2022-04-19 16:18:11 +02:00
ec2ac2e80c fix: Only trigger rename on KeyPress
This is macOS specific
2022-04-19 18:43:51 +05:30
27c72935f8 fix: use size in bytes to sort by world size 2022-04-19 15:07:14 +02:00
31c757d912 Merge pull request #455 from kthchew/fix/mac-version-num
Fix formatting of version string on macOS
2022-04-19 22:40:01 +10:00
fcdc7a1a35 fix: fix Modrinth query when Quilt is in use 2022-04-19 10:22:50 +02:00
f13c776099 Merge pull request #449 from DioEgizio/CI/specify-build-platform
specify -DLauncher_BUILD_PLATFORM on CI builds (EDIT: also remove "on x")
2022-04-19 09:59:11 +02:00
ebded1ec49 Fix formatting of version string on macOS 2022-04-18 13:56:32 -04:00
7b9d462fbc remove "on x" 2022-04-18 18:31:50 +02:00
c174a1eb01 fix: don't set mod loader as important 2022-04-18 15:05:41 +02:00
ac77997a7a fix: handle network errors when downloading modlist 2022-04-18 14:36:36 +02:00
fa352ff4d3 fix: actually check if a mod loader is selected
Thus also removes a suggestCurrent call from loaderFilterChanged, as it will already be triggered by setSelectedLoaderVersion
2022-04-18 14:15:02 +02:00
fd7745cbeb Merge pull request #442 from kthchew/fix/typos 2022-04-18 13:38:54 +02:00
c348de96e4 Merge pull request #443 from flowln/fix_tr 2022-04-18 13:38:35 +02:00
1bb35b9204 specify -DLauncher_BUILD_PLATFORM on CI builds
more cool
also maybe helps with updater?
2022-04-18 12:22:53 +02:00
cbbcc2d68b fix(translation): don't translate placeholders
Those are modified programatically, and never show up to the user!
2022-04-17 19:24:49 -03:00
fcbf37f60f Fix typos and inconsistent capitalization in sort options 2022-04-17 17:58:51 -04:00
45 changed files with 761 additions and 483 deletions

View File

@ -17,6 +17,9 @@ jobs:
- os: ubuntu-20.04
- os: ubuntu-20.04
appimage: true
- os: windows-2022
name: "Windows-i686"
msystem: mingw32
@ -66,30 +69,25 @@ jobs:
ver_short=`git rev-parse --short HEAD`
echo "VERSION=$ver_short" >> $GITHUB_ENV
- name: Install OpenJDK
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
- name: Install Qt (macOS)
if: runner.os == 'macOS'
run: |
brew update
brew install qt@5
brew install qt@5 ninja
- name: Update Qt (AppImage)
if: runner.os == 'Linux' && matrix.appimage == true
run: |
sudo add-apt-repository ppa:savoury1/qt-5-15
- name: Install Qt (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get -y update
sudo apt-get -y install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5
- name: Install Ninja
if: runner.os != 'Windows'
uses: urkle/action-get-ninja@v1
sudo apt-get -y install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 ninja-build
- name: Prepare AppImage (Linux)
if: runner.os == 'Linux'
if: runner.os == 'Linux' && matrix.appimage == true
run: |
wget "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage"
wget "https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage"
@ -104,18 +102,18 @@ jobs:
- name: Configure CMake (macOS)
if: runner.os == 'macOS'
run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DQt5_DIR=/usr/local/opt/qt@5 -DCMAKE_PREFIX_PATH=/usr/local/opt/qt@5 -G Ninja
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DQt5_DIR=/usr/local/opt/qt@5 -DCMAKE_PREFIX_PATH=/usr/local/opt/qt@5 -DLauncher_BUILD_PLATFORM=macOS -G Ninja
- name: Configure CMake (Windows)
if: runner.os == 'Windows'
shell: msys2 {0}
run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -G Ninja
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=${{ matrix.name }} -G Ninja
- name: Configure CMake (Linux)
if: runner.os == 'Linux'
run: |
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -G Ninja
cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=Linux -G Ninja
##
# BUILD
@ -143,6 +141,7 @@ jobs:
cd ${{ env.INSTALL_DIR }}
chmod +x "PolyMC.app/Contents/MacOS/polymc"
sudo codesign --sign - --deep --force --entitlements "../program_info/App.entitlements" --options runtime "PolyMC.app/Contents/MacOS/polymc"
tar -czf ../PolyMC.tar.gz *
- name: Package (Windows)
@ -166,15 +165,15 @@ jobs:
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
- name: Package (Linux)
if: runner.os == 'Linux'
if: runner.os == 'Linux' && matrix.appimage != true
run: |
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_DIR }}
cd ${{ env.INSTALL_DIR }}
tar -czf ../PolyMC.tar.gz *
tar --owner root --group root -czf ../PolyMC.tar.gz *
- name: Package (Linux, portable)
if: runner.os == 'Linux'
if: runner.os == 'Linux' && matrix.appimage != true
run: |
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }}
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable
@ -183,7 +182,7 @@ jobs:
tar -czf ../PolyMC-portable.tar.gz *
- name: Package AppImage (Linux)
if: runner.os == 'Linux'
if: runner.os == 'Linux' && matrix.appimage == true
shell: bash
run: |
cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_APPIMAGE_DIR }}/usr
@ -233,21 +232,21 @@ jobs:
path: ${{ env.INSTALL_PORTABLE_DIR }}/**
- name: Upload binary tarball (Linux)
if: runner.os == 'Linux'
if: runner.os == 'Linux' && matrix.appimage != true
uses: actions/upload-artifact@v3
with:
name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}
path: PolyMC.tar.gz
- name: Upload binary tarball (Linux, portable)
if: runner.os == 'Linux'
if: runner.os == 'Linux' && matrix.appimage != true
uses: actions/upload-artifact@v3
with:
name: PolyMC-${{ runner.os }}-Portable-${{ env.VERSION }}-${{ inputs.build_type }}
path: PolyMC-portable.tar.gz
- name: Upload AppImage (Linux)
if: runner.os == 'Linux'
if: runner.os == 'Linux' && matrix.appimage == true
uses: actions/upload-artifact@v3
with:
name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage

View File

@ -33,6 +33,7 @@ jobs:
- name: Package artifacts properly
run: |
mv ${{ github.workspace }}/PolyMC-source PolyMC-${{ env.VERSION }}
mv PolyMC-Linux-Portable*/PolyMC-portable.tar.gz PolyMC-Linux-Portable-${{ env.VERSION }}.tar.gz
mv PolyMC-Linux*/PolyMC.tar.gz PolyMC-Linux-${{ env.VERSION }}.tar.gz
mv PolyMC-*.AppImage/PolyMC-*.AppImage PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage
mv PolyMC-macOS*/PolyMC.tar.gz PolyMC-macOS-${{ env.VERSION }}.tar.gz
@ -42,9 +43,9 @@ jobs:
for d in PolyMC-Windows-*; do
cd "${d}" || continue
ARCH="$(echo -n ${d} | cut -d '-' -f 3)"
PORT="$(echo -n ${d} | grep -o portable || true)"
PORT="$(echo -n ${d} | grep -o Portable || true)"
NAME="PolyMC-Windows-${ARCH}"
test -z "${PORT}" || NAME="${NAME}-portable"
test -z "${PORT}" || NAME="${NAME}-Portable"
zip -r -9 "../${NAME}-${{ env.VERSION }}.zip" *
cd ..
done
@ -61,10 +62,11 @@ jobs:
prerelease: false
files: |
PolyMC-Linux-${{ env.VERSION }}.tar.gz
PolyMC-Linux-Portable-${{ env.VERSION }}.tar.gz
PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage
PolyMC-Windows-i686-${{ env.VERSION }}.zip
PolyMC-Windows-i686-portable-${{ env.VERSION }}.zip
PolyMC-Windows-i686-Portable-${{ env.VERSION }}.zip
PolyMC-Windows-x86_64-${{ env.VERSION }}.zip
PolyMC-Windows-x86_64-portable-${{ env.VERSION }}.zip
PolyMC-Windows-x86_64-Portable-${{ env.VERSION }}.zip
PolyMC-macOS-${{ env.VERSION }}.tar.gz
PolyMC-${{ env.VERSION }}.tar.gz

2
.gitignore vendored
View File

@ -14,6 +14,8 @@ CMakeLists.txt.user.*
/.project
/.settings
/.idea
/.vscode
.clang-format
cmake-build-*/
Debug

View File

@ -69,7 +69,7 @@ set(Launcher_HELP_URL "https://polymc.org/wiki/help-pages/%1" CACHE STRING "URL
######## Set version numbers ########
set(Launcher_VERSION_MAJOR 1)
set(Launcher_VERSION_MINOR 2)
set(Launcher_VERSION_HOTFIX 0)
set(Launcher_VERSION_HOTFIX 2)
# Build number
set(Launcher_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.")
@ -89,6 +89,10 @@ set(Launcher_IMGUR_CLIENT_ID "5b97b0713fba4a3" CACHE STRING "Client ID you can g
# MSA Client ID
set(Launcher_MSA_CLIENT_ID "549033b2-1532-4d4e-ae77-1bbaa46f9d74" CACHE STRING "Client ID you can get from Microsoft Identity Platform when you register an application")
# CurseForge API Key
# CHANGE THIS IF YOU FORK THIS PROJECT!
set(Launcher_CURSEFORGE_API_KEY "$2a$10$iR1RdPDG95FWdILZbHuoMOlV4vL4eckBx7QPZR6SVZmliEb9ZQplu" CACHE STRING "CurseForge API Key")
# Bug tracker URL
set(Launcher_BUG_TRACKER_URL "https://github.com/PolyMC/PolyMC/issues" CACHE STRING "URL for the bug tracker.")
@ -183,9 +187,9 @@ if(UNIX AND APPLE)
set(MACOSX_BUNDLE_BUNDLE_NAME "${Launcher_Name}")
set(MACOSX_BUNDLE_INFO_STRING "${Launcher_Name}: A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once.")
set(MACOSX_BUNDLE_GUI_IDENTIFIER "org.polymc.${Launcher_Name}")
set(MACOSX_BUNDLE_BUNDLE_VERSION "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}.${Launcher_VERSION_BUILD}")
set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}.${Launcher_VERSION_BUILD}")
set(MACOSX_BUNDLE_LONG_VERSION_STRING "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}.${Launcher_VERSION_BUILD}")
set(MACOSX_BUNDLE_BUNDLE_VERSION "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}")
set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}")
set(MACOSX_BUNDLE_LONG_VERSION_STRING "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}")
set(MACOSX_BUNDLE_ICON_FILE ${Launcher_Name}.icns)
set(MACOSX_BUNDLE_COPYRIGHT "Copyright 2021-2022 ${Launcher_Copyright}")

View File

@ -90,6 +90,7 @@ Config::Config()
HELP_URL = "@Launcher_HELP_URL@";
IMGUR_CLIENT_ID = "@Launcher_IMGUR_CLIENT_ID@";
MSA_CLIENT_ID = "@Launcher_MSA_CLIENT_ID@";
CURSEFORGE_API_KEY = "@Launcher_CURSEFORGE_API_KEY@";
META_URL = "@Launcher_META_URL@";
BUG_TRACKER_URL = "@Launcher_BUG_TRACKER_URL@";

View File

@ -39,9 +39,8 @@
/**
* \brief The Config class holds all the build-time information passed from the build system.
*/
class Config
{
public:
class Config {
public:
Config();
QString LAUNCHER_NAME;
QString LAUNCHER_DISPLAYNAME;
@ -74,7 +73,6 @@ public:
/// URL for the updater's channel
QString UPDATER_BASE;
/// User-Agent to use.
QString USER_AGENT;
@ -116,6 +114,11 @@ public:
*/
QString MSA_CLIENT_ID;
/**
* Client API key for CurseForge
*/
QString CURSEFORGE_API_KEY;
/**
* Metadata repository URL prefix
*/
@ -154,4 +157,3 @@ public:
};
extern const Config BuildConfig;

View File

@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSCameraUsageDescription</key>
<string>A Minecraft mod wants to access your camera.</string>
<key>NSMicrophoneUsageDescription</key>
<string>A Minecraft mod wants to access your microphone.</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSHighResolutionCapable</key>

View File

@ -235,6 +235,8 @@ set(MINECRAFT_SOURCES
minecraft/auth/steps/MigrationEligibilityStep.h
minecraft/auth/steps/MinecraftProfileStep.cpp
minecraft/auth/steps/MinecraftProfileStep.h
minecraft/auth/steps/MinecraftProfileStepMojang.cpp
minecraft/auth/steps/MinecraftProfileStepMojang.h
minecraft/auth/steps/MSAStep.cpp
minecraft/auth/steps/MSAStep.h
minecraft/auth/steps/XboxAuthorizationStep.cpp

View File

@ -31,7 +31,7 @@ void InstanceCreationTask::executeTask()
components->buildingFromScratch();
components->setComponentVersion("net.minecraft", m_version->descriptor(), true);
if(m_usingLoader)
components->setComponentVersion(m_loader, m_loaderVersion->descriptor(), true);
components->setComponentVersion(m_loader, m_loaderVersion->descriptor());
inst.setName(m_instName);
inst.setIconKey(m_instIcon);
instanceSettings->resumeSave();

View File

@ -213,12 +213,8 @@ QString MinecraftInstance::binRoot() const
QString MinecraftInstance::getNativePath() const
{
#ifdef Q_OS_FREEBSD
QDir natives_dir("/usr/local/lib/lwjgl/");
#else
QDir natives_dir(FS::PathCombine(instanceRoot(), "natives/"));
return natives_dir.absolutePath();
#endif
}
QString MinecraftInstance::getLocalLibraryPath() const

View File

@ -17,6 +17,7 @@
#include "Application.h"
#include <FileSystem.h>
#include <Qt>
#include <QMimeData>
#include <QUrl>
#include <QUuid>
@ -190,6 +191,16 @@ QVariant WorldList::data(const QModelIndex &index, int role) const
return QVariant();
}
case Qt::UserRole:
switch (column)
{
case SizeColumn:
return qVariantFromValue<qlonglong>(world.bytes());
default:
return data(index, Qt::DisplayRole);
}
case Qt::ToolTipRole:
{
return world.folderName();
@ -216,7 +227,7 @@ QVariant WorldList::data(const QModelIndex &index, int role) const
}
case SizeRole:
{
return locale.formattedDataSize(world.bytes());
return qVariantFromValue<qlonglong>(world.bytes());
}
case IconFileRole:
{

View File

@ -1,4 +1,5 @@
#include "Parsers.h"
#include "Json.h"
#include <QJsonDocument>
#include <QJsonArray>
@ -212,6 +213,180 @@ bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) {
return true;
}
namespace {
// these skin URLs are for the MHF_Steve and MHF_Alex accounts (made by a Mojang employee)
// they are needed because the session server doesn't return skin urls for default skins
static const QString SKIN_URL_STEVE = "http://textures.minecraft.net/texture/1a4af718455d4aab528e7a61f86fa25e6a369d1768dcb13f7df319a713eb810b";
static const QString SKIN_URL_ALEX = "http://textures.minecraft.net/texture/83cee5ca6afcdb171285aa00e8049c297b2dbeba0efb8ff970a5677a1b644032";
bool isDefaultModelSteve(QString uuid) {
// need to calculate *Java* hashCode of UUID
// if number is even, skin/model is steve, otherwise it is alex
// just in case dashes are in the id
uuid.remove('-');
if (uuid.size() != 32) {
return true;
}
// qulonglong is guaranteed to be 64 bits
// we need to use unsigned numbers to guarantee truncation below
qulonglong most = uuid.left(16).toULongLong(nullptr, 16);
qulonglong least = uuid.right(16).toULongLong(nullptr, 16);
qulonglong xored = most ^ least;
return ((static_cast<quint32>(xored >> 32)) ^ static_cast<quint32>(xored)) % 2 == 0;
}
}
/**
Uses session server for skin/cape lookup instead of profile,
because locked Mojang accounts cannot access profile endpoint
(https://api.minecraftservices.com/minecraft/profile/)
ref: https://wiki.vg/Mojang_API#UUID_to_Profile_and_Skin.2FCape
{
"id": "<profile identifier>",
"name": "<player name>",
"properties": [
{
"name": "textures",
"value": "<base64 string>"
}
]
}
decoded base64 "value":
{
"timestamp": <java time in ms>,
"profileId": "<profile uuid>",
"profileName": "<player name>",
"textures": {
"SKIN": {
"url": "<player skin URL>"
},
"CAPE": {
"url": "<player cape URL>"
}
}
}
*/
bool parseMinecraftProfileMojang(QByteArray & data, MinecraftProfile &output) {
qDebug() << "Parsing Minecraft profile...";
#ifndef NDEBUG
qDebug() << data;
#endif
QJsonParseError jsonError;
QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
if(jsonError.error) {
qWarning() << "Failed to parse response as JSON: " << jsonError.errorString();
return false;
}
auto obj = Json::requireObject(doc, "mojang minecraft profile");
if(!getString(obj.value("id"), output.id)) {
qWarning() << "Minecraft profile id is not a string";
return false;
}
if(!getString(obj.value("name"), output.name)) {
qWarning() << "Minecraft profile name is not a string";
return false;
}
auto propsArray = obj.value("properties").toArray();
QByteArray texturePayload;
for( auto p : propsArray) {
auto pObj = p.toObject();
auto name = pObj.value("name");
if (!name.isString() || name.toString() != "textures") {
continue;
}
auto value = pObj.value("value");
if (value.isString()) {
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
texturePayload = QByteArray::fromBase64(value.toString().toUtf8(), QByteArray::AbortOnBase64DecodingErrors);
#else
texturePayload = QByteArray::fromBase64(value.toString().toUtf8());
#endif
}
if (!texturePayload.isEmpty()) {
break;
}
}
if (texturePayload.isNull()) {
qWarning() << "No texture payload data";
return false;
}
doc = QJsonDocument::fromJson(texturePayload, &jsonError);
if(jsonError.error) {
qWarning() << "Failed to parse response as JSON: " << jsonError.errorString();
return false;
}
obj = Json::requireObject(doc, "session texture payload");
auto textures = obj.value("textures");
if (!textures.isObject()) {
qWarning() << "No textures array in response";
return false;
}
Skin skinOut;
// fill in default skin info ourselves, as this endpoint doesn't provide it
bool steve = isDefaultModelSteve(output.id);
skinOut.variant = steve ? "classic" : "slim";
skinOut.url = steve ? SKIN_URL_STEVE : SKIN_URL_ALEX;
// sadly we can't figure this out, but I don't think it really matters...
skinOut.id = "00000000-0000-0000-0000-000000000000";
Cape capeOut;
auto tObj = textures.toObject();
for (auto idx = tObj.constBegin(); idx != tObj.constEnd(); ++idx) {
if (idx->isObject()) {
if (idx.key() == "SKIN") {
auto skin = idx->toObject();
if (!getString(skin.value("url"), skinOut.url)) {
qWarning() << "Skin url is not a string";
return false;
}
auto maybeMeta = skin.find("metadata");
if (maybeMeta != skin.end() && maybeMeta->isObject()) {
auto meta = maybeMeta->toObject();
// might not be present
getString(meta.value("model"), skinOut.variant);
}
}
else if (idx.key() == "CAPE") {
auto cape = idx->toObject();
if (!getString(cape.value("url"), capeOut.url)) {
qWarning() << "Cape url is not a string";
return false;
}
// we don't know the cape ID as it is not returned from the session server
// so just fake it - changing capes is probably locked anyway :(
capeOut.alias = "cape";
}
}
}
output.skin = skinOut;
if (capeOut.alias == "cape") {
output.capes = QMap<QString, Cape>({{capeOut.alias, capeOut}});
output.currentCape = capeOut.alias;
}
output.validity = Katabasis::Validity::Certain;
return true;
}
bool parseMinecraftEntitlements(QByteArray & data, MinecraftEntitlement &output) {
qDebug() << "Parsing Minecraft entitlements...";
#ifndef NDEBUG

View File

@ -14,6 +14,7 @@ namespace Parsers
bool parseMojangResponse(QByteArray &data, Katabasis::Token &output);
bool parseMinecraftProfile(QByteArray &data, MinecraftProfile &output);
bool parseMinecraftProfileMojang(QByteArray &data, MinecraftProfile &output);
bool parseMinecraftEntitlements(QByteArray &data, MinecraftEntitlement &output);
bool parseRolloutResponse(QByteArray &data, bool& result);
}

View File

@ -209,6 +209,28 @@ void Yggdrasil::processResponse(QJsonObject responseData) {
m_data->yggdrasilToken.validity = Katabasis::Validity::Certain;
m_data->yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc();
// Get UUID here since we need it for later
auto profile = responseData.value("selectedProfile");
if (!profile.isObject()) {
changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a selected profile."));
return;
}
auto profileObj = profile.toObject();
for (auto i = profileObj.constBegin(); i != profileObj.constEnd(); ++i) {
if (i.key() == "name" && i.value().isString()) {
m_data->minecraftProfile.name = i->toString();
}
else if (i.key() == "id" && i.value().isString()) {
m_data->minecraftProfile.id = i->toString();
}
}
if (m_data->minecraftProfile.id.isEmpty()) {
changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a UUID in selected profile."));
return;
}
// We've made it through the minefield of possible errors. Return true to indicate that
// we've succeeded.
qDebug() << "Finished reading authentication response.";

View File

@ -1,7 +1,7 @@
#include "Mojang.h"
#include "minecraft/auth/steps/YggdrasilStep.h"
#include "minecraft/auth/steps/MinecraftProfileStep.h"
#include "minecraft/auth/steps/MinecraftProfileStepMojang.h"
#include "minecraft/auth/steps/MigrationEligibilityStep.h"
#include "minecraft/auth/steps/GetSkinStep.h"
@ -10,7 +10,7 @@ MojangRefresh::MojangRefresh(
QObject *parent
) : AuthFlow(data, parent) {
m_steps.append(new YggdrasilStep(m_data, QString()));
m_steps.append(new MinecraftProfileStep(m_data));
m_steps.append(new MinecraftProfileStepMojang(m_data));
m_steps.append(new MigrationEligibilityStep(m_data));
m_steps.append(new GetSkinStep(m_data));
}
@ -21,7 +21,7 @@ MojangLogin::MojangLogin(
QObject *parent
): AuthFlow(data, parent), m_password(password) {
m_steps.append(new YggdrasilStep(m_data, m_password));
m_steps.append(new MinecraftProfileStep(m_data));
m_steps.append(new MinecraftProfileStepMojang(m_data));
m_steps.append(new MigrationEligibilityStep(m_data));
m_steps.append(new GetSkinStep(m_data));
}

View File

@ -50,7 +50,9 @@ void LauncherLoginStep::onRequestDone(
auto requestor = qobject_cast<AuthRequest *>(QObject::sender());
requestor->deleteLater();
#ifndef NDEBUG
qDebug() << data;
#endif
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
#ifndef NDEBUG

View File

@ -0,0 +1,94 @@
#include "MinecraftProfileStepMojang.h"
#include <QNetworkRequest>
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
MinecraftProfileStepMojang::MinecraftProfileStepMojang(AccountData* data) : AuthStep(data) {
}
MinecraftProfileStepMojang::~MinecraftProfileStepMojang() noexcept = default;
QString MinecraftProfileStepMojang::describe() {
return tr("Fetching the Minecraft profile.");
}
void MinecraftProfileStepMojang::perform() {
if (m_data->minecraftProfile.id.isEmpty()) {
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("A UUID is required to get the profile."));
return;
}
// use session server instead of profile due to profile endpoint being locked for locked Mojang accounts
QUrl url = QUrl("https://sessionserver.mojang.com/session/minecraft/profile/" + m_data->minecraftProfile.id);
QNetworkRequest req = QNetworkRequest(url);
AuthRequest *request = new AuthRequest(this);
connect(request, &AuthRequest::finished, this, &MinecraftProfileStepMojang::onRequestDone);
request->get(req);
}
void MinecraftProfileStepMojang::rehydrate() {
// NOOP, for now. We only save bools and there's nothing to check.
}
void MinecraftProfileStepMojang::onRequestDone(
QNetworkReply::NetworkError error,
QByteArray data,
QList<QNetworkReply::RawHeaderPair> headers
) {
auto requestor = qobject_cast<AuthRequest *>(QObject::sender());
requestor->deleteLater();
#ifndef NDEBUG
qDebug() << data;
#endif
if (error == QNetworkReply::ContentNotFoundError) {
// NOTE: Succeed even if we do not have a profile. This is a valid account state.
if(m_data->type == AccountType::Mojang) {
m_data->minecraftEntitlement.canPlayMinecraft = false;
m_data->minecraftEntitlement.ownsMinecraft = false;
}
m_data->minecraftProfile = MinecraftProfile();
emit finished(
AccountTaskState::STATE_SUCCEEDED,
tr("Account has no Minecraft profile.")
);
return;
}
if (error != QNetworkReply::NoError) {
qWarning() << "Error getting profile:";
qWarning() << " HTTP Status: " << requestor->httpStatus_;
qWarning() << " Internal error no.: " << error;
qWarning() << " Error string: " << requestor->errorString_;
qWarning() << " Response:";
qWarning() << QString::fromUtf8(data);
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("Minecraft Java profile acquisition failed.")
);
return;
}
if(!Parsers::parseMinecraftProfileMojang(data, m_data->minecraftProfile)) {
m_data->minecraftProfile = MinecraftProfile();
emit finished(
AccountTaskState::STATE_FAILED_SOFT,
tr("Minecraft Java profile response could not be parsed")
);
return;
}
if(m_data->type == AccountType::Mojang) {
auto validProfile = m_data->minecraftProfile.validity == Katabasis::Validity::Certain;
m_data->minecraftEntitlement.canPlayMinecraft = validProfile;
m_data->minecraftEntitlement.ownsMinecraft = validProfile;
}
emit finished(
AccountTaskState::STATE_WORKING,
tr("Minecraft Java profile acquisition succeeded.")
);
}

View File

@ -0,0 +1,22 @@
#pragma once
#include <QObject>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
class MinecraftProfileStepMojang : public AuthStep {
Q_OBJECT
public:
explicit MinecraftProfileStepMojang(AccountData *data);
virtual ~MinecraftProfileStepMojang() noexcept;
void perform() override;
void rehydrate() override;
QString describe() override;
private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
};

View File

@ -1,14 +1,9 @@
#include "FileResolvingTask.h"
#include "Json.h"
namespace {
const char * metabase = "https://cursemeta.dries007.net";
}
Flame::FileResolvingTask::FileResolvingTask(shared_qobject_ptr<QNetworkAccessManager> network, Flame::Manifest& toProcess)
: m_network(network), m_toProcess(toProcess)
{
}
{}
void Flame::FileResolvingTask::executeTask()
{
@ -17,14 +12,13 @@ void Flame::FileResolvingTask::executeTask()
m_dljob = new NetJob("Mod id resolver", m_network);
results.resize(m_toProcess.files.size());
int index = 0;
for(auto & file: m_toProcess.files)
{
for (auto& file : m_toProcess.files) {
auto projectIdStr = QString::number(file.projectId);
auto fileIdStr = QString::number(file.fileId);
QString metaurl = QString("%1/%2/%3.json").arg(metabase, projectIdStr, fileIdStr);
QString metaurl = QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(projectIdStr, fileIdStr);
auto dl = Net::Download::makeByteArray(QUrl(metaurl), &results[index]);
m_dljob->addNetAction(dl);
index ++;
index++;
}
connect(m_dljob.get(), &NetJob::finished, this, &Flame::FileResolvingTask::netJobFinished);
m_dljob->start();
@ -34,16 +28,11 @@ void Flame::FileResolvingTask::netJobFinished()
{
bool failed = false;
int index = 0;
for(auto & bytes: results)
{
auto & out = m_toProcess.files[index];
try
{
for (auto& bytes : results) {
auto& out = m_toProcess.files[index];
try {
failed &= (!out.parseFromBytes(bytes));
}
catch (const JSONValidationError &e)
{
} catch (const JSONValidationError& e) {
qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of a parsing error:";
qCritical() << e.cause();
qCritical() << "JSON:";
@ -52,12 +41,9 @@ void Flame::FileResolvingTask::netJobFinished()
}
index++;
}
if(!failed)
{
if (!failed) {
emitSucceeded();
}
else
{
} else {
emitFailed(tr("Some mod ID resolving tasks failed."));
}
}

View File

@ -3,33 +3,53 @@
#include "modplatform/helpers/NetworkModAPI.h"
class FlameAPI : public NetworkModAPI {
private:
inline auto getSortFieldInt(QString sortString) const -> int
{
return sortString == "Featured" ? 1
: sortString == "Popularity" ? 2
: sortString == "LastUpdated" ? 3
: sortString == "Name" ? 4
: sortString == "Author" ? 5
: sortString == "TotalDownloads" ? 6
: sortString == "Category" ? 7
: sortString == "GameVersion" ? 8
: 1;
}
private:
inline auto getModSearchURL(SearchArgs& args) const -> QString override
{
auto gameVersionStr = args.versions.size() != 0 ? QString("gameVersion=%1").arg(args.versions.front().toString()) : QString();
return QString(
"https://addons-ecs.forgesvc.net/api/v2/addon/search?"
"https://api.curseforge.com/v1/mods/search?"
"gameId=432&"
"categoryId=0&"
"sectionId=6&"
"classId=6&"
"index=%1&"
"pageSize=25&"
"searchFilter=%2&"
"sort=%3&"
"sortField=%3&"
"sortOrder=desc&"
"modLoaderType=%4&"
"%5")
.arg(args.offset)
.arg(args.search)
.arg(args.sorting)
.arg(getSortFieldInt(args.sorting))
.arg(getMappedModLoader(args.mod_loader))
.arg(gameVersionStr);
};
inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override
{
return QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(args.addonId);
QString gameVersionQuery = args.mcVersions.size() == 1 ? QString("gameVersion=%1&").arg(args.mcVersions.front().toString()) : "";
QString modLoaderQuery = QString("modLoaderType=%1&").arg(getMappedModLoader(args.loader));
return QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&%2%3")
.arg(args.addonId)
.arg(gameVersionQuery)
.arg(modLoaderQuery);
};
public:

View File

@ -10,23 +10,12 @@ void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
pack.addonId = Json::requireInteger(obj, "id");
pack.name = Json::requireString(obj, "name");
pack.websiteUrl = Json::ensureString(obj, "websiteUrl", "");
pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", "");
pack.description = Json::ensureString(obj, "summary", "");
bool thumbnailFound = false;
auto attachments = Json::requireArray(obj, "attachments");
for (auto attachmentRaw : attachments) {
auto attachmentObj = Json::requireObject(attachmentRaw);
bool isDefault = attachmentObj.value("isDefault").toBool(false);
if (isDefault) {
thumbnailFound = true;
pack.logoName = Json::requireString(attachmentObj, "title");
pack.logoUrl = Json::requireString(attachmentObj, "thumbnailUrl");
break;
}
}
if (!thumbnailFound) { throw JSONValidationError(QString("Pack without an icon, skipping: %1").arg(pack.name)); }
QJsonObject logo = Json::requireObject(obj, "logo");
pack.logoName = Json::requireString(logo, "title");
pack.logoUrl = Json::requireString(logo, "thumbnailUrl");
auto authors = Json::requireArray(obj, "authors");
for (auto authorIter : authors) {
@ -45,18 +34,22 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
{
QVector<ModPlatform::IndexedVersion> unsortedVersions;
auto profile = (dynamic_cast<MinecraftInstance*>(inst))->getPackProfile();
bool hasFabric = FlameAPI::getMappedModLoader(profile->getModLoader()) == ModAPI::Fabric;
QString mcVersion = profile->getComponentVersion("net.minecraft");
for (auto versionIter : arr) {
auto obj = versionIter.toObject();
auto versionArray = Json::requireArray(obj, "gameVersion");
if (versionArray.isEmpty()) { continue; }
auto versionArray = Json::requireArray(obj, "gameVersions");
if (versionArray.isEmpty()) {
continue;
}
ModPlatform::IndexedVersion file;
for (auto mcVer : versionArray) {
file.mcVersion.append(mcVer.toString());
auto str = mcVer.toString();
if (str.contains('.'))
file.mcVersion.append(str);
}
file.addonId = pack.addonId;
@ -66,28 +59,9 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
file.downloadUrl = Json::requireString(obj, "downloadUrl");
file.fileName = Json::requireString(obj, "fileName");
auto modules = Json::requireArray(obj, "modules");
bool is_valid_fabric_version = false;
for (auto m : modules) {
auto fname = Json::requireString(m.toObject(), "foldername");
// FIXME: This does not work properly when a mod supports more than one mod loader, since
// FIXME: This also doesn't deal with Quilt mods at the moment
// they bundle the meta files for all of them in the same arquive, even when that version
// doesn't support the given mod loader.
if (hasFabric) {
if (fname == "fabric.mod.json") {
is_valid_fabric_version = true;
break;
}
} else
break;
// NOTE: Since we're not validating forge versions, we can just skip this loop.
}
if (hasFabric && !is_valid_fabric_version) continue;
unsortedVersions.append(file);
}
auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool {
// dates are in RFC 3339 format
return a.date > b.date;

View File

@ -2,76 +2,63 @@
#include "Json.h"
void Flame::loadIndexedPack(Flame::IndexedPack & pack, QJsonObject & obj)
void Flame::loadIndexedPack(Flame::IndexedPack& pack, QJsonObject& obj)
{
pack.addonId = Json::requireInteger(obj, "id");
pack.name = Json::requireString(obj, "name");
pack.websiteUrl = Json::ensureString(obj, "websiteUrl", "");
pack.description = Json::ensureString(obj, "summary", "");
bool thumbnailFound = false;
auto attachments = Json::requireArray(obj, "attachments");
for(auto attachmentRaw: attachments) {
auto attachmentObj = Json::requireObject(attachmentRaw);
bool isDefault = attachmentObj.value("isDefault").toBool(false);
if(isDefault) {
thumbnailFound = true;
pack.logoName = Json::requireString(attachmentObj, "title");
pack.logoUrl = Json::requireString(attachmentObj, "thumbnailUrl");
break;
}
}
if(!thumbnailFound) {
throw JSONValidationError(QString("Pack without an icon, skipping: %1").arg(pack.name));
}
auto logo = Json::requireObject(obj, "logo");
pack.logoName = Json::requireString(logo, "title");
pack.logoUrl = Json::requireString(logo, "thumbnailUrl");
auto authors = Json::requireArray(obj, "authors");
for(auto authorIter: authors) {
for (auto authorIter : authors) {
auto author = Json::requireObject(authorIter);
Flame::ModpackAuthor packAuthor;
packAuthor.name = Json::requireString(author, "name");
packAuthor.url = Json::requireString(author, "url");
pack.authors.append(packAuthor);
}
int defaultFileId = Json::requireInteger(obj, "defaultFileId");
int defaultFileId = Json::requireInteger(obj, "mainFileId");
bool found = false;
// check if there are some files before adding the pack
auto files = Json::requireArray(obj, "latestFiles");
for(auto fileIter: files) {
for (auto fileIter : files) {
auto file = Json::requireObject(fileIter);
int id = Json::requireInteger(file, "id");
// NOTE: for now, ignore everything that's not the default...
if(id != defaultFileId) {
if (id != defaultFileId) {
continue;
}
auto versionArray = Json::requireArray(file, "gameVersion");
if(versionArray.size() < 1) {
auto versionArray = Json::requireArray(file, "gameVersions");
if (versionArray.size() < 1) {
continue;
}
found = true;
break;
}
if(!found) {
if (!found) {
throw JSONValidationError(QString("Pack with no good file, skipping: %1").arg(pack.name));
}
}
void Flame::loadIndexedPackVersions(Flame::IndexedPack & pack, QJsonArray & arr)
void Flame::loadIndexedPackVersions(Flame::IndexedPack& pack, QJsonArray& arr)
{
QVector<Flame::IndexedVersion> unsortedVersions;
for(auto versionIter: arr) {
for (auto versionIter : arr) {
auto version = Json::requireObject(versionIter);
Flame::IndexedVersion file;
Flame::IndexedVersion file;
file.addonId = pack.addonId;
file.fileId = Json::requireInteger(version, "id");
auto versionArray = Json::requireArray(version, "gameVersion");
if(versionArray.size() < 1) {
auto versionArray = Json::requireArray(version, "gameVersions");
if (versionArray.size() < 1) {
continue;
}
@ -82,10 +69,7 @@ void Flame::loadIndexedPackVersions(Flame::IndexedPack & pack, QJsonArray & arr)
unsortedVersions.append(file);
}
auto orderSortPredicate = [](const IndexedVersion & a, const IndexedVersion & b) -> bool
{
return a.fileId > b.fileId;
};
auto orderSortPredicate = [](const IndexedVersion& a, const IndexedVersion& b) -> bool { return a.fileId > b.fileId; };
std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate);
pack.versions = unsortedVersions;
pack.versionsLoaded = true;

View File

@ -1,28 +1,27 @@
#include "PackManifest.h"
#include "Json.h"
static void loadFileV1(Flame::File & f, QJsonObject & file)
static void loadFileV1(Flame::File& f, QJsonObject& file)
{
f.projectId = Json::requireInteger(file, "projectID");
f.fileId = Json::requireInteger(file, "fileID");
f.required = Json::ensureBoolean(file, QString("required"), true);
}
static void loadModloaderV1(Flame::Modloader & m, QJsonObject & modLoader)
static void loadModloaderV1(Flame::Modloader& m, QJsonObject& modLoader)
{
m.id = Json::requireString(modLoader, "id");
m.primary = Json::ensureBoolean(modLoader, QString("primary"), false);
}
static void loadMinecraftV1(Flame::Minecraft & m, QJsonObject & minecraft)
static void loadMinecraftV1(Flame::Minecraft& m, QJsonObject& minecraft)
{
m.version = Json::requireString(minecraft, "version");
// extra libraries... apparently only used for a custom Minecraft launcher in the 1.2.5 FTB retro pack
// intended use is likely hardcoded in the 'Flame' client, the manifest says nothing
m.libraries = Json::ensureString(minecraft, QString("libraries"), QString());
auto arr = Json::ensureArray(minecraft, "modLoaders", QJsonArray());
for (QJsonValueRef item : arr)
{
for (QJsonValueRef item : arr) {
auto obj = Json::requireObject(item);
Flame::Modloader loader;
loadModloaderV1(loader, obj);
@ -30,16 +29,15 @@ static void loadMinecraftV1(Flame::Minecraft & m, QJsonObject & minecraft)
}
}
static void loadManifestV1(Flame::Manifest & m, QJsonObject & manifest)
static void loadManifestV1(Flame::Manifest& m, QJsonObject& manifest)
{
auto mc = Json::requireObject(manifest, "minecraft");
loadMinecraftV1(m.minecraft, mc);
m.name = Json::ensureString(manifest, QString("name"), "Unnamed");
m.version = Json::ensureString(manifest, QString("version"), QString());
m.author = Json::ensureString(manifest, QString("author"), "Anonymous Coward");
m.author = Json::ensureString(manifest, QString("author"), "Anonymous");
auto arr = Json::ensureArray(manifest, "files", QJsonArray());
for (QJsonValueRef item : arr)
{
for (QJsonValueRef item : arr) {
auto obj = Json::requireObject(item);
Flame::File file;
loadFileV1(file, obj);
@ -48,18 +46,16 @@ static void loadManifestV1(Flame::Manifest & m, QJsonObject & manifest)
m.overrides = Json::ensureString(manifest, "overrides", "overrides");
}
void Flame::loadManifest(Flame::Manifest & m, const QString &filepath)
void Flame::loadManifest(Flame::Manifest& m, const QString& filepath)
{
auto doc = Json::requireDocument(filepath);
auto obj = Json::requireObject(doc);
m.manifestType = Json::requireString(obj, "manifestType");
if(m.manifestType != "minecraftModpack")
{
if (m.manifestType != "minecraftModpack") {
throw JSONValidationError("Not a modpack manifest!");
}
m.manifestVersion = Json::requireInteger(obj, "manifestVersion");
if(m.manifestVersion != 1)
{
if (m.manifestVersion != 1) {
throw JSONValidationError(QString("Unknown manifest version (%1)").arg(m.manifestVersion));
}
loadManifestV1(m, obj);
@ -68,59 +64,30 @@ void Flame::loadManifest(Flame::Manifest & m, const QString &filepath)
bool Flame::File::parseFromBytes(const QByteArray& bytes)
{
auto doc = Json::requireDocument(bytes);
auto obj = Json::requireObject(doc);
// result code signifies true failure.
if(obj.contains("code"))
{
qCritical() << "Resolving of" << projectId << fileId << "failed because of a negative result:";
qCritical() << bytes;
return false;
if (!doc.isObject()) {
throw JSONValidationError(QString("data is not an object? that's not supposed to happen"));
}
fileName = Json::requireString(obj, "FileNameOnDisk");
QString rawUrl = Json::requireString(obj, "DownloadURL");
auto obj = Json::ensureObject(doc.object(), "data");
fileName = Json::requireString(obj, "fileName");
QString rawUrl = Json::requireString(obj, "downloadUrl");
url = QUrl(rawUrl, QUrl::TolerantMode);
if(!url.isValid())
{
if (!url.isValid()) {
throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl));
}
// This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience
// It is also optional
QJsonObject projObj = Json::ensureObject(obj, "_Project", {});
if(!projObj.isEmpty())
{
QString strType = Json::ensureString(projObj, "PackageType", "mod").toLower();
if(strType == "singlefile")
{
type = File::Type::SingleFile;
}
else if(strType == "ctoc")
{
type = File::Type::Ctoc;
}
else if(strType == "cmod2")
{
type = File::Type::Cmod2;
}
else if(strType == "mod")
{
type = File::Type::Mod;
}
else if(strType == "folder")
{
type = File::Type::Folder;
}
else if(strType == "modpack")
{
type = File::Type::Modpack;
}
else
{
qCritical() << "Resolving of" << projectId << fileId << "failed because of unknown file type:" << strType;
type = File::Type::Unknown;
return false;
}
targetFolder = Json::ensureString(projObj, "Path", "mods");
type = File::Type::SingleFile;
if (fileName.endsWith(".zip")) {
// this is probably a resource pack
targetFolder = "resourcepacks";
} else {
// this is probably a mod, dunno what else could modpacks download
targetFolder = "mods";
}
resolved = true;
return true;
}

View File

@ -1,5 +1,6 @@
#pragma once
#include "modplatform/ModAPI.h"
#include "modplatform/helpers/NetworkModAPI.h"
#include <QDebug>
@ -8,6 +9,36 @@ class ModrinthAPI : public NetworkModAPI {
public:
inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; };
static auto getModLoaderStrings(ModLoaderType type) -> const QStringList
{
QStringList l;
switch (type)
{
case Unspecified:
for (auto loader : {Forge, Fabric, Quilt})
{
l << ModAPI::getModLoaderString(loader);
}
break;
case Quilt:
l << ModAPI::getModLoaderString(Fabric);
default:
l << ModAPI::getModLoaderString(type);
}
return l;
}
static auto getModLoaderFilters(ModLoaderType type) -> const QString
{
QStringList l;
for (auto loader : getModLoaderStrings(type))
{
l << QString("\"categories:%1\"").arg(loader);
}
return l.join(',');
}
private:
inline auto getModSearchURL(SearchArgs& args) const -> QString override
{
@ -22,11 +53,11 @@ class ModrinthAPI : public NetworkModAPI {
"limit=25&"
"query=%2&"
"index=%3&"
"facets=[[\"categories:%4\"],%5[\"project_type:mod\"]]")
"facets=[[%4],%5[\"project_type:mod\"]]")
.arg(args.offset)
.arg(args.search)
.arg(args.sorting)
.arg(getModLoaderString(args.mod_loader))
.arg(getModLoaderFilters(args.mod_loader))
.arg(getGameVersionsArray(args.versions));
};
@ -34,10 +65,10 @@ class ModrinthAPI : public NetworkModAPI {
{
return QString("https://api.modrinth.com/v2/project/%1/version?"
"game_versions=[%2]"
"loaders=[%3]")
"loaders=[\"%3\"]")
.arg(args.addonId)
.arg(getGameVersionsString(args.mcVersions))
.arg(getModLoaderString(args.loader));
.arg(getModLoaderStrings(args.loader).join("\",\""));
};
auto getGameVersionsArray(std::list<Version> mcVersions) const -> QString
@ -50,16 +81,6 @@ class ModrinthAPI : public NetworkModAPI {
return s.isEmpty() ? QString() : QString("[%1],").arg(s);
}
static auto getModLoaderString(ModLoaderType type) -> const QString
{
if (type == Unspecified)
return "fabric, forge, quilt";
// TODO: remove this once Quilt drops official Fabric support
if (type == Quilt) // NOTE: Most if not all Fabric mods should work *currently*
return "fabric, quilt";
return ModAPI::getModLoaderString(type);
}
inline auto validateModLoader(ModLoaderType modLoader) const -> bool
{
return modLoader == Unspecified || modLoader == Forge || modLoader == Fabric || modLoader == Quilt;

View File

@ -15,27 +15,27 @@
#include "Download.h"
#include <QFileInfo>
#include <QDateTime>
#include <QDebug>
#include <QFileInfo>
#include "FileSystem.h"
#include "ChecksumValidator.h"
#include "MetaCacheSink.h"
#include "ByteArraySink.h"
#include "ChecksumValidator.h"
#include "FileSystem.h"
#include "MetaCacheSink.h"
#include "BuildConfig.h"
namespace Net {
Download::Download():NetAction()
Download::Download() : NetAction()
{
m_status = Job_NotStarted;
}
Download::Ptr Download::makeCached(QUrl url, MetaEntryPtr entry, Options options)
{
Download * dl = new Download();
Download* dl = new Download();
dl->m_url = url;
dl->m_options = options;
auto md5Node = new ChecksumValidator(QCryptographicHash::Md5);
@ -45,9 +45,9 @@ Download::Ptr Download::makeCached(QUrl url, MetaEntryPtr entry, Options options
return dl;
}
Download::Ptr Download::makeByteArray(QUrl url, QByteArray *output, Options options)
Download::Ptr Download::makeByteArray(QUrl url, QByteArray* output, Options options)
{
Download * dl = new Download();
Download* dl = new Download();
dl->m_url = url;
dl->m_options = options;
dl->m_sink.reset(new ByteArraySink(output));
@ -56,30 +56,28 @@ Download::Ptr Download::makeByteArray(QUrl url, QByteArray *output, Options opti
Download::Ptr Download::makeFile(QUrl url, QString path, Options options)
{
Download * dl = new Download();
Download* dl = new Download();
dl->m_url = url;
dl->m_options = options;
dl->m_sink.reset(new FileSink(path));
return dl;
}
void Download::addValidator(Validator * v)
void Download::addValidator(Validator* v)
{
m_sink->addValidator(v);
}
void Download::startImpl()
{
if(m_status == Job_Aborted)
{
if (m_status == Job_Aborted) {
qWarning() << "Attempt to start an aborted Download:" << m_url.toString();
emit aborted(m_index_within_job);
return;
}
QNetworkRequest request(m_url);
m_status = m_sink->init(request);
switch(m_status)
{
switch (m_status) {
case Job_Finished:
emit succeeded(m_index_within_job);
qDebug() << "Download cache hit " << m_url.toString();
@ -87,7 +85,7 @@ void Download::startImpl()
case Job_InProgress:
qDebug() << "Downloading " << m_url.toString();
break;
case Job_Failed_Proceed: // this is meaningless in this context. We do need a sink.
case Job_Failed_Proceed: // this is meaningless in this context. We do need a sink.
case Job_NotStarted:
case Job_Failed:
emit failed(m_index_within_job);
@ -97,8 +95,11 @@ void Download::startImpl()
}
request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT);
if (request.url().host().contains("api.curseforge.com")) {
request.setRawHeader("x-api-key", BuildConfig.CURSEFORGE_API_KEY.toUtf8());
};
QNetworkReply *rep = m_network->get(request);
QNetworkReply* rep = m_network->get(request);
m_reply.reset(rep);
connect(rep, SIGNAL(downloadProgress(qint64, qint64)), SLOT(downloadProgress(qint64, qint64)));
@ -117,17 +118,12 @@ void Download::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
void Download::downloadError(QNetworkReply::NetworkError error)
{
if(error == QNetworkReply::OperationCanceledError)
{
if (error == QNetworkReply::OperationCanceledError) {
qCritical() << "Aborted " << m_url.toString();
m_status = Job_Aborted;
}
else
{
if(m_options & Option::AcceptLocalFiles)
{
if(m_sink->hasLocalData())
{
} else {
if (m_options & Option::AcceptLocalFiles) {
if (m_sink->hasLocalData()) {
m_status = Job_Failed_Proceed;
return;
}
@ -138,11 +134,10 @@ void Download::downloadError(QNetworkReply::NetworkError error)
}
}
void Download::sslErrors(const QList<QSslError> & errors)
void Download::sslErrors(const QList<QSslError>& errors)
{
int i = 1;
for (auto error : errors)
{
for (auto error : errors) {
qCritical() << "Download" << m_url.toString() << "SSL Error #" << i << " : " << error.errorString();
auto cert = error.certificate();
qCritical() << "Certificate in question:\n" << cert.toText();
@ -153,33 +148,27 @@ void Download::sslErrors(const QList<QSslError> & errors)
bool Download::handleRedirect()
{
QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl();
if(!redirect.isValid())
{
if(!m_reply->hasRawHeader("Location"))
{
if (!redirect.isValid()) {
if (!m_reply->hasRawHeader("Location")) {
// no redirect -> it's fine to continue
return false;
}
// there is a Location header, but it's not correct. we need to apply some workarounds...
QByteArray redirectBA = m_reply->rawHeader("Location");
if(redirectBA.size() == 0)
{
if (redirectBA.size() == 0) {
// empty, yet present redirect header? WTF?
return false;
}
QString redirectStr = QString::fromUtf8(redirectBA);
if(redirectStr.startsWith("//"))
{
if (redirectStr.startsWith("//")) {
/*
* IF the URL begins with //, we need to insert the URL scheme.
* See: https://bugreports.qt.io/browse/QTBUG-41061
* See: http://tools.ietf.org/html/rfc3986#section-4.2
*/
redirectStr = m_reply->url().scheme() + ":" + redirectStr;
}
else if(redirectStr.startsWith("/"))
{
} else if (redirectStr.startsWith("/")) {
/*
* IF the URL begins with /, we need to process it as a relative URL
*/
@ -193,16 +182,13 @@ bool Download::handleRedirect()
* FIXME: report Qt bug for this
*/
redirect = QUrl(redirectStr, QUrl::TolerantMode);
if(!redirect.isValid())
{
if (!redirect.isValid()) {
qWarning() << "Failed to parse redirect URL:" << redirectStr;
downloadError(QNetworkReply::ProtocolFailure);
return false;
}
qDebug() << "Fixed location header:" << redirect;
}
else
{
} else {
qDebug() << "Location header:" << redirect;
}
@ -212,35 +198,28 @@ bool Download::handleRedirect()
return true;
}
void Download::downloadFinished()
{
// handle HTTP redirection first
if(handleRedirect())
{
if (handleRedirect()) {
qDebug() << "Download redirected:" << m_url.toString();
return;
}
// if the download failed before this point ...
if (m_status == Job_Failed_Proceed)
{
if (m_status == Job_Failed_Proceed) {
qDebug() << "Download failed but we are allowed to proceed:" << m_url.toString();
m_sink->abort();
m_reply.reset();
emit succeeded(m_index_within_job);
return;
}
else if (m_status == Job_Failed)
{
} else if (m_status == Job_Failed) {
qDebug() << "Download failed in previous step:" << m_url.toString();
m_sink->abort();
m_reply.reset();
emit failed(m_index_within_job);
return;
}
else if(m_status == Job_Aborted)
{
} else if (m_status == Job_Aborted) {
qDebug() << "Download aborted in previous step:" << m_url.toString();
m_sink->abort();
m_reply.reset();
@ -250,16 +229,14 @@ void Download::downloadFinished()
// make sure we got all the remaining data, if any
auto data = m_reply->readAll();
if(data.size())
{
if (data.size()) {
qDebug() << "Writing extra" << data.size() << "bytes to" << m_target_path;
m_status = m_sink->write(data);
}
// otherwise, finalize the whole graph
m_status = m_sink->finalize(*m_reply.get());
if (m_status != Job_Finished)
{
if (m_status != Job_Finished) {
qDebug() << "Download failed to finalize:" << m_url.toString();
m_sink->abort();
m_reply.reset();
@ -273,32 +250,25 @@ void Download::downloadFinished()
void Download::downloadReadyRead()
{
if(m_status == Job_InProgress)
{
if (m_status == Job_InProgress) {
auto data = m_reply->readAll();
m_status = m_sink->write(data);
if(m_status == Job_Failed)
{
if (m_status == Job_Failed) {
qCritical() << "Failed to process response chunk for " << m_target_path;
}
// qDebug() << "Download" << m_url.toString() << "gained" << data.size() << "bytes";
}
else
{
} else {
qCritical() << "Cannot write to " << m_target_path << ", illegal status" << m_status;
}
}
}
} // namespace Net
bool Net::Download::abort()
{
if(m_reply)
{
if (m_reply) {
m_reply->abort();
}
else
{
} else {
m_status = Job_Aborted;
}
return true;

View File

@ -831,10 +831,6 @@ public:
void retranslateUi(QMainWindow *MainWindow)
{
QString winTitle = tr("%1 - Version %2", "Launcher - Version X").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString());
if (!BuildConfig.BUILD_PLATFORM.isEmpty())
{
winTitle += tr(" on %1", "on platform, as in operating system").arg(BuildConfig.BUILD_PLATFORM);
}
MainWindow->setWindowTitle(winTitle);
// all the actions
for(auto * item: all_actions)

View File

@ -364,7 +364,7 @@ public:
{
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
auto key = keyEvent->key();
if (key == Qt::Key_Return || key == Qt::Key_Enter)
if ((key == Qt::Key_Return || key == Qt::Key_Enter) && eventType == QEvent::KeyPress)
{
emit editingDone();
return true;

View File

@ -45,6 +45,7 @@
#include <QTreeView>
#include <QInputDialog>
#include <QProcess>
#include <Qt>
#include "tools/MCEditTool.h"
#include "FileSystem.h"
@ -92,6 +93,7 @@ WorldListPage::WorldListPage(BaseInstance *inst, std::shared_ptr<WorldList> worl
WorldListProxyModel * proxy = new WorldListProxyModel(this);
proxy->setSortCaseSensitivity(Qt::CaseInsensitive);
proxy->setSourceModel(m_worlds.get());
proxy->setSortRole(Qt::UserRole);
ui->worldTreeView->setSortingEnabled(true);
ui->worldTreeView->setModel(proxy);
ui->worldTreeView->installEventFilter(this);

View File

@ -21,7 +21,8 @@ auto ListModel::debugName() const -> QString
void ListModel::fetchMore(const QModelIndex& parent)
{
if (parent.isValid()) return;
if (parent.isValid())
return;
if (nextSearchOffset == 0) {
qWarning() << "fetchMore with 0 offset is wrong...";
return;
@ -32,7 +33,9 @@ void ListModel::fetchMore(const QModelIndex& parent)
auto ListModel::data(const QModelIndex& index, int role) const -> QVariant
{
int pos = index.row();
if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { return QString("INVALID INDEX %1").arg(pos); }
if (pos >= modpacks.size() || pos < 0 || !index.isValid()) {
return QString("INVALID INDEX %1").arg(pos);
}
ModPlatform::IndexedPack pack = modpacks.at(pos);
if (role == Qt::DisplayRole) {
@ -46,7 +49,9 @@ auto ListModel::data(const QModelIndex& index, int role) const -> QVariant
}
return pack.description;
} else if (role == Qt::DecorationRole) {
if (m_logoMap.contains(pack.logoName)) { return (m_logoMap.value(pack.logoName)); }
if (m_logoMap.contains(pack.logoName)) {
return (m_logoMap.value(pack.logoName));
}
QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder");
((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl);
return icon;
@ -63,16 +68,15 @@ void ListModel::requestModVersions(ModPlatform::IndexedPack const& current)
{
auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile();
m_parent->apiProvider()->getVersions(this,
{ current.addonId.toString(), getMineVersions(), profile->getModLoader() });
m_parent->apiProvider()->getVersions(this, { current.addonId.toString(), getMineVersions(), profile->getModLoader() });
}
void ListModel::performPaginatedSearch()
{
auto profile = (dynamic_cast<MinecraftInstance*>((dynamic_cast<ModPage*>(parent()))->m_instance))->getPackProfile();
m_parent->apiProvider()->searchMods(this,
{ nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoader(), getMineVersions() });
m_parent->apiProvider()->searchMods(
this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoader(), getMineVersions() });
}
void ListModel::refresh()
@ -93,11 +97,9 @@ void ListModel::refresh()
void ListModel::searchWithTerm(const QString& term, const int sort, const bool filter_changed)
{
if (currentSearchTerm == term
&& currentSearchTerm.isNull() == term.isNull()
&& currentSort == sort
&& !filter_changed)
{ return; }
if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort && !filter_changed) {
return;
}
currentSearchTerm = term;
currentSort = sort;
@ -118,7 +120,9 @@ void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallbac
void ListModel::requestLogo(QString logo, QString url)
{
if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) { return; }
if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) {
return;
}
MetaEntryPtr entry =
APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0)));
@ -129,7 +133,9 @@ void ListModel::requestLogo(QString logo, QString url)
QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] {
job->deleteLater();
emit logoLoaded(logo, QIcon(fullPath));
if (waitingCallbacks.contains(logo)) { waitingCallbacks.value(logo)(fullPath); }
if (waitingCallbacks.contains(logo)) {
waitingCallbacks.value(logo)(fullPath);
}
});
QObject::connect(job, &NetJob::failed, this, [this, logo, job] {
@ -148,7 +154,9 @@ void ListModel::logoLoaded(QString logo, QIcon out)
m_loadingLogos.removeAll(logo);
m_logoMap.insert(logo, out);
for (int i = 0; i < modpacks.size(); i++) {
if (modpacks[i].logoName == logo) { emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); }
if (modpacks[i].logoName == logo) {
emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole });
}
}
}
@ -192,13 +200,16 @@ void ListModel::searchRequestFinished(QJsonDocument& doc)
void ListModel::searchRequestFailed(QString reason)
{
if (jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) {
if (!jobPtr->first()->m_reply) {
// Network error
QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load mods."));
} else if (jobPtr->first()->m_reply && jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) {
// 409 Gone, notify user to update
QMessageBox::critical(nullptr, tr("Error"),
//: %1 refers to the launcher itself
QString("%1 %2").arg(m_parent->displayName()).arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_NAME)));
// self-destruct
(dynamic_cast<ModDownloadDialog*>((dynamic_cast<ModPage*>(parent()))->parentWidget()))->reject();
QString("%1 %2")
.arg(m_parent->displayName())
.arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_NAME)));
}
jobPtr.reset();
@ -217,9 +228,12 @@ void ListModel::searchRequestFailed(QString reason)
void ListModel::versionRequestSucceeded(QJsonDocument doc, QString addonId)
{
auto& current = m_parent->getCurrent();
if (addonId != current.addonId) { return; }
if (addonId != current.addonId) {
return;
}
auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
QJsonArray arr = doc.array();
try {
loadIndexedPackVersions(current, arr);
} catch (const JSONValidationError& e) {

View File

@ -170,14 +170,12 @@ void ModPage::updateModVersions(int prev_count)
QString mcVersion = packProfile->getComponentVersion("net.minecraft");
QString loaderString = ModAPI::getModLoaderString(packProfile->getModLoader());
for (int i = 0; i < current.versions.size(); i++) {
auto version = current.versions[i];
bool valid = false;
for(auto& mcVer : m_filter->versions){
//NOTE: Flame doesn't care about loaderString, so passing it changes nothing.
if (validateVersion(version, mcVer.toString(), loaderString)) {
//NOTE: Flame doesn't care about loader, so passing it changes nothing.
if (validateVersion(version, mcVer.toString(), packProfile->getModLoader())) {
valid = true;
break;
}

View File

@ -37,7 +37,7 @@ class ModPage : public QWidget, public BasePage {
void retranslate() override;
auto shouldDisplay() const -> bool override = 0;
virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const -> bool = 0;
virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader = ModAPI::Unspecified) const -> bool = 0;
auto apiProvider() const -> const ModAPI* { return api.get(); };
auto getFilter() const -> const std::shared_ptr<ModFilterWidget::Filter> { return m_filter; }

View File

@ -118,7 +118,18 @@ void VanillaPage::filterChanged()
void VanillaPage::loaderFilterChanged()
{
auto minecraftVersion = m_selectedVersion->descriptor();
QString minecraftVersion;
if (m_selectedVersion)
{
minecraftVersion = m_selectedVersion->descriptor();
}
else
{
ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, "AAA"); // empty list
ui->loaderVersionList->setEmptyString(tr("No Minecraft version is selected."));
ui->loaderVersionList->setEmptyMode(VersionListView::String);
return;
}
if(ui->noneFilter->isChecked())
{
ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, "AAA"); // empty list
@ -158,7 +169,6 @@ void VanillaPage::loaderFilterChanged()
auto vlist = APPLICATION->metadataIndex()->get(m_selectedLoader);
ui->loaderVersionList->initialize(vlist.get());
ui->loaderVersionList->selectRecommended();
suggestCurrent();
ui->loaderVersionList->setEmptyString(tr("No versions are currently available for Minecraft %1").arg(minecraftVersion));
}
@ -205,8 +215,8 @@ void VanillaPage::suggestCurrent()
return;
}
// List is empty if either no mod loader is selected, or no versions are available
if(!ui->loaderVersionList->hasVersions())
// There isn't a selected version if the version list is empty
if(ui->loaderVersionList->selectedVersion() == nullptr)
dialog->setSuggestedPack(m_selectedVersion->descriptor(), new InstanceCreationTask(m_selectedVersion));
else
{

View File

@ -27,9 +27,9 @@ namespace Atl {
FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent)
{
currentSorting = Sorting::ByPopularity;
sortings.insert(tr("Sort by popularity"), Sorting::ByPopularity);
sortings.insert(tr("Sort by name"), Sorting::ByName);
sortings.insert(tr("Sort by game version"), Sorting::ByGameVersion);
sortings.insert(tr("Sort by Popularity"), Sorting::ByPopularity);
sortings.insert(tr("Sort by Name"), Sorting::ByName);
sortings.insert(tr("Sort by Game Version"), Sorting::ByGameVersion);
searchTerm = "";
}

View File

@ -1,5 +1,5 @@
#include "FlameModModel.h"
#include "Json.h"
#include "modplatform/flame/FlameModIndex.h"
namespace FlameMod {
@ -19,7 +19,7 @@ void ListModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray&
auto ListModel::documentToArray(QJsonDocument& obj) const -> QJsonArray
{
return obj.array();
return Json::ensureArray(obj.object(), "data");
}
} // namespace FlameMod

View File

@ -48,7 +48,7 @@ FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance)
// index is used to set the sorting with the flame api
ui->sortByBox->addItem(tr("Sort by Featured"));
ui->sortByBox->addItem(tr("Sort by Popularity"));
ui->sortByBox->addItem(tr("Sort by last updated"));
ui->sortByBox->addItem(tr("Sort by Last Updated"));
ui->sortByBox->addItem(tr("Sort by Name"));
ui->sortByBox->addItem(tr("Sort by Author"));
ui->sortByBox->addItem(tr("Sort by Downloads"));
@ -61,9 +61,9 @@ FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance)
connect(ui->modSelectionButton, &QPushButton::clicked, this, &FlameModPage::onModSelected);
}
auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer) const -> bool
auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader) const -> bool
{
(void) loaderVer;
Q_UNUSED(loader);
return ver.mcVersion.contains(mineVer);
}

View File

@ -35,6 +35,7 @@
#pragma once
#include "modplatform/ModAPI.h"
#include "ui/pages/modplatform/ModPage.h"
#include "modplatform/flame/FlameAPI.h"
@ -54,7 +55,7 @@ class FlameModPage : public ModPage {
inline auto debugName() const -> QString override { return "Flame"; }
inline auto metaEntryBase() const -> QString override { return "FlameMods"; };
auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const -> bool override;
auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader = ModAPI::Unspecified) const -> bool override;
auto shouldDisplay() const -> bool override;
};

View File

@ -1,6 +1,6 @@
#include "FlameModel.h"
#include "Application.h"
#include <Json.h>
#include "Application.h"
#include <MMCStrings.h>
#include <Version.h>
@ -9,61 +9,46 @@
namespace Flame {
ListModel::ListModel(QObject *parent) : QAbstractListModel(parent)
{
}
ListModel::ListModel(QObject* parent) : QAbstractListModel(parent) {}
ListModel::~ListModel()
{
}
ListModel::~ListModel() {}
int ListModel::rowCount(const QModelIndex &parent) const
int ListModel::rowCount(const QModelIndex& parent) const
{
return modpacks.size();
}
int ListModel::columnCount(const QModelIndex &parent) const
int ListModel::columnCount(const QModelIndex& parent) const
{
return 1;
}
QVariant ListModel::data(const QModelIndex &index, int role) const
QVariant ListModel::data(const QModelIndex& index, int role) const
{
int pos = index.row();
if(pos >= modpacks.size() || pos < 0 || !index.isValid())
{
if (pos >= modpacks.size() || pos < 0 || !index.isValid()) {
return QString("INVALID INDEX %1").arg(pos);
}
IndexedPack pack = modpacks.at(pos);
if(role == Qt::DisplayRole)
{
if (role == Qt::DisplayRole) {
return pack.name;
}
else if (role == Qt::ToolTipRole)
{
if(pack.description.length() > 100)
{
//some magic to prevent to long tooltips and replace html linebreaks
} else if (role == Qt::ToolTipRole) {
if (pack.description.length() > 100) {
// some magic to prevent to long tooltips and replace html linebreaks
QString edit = pack.description.left(97);
edit = edit.left(edit.lastIndexOf("<br>")).left(edit.lastIndexOf(" ")).append("...");
return edit;
}
return pack.description;
}
else if(role == Qt::DecorationRole)
{
if(m_logoMap.contains(pack.logoName))
{
} else if (role == Qt::DecorationRole) {
if (m_logoMap.contains(pack.logoName)) {
return (m_logoMap.value(pack.logoName));
}
QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder");
((ListModel *)this)->requestLogo(pack.logoName, pack.logoUrl);
((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl);
return icon;
}
else if(role == Qt::UserRole)
{
} else if (role == Qt::UserRole) {
QVariant v;
v.setValue(pack);
return v;
@ -76,9 +61,9 @@ void ListModel::logoLoaded(QString logo, QIcon out)
{
m_loadingLogos.removeAll(logo);
m_logoMap.insert(logo, out);
for(int i = 0; i < modpacks.size(); i++) {
if(modpacks[i].logoName == logo) {
emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole});
for (int i = 0; i < modpacks.size(); i++) {
if (modpacks[i].logoName == logo) {
emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole });
}
}
}
@ -91,8 +76,7 @@ void ListModel::logoFailed(QString logo)
void ListModel::requestLogo(QString logo, QString url)
{
if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo))
{
if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) {
return;
}
@ -101,18 +85,15 @@ void ListModel::requestLogo(QString logo, QString url)
job->addNetAction(Net::Download::makeCached(QUrl(url), entry));
auto fullPath = entry->getFullPath();
QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job]
{
QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] {
job->deleteLater();
emit logoLoaded(logo, QIcon(fullPath));
if(waitingCallbacks.contains(logo))
{
if (waitingCallbacks.contains(logo)) {
waitingCallbacks.value(logo)(fullPath);
}
});
QObject::connect(job, &NetJob::failed, this, [this, logo, job]
{
QObject::connect(job, &NetJob::failed, this, [this, logo, job] {
job->deleteLater();
emit logoFailed(logo);
});
@ -122,19 +103,16 @@ void ListModel::requestLogo(QString logo, QString url)
m_loadingLogos.append(logo);
}
void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback)
void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback)
{
if(m_logoMap.contains(logo))
{
if (m_logoMap.contains(logo)) {
callback(APPLICATION->metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath());
}
else
{
} else {
requestLogo(logo, logoUrl);
}
}
Qt::ItemFlags ListModel::flags(const QModelIndex &index) const
Qt::ItemFlags ListModel::flags(const QModelIndex& index) const
{
return QAbstractListModel::flags(index);
}
@ -148,7 +126,7 @@ void ListModel::fetchMore(const QModelIndex& parent)
{
if (parent.isValid())
return;
if(nextSearchOffset == 0) {
if (nextSearchOffset == 0) {
qWarning() << "fetchMore with 0 offset is wrong...";
return;
}
@ -157,17 +135,20 @@ void ListModel::fetchMore(const QModelIndex& parent)
void ListModel::performPaginatedSearch()
{
NetJob *netJob = new NetJob("Flame::Search", APPLICATION->network());
NetJob* netJob = new NetJob("Flame::Search", APPLICATION->network());
auto searchUrl = QString(
"https://addons-ecs.forgesvc.net/api/v2/addon/search?"
"categoryId=0&"
"gameId=432&"
"index=%1&"
"pageSize=25&"
"searchFilter=%2&"
"sectionId=4471&"
"sort=%3"
).arg(nextSearchOffset).arg(currentSearchTerm).arg(currentSort);
"https://api.curseforge.com/v1/mods/search?"
"gameId=432&"
"classId=4471&"
"index=%1&"
"pageSize=25&"
"searchFilter=%2&"
"sortField=%3&"
"sortOrder=desc")
.arg(nextSearchOffset)
.arg(currentSearchTerm)
.arg(currentSort + 1);
netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
jobPtr = netJob;
jobPtr->start();
@ -177,17 +158,16 @@ void ListModel::performPaginatedSearch()
void ListModel::searchWithTerm(const QString& term, int sort)
{
if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) {
if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) {
return;
}
currentSearchTerm = term;
currentSort = sort;
if(jobPtr) {
if (jobPtr) {
jobPtr->abort();
searchState = ResetRequested;
return;
}
else {
} else {
beginResetModel();
modpacks.clear();
endResetModel();
@ -203,30 +183,28 @@ void Flame::ListModel::searchRequestFinished()
QJsonParseError parse_error;
QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
if(parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset << " reason: " << parse_error.errorString();
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << response;
return;
}
QList<Flame::IndexedPack> newList;
auto packs = doc.array();
for(auto packRaw : packs) {
auto packs = Json::ensureArray(doc.object(), "data");
for (auto packRaw : packs) {
auto packObj = packRaw.toObject();
Flame::IndexedPack pack;
try
{
try {
Flame::loadIndexedPack(pack, packObj);
newList.append(pack);
}
catch(const JSONValidationError &e)
{
} catch (const JSONValidationError& e) {
qWarning() << "Error while loading pack from CurseForge: " << e.cause();
continue;
}
}
if(packs.size() < 25) {
if (packs.size() < 25) {
searchState = Finished;
} else {
nextSearchOffset += 25;
@ -241,7 +219,7 @@ void Flame::ListModel::searchRequestFailed(QString reason)
{
jobPtr.reset();
if(searchState == ResetRequested) {
if (searchState == ResetRequested) {
beginResetModel();
modpacks.clear();
endResetModel();
@ -253,5 +231,4 @@ void Flame::ListModel::searchRequestFailed(QString reason)
}
}
}
} // namespace Flame

View File

@ -39,13 +39,12 @@
#include <QKeyEvent>
#include "Application.h"
#include "FlameModel.h"
#include "InstanceImportTask.h"
#include "Json.h"
#include "ui/dialogs/NewInstanceDialog.h"
#include "InstanceImportTask.h"
#include "FlameModel.h"
FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget *parent)
: QWidget(parent), ui(new Ui::FlamePage), dialog(dialog)
FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), ui(new Ui::FlamePage), dialog(dialog)
{
ui->setupUi(this);
connect(ui->searchButton, &QPushButton::clicked, this, &FlamePage::triggerSearch);
@ -57,12 +56,12 @@ FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget *parent)
ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300);
// index is used to set the sorting with the curseforge api
ui->sortByBox->addItem(tr("Sort by featured"));
ui->sortByBox->addItem(tr("Sort by popularity"));
ui->sortByBox->addItem(tr("Sort by last updated"));
ui->sortByBox->addItem(tr("Sort by name"));
ui->sortByBox->addItem(tr("Sort by author"));
ui->sortByBox->addItem(tr("Sort by total downloads"));
ui->sortByBox->addItem(tr("Sort by Featured"));
ui->sortByBox->addItem(tr("Sort by Popularity"));
ui->sortByBox->addItem(tr("Sort by Last Updated"));
ui->sortByBox->addItem(tr("Sort by Name"));
ui->sortByBox->addItem(tr("Sort by Author"));
ui->sortByBox->addItem(tr("Sort by Total Downloads"));
connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlamePage::onSelectionChanged);
@ -112,10 +111,8 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second)
{
ui->versionSelectionBox->clear();
if(!first.isValid())
{
if(isOpened)
{
if (!first.isValid()) {
if (isOpened) {
dialog->setSuggestedPack();
}
return;
@ -130,14 +127,14 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second)
else
text = "<a href=\"" + current.websiteUrl + "\">" + name + "</a>";
if (!current.authors.empty()) {
auto authorToStr = [](Flame::ModpackAuthor & author) {
if(author.url.isEmpty()) {
auto authorToStr = [](Flame::ModpackAuthor& author) {
if (author.url.isEmpty()) {
return author.name;
}
return QString("<a href=\"%1\">%2</a>").arg(author.url, author.name);
};
QStringList authorStrs;
for(auto & author: current.authors) {
for (auto& author : current.authors) {
authorStrs.push_back(authorToStr(author));
}
text += "<br>" + tr(" by ") + authorStrs.join(", ");
@ -146,53 +143,46 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second)
ui->packDescription->setHtml(text + current.description);
if (current.versionsLoaded == false)
{
if (current.versionsLoaded == false) {
qDebug() << "Loading flame modpack versions";
auto netJob = new NetJob(QString("Flame::PackVersions(%1)").arg(current.name), APPLICATION->network());
auto response = new QByteArray();
int addonId = current.addonId;
netJob->addNetAction(Net::Download::makeByteArray(QString("https://addons-ecs.forgesvc.net/api/v2/addon/%1/files").arg(addonId), response));
netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.curseforge.com/v1/mods/%1/files").arg(addonId), response));
QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId]
{
if(addonId != current.addonId){
return; //wrong request
QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId] {
if (addonId != current.addonId) {
return; // wrong request
}
QJsonParseError parse_error;
QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if(parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset << " reason: " << parse_error.errorString();
if (parse_error.error != QJsonParseError::NoError) {
qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
}
QJsonArray arr = doc.array();
try
{
auto arr = Json::ensureArray(doc.object(), "data");
try {
Flame::loadIndexedPackVersions(current, arr);
}
catch(const JSONValidationError &e)
{
} catch (const JSONValidationError& e) {
qDebug() << *response;
qWarning() << "Error while reading flame modpack version: " << e.cause();
}
for(auto version : current.versions) {
for (auto version : current.versions) {
ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl));
}
suggestCurrent();
});
QObject::connect(netJob, &NetJob::finished, this, [response, netJob]
{
QObject::connect(netJob, &NetJob::finished, this, [response, netJob] {
netJob->deleteLater();
delete response;
});
netJob->start();
}
else
{
for(auto version : current.versions) {
} else {
for (auto version : current.versions) {
ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl));
}
@ -202,13 +192,11 @@ void FlamePage::onSelectionChanged(QModelIndex first, QModelIndex second)
void FlamePage::suggestCurrent()
{
if(!isOpened)
{
if (!isOpened) {
return;
}
if (selectedVersion.isEmpty())
{
if (selectedVersion.isEmpty()) {
dialog->setSuggestedPack();
return;
}
@ -216,16 +204,13 @@ void FlamePage::suggestCurrent()
dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion));
QString editedLogoName;
editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0);
listModel->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo)
{
dialog->setSuggestedIconFromFile(logo, editedLogoName);
});
listModel->getLogo(current.logoName, current.logoUrl,
[this, editedLogoName](QString logo) { dialog->setSuggestedIconFromFile(logo, editedLogoName); });
}
void FlamePage::onVersionSelectionChanged(QString data)
{
if(data.isNull() || data.isEmpty())
{
if (data.isNull() || data.isEmpty()) {
selectedVersion = "";
return;
}

View File

@ -26,9 +26,9 @@ namespace Ftb {
FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent)
{
currentSorting = Sorting::ByPlays;
sortings.insert(tr("Sort by plays"), Sorting::ByPlays);
sortings.insert(tr("Sort by installs"), Sorting::ByInstalls);
sortings.insert(tr("Sort by name"), Sorting::ByName);
sortings.insert(tr("Sort by Plays"), Sorting::ByPlays);
sortings.insert(tr("Sort by Installs"), Sorting::ByInstalls);
sortings.insert(tr("Sort by Name"), Sorting::ByName);
}
const QMap<QString, FilterModel::Sorting> FilterModel::getAvailableSortings()

View File

@ -16,8 +16,8 @@ namespace LegacyFTB {
FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent)
{
currentSorting = Sorting::ByGameVersion;
sortings.insert(tr("Sort by name"), Sorting::ByName);
sortings.insert(tr("Sort by game version"), Sorting::ByGameVersion);
sortings.insert(tr("Sort by Name"), Sorting::ByName);
sortings.insert(tr("Sort by Game Version"), Sorting::ByGameVersion);
}
bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const

View File

@ -34,6 +34,7 @@
*/
#include "ModrinthPage.h"
#include "modplatform/modrinth/ModrinthAPI.h"
#include "ui_ModPage.h"
#include "ModrinthModel.h"
@ -46,23 +47,33 @@ ModrinthPage::ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance)
ui->packView->setModel(listModel);
// index is used to set the sorting with the modrinth api
ui->sortByBox->addItem(tr("Sort by Relevence"));
ui->sortByBox->addItem(tr("Sort by Relevance"));
ui->sortByBox->addItem(tr("Sort by Downloads"));
ui->sortByBox->addItem(tr("Sort by Follows"));
ui->sortByBox->addItem(tr("Sort by last updated"));
ui->sortByBox->addItem(tr("Sort by newest"));
ui->sortByBox->addItem(tr("Sort by Last Updated"));
ui->sortByBox->addItem(tr("Sort by Newest"));
// sometimes Qt just ignores virtual slots and doesn't work as intended it seems,
// so it's best not to connect them in the parent's contructor...
// so it's best not to connect them in the parent's constructor...
connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch()));
connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged);
connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthPage::onVersionSelectionChanged);
connect(ui->modSelectionButton, &QPushButton::clicked, this, &ModrinthPage::onModSelected);
}
auto ModrinthPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer) const -> bool
auto ModrinthPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader) const -> bool
{
return ver.mcVersion.contains(mineVer) && ver.loaders.contains(loaderVer);
auto loaderStrings = ModrinthAPI::getModLoaderStrings(loader);
auto loaderCompatible = false;
for (auto remoteLoader : ver.loaders)
{
if (loaderStrings.contains(remoteLoader)) {
loaderCompatible = true;
break;
}
}
return ver.mcVersion.contains(mineVer) && loaderCompatible;
}
// I don't know why, but doing this on the parent class makes it so that

View File

@ -35,6 +35,7 @@
#pragma once
#include "modplatform/ModAPI.h"
#include "ui/pages/modplatform/ModPage.h"
#include "modplatform/modrinth/ModrinthAPI.h"
@ -54,7 +55,7 @@ class ModrinthPage : public ModPage {
inline auto debugName() const -> QString override { return "Modrinth"; }
inline auto metaEntryBase() const -> QString override { return "ModrinthPacks"; };
auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, QString loaderVer = "") const -> bool override;
auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader = ModAPI::Unspecified) const -> bool override;
auto shouldDisplay() const -> bool override;
};

View File

@ -23,12 +23,21 @@ void ModFilterWidget::setInstance(MinecraftInstance* instance)
{
m_instance = instance;
auto mcVersionSplit = mcVersionStr().split(".");
ui->strictVersionButton->setText(
tr("Strict match (= %1)").arg(mcVersionStr()));
ui->majorVersionButton->setText(
tr("Major version match (= %1.%2.x)").arg(mcVersionSplit[0], mcVersionSplit[1]));
// we can't do this for snapshots sadly
if(mcVersionStr().contains('.'))
{
auto mcVersionSplit = mcVersionStr().split(".");
ui->majorVersionButton->setText(
tr("Major version match (= %1.%2.x)").arg(mcVersionSplit[0], mcVersionSplit[1]));
}
else
{
ui->majorVersionButton->setText(tr("Major version match (unsupported)"));
disableVersionButton(Major);
}
ui->allVersionsButton->setText(
tr("Any version"));
//ui->betweenVersionsButton->setText(
@ -67,7 +76,6 @@ void ModFilterWidget::onVersionFilterChanged(int id)
//ui->lowerVersionComboBox->setEnabled(id == VersionButtonID::Between);
//ui->upperVersionComboBox->setEnabled(id == VersionButtonID::Between);
auto versionSplit = mcVersionStr().split(".");
int index = 0;
auto cast_id = (VersionButtonID) id;
@ -83,12 +91,14 @@ void ModFilterWidget::onVersionFilterChanged(int id)
case(VersionButtonID::Strict):
m_filter->versions.push_front(mcVersion());
break;
case(VersionButtonID::Major):
case(VersionButtonID::Major): {
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);
i = Version(QString("%1.%2.%3").arg(versionSplit[0], versionSplit[1], QString("%1").arg(index)));
}
break;
}
case(VersionButtonID::All):
// Empty list to avoid enumerating all versions :P
break;

View File

@ -26,21 +26,21 @@
<item row="2" column="0">
<widget class="QRadioButton" name="allVersionsButton">
<property name="text">
<string>allVersions</string>
<string notr="true">allVersions</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QRadioButton" name="strictVersionButton">
<property name="text">
<string>strictVersion</string>
<string notr="true">strictVersion</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QRadioButton" name="majorVersionButton">
<property name="text">
<string>majorVersion</string>
<string notr="true">majorVersion</string>
</property>
</widget>
</item>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
</dict>
</plist>