Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
da25f3b84e | |||
1e34de98ab | |||
7b46f50cf1 | |||
cab40026f2 | |||
ac66bddeda | |||
f4237be9bd | |||
dd11ccb3fd | |||
c4549a5375 | |||
e9b3140d12 | |||
22f5128e39 | |||
ae1aa6f63e |
36
.github/workflows/build.yml
vendored
36
.github/workflows/build.yml
vendored
@ -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"
|
||||
@ -167,7 +165,7 @@ 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 }}
|
||||
|
||||
@ -175,7 +173,7 @@ jobs:
|
||||
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
|
||||
@ -184,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
|
||||
@ -234,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
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -14,6 +14,8 @@ CMakeLists.txt.user.*
|
||||
/.project
|
||||
/.settings
|
||||
/.idea
|
||||
/.vscode
|
||||
.clang-format
|
||||
cmake-build-*/
|
||||
Debug
|
||||
|
||||
|
@ -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 1)
|
||||
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.")
|
||||
|
||||
|
@ -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@";
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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.";
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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
|
||||
|
94
launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp
Normal file
94
launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp
Normal 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.")
|
||||
);
|
||||
}
|
22
launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h
Normal file
22
launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h
Normal 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>);
|
||||
};
|
@ -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."));
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -199,7 +207,9 @@ void ListModel::searchRequestFailed(QString reason)
|
||||
// 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)));
|
||||
QString("%1 %2")
|
||||
.arg(m_parent->displayName())
|
||||
.arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_NAME)));
|
||||
}
|
||||
jobPtr.reset();
|
||||
|
||||
@ -218,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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
@ -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;
|
||||
}
|
||||
|
Reference in New Issue
Block a user