NOISSUE move files into paths that make more sense

This commit is contained in:
Petr Mrázek
2016-02-27 19:58:40 +01:00
parent 71e4b147ec
commit 17ad1e64f8
75 changed files with 148 additions and 144 deletions

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View 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:
};

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

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

View File

@ -0,0 +1,10 @@
#pragma once
#include <QString>
struct ForgeMirror
{
QString name;
QString logo_url;
QString website_url;
QString mirror_url;
};

View 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()
{
}

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

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

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

View 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.";
}
}

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

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

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

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

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

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

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

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

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

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

View 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");
}

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

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

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
#pragma once
#include "ProfileStrategy.h"
#include "minecraft/ProfileStrategy.h"
class OneSixInstance;

View File

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