1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-03-21 21:17:49 +02:00
vcmi/launcher/modManager/chroniclesextractor.cpp

267 lines
8.8 KiB
C++
Raw Normal View History

2024-08-30 21:17:18 +02:00
/*
* chroniclesextractor.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "chroniclesextractor.h"
2024-08-30 22:20:33 +02:00
#include "../../lib/VCMIDirs.h"
2024-08-31 00:44:20 +02:00
#include "../../lib/filesystem/CArchiveLoader.h"
2024-08-30 22:20:33 +02:00
2024-09-02 23:36:42 +02:00
#include "../innoextract.h"
2025-01-15 20:30:48 +01:00
#include "../helper.h"
2025-01-03 13:51:15 +01:00
2024-08-30 22:20:33 +02:00
ChroniclesExtractor::ChroniclesExtractor(QWidget *p, std::function<void(float percent)> cb) :
parent(p), cb(cb)
2024-08-30 21:17:18 +02:00
{
2024-08-30 22:20:33 +02:00
}
2024-09-02 23:36:42 +02:00
bool ChroniclesExtractor::createTempDir()
2024-08-30 22:20:33 +02:00
{
2024-09-02 23:36:42 +02:00
tempDir = QDir(pathToQString(VCMIDirs::get().userDataPath()));
if(tempDir.cd("tmp"))
2024-08-30 21:17:18 +02:00
{
2024-09-02 23:36:42 +02:00
tempDir.removeRecursively(); // remove if already exists (e.g. previous run)
tempDir.cdUp();
2024-08-30 22:20:33 +02:00
}
2024-09-02 23:36:42 +02:00
tempDir.mkdir("tmp");
if(!tempDir.cd("tmp"))
return false; // should not happen - but avoid deleting wrong folder in any case
2024-08-30 22:20:33 +02:00
return true;
}
2024-09-02 23:36:42 +02:00
void ChroniclesExtractor::removeTempDir()
{
tempDir.removeRecursively();
}
int ChroniclesExtractor::getChronicleNo()
2024-08-30 22:20:33 +02:00
{
2024-12-16 20:34:15 +00:00
QStringList appDirCandidates = tempDir.entryList({"app"}, QDir::Filter::Dirs);
2024-08-30 21:17:18 +02:00
2024-12-16 20:34:15 +00:00
if (!appDirCandidates.empty())
{
QDir appDir = tempDir.filePath(appDirCandidates.front());
2024-08-30 22:20:33 +02:00
2024-12-16 20:34:15 +00:00
for (size_t i = 1; i < chronicles.size(); ++i)
{
QString chronicleName = chronicles.at(i);
QStringList chroniclesDirCandidates = appDir.entryList({chronicleName}, QDir::Filter::Dirs);
2024-12-16 20:34:15 +00:00
if (!chroniclesDirCandidates.empty())
return i;
}
2024-08-30 22:20:33 +02:00
}
QMessageBox::critical(parent, tr("Invalid file selected"), tr("You have to select a Heroes Chronicles installer file!"));
return 0;
2024-08-30 22:20:33 +02:00
}
2024-08-30 21:17:18 +02:00
2024-08-30 22:20:33 +02:00
bool ChroniclesExtractor::extractGogInstaller(QString file)
{
2024-09-02 23:36:42 +02:00
QString errorText = Innoextract::extract(file, tempDir.path(), [this](float progress) {
float overallProgress = ((1.0 / static_cast<float>(fileCount)) * static_cast<float>(extractionFile)) + (progress / static_cast<float>(fileCount));
if(cb)
cb(overallProgress);
});
2024-08-30 22:20:33 +02:00
2024-09-02 23:36:42 +02:00
if(!errorText.isEmpty())
{
2025-01-03 13:51:15 +01:00
logGlobal->error("Gog chronicles installer extraction failure! Reason: %s", errorText.toStdString());
2024-12-02 20:29:17 +01:00
QString hashError = Innoextract::getHashError(file, {}, {}, {});
2024-09-02 23:36:42 +02:00
QMessageBox::critical(parent, tr("Extracting error!"), errorText);
2024-12-01 23:46:03 +01:00
if(!hashError.isEmpty())
2025-01-03 13:51:15 +01:00
{
logGlobal->error("Hash error: %s", hashError.toStdString());
2024-12-01 23:46:03 +01:00
QMessageBox::critical(parent, tr("Hash error!"), hashError, QMessageBox::Ok, QMessageBox::Ok);
2025-01-03 13:51:15 +01:00
}
2024-09-02 23:36:42 +02:00
return false;
}
2024-08-30 22:20:33 +02:00
2024-09-02 23:36:42 +02:00
return true;
2024-08-30 22:20:33 +02:00
}
2024-09-01 02:16:03 +02:00
void ChroniclesExtractor::createBaseMod() const
2024-08-31 00:44:20 +02:00
{
QDir dir(pathToQString(VCMIDirs::get().userDataPath() / "Mods"));
dir.mkdir("chronicles");
dir.cd("chronicles");
dir.mkdir("Mods");
QJsonObject mod
{
2024-08-31 18:46:45 +02:00
{ "modType", "Expansion" },
2024-09-02 23:36:42 +02:00
{ "name", tr("Heroes Chronicles") },
{ "description", tr("Heroes Chronicles") },
2024-08-31 00:44:20 +02:00
{ "author", "3DO" },
{ "version", "1.0" },
2024-09-01 00:41:16 +02:00
{ "contact", "vcmi.eu" },
2024-11-04 23:30:56 +01:00
{ "heroes", QJsonArray({"config/portraitsChronicles.json"}) },
{ "settings", QJsonObject({{"mapFormat", QJsonObject({{"chronicles", QJsonObject({{
{"supported", true},
{"portraits", QJsonObject({
{"portraitTarnumBarbarian", 163},
{"portraitTarnumKnight", 164},
{"portraitTarnumWizard", 165},
{"portraitTarnumRanger", 166},
{"portraitTarnumOverlord", 167},
{"portraitTarnumBeastmaster", 168},
})},
}})}})}})},
2024-08-31 00:44:20 +02:00
};
QFile jsonFile(dir.filePath("mod.json"));
jsonFile.open(QFile::WriteOnly);
jsonFile.write(QJsonDocument(mod).toJson());
2024-11-04 23:30:56 +01:00
for(auto & dataPath : VCMIDirs::get().dataPaths())
{
auto file = pathToQString(dataPath / "config" / "heroes" / "portraitsChronicles.json");
2024-11-04 23:30:56 +01:00
auto destFolder = VCMIDirs::get().userDataPath() / "Mods" / "chronicles" / "content" / "config";
auto destFile = pathToQString(destFolder / "portraitsChronicles.json");
if(QFile::exists(file))
2024-11-04 23:30:56 +01:00
{
QDir().mkpath(pathToQString(destFolder));
QFile::remove(destFile);
QFile::copy(file, destFile);
2024-11-04 23:30:56 +01:00
}
}
2024-08-31 00:44:20 +02:00
}
void ChroniclesExtractor::createChronicleMod(int no)
{
2024-08-31 18:46:45 +02:00
QDir dir(pathToQString(VCMIDirs::get().userDataPath() / "Mods" / "chronicles" / "Mods" / ("chronicles_" + std::to_string(no))));
dir.removeRecursively();
dir.mkpath(".");
2024-08-31 00:44:20 +02:00
QString tmpChronicles = chronicles.at(no);
2024-09-02 23:36:42 +02:00
2024-08-31 00:44:20 +02:00
QJsonObject mod
{
2024-08-31 18:46:45 +02:00
{ "modType", "Expansion" },
2024-12-16 20:34:15 +00:00
{ "name", QString("%1 - %2").arg(no).arg(tmpChronicles) },
{ "description", tr("Heroes Chronicles %1 - %2").arg(no).arg(tmpChronicles) },
2024-08-31 00:44:20 +02:00
{ "author", "3DO" },
{ "version", "1.0" },
2024-09-01 00:41:16 +02:00
{ "contact", "vcmi.eu" },
2024-08-31 00:44:20 +02:00
};
2024-08-31 02:52:34 +02:00
2024-08-31 00:44:20 +02:00
QFile jsonFile(dir.filePath("mod.json"));
jsonFile.open(QFile::WriteOnly);
jsonFile.write(QJsonDocument(mod).toJson());
2024-08-31 02:52:34 +02:00
dir.cd("content");
2024-08-31 18:46:45 +02:00
2024-08-31 00:44:20 +02:00
extractFiles(no);
}
2024-09-01 02:16:03 +02:00
void ChroniclesExtractor::extractFiles(int no) const
2024-08-31 00:44:20 +02:00
{
QString tmpChronicles = chronicles.at(no);
2024-08-31 00:44:20 +02:00
2024-09-02 22:51:30 +02:00
std::string chroniclesDir = "chronicles_" + std::to_string(no);
2024-08-31 00:44:20 +02:00
QDir tmpDir = tempDir.filePath(tempDir.entryList({"app"}, QDir::Filter::Dirs).front());
tmpDir.setPath(tmpDir.filePath(tmpDir.entryList({QString(tmpChronicles)}, QDir::Filter::Dirs).front()));
tmpDir.setPath(tmpDir.filePath(tmpDir.entryList({"data"}, QDir::Filter::Dirs).front()));
2024-09-02 22:51:30 +02:00
auto basePath = VCMIDirs::get().userDataPath() / "Mods" / "chronicles" / "Mods" / chroniclesDir / "content";
QDir outDirDataPortraits(pathToQString(VCMIDirs::get().userDataPath() / "Mods" / "chronicles" / "content" / "Data"));
QDir outDirData(pathToQString(basePath / "Data" / chroniclesDir));
QDir outDirSprites(pathToQString(basePath / "Sprites" / chroniclesDir));
QDir outDirVideo(pathToQString(basePath / "Video" / chroniclesDir));
QDir outDirSounds(pathToQString(basePath / "Sounds" / chroniclesDir));
QDir outDirMaps(pathToQString(basePath / "Maps" / "Chronicles"));
2024-08-31 16:27:39 +02:00
auto extract = [](QDir scrDir, QDir dest, QString file, std::vector<std::string> files = {}){
2024-08-31 15:25:45 +02:00
CArchiveLoader archive("", scrDir.filePath(scrDir.entryList({file}).front()).toStdString(), false);
2024-08-31 02:52:34 +02:00
for(auto & entry : archive.getEntries())
2024-08-31 15:25:45 +02:00
if(files.empty())
archive.extractToFolder(dest.absolutePath().toStdString(), "", entry.second, true);
else
{
2024-09-01 02:16:03 +02:00
for(const auto & item : files)
2024-09-02 22:51:30 +02:00
if(boost::algorithm::to_lower_copy(entry.second.name).find(boost::algorithm::to_lower_copy(item)) != std::string::npos)
2024-08-31 15:25:45 +02:00
archive.extractToFolder(dest.absolutePath().toStdString(), "", entry.second, true);
}
2024-08-31 02:52:34 +02:00
};
2024-08-31 15:25:45 +02:00
extract(tmpDir, outDirData, "xBitmap.lod");
extract(tmpDir, outDirData, "xlBitmap.lod");
extract(tmpDir, outDirSprites, "xSprite.lod");
extract(tmpDir, outDirSprites, "xlSprite.lod");
extract(tmpDir, outDirVideo, "xVideo.vid");
extract(tmpDir, outDirSounds, "xSound.snd");
2024-08-31 02:52:34 +02:00
tmpDir.cdUp();
2024-09-02 22:51:30 +02:00
if(tmpDir.entryList({"maps"}, QDir::Filter::Dirs).size()) // special case for "The World Tree": the map is in the "Maps" folder instead of inside the lod
2024-08-31 02:52:34 +02:00
{
QDir tmpDirMaps = tmpDir.filePath(tmpDir.entryList({"maps"}, QDir::Filter::Dirs).front());
2024-09-01 02:16:03 +02:00
for(const auto & entry : tmpDirMaps.entryList())
2024-08-31 02:52:34 +02:00
QFile(tmpDirMaps.filePath(entry)).copy(outDirData.filePath(entry));
}
2024-08-31 15:25:45 +02:00
tmpDir.cdUp();
QDir tmpDirData = tmpDir.filePath(tmpDir.entryList({"data"}, QDir::Filter::Dirs).front());
2024-09-02 22:51:30 +02:00
auto tarnumPortraits = std::vector<std::string>{"HPS137", "HPS138", "HPS139", "HPS140", "HPS141", "HPS142", "HPL137", "HPL138", "HPL139", "HPL140", "HPL141", "HPL142"};
extract(tmpDirData, outDirDataPortraits, "bitmap.lod", tarnumPortraits);
2024-08-31 17:16:42 +02:00
extract(tmpDirData, outDirData, "lbitmap.lod", std::vector<std::string>{"INTRORIM"});
2024-08-31 15:25:45 +02:00
2024-08-31 12:14:51 +02:00
if(!outDirMaps.exists())
outDirMaps.mkpath(".");
QString campaignFileName = "Hc" + QString::number(no) + "_Main.h3c";
2024-09-02 22:51:30 +02:00
QFile(outDirData.filePath(outDirData.entryList({"Main.h3c"}).front())).copy(outDirMaps.filePath(campaignFileName));
2024-08-31 00:44:20 +02:00
}
2024-08-30 22:20:33 +02:00
void ChroniclesExtractor::installChronicles(QStringList exe)
{
logGlobal->info("Installing Chronicles");
2024-08-30 22:20:33 +02:00
extractionFile = -1;
fileCount = exe.size();
for(QString f : exe)
{
extractionFile++;
logGlobal->info("Creating temporary directory");
2024-12-10 23:14:27 +01:00
if(!createTempDir())
2024-08-30 22:20:33 +02:00
continue;
2024-12-10 23:14:27 +01:00
// FIXME: this is required at the moment for Android (and possibly iOS)
// Incoming file names are in content URI form, e.g. content://media/internal/chronicles.exe
// Qt can handle those like it does regular files
// however, innoextract fails to open such files
// so make a copy in directory to which vcmi always has full access and operate on it
2024-12-10 23:14:27 +01:00
QString filepath = tempDir.filePath("chr.exe");
2025-01-03 13:51:15 +01:00
logGlobal->info("Copying offline installer from '%s' to '%s'", f.toStdString(), filepath.toStdString());
2025-01-15 20:30:48 +01:00
Helper::performNativeCopy(f, filepath);
2024-12-10 23:14:27 +01:00
QFile file(filepath);
2024-08-30 22:20:33 +02:00
logGlobal->info("Extracting offline installer");
if(!extractGogInstaller(filepath))
2024-08-30 22:20:33 +02:00
continue;
logGlobal->info("Detecting Chronicle");
int chronicleNo = getChronicleNo();
if(!chronicleNo)
2024-08-30 22:20:33 +02:00
continue;
logGlobal->info("Creating base Chronicle mod");
2024-08-31 00:44:20 +02:00
createBaseMod();
logGlobal->info("Creating Chronicle mod");
2024-08-31 00:44:20 +02:00
createChronicleMod(chronicleNo);
2024-08-30 22:20:33 +02:00
logGlobal->info("Removing temporary directory");
2024-09-02 23:36:42 +02:00
removeTempDir();
2024-08-30 21:17:18 +02:00
}
logGlobal->info("Chronicles installed");
2024-08-31 16:27:39 +02:00
}