Legacy jar reassembly, base of proper custom jar support

This commit is contained in:
Petr Mrázek 2013-08-24 03:09:46 +02:00
parent b781231666
commit e3b55067eb
18 changed files with 286 additions and 151 deletions

View File

@ -1,7 +1,7 @@
#include "JlCompress.h"
#include <QDebug>
static bool copyData(QIODevice &inFile, QIODevice &outFile)
bool JlCompress::copyData(QIODevice &inFile, QIODevice &outFile)
{
while (!inFile.atEnd()) {
char buf[4096];

View File

@ -51,6 +51,8 @@ private:
static bool removeFile(QStringList listFile);
public:
/// copy data from inFile to outFile
static bool copyData(QIODevice &inFile, QIODevice &outFile);
/// Compress a single file.
/**
\param fileCompressed The name of the archive.

View File

@ -29,7 +29,17 @@ LIBUTIL_EXPORT QString RemoveInvalidFilenameChars(QString string, QChar replaceW
LIBUTIL_EXPORT QString DirNameFromString(QString string, QString inDir = ".");
LIBUTIL_EXPORT bool ensurePathExists(QString filenamepath);
/**
* Creates all the folders in a path for the specified path
* last segment of the path is treated as a file name and is ignored!
*/
LIBUTIL_EXPORT bool ensureFilePathExists(QString filenamepath);
/**
* Creates all the folders in a path for the specified path
* last segment of the path is treated as a folder name and is created!
*/
LIBUTIL_EXPORT bool ensureFolderPathExists(QString filenamepath);
LIBUTIL_EXPORT bool copyPath(QString src, QString dst);

View File

@ -19,6 +19,7 @@
#include <QDir>
#include <QDesktopServices>
#include <QUrl>
#include <QDebug>
QString PathCombine(QString path1, QString path2)
{
@ -68,24 +69,38 @@ QString DirNameFromString(QString string, QString inDir)
return dirName;
}
bool ensurePathExists(QString filenamepath)
bool ensureFilePathExists(QString filenamepath)
{
QFileInfo a ( filenamepath );
QDir dir;
return (dir.mkpath ( a.filePath() ));
QString ensuredPath = a.path();
bool success = dir.mkpath ( ensuredPath );
qDebug() << "ensureFilePathExists:" << success << ensuredPath << filenamepath;
return success;
}
bool ensureFolderPathExists(QString foldernamepath)
{
QFileInfo a ( foldernamepath );
QDir dir;
QString ensuredPath = a.filePath();
bool success = dir.mkpath ( ensuredPath );
qDebug() << "ensureFolderPathExists:" << success << ensuredPath << foldernamepath;
return success;
}
bool copyPath(QString src, QString dst)
{
QDir dir(src);
if (!dir.exists())
return false;
if(!ensurePathExists(dst))
if(!ensureFolderPathExists(dst))
return false;
foreach (QString d, dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot))
{
QString inner_src = src+ QDir::separator() + d;
QString inner_src = src + QDir::separator() + d;
QString inner_dst = dst + QDir::separator() + d;
copyPath(inner_src, inner_dst);
}

View File

@ -28,9 +28,9 @@ LegacyModEditDialog::LegacyModEditDialog( LegacyInstance* inst, QWidget* parent
ui(new Ui::LegacyModEditDialog)
{
ui->setupUi(this);
ensurePathExists(m_inst->coreModsDir());
ensurePathExists(m_inst->mlModsDir());
ensurePathExists(m_inst->jarModsDir());
ensureFolderPathExists(m_inst->coreModsDir());
ensureFolderPathExists(m_inst->mlModsDir());
ensureFolderPathExists(m_inst->jarModsDir());
m_mods = m_inst->loaderModList();
m_coremods = m_inst->coreModList();

View File

@ -42,6 +42,15 @@ BaseInstance::BaseInstance( BaseInstancePrivate* d_in,
settings().registerSetting(new Setting("notes", ""));
settings().registerSetting(new Setting("lastLaunchTime", 0));
/*
* custom base jar has no default. it is determined in code... see the accessor methods for it
*
* for instances that DO NOT have the CustomBaseJar setting (legacy instances),
* [.]minecraft/bin/mcbackup.jar is the default base jar
*/
settings().registerSetting(new Setting("UseCustomBaseJar", true));
settings().registerSetting(new Setting("CustomBaseJar", ""));
// Java Settings
settings().registerSetting(new Setting("OverrideJava", false));
settings().registerSetting(new OverrideSetting("JavaPath", globalSettings->getSetting("JavaPath")));
@ -121,6 +130,51 @@ SettingsObject &BaseInstance::settings() const
return *d->m_settings;
}
QString BaseInstance::baseJar() const
{
I_D(BaseInstance);
bool customJar = d->m_settings->get("UseCustomBaseJar").toBool();
if(customJar)
{
return customBaseJar();
}
else
return defaultBaseJar();
}
QString BaseInstance::customBaseJar() const
{
I_D(BaseInstance);
QString value = d->m_settings->get ( "CustomBaseJar" ).toString();
if(value.isNull() || value.isEmpty())
{
return defaultCustomBaseJar();
}
return value;
}
void BaseInstance::setCustomBaseJar ( QString val )
{
I_D(BaseInstance);
if(val.isNull() || val.isEmpty() || val == defaultCustomBaseJar())
d->m_settings->reset ( "CustomBaseJar" );
else
d->m_settings->set ( "CustomBaseJar", val );
}
void BaseInstance::setShouldUseCustomBaseJar ( bool val )
{
I_D(BaseInstance);
d->m_settings->set ( "UseCustomBaseJar", val );
}
bool BaseInstance::shouldUseCustomBaseJar() const
{
I_D(BaseInstance);
return d->m_settings->get ( "UseCustomBaseJar" ).toBool();
}
qint64 BaseInstance::lastLaunch() const
{
I_D(BaseInstance);

View File

@ -89,6 +89,25 @@ public:
*/
virtual bool shouldUpdate() const = 0;
virtual void setShouldUpdate(bool val) = 0;
/// Get the curent base jar of this instance. By default, it's the versions/$version/$version.jar
QString baseJar() const;
/// the default base jar of this instance
virtual QString defaultBaseJar() const = 0;
/// the default custom base jar of this instance
virtual QString defaultCustomBaseJar() const = 0;
/*!
* Whether or not custom base jar is used
*/
bool shouldUseCustomBaseJar() const;
void setShouldUseCustomBaseJar(bool val);
/*!
* The value of the custom base jar
*/
QString customBaseJar() const;
void setCustomBaseJar(QString val);
/**
* Gets the time that the instance was last launched.

View File

@ -39,8 +39,8 @@ IconList::IconList() : QAbstractListModel(), d(new Private())
}
// FIXME: get from settings
ensurePathExists("icons/");
QDir user_icons("icons/");
ensureFolderPathExists("icons");
QDir user_icons("icons");
file_info_list = user_icons.entryInfoList(QDir::Files, QDir::Name);
for(auto file_info: file_info_list)
{

View File

@ -90,16 +90,19 @@ InstanceFactory::InstCreateError InstanceFactory::createInstance( BaseInstance*&
m_settings->set("InstanceType", "Legacy");
inst = new LegacyInstance(instDir, m_settings, this);
inst->setIntendedVersionId(version->descriptor);
inst->setShouldUseCustomBaseJar(false);
break;
case MinecraftVersion::OneSix:
m_settings->set("InstanceType", "OneSix");
inst = new OneSixInstance(instDir, m_settings, this);
inst->setIntendedVersionId(version->descriptor);
inst->setShouldUseCustomBaseJar(false);
break;
case MinecraftVersion::Nostalgia:
m_settings->set("InstanceType", "Nostalgia");
inst = new NostalgiaInstance(instDir, m_settings, this);
inst->setIntendedVersionId(version->descriptor);
inst->setShouldUseCustomBaseJar(false);
break;
default:
{

View File

@ -169,30 +169,25 @@ QString LegacyInstance::resourceDir() const
return PathCombine(minecraftRoot(), "resources");
}
QString LegacyInstance::mcJar() const
QString LegacyInstance::runnableJar() const
{
return PathCombine(binDir(), "minecraft.jar");
}
QString LegacyInstance::mcBackup() const
{
return PathCombine(binDir(), "mcbackup.jar");
}
QString LegacyInstance::modListFile() const
{
return PathCombine(instanceRoot(), "modlist");
}
/*
bool LegacyInstance::shouldUpdateCurrentVersion() const
{
QFileInfo jar(mcJar());
QFileInfo jar(runnableJar());
return jar.lastModified().toUTC().toMSecsSinceEpoch() != lastCurrentVersionUpdate();
}
void LegacyInstance::updateCurrentVersion(bool keepCurrent)
{
QFileInfo jar(mcJar());
QFileInfo jar(runnableJar());
if(!jar.exists())
{
@ -221,6 +216,7 @@ void LegacyInstance::setLastCurrentVersionUpdate ( qint64 val )
I_D(LegacyInstance);
d->m_settings->set ( "lastVersionUpdate", val );
}
*/
bool LegacyInstance::shouldRebuild() const
{
I_D(LegacyInstance);
@ -278,3 +274,14 @@ void LegacyInstance::setShouldUpdate ( bool val )
{
settings().set ( "ShouldUpdate", val );
}
QString LegacyInstance::defaultBaseJar() const
{
return "versions/" + intendedVersionId() + "/" + intendedVersionId() + ".jar";
}
QString LegacyInstance::defaultCustomBaseJar() const
{
return PathCombine(binDir(), "mcbackup.jar");
}

View File

@ -13,10 +13,7 @@ public:
explicit LegacyInstance(const QString &rootDir, SettingsObject * settings, QObject *parent = 0);
/// Path to the instance's minecraft.jar
QString mcJar() const;
//! Path to the instance's mcbackup.jar
QString mcBackup() const;
QString runnableJar() const;
//! Path to the instance's modlist file.
QString modListFile() const;
@ -34,35 +31,6 @@ public:
QString coreModsDir() const;
QString resourceDir() const;
/*!
* \brief Checks whether or not the currentVersion of the instance needs to be updated.
* If this returns true, updateCurrentVersion is called. In the
* standard instance, this is determined by checking a timestamp
* stored in the instance config file against the last modified time of Minecraft.jar.
* \return True if updateCurrentVersion() should be called.
*/
bool shouldUpdateCurrentVersion() const;
/*!
* \brief Updates the current version.
* This function should first set the current version timestamp
* (setCurrentVersionTimestamp()) to the current time. Next, if
* keepCurrent is false, this function should check what the
* instance's current version is and call setCurrentVersion() to
* update it. This function will automatically be called when the
* instance is loaded if shouldUpdateCurrentVersion returns true.
* \param keepCurrent If true, only the version timestamp will be updated.
*/
void updateCurrentVersion(bool keepCurrent = false);
/*!
* Gets the last time that the current version was checked.
* This is checked against the last modified time on the jar file to see if
* the current version needs to be checked again.
*/
qint64 lastCurrentVersionUpdate() const;
void setLastCurrentVersionUpdate(qint64 val);
/*!
* Whether or not the instance's minecraft.jar needs to be rebuilt.
* If this is true, when the instance launches, its jar mods will be
@ -90,6 +58,9 @@ public:
virtual void cleanupAfterRun();
virtual QDialog * createModEditDialog ( QWidget* parent );
virtual QString defaultBaseJar() const;
virtual QString defaultCustomBaseJar() const;
protected slots:
virtual void jarModsChanged();
};

View File

@ -4,9 +4,11 @@
#include "BaseInstance.h"
#include "LegacyInstance.h"
#include "net/NetWorker.h"
#include "ModList.h"
#include <pathutils.h>
#include <quazip.h>
#include <quazipfile.h>
#include <JlCompress.h>
LegacyUpdate::LegacyUpdate ( BaseInstance* inst, QObject* parent ) : BaseUpdate ( inst, parent ) {}
@ -22,7 +24,7 @@ void LegacyUpdate::lwjglStart()
lwjglVersion = inst->lwjglVersion();
lwjglTargetPath = PathCombine("lwjgl", lwjglVersion );
lwjglNativesPath = PathCombine( lwjglTargetPath, "natives/");
lwjglNativesPath = PathCombine( lwjglTargetPath, "natives");
// if the 'done' file exists, we don't have to download this again
QFileInfo doneFile(PathCombine(lwjglTargetPath, "done"));
@ -112,7 +114,7 @@ void LegacyUpdate::extractLwjgl()
{
// make sure the directories are there
bool success = ensurePathExists(lwjglNativesPath);
bool success = ensureFolderPathExists(lwjglNativesPath);
if(!success)
{
@ -201,33 +203,14 @@ void LegacyUpdate::lwjglFailed()
void LegacyUpdate::jarStart()
{
setStatus("Checking ...");
LegacyInstance * inst = (LegacyInstance *) m_inst;
QString current_version_id = inst->currentVersionId();
QString intended_version_id = inst->intendedVersionId();
bool shouldUpdate = inst->shouldUpdate();
if(!shouldUpdate)
if(!inst->shouldUpdate() || inst->shouldUseCustomBaseJar())
{
emitSucceeded();
ModTheJar();
return;
}
// nuke the backup file, we are replacing the base jar anyway
QFile mc_backup(inst->mcBackup());
if (mc_backup.exists())
{
mc_backup.remove();
}
// Get a pointer to the version object that corresponds to the instance's version.
auto targetVersion = MinecraftVersionList::getMainList().findVersion(intended_version_id);
if(!targetVersion)
{
emitFailed("Not a valid version:" + intended_version_id);
return;
}
setStatus("Checking for jar updates...");
// Make directories
QDir binDir(inst->binDir());
if (!binDir.exists() && !binDir.mkpath("."))
@ -239,14 +222,11 @@ void LegacyUpdate::jarStart()
// Build a list of URLs that will need to be downloaded.
setStatus("Downloading new minecraft.jar");
// This will be either 'minecraft' or the version number, depending on where
// we're downloading from.
QString jarFilename = "minecraft";
QString download_path = PathCombine(inst->minecraftRoot(), "bin/minecraft.jar");
QString urlstr("http://s3.amazonaws.com/Minecraft.Download/versions/");
urlstr += targetVersion->descriptor + "/" + targetVersion->descriptor + ".jar";
auto dljob = DownloadJob::create(QUrl(urlstr), download_path);
QString intended_version_id = inst->intendedVersionId();
urlstr += intended_version_id + "/" + intended_version_id + ".jar";
auto dljob = DownloadJob::create(QUrl(urlstr), inst->defaultBaseJar());
legacyDownloadJob.reset(new JobList());
legacyDownloadJob->add(dljob);
@ -259,7 +239,7 @@ void LegacyUpdate::jarStart()
void LegacyUpdate::jarFinished()
{
// process the jar
emitSucceeded();
ModTheJar();
}
void LegacyUpdate::jarFailed()
@ -268,74 +248,132 @@ void LegacyUpdate::jarFailed()
emitFailed("Failed to download the minecraft jar. Try again later.");
}
bool LegacyUpdate::MergeZipFiles( QuaZip* into, QFileInfo from, QSet< QString >& contained )
{
setStatus("Installing mods - Adding " + from.fileName());
QuaZip modZip(from.filePath());
modZip.open(QuaZip::mdUnzip);
QuaZipFile fileInsideMod(&modZip);
QuaZipFile zipOutFile( into );
for(bool more=modZip.goToFirstFile(); more; more=modZip.goToNextFile())
{
QString filename = modZip.getCurrentFileName();
if(filename.contains("META-INF"))
continue;
if(contained.contains(filename))
continue;
contained.insert(filename);
qDebug() << "Adding file " << filename << " from " << from.fileName();
if(!fileInsideMod.open(QIODevice::ReadOnly))
{
return false;
}
if(!zipOutFile.open(QIODevice::WriteOnly, QuaZipNewInfo(fileInsideMod.getActualFileName())))
{
fileInsideMod.close();
return false;
}
if(!JlCompress::copyData(fileInsideMod, zipOutFile))
{
zipOutFile.close();
fileInsideMod.close();
return false;
}
zipOutFile.close();
fileInsideMod.close();
}
return true;
}
void LegacyUpdate::ModTheJar()
{
/*
LegacyInstance * inst = (LegacyInstance *) m_inst;
// Get the mod list
auto modList = inst->getJarModList();
QFileInfo mcBin(inst->binDir());
QFileInfo mcJar(inst->mcJar());
QFileInfo mcBackup(inst->mcBackup());
// Nothing to do if there are no jar mods to install, no backup and just the mc jar
if(mcJar.isFile() && !mcBackup.exists() && modList->empty())
if(!inst->shouldRebuild())
{
inst->setShouldRebuild(false);
emitSucceeded();
return;
}
setStatus("Installing mods - backing up minecraft.jar...");
if (!mcBackup.exists() && !QFile::copy(mcJar.absoluteFilePath(), mcBackup.absoluteFilePath()) )
// Get the mod list
auto modList = inst->jarModList();
QFileInfo runnableJar (inst->runnableJar());
QFileInfo baseJar (inst->baseJar());
bool base_is_custom = inst->shouldUseCustomBaseJar();
// Nothing to do if there are no jar mods to install, no backup and just the mc jar
if(base_is_custom)
{
emitFailed("Failed to back up minecraft.jar");
// yes, this can happen if the instance only has the runnable jar and not the base jar
// it *could* be assumed that such an instance is vanilla, but that wouldn't be safe
// because that's not something mmc4 guarantees
if(runnableJar.isFile() && !baseJar.exists() && modList->empty())
{
inst->setShouldRebuild(false);
emitSucceeded();
return;
}
setStatus("Installing mods - backing up minecraft.jar...");
if (!baseJar.exists() && !QFile::copy(runnableJar.filePath(), baseJar.filePath()))
{
emitFailed("Failed to back up minecraft.jar");
return;
}
}
if (!baseJar.exists())
{
emitFailed("The base jar " + baseJar.filePath() + " does not exist");
return;
}
if (mcJar.isFile() && !QFile::remove(mcJar.absoluteFilePath()))
if (runnableJar.exists() && !QFile::remove(runnableJar.filePath()))
{
emitFailed("Failed to delete old minecraft.jar");
return;
}
//TaskStep(); // STEP 1
setStatus("Installing mods - Opening minecraft.jar");
wxFFileOutputStream jarStream(mcJar.absoluteFilePath());
wxZipOutputStream zipOut(jarStream);
QuaZip zipOut(runnableJar.filePath());
if(!zipOut.open(QuaZip::mdCreate))
{
QFile::remove(runnableJar.filePath());
emitFailed("Failed to open the minecraft.jar for modding");
return;
}
// Files already added to the jar.
// These files will be skipped.
QSet<QString> addedFiles;
// Modify the jar
setStatus("Installing mods - Adding mod files...");
for (ModList::const_reverse_iterator iter = modList->rbegin(); iter != modList->rend(); iter++)
for (int i = modList->size() - 1; i >= 0; i--)
{
wxFileName modFileName = iter->GetFileName();
setStatus("Installing mods - Adding " + modFileName.GetFullName());
if (iter->GetModType() == Mod::ModType::MOD_ZIPFILE)
auto &mod = modList->operator[](i);
if (mod.type() == Mod::MOD_ZIPFILE)
{
wxFFileInputStream modStream(modFileName.GetFullPath());
wxZipInputStream zipStream(modStream);
std::unique_ptr<wxZipEntry> entry;
while (entry.reset(zipStream.GetNextEntry()), entry.get() != NULL)
if(!MergeZipFiles(&zipOut, mod.filename(), addedFiles))
{
if (entry->IsDir())
continue;
wxString name = entry->GetName();
if (addedFiles.count(name) == 0)
{
if (!zipOut.CopyEntry(entry.release(), zipStream))
break;
addedFiles.insert(name);
}
zipOut.close();
QFile::remove(runnableJar.filePath());
emitFailed("Failed to add " + mod.filename().fileName() + " to the jar.");
return;
}
}
else
else if (mod.type() == Mod::MOD_SINGLEFILE)
{
zipOut.close();
QFile::remove(runnableJar.filePath());
emitFailed("Loose files are NOT supported as jar mods.");
return;
/*
wxFileName destFileName = modFileName;
destFileName.MakeRelativeTo(m_inst->GetInstModsDir().GetFullPath());
wxString destFile = destFileName.GetFullPath();
@ -348,34 +386,35 @@ void LegacyUpdate::ModTheJar()
addedFiles.insert(destFile);
}
*/
}
}
{
wxFFileInputStream inStream(mcBackup.GetFullPath());
wxZipInputStream zipIn(inStream);
std::auto_ptr<wxZipEntry> entry;
while (entry.reset(zipIn.GetNextEntry()), entry.get() != NULL)
else if (mod.type() == Mod::MOD_FOLDER)
{
wxString name = entry->GetName();
if (!name.Matches("META-INF*") &&
addedFiles.count(name) == 0)
{
if (!zipOut.CopyEntry(entry.release(), zipIn))
break;
addedFiles.insert(name);
}
zipOut.close();
QFile::remove(runnableJar.filePath());
emitFailed("Folders are NOT supported as jar mods.");
return;
}
}
if(!MergeZipFiles(&zipOut, baseJar, addedFiles))
{
zipOut.close();
QFile::remove(runnableJar.filePath());
emitFailed("Failed to insert minecraft.jar contents.");
return;
}
// Recompress the jar
TaskStep(); // STEP 3
SetStatus(_("Installing mods - Recompressing jar..."));
inst->SetNeedsRebuild(false);
inst->UpdateVersion(true);
return (ExitCode)1;
*/
zipOut.close();
if(zipOut.getZipError()!=0)
{
QFile::remove(runnableJar.filePath());
emitFailed("Failed to finalize minecraft.jar!");
return;
}
inst->setShouldRebuild(false);
//inst->UpdateVersion(true);
emitSucceeded();
return;
}

View File

@ -25,6 +25,8 @@
class MinecraftVersion;
class BaseInstance;
class QuaZip;
class Mod;
class LegacyUpdate : public BaseUpdate
{
@ -46,6 +48,8 @@ private slots:
void ModTheJar();
private:
bool MergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString>& contained);
private:
QSharedPointer<QNetworkReply> m_reply;
@ -59,9 +63,6 @@ private:
private:
JobListPtr legacyDownloadJob;
JobListQueue download_queue;
// target version, determined during this task
QSharedPointer<MinecraftVersion> targetVersion;
};

View File

@ -37,6 +37,7 @@ public:
virtual int columnCount ( const QModelIndex& parent ) const;
size_t size() const { return mods.size(); };
bool empty() const { return size() == 0; }
Mod& operator[](size_t index) { return mods[index]; };
/// Reloads the mod list and returns true if the list changed.

View File

@ -92,7 +92,7 @@ MinecraftProcess* OneSixInstance::prepareForLaunch ( QString user, QString sessi
return nullptr;
auto libs_to_extract = version->getActiveNativeLibs();
QString natives_dir_raw = PathCombine(instanceRoot(), "natives/");
bool success = ensurePathExists(natives_dir_raw);
bool success = ensureFolderPathExists(natives_dir_raw);
if(!success)
{
// FIXME: handle errors
@ -216,3 +216,13 @@ QSharedPointer< FullVersion > OneSixInstance::getFullVersion()
I_D(OneSixInstance);
return d->version;
}
QString OneSixInstance::defaultBaseJar() const
{
return "versions/" + intendedVersionId() + "/" + intendedVersionId() + ".jar";
}
QString OneSixInstance::defaultCustomBaseJar() const
{
return PathCombine(instanceRoot(), "custom.jar");
}

View File

@ -29,6 +29,9 @@ public:
bool reloadFullVersion();
/// get the current full version info
QSharedPointer<FullVersion> getFullVersion();
virtual QString defaultBaseJar() const;
virtual QString defaultCustomBaseJar() const;
private:
QStringList processMinecraftArgs( QString user, QString session );
};

View File

@ -91,7 +91,7 @@ void OneSixUpdate::versionFileFinished()
// save the version file in $instanceId/version.json
{
QString version1 = PathCombine(inst_dir, "/version.json");
ensurePathExists(version1);
ensureFilePathExists(version1);
// FIXME: detect errors here, download to a temp file, swap
QFile vfile1 (version1);
vfile1.open(QIODevice::Truncate | QIODevice::WriteOnly );

View File

@ -48,7 +48,7 @@ void DownloadJob::start()
m_expected_md5 = hash;
}
}
if(!ensurePathExists(filename))
if(!ensureFilePathExists(filename))
{
emit fail();
return;