NOISSUE move files into paths that make more sense
This commit is contained in:
@ -8,7 +8,7 @@
|
||||
#include <QUrl>
|
||||
#include <memory>
|
||||
|
||||
#include "minecraft/OneSixRule.h"
|
||||
#include "OneSixRule.h"
|
||||
#include "minecraft/OpSys.h"
|
||||
#include "GradleSpecifier.h"
|
||||
#include "net/URLConstants.h"
|
||||
|
30
logic/minecraft/auth/AuthSession.cpp
Normal file
30
logic/minecraft/auth/AuthSession.cpp
Normal file
@ -0,0 +1,30 @@
|
||||
#include "AuthSession.h"
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QStringList>
|
||||
|
||||
QString AuthSession::serializeUserProperties()
|
||||
{
|
||||
QJsonObject userAttrs;
|
||||
for (auto key : u.properties.keys())
|
||||
{
|
||||
auto array = QJsonArray::fromStringList(u.properties.values(key));
|
||||
userAttrs.insert(key, array);
|
||||
}
|
||||
QJsonDocument value(userAttrs);
|
||||
return value.toJson(QJsonDocument::Compact);
|
||||
|
||||
}
|
||||
|
||||
bool AuthSession::MakeOffline(QString offline_playername)
|
||||
{
|
||||
if (status != PlayableOffline && status != PlayableOnline)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
session = "-";
|
||||
player_name = offline_playername;
|
||||
status = PlayableOffline;
|
||||
return true;
|
||||
}
|
51
logic/minecraft/auth/AuthSession.h
Normal file
51
logic/minecraft/auth/AuthSession.h
Normal file
@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QMultiMap>
|
||||
#include <memory>
|
||||
|
||||
#include "multimc_logic_export.h"
|
||||
|
||||
struct User
|
||||
{
|
||||
QString id;
|
||||
QMultiMap<QString, QString> properties;
|
||||
};
|
||||
|
||||
struct MULTIMC_LOGIC_EXPORT AuthSession
|
||||
{
|
||||
bool MakeOffline(QString offline_playername);
|
||||
|
||||
QString serializeUserProperties();
|
||||
|
||||
enum Status
|
||||
{
|
||||
Undetermined,
|
||||
RequiresPassword,
|
||||
PlayableOffline,
|
||||
PlayableOnline
|
||||
} status = Undetermined;
|
||||
|
||||
User u;
|
||||
|
||||
// client token
|
||||
QString client_token;
|
||||
// account user name
|
||||
QString username;
|
||||
// combined session ID
|
||||
QString session;
|
||||
// volatile auth token
|
||||
QString access_token;
|
||||
// profile name
|
||||
QString player_name;
|
||||
// profile ID
|
||||
QString uuid;
|
||||
// 'legacy' or 'mojang', depending on account type
|
||||
QString user_type;
|
||||
// Did the auth server reply?
|
||||
bool auth_server_online = false;
|
||||
// Did the user request online mode?
|
||||
bool wants_online = true;
|
||||
};
|
||||
|
||||
typedef std::shared_ptr<AuthSession> AuthSessionPtr;
|
278
logic/minecraft/auth/MojangAccount.cpp
Normal file
278
logic/minecraft/auth/MojangAccount.cpp
Normal file
@ -0,0 +1,278 @@
|
||||
/* Copyright 2013-2015 MultiMC Contributors
|
||||
*
|
||||
* Authors: Orochimarufan <orochimarufan.x3@gmail.com>
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "MojangAccount.h"
|
||||
#include "flows/RefreshTask.h"
|
||||
#include "flows/AuthenticateTask.h"
|
||||
|
||||
#include <QUuid>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QRegExp>
|
||||
#include <QStringList>
|
||||
#include <QJsonDocument>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
MojangAccountPtr MojangAccount::loadFromJson(const QJsonObject &object)
|
||||
{
|
||||
// The JSON object must at least have a username for it to be valid.
|
||||
if (!object.value("username").isString())
|
||||
{
|
||||
qCritical() << "Can't load Mojang account info from JSON object. Username field is "
|
||||
"missing or of the wrong type.";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QString username = object.value("username").toString("");
|
||||
QString clientToken = object.value("clientToken").toString("");
|
||||
QString accessToken = object.value("accessToken").toString("");
|
||||
|
||||
QJsonArray profileArray = object.value("profiles").toArray();
|
||||
if (profileArray.size() < 1)
|
||||
{
|
||||
qCritical() << "Can't load Mojang account with username \"" << username
|
||||
<< "\". No profiles found.";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QList<AccountProfile> profiles;
|
||||
for (QJsonValue profileVal : profileArray)
|
||||
{
|
||||
QJsonObject profileObject = profileVal.toObject();
|
||||
QString id = profileObject.value("id").toString("");
|
||||
QString name = profileObject.value("name").toString("");
|
||||
bool legacy = profileObject.value("legacy").toBool(false);
|
||||
if (id.isEmpty() || name.isEmpty())
|
||||
{
|
||||
qWarning() << "Unable to load a profile because it was missing an ID or a name.";
|
||||
continue;
|
||||
}
|
||||
profiles.append({id, name, legacy});
|
||||
}
|
||||
|
||||
MojangAccountPtr account(new MojangAccount());
|
||||
if (object.value("user").isObject())
|
||||
{
|
||||
User u;
|
||||
QJsonObject userStructure = object.value("user").toObject();
|
||||
u.id = userStructure.value("id").toString();
|
||||
/*
|
||||
QJsonObject propMap = userStructure.value("properties").toObject();
|
||||
for(auto key: propMap.keys())
|
||||
{
|
||||
auto values = propMap.operator[](key).toArray();
|
||||
for(auto value: values)
|
||||
u.properties.insert(key, value.toString());
|
||||
}
|
||||
*/
|
||||
account->m_user = u;
|
||||
}
|
||||
account->m_username = username;
|
||||
account->m_clientToken = clientToken;
|
||||
account->m_accessToken = accessToken;
|
||||
account->m_profiles = profiles;
|
||||
|
||||
// Get the currently selected profile.
|
||||
QString currentProfile = object.value("activeProfile").toString("");
|
||||
if (!currentProfile.isEmpty())
|
||||
account->setCurrentProfile(currentProfile);
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
MojangAccountPtr MojangAccount::createFromUsername(const QString &username)
|
||||
{
|
||||
MojangAccountPtr account(new MojangAccount());
|
||||
account->m_clientToken = QUuid::createUuid().toString().remove(QRegExp("[{}-]"));
|
||||
account->m_username = username;
|
||||
return account;
|
||||
}
|
||||
|
||||
QJsonObject MojangAccount::saveToJson() const
|
||||
{
|
||||
QJsonObject json;
|
||||
json.insert("username", m_username);
|
||||
json.insert("clientToken", m_clientToken);
|
||||
json.insert("accessToken", m_accessToken);
|
||||
|
||||
QJsonArray profileArray;
|
||||
for (AccountProfile profile : m_profiles)
|
||||
{
|
||||
QJsonObject profileObj;
|
||||
profileObj.insert("id", profile.id);
|
||||
profileObj.insert("name", profile.name);
|
||||
profileObj.insert("legacy", profile.legacy);
|
||||
profileArray.append(profileObj);
|
||||
}
|
||||
json.insert("profiles", profileArray);
|
||||
|
||||
QJsonObject userStructure;
|
||||
{
|
||||
userStructure.insert("id", m_user.id);
|
||||
/*
|
||||
QJsonObject userAttrs;
|
||||
for(auto key: m_user.properties.keys())
|
||||
{
|
||||
auto array = QJsonArray::fromStringList(m_user.properties.values(key));
|
||||
userAttrs.insert(key, array);
|
||||
}
|
||||
userStructure.insert("properties", userAttrs);
|
||||
*/
|
||||
}
|
||||
json.insert("user", userStructure);
|
||||
|
||||
if (m_currentProfile != -1)
|
||||
json.insert("activeProfile", currentProfile()->id);
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
bool MojangAccount::setCurrentProfile(const QString &profileId)
|
||||
{
|
||||
for (int i = 0; i < m_profiles.length(); i++)
|
||||
{
|
||||
if (m_profiles[i].id == profileId)
|
||||
{
|
||||
m_currentProfile = i;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const AccountProfile *MojangAccount::currentProfile() const
|
||||
{
|
||||
if (m_currentProfile == -1)
|
||||
return nullptr;
|
||||
return &m_profiles[m_currentProfile];
|
||||
}
|
||||
|
||||
AccountStatus MojangAccount::accountStatus() const
|
||||
{
|
||||
if (m_accessToken.isEmpty())
|
||||
return NotVerified;
|
||||
else
|
||||
return Verified;
|
||||
}
|
||||
|
||||
std::shared_ptr<YggdrasilTask> MojangAccount::login(AuthSessionPtr session,
|
||||
QString password)
|
||||
{
|
||||
Q_ASSERT(m_currentTask.get() == nullptr);
|
||||
|
||||
// take care of the true offline status
|
||||
if (accountStatus() == NotVerified && password.isEmpty())
|
||||
{
|
||||
if (session)
|
||||
{
|
||||
session->status = AuthSession::RequiresPassword;
|
||||
fillSession(session);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (password.isEmpty())
|
||||
{
|
||||
m_currentTask.reset(new RefreshTask(this));
|
||||
}
|
||||
else
|
||||
{
|
||||
m_currentTask.reset(new AuthenticateTask(this, password));
|
||||
}
|
||||
m_currentTask->assignSession(session);
|
||||
|
||||
connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
|
||||
connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString)));
|
||||
return m_currentTask;
|
||||
}
|
||||
|
||||
void MojangAccount::authSucceeded()
|
||||
{
|
||||
auto session = m_currentTask->getAssignedSession();
|
||||
if (session)
|
||||
{
|
||||
session->status =
|
||||
session->wants_online ? AuthSession::PlayableOnline : AuthSession::PlayableOffline;
|
||||
fillSession(session);
|
||||
session->auth_server_online = true;
|
||||
}
|
||||
m_currentTask.reset();
|
||||
emit changed();
|
||||
}
|
||||
|
||||
void MojangAccount::authFailed(QString reason)
|
||||
{
|
||||
auto session = m_currentTask->getAssignedSession();
|
||||
// This is emitted when the yggdrasil tasks time out or are cancelled.
|
||||
// -> we treat the error as no-op
|
||||
if (m_currentTask->state() == YggdrasilTask::STATE_FAILED_SOFT)
|
||||
{
|
||||
if (session)
|
||||
{
|
||||
session->status = accountStatus() == Verified ? AuthSession::PlayableOffline
|
||||
: AuthSession::RequiresPassword;
|
||||
session->auth_server_online = false;
|
||||
fillSession(session);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_accessToken = QString();
|
||||
emit changed();
|
||||
if (session)
|
||||
{
|
||||
session->status = AuthSession::RequiresPassword;
|
||||
session->auth_server_online = true;
|
||||
fillSession(session);
|
||||
}
|
||||
}
|
||||
m_currentTask.reset();
|
||||
}
|
||||
|
||||
void MojangAccount::fillSession(AuthSessionPtr session)
|
||||
{
|
||||
// the user name. you have to have an user name
|
||||
session->username = m_username;
|
||||
// volatile auth token
|
||||
session->access_token = m_accessToken;
|
||||
// the semi-permanent client token
|
||||
session->client_token = m_clientToken;
|
||||
if (currentProfile())
|
||||
{
|
||||
// profile name
|
||||
session->player_name = currentProfile()->name;
|
||||
// profile ID
|
||||
session->uuid = currentProfile()->id;
|
||||
// 'legacy' or 'mojang', depending on account type
|
||||
session->user_type = currentProfile()->legacy ? "legacy" : "mojang";
|
||||
if (!session->access_token.isEmpty())
|
||||
{
|
||||
session->session = "token:" + m_accessToken + ":" + m_profiles[m_currentProfile].id;
|
||||
}
|
||||
else
|
||||
{
|
||||
session->session = "-";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
session->player_name = "Player";
|
||||
session->session = "-";
|
||||
}
|
||||
session->u = user();
|
||||
}
|
173
logic/minecraft/auth/MojangAccount.h
Normal file
173
logic/minecraft/auth/MojangAccount.h
Normal file
@ -0,0 +1,173 @@
|
||||
/* Copyright 2013-2015 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QList>
|
||||
#include <QJsonObject>
|
||||
#include <QPair>
|
||||
#include <QMap>
|
||||
|
||||
#include <memory>
|
||||
#include "AuthSession.h"
|
||||
|
||||
#include "multimc_logic_export.h"
|
||||
|
||||
class Task;
|
||||
class YggdrasilTask;
|
||||
class MojangAccount;
|
||||
|
||||
typedef std::shared_ptr<MojangAccount> MojangAccountPtr;
|
||||
Q_DECLARE_METATYPE(MojangAccountPtr)
|
||||
|
||||
/**
|
||||
* A profile within someone's Mojang account.
|
||||
*
|
||||
* Currently, the profile system has not been implemented by Mojang yet,
|
||||
* but we might as well add some things for it in MultiMC right now so
|
||||
* we don't have to rip the code to pieces to add it later.
|
||||
*/
|
||||
struct AccountProfile
|
||||
{
|
||||
QString id;
|
||||
QString name;
|
||||
bool legacy;
|
||||
};
|
||||
|
||||
enum AccountStatus
|
||||
{
|
||||
NotVerified,
|
||||
Verified
|
||||
};
|
||||
|
||||
/**
|
||||
* Object that stores information about a certain Mojang account.
|
||||
*
|
||||
* Said information may include things such as that account's username, client token, and access
|
||||
* token if the user chose to stay logged in.
|
||||
*/
|
||||
class MULTIMC_LOGIC_EXPORT MojangAccount : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public: /* construction */
|
||||
//! Do not copy accounts. ever.
|
||||
explicit MojangAccount(const MojangAccount &other, QObject *parent) = delete;
|
||||
|
||||
//! Default constructor
|
||||
explicit MojangAccount(QObject *parent = 0) : QObject(parent) {};
|
||||
|
||||
//! Creates an empty account for the specified user name.
|
||||
static MojangAccountPtr createFromUsername(const QString &username);
|
||||
|
||||
//! Loads a MojangAccount from the given JSON object.
|
||||
static MojangAccountPtr loadFromJson(const QJsonObject &json);
|
||||
|
||||
//! Saves a MojangAccount to a JSON object and returns it.
|
||||
QJsonObject saveToJson() const;
|
||||
|
||||
public: /* manipulation */
|
||||
/**
|
||||
* Sets the currently selected profile to the profile with the given ID string.
|
||||
* If profileId is not in the list of available profiles, the function will simply return
|
||||
* false.
|
||||
*/
|
||||
bool setCurrentProfile(const QString &profileId);
|
||||
|
||||
/**
|
||||
* Attempt to login. Empty password means we use the token.
|
||||
* If the attempt fails because we already are performing some task, it returns false.
|
||||
*/
|
||||
std::shared_ptr<YggdrasilTask> login(AuthSessionPtr session,
|
||||
QString password = QString());
|
||||
|
||||
public: /* queries */
|
||||
const QString &username() const
|
||||
{
|
||||
return m_username;
|
||||
}
|
||||
|
||||
const QString &clientToken() const
|
||||
{
|
||||
return m_clientToken;
|
||||
}
|
||||
|
||||
const QString &accessToken() const
|
||||
{
|
||||
return m_accessToken;
|
||||
}
|
||||
|
||||
const QList<AccountProfile> &profiles() const
|
||||
{
|
||||
return m_profiles;
|
||||
}
|
||||
|
||||
const User &user()
|
||||
{
|
||||
return m_user;
|
||||
}
|
||||
|
||||
//! Returns the currently selected profile (if none, returns nullptr)
|
||||
const AccountProfile *currentProfile() const;
|
||||
|
||||
//! Returns whether the account is NotVerified, Verified or Online
|
||||
AccountStatus accountStatus() const;
|
||||
|
||||
signals:
|
||||
/**
|
||||
* This signal is emitted when the account changes
|
||||
*/
|
||||
void changed();
|
||||
|
||||
// TODO: better signalling for the various possible state changes - especially errors
|
||||
|
||||
protected: /* variables */
|
||||
QString m_username;
|
||||
|
||||
// Used to identify the client - the user can have multiple clients for the same account
|
||||
// Think: different launchers, all connecting to the same account/profile
|
||||
QString m_clientToken;
|
||||
|
||||
// Blank if not logged in.
|
||||
QString m_accessToken;
|
||||
|
||||
// Index of the selected profile within the list of available
|
||||
// profiles. -1 if nothing is selected.
|
||||
int m_currentProfile = -1;
|
||||
|
||||
// List of available profiles.
|
||||
QList<AccountProfile> m_profiles;
|
||||
|
||||
// the user structure, whatever it is.
|
||||
User m_user;
|
||||
|
||||
// current task we are executing here
|
||||
std::shared_ptr<YggdrasilTask> m_currentTask;
|
||||
|
||||
private
|
||||
slots:
|
||||
void authSucceeded();
|
||||
void authFailed(QString reason);
|
||||
|
||||
private:
|
||||
void fillSession(AuthSessionPtr session);
|
||||
|
||||
public:
|
||||
friend class YggdrasilTask;
|
||||
friend class AuthenticateTask;
|
||||
friend class ValidateTask;
|
||||
friend class RefreshTask;
|
||||
};
|
427
logic/minecraft/auth/MojangAccountList.cpp
Normal file
427
logic/minecraft/auth/MojangAccountList.cpp
Normal file
@ -0,0 +1,427 @@
|
||||
/* Copyright 2013-2015 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "MojangAccountList.h"
|
||||
#include "MojangAccount.h"
|
||||
|
||||
#include <QIODevice>
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QDir>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#include <FileSystem.h>
|
||||
|
||||
#define ACCOUNT_LIST_FORMAT_VERSION 2
|
||||
|
||||
MojangAccountList::MojangAccountList(QObject *parent) : QAbstractListModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
MojangAccountPtr MojangAccountList::findAccount(const QString &username) const
|
||||
{
|
||||
for (int i = 0; i < count(); i++)
|
||||
{
|
||||
MojangAccountPtr account = at(i);
|
||||
if (account->username() == username)
|
||||
return account;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const MojangAccountPtr MojangAccountList::at(int i) const
|
||||
{
|
||||
return MojangAccountPtr(m_accounts.at(i));
|
||||
}
|
||||
|
||||
void MojangAccountList::addAccount(const MojangAccountPtr account)
|
||||
{
|
||||
beginResetModel();
|
||||
connect(account.get(), SIGNAL(changed()), SLOT(accountChanged()));
|
||||
m_accounts.append(account);
|
||||
endResetModel();
|
||||
onListChanged();
|
||||
}
|
||||
|
||||
void MojangAccountList::removeAccount(const QString &username)
|
||||
{
|
||||
beginResetModel();
|
||||
for (auto account : m_accounts)
|
||||
{
|
||||
if (account->username() == username)
|
||||
{
|
||||
m_accounts.removeOne(account);
|
||||
return;
|
||||
}
|
||||
}
|
||||
endResetModel();
|
||||
onListChanged();
|
||||
}
|
||||
|
||||
void MojangAccountList::removeAccount(QModelIndex index)
|
||||
{
|
||||
beginResetModel();
|
||||
m_accounts.removeAt(index.row());
|
||||
endResetModel();
|
||||
onListChanged();
|
||||
}
|
||||
|
||||
MojangAccountPtr MojangAccountList::activeAccount() const
|
||||
{
|
||||
return m_activeAccount;
|
||||
}
|
||||
|
||||
void MojangAccountList::setActiveAccount(const QString &username)
|
||||
{
|
||||
beginResetModel();
|
||||
if (username.isEmpty())
|
||||
{
|
||||
m_activeAccount = nullptr;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (MojangAccountPtr account : m_accounts)
|
||||
{
|
||||
if (account->username() == username)
|
||||
m_activeAccount = account;
|
||||
}
|
||||
}
|
||||
endResetModel();
|
||||
onActiveChanged();
|
||||
}
|
||||
|
||||
void MojangAccountList::accountChanged()
|
||||
{
|
||||
// the list changed. there is no doubt.
|
||||
onListChanged();
|
||||
}
|
||||
|
||||
void MojangAccountList::onListChanged()
|
||||
{
|
||||
if (m_autosave)
|
||||
// TODO: Alert the user if this fails.
|
||||
saveList();
|
||||
|
||||
emit listChanged();
|
||||
}
|
||||
|
||||
void MojangAccountList::onActiveChanged()
|
||||
{
|
||||
if (m_autosave)
|
||||
saveList();
|
||||
|
||||
emit activeAccountChanged();
|
||||
}
|
||||
|
||||
int MojangAccountList::count() const
|
||||
{
|
||||
return m_accounts.count();
|
||||
}
|
||||
|
||||
QVariant MojangAccountList::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return QVariant();
|
||||
|
||||
if (index.row() > count())
|
||||
return QVariant();
|
||||
|
||||
MojangAccountPtr account = at(index.row());
|
||||
|
||||
switch (role)
|
||||
{
|
||||
case Qt::DisplayRole:
|
||||
switch (index.column())
|
||||
{
|
||||
case NameColumn:
|
||||
return account->username();
|
||||
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
case Qt::ToolTipRole:
|
||||
return account->username();
|
||||
|
||||
case PointerRole:
|
||||
return qVariantFromValue(account);
|
||||
|
||||
case Qt::CheckStateRole:
|
||||
switch (index.column())
|
||||
{
|
||||
case ActiveColumn:
|
||||
return account == m_activeAccount;
|
||||
}
|
||||
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
QVariant MojangAccountList::headerData(int section, Qt::Orientation orientation, int role) const
|
||||
{
|
||||
switch (role)
|
||||
{
|
||||
case Qt::DisplayRole:
|
||||
switch (section)
|
||||
{
|
||||
case ActiveColumn:
|
||||
return tr("Active?");
|
||||
|
||||
case NameColumn:
|
||||
return tr("Name");
|
||||
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
case Qt::ToolTipRole:
|
||||
switch (section)
|
||||
{
|
||||
case NameColumn:
|
||||
return tr("The name of the version.");
|
||||
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
int MojangAccountList::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
// Return count
|
||||
return count();
|
||||
}
|
||||
|
||||
int MojangAccountList::columnCount(const QModelIndex &parent) const
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
Qt::ItemFlags MojangAccountList::flags(const QModelIndex &index) const
|
||||
{
|
||||
if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid())
|
||||
{
|
||||
return Qt::NoItemFlags;
|
||||
}
|
||||
|
||||
return Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable;
|
||||
}
|
||||
|
||||
bool MojangAccountList::setData(const QModelIndex &index, const QVariant &value, int role)
|
||||
{
|
||||
if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if(role == Qt::CheckStateRole)
|
||||
{
|
||||
if(value == Qt::Checked)
|
||||
{
|
||||
MojangAccountPtr account = this->at(index.row());
|
||||
this->setActiveAccount(account->username());
|
||||
}
|
||||
}
|
||||
|
||||
emit dataChanged(index, index);
|
||||
return true;
|
||||
}
|
||||
|
||||
void MojangAccountList::updateListData(QList<MojangAccountPtr> versions)
|
||||
{
|
||||
beginResetModel();
|
||||
m_accounts = versions;
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
bool MojangAccountList::loadList(const QString &filePath)
|
||||
{
|
||||
QString path = filePath;
|
||||
if (path.isEmpty())
|
||||
path = m_listFilePath;
|
||||
if (path.isEmpty())
|
||||
{
|
||||
qCritical() << "Can't load Mojang account list. No file path given and no default set.";
|
||||
return false;
|
||||
}
|
||||
|
||||
QFile file(path);
|
||||
|
||||
// Try to open the file and fail if we can't.
|
||||
// TODO: We should probably report this error to the user.
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
{
|
||||
qCritical() << QString("Failed to read the account list file (%1).").arg(path).toUtf8();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read the file and close it.
|
||||
QByteArray jsonData = file.readAll();
|
||||
file.close();
|
||||
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError);
|
||||
|
||||
// Fail if the JSON is invalid.
|
||||
if (parseError.error != QJsonParseError::NoError)
|
||||
{
|
||||
qCritical() << QString("Failed to parse account list file: %1 at offset %2")
|
||||
.arg(parseError.errorString(), QString::number(parseError.offset))
|
||||
.toUtf8();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure the root is an object.
|
||||
if (!jsonDoc.isObject())
|
||||
{
|
||||
qCritical() << "Invalid account list JSON: Root should be an array.";
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject root = jsonDoc.object();
|
||||
|
||||
// Make sure the format version matches.
|
||||
if (root.value("formatVersion").toVariant().toInt() != ACCOUNT_LIST_FORMAT_VERSION)
|
||||
{
|
||||
QString newName = "accounts-old.json";
|
||||
qWarning() << "Format version mismatch when loading account list. Existing one will be renamed to"
|
||||
<< newName;
|
||||
|
||||
// Attempt to rename the old version.
|
||||
file.rename(newName);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Now, load the accounts array.
|
||||
beginResetModel();
|
||||
QJsonArray accounts = root.value("accounts").toArray();
|
||||
for (QJsonValue accountVal : accounts)
|
||||
{
|
||||
QJsonObject accountObj = accountVal.toObject();
|
||||
MojangAccountPtr account = MojangAccount::loadFromJson(accountObj);
|
||||
if (account.get() != nullptr)
|
||||
{
|
||||
connect(account.get(), SIGNAL(changed()), SLOT(accountChanged()));
|
||||
m_accounts.append(account);
|
||||
}
|
||||
else
|
||||
{
|
||||
qWarning() << "Failed to load an account.";
|
||||
}
|
||||
}
|
||||
// Load the active account.
|
||||
m_activeAccount = findAccount(root.value("activeAccount").toString(""));
|
||||
endResetModel();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MojangAccountList::saveList(const QString &filePath)
|
||||
{
|
||||
QString path(filePath);
|
||||
if (path.isEmpty())
|
||||
path = m_listFilePath;
|
||||
if (path.isEmpty())
|
||||
{
|
||||
qCritical() << "Can't save Mojang account list. No file path given and no default set.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// make sure the parent folder exists
|
||||
if(!FS::ensureFilePathExists(path))
|
||||
return false;
|
||||
|
||||
// make sure the file wasn't overwritten with a folder before (fixes a bug)
|
||||
QFileInfo finfo(path);
|
||||
if(finfo.isDir())
|
||||
{
|
||||
QDir badDir(path);
|
||||
badDir.removeRecursively();
|
||||
}
|
||||
|
||||
qDebug() << "Writing account list to" << path;
|
||||
|
||||
qDebug() << "Building JSON data structure.";
|
||||
// Build the JSON document to write to the list file.
|
||||
QJsonObject root;
|
||||
|
||||
root.insert("formatVersion", ACCOUNT_LIST_FORMAT_VERSION);
|
||||
|
||||
// Build a list of accounts.
|
||||
qDebug() << "Building account array.";
|
||||
QJsonArray accounts;
|
||||
for (MojangAccountPtr account : m_accounts)
|
||||
{
|
||||
QJsonObject accountObj = account->saveToJson();
|
||||
accounts.append(accountObj);
|
||||
}
|
||||
|
||||
// Insert the account list into the root object.
|
||||
root.insert("accounts", accounts);
|
||||
|
||||
if(m_activeAccount)
|
||||
{
|
||||
// Save the active account.
|
||||
root.insert("activeAccount", m_activeAccount->username());
|
||||
}
|
||||
|
||||
// Create a JSON document object to convert our JSON to bytes.
|
||||
QJsonDocument doc(root);
|
||||
|
||||
// Now that we're done building the JSON object, we can write it to the file.
|
||||
qDebug() << "Writing account list to file.";
|
||||
QFile file(path);
|
||||
|
||||
// Try to open the file and fail if we can't.
|
||||
// TODO: We should probably report this error to the user.
|
||||
if (!file.open(QIODevice::WriteOnly))
|
||||
{
|
||||
qCritical() << QString("Failed to read the account list file (%1).").arg(path).toUtf8();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write the JSON to the file.
|
||||
file.write(doc.toJson());
|
||||
file.setPermissions(QFile::ReadOwner|QFile::WriteOwner|QFile::ReadUser|QFile::WriteUser);
|
||||
file.close();
|
||||
|
||||
qDebug() << "Saved account list to" << path;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void MojangAccountList::setListFilePath(QString path, bool autosave)
|
||||
{
|
||||
m_listFilePath = path;
|
||||
m_autosave = autosave;
|
||||
}
|
||||
|
||||
bool MojangAccountList::anyAccountIsValid()
|
||||
{
|
||||
for(auto account:m_accounts)
|
||||
{
|
||||
if(account->accountStatus() != NotVerified)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
201
logic/minecraft/auth/MojangAccountList.h
Normal file
201
logic/minecraft/auth/MojangAccountList.h
Normal file
@ -0,0 +1,201 @@
|
||||
/* Copyright 2013-2015 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "MojangAccount.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QVariant>
|
||||
#include <QAbstractListModel>
|
||||
#include <QSharedPointer>
|
||||
|
||||
#include "multimc_logic_export.h"
|
||||
|
||||
/*!
|
||||
* \brief List of available Mojang accounts.
|
||||
* This should be loaded in the background by MultiMC on startup.
|
||||
*
|
||||
* This class also inherits from QAbstractListModel. Methods from that
|
||||
* class determine how this list shows up in a list view. Said methods
|
||||
* all have a default implementation, but they can be overridden by subclasses to
|
||||
* change the behavior of the list.
|
||||
*/
|
||||
class MULTIMC_LOGIC_EXPORT MojangAccountList : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum ModelRoles
|
||||
{
|
||||
PointerRole = 0x34B1CB48
|
||||
};
|
||||
|
||||
enum VListColumns
|
||||
{
|
||||
// TODO: Add icon column.
|
||||
|
||||
// First column - Active?
|
||||
ActiveColumn = 0,
|
||||
|
||||
// Second column - Name
|
||||
NameColumn,
|
||||
};
|
||||
|
||||
explicit MojangAccountList(QObject *parent = 0);
|
||||
|
||||
//! Gets the account at the given index.
|
||||
virtual const MojangAccountPtr at(int i) const;
|
||||
|
||||
//! Returns the number of accounts in the list.
|
||||
virtual int count() const;
|
||||
|
||||
//////// List Model Functions ////////
|
||||
virtual QVariant data(const QModelIndex &index, int role) const;
|
||||
virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const;
|
||||
virtual int rowCount(const QModelIndex &parent) const;
|
||||
virtual int columnCount(const QModelIndex &parent) const;
|
||||
virtual Qt::ItemFlags flags(const QModelIndex &index) const;
|
||||
virtual bool setData(const QModelIndex &index, const QVariant &value, int role);
|
||||
|
||||
/*!
|
||||
* Adds a the given Mojang account to the account list.
|
||||
*/
|
||||
virtual void addAccount(const MojangAccountPtr account);
|
||||
|
||||
/*!
|
||||
* Removes the mojang account with the given username from the account list.
|
||||
*/
|
||||
virtual void removeAccount(const QString &username);
|
||||
|
||||
/*!
|
||||
* Removes the account at the given QModelIndex.
|
||||
*/
|
||||
virtual void removeAccount(QModelIndex index);
|
||||
|
||||
/*!
|
||||
* \brief Finds an account by its username.
|
||||
* \param The username of the account to find.
|
||||
* \return A const pointer to the account with the given username. NULL if
|
||||
* one doesn't exist.
|
||||
*/
|
||||
virtual MojangAccountPtr findAccount(const QString &username) const;
|
||||
|
||||
/*!
|
||||
* Sets the default path to save the list file to.
|
||||
* If autosave is true, this list will automatically save to the given path whenever it changes.
|
||||
* THIS FUNCTION DOES NOT LOAD THE LIST. If you set autosave, be sure to call loadList() immediately
|
||||
* after calling this function to ensure an autosaved change doesn't overwrite the list you intended
|
||||
* to load.
|
||||
*/
|
||||
virtual void setListFilePath(QString path, bool autosave = false);
|
||||
|
||||
/*!
|
||||
* \brief Loads the account list from the given file path.
|
||||
* If the given file is an empty string (default), will load from the default account list file.
|
||||
* \return True if successful, otherwise false.
|
||||
*/
|
||||
virtual bool loadList(const QString &file = "");
|
||||
|
||||
/*!
|
||||
* \brief Saves the account list to the given file.
|
||||
* If the given file is an empty string (default), will save from the default account list file.
|
||||
* \return True if successful, otherwise false.
|
||||
*/
|
||||
virtual bool saveList(const QString &file = "");
|
||||
|
||||
/*!
|
||||
* \brief Gets a pointer to the account that the user has selected as their "active" account.
|
||||
* Which account is active can be overridden on a per-instance basis, but this will return the one that
|
||||
* is set as active globally.
|
||||
* \return The currently active MojangAccount. If there isn't an active account, returns a null pointer.
|
||||
*/
|
||||
virtual MojangAccountPtr activeAccount() const;
|
||||
|
||||
/*!
|
||||
* Sets the given account as the current active account.
|
||||
* If the username given is an empty string, sets the active account to nothing.
|
||||
*/
|
||||
virtual void setActiveAccount(const QString &username);
|
||||
|
||||
/*!
|
||||
* Returns true if any of the account is at least Validated
|
||||
*/
|
||||
bool anyAccountIsValid();
|
||||
|
||||
signals:
|
||||
/*!
|
||||
* Signal emitted to indicate that the account list has changed.
|
||||
* This will also fire if the value of an element in the list changes (will be implemented
|
||||
* later).
|
||||
*/
|
||||
void listChanged();
|
||||
|
||||
/*!
|
||||
* Signal emitted to indicate that the active account has changed.
|
||||
*/
|
||||
void activeAccountChanged();
|
||||
|
||||
public
|
||||
slots:
|
||||
/**
|
||||
* This is called when one of the accounts changes and the list needs to be updated
|
||||
*/
|
||||
void accountChanged();
|
||||
|
||||
protected:
|
||||
/*!
|
||||
* Called whenever the list changes.
|
||||
* This emits the listChanged() signal and autosaves the list (if autosave is enabled).
|
||||
*/
|
||||
void onListChanged();
|
||||
|
||||
/*!
|
||||
* Called whenever the active account changes.
|
||||
* Emits the activeAccountChanged() signal and autosaves the list if enabled.
|
||||
*/
|
||||
void onActiveChanged();
|
||||
|
||||
QList<MojangAccountPtr> m_accounts;
|
||||
|
||||
/*!
|
||||
* Account that is currently active.
|
||||
*/
|
||||
MojangAccountPtr m_activeAccount;
|
||||
|
||||
//! Path to the account list file. Empty string if there isn't one.
|
||||
QString m_listFilePath;
|
||||
|
||||
/*!
|
||||
* If true, the account list will automatically save to the account list path when it changes.
|
||||
* Ignored if m_listFilePath is blank.
|
||||
*/
|
||||
bool m_autosave = false;
|
||||
|
||||
protected
|
||||
slots:
|
||||
/*!
|
||||
* Updates this list with the given list of accounts.
|
||||
* This is done by copying each account in the given list and inserting it
|
||||
* into this one.
|
||||
* We need to do this so that we can set the parents of the accounts are set to this
|
||||
* account list. This can't be done in the load task, because the accounts the load
|
||||
* task creates are on the load task's thread and Qt won't allow their parents
|
||||
* to be set to something created on another thread.
|
||||
* To get around that problem, we invoke this method on the GUI thread, which
|
||||
* then copies the accounts and sets their parents correctly.
|
||||
* \param accounts List of accounts whose parents should be set.
|
||||
*/
|
||||
virtual void updateListData(QList<MojangAccountPtr> versions);
|
||||
};
|
255
logic/minecraft/auth/YggdrasilTask.cpp
Normal file
255
logic/minecraft/auth/YggdrasilTask.cpp
Normal file
@ -0,0 +1,255 @@
|
||||
/* Copyright 2013-2015 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "YggdrasilTask.h"
|
||||
#include "MojangAccount.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonDocument>
|
||||
#include <QNetworkReply>
|
||||
#include <QByteArray>
|
||||
|
||||
#include <Env.h>
|
||||
|
||||
#include <net/URLConstants.h>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
YggdrasilTask::YggdrasilTask(MojangAccount *account, QObject *parent)
|
||||
: Task(parent), m_account(account)
|
||||
{
|
||||
changeState(STATE_CREATED);
|
||||
}
|
||||
|
||||
void YggdrasilTask::executeTask()
|
||||
{
|
||||
changeState(STATE_SENDING_REQUEST);
|
||||
|
||||
// Get the content of the request we're going to send to the server.
|
||||
QJsonDocument doc(getRequestContent());
|
||||
|
||||
auto worker = ENV.qnam();
|
||||
QUrl reqUrl("https://" + URLConstants::AUTH_BASE + getEndpoint());
|
||||
QNetworkRequest netRequest(reqUrl);
|
||||
netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
QByteArray requestData = doc.toJson();
|
||||
m_netReply = worker->post(netRequest, requestData);
|
||||
connect(m_netReply, &QNetworkReply::finished, this, &YggdrasilTask::processReply);
|
||||
connect(m_netReply, &QNetworkReply::uploadProgress, this, &YggdrasilTask::refreshTimers);
|
||||
connect(m_netReply, &QNetworkReply::downloadProgress, this, &YggdrasilTask::refreshTimers);
|
||||
connect(m_netReply, &QNetworkReply::sslErrors, this, &YggdrasilTask::sslErrors);
|
||||
timeout_keeper.setSingleShot(true);
|
||||
timeout_keeper.start(timeout_max);
|
||||
counter.setSingleShot(false);
|
||||
counter.start(time_step);
|
||||
progress(0, timeout_max);
|
||||
connect(&timeout_keeper, &QTimer::timeout, this, &YggdrasilTask::abortByTimeout);
|
||||
connect(&counter, &QTimer::timeout, this, &YggdrasilTask::heartbeat);
|
||||
}
|
||||
|
||||
void YggdrasilTask::refreshTimers(qint64, qint64)
|
||||
{
|
||||
timeout_keeper.stop();
|
||||
timeout_keeper.start(timeout_max);
|
||||
progress(count = 0, timeout_max);
|
||||
}
|
||||
void YggdrasilTask::heartbeat()
|
||||
{
|
||||
count += time_step;
|
||||
progress(count, timeout_max);
|
||||
}
|
||||
|
||||
bool YggdrasilTask::abort()
|
||||
{
|
||||
progress(timeout_max, timeout_max);
|
||||
// TODO: actually use this in a meaningful way
|
||||
m_aborted = YggdrasilTask::BY_USER;
|
||||
m_netReply->abort();
|
||||
return true;
|
||||
}
|
||||
|
||||
void YggdrasilTask::abortByTimeout()
|
||||
{
|
||||
progress(timeout_max, timeout_max);
|
||||
// TODO: actually use this in a meaningful way
|
||||
m_aborted = YggdrasilTask::BY_TIMEOUT;
|
||||
m_netReply->abort();
|
||||
}
|
||||
|
||||
void YggdrasilTask::sslErrors(QList<QSslError> errors)
|
||||
{
|
||||
int i = 1;
|
||||
for (auto error : errors)
|
||||
{
|
||||
qCritical() << "LOGIN SSL Error #" << i << " : " << error.errorString();
|
||||
auto cert = error.certificate();
|
||||
qCritical() << "Certificate in question:\n" << cert.toText();
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
void YggdrasilTask::processReply()
|
||||
{
|
||||
changeState(STATE_PROCESSING_RESPONSE);
|
||||
|
||||
switch (m_netReply->error())
|
||||
{
|
||||
case QNetworkReply::NoError:
|
||||
break;
|
||||
case QNetworkReply::TimeoutError:
|
||||
changeState(STATE_FAILED_SOFT, tr("Authentication operation timed out."));
|
||||
return;
|
||||
case QNetworkReply::OperationCanceledError:
|
||||
changeState(STATE_FAILED_SOFT, tr("Authentication operation cancelled."));
|
||||
return;
|
||||
case QNetworkReply::SslHandshakeFailedError:
|
||||
changeState(
|
||||
STATE_FAILED_SOFT,
|
||||
tr("<b>SSL Handshake failed.</b><br/>There might be a few causes for it:<br/>"
|
||||
"<ul>"
|
||||
"<li>You use Windows XP and need to <a "
|
||||
"href=\"http://www.microsoft.com/en-us/download/details.aspx?id=38918\">update "
|
||||
"your root certificates</a></li>"
|
||||
"<li>Some device on your network is interfering with SSL traffic. In that case, "
|
||||
"you have bigger worries than Minecraft not starting.</li>"
|
||||
"<li>Possibly something else. Check the MultiMC log file for details</li>"
|
||||
"</ul>"));
|
||||
return;
|
||||
// used for invalid credentials and similar errors. Fall through.
|
||||
case QNetworkReply::ContentOperationNotPermittedError:
|
||||
break;
|
||||
default:
|
||||
changeState(STATE_FAILED_SOFT,
|
||||
tr("Authentication operation failed due to a network error: %1 (%2)")
|
||||
.arg(m_netReply->errorString()).arg(m_netReply->error()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to parse the response regardless of the response code.
|
||||
// Sometimes the auth server will give more information and an error code.
|
||||
QJsonParseError jsonError;
|
||||
QByteArray replyData = m_netReply->readAll();
|
||||
QJsonDocument doc = QJsonDocument::fromJson(replyData, &jsonError);
|
||||
// Check the response code.
|
||||
int responseCode = m_netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
if (responseCode == 200)
|
||||
{
|
||||
// If the response code was 200, then there shouldn't be an error. Make sure
|
||||
// anyways.
|
||||
// Also, sometimes an empty reply indicates success. If there was no data received,
|
||||
// pass an empty json object to the processResponse function.
|
||||
if (jsonError.error == QJsonParseError::NoError || replyData.size() == 0)
|
||||
{
|
||||
processResponse(replyData.size() > 0 ? doc.object() : QJsonObject());
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
changeState(STATE_FAILED_SOFT, tr("Failed to parse authentication server response "
|
||||
"JSON response: %1 at offset %2.")
|
||||
.arg(jsonError.errorString())
|
||||
.arg(jsonError.offset));
|
||||
qCritical() << replyData;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If the response code was not 200, then Yggdrasil may have given us information
|
||||
// about the error.
|
||||
// If we can parse the response, then get information from it. Otherwise just say
|
||||
// there was an unknown error.
|
||||
if (jsonError.error == QJsonParseError::NoError)
|
||||
{
|
||||
// We were able to parse the server's response. Woo!
|
||||
// Call processError. If a subclass has overridden it then they'll handle their
|
||||
// stuff there.
|
||||
qDebug() << "The request failed, but the server gave us an error message. "
|
||||
"Processing error.";
|
||||
processError(doc.object());
|
||||
}
|
||||
else
|
||||
{
|
||||
// The server didn't say anything regarding the error. Give the user an unknown
|
||||
// error.
|
||||
qDebug()
|
||||
<< "The request failed and the server gave no error message. Unknown error.";
|
||||
changeState(STATE_FAILED_SOFT,
|
||||
tr("An unknown error occurred when trying to communicate with the "
|
||||
"authentication server: %1").arg(m_netReply->errorString()));
|
||||
}
|
||||
}
|
||||
|
||||
void YggdrasilTask::processError(QJsonObject responseData)
|
||||
{
|
||||
QJsonValue errorVal = responseData.value("error");
|
||||
QJsonValue errorMessageValue = responseData.value("errorMessage");
|
||||
QJsonValue causeVal = responseData.value("cause");
|
||||
|
||||
if (errorVal.isString() && errorMessageValue.isString())
|
||||
{
|
||||
m_error = std::shared_ptr<Error>(new Error{
|
||||
errorVal.toString(""), errorMessageValue.toString(""), causeVal.toString("")});
|
||||
changeState(STATE_FAILED_HARD, m_error->m_errorMessageVerbose);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Error is not in standard format. Don't set m_error and return unknown error.
|
||||
changeState(STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred."));
|
||||
}
|
||||
}
|
||||
|
||||
QString YggdrasilTask::getStateMessage() const
|
||||
{
|
||||
switch (m_state)
|
||||
{
|
||||
case STATE_CREATED:
|
||||
return "Waiting...";
|
||||
case STATE_SENDING_REQUEST:
|
||||
return tr("Sending request to auth servers...");
|
||||
case STATE_PROCESSING_RESPONSE:
|
||||
return tr("Processing response from servers...");
|
||||
case STATE_SUCCEEDED:
|
||||
return tr("Authentication task succeeded.");
|
||||
case STATE_FAILED_SOFT:
|
||||
return tr("Failed to contact the authentication server.");
|
||||
case STATE_FAILED_HARD:
|
||||
return tr("Failed to authenticate.");
|
||||
default:
|
||||
return tr("...");
|
||||
}
|
||||
}
|
||||
|
||||
void YggdrasilTask::changeState(YggdrasilTask::State newState, QString reason)
|
||||
{
|
||||
m_state = newState;
|
||||
setStatus(getStateMessage());
|
||||
if (newState == STATE_SUCCEEDED)
|
||||
{
|
||||
emitSucceeded();
|
||||
}
|
||||
else if (newState == STATE_FAILED_HARD || newState == STATE_FAILED_SOFT)
|
||||
{
|
||||
emitFailed(reason);
|
||||
}
|
||||
}
|
||||
|
||||
YggdrasilTask::State YggdrasilTask::state()
|
||||
{
|
||||
return m_state;
|
||||
}
|
150
logic/minecraft/auth/YggdrasilTask.h
Normal file
150
logic/minecraft/auth/YggdrasilTask.h
Normal file
@ -0,0 +1,150 @@
|
||||
/* Copyright 2013-2015 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <tasks/Task.h>
|
||||
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
#include <QTimer>
|
||||
#include <qsslerror.h>
|
||||
|
||||
#include "MojangAccount.h"
|
||||
|
||||
class QNetworkReply;
|
||||
|
||||
/**
|
||||
* A Yggdrasil task is a task that performs an operation on a given mojang account.
|
||||
*/
|
||||
class YggdrasilTask : public Task
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit YggdrasilTask(MojangAccount * account, QObject *parent = 0);
|
||||
|
||||
/**
|
||||
* assign a session to this task. the session will be filled with required infomration
|
||||
* upon completion
|
||||
*/
|
||||
void assignSession(AuthSessionPtr session)
|
||||
{
|
||||
m_session = session;
|
||||
}
|
||||
|
||||
/// get the assigned session for filling with information.
|
||||
AuthSessionPtr getAssignedSession()
|
||||
{
|
||||
return m_session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class describing a Yggdrasil error response.
|
||||
*/
|
||||
struct Error
|
||||
{
|
||||
QString m_errorMessageShort;
|
||||
QString m_errorMessageVerbose;
|
||||
QString m_cause;
|
||||
};
|
||||
|
||||
enum AbortedBy
|
||||
{
|
||||
BY_NOTHING,
|
||||
BY_USER,
|
||||
BY_TIMEOUT
|
||||
} m_aborted = BY_NOTHING;
|
||||
|
||||
/**
|
||||
* Enum for describing the state of the current task.
|
||||
* Used by the getStateMessage function to determine what the status message should be.
|
||||
*/
|
||||
enum State
|
||||
{
|
||||
STATE_CREATED,
|
||||
STATE_SENDING_REQUEST,
|
||||
STATE_PROCESSING_RESPONSE,
|
||||
STATE_FAILED_SOFT, //!< soft failure. this generally means the user auth details haven't been invalidated
|
||||
STATE_FAILED_HARD, //!< hard failure. auth is invalid
|
||||
STATE_SUCCEEDED
|
||||
} m_state = STATE_CREATED;
|
||||
|
||||
protected:
|
||||
|
||||
virtual void executeTask() override;
|
||||
|
||||
/**
|
||||
* Gets the JSON object that will be sent to the authentication server.
|
||||
* Should be overridden by subclasses.
|
||||
*/
|
||||
virtual QJsonObject getRequestContent() const = 0;
|
||||
|
||||
/**
|
||||
* Gets the endpoint to POST to.
|
||||
* No leading slash.
|
||||
*/
|
||||
virtual QString getEndpoint() const = 0;
|
||||
|
||||
/**
|
||||
* Processes the response received from the server.
|
||||
* If an error occurred, this should emit a failed signal and return false.
|
||||
* If Yggdrasil gave an error response, it should call setError() first, and then return false.
|
||||
* Otherwise, it should return true.
|
||||
* Note: If the response from the server was blank, and the HTTP code was 200, this function is called with
|
||||
* an empty QJsonObject.
|
||||
*/
|
||||
virtual void processResponse(QJsonObject responseData) = 0;
|
||||
|
||||
/**
|
||||
* Processes an error response received from the server.
|
||||
* The default implementation will read data from Yggdrasil's standard error response format and set it as this task's Error.
|
||||
* \returns a QString error message that will be passed to emitFailed.
|
||||
*/
|
||||
virtual void processError(QJsonObject responseData);
|
||||
|
||||
/**
|
||||
* Returns the state message for the given state.
|
||||
* Used to set the status message for the task.
|
||||
* Should be overridden by subclasses that want to change messages for a given state.
|
||||
*/
|
||||
virtual QString getStateMessage() const;
|
||||
|
||||
protected
|
||||
slots:
|
||||
void processReply();
|
||||
void refreshTimers(qint64, qint64);
|
||||
void heartbeat();
|
||||
void sslErrors(QList<QSslError>);
|
||||
|
||||
void changeState(State newState, QString reason=QString());
|
||||
public
|
||||
slots:
|
||||
virtual bool abort() override;
|
||||
void abortByTimeout();
|
||||
State state();
|
||||
protected:
|
||||
// FIXME: segfault disaster waiting to happen
|
||||
MojangAccount *m_account = nullptr;
|
||||
QNetworkReply *m_netReply = nullptr;
|
||||
std::shared_ptr<Error> m_error;
|
||||
QTimer timeout_keeper;
|
||||
QTimer counter;
|
||||
int count = 0; // num msec since time reset
|
||||
|
||||
const int timeout_max = 30000;
|
||||
const int time_step = 50;
|
||||
|
||||
AuthSessionPtr m_session;
|
||||
};
|
202
logic/minecraft/auth/flows/AuthenticateTask.cpp
Normal file
202
logic/minecraft/auth/flows/AuthenticateTask.cpp
Normal file
@ -0,0 +1,202 @@
|
||||
|
||||
/* Copyright 2013-2015 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "AuthenticateTask.h"
|
||||
#include "../MojangAccount.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QVariant>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QUuid>
|
||||
|
||||
AuthenticateTask::AuthenticateTask(MojangAccount * account, const QString &password,
|
||||
QObject *parent)
|
||||
: YggdrasilTask(account, parent), m_password(password)
|
||||
{
|
||||
}
|
||||
|
||||
QJsonObject AuthenticateTask::getRequestContent() const
|
||||
{
|
||||
/*
|
||||
* {
|
||||
* "agent": { // optional
|
||||
* "name": "Minecraft", // So far this is the only encountered value
|
||||
* "version": 1 // This number might be increased
|
||||
* // by the vanilla client in the future
|
||||
* },
|
||||
* "username": "mojang account name", // Can be an email address or player name for
|
||||
// unmigrated accounts
|
||||
* "password": "mojang account password",
|
||||
* "clientToken": "client identifier" // optional
|
||||
* "requestUser": true/false // request the user structure
|
||||
* }
|
||||
*/
|
||||
QJsonObject req;
|
||||
|
||||
{
|
||||
QJsonObject agent;
|
||||
// C++ makes string literals void* for some stupid reason, so we have to tell it
|
||||
// QString... Thanks Obama.
|
||||
agent.insert("name", QString("Minecraft"));
|
||||
agent.insert("version", 1);
|
||||
req.insert("agent", agent);
|
||||
}
|
||||
|
||||
req.insert("username", m_account->username());
|
||||
req.insert("password", m_password);
|
||||
req.insert("requestUser", true);
|
||||
|
||||
// If we already have a client token, give it to the server.
|
||||
// Otherwise, let the server give us one.
|
||||
|
||||
if(m_account->m_clientToken.isEmpty())
|
||||
{
|
||||
auto uuid = QUuid::createUuid();
|
||||
auto uuidString = uuid.toString().remove('{').remove('-').remove('}');
|
||||
m_account->m_clientToken = uuidString;
|
||||
}
|
||||
req.insert("clientToken", m_account->m_clientToken);
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
void AuthenticateTask::processResponse(QJsonObject responseData)
|
||||
{
|
||||
// Read the response data. We need to get the client token, access token, and the selected
|
||||
// profile.
|
||||
qDebug() << "Processing authentication response.";
|
||||
// qDebug() << responseData;
|
||||
// If we already have a client token, make sure the one the server gave us matches our
|
||||
// existing one.
|
||||
qDebug() << "Getting client token.";
|
||||
QString clientToken = responseData.value("clientToken").toString("");
|
||||
if (clientToken.isEmpty())
|
||||
{
|
||||
// Fail if the server gave us an empty client token
|
||||
changeState(STATE_FAILED_HARD, tr("Authentication server didn't send a client token."));
|
||||
return;
|
||||
}
|
||||
if (!m_account->m_clientToken.isEmpty() && clientToken != m_account->m_clientToken)
|
||||
{
|
||||
changeState(STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported."));
|
||||
return;
|
||||
}
|
||||
// Set the client token.
|
||||
m_account->m_clientToken = clientToken;
|
||||
|
||||
// Now, we set the access token.
|
||||
qDebug() << "Getting access token.";
|
||||
QString accessToken = responseData.value("accessToken").toString("");
|
||||
if (accessToken.isEmpty())
|
||||
{
|
||||
// Fail if the server didn't give us an access token.
|
||||
changeState(STATE_FAILED_HARD, tr("Authentication server didn't send an access token."));
|
||||
return;
|
||||
}
|
||||
// Set the access token.
|
||||
m_account->m_accessToken = accessToken;
|
||||
|
||||
// Now we load the list of available profiles.
|
||||
// Mojang hasn't yet implemented the profile system,
|
||||
// but we might as well support what's there so we
|
||||
// don't have trouble implementing it later.
|
||||
qDebug() << "Loading profile list.";
|
||||
QJsonArray availableProfiles = responseData.value("availableProfiles").toArray();
|
||||
QList<AccountProfile> loadedProfiles;
|
||||
for (auto iter : availableProfiles)
|
||||
{
|
||||
QJsonObject profile = iter.toObject();
|
||||
// Profiles are easy, we just need their ID and name.
|
||||
QString id = profile.value("id").toString("");
|
||||
QString name = profile.value("name").toString("");
|
||||
bool legacy = profile.value("legacy").toBool(false);
|
||||
|
||||
if (id.isEmpty() || name.isEmpty())
|
||||
{
|
||||
// This should never happen, but we might as well
|
||||
// warn about it if it does so we can debug it easily.
|
||||
// You never know when Mojang might do something truly derpy.
|
||||
qWarning() << "Found entry in available profiles list with missing ID or name "
|
||||
"field. Ignoring it.";
|
||||
}
|
||||
|
||||
// Now, add a new AccountProfile entry to the list.
|
||||
loadedProfiles.append({id, name, legacy});
|
||||
}
|
||||
// Put the list of profiles we loaded into the MojangAccount object.
|
||||
m_account->m_profiles = loadedProfiles;
|
||||
|
||||
// Finally, we set the current profile to the correct value. This is pretty simple.
|
||||
// We do need to make sure that the current profile that the server gave us
|
||||
// is actually in the available profiles list.
|
||||
// If it isn't, we'll just fail horribly (*shouldn't* ever happen, but you never know).
|
||||
qDebug() << "Setting current profile.";
|
||||
QJsonObject currentProfile = responseData.value("selectedProfile").toObject();
|
||||
QString currentProfileId = currentProfile.value("id").toString("");
|
||||
if (currentProfileId.isEmpty())
|
||||
{
|
||||
changeState(STATE_FAILED_HARD, tr("Authentication server didn't specify a currently selected profile. The account exists, but likely isn't premium."));
|
||||
return;
|
||||
}
|
||||
if (!m_account->setCurrentProfile(currentProfileId))
|
||||
{
|
||||
changeState(STATE_FAILED_HARD, tr("Authentication server specified a selected profile that wasn't in the available profiles list."));
|
||||
return;
|
||||
}
|
||||
|
||||
// this is what the vanilla launcher passes to the userProperties launch param
|
||||
if (responseData.contains("user"))
|
||||
{
|
||||
User u;
|
||||
auto obj = responseData.value("user").toObject();
|
||||
u.id = obj.value("id").toString();
|
||||
auto propArray = obj.value("properties").toArray();
|
||||
for (auto prop : propArray)
|
||||
{
|
||||
auto propTuple = prop.toObject();
|
||||
auto name = propTuple.value("name").toString();
|
||||
auto value = propTuple.value("value").toString();
|
||||
u.properties.insert(name, value);
|
||||
}
|
||||
m_account->m_user = u;
|
||||
}
|
||||
|
||||
// We've made it through the minefield of possible errors. Return true to indicate that
|
||||
// we've succeeded.
|
||||
qDebug() << "Finished reading authentication response.";
|
||||
changeState(STATE_SUCCEEDED);
|
||||
}
|
||||
|
||||
QString AuthenticateTask::getEndpoint() const
|
||||
{
|
||||
return "authenticate";
|
||||
}
|
||||
|
||||
QString AuthenticateTask::getStateMessage() const
|
||||
{
|
||||
switch (m_state)
|
||||
{
|
||||
case STATE_SENDING_REQUEST:
|
||||
return tr("Authenticating: Sending request...");
|
||||
case STATE_PROCESSING_RESPONSE:
|
||||
return tr("Authenticating: Processing response...");
|
||||
default:
|
||||
return YggdrasilTask::getStateMessage();
|
||||
}
|
||||
}
|
46
logic/minecraft/auth/flows/AuthenticateTask.h
Normal file
46
logic/minecraft/auth/flows/AuthenticateTask.h
Normal file
@ -0,0 +1,46 @@
|
||||
/* Copyright 2013-2015 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../YggdrasilTask.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
|
||||
/**
|
||||
* The authenticate task takes a MojangAccount with no access token and password and attempts to
|
||||
* authenticate with Mojang's servers.
|
||||
* If successful, it will set the MojangAccount's access token.
|
||||
*/
|
||||
class AuthenticateTask : public YggdrasilTask
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
AuthenticateTask(MojangAccount *account, const QString &password, QObject *parent = 0);
|
||||
|
||||
protected:
|
||||
virtual QJsonObject getRequestContent() const override;
|
||||
|
||||
virtual QString getEndpoint() const override;
|
||||
|
||||
virtual void processResponse(QJsonObject responseData) override;
|
||||
|
||||
virtual QString getStateMessage() const override;
|
||||
|
||||
private:
|
||||
QString m_password;
|
||||
};
|
144
logic/minecraft/auth/flows/RefreshTask.cpp
Normal file
144
logic/minecraft/auth/flows/RefreshTask.cpp
Normal file
@ -0,0 +1,144 @@
|
||||
/* Copyright 2013-2015 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "RefreshTask.h"
|
||||
#include "../MojangAccount.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QVariant>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
RefreshTask::RefreshTask(MojangAccount *account) : YggdrasilTask(account)
|
||||
{
|
||||
}
|
||||
|
||||
QJsonObject RefreshTask::getRequestContent() const
|
||||
{
|
||||
/*
|
||||
* {
|
||||
* "clientToken": "client identifier"
|
||||
* "accessToken": "current access token to be refreshed"
|
||||
* "selectedProfile": // specifying this causes errors
|
||||
* {
|
||||
* "id": "profile ID"
|
||||
* "name": "profile name"
|
||||
* }
|
||||
* "requestUser": true/false // request the user structure
|
||||
* }
|
||||
*/
|
||||
QJsonObject req;
|
||||
req.insert("clientToken", m_account->m_clientToken);
|
||||
req.insert("accessToken", m_account->m_accessToken);
|
||||
/*
|
||||
{
|
||||
auto currentProfile = m_account->currentProfile();
|
||||
QJsonObject profile;
|
||||
profile.insert("id", currentProfile->id());
|
||||
profile.insert("name", currentProfile->name());
|
||||
req.insert("selectedProfile", profile);
|
||||
}
|
||||
*/
|
||||
req.insert("requestUser", true);
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
void RefreshTask::processResponse(QJsonObject responseData)
|
||||
{
|
||||
// Read the response data. We need to get the client token, access token, and the selected
|
||||
// profile.
|
||||
qDebug() << "Processing authentication response.";
|
||||
|
||||
// qDebug() << responseData;
|
||||
// If we already have a client token, make sure the one the server gave us matches our
|
||||
// existing one.
|
||||
QString clientToken = responseData.value("clientToken").toString("");
|
||||
if (clientToken.isEmpty())
|
||||
{
|
||||
// Fail if the server gave us an empty client token
|
||||
changeState(STATE_FAILED_HARD, tr("Authentication server didn't send a client token."));
|
||||
return;
|
||||
}
|
||||
if (!m_account->m_clientToken.isEmpty() && clientToken != m_account->m_clientToken)
|
||||
{
|
||||
changeState(STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Now, we set the access token.
|
||||
qDebug() << "Getting new access token.";
|
||||
QString accessToken = responseData.value("accessToken").toString("");
|
||||
if (accessToken.isEmpty())
|
||||
{
|
||||
// Fail if the server didn't give us an access token.
|
||||
changeState(STATE_FAILED_HARD, tr("Authentication server didn't send an access token."));
|
||||
return;
|
||||
}
|
||||
|
||||
// we validate that the server responded right. (our current profile = returned current
|
||||
// profile)
|
||||
QJsonObject currentProfile = responseData.value("selectedProfile").toObject();
|
||||
QString currentProfileId = currentProfile.value("id").toString("");
|
||||
if (m_account->currentProfile()->id != currentProfileId)
|
||||
{
|
||||
changeState(STATE_FAILED_HARD, tr("Authentication server didn't specify the same prefile as expected."));
|
||||
return;
|
||||
}
|
||||
|
||||
// this is what the vanilla launcher passes to the userProperties launch param
|
||||
if (responseData.contains("user"))
|
||||
{
|
||||
User u;
|
||||
auto obj = responseData.value("user").toObject();
|
||||
u.id = obj.value("id").toString();
|
||||
auto propArray = obj.value("properties").toArray();
|
||||
for (auto prop : propArray)
|
||||
{
|
||||
auto propTuple = prop.toObject();
|
||||
auto name = propTuple.value("name").toString();
|
||||
auto value = propTuple.value("value").toString();
|
||||
u.properties.insert(name, value);
|
||||
}
|
||||
m_account->m_user = u;
|
||||
}
|
||||
|
||||
// We've made it through the minefield of possible errors. Return true to indicate that
|
||||
// we've succeeded.
|
||||
qDebug() << "Finished reading refresh response.";
|
||||
// Reset the access token.
|
||||
m_account->m_accessToken = accessToken;
|
||||
changeState(STATE_SUCCEEDED);
|
||||
}
|
||||
|
||||
QString RefreshTask::getEndpoint() const
|
||||
{
|
||||
return "refresh";
|
||||
}
|
||||
|
||||
QString RefreshTask::getStateMessage() const
|
||||
{
|
||||
switch (m_state)
|
||||
{
|
||||
case STATE_SENDING_REQUEST:
|
||||
return tr("Refreshing login token...");
|
||||
case STATE_PROCESSING_RESPONSE:
|
||||
return tr("Refreshing login token: Processing response...");
|
||||
default:
|
||||
return YggdrasilTask::getStateMessage();
|
||||
}
|
||||
}
|
44
logic/minecraft/auth/flows/RefreshTask.h
Normal file
44
logic/minecraft/auth/flows/RefreshTask.h
Normal file
@ -0,0 +1,44 @@
|
||||
/* Copyright 2013-2015 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../YggdrasilTask.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
|
||||
/**
|
||||
* The authenticate task takes a MojangAccount with a possibly timed-out access token
|
||||
* and attempts to authenticate with Mojang's servers.
|
||||
* If successful, it will set the new access token. The token is considered validated.
|
||||
*/
|
||||
class RefreshTask : public YggdrasilTask
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
RefreshTask(MojangAccount * account);
|
||||
|
||||
protected:
|
||||
virtual QJsonObject getRequestContent() const override;
|
||||
|
||||
virtual QString getEndpoint() const override;
|
||||
|
||||
virtual void processResponse(QJsonObject responseData) override;
|
||||
|
||||
virtual QString getStateMessage() const override;
|
||||
};
|
||||
|
61
logic/minecraft/auth/flows/ValidateTask.cpp
Normal file
61
logic/minecraft/auth/flows/ValidateTask.cpp
Normal file
@ -0,0 +1,61 @@
|
||||
|
||||
/* Copyright 2013-2015 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "ValidateTask.h"
|
||||
#include "../MojangAccount.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QVariant>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
ValidateTask::ValidateTask(MojangAccount * account, QObject *parent)
|
||||
: YggdrasilTask(account, parent)
|
||||
{
|
||||
}
|
||||
|
||||
QJsonObject ValidateTask::getRequestContent() const
|
||||
{
|
||||
QJsonObject req;
|
||||
req.insert("accessToken", m_account->m_accessToken);
|
||||
return req;
|
||||
}
|
||||
|
||||
void ValidateTask::processResponse(QJsonObject responseData)
|
||||
{
|
||||
// Assume that if processError wasn't called, then the request was successful.
|
||||
changeState(YggdrasilTask::STATE_SUCCEEDED);
|
||||
}
|
||||
|
||||
QString ValidateTask::getEndpoint() const
|
||||
{
|
||||
return "validate";
|
||||
}
|
||||
|
||||
QString ValidateTask::getStateMessage() const
|
||||
{
|
||||
switch (m_state)
|
||||
{
|
||||
case YggdrasilTask::STATE_SENDING_REQUEST:
|
||||
return tr("Validating access token: Sending request...");
|
||||
case YggdrasilTask::STATE_PROCESSING_RESPONSE:
|
||||
return tr("Validating access token: Processing response...");
|
||||
default:
|
||||
return YggdrasilTask::getStateMessage();
|
||||
}
|
||||
}
|
47
logic/minecraft/auth/flows/ValidateTask.h
Normal file
47
logic/minecraft/auth/flows/ValidateTask.h
Normal file
@ -0,0 +1,47 @@
|
||||
/* Copyright 2013-2015 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* :FIXME: DEAD CODE, DEAD CODE, DEAD CODE! :FIXME:
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../YggdrasilTask.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QJsonObject>
|
||||
|
||||
/**
|
||||
* The validate task takes a MojangAccount and checks to make sure its access token is valid.
|
||||
*/
|
||||
class ValidateTask : public YggdrasilTask
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
ValidateTask(MojangAccount *account, QObject *parent = 0);
|
||||
|
||||
protected:
|
||||
virtual QJsonObject getRequestContent() const override;
|
||||
|
||||
virtual QString getEndpoint() const override;
|
||||
|
||||
virtual void processResponse(QJsonObject responseData) override;
|
||||
|
||||
virtual QString getStateMessage() const override;
|
||||
|
||||
private:
|
||||
};
|
442
logic/minecraft/forge/ForgeInstaller.cpp
Normal file
442
logic/minecraft/forge/ForgeInstaller.cpp
Normal file
@ -0,0 +1,442 @@
|
||||
/* Copyright 2013-2015 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "ForgeInstaller.h"
|
||||
#include "ForgeVersionList.h"
|
||||
|
||||
#include "minecraft/MinecraftProfile.h"
|
||||
#include "minecraft/GradleSpecifier.h"
|
||||
#include "net/HttpMetaCache.h"
|
||||
#include "tasks/Task.h"
|
||||
#include "minecraft/onesix/OneSixInstance.h"
|
||||
#include "minecraft/VersionFilterData.h"
|
||||
#include "Env.h"
|
||||
#include "Exception.h"
|
||||
#include <FileSystem.h>
|
||||
|
||||
#include <quazip.h>
|
||||
#include <quazipfile.h>
|
||||
#include <QStringList>
|
||||
#include <QRegularExpression>
|
||||
#include <QRegularExpressionMatch>
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
#include <QSaveFile>
|
||||
#include <QCryptographicHash>
|
||||
|
||||
ForgeInstaller::ForgeInstaller() : BaseInstaller()
|
||||
{
|
||||
}
|
||||
|
||||
void ForgeInstaller::prepare(const QString &filename, const QString &universalUrl)
|
||||
{
|
||||
std::shared_ptr<MinecraftProfile> newVersion;
|
||||
m_universal_url = universalUrl;
|
||||
|
||||
QuaZip zip(filename);
|
||||
if (!zip.open(QuaZip::mdUnzip))
|
||||
return;
|
||||
|
||||
QuaZipFile file(&zip);
|
||||
|
||||
// read the install profile
|
||||
if (!zip.setCurrentFile("install_profile.json"))
|
||||
return;
|
||||
|
||||
QJsonParseError jsonError;
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
return;
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(file.readAll(), &jsonError);
|
||||
file.close();
|
||||
if (jsonError.error != QJsonParseError::NoError)
|
||||
return;
|
||||
|
||||
if (!jsonDoc.isObject())
|
||||
return;
|
||||
|
||||
QJsonObject root = jsonDoc.object();
|
||||
|
||||
auto installVal = root.value("install");
|
||||
auto versionInfoVal = root.value("versionInfo");
|
||||
if (!installVal.isObject() || !versionInfoVal.isObject())
|
||||
return;
|
||||
|
||||
// read the forge version info
|
||||
{
|
||||
newVersion = MinecraftProfile::fromJson(versionInfoVal.toObject());
|
||||
if (!newVersion)
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject installObj = installVal.toObject();
|
||||
QString libraryName = installObj.value("path").toString();
|
||||
internalPath = installObj.value("filePath").toString();
|
||||
m_forgeVersionString = installObj.value("version").toString().remove("Forge").trimmed();
|
||||
|
||||
// where do we put the library? decode the mojang path
|
||||
GradleSpecifier lib(libraryName);
|
||||
|
||||
auto cacheentry = ENV.metacache()->resolveEntry("libraries", lib.toPath());
|
||||
finalPath = "libraries/" + lib.toPath();
|
||||
if (!FS::ensureFilePathExists(finalPath))
|
||||
return;
|
||||
|
||||
if (!zip.setCurrentFile(internalPath))
|
||||
return;
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
return;
|
||||
{
|
||||
QByteArray data = file.readAll();
|
||||
// extract file
|
||||
QSaveFile extraction(finalPath);
|
||||
if (!extraction.open(QIODevice::WriteOnly))
|
||||
return;
|
||||
if (extraction.write(data) != data.size())
|
||||
return;
|
||||
if (!extraction.commit())
|
||||
return;
|
||||
QCryptographicHash md5sum(QCryptographicHash::Md5);
|
||||
md5sum.addData(data);
|
||||
|
||||
cacheentry->stale = false;
|
||||
cacheentry->md5sum = md5sum.result().toHex().constData();
|
||||
ENV.metacache()->updateEntry(cacheentry);
|
||||
}
|
||||
file.close();
|
||||
|
||||
m_forge_json = newVersion;
|
||||
m_forge_json->id = installObj.value("minecraft").toString();
|
||||
}
|
||||
|
||||
bool ForgeInstaller::add(OneSixInstance *to)
|
||||
{
|
||||
if (!BaseInstaller::add(to))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject obj;
|
||||
obj.insert("order", 5);
|
||||
|
||||
if (!m_forge_json)
|
||||
return false;
|
||||
int sliding_insert_window = 0;
|
||||
{
|
||||
QJsonArray librariesPlus;
|
||||
// A blacklist
|
||||
QSet<QString> blacklist{"authlib", "realms"};
|
||||
//
|
||||
QList<QString> xzlist{"org.scala-lang", "com.typesafe"};
|
||||
// for each library in the version we are adding (except for the blacklisted)
|
||||
for (auto lib : m_forge_json->libraries)
|
||||
{
|
||||
QString libName = lib->artifactId();
|
||||
QString rawName = lib->rawName();
|
||||
|
||||
// ignore lwjgl libraries.
|
||||
if (g_VersionFilterData.lwjglWhitelist.contains(lib->artifactPrefix()))
|
||||
continue;
|
||||
// ignore other blacklisted (realms, authlib)
|
||||
if (blacklist.contains(libName))
|
||||
continue;
|
||||
|
||||
// WARNING: This could actually break.
|
||||
// if this is the actual forge lib, set an absolute url for the download
|
||||
if (m_forge_version->type == ForgeVersion::Gradle)
|
||||
{
|
||||
if (libName == "forge")
|
||||
{
|
||||
lib->setClassifier("universal");
|
||||
}
|
||||
else if (libName == "minecraftforge")
|
||||
{
|
||||
QString forgeCoord("net.minecraftforge:forge:%1:universal");
|
||||
// using insane form of the MC version...
|
||||
QString longVersion =
|
||||
m_forge_version->mcver + "-" + m_forge_version->jobbuildver;
|
||||
GradleSpecifier spec(forgeCoord.arg(longVersion));
|
||||
lib->setRawName(spec);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (libName.contains("minecraftforge"))
|
||||
{
|
||||
lib->setAbsoluteUrl(m_universal_url);
|
||||
}
|
||||
}
|
||||
|
||||
// WARNING: This could actually break.
|
||||
// mark bad libraries based on the xzlist above
|
||||
for (auto entry : xzlist)
|
||||
{
|
||||
qDebug() << "Testing " << rawName << " : " << entry;
|
||||
if (rawName.startsWith(entry))
|
||||
{
|
||||
lib->setHint("forge-pack-xz");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
QJsonObject libObj = lib->toJson();
|
||||
|
||||
bool found = false;
|
||||
bool equals = false;
|
||||
// find an entry that matches this one
|
||||
for (auto tolib : to->getMinecraftProfile()->vanillaLibraries)
|
||||
{
|
||||
if (tolib->artifactId() != libName)
|
||||
continue;
|
||||
found = true;
|
||||
if (tolib->toJson() == libObj)
|
||||
{
|
||||
equals = true;
|
||||
}
|
||||
// replace lib
|
||||
libObj.insert("insert", QString("replace"));
|
||||
break;
|
||||
}
|
||||
if (equals)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!found)
|
||||
{
|
||||
// add lib
|
||||
libObj.insert("insert", QString("prepend"));
|
||||
if (lib->artifactId() == "minecraftforge" || lib->artifactId() == "forge")
|
||||
{
|
||||
libObj.insert("MMC-depend", QString("hard"));
|
||||
}
|
||||
sliding_insert_window++;
|
||||
}
|
||||
librariesPlus.prepend(libObj);
|
||||
}
|
||||
obj.insert("+libraries", librariesPlus);
|
||||
obj.insert("mainClass", m_forge_json->mainClass);
|
||||
QString args = m_forge_json->minecraftArguments;
|
||||
QStringList tweakers;
|
||||
{
|
||||
QRegularExpression expression("--tweakClass ([a-zA-Z0-9\\.]*)");
|
||||
QRegularExpressionMatch match = expression.match(args);
|
||||
while (match.hasMatch())
|
||||
{
|
||||
tweakers.append(match.captured(1));
|
||||
args.remove(match.capturedStart(), match.capturedLength());
|
||||
match = expression.match(args);
|
||||
}
|
||||
}
|
||||
if (!args.isEmpty() && args != to->getMinecraftProfile()->vanillaMinecraftArguments)
|
||||
{
|
||||
obj.insert("minecraftArguments", args);
|
||||
}
|
||||
if (!tweakers.isEmpty())
|
||||
{
|
||||
obj.insert("+tweakers", QJsonArray::fromStringList(tweakers));
|
||||
}
|
||||
if (!m_forge_json->processArguments.isEmpty() &&
|
||||
m_forge_json->processArguments != to->getMinecraftProfile()->vanillaProcessArguments)
|
||||
{
|
||||
obj.insert("processArguments", m_forge_json->processArguments);
|
||||
}
|
||||
}
|
||||
|
||||
obj.insert("name", QString("Forge"));
|
||||
obj.insert("fileId", id());
|
||||
obj.insert("version", m_forgeVersionString);
|
||||
obj.insert("mcVersion", to->intendedVersionId());
|
||||
|
||||
QFile file(filename(to->instanceRoot()));
|
||||
if (!file.open(QFile::WriteOnly))
|
||||
{
|
||||
qCritical() << "Error opening" << file.fileName()
|
||||
<< "for reading:" << file.errorString();
|
||||
return false;
|
||||
}
|
||||
file.write(QJsonDocument(obj).toJson());
|
||||
file.close();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ForgeInstaller::addLegacy(OneSixInstance *to)
|
||||
{
|
||||
if (!BaseInstaller::add(to))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
auto entry = ENV.metacache()->resolveEntry("minecraftforge", m_forge_version->filename());
|
||||
finalPath = FS::PathCombine(to->jarModsDir(), m_forge_version->filename());
|
||||
if (!FS::ensureFilePathExists(finalPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!QFile::copy(entry->getFullPath(), finalPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
QJsonObject obj;
|
||||
obj.insert("order", 5);
|
||||
{
|
||||
QJsonArray jarmodsPlus;
|
||||
{
|
||||
QJsonObject libObj;
|
||||
libObj.insert("name", m_forge_version->universal_filename);
|
||||
jarmodsPlus.append(libObj);
|
||||
}
|
||||
obj.insert("+jarMods", jarmodsPlus);
|
||||
}
|
||||
|
||||
obj.insert("name", QString("Forge"));
|
||||
obj.insert("fileId", id());
|
||||
obj.insert("version", m_forge_version->jobbuildver);
|
||||
obj.insert("mcVersion", to->intendedVersionId());
|
||||
if (g_VersionFilterData.fmlLibsMapping.contains(m_forge_version->mcver))
|
||||
{
|
||||
QJsonArray traitsPlus;
|
||||
traitsPlus.append(QString("legacyFML"));
|
||||
obj.insert("+traits", traitsPlus);
|
||||
}
|
||||
auto fullversion = to->getMinecraftProfile();
|
||||
fullversion->remove("net.minecraftforge");
|
||||
|
||||
QFile file(filename(to->instanceRoot()));
|
||||
if (!file.open(QFile::WriteOnly))
|
||||
{
|
||||
qCritical() << "Error opening" << file.fileName()
|
||||
<< "for reading:" << file.errorString();
|
||||
return false;
|
||||
}
|
||||
file.write(QJsonDocument(obj).toJson());
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
class ForgeInstallTask : public Task
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
ForgeInstallTask(ForgeInstaller *installer, OneSixInstance *instance,
|
||||
BaseVersionPtr version, QObject *parent = 0)
|
||||
: Task(parent), m_installer(installer), m_instance(instance), m_version(version)
|
||||
{
|
||||
}
|
||||
|
||||
protected:
|
||||
void executeTask() override
|
||||
{
|
||||
setStatus(tr("Installing Forge..."));
|
||||
ForgeVersionPtr forgeVersion = std::dynamic_pointer_cast<ForgeVersion>(m_version);
|
||||
if (!forgeVersion)
|
||||
{
|
||||
emitFailed(tr("Unknown error occured"));
|
||||
return;
|
||||
}
|
||||
prepare(forgeVersion);
|
||||
}
|
||||
void prepare(ForgeVersionPtr forgeVersion)
|
||||
{
|
||||
auto entry = ENV.metacache()->resolveEntry("minecraftforge", forgeVersion->filename());
|
||||
auto installFunction = [this, entry, forgeVersion]()
|
||||
{
|
||||
if (!install(entry, forgeVersion))
|
||||
{
|
||||
qCritical() << "Failure installing Forge";
|
||||
emitFailed(tr("Failure to install Forge"));
|
||||
}
|
||||
else
|
||||
{
|
||||
reload();
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* HACK IF the local non-stale file is too small, mark is as stale
|
||||
*
|
||||
* This fixes some problems with bad files acquired because of unhandled HTTP redirects
|
||||
* in old versions of MultiMC.
|
||||
*/
|
||||
if (!entry->stale)
|
||||
{
|
||||
QFileInfo localFile(entry->getFullPath());
|
||||
if (localFile.size() <= 0x4000)
|
||||
{
|
||||
entry->stale = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (entry->stale)
|
||||
{
|
||||
NetJob *fjob = new NetJob("Forge download");
|
||||
fjob->addNetAction(CacheDownload::make(forgeVersion->url(), entry));
|
||||
connect(fjob, &NetJob::progress, this, &Task::setProgress);
|
||||
connect(fjob, &NetJob::status, this, &Task::setStatus);
|
||||
connect(fjob, &NetJob::failed, [this](QString reason)
|
||||
{ emitFailed(tr("Failure to download Forge:\n%1").arg(reason)); });
|
||||
connect(fjob, &NetJob::succeeded, installFunction);
|
||||
fjob->start();
|
||||
}
|
||||
else
|
||||
{
|
||||
installFunction();
|
||||
}
|
||||
}
|
||||
bool install(const std::shared_ptr<MetaEntry> &entry, const ForgeVersionPtr &forgeVersion)
|
||||
{
|
||||
if (forgeVersion->usesInstaller())
|
||||
{
|
||||
QString forgePath = entry->getFullPath();
|
||||
m_installer->prepare(forgePath, forgeVersion->universal_url);
|
||||
return m_installer->add(m_instance);
|
||||
}
|
||||
else
|
||||
return m_installer->addLegacy(m_instance);
|
||||
}
|
||||
void reload()
|
||||
{
|
||||
try
|
||||
{
|
||||
m_instance->reloadProfile();
|
||||
emitSucceeded();
|
||||
}
|
||||
catch (Exception &e)
|
||||
{
|
||||
emitFailed(e.cause());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
emitFailed(tr("Failed to load the version description file for reasons unknown."));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
ForgeInstaller *m_installer;
|
||||
OneSixInstance *m_instance;
|
||||
BaseVersionPtr m_version;
|
||||
};
|
||||
|
||||
Task *ForgeInstaller::createInstallTask(OneSixInstance *instance,
|
||||
BaseVersionPtr version, QObject *parent)
|
||||
{
|
||||
if (!version)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
m_forge_version = std::dynamic_pointer_cast<ForgeVersion>(version);
|
||||
return new ForgeInstallTask(this, instance, version, parent);
|
||||
}
|
||||
|
||||
#include "ForgeInstaller.moc"
|
52
logic/minecraft/forge/ForgeInstaller.h
Normal file
52
logic/minecraft/forge/ForgeInstaller.h
Normal file
@ -0,0 +1,52 @@
|
||||
/* Copyright 2013-2015 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BaseInstaller.h"
|
||||
|
||||
#include <QString>
|
||||
#include <memory>
|
||||
|
||||
#include "multimc_logic_export.h"
|
||||
|
||||
class MinecraftProfile;
|
||||
class ForgeInstallTask;
|
||||
struct ForgeVersion;
|
||||
|
||||
class MULTIMC_LOGIC_EXPORT ForgeInstaller : public BaseInstaller
|
||||
{
|
||||
friend class ForgeInstallTask;
|
||||
public:
|
||||
ForgeInstaller();
|
||||
virtual ~ForgeInstaller(){}
|
||||
virtual Task *createInstallTask(OneSixInstance *instance, BaseVersionPtr version, QObject *parent) override;
|
||||
virtual QString id() const override { return "net.minecraftforge"; }
|
||||
|
||||
protected:
|
||||
void prepare(const QString &filename, const QString &universalUrl);
|
||||
bool add(OneSixInstance *to) override;
|
||||
bool addLegacy(OneSixInstance *to);
|
||||
|
||||
private:
|
||||
// the parsed version json, read from the installer
|
||||
std::shared_ptr<MinecraftProfile> m_forge_json;
|
||||
// the actual forge version
|
||||
std::shared_ptr<ForgeVersion> m_forge_version;
|
||||
QString internalPath;
|
||||
QString finalPath;
|
||||
QString m_forgeVersionString;
|
||||
QString m_universal_url;
|
||||
};
|
10
logic/minecraft/forge/ForgeMirror.h
Normal file
10
logic/minecraft/forge/ForgeMirror.h
Normal file
@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
#include <QString>
|
||||
|
||||
struct ForgeMirror
|
||||
{
|
||||
QString name;
|
||||
QString logo_url;
|
||||
QString website_url;
|
||||
QString mirror_url;
|
||||
};
|
118
logic/minecraft/forge/ForgeMirrors.cpp
Normal file
118
logic/minecraft/forge/ForgeMirrors.cpp
Normal file
@ -0,0 +1,118 @@
|
||||
#include "Env.h"
|
||||
#include "ForgeMirrors.h"
|
||||
#include <QDebug>
|
||||
#include <algorithm>
|
||||
#include <random>
|
||||
|
||||
ForgeMirrors::ForgeMirrors(QList<ForgeXzDownloadPtr> &libs, NetJobPtr parent_job,
|
||||
QString mirrorlist)
|
||||
{
|
||||
m_libs = libs;
|
||||
m_parent_job = parent_job;
|
||||
m_url = QUrl(mirrorlist);
|
||||
m_status = Job_NotStarted;
|
||||
}
|
||||
|
||||
void ForgeMirrors::start()
|
||||
{
|
||||
qDebug() << "Downloading " << m_url.toString();
|
||||
QNetworkRequest request(m_url);
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)");
|
||||
auto worker = ENV.qnam();
|
||||
QNetworkReply *rep = worker->get(request);
|
||||
|
||||
m_reply.reset(rep);
|
||||
connect(rep, SIGNAL(downloadProgress(qint64, qint64)),
|
||||
SLOT(downloadProgress(qint64, qint64)));
|
||||
connect(rep, SIGNAL(finished()), SLOT(downloadFinished()));
|
||||
connect(rep, SIGNAL(error(QNetworkReply::NetworkError)),
|
||||
SLOT(downloadError(QNetworkReply::NetworkError)));
|
||||
connect(rep, SIGNAL(readyRead()), SLOT(downloadReadyRead()));
|
||||
}
|
||||
|
||||
void ForgeMirrors::downloadError(QNetworkReply::NetworkError error)
|
||||
{
|
||||
// error happened during download.
|
||||
qCritical() << "Error getting URL:" << m_url.toString().toLocal8Bit()
|
||||
<< "Network error: " << error;
|
||||
m_status = Job_Failed;
|
||||
}
|
||||
|
||||
void ForgeMirrors::downloadFinished()
|
||||
{
|
||||
// if the download succeeded
|
||||
if (m_status != Job_Failed)
|
||||
{
|
||||
// nothing went wrong... ?
|
||||
parseMirrorList();
|
||||
return;
|
||||
}
|
||||
// else the download failed, we use a fixed list
|
||||
else
|
||||
{
|
||||
m_status = Job_Finished;
|
||||
m_reply.reset();
|
||||
deferToFixedList();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void ForgeMirrors::deferToFixedList()
|
||||
{
|
||||
m_mirrors.clear();
|
||||
m_mirrors.append(
|
||||
{"Minecraft Forge", "http://files.minecraftforge.net/forge_logo.png",
|
||||
"http://files.minecraftforge.net/", "http://files.minecraftforge.net/maven/"});
|
||||
m_mirrors.append({"Creeper Host",
|
||||
"http://files.minecraftforge.net/forge_logo.png",
|
||||
"https://www.creeperhost.net/link.php?id=1",
|
||||
"http://new.creeperrepo.net/forge/maven/"});
|
||||
injectDownloads();
|
||||
emit succeeded(m_index_within_job);
|
||||
}
|
||||
|
||||
void ForgeMirrors::parseMirrorList()
|
||||
{
|
||||
m_status = Job_Finished;
|
||||
auto data = m_reply->readAll();
|
||||
m_reply.reset();
|
||||
auto dataLines = data.split('\n');
|
||||
for(auto line: dataLines)
|
||||
{
|
||||
auto elements = line.split('!');
|
||||
if (elements.size() == 4)
|
||||
{
|
||||
m_mirrors.append({elements[0],elements[1],elements[2],elements[3]});
|
||||
}
|
||||
}
|
||||
if(!m_mirrors.size())
|
||||
deferToFixedList();
|
||||
injectDownloads();
|
||||
emit succeeded(m_index_within_job);
|
||||
}
|
||||
|
||||
void ForgeMirrors::injectDownloads()
|
||||
{
|
||||
// shuffle the mirrors randomly
|
||||
std::random_device rd;
|
||||
std::mt19937 rng(rd());
|
||||
std::shuffle(m_mirrors.begin(), m_mirrors.end(), rng);
|
||||
|
||||
// tell parent to download the libs
|
||||
for(auto lib: m_libs)
|
||||
{
|
||||
lib->setMirrors(m_mirrors);
|
||||
m_parent_job->addNetAction(lib);
|
||||
}
|
||||
}
|
||||
|
||||
void ForgeMirrors::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
|
||||
{
|
||||
m_total_progress = bytesTotal;
|
||||
m_progress = bytesReceived;
|
||||
emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal);
|
||||
}
|
||||
|
||||
void ForgeMirrors::downloadReadyRead()
|
||||
{
|
||||
}
|
61
logic/minecraft/forge/ForgeMirrors.h
Normal file
61
logic/minecraft/forge/ForgeMirrors.h
Normal file
@ -0,0 +1,61 @@
|
||||
/* Copyright 2013-2015 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ForgeXzDownload.h"
|
||||
|
||||
#include "net/NetAction.h"
|
||||
#include "net/HttpMetaCache.h"
|
||||
#include "net/NetJob.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QTemporaryFile>
|
||||
|
||||
typedef std::shared_ptr<class ForgeMirrors> ForgeMirrorsPtr;
|
||||
|
||||
class ForgeMirrors : public NetAction
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
QList<ForgeXzDownloadPtr> m_libs;
|
||||
NetJobPtr m_parent_job;
|
||||
QList<ForgeMirror> m_mirrors;
|
||||
|
||||
public:
|
||||
explicit ForgeMirrors(QList<ForgeXzDownloadPtr> &libs, NetJobPtr parent_job,
|
||||
QString mirrorlist);
|
||||
static ForgeMirrorsPtr make(QList<ForgeXzDownloadPtr> &libs, NetJobPtr parent_job,
|
||||
QString mirrorlist)
|
||||
{
|
||||
return ForgeMirrorsPtr(new ForgeMirrors(libs, parent_job, mirrorlist));
|
||||
}
|
||||
virtual ~ForgeMirrors(){};
|
||||
protected
|
||||
slots:
|
||||
virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
|
||||
virtual void downloadError(QNetworkReply::NetworkError error);
|
||||
virtual void downloadFinished();
|
||||
virtual void downloadReadyRead();
|
||||
|
||||
private:
|
||||
void parseMirrorList();
|
||||
void deferToFixedList();
|
||||
void injectDownloads();
|
||||
|
||||
public
|
||||
slots:
|
||||
virtual void start();
|
||||
};
|
55
logic/minecraft/forge/ForgeVersion.cpp
Normal file
55
logic/minecraft/forge/ForgeVersion.cpp
Normal file
@ -0,0 +1,55 @@
|
||||
#include "ForgeVersion.h"
|
||||
#include "minecraft/VersionFilterData.h"
|
||||
#include <QObject>
|
||||
|
||||
QString ForgeVersion::name()
|
||||
{
|
||||
return "Forge " + jobbuildver;
|
||||
}
|
||||
|
||||
QString ForgeVersion::descriptor()
|
||||
{
|
||||
return universal_filename;
|
||||
}
|
||||
|
||||
QString ForgeVersion::typeString() const
|
||||
{
|
||||
if (is_recommended)
|
||||
return QObject::tr("Recommended");
|
||||
return QString();
|
||||
}
|
||||
|
||||
bool ForgeVersion::operator<(BaseVersion &a)
|
||||
{
|
||||
ForgeVersion *pa = dynamic_cast<ForgeVersion *>(&a);
|
||||
if (!pa)
|
||||
return true;
|
||||
return m_buildnr < pa->m_buildnr;
|
||||
}
|
||||
|
||||
bool ForgeVersion::operator>(BaseVersion &a)
|
||||
{
|
||||
ForgeVersion *pa = dynamic_cast<ForgeVersion *>(&a);
|
||||
if (!pa)
|
||||
return false;
|
||||
return m_buildnr > pa->m_buildnr;
|
||||
}
|
||||
|
||||
bool ForgeVersion::usesInstaller()
|
||||
{
|
||||
if(installer_url.isEmpty())
|
||||
return false;
|
||||
if(g_VersionFilterData.forgeInstallerBlacklist.contains(mcver))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
QString ForgeVersion::filename()
|
||||
{
|
||||
return usesInstaller() ? installer_filename : universal_filename;
|
||||
}
|
||||
|
||||
QString ForgeVersion::url()
|
||||
{
|
||||
return usesInstaller() ? installer_url : universal_url;
|
||||
}
|
42
logic/minecraft/forge/ForgeVersion.h
Normal file
42
logic/minecraft/forge/ForgeVersion.h
Normal file
@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
#include <QString>
|
||||
#include <memory>
|
||||
#include "BaseVersion.h"
|
||||
|
||||
struct ForgeVersion;
|
||||
typedef std::shared_ptr<ForgeVersion> ForgeVersionPtr;
|
||||
|
||||
struct ForgeVersion : public BaseVersion
|
||||
{
|
||||
virtual QString descriptor() override;
|
||||
virtual QString name() override;
|
||||
virtual QString typeString() const override;
|
||||
virtual bool operator<(BaseVersion &a) override;
|
||||
virtual bool operator>(BaseVersion &a) override;
|
||||
|
||||
QString filename();
|
||||
QString url();
|
||||
|
||||
enum
|
||||
{
|
||||
Invalid,
|
||||
Legacy,
|
||||
Gradle
|
||||
} type = Invalid;
|
||||
|
||||
bool usesInstaller();
|
||||
|
||||
int m_buildnr = 0;
|
||||
QString branch;
|
||||
QString universal_url;
|
||||
QString changelog_url;
|
||||
QString installer_url;
|
||||
QString jobbuildver;
|
||||
QString mcver;
|
||||
QString mcver_sane;
|
||||
QString universal_filename;
|
||||
QString installer_filename;
|
||||
bool is_recommended = false;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(ForgeVersionPtr)
|
450
logic/minecraft/forge/ForgeVersionList.cpp
Normal file
450
logic/minecraft/forge/ForgeVersionList.cpp
Normal file
@ -0,0 +1,450 @@
|
||||
/* Copyright 2013-2015 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "ForgeVersionList.h"
|
||||
#include "ForgeVersion.h"
|
||||
|
||||
#include "net/NetJob.h"
|
||||
#include "net/URLConstants.h"
|
||||
#include "Env.h"
|
||||
|
||||
#include <QtNetwork>
|
||||
#include <QtXml>
|
||||
#include <QRegExp>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
ForgeVersionList::ForgeVersionList(QObject *parent) : BaseVersionList(parent)
|
||||
{
|
||||
}
|
||||
|
||||
Task *ForgeVersionList::getLoadTask()
|
||||
{
|
||||
return new ForgeListLoadTask(this);
|
||||
}
|
||||
|
||||
bool ForgeVersionList::isLoaded()
|
||||
{
|
||||
return m_loaded;
|
||||
}
|
||||
|
||||
const BaseVersionPtr ForgeVersionList::at(int i) const
|
||||
{
|
||||
return m_vlist.at(i);
|
||||
}
|
||||
|
||||
int ForgeVersionList::count() const
|
||||
{
|
||||
return m_vlist.count();
|
||||
}
|
||||
|
||||
int ForgeVersionList::columnCount(const QModelIndex &parent) const
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
QVariant ForgeVersionList::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return QVariant();
|
||||
|
||||
if (index.row() > count())
|
||||
return QVariant();
|
||||
|
||||
auto version = std::dynamic_pointer_cast<ForgeVersion>(m_vlist[index.row()]);
|
||||
switch (role)
|
||||
{
|
||||
case VersionPointerRole:
|
||||
return qVariantFromValue(m_vlist[index.row()]);
|
||||
|
||||
case VersionRole:
|
||||
return version->name();
|
||||
|
||||
case VersionIdRole:
|
||||
return version->descriptor();
|
||||
|
||||
case ParentGameVersionRole:
|
||||
return version->mcver_sane;
|
||||
|
||||
case RecommendedRole:
|
||||
return version->is_recommended;
|
||||
|
||||
case BranchRole:
|
||||
return version->branch;
|
||||
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
QList<BaseVersionList::ModelRoles> ForgeVersionList::providesRoles()
|
||||
{
|
||||
return {VersionPointerRole, VersionRole, VersionIdRole, ParentGameVersionRole, RecommendedRole, BranchRole};
|
||||
}
|
||||
|
||||
BaseVersionPtr ForgeVersionList::getLatestStable() const
|
||||
{
|
||||
return BaseVersionPtr();
|
||||
}
|
||||
|
||||
void ForgeVersionList::updateListData(QList<BaseVersionPtr> versions)
|
||||
{
|
||||
beginResetModel();
|
||||
m_vlist = versions;
|
||||
m_loaded = true;
|
||||
endResetModel();
|
||||
// NOW SORT!!
|
||||
// sort();
|
||||
}
|
||||
|
||||
void ForgeVersionList::sortVersions()
|
||||
{
|
||||
// NO-OP for now
|
||||
}
|
||||
|
||||
ForgeListLoadTask::ForgeListLoadTask(ForgeVersionList *vlist) : Task()
|
||||
{
|
||||
m_list = vlist;
|
||||
}
|
||||
|
||||
void ForgeListLoadTask::executeTask()
|
||||
{
|
||||
setStatus(tr("Fetching Forge version lists..."));
|
||||
auto job = new NetJob("Version index");
|
||||
// we do not care if the version is stale or not.
|
||||
auto forgeListEntry = ENV.metacache()->resolveEntry("minecraftforge", "list.json");
|
||||
auto gradleForgeListEntry = ENV.metacache()->resolveEntry("minecraftforge", "json");
|
||||
|
||||
// verify by poking the server.
|
||||
forgeListEntry->stale = true;
|
||||
gradleForgeListEntry->stale = true;
|
||||
|
||||
job->addNetAction(listDownload = CacheDownload::make(QUrl(URLConstants::FORGE_LEGACY_URL),
|
||||
forgeListEntry));
|
||||
job->addNetAction(gradleListDownload = CacheDownload::make(
|
||||
QUrl(URLConstants::FORGE_GRADLE_URL), gradleForgeListEntry));
|
||||
|
||||
connect(listDownload.get(), SIGNAL(failed(int)), SLOT(listFailed()));
|
||||
connect(gradleListDownload.get(), SIGNAL(failed(int)), SLOT(gradleListFailed()));
|
||||
|
||||
listJob.reset(job);
|
||||
connect(listJob.get(), SIGNAL(succeeded()), SLOT(listDownloaded()));
|
||||
connect(listJob.get(), SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64)));
|
||||
listJob->start();
|
||||
}
|
||||
|
||||
bool ForgeListLoadTask::abort()
|
||||
{
|
||||
return listJob->abort();
|
||||
}
|
||||
|
||||
bool ForgeListLoadTask::parseForgeList(QList<BaseVersionPtr> &out)
|
||||
{
|
||||
QByteArray data;
|
||||
{
|
||||
auto dlJob = listDownload;
|
||||
auto filename = std::dynamic_pointer_cast<CacheDownload>(dlJob)->getTargetFilepath();
|
||||
QFile listFile(filename);
|
||||
if (!listFile.open(QIODevice::ReadOnly))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
data = listFile.readAll();
|
||||
dlJob.reset();
|
||||
}
|
||||
|
||||
QJsonParseError jsonError;
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
|
||||
|
||||
if (jsonError.error != QJsonParseError::NoError)
|
||||
{
|
||||
emitFailed("Error parsing version list JSON:" + jsonError.errorString());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!jsonDoc.isObject())
|
||||
{
|
||||
emitFailed("Error parsing version list JSON: JSON root is not an object");
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject root = jsonDoc.object();
|
||||
|
||||
// Now, get the array of versions.
|
||||
if (!root.value("builds").isArray())
|
||||
{
|
||||
emitFailed(
|
||||
"Error parsing version list JSON: version list object is missing 'builds' array");
|
||||
return false;
|
||||
}
|
||||
QJsonArray builds = root.value("builds").toArray();
|
||||
|
||||
for (int i = 0; i < builds.count(); i++)
|
||||
{
|
||||
// Load the version info.
|
||||
if (!builds[i].isObject())
|
||||
{
|
||||
// FIXME: log this somewhere
|
||||
continue;
|
||||
}
|
||||
QJsonObject obj = builds[i].toObject();
|
||||
int build_nr = obj.value("build").toDouble(0);
|
||||
if (!build_nr)
|
||||
continue;
|
||||
QJsonArray files = obj.value("files").toArray();
|
||||
QString url, jobbuildver, mcver, buildtype, universal_filename;
|
||||
QString changelog_url, installer_url;
|
||||
QString installer_filename;
|
||||
bool valid = false;
|
||||
for (int j = 0; j < files.count(); j++)
|
||||
{
|
||||
if (!files[j].isObject())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
QJsonObject file = files[j].toObject();
|
||||
buildtype = file.value("buildtype").toString();
|
||||
if ((buildtype == "client" || buildtype == "universal") && !valid)
|
||||
{
|
||||
mcver = file.value("mcver").toString();
|
||||
url = file.value("url").toString();
|
||||
jobbuildver = file.value("jobbuildver").toString();
|
||||
int lastSlash = url.lastIndexOf('/');
|
||||
universal_filename = url.mid(lastSlash + 1);
|
||||
valid = true;
|
||||
}
|
||||
else if (buildtype == "changelog")
|
||||
{
|
||||
QString ext = file.value("ext").toString();
|
||||
if (ext.isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
changelog_url = file.value("url").toString();
|
||||
}
|
||||
else if (buildtype == "installer")
|
||||
{
|
||||
installer_url = file.value("url").toString();
|
||||
int lastSlash = installer_url.lastIndexOf('/');
|
||||
installer_filename = installer_url.mid(lastSlash + 1);
|
||||
}
|
||||
}
|
||||
if (valid)
|
||||
{
|
||||
// Now, we construct the version object and add it to the list.
|
||||
std::shared_ptr<ForgeVersion> fVersion(new ForgeVersion());
|
||||
fVersion->universal_url = url;
|
||||
fVersion->changelog_url = changelog_url;
|
||||
fVersion->installer_url = installer_url;
|
||||
fVersion->jobbuildver = jobbuildver;
|
||||
fVersion->mcver = fVersion->mcver_sane = mcver;
|
||||
fVersion->installer_filename = installer_filename;
|
||||
fVersion->universal_filename = universal_filename;
|
||||
fVersion->m_buildnr = build_nr;
|
||||
fVersion->type = ForgeVersion::Legacy;
|
||||
out.append(fVersion);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ForgeListLoadTask::parseForgeGradleList(QList<BaseVersionPtr> &out)
|
||||
{
|
||||
QMap<int, std::shared_ptr<ForgeVersion>> lookup;
|
||||
QByteArray data;
|
||||
{
|
||||
auto dlJob = gradleListDownload;
|
||||
auto filename = std::dynamic_pointer_cast<CacheDownload>(dlJob)->getTargetFilepath();
|
||||
QFile listFile(filename);
|
||||
if (!listFile.open(QIODevice::ReadOnly))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
data = listFile.readAll();
|
||||
dlJob.reset();
|
||||
}
|
||||
|
||||
QJsonParseError jsonError;
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
|
||||
|
||||
if (jsonError.error != QJsonParseError::NoError)
|
||||
{
|
||||
emitFailed("Error parsing gradle version list JSON:" + jsonError.errorString());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!jsonDoc.isObject())
|
||||
{
|
||||
emitFailed("Error parsing gradle version list JSON: JSON root is not an object");
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject root = jsonDoc.object();
|
||||
|
||||
// we probably could hard code these, but it might still be worth doing it this way
|
||||
const QString webpath = root.value("webpath").toString();
|
||||
const QString artifact = root.value("artifact").toString();
|
||||
|
||||
QJsonObject numbers = root.value("number").toObject();
|
||||
for (auto it = numbers.begin(); it != numbers.end(); ++it)
|
||||
{
|
||||
QJsonObject number = it.value().toObject();
|
||||
std::shared_ptr<ForgeVersion> fVersion(new ForgeVersion());
|
||||
fVersion->m_buildnr = number.value("build").toDouble();
|
||||
if(fVersion->m_buildnr >= 953 && fVersion->m_buildnr <= 965)
|
||||
{
|
||||
qDebug() << fVersion->m_buildnr;
|
||||
}
|
||||
fVersion->jobbuildver = number.value("version").toString();
|
||||
fVersion->branch = number.value("branch").toString("");
|
||||
fVersion->mcver = number.value("mcversion").toString();
|
||||
fVersion->universal_filename = "";
|
||||
fVersion->installer_filename = "";
|
||||
// HACK: here, we fix the minecraft version used by forge.
|
||||
// HACK: this will inevitably break (later)
|
||||
// FIXME: replace with a dictionary
|
||||
fVersion->mcver_sane = fVersion->mcver;
|
||||
fVersion->mcver_sane.replace("_pre", "-pre");
|
||||
|
||||
QString universal_filename, installer_filename;
|
||||
QJsonArray files = number.value("files").toArray();
|
||||
for (auto fIt = files.begin(); fIt != files.end(); ++fIt)
|
||||
{
|
||||
// TODO with gradle we also get checksums, use them
|
||||
QJsonArray file = (*fIt).toArray();
|
||||
if (file.size() < 3)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
QString extension = file.at(0).toString();
|
||||
QString part = file.at(1).toString();
|
||||
QString checksum = file.at(2).toString();
|
||||
|
||||
// insane form of mcver is used here
|
||||
QString longVersion = fVersion->mcver + "-" + fVersion->jobbuildver;
|
||||
if (!fVersion->branch.isEmpty())
|
||||
{
|
||||
longVersion = longVersion + "-" + fVersion->branch;
|
||||
}
|
||||
QString filename = artifact + "-" + longVersion + "-" + part + "." + extension;
|
||||
|
||||
QString url = QString("%1/%2/%3")
|
||||
.arg(webpath)
|
||||
.arg(longVersion)
|
||||
.arg(filename);
|
||||
|
||||
if (part == "installer")
|
||||
{
|
||||
fVersion->installer_url = url;
|
||||
installer_filename = filename;
|
||||
}
|
||||
else if (part == "universal")
|
||||
{
|
||||
fVersion->universal_url = url;
|
||||
universal_filename = filename;
|
||||
}
|
||||
else if (part == "changelog")
|
||||
{
|
||||
fVersion->changelog_url = url;
|
||||
}
|
||||
}
|
||||
if (fVersion->installer_url.isEmpty() && fVersion->universal_url.isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
fVersion->universal_filename = universal_filename;
|
||||
fVersion->installer_filename = installer_filename;
|
||||
fVersion->type = ForgeVersion::Gradle;
|
||||
out.append(fVersion);
|
||||
lookup[fVersion->m_buildnr] = fVersion;
|
||||
}
|
||||
QJsonObject promos = root.value("promos").toObject();
|
||||
for (auto it = promos.begin(); it != promos.end(); ++it)
|
||||
{
|
||||
QString key = it.key();
|
||||
int build = it.value().toInt();
|
||||
QRegularExpression regexp("^(?<mcversion>[0-9]+(.[0-9]+)*)-(?<label>[a-z]+)$");
|
||||
auto match = regexp.match(key);
|
||||
if(!match.hasMatch())
|
||||
{
|
||||
qDebug() << key << "doesn't match." << "build" << build;
|
||||
continue;
|
||||
}
|
||||
|
||||
QString label = match.captured("label");
|
||||
if(label != "recommended")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
QString mcversion = match.captured("mcversion");
|
||||
qDebug() << "Forge build" << build << "is the" << label << "for Minecraft" << mcversion << QString("<%1>").arg(key);
|
||||
lookup[build]->is_recommended = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ForgeListLoadTask::listDownloaded()
|
||||
{
|
||||
QList<BaseVersionPtr> list;
|
||||
bool ret = true;
|
||||
if (!parseForgeList(list))
|
||||
{
|
||||
ret = false;
|
||||
}
|
||||
if (!parseForgeGradleList(list))
|
||||
{
|
||||
ret = false;
|
||||
}
|
||||
|
||||
if (!ret)
|
||||
{
|
||||
return;
|
||||
}
|
||||
std::sort(list.begin(), list.end(), [](const BaseVersionPtr & l, const BaseVersionPtr & r)
|
||||
{ return (*l > *r); });
|
||||
|
||||
m_list->updateListData(list);
|
||||
|
||||
emitSucceeded();
|
||||
return;
|
||||
}
|
||||
|
||||
void ForgeListLoadTask::listFailed()
|
||||
{
|
||||
auto &reply = listDownload->m_reply;
|
||||
if (reply)
|
||||
{
|
||||
qCritical() << "Getting forge version list failed: " << reply->errorString();
|
||||
}
|
||||
else
|
||||
{
|
||||
qCritical() << "Getting forge version list failed for reasons unknown.";
|
||||
}
|
||||
}
|
||||
|
||||
void ForgeListLoadTask::gradleListFailed()
|
||||
{
|
||||
auto &reply = gradleListDownload->m_reply;
|
||||
if (reply)
|
||||
{
|
||||
qCritical() << "Getting forge version list failed: " << reply->errorString();
|
||||
}
|
||||
else
|
||||
{
|
||||
qCritical() << "Getting forge version list failed for reasons unknown.";
|
||||
}
|
||||
}
|
90
logic/minecraft/forge/ForgeVersionList.h
Normal file
90
logic/minecraft/forge/ForgeVersionList.h
Normal file
@ -0,0 +1,90 @@
|
||||
/* Copyright 2013-2015 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ForgeVersion.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QAbstractListModel>
|
||||
#include <QUrl>
|
||||
#include <QNetworkReply>
|
||||
|
||||
#include "BaseVersionList.h"
|
||||
#include "tasks/Task.h"
|
||||
#include "net/NetJob.h"
|
||||
|
||||
#include "multimc_logic_export.h"
|
||||
|
||||
class MULTIMC_LOGIC_EXPORT ForgeVersionList : public BaseVersionList
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
friend class ForgeListLoadTask;
|
||||
|
||||
explicit ForgeVersionList(QObject *parent = 0);
|
||||
|
||||
virtual Task *getLoadTask() override;
|
||||
virtual bool isLoaded() override;
|
||||
virtual const BaseVersionPtr at(int i) const override;
|
||||
virtual int count() const override;
|
||||
virtual void sortVersions() override;
|
||||
|
||||
virtual BaseVersionPtr getLatestStable() const override;
|
||||
|
||||
ForgeVersionPtr findVersionByVersionNr(QString version);
|
||||
|
||||
virtual QVariant data(const QModelIndex &index, int role) const override;
|
||||
virtual QList<ModelRoles> providesRoles() override;
|
||||
|
||||
virtual int columnCount(const QModelIndex &parent) const override;
|
||||
|
||||
protected:
|
||||
QList<BaseVersionPtr> m_vlist;
|
||||
|
||||
bool m_loaded = false;
|
||||
|
||||
protected
|
||||
slots:
|
||||
virtual void updateListData(QList<BaseVersionPtr> versions) override;
|
||||
};
|
||||
|
||||
class ForgeListLoadTask : public Task
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ForgeListLoadTask(ForgeVersionList *vlist);
|
||||
|
||||
virtual void executeTask();
|
||||
virtual bool abort();
|
||||
|
||||
protected
|
||||
slots:
|
||||
void listDownloaded();
|
||||
void listFailed();
|
||||
void gradleListFailed();
|
||||
|
||||
protected:
|
||||
NetJobPtr listJob;
|
||||
ForgeVersionList *m_list;
|
||||
|
||||
CacheDownloadPtr listDownload;
|
||||
CacheDownloadPtr gradleListDownload;
|
||||
|
||||
private:
|
||||
bool parseForgeList(QList<BaseVersionPtr> &out);
|
||||
bool parseForgeGradleList(QList<BaseVersionPtr> &out);
|
||||
};
|
389
logic/minecraft/forge/ForgeXzDownload.cpp
Normal file
389
logic/minecraft/forge/ForgeXzDownload.cpp
Normal file
@ -0,0 +1,389 @@
|
||||
/* Copyright 2013-2015 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "Env.h"
|
||||
#include "ForgeXzDownload.h"
|
||||
#include <FileSystem.h>
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QFileInfo>
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QDebug>
|
||||
|
||||
ForgeXzDownload::ForgeXzDownload(QString relative_path, MetaEntryPtr entry) : NetAction()
|
||||
{
|
||||
m_entry = entry;
|
||||
m_target_path = entry->getFullPath();
|
||||
m_pack200_xz_file.setFileTemplate("./dl_temp.XXXXXX");
|
||||
m_status = Job_NotStarted;
|
||||
m_url_path = relative_path;
|
||||
}
|
||||
|
||||
void ForgeXzDownload::setMirrors(QList<ForgeMirror> &mirrors)
|
||||
{
|
||||
m_mirror_index = 0;
|
||||
m_mirrors = mirrors;
|
||||
updateUrl();
|
||||
}
|
||||
|
||||
void ForgeXzDownload::start()
|
||||
{
|
||||
m_status = Job_InProgress;
|
||||
if (!m_entry->stale)
|
||||
{
|
||||
m_status = Job_Finished;
|
||||
emit succeeded(m_index_within_job);
|
||||
return;
|
||||
}
|
||||
// can we actually create the real, final file?
|
||||
if (!FS::ensureFilePathExists(m_target_path))
|
||||
{
|
||||
m_status = Job_Failed;
|
||||
emit failed(m_index_within_job);
|
||||
return;
|
||||
}
|
||||
if (m_mirrors.empty())
|
||||
{
|
||||
m_status = Job_Failed;
|
||||
emit failed(m_index_within_job);
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "Downloading " << m_url.toString();
|
||||
QNetworkRequest request(m_url);
|
||||
request.setRawHeader(QString("If-None-Match").toLatin1(), m_entry->etag.toLatin1());
|
||||
request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Cached)");
|
||||
|
||||
auto worker = ENV.qnam();
|
||||
QNetworkReply *rep = worker->get(request);
|
||||
|
||||
m_reply.reset(rep);
|
||||
connect(rep, SIGNAL(downloadProgress(qint64, qint64)),
|
||||
SLOT(downloadProgress(qint64, qint64)));
|
||||
connect(rep, SIGNAL(finished()), SLOT(downloadFinished()));
|
||||
connect(rep, SIGNAL(error(QNetworkReply::NetworkError)),
|
||||
SLOT(downloadError(QNetworkReply::NetworkError)));
|
||||
connect(rep, SIGNAL(readyRead()), SLOT(downloadReadyRead()));
|
||||
}
|
||||
|
||||
void ForgeXzDownload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
|
||||
{
|
||||
m_total_progress = bytesTotal;
|
||||
m_progress = bytesReceived;
|
||||
emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal);
|
||||
}
|
||||
|
||||
void ForgeXzDownload::downloadError(QNetworkReply::NetworkError error)
|
||||
{
|
||||
// error happened during download.
|
||||
// TODO: log the reason why
|
||||
m_status = Job_Failed;
|
||||
}
|
||||
|
||||
void ForgeXzDownload::failAndTryNextMirror()
|
||||
{
|
||||
m_status = Job_Failed;
|
||||
int next = m_mirror_index + 1;
|
||||
if(m_mirrors.size() == next)
|
||||
m_mirror_index = 0;
|
||||
else
|
||||
m_mirror_index = next;
|
||||
|
||||
updateUrl();
|
||||
emit failed(m_index_within_job);
|
||||
}
|
||||
|
||||
void ForgeXzDownload::updateUrl()
|
||||
{
|
||||
qDebug() << "Updating URL for " << m_url_path;
|
||||
for (auto possible : m_mirrors)
|
||||
{
|
||||
qDebug() << "Possible: " << possible.name << " : " << possible.mirror_url;
|
||||
}
|
||||
QString aggregate = m_mirrors[m_mirror_index].mirror_url + m_url_path + ".pack.xz";
|
||||
m_url = QUrl(aggregate);
|
||||
}
|
||||
|
||||
void ForgeXzDownload::downloadFinished()
|
||||
{
|
||||
//TEST: defer to other possible mirrors (autofail the first one)
|
||||
/*
|
||||
qDebug() <<"dl " << index_within_job << " mirror " << m_mirror_index;
|
||||
if( m_mirror_index == 0)
|
||||
{
|
||||
qDebug() <<"dl " << index_within_job << " AUTOFAIL";
|
||||
m_status = Job_Failed;
|
||||
m_pack200_xz_file.close();
|
||||
m_pack200_xz_file.remove();
|
||||
m_reply.reset();
|
||||
failAndTryNextMirror();
|
||||
return;
|
||||
}
|
||||
*/
|
||||
|
||||
// if the download succeeded
|
||||
if (m_status != Job_Failed)
|
||||
{
|
||||
// nothing went wrong...
|
||||
m_status = Job_Finished;
|
||||
if (m_pack200_xz_file.isOpen())
|
||||
{
|
||||
// we actually downloaded something! process and isntall it
|
||||
decompressAndInstall();
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// something bad happened -- on the local machine!
|
||||
m_status = Job_Failed;
|
||||
m_pack200_xz_file.remove();
|
||||
m_reply.reset();
|
||||
emit failed(m_index_within_job);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// else the download failed
|
||||
else
|
||||
{
|
||||
m_status = Job_Failed;
|
||||
m_pack200_xz_file.close();
|
||||
m_pack200_xz_file.remove();
|
||||
m_reply.reset();
|
||||
failAndTryNextMirror();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void ForgeXzDownload::downloadReadyRead()
|
||||
{
|
||||
|
||||
if (!m_pack200_xz_file.isOpen())
|
||||
{
|
||||
if (!m_pack200_xz_file.open())
|
||||
{
|
||||
/*
|
||||
* Can't open the file... the job failed
|
||||
*/
|
||||
m_reply->abort();
|
||||
emit failed(m_index_within_job);
|
||||
return;
|
||||
}
|
||||
}
|
||||
m_pack200_xz_file.write(m_reply->readAll());
|
||||
}
|
||||
|
||||
#include "xz.h"
|
||||
#include "unpack200.h"
|
||||
#include <stdexcept>
|
||||
|
||||
const size_t buffer_size = 8196;
|
||||
|
||||
void ForgeXzDownload::decompressAndInstall()
|
||||
{
|
||||
// rewind the downloaded temp file
|
||||
m_pack200_xz_file.seek(0);
|
||||
// de-xz'd file
|
||||
QTemporaryFile pack200_file("./dl_temp.XXXXXX");
|
||||
pack200_file.open();
|
||||
|
||||
bool xz_success = false;
|
||||
// first, de-xz
|
||||
{
|
||||
uint8_t in[buffer_size];
|
||||
uint8_t out[buffer_size];
|
||||
struct xz_buf b;
|
||||
struct xz_dec *s;
|
||||
enum xz_ret ret;
|
||||
xz_crc32_init();
|
||||
xz_crc64_init();
|
||||
s = xz_dec_init(XZ_DYNALLOC, 1 << 26);
|
||||
if (s == nullptr)
|
||||
{
|
||||
xz_dec_end(s);
|
||||
failAndTryNextMirror();
|
||||
return;
|
||||
}
|
||||
b.in = in;
|
||||
b.in_pos = 0;
|
||||
b.in_size = 0;
|
||||
b.out = out;
|
||||
b.out_pos = 0;
|
||||
b.out_size = buffer_size;
|
||||
while (!xz_success)
|
||||
{
|
||||
if (b.in_pos == b.in_size)
|
||||
{
|
||||
b.in_size = m_pack200_xz_file.read((char *)in, sizeof(in));
|
||||
b.in_pos = 0;
|
||||
}
|
||||
|
||||
ret = xz_dec_run(s, &b);
|
||||
|
||||
if (b.out_pos == sizeof(out))
|
||||
{
|
||||
if (pack200_file.write((char *)out, b.out_pos) != b.out_pos)
|
||||
{
|
||||
// msg = "Write error\n";
|
||||
xz_dec_end(s);
|
||||
failAndTryNextMirror();
|
||||
return;
|
||||
}
|
||||
|
||||
b.out_pos = 0;
|
||||
}
|
||||
|
||||
if (ret == XZ_OK)
|
||||
continue;
|
||||
|
||||
if (ret == XZ_UNSUPPORTED_CHECK)
|
||||
{
|
||||
// unsupported check. this is OK, but we should log this
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pack200_file.write((char *)out, b.out_pos) != b.out_pos)
|
||||
{
|
||||
// write error
|
||||
pack200_file.close();
|
||||
xz_dec_end(s);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (ret)
|
||||
{
|
||||
case XZ_STREAM_END:
|
||||
xz_dec_end(s);
|
||||
xz_success = true;
|
||||
break;
|
||||
|
||||
case XZ_MEM_ERROR:
|
||||
qCritical() << "Memory allocation failed\n";
|
||||
xz_dec_end(s);
|
||||
failAndTryNextMirror();
|
||||
return;
|
||||
|
||||
case XZ_MEMLIMIT_ERROR:
|
||||
qCritical() << "Memory usage limit reached\n";
|
||||
xz_dec_end(s);
|
||||
failAndTryNextMirror();
|
||||
return;
|
||||
|
||||
case XZ_FORMAT_ERROR:
|
||||
qCritical() << "Not a .xz file\n";
|
||||
xz_dec_end(s);
|
||||
failAndTryNextMirror();
|
||||
return;
|
||||
|
||||
case XZ_OPTIONS_ERROR:
|
||||
qCritical() << "Unsupported options in the .xz headers\n";
|
||||
xz_dec_end(s);
|
||||
failAndTryNextMirror();
|
||||
return;
|
||||
|
||||
case XZ_DATA_ERROR:
|
||||
case XZ_BUF_ERROR:
|
||||
qCritical() << "File is corrupt\n";
|
||||
xz_dec_end(s);
|
||||
failAndTryNextMirror();
|
||||
return;
|
||||
|
||||
default:
|
||||
qCritical() << "Bug!\n";
|
||||
xz_dec_end(s);
|
||||
failAndTryNextMirror();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
m_pack200_xz_file.remove();
|
||||
|
||||
// revert pack200
|
||||
pack200_file.seek(0);
|
||||
int handle_in = pack200_file.handle();
|
||||
// FIXME: dispose of file handles, pointers and the like. Ideally wrap in objects.
|
||||
if(handle_in == -1)
|
||||
{
|
||||
qCritical() << "Error reopening " << pack200_file.fileName();
|
||||
failAndTryNextMirror();
|
||||
return;
|
||||
}
|
||||
FILE * file_in = fdopen(handle_in,"r");
|
||||
if(!file_in)
|
||||
{
|
||||
qCritical() << "Error reopening " << pack200_file.fileName();
|
||||
failAndTryNextMirror();
|
||||
return;
|
||||
}
|
||||
QFile qfile_out(m_target_path);
|
||||
if(!qfile_out.open(QIODevice::WriteOnly))
|
||||
{
|
||||
qCritical() << "Error opening " << qfile_out.fileName();
|
||||
failAndTryNextMirror();
|
||||
return;
|
||||
}
|
||||
int handle_out = qfile_out.handle();
|
||||
if(handle_out == -1)
|
||||
{
|
||||
qCritical() << "Error opening " << qfile_out.fileName();
|
||||
failAndTryNextMirror();
|
||||
return;
|
||||
}
|
||||
FILE * file_out = fdopen(handle_out,"w");
|
||||
if(!file_out)
|
||||
{
|
||||
qCritical() << "Error opening " << qfile_out.fileName();
|
||||
failAndTryNextMirror();
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
unpack_200(file_in, file_out);
|
||||
}
|
||||
catch (std::runtime_error &err)
|
||||
{
|
||||
m_status = Job_Failed;
|
||||
qCritical() << "Error unpacking " << pack200_file.fileName() << " : " << err.what();
|
||||
QFile f(m_target_path);
|
||||
if (f.exists())
|
||||
f.remove();
|
||||
failAndTryNextMirror();
|
||||
return;
|
||||
}
|
||||
pack200_file.remove();
|
||||
|
||||
QFile jar_file(m_target_path);
|
||||
|
||||
if (!jar_file.open(QIODevice::ReadOnly))
|
||||
{
|
||||
jar_file.remove();
|
||||
failAndTryNextMirror();
|
||||
return;
|
||||
}
|
||||
m_entry->md5sum = QCryptographicHash::hash(jar_file.readAll(), QCryptographicHash::Md5)
|
||||
.toHex()
|
||||
.constData();
|
||||
jar_file.close();
|
||||
|
||||
QFileInfo output_file_info(m_target_path);
|
||||
m_entry->etag = m_reply->rawHeader("ETag").constData();
|
||||
m_entry->local_changed_timestamp =
|
||||
output_file_info.lastModified().toUTC().toMSecsSinceEpoch();
|
||||
m_entry->stale = false;
|
||||
ENV.metacache()->updateEntry(m_entry);
|
||||
|
||||
m_reply.reset();
|
||||
emit succeeded(m_index_within_job);
|
||||
}
|
66
logic/minecraft/forge/ForgeXzDownload.h
Normal file
66
logic/minecraft/forge/ForgeXzDownload.h
Normal file
@ -0,0 +1,66 @@
|
||||
/* Copyright 2013-2015 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "net/NetAction.h"
|
||||
#include "net/HttpMetaCache.h"
|
||||
#include <QFile>
|
||||
#include <QTemporaryFile>
|
||||
#include "ForgeMirror.h"
|
||||
|
||||
typedef std::shared_ptr<class ForgeXzDownload> ForgeXzDownloadPtr;
|
||||
|
||||
class ForgeXzDownload : public NetAction
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
MetaEntryPtr m_entry;
|
||||
/// if saving to file, use the one specified in this string
|
||||
QString m_target_path;
|
||||
/// this is the output file, if any
|
||||
QTemporaryFile m_pack200_xz_file;
|
||||
/// mirror index (NOT OPTICS, I SWEAR)
|
||||
int m_mirror_index = 0;
|
||||
/// list of mirrors to use. Mirror has the url base
|
||||
QList<ForgeMirror> m_mirrors;
|
||||
/// path relative to the mirror base
|
||||
QString m_url_path;
|
||||
|
||||
public:
|
||||
explicit ForgeXzDownload(QString relative_path, MetaEntryPtr entry);
|
||||
static ForgeXzDownloadPtr make(QString relative_path, MetaEntryPtr entry)
|
||||
{
|
||||
return ForgeXzDownloadPtr(new ForgeXzDownload(relative_path, entry));
|
||||
}
|
||||
virtual ~ForgeXzDownload(){};
|
||||
void setMirrors(QList<ForgeMirror> & mirrors);
|
||||
|
||||
protected
|
||||
slots:
|
||||
virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
|
||||
virtual void downloadError(QNetworkReply::NetworkError error);
|
||||
virtual void downloadFinished();
|
||||
virtual void downloadReadyRead();
|
||||
|
||||
public
|
||||
slots:
|
||||
virtual void start();
|
||||
|
||||
private:
|
||||
void decompressAndInstall();
|
||||
void failAndTryNextMirror();
|
||||
void updateUrl();
|
||||
};
|
56
logic/minecraft/forge/LegacyForge.cpp
Normal file
56
logic/minecraft/forge/LegacyForge.cpp
Normal file
@ -0,0 +1,56 @@
|
||||
/* Copyright 2013-2015 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "LegacyForge.h"
|
||||
|
||||
MinecraftForge::MinecraftForge(const QString &file) : Mod(file)
|
||||
{
|
||||
}
|
||||
|
||||
bool MinecraftForge::FixVersionIfNeeded(QString newVersion)
|
||||
{/*
|
||||
wxString reportedVersion = GetModVersion();
|
||||
if(reportedVersion == "..." || reportedVersion.empty())
|
||||
{
|
||||
std::auto_ptr<wxFFileInputStream> in(new wxFFileInputStream("forge.zip"));
|
||||
wxTempFileOutputStream out("forge.zip");
|
||||
wxTextOutputStream textout(out);
|
||||
wxZipInputStream inzip(*in);
|
||||
wxZipOutputStream outzip(out);
|
||||
std::auto_ptr<wxZipEntry> entry;
|
||||
// preserve metadata
|
||||
outzip.CopyArchiveMetaData(inzip);
|
||||
// copy all entries
|
||||
while (entry.reset(inzip.GetNextEntry()), entry.get() != NULL)
|
||||
if (!outzip.CopyEntry(entry.release(), inzip))
|
||||
return false;
|
||||
// release last entry
|
||||
in.reset();
|
||||
outzip.PutNextEntry("forgeversion.properties");
|
||||
|
||||
wxStringTokenizer tokenizer(newVersion,".");
|
||||
wxString verFile;
|
||||
verFile << wxString("forge.major.number=") << tokenizer.GetNextToken() << "\n";
|
||||
verFile << wxString("forge.minor.number=") << tokenizer.GetNextToken() << "\n";
|
||||
verFile << wxString("forge.revision.number=") << tokenizer.GetNextToken() << "\n";
|
||||
verFile << wxString("forge.build.number=") << tokenizer.GetNextToken() << "\n";
|
||||
auto buf = verFile.ToUTF8();
|
||||
outzip.Write(buf.data(), buf.length());
|
||||
// check if we succeeded
|
||||
return inzip.Eof() && outzip.Close() && out.Commit();
|
||||
}
|
||||
*/
|
||||
return true;
|
||||
}
|
25
logic/minecraft/forge/LegacyForge.h
Normal file
25
logic/minecraft/forge/LegacyForge.h
Normal file
@ -0,0 +1,25 @@
|
||||
/* Copyright 2013-2015 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "minecraft/Mod.h"
|
||||
|
||||
class MinecraftForge : public Mod
|
||||
{
|
||||
public:
|
||||
MinecraftForge(const QString &file);
|
||||
bool FixVersionIfNeeded(QString newVersion);
|
||||
};
|
395
logic/minecraft/ftb/FTBPlugin.cpp
Normal file
395
logic/minecraft/ftb/FTBPlugin.cpp
Normal file
@ -0,0 +1,395 @@
|
||||
#include "FTBPlugin.h"
|
||||
#include "FTBVersion.h"
|
||||
#include "LegacyFTBInstance.h"
|
||||
#include "OneSixFTBInstance.h"
|
||||
#include <BaseInstance.h>
|
||||
#include <icons/IconList.h>
|
||||
#include <InstanceList.h>
|
||||
#include <minecraft/MinecraftVersionList.h>
|
||||
#include <settings/INISettingsObject.h>
|
||||
#include <FileSystem.h>
|
||||
#include "QDebug"
|
||||
#include <QXmlStreamReader>
|
||||
#include <QRegularExpression>
|
||||
|
||||
struct FTBRecord
|
||||
{
|
||||
QString dirName;
|
||||
QString name;
|
||||
QString logo;
|
||||
QString iconKey;
|
||||
QString mcVersion;
|
||||
QString description;
|
||||
QString instanceDir;
|
||||
QString templateDir;
|
||||
bool operator==(const FTBRecord other) const
|
||||
{
|
||||
return instanceDir == other.instanceDir;
|
||||
}
|
||||
};
|
||||
|
||||
inline uint qHash(FTBRecord record)
|
||||
{
|
||||
return qHash(record.instanceDir);
|
||||
}
|
||||
|
||||
QSet<FTBRecord> discoverFTBInstances(SettingsObjectPtr globalSettings)
|
||||
{
|
||||
QSet<FTBRecord> records;
|
||||
QDir dir = QDir(globalSettings->get("FTBLauncherLocal").toString());
|
||||
QDir dataDir = QDir(globalSettings->get("FTBRoot").toString());
|
||||
if (!dataDir.exists())
|
||||
{
|
||||
qDebug() << "The FTB directory specified does not exist. Please check your settings";
|
||||
return records;
|
||||
}
|
||||
else if (!dir.exists())
|
||||
{
|
||||
qDebug() << "The FTB launcher data directory specified does not exist. Please check "
|
||||
"your settings";
|
||||
return records;
|
||||
}
|
||||
dir.cd("ModPacks");
|
||||
auto allFiles = dir.entryList(QDir::Readable | QDir::Files, QDir::Name);
|
||||
for (auto filename : allFiles)
|
||||
{
|
||||
if (!filename.endsWith(".xml"))
|
||||
continue;
|
||||
auto fpath = dir.absoluteFilePath(filename);
|
||||
QFile f(fpath);
|
||||
qDebug() << "Discovering FTB instances -- " << fpath;
|
||||
if (!f.open(QFile::ReadOnly))
|
||||
continue;
|
||||
|
||||
// read the FTB packs XML.
|
||||
QXmlStreamReader reader(&f);
|
||||
while (!reader.atEnd())
|
||||
{
|
||||
switch (reader.readNext())
|
||||
{
|
||||
case QXmlStreamReader::StartElement:
|
||||
{
|
||||
if (reader.name() == "modpack")
|
||||
{
|
||||
QXmlStreamAttributes attrs = reader.attributes();
|
||||
FTBRecord record;
|
||||
record.dirName = attrs.value("dir").toString();
|
||||
record.instanceDir = dataDir.absoluteFilePath(record.dirName);
|
||||
record.templateDir = dir.absoluteFilePath(record.dirName);
|
||||
QDir test(record.instanceDir);
|
||||
qDebug() << dataDir.absolutePath() << record.instanceDir << record.dirName;
|
||||
if (!test.exists())
|
||||
continue;
|
||||
record.name = attrs.value("name").toString();
|
||||
record.logo = attrs.value("logo").toString();
|
||||
QString logo = record.logo;
|
||||
record.iconKey = logo.remove(QRegularExpression("\\..*"));
|
||||
auto customVersions = attrs.value("customMCVersions");
|
||||
if (!customVersions.isNull())
|
||||
{
|
||||
QMap<QString, QString> versionMatcher;
|
||||
QString customVersionsStr = customVersions.toString();
|
||||
QStringList list = customVersionsStr.split(';');
|
||||
for (auto item : list)
|
||||
{
|
||||
auto segment = item.split('^');
|
||||
if (segment.size() != 2)
|
||||
{
|
||||
qCritical() << "FTB: Segment of size < 2 in "
|
||||
<< customVersionsStr;
|
||||
continue;
|
||||
}
|
||||
versionMatcher[segment[0]] = segment[1];
|
||||
}
|
||||
auto actualVersion = attrs.value("version").toString();
|
||||
if (versionMatcher.contains(actualVersion))
|
||||
{
|
||||
record.mcVersion = versionMatcher[actualVersion];
|
||||
}
|
||||
else
|
||||
{
|
||||
record.mcVersion = attrs.value("mcVersion").toString();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
record.mcVersion = attrs.value("mcVersion").toString();
|
||||
}
|
||||
record.description = attrs.value("description").toString();
|
||||
records.insert(record);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case QXmlStreamReader::EndElement:
|
||||
break;
|
||||
case QXmlStreamReader::Characters:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
InstancePtr loadInstance(SettingsObjectPtr globalSettings, QMap<QString, QString> &groupMap, const FTBRecord & record)
|
||||
{
|
||||
InstancePtr inst;
|
||||
|
||||
auto m_settings = std::make_shared<INISettingsObject>(FS::PathCombine(record.instanceDir, "instance.cfg"));
|
||||
m_settings->registerSetting("InstanceType", "Legacy");
|
||||
|
||||
qDebug() << "Loading existing " << record.name;
|
||||
|
||||
QString inst_type = m_settings->get("InstanceType").toString();
|
||||
if (inst_type == "LegacyFTB")
|
||||
{
|
||||
inst.reset(new LegacyFTBInstance(globalSettings, m_settings, record.instanceDir));
|
||||
}
|
||||
else if (inst_type == "OneSixFTB")
|
||||
{
|
||||
inst.reset(new OneSixFTBInstance(globalSettings, m_settings, record.instanceDir));
|
||||
}
|
||||
else
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
qDebug() << "Construction " << record.instanceDir;
|
||||
|
||||
SettingsObject::Lock lock(inst->settings());
|
||||
inst->init();
|
||||
qDebug() << "Init " << record.instanceDir;
|
||||
inst->setGroupInitial("FTB");
|
||||
/**
|
||||
* FIXME: this does not respect the user's preferences. BUT, it would work nicely with the planned pack support
|
||||
* -> instead of changing the user values, change pack values (defaults you can look at and revert to)
|
||||
*/
|
||||
/*
|
||||
inst->setName(record.name);
|
||||
inst->setIconKey(record.iconKey);
|
||||
inst->setNotes(record.description);
|
||||
*/
|
||||
if (inst->intendedVersionId() != record.mcVersion)
|
||||
{
|
||||
inst->setIntendedVersionId(record.mcVersion);
|
||||
}
|
||||
qDebug() << "Post-Process " << record.instanceDir;
|
||||
if (!InstanceList::continueProcessInstance(inst, InstanceList::NoCreateError, record.instanceDir, groupMap))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
qDebug() << "Final " << record.instanceDir;
|
||||
return inst;
|
||||
}
|
||||
|
||||
InstancePtr createInstance(SettingsObjectPtr globalSettings, QMap<QString, QString> &groupMap, const FTBRecord & record)
|
||||
{
|
||||
QDir rootDir(record.instanceDir);
|
||||
|
||||
InstancePtr inst;
|
||||
|
||||
qDebug() << "Converting " << record.name << " as new.";
|
||||
|
||||
auto mcVersion = std::dynamic_pointer_cast<MinecraftVersion>(ENV.getVersion("net.minecraft", record.mcVersion));
|
||||
if (!mcVersion)
|
||||
{
|
||||
qCritical() << "Can't load instance " << record.instanceDir
|
||||
<< " because minecraft version " << record.mcVersion
|
||||
<< " can't be resolved.";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!rootDir.exists() && !rootDir.mkpath("."))
|
||||
{
|
||||
qCritical() << "Can't create instance folder" << record.instanceDir;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto m_settings = std::make_shared<INISettingsObject>(FS::PathCombine(record.instanceDir, "instance.cfg"));
|
||||
m_settings->registerSetting("InstanceType", "Legacy");
|
||||
|
||||
if (mcVersion->usesLegacyLauncher())
|
||||
{
|
||||
m_settings->set("InstanceType", "LegacyFTB");
|
||||
inst.reset(new LegacyFTBInstance(globalSettings, m_settings, record.instanceDir));
|
||||
}
|
||||
else
|
||||
{
|
||||
m_settings->set("InstanceType", "OneSixFTB");
|
||||
inst.reset(new OneSixFTBInstance(globalSettings, m_settings, record.instanceDir));
|
||||
}
|
||||
// initialize
|
||||
{
|
||||
SettingsObject::Lock lock(inst->settings());
|
||||
inst->setIntendedVersionId(mcVersion->descriptor());
|
||||
inst->init();
|
||||
inst->setGroupInitial("FTB");
|
||||
inst->setName(record.name);
|
||||
inst->setIconKey(record.iconKey);
|
||||
inst->setNotes(record.description);
|
||||
qDebug() << "Post-Process " << record.instanceDir;
|
||||
if (!InstanceList::continueProcessInstance(inst, InstanceList::NoCreateError, record.instanceDir, groupMap))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
return inst;
|
||||
}
|
||||
|
||||
void FTBPlugin::loadInstances(SettingsObjectPtr globalSettings, QMap<QString, QString> &groupMap, QList<InstancePtr> &tempList)
|
||||
{
|
||||
// nothing to load when we don't have
|
||||
if (globalSettings->get("TrackFTBInstances").toBool() != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto records = discoverFTBInstances(globalSettings);
|
||||
if (!records.size())
|
||||
{
|
||||
qDebug() << "No FTB instances to load.";
|
||||
return;
|
||||
}
|
||||
qDebug() << "Loading FTB instances! -- got " << records.size();
|
||||
// process the records we acquired.
|
||||
for (auto record : records)
|
||||
{
|
||||
qDebug() << "Loading FTB instance from " << record.instanceDir;
|
||||
QString iconKey = record.iconKey;
|
||||
ENV.icons()->addIcon(iconKey, iconKey, FS::PathCombine(record.templateDir, record.logo), MMCIcon::Transient);
|
||||
auto settingsFilePath = FS::PathCombine(record.instanceDir, "instance.cfg");
|
||||
qDebug() << "ICON get!";
|
||||
|
||||
if (QFileInfo(settingsFilePath).exists())
|
||||
{
|
||||
auto instPtr = loadInstance(globalSettings, groupMap, record);
|
||||
if (!instPtr)
|
||||
{
|
||||
qWarning() << "Couldn't load instance config:" << settingsFilePath;
|
||||
if(!QFile::remove(settingsFilePath))
|
||||
{
|
||||
qWarning() << "Couldn't remove broken instance config!";
|
||||
continue;
|
||||
}
|
||||
// failed to load, but removed the poisonous file
|
||||
}
|
||||
else
|
||||
{
|
||||
tempList.append(InstancePtr(instPtr));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
auto instPtr = createInstance(globalSettings, groupMap, record);
|
||||
if (!instPtr)
|
||||
{
|
||||
qWarning() << "Couldn't create FTB instance!";
|
||||
continue;
|
||||
}
|
||||
tempList.append(InstancePtr(instPtr));
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
#include <windows.h>
|
||||
static const int APPDATA_BUFFER_SIZE = 1024;
|
||||
#endif
|
||||
|
||||
static QString getLocalCacheStorageLocation()
|
||||
{
|
||||
QString ftbDefault;
|
||||
#ifdef Q_OS_WIN32
|
||||
wchar_t buf[APPDATA_BUFFER_SIZE];
|
||||
if (GetEnvironmentVariableW(L"LOCALAPPDATA", buf, APPDATA_BUFFER_SIZE)) // local
|
||||
{
|
||||
ftbDefault = QDir(QString::fromWCharArray(buf)).absoluteFilePath("ftblauncher");
|
||||
}
|
||||
else if (GetEnvironmentVariableW(L"APPDATA", buf, APPDATA_BUFFER_SIZE)) // roaming
|
||||
{
|
||||
ftbDefault = QDir(QString::fromWCharArray(buf)).absoluteFilePath("ftblauncher");
|
||||
}
|
||||
else
|
||||
{
|
||||
qCritical() << "Your LOCALAPPDATA and APPDATA folders are missing!"
|
||||
" If you are on windows, this means your system is broken.";
|
||||
}
|
||||
#elif defined(Q_OS_MAC)
|
||||
ftbDefault = FS::PathCombine(QDir::homePath(), "Library/Application Support/ftblauncher");
|
||||
#else
|
||||
ftbDefault = QDir::home().absoluteFilePath(".ftblauncher");
|
||||
#endif
|
||||
return ftbDefault;
|
||||
}
|
||||
|
||||
|
||||
static QString getRoamingStorageLocation()
|
||||
{
|
||||
QString ftbDefault;
|
||||
#ifdef Q_OS_WIN32
|
||||
wchar_t buf[APPDATA_BUFFER_SIZE];
|
||||
QString cacheStorage;
|
||||
if (GetEnvironmentVariableW(L"APPDATA", buf, APPDATA_BUFFER_SIZE))
|
||||
{
|
||||
ftbDefault = QDir(QString::fromWCharArray(buf)).absoluteFilePath("ftblauncher");
|
||||
}
|
||||
else
|
||||
{
|
||||
qCritical() << "Your APPDATA folder is missing! If you are on windows, this means your system is broken.";
|
||||
}
|
||||
#elif defined(Q_OS_MAC)
|
||||
ftbDefault = FS::PathCombine(QDir::homePath(), "Library/Application Support/ftblauncher");
|
||||
#else
|
||||
ftbDefault = QDir::home().absoluteFilePath(".ftblauncher");
|
||||
#endif
|
||||
return ftbDefault;
|
||||
}
|
||||
|
||||
void FTBPlugin::initialize(SettingsObjectPtr globalSettings)
|
||||
{
|
||||
// FTB
|
||||
globalSettings->registerSetting("TrackFTBInstances", false);
|
||||
QString ftbRoaming = getRoamingStorageLocation();
|
||||
QString ftbLocal = getLocalCacheStorageLocation();
|
||||
|
||||
globalSettings->registerSetting("FTBLauncherRoaming", ftbRoaming);
|
||||
globalSettings->registerSetting("FTBLauncherLocal", ftbLocal);
|
||||
qDebug() << "FTB Launcher paths:" << globalSettings->get("FTBLauncherRoaming").toString()
|
||||
<< "and" << globalSettings->get("FTBLauncherLocal").toString();
|
||||
|
||||
globalSettings->registerSetting("FTBRoot");
|
||||
if (globalSettings->get("FTBRoot").isNull())
|
||||
{
|
||||
QString ftbRoot;
|
||||
QFile f(QDir(globalSettings->get("FTBLauncherRoaming").toString()).absoluteFilePath("ftblaunch.cfg"));
|
||||
qDebug() << "Attempting to read" << f.fileName();
|
||||
if (f.open(QFile::ReadOnly))
|
||||
{
|
||||
const QString data = QString::fromLatin1(f.readAll());
|
||||
QRegularExpression exp("installPath=(.*)");
|
||||
ftbRoot = QDir::cleanPath(exp.match(data).captured(1));
|
||||
#ifdef Q_OS_WIN32
|
||||
if (!ftbRoot.isEmpty())
|
||||
{
|
||||
if (ftbRoot.at(0).isLetter() && ftbRoot.size() > 1 && ftbRoot.at(1) == '/')
|
||||
{
|
||||
ftbRoot.remove(1, 1);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (ftbRoot.isEmpty())
|
||||
{
|
||||
qDebug() << "Failed to get FTB root path";
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "FTB is installed at" << ftbRoot;
|
||||
globalSettings->set("FTBRoot", ftbRoot);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qWarning() << "Couldn't open" << f.fileName() << ":" << f.errorString();
|
||||
qWarning() << "This is perfectly normal if you don't have FTB installed";
|
||||
}
|
||||
}
|
||||
}
|
13
logic/minecraft/ftb/FTBPlugin.h
Normal file
13
logic/minecraft/ftb/FTBPlugin.h
Normal file
@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <BaseInstance.h>
|
||||
|
||||
#include "multimc_logic_export.h"
|
||||
|
||||
// Pseudo-plugin for FTB related things. Super derpy!
|
||||
class MULTIMC_LOGIC_EXPORT FTBPlugin
|
||||
{
|
||||
public:
|
||||
static void initialize(SettingsObjectPtr globalSettings);
|
||||
static void loadInstances(SettingsObjectPtr globalSettings, QMap<QString, QString> &groupMap, QList<InstancePtr> &tempList);
|
||||
};
|
128
logic/minecraft/ftb/FTBProfileStrategy.cpp
Normal file
128
logic/minecraft/ftb/FTBProfileStrategy.cpp
Normal file
@ -0,0 +1,128 @@
|
||||
#include "FTBProfileStrategy.h"
|
||||
#include "OneSixFTBInstance.h"
|
||||
|
||||
#include "minecraft/VersionBuildError.h"
|
||||
#include "minecraft/MinecraftVersionList.h"
|
||||
#include <FileSystem.h>
|
||||
|
||||
#include <QDir>
|
||||
#include <QUuid>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
|
||||
FTBProfileStrategy::FTBProfileStrategy(OneSixFTBInstance* instance) : OneSixProfileStrategy(instance)
|
||||
{
|
||||
}
|
||||
|
||||
void FTBProfileStrategy::loadDefaultBuiltinPatches()
|
||||
{
|
||||
// FIXME: this should be here, but it needs us to be able to deal with multiple libraries paths
|
||||
// OneSixProfileStrategy::loadDefaultBuiltinPatches();
|
||||
auto mcVersion = m_instance->intendedVersionId();
|
||||
|
||||
ProfilePatchPtr minecraftPatch;
|
||||
{
|
||||
auto mcJson = m_instance->versionsPath().absoluteFilePath(mcVersion + "/" + mcVersion + ".json");
|
||||
// load up the base minecraft patch
|
||||
if(QFile::exists(mcJson))
|
||||
{
|
||||
auto file = ProfileUtils::parseJsonFile(QFileInfo(mcJson), false);
|
||||
file->fileId = "net.minecraft";
|
||||
file->name = QObject::tr("Minecraft (tracked)");
|
||||
file->setVanilla(true);
|
||||
if(file->version.isEmpty())
|
||||
{
|
||||
file->version = mcVersion;
|
||||
}
|
||||
minecraftPatch = std::dynamic_pointer_cast<ProfilePatch>(file);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw VersionIncomplete("net.minecraft");
|
||||
}
|
||||
minecraftPatch->setOrder(-2);
|
||||
}
|
||||
profile->appendPatch(minecraftPatch);
|
||||
|
||||
auto nativeInstance = dynamic_cast<OneSixFTBInstance *>(m_instance);
|
||||
ProfilePatchPtr packPatch;
|
||||
{
|
||||
auto mcJson = m_instance->minecraftRoot() + "/pack.json";
|
||||
// load up the base minecraft patch
|
||||
if(QFile::exists(mcJson))
|
||||
{
|
||||
auto file = ProfileUtils::parseJsonFile(QFileInfo(mcJson), false);
|
||||
|
||||
// adapt the loaded file - the FTB patch file format is different than ours.
|
||||
file->addLibs = file->overwriteLibs;
|
||||
file->overwriteLibs.clear();
|
||||
file->shouldOverwriteLibs = false;
|
||||
file->id.clear();
|
||||
for(auto addLib: file->addLibs)
|
||||
{
|
||||
addLib->m_hint = "local";
|
||||
addLib->setStoragePrefix(nativeInstance->librariesPath().absolutePath());
|
||||
}
|
||||
file->fileId = "org.multimc.ftb.pack";
|
||||
file->setVanilla(true);
|
||||
file->name = QObject::tr("%1 (FTB pack)").arg(m_instance->name());
|
||||
if(file->version.isEmpty())
|
||||
{
|
||||
file->version = QObject::tr("Unknown");
|
||||
QFile versionFile (FS::PathCombine(m_instance->instanceRoot(), "version"));
|
||||
if(versionFile.exists())
|
||||
{
|
||||
if(versionFile.open(QIODevice::ReadOnly))
|
||||
{
|
||||
// FIXME: just guessing the encoding/charset here.
|
||||
auto version = QString::fromUtf8(versionFile.readAll());
|
||||
file->version = version;
|
||||
}
|
||||
}
|
||||
}
|
||||
packPatch = std::dynamic_pointer_cast<ProfilePatch>(file);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw VersionIncomplete("org.multimc.ftb.pack");
|
||||
}
|
||||
packPatch->setOrder(1);
|
||||
}
|
||||
profile->appendPatch(packPatch);
|
||||
|
||||
}
|
||||
|
||||
void FTBProfileStrategy::load()
|
||||
{
|
||||
profile->clearPatches();
|
||||
|
||||
loadDefaultBuiltinPatches();
|
||||
loadUserPatches();
|
||||
|
||||
profile->finalize();
|
||||
}
|
||||
|
||||
bool FTBProfileStrategy::saveOrder(ProfileUtils::PatchOrder order)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FTBProfileStrategy::resetOrder()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FTBProfileStrategy::installJarMods(QStringList filepaths)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FTBProfileStrategy::customizePatch(ProfilePatchPtr patch)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FTBProfileStrategy::revertPatch(ProfilePatchPtr patch)
|
||||
{
|
||||
return false;
|
||||
}
|
21
logic/minecraft/ftb/FTBProfileStrategy.h
Normal file
21
logic/minecraft/ftb/FTBProfileStrategy.h
Normal file
@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
#include "minecraft/ProfileStrategy.h"
|
||||
#include "minecraft/onesix/OneSixProfileStrategy.h"
|
||||
|
||||
class OneSixFTBInstance;
|
||||
|
||||
class FTBProfileStrategy : public OneSixProfileStrategy
|
||||
{
|
||||
public:
|
||||
FTBProfileStrategy(OneSixFTBInstance * instance);
|
||||
virtual ~FTBProfileStrategy() {};
|
||||
virtual void load() override;
|
||||
virtual bool resetOrder() override;
|
||||
virtual bool saveOrder(ProfileUtils::PatchOrder order) override;
|
||||
virtual bool installJarMods(QStringList filepaths) override;
|
||||
virtual bool customizePatch (ProfilePatchPtr patch) override;
|
||||
virtual bool revertPatch (ProfilePatchPtr patch) override;
|
||||
|
||||
protected:
|
||||
virtual void loadDefaultBuiltinPatches() override;
|
||||
};
|
32
logic/minecraft/ftb/FTBVersion.h
Normal file
32
logic/minecraft/ftb/FTBVersion.h
Normal file
@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
#include <minecraft/MinecraftVersion.h>
|
||||
|
||||
class FTBVersion : public BaseVersion
|
||||
{
|
||||
public:
|
||||
FTBVersion(MinecraftVersionPtr parent) : m_version(parent){};
|
||||
|
||||
public:
|
||||
virtual QString descriptor() override
|
||||
{
|
||||
return m_version->descriptor();
|
||||
}
|
||||
|
||||
virtual QString name() override
|
||||
{
|
||||
return m_version->name();
|
||||
}
|
||||
|
||||
virtual QString typeString() const override
|
||||
{
|
||||
return m_version->typeString();
|
||||
}
|
||||
|
||||
MinecraftVersionPtr getMinecraftVersion()
|
||||
{
|
||||
return m_version;
|
||||
}
|
||||
|
||||
private:
|
||||
MinecraftVersionPtr m_version;
|
||||
};
|
27
logic/minecraft/ftb/LegacyFTBInstance.cpp
Normal file
27
logic/minecraft/ftb/LegacyFTBInstance.cpp
Normal file
@ -0,0 +1,27 @@
|
||||
#include "LegacyFTBInstance.h"
|
||||
#include <settings/INISettingsObject.h>
|
||||
#include <QDir>
|
||||
|
||||
LegacyFTBInstance::LegacyFTBInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir) :
|
||||
LegacyInstance(globalSettings, settings, rootDir)
|
||||
{
|
||||
}
|
||||
|
||||
QString LegacyFTBInstance::id() const
|
||||
{
|
||||
return "FTB/" + BaseInstance::id();
|
||||
}
|
||||
|
||||
void LegacyFTBInstance::copy(const QDir &newDir)
|
||||
{
|
||||
// set the target instance to be plain Legacy
|
||||
INISettingsObject settings_obj(newDir.absoluteFilePath("instance.cfg"));
|
||||
settings_obj.registerSetting("InstanceType", "Legacy");
|
||||
QString inst_type = settings_obj.get("InstanceType").toString();
|
||||
settings_obj.set("InstanceType", "Legacy");
|
||||
}
|
||||
|
||||
QString LegacyFTBInstance::typeName() const
|
||||
{
|
||||
return tr("Legacy FTB");
|
||||
}
|
13
logic/minecraft/ftb/LegacyFTBInstance.h
Normal file
13
logic/minecraft/ftb/LegacyFTBInstance.h
Normal file
@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "minecraft/legacy/LegacyInstance.h"
|
||||
|
||||
class LegacyFTBInstance : public LegacyInstance
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit LegacyFTBInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir);
|
||||
virtual QString id() const;
|
||||
virtual void copy(const QDir &newDir);
|
||||
virtual QString typeName() const;
|
||||
};
|
138
logic/minecraft/ftb/OneSixFTBInstance.cpp
Normal file
138
logic/minecraft/ftb/OneSixFTBInstance.cpp
Normal file
@ -0,0 +1,138 @@
|
||||
#include "OneSixFTBInstance.h"
|
||||
#include "FTBProfileStrategy.h"
|
||||
|
||||
#include "minecraft/MinecraftProfile.h"
|
||||
#include "minecraft/GradleSpecifier.h"
|
||||
#include "tasks/SequentialTask.h"
|
||||
#include <settings/INISettingsObject.h>
|
||||
#include <FileSystem.h>
|
||||
|
||||
#include <QJsonArray>
|
||||
|
||||
OneSixFTBInstance::OneSixFTBInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir) :
|
||||
OneSixInstance(globalSettings, settings, rootDir)
|
||||
{
|
||||
m_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
void OneSixFTBInstance::copy(const QDir &newDir)
|
||||
{
|
||||
QStringList libraryNames;
|
||||
// create patch file
|
||||
{
|
||||
qDebug()<< "Creating patch file for FTB instance...";
|
||||
QFile f(minecraftRoot() + "/pack.json");
|
||||
if (!f.open(QFile::ReadOnly))
|
||||
{
|
||||
qCritical() << "Couldn't open" << f.fileName() << ":" << f.errorString();
|
||||
return;
|
||||
}
|
||||
QJsonObject root = QJsonDocument::fromJson(f.readAll()).object();
|
||||
QJsonArray libs = root.value("libraries").toArray();
|
||||
QJsonArray outLibs;
|
||||
for (auto lib : libs)
|
||||
{
|
||||
QJsonObject libObj = lib.toObject();
|
||||
libObj.insert("MMC-hint", QString("local"));
|
||||
libObj.insert("insert", QString("prepend"));
|
||||
libraryNames.append(libObj.value("name").toString());
|
||||
outLibs.append(libObj);
|
||||
}
|
||||
root.remove("libraries");
|
||||
root.remove("id");
|
||||
|
||||
// HACK HACK HACK HACK
|
||||
// A workaround for a problem in MultiMC, triggered by a historical problem in FTB,
|
||||
// triggered by Mojang getting their library versions wrong in 1.7.10
|
||||
if(intendedVersionId() == "1.7.10")
|
||||
{
|
||||
auto insert = [&outLibs, &libraryNames](QString name)
|
||||
{
|
||||
QJsonObject libObj;
|
||||
libObj.insert("insert", QString("replace"));
|
||||
libObj.insert("name", name);
|
||||
libraryNames.push_back(name);
|
||||
outLibs.prepend(libObj);
|
||||
};
|
||||
insert("com.google.guava:guava:16.0");
|
||||
insert("org.apache.commons:commons-lang3:3.2.1");
|
||||
}
|
||||
root.insert("+libraries", outLibs);
|
||||
root.insert("order", 1);
|
||||
root.insert("fileId", QString("org.multimc.ftb.pack.json"));
|
||||
root.insert("name", name());
|
||||
root.insert("mcVersion", intendedVersionId());
|
||||
root.insert("version", intendedVersionId());
|
||||
FS::ensureFilePathExists(newDir.absoluteFilePath("patches/ftb.json"));
|
||||
QFile out(newDir.absoluteFilePath("patches/ftb.json"));
|
||||
if (!out.open(QFile::WriteOnly | QFile::Truncate))
|
||||
{
|
||||
qCritical() << "Couldn't open" << out.fileName() << ":" << out.errorString();
|
||||
return;
|
||||
}
|
||||
out.write(QJsonDocument(root).toJson());
|
||||
}
|
||||
// copy libraries
|
||||
{
|
||||
qDebug() << "Copying FTB libraries";
|
||||
for (auto library : libraryNames)
|
||||
{
|
||||
GradleSpecifier lib(library);
|
||||
const QString out = QDir::current().absoluteFilePath("libraries/" + lib.toPath());
|
||||
if (QFile::exists(out))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!FS::ensureFilePathExists(out))
|
||||
{
|
||||
qCritical() << "Couldn't create folder structure for" << out;
|
||||
}
|
||||
if (!QFile::copy(librariesPath().absoluteFilePath(lib.toPath()), out))
|
||||
{
|
||||
qCritical() << "Couldn't copy" << QString(lib);
|
||||
}
|
||||
}
|
||||
}
|
||||
// now set the target instance to be plain OneSix
|
||||
INISettingsObject settings_obj(newDir.absoluteFilePath("instance.cfg"));
|
||||
settings_obj.registerSetting("InstanceType", "Legacy");
|
||||
QString inst_type = settings_obj.get("InstanceType").toString();
|
||||
settings_obj.set("InstanceType", "OneSix");
|
||||
}
|
||||
|
||||
QString OneSixFTBInstance::id() const
|
||||
{
|
||||
return "FTB/" + BaseInstance::id();
|
||||
}
|
||||
|
||||
QDir OneSixFTBInstance::librariesPath() const
|
||||
{
|
||||
return QDir(m_globalSettings->get("FTBRoot").toString() + "/libraries");
|
||||
}
|
||||
|
||||
QDir OneSixFTBInstance::versionsPath() const
|
||||
{
|
||||
return QDir(m_globalSettings->get("FTBRoot").toString() + "/versions");
|
||||
}
|
||||
|
||||
bool OneSixFTBInstance::providesVersionFile() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void OneSixFTBInstance::createProfile()
|
||||
{
|
||||
m_version.reset(new MinecraftProfile(new FTBProfileStrategy(this)));
|
||||
}
|
||||
|
||||
std::shared_ptr<Task> OneSixFTBInstance::createUpdateTask()
|
||||
{
|
||||
return OneSixInstance::createUpdateTask();
|
||||
}
|
||||
|
||||
QString OneSixFTBInstance::typeName() const
|
||||
{
|
||||
return tr("OneSix FTB");
|
||||
}
|
||||
|
||||
#include "OneSixFTBInstance.moc"
|
27
logic/minecraft/ftb/OneSixFTBInstance.h
Normal file
27
logic/minecraft/ftb/OneSixFTBInstance.h
Normal file
@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include "minecraft/onesix/OneSixInstance.h"
|
||||
|
||||
class OneSixFTBInstance : public OneSixInstance
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit OneSixFTBInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir);
|
||||
virtual ~OneSixFTBInstance(){};
|
||||
|
||||
void copy(const QDir &newDir) override;
|
||||
|
||||
virtual void createProfile() override;
|
||||
|
||||
virtual std::shared_ptr<Task> createUpdateTask() override;
|
||||
|
||||
virtual QString id() const override;
|
||||
|
||||
QDir librariesPath() const override;
|
||||
QDir versionsPath() const override;
|
||||
bool providesVersionFile() const override;
|
||||
virtual QString typeName() const override;
|
||||
|
||||
private:
|
||||
SettingsObjectPtr m_globalSettings;
|
||||
};
|
@ -20,7 +20,7 @@
|
||||
|
||||
#include "LegacyInstance.h"
|
||||
|
||||
#include "minecraft/LegacyUpdate.h"
|
||||
#include "minecraft/legacy/LegacyUpdate.h"
|
||||
#include "icons/IconList.h"
|
||||
#include "launch/LaunchTask.h"
|
||||
#include <launch/steps/LaunchMinecraft.h>
|
@ -25,10 +25,10 @@
|
||||
|
||||
#include "LegacyUpdate.h"
|
||||
|
||||
#include "minecraft/LwjglVersionList.h"
|
||||
#include "LwjglVersionList.h"
|
||||
#include "minecraft/MinecraftVersionList.h"
|
||||
#include "minecraft/ModList.h"
|
||||
#include "minecraft/LegacyInstance.h"
|
||||
#include "LegacyInstance.h"
|
||||
#include <FileSystem.h>
|
||||
|
||||
LegacyUpdate::LegacyUpdate(BaseInstance *inst, QObject *parent) : Task(parent), m_inst(inst)
|
141
logic/minecraft/liteloader/LiteLoaderInstaller.cpp
Normal file
141
logic/minecraft/liteloader/LiteLoaderInstaller.cpp
Normal file
@ -0,0 +1,141 @@
|
||||
/* Copyright 2013-2015 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "LiteLoaderInstaller.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#include "minecraft/MinecraftProfile.h"
|
||||
#include "minecraft/RawLibrary.h"
|
||||
#include "minecraft/onesix/OneSixInstance.h"
|
||||
#include "minecraft/liteloader/LiteLoaderVersionList.h"
|
||||
#include "Exception.h"
|
||||
|
||||
LiteLoaderInstaller::LiteLoaderInstaller() : BaseInstaller()
|
||||
{
|
||||
}
|
||||
|
||||
void LiteLoaderInstaller::prepare(LiteLoaderVersionPtr version)
|
||||
{
|
||||
m_version = version;
|
||||
}
|
||||
bool LiteLoaderInstaller::add(OneSixInstance *to)
|
||||
{
|
||||
if (!BaseInstaller::add(to))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject obj;
|
||||
|
||||
obj.insert("mainClass", QString("net.minecraft.launchwrapper.Launch"));
|
||||
obj.insert("+tweakers", QJsonArray::fromStringList(QStringList() << m_version->tweakClass));
|
||||
obj.insert("order", 10);
|
||||
|
||||
QJsonArray libraries;
|
||||
|
||||
for (auto rawLibrary : m_version->libraries)
|
||||
{
|
||||
libraries.append(rawLibrary->toJson());
|
||||
}
|
||||
|
||||
// liteloader
|
||||
{
|
||||
RawLibrary liteloaderLib("com.mumfrey:liteloader:" + m_version->version);
|
||||
liteloaderLib.setAbsoluteUrl(QString("http://dl.liteloader.com/versions/com/mumfrey/liteloader/%1/%2").arg(m_version->mcVersion, m_version->file));
|
||||
QJsonObject llLibObj = liteloaderLib.toJson();
|
||||
libraries.append(llLibObj);
|
||||
}
|
||||
|
||||
obj.insert("+libraries", libraries);
|
||||
obj.insert("name", QString("LiteLoader"));
|
||||
obj.insert("fileId", id());
|
||||
obj.insert("version", m_version->version);
|
||||
obj.insert("mcVersion", to->intendedVersionId());
|
||||
|
||||
QFile file(filename(to->instanceRoot()));
|
||||
if (!file.open(QFile::WriteOnly))
|
||||
{
|
||||
qCritical() << "Error opening" << file.fileName()
|
||||
<< "for reading:" << file.errorString();
|
||||
return false;
|
||||
}
|
||||
file.write(QJsonDocument(obj).toJson());
|
||||
file.close();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
class LiteLoaderInstallTask : public Task
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
LiteLoaderInstallTask(LiteLoaderInstaller *installer, OneSixInstance *instance,
|
||||
BaseVersionPtr version, QObject *parent)
|
||||
: Task(parent), m_installer(installer), m_instance(instance), m_version(version)
|
||||
{
|
||||
}
|
||||
|
||||
protected:
|
||||
void executeTask() override
|
||||
{
|
||||
LiteLoaderVersionPtr liteloaderVersion =
|
||||
std::dynamic_pointer_cast<LiteLoaderVersion>(m_version);
|
||||
if (!liteloaderVersion)
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_installer->prepare(liteloaderVersion);
|
||||
if (!m_installer->add(m_instance))
|
||||
{
|
||||
emitFailed(tr("For reasons unknown, the LiteLoader installation failed. Check your "
|
||||
"MultiMC log files for details."));
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
m_instance->reloadProfile();
|
||||
emitSucceeded();
|
||||
}
|
||||
catch (Exception &e)
|
||||
{
|
||||
emitFailed(e.cause());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
emitFailed(
|
||||
tr("Failed to load the version description file for reasons unknown."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
LiteLoaderInstaller *m_installer;
|
||||
OneSixInstance *m_instance;
|
||||
BaseVersionPtr m_version;
|
||||
};
|
||||
|
||||
Task *LiteLoaderInstaller::createInstallTask(OneSixInstance *instance,
|
||||
BaseVersionPtr version,
|
||||
QObject *parent)
|
||||
{
|
||||
return new LiteLoaderInstallTask(this, instance, version, parent);
|
||||
}
|
||||
|
||||
#include "LiteLoaderInstaller.moc"
|
39
logic/minecraft/liteloader/LiteLoaderInstaller.h
Normal file
39
logic/minecraft/liteloader/LiteLoaderInstaller.h
Normal file
@ -0,0 +1,39 @@
|
||||
/* Copyright 2013-2015 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QMap>
|
||||
|
||||
#include "BaseInstaller.h"
|
||||
#include "LiteLoaderVersionList.h"
|
||||
|
||||
#include "multimc_logic_export.h"
|
||||
|
||||
class MULTIMC_LOGIC_EXPORT LiteLoaderInstaller : public BaseInstaller
|
||||
{
|
||||
public:
|
||||
LiteLoaderInstaller();
|
||||
|
||||
void prepare(LiteLoaderVersionPtr version);
|
||||
bool add(OneSixInstance *to) override;
|
||||
virtual QString id() const override { return "com.mumfrey.liteloader"; }
|
||||
|
||||
Task *createInstallTask(OneSixInstance *instance, BaseVersionPtr version, QObject *parent) override;
|
||||
|
||||
private:
|
||||
LiteLoaderVersionPtr m_version;
|
||||
};
|
275
logic/minecraft/liteloader/LiteLoaderVersionList.cpp
Normal file
275
logic/minecraft/liteloader/LiteLoaderVersionList.cpp
Normal file
@ -0,0 +1,275 @@
|
||||
/* Copyright 2013-2015 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "LiteLoaderVersionList.h"
|
||||
#include "Env.h"
|
||||
#include "net/URLConstants.h"
|
||||
#include "Exception.h"
|
||||
|
||||
#include <QtXml>
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonValue>
|
||||
#include <QJsonParseError>
|
||||
|
||||
#include <QtAlgorithms>
|
||||
|
||||
#include <QtNetwork>
|
||||
|
||||
LiteLoaderVersionList::LiteLoaderVersionList(QObject *parent) : BaseVersionList(parent)
|
||||
{
|
||||
}
|
||||
|
||||
Task *LiteLoaderVersionList::getLoadTask()
|
||||
{
|
||||
return new LLListLoadTask(this);
|
||||
}
|
||||
|
||||
bool LiteLoaderVersionList::isLoaded()
|
||||
{
|
||||
return m_loaded;
|
||||
}
|
||||
|
||||
const BaseVersionPtr LiteLoaderVersionList::at(int i) const
|
||||
{
|
||||
return m_vlist.at(i);
|
||||
}
|
||||
|
||||
int LiteLoaderVersionList::count() const
|
||||
{
|
||||
return m_vlist.count();
|
||||
}
|
||||
|
||||
static bool cmpVersions(BaseVersionPtr first, BaseVersionPtr second)
|
||||
{
|
||||
auto left = std::dynamic_pointer_cast<LiteLoaderVersion>(first);
|
||||
auto right = std::dynamic_pointer_cast<LiteLoaderVersion>(second);
|
||||
return left->timestamp > right->timestamp;
|
||||
}
|
||||
|
||||
void LiteLoaderVersionList::sortVersions()
|
||||
{
|
||||
beginResetModel();
|
||||
std::sort(m_vlist.begin(), m_vlist.end(), cmpVersions);
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
QVariant LiteLoaderVersionList::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return QVariant();
|
||||
|
||||
if (index.row() > count())
|
||||
return QVariant();
|
||||
|
||||
auto version = std::dynamic_pointer_cast<LiteLoaderVersion>(m_vlist[index.row()]);
|
||||
switch (role)
|
||||
{
|
||||
case VersionPointerRole:
|
||||
return qVariantFromValue(m_vlist[index.row()]);
|
||||
|
||||
case VersionRole:
|
||||
return version->name();
|
||||
|
||||
case VersionIdRole:
|
||||
return version->descriptor();
|
||||
|
||||
case ParentGameVersionRole:
|
||||
return version->mcVersion;
|
||||
|
||||
case RecommendedRole:
|
||||
return version->isLatest;
|
||||
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
QList<BaseVersionList::ModelRoles> LiteLoaderVersionList::providesRoles()
|
||||
{
|
||||
return {VersionPointerRole, VersionRole, VersionIdRole, ParentGameVersionRole, RecommendedRole};
|
||||
}
|
||||
|
||||
BaseVersionPtr LiteLoaderVersionList::getLatestStable() const
|
||||
{
|
||||
for (int i = 0; i < m_vlist.length(); i++)
|
||||
{
|
||||
auto ver = std::dynamic_pointer_cast<LiteLoaderVersion>(m_vlist.at(i));
|
||||
if (ver->isLatest)
|
||||
{
|
||||
return m_vlist.at(i);
|
||||
}
|
||||
}
|
||||
return BaseVersionPtr();
|
||||
}
|
||||
|
||||
void LiteLoaderVersionList::updateListData(QList<BaseVersionPtr> versions)
|
||||
{
|
||||
beginResetModel();
|
||||
m_vlist = versions;
|
||||
m_loaded = true;
|
||||
std::sort(m_vlist.begin(), m_vlist.end(), cmpVersions);
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
LLListLoadTask::LLListLoadTask(LiteLoaderVersionList *vlist)
|
||||
{
|
||||
m_list = vlist;
|
||||
}
|
||||
|
||||
LLListLoadTask::~LLListLoadTask()
|
||||
{
|
||||
}
|
||||
|
||||
void LLListLoadTask::executeTask()
|
||||
{
|
||||
setStatus(tr("Loading LiteLoader version list..."));
|
||||
auto job = new NetJob("Version index");
|
||||
// we do not care if the version is stale or not.
|
||||
auto liteloaderEntry = ENV.metacache()->resolveEntry("liteloader", "versions.json");
|
||||
|
||||
// verify by poking the server.
|
||||
liteloaderEntry->stale = true;
|
||||
|
||||
job->addNetAction(listDownload = CacheDownload::make(QUrl(URLConstants::LITELOADER_URL),
|
||||
liteloaderEntry));
|
||||
|
||||
connect(listDownload.get(), SIGNAL(failed(int)), SLOT(listFailed()));
|
||||
|
||||
listJob.reset(job);
|
||||
connect(listJob.get(), SIGNAL(succeeded()), SLOT(listDownloaded()));
|
||||
connect(listJob.get(), SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64)));
|
||||
listJob->start();
|
||||
}
|
||||
|
||||
void LLListLoadTask::listFailed()
|
||||
{
|
||||
emitFailed("Failed to load LiteLoader version list.");
|
||||
return;
|
||||
}
|
||||
|
||||
void LLListLoadTask::listDownloaded()
|
||||
{
|
||||
QByteArray data;
|
||||
{
|
||||
auto dlJob = listDownload;
|
||||
auto filename = std::dynamic_pointer_cast<CacheDownload>(dlJob)->getTargetFilepath();
|
||||
QFile listFile(filename);
|
||||
if (!listFile.open(QIODevice::ReadOnly))
|
||||
{
|
||||
emitFailed("Failed to open the LiteLoader version list.");
|
||||
return;
|
||||
}
|
||||
data = listFile.readAll();
|
||||
listFile.close();
|
||||
dlJob.reset();
|
||||
}
|
||||
|
||||
QJsonParseError jsonError;
|
||||
QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError);
|
||||
|
||||
if (jsonError.error != QJsonParseError::NoError)
|
||||
{
|
||||
emitFailed("Error parsing version list JSON:" + jsonError.errorString());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!jsonDoc.isObject())
|
||||
{
|
||||
emitFailed("Error parsing version list JSON: jsonDoc is not an object");
|
||||
return;
|
||||
}
|
||||
|
||||
const QJsonObject root = jsonDoc.object();
|
||||
|
||||
// Now, get the array of versions.
|
||||
if (!root.value("versions").isObject())
|
||||
{
|
||||
emitFailed("Error parsing version list JSON: missing 'versions' object");
|
||||
return;
|
||||
}
|
||||
|
||||
auto meta = root.value("meta").toObject();
|
||||
QString description = meta.value("description").toString(tr("This is a lightweight loader for mods that don't change game mechanics."));
|
||||
QString defaultUrl = meta.value("url").toString("http://dl.liteloader.com");
|
||||
QString authors = meta.value("authors").toString("Mumfrey");
|
||||
auto versions = root.value("versions").toObject();
|
||||
|
||||
QList<BaseVersionPtr> tempList;
|
||||
for (auto vIt = versions.begin(); vIt != versions.end(); ++vIt)
|
||||
{
|
||||
const QString mcVersion = vIt.key();
|
||||
QString latest;
|
||||
const QJsonObject artefacts = vIt.value()
|
||||
.toObject()
|
||||
.value("artefacts")
|
||||
.toObject()
|
||||
.value("com.mumfrey:liteloader")
|
||||
.toObject();
|
||||
QList<BaseVersionPtr> perMcVersionList;
|
||||
for (auto aIt = artefacts.begin(); aIt != artefacts.end(); ++aIt)
|
||||
{
|
||||
const QString identifier = aIt.key();
|
||||
const QJsonObject artefact = aIt.value().toObject();
|
||||
if (identifier == "latest")
|
||||
{
|
||||
latest = artefact.value("version").toString();
|
||||
continue;
|
||||
}
|
||||
LiteLoaderVersionPtr version(new LiteLoaderVersion());
|
||||
version->version = artefact.value("version").toString();
|
||||
version->file = artefact.value("file").toString();
|
||||
version->mcVersion = mcVersion;
|
||||
version->md5 = artefact.value("md5").toString();
|
||||
version->timestamp = artefact.value("timestamp").toString().toInt();
|
||||
version->tweakClass = artefact.value("tweakClass").toString();
|
||||
version->authors = authors;
|
||||
version->description = description;
|
||||
version->defaultUrl = defaultUrl;
|
||||
const QJsonArray libs = artefact.value("libraries").toArray();
|
||||
for (auto lIt = libs.begin(); lIt != libs.end(); ++lIt)
|
||||
{
|
||||
auto libobject = (*lIt).toObject();
|
||||
try
|
||||
{
|
||||
auto lib = RawLibrary::fromJson(libobject, "versions.json");
|
||||
// hack to make liteloader 1.7.10_00 work
|
||||
if(lib->rawName() == GradleSpecifier("org.ow2.asm:asm-all:5.0.3"))
|
||||
{
|
||||
lib->setBaseUrl("http://repo.maven.apache.org/maven2/");
|
||||
}
|
||||
version->libraries.append(lib);
|
||||
}
|
||||
catch (Exception &e)
|
||||
{
|
||||
qCritical() << "Couldn't read JSON object:";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
perMcVersionList.append(version);
|
||||
}
|
||||
for (auto version : perMcVersionList)
|
||||
{
|
||||
auto v = std::dynamic_pointer_cast<LiteLoaderVersion>(version);
|
||||
v->isLatest = v->version == latest;
|
||||
}
|
||||
tempList.append(perMcVersionList);
|
||||
}
|
||||
m_list->updateListData(tempList);
|
||||
|
||||
emitSucceeded();
|
||||
}
|
119
logic/minecraft/liteloader/LiteLoaderVersionList.h
Normal file
119
logic/minecraft/liteloader/LiteLoaderVersionList.h
Normal file
@ -0,0 +1,119 @@
|
||||
/* Copyright 2013-2015 MultiMC Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include "BaseVersion.h"
|
||||
#include "BaseVersionList.h"
|
||||
#include "tasks/Task.h"
|
||||
#include "net/NetJob.h"
|
||||
#include <minecraft/RawLibrary.h>
|
||||
|
||||
#include "multimc_logic_export.h"
|
||||
|
||||
class LLListLoadTask;
|
||||
class QNetworkReply;
|
||||
|
||||
class LiteLoaderVersion : public BaseVersion
|
||||
{
|
||||
public:
|
||||
QString descriptor() override
|
||||
{
|
||||
if (isLatest)
|
||||
{
|
||||
return QObject::tr("Latest");
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
QString typeString() const override
|
||||
{
|
||||
return mcVersion;
|
||||
}
|
||||
QString name() override
|
||||
{
|
||||
return version;
|
||||
}
|
||||
|
||||
// important info
|
||||
QString version;
|
||||
QString file;
|
||||
QString mcVersion;
|
||||
QString md5;
|
||||
int timestamp;
|
||||
bool isLatest;
|
||||
QString tweakClass;
|
||||
QList<RawLibraryPtr> libraries;
|
||||
|
||||
// meta
|
||||
QString defaultUrl;
|
||||
QString description;
|
||||
QString authors;
|
||||
};
|
||||
typedef std::shared_ptr<LiteLoaderVersion> LiteLoaderVersionPtr;
|
||||
|
||||
class MULTIMC_LOGIC_EXPORT LiteLoaderVersionList : public BaseVersionList
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
friend class LLListLoadTask;
|
||||
|
||||
explicit LiteLoaderVersionList(QObject *parent = 0);
|
||||
|
||||
virtual Task *getLoadTask();
|
||||
virtual bool isLoaded();
|
||||
virtual const BaseVersionPtr at(int i) const;
|
||||
virtual int count() const;
|
||||
virtual void sortVersions();
|
||||
virtual QVariant data ( const QModelIndex & index, int role = Qt::DisplayRole ) const;
|
||||
virtual QList< ModelRoles > providesRoles();
|
||||
|
||||
virtual BaseVersionPtr getLatestStable() const;
|
||||
|
||||
protected:
|
||||
QList<BaseVersionPtr> m_vlist;
|
||||
|
||||
bool m_loaded = false;
|
||||
|
||||
protected
|
||||
slots:
|
||||
virtual void updateListData(QList<BaseVersionPtr> versions);
|
||||
};
|
||||
|
||||
class LLListLoadTask : public Task
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit LLListLoadTask(LiteLoaderVersionList *vlist);
|
||||
~LLListLoadTask();
|
||||
|
||||
virtual void executeTask();
|
||||
|
||||
protected
|
||||
slots:
|
||||
void listDownloaded();
|
||||
void listFailed();
|
||||
|
||||
protected:
|
||||
NetJobPtr listJob;
|
||||
CacheDownloadPtr listDownload;
|
||||
LiteLoaderVersionList *m_list;
|
||||
};
|
||||
|
||||
Q_DECLARE_METATYPE(LiteLoaderVersionPtr)
|
@ -16,20 +16,20 @@
|
||||
#include <QIcon>
|
||||
#include <QDebug>
|
||||
|
||||
#include "minecraft/OneSixInstance.h"
|
||||
#include "OneSixInstance.h"
|
||||
#include "OneSixUpdate.h"
|
||||
#include "OneSixProfileStrategy.h"
|
||||
|
||||
#include "minecraft/OneSixUpdate.h"
|
||||
#include "minecraft/MinecraftProfile.h"
|
||||
#include "minecraft/VersionBuildError.h"
|
||||
#include "launch/LaunchTask.h"
|
||||
#include <launch/steps/PreLaunchCommand.h>
|
||||
#include <launch/steps/Update.h>
|
||||
#include <launch/steps/LaunchMinecraft.h>
|
||||
#include <launch/steps/PostLaunchCommand.h>
|
||||
#include <launch/steps/TextPrint.h>
|
||||
#include <launch/steps/ModMinecraftJar.h>
|
||||
#include <launch/steps/CheckJava.h>
|
||||
#include "minecraft/OneSixProfileStrategy.h"
|
||||
#include "launch/steps/PreLaunchCommand.h"
|
||||
#include "launch/steps/Update.h"
|
||||
#include "launch/steps/LaunchMinecraft.h"
|
||||
#include "launch/steps/PostLaunchCommand.h"
|
||||
#include "launch/steps/TextPrint.h"
|
||||
#include "launch/steps/ModMinecraftJar.h"
|
||||
#include "launch/steps/CheckJava.h"
|
||||
#include "MMCZip.h"
|
||||
|
||||
#include "minecraft/AssetsUtils.h"
|
@ -1,6 +1,7 @@
|
||||
#include "minecraft/OneSixProfileStrategy.h"
|
||||
#include "OneSixProfileStrategy.h"
|
||||
#include "OneSixInstance.h"
|
||||
|
||||
#include "minecraft/VersionBuildError.h"
|
||||
#include "minecraft/OneSixInstance.h"
|
||||
#include "minecraft/MinecraftVersionList.h"
|
||||
#include "Env.h"
|
||||
#include <FileSystem.h>
|
@ -1,5 +1,5 @@
|
||||
#pragma once
|
||||
#include "ProfileStrategy.h"
|
||||
#include "minecraft/ProfileStrategy.h"
|
||||
|
||||
class OneSixInstance;
|
||||
|
@ -15,6 +15,7 @@
|
||||
|
||||
#include "Env.h"
|
||||
#include "OneSixUpdate.h"
|
||||
#include "OneSixInstance.h"
|
||||
|
||||
#include <QtNetwork>
|
||||
|
||||
@ -28,8 +29,7 @@
|
||||
#include "minecraft/MinecraftVersionList.h"
|
||||
#include "minecraft/MinecraftProfile.h"
|
||||
#include "minecraft/RawLibrary.h"
|
||||
#include "minecraft/OneSixInstance.h"
|
||||
#include "forge/ForgeMirrors.h"
|
||||
#include "minecraft/forge/ForgeMirrors.h"
|
||||
#include "net/URLConstants.h"
|
||||
#include "minecraft/AssetsUtils.h"
|
||||
#include "Exception.h"
|
Reference in New Issue
Block a user