123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294 |
- /*
- * 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"
- #include "../../lib/VCMIDirs.h"
- #include "../../lib/filesystem/CArchiveLoader.h"
- #include "../innoextract.h"
- #include "../helper.h"
- ChroniclesExtractor::ChroniclesExtractor(QWidget *p, std::function<void(float percent)> cb) :
- parent(p), cb(cb)
- {
- }
- bool ChroniclesExtractor::createTempDir()
- {
- tempDir = QDir(pathToQString(VCMIDirs::get().userDataPath()));
- if(tempDir.cd("tmp"))
- {
- tempDir.removeRecursively(); // remove if already exists (e.g. previous run)
- tempDir.cdUp();
- }
- tempDir.mkdir("tmp");
- if(!tempDir.cd("tmp"))
- return false; // should not happen - but avoid deleting wrong folder in any case
- return true;
- }
- void ChroniclesExtractor::removeTempDir()
- {
- tempDir.removeRecursively();
- }
- std::vector<int> ChroniclesExtractor::getChronicleNo()
- {
- // supports "All in one" and seperate installers
- QStringList appDirCandidates = tempDir.entryList({"app"}, QDir::Filter::Dirs);
- std::vector<int> tmp;
- if (!appDirCandidates.empty())
- {
- QDir appDir = tempDir.filePath(appDirCandidates.front());
- for (size_t i = 1; i < chronicles.size(); ++i)
- {
- QString chronicleName = chronicles.at(i);
- QStringList chroniclesDirCandidates = appDir.entryList({chronicleName}, QDir::Filter::Dirs);
- if (!chroniclesDirCandidates.empty())
- tmp.push_back(i);
- }
- }
- if(tmp.empty())
- QMessageBox::critical(parent, tr("Invalid file selected"), tr("You have to select a Heroes Chronicles installer file!"));
- return tmp;
- }
- bool ChroniclesExtractor::extractGogInstaller(QString file)
- {
- 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);
- });
- if(!errorText.isEmpty())
- {
- logGlobal->error("Gog chronicles installer extraction failure! Reason: %s", errorText.toStdString());
- QString hashError = Innoextract::getHashError(file, {}, {}, {});
- QMessageBox::critical(parent, tr("Extracting error!"), errorText);
- if(!hashError.isEmpty())
- {
- logGlobal->error("Hash error: %s", hashError.toStdString());
- QMessageBox::critical(parent, tr("Hash error!"), hashError, QMessageBox::Ok, QMessageBox::Ok);
- }
- return false;
- }
- return true;
- }
- void ChroniclesExtractor::createBaseMod() const
- {
- QDir dir(pathToQString(VCMIDirs::get().userDataPath() / "Mods"));
- dir.mkdir("chronicles");
- dir.cd("chronicles");
- dir.mkdir("Mods");
- QJsonObject mod
- {
- { "modType", "Expansion" },
- { "name", tr("Heroes Chronicles") },
- { "description", tr("Heroes Chronicles") },
- { "author", "New World Computing, 3DO" },
- { "version", "1.0" },
- { "contact", "vcmi.eu" },
- { "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},
- })},
- }})}})}})},
- };
- QFile jsonFile(dir.filePath("mod.json"));
- jsonFile.open(QFile::WriteOnly);
- jsonFile.write(QJsonDocument(mod).toJson());
- for(auto & dataPath : VCMIDirs::get().dataPaths())
- {
- auto file = pathToQString(dataPath / "config" / "heroes" / "portraitsChronicles.json");
- auto destFolder = VCMIDirs::get().userDataPath() / "Mods" / "chronicles" / "content" / "config";
- auto destFile = pathToQString(destFolder / "portraitsChronicles.json");
- if(QFile::exists(file))
- {
- QDir().mkpath(pathToQString(destFolder));
- QFile::remove(destFile);
- QFile::copy(file, destFile);
- }
- }
- }
- void ChroniclesExtractor::createChronicleMod(int no)
- {
- QDir dir(pathToQString(VCMIDirs::get().userDataPath() / "Mods" / "chronicles" / "Mods" / ("chronicles_" + std::to_string(no))));
- dir.removeRecursively();
- dir.mkpath(".");
- QString tmpChronicles = chronicles.at(no);
- QJsonObject mod
- {
- { "modType", "Expansion" },
- { "name", QString("%1 - %2").arg(no).arg(tmpChronicles) },
- { "description", tr("Heroes Chronicles %1 - %2").arg(no).arg(tmpChronicles) },
- { "author", "New World Computing, 3DO" },
- { "version", "1.0" },
- { "contact", "vcmi.eu" },
- };
-
- QFile jsonFile(dir.filePath("mod.json"));
- jsonFile.open(QFile::WriteOnly);
- jsonFile.write(QJsonDocument(mod).toJson());
- dir.cd("content");
-
- extractFiles(no);
- }
- void ChroniclesExtractor::extractFiles(int no) const
- {
- QString tmpChronicles = chronicles.at(no);
- std::string chroniclesDir = "chronicles_" + std::to_string(no);
- 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()));
- 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"));
- auto extract = [](QDir scrDir, QDir dest, QString file, std::vector<std::string> files = {}){
- if(scrDir.entryList({file}).isEmpty())
- return; // file does not exists (needed for "All in one" installer)
- CArchiveLoader archive("", scrDir.filePath(scrDir.entryList({file}).front()).toStdString(), false);
- for(auto & entry : archive.getEntries())
- if(files.empty())
- archive.extractToFolder(dest.absolutePath().toStdString(), "", entry.second, true);
- else
- {
- for(const auto & item : files)
- if(boost::algorithm::to_lower_copy(entry.second.name).find(boost::algorithm::to_lower_copy(item)) != std::string::npos)
- archive.extractToFolder(dest.absolutePath().toStdString(), "", entry.second, true);
- }
- };
- 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");
- tmpDir.cdUp();
- 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
- {
- QDir tmpDirMaps = tmpDir.filePath(tmpDir.entryList({"maps"}, QDir::Filter::Dirs).front());
- for(const auto & entry : tmpDirMaps.entryList())
- QFile(tmpDirMaps.filePath(entry)).copy(outDirData.filePath(entry));
- }
- tmpDir.cdUp();
- QDir tmpDirData = tmpDir.filePath(tmpDir.entryList({"data"}, QDir::Filter::Dirs).front());
- auto tarnumPortraits = std::vector<std::string>{"HPS137", "HPS138", "HPS139", "HPS140", "HPS141", "HPS142", "HPL137", "HPL138", "HPL139", "HPL140", "HPL141", "HPL142"};
- extract(tmpDirData, outDirDataPortraits, "bitmap.lod", tarnumPortraits);
- extract(tmpDirData, outDirData, "lbitmap.lod", std::vector<std::string>{"INTRORIM"});
- // special case - "All in one" installer
- {
- tmpDir.cdUp();
- auto mapping = std::map<std::string, int>{{ {"Intro", 1}, {"Intr2", 2}, {"Intr3", 3}, {"Intr4", 4}, {"Intro5", 7}, {"Intro6", 8} }};
- std::vector<std::string> videoFiles;
- for(auto & elem : mapping)
- for(const auto & ending : {".bik", ".smk"})
- videoFiles.push_back(elem.first + ending);
- extract(tmpDirData, tmpDir, "Hchron.vid", videoFiles);
- for(auto & ending : {".bik", ".smk"})
- {
- if(!vstd::reverseMap(mapping).count(no))
- continue;
- auto srcName = vstd::reverseMap(mapping).at(no);
- auto dstName = (no == 7 || no == 8) ? srcName : "Intro";
- QFile::copy(tmpDir.filePath(QString::fromStdString(srcName + ending)), outDirVideo.filePath(QString::fromStdString(dstName + ending)));
- }
- }
- if(!outDirMaps.exists())
- outDirMaps.mkpath(".");
- QString campaignFileName = "Hc" + QString::number(no) + "_Main.h3c";
- QFile(outDirData.filePath(outDirData.entryList({"Main.h3c"}).front())).copy(outDirMaps.filePath(campaignFileName));
- }
- void ChroniclesExtractor::installChronicles(QStringList exe)
- {
- logGlobal->info("Installing Chronicles");
- extractionFile = -1;
- fileCount = exe.size();
- for(QString f : exe)
- {
- extractionFile++;
- logGlobal->info("Creating temporary directory");
- if(!createTempDir())
- continue;
-
- // 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
- QString filepath = tempDir.filePath("chr.exe");
- logGlobal->info("Copying offline installer from '%s' to '%s'", f.toStdString(), filepath.toStdString());
- Helper::performNativeCopy(f, filepath);
- QFile file(filepath);
- logGlobal->info("Extracting offline installer");
- if(!extractGogInstaller(filepath))
- continue;
- logGlobal->info("Detecting Chronicles");
- auto chronicleNo = getChronicleNo();
- if(chronicleNo.empty())
- continue;
- logGlobal->info("Creating base Chronicle mod");
- createBaseMod();
- for(const auto & no : chronicleNo)
- {
- logGlobal->info("Creating Chronicle mod (%i)", no);
- createChronicleMod(no);
- }
- logGlobal->info("Removing temporary directory");
- removeTempDir();
- }
- logGlobal->info("Chronicles installed");
- }
|