chroniclesextractor.cpp 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. /*
  2. * chroniclesextractor.cpp, part of VCMI engine
  3. *
  4. * Authors: listed in file AUTHORS in main folder
  5. *
  6. * License: GNU General Public License v2.0 or later
  7. * Full text of license available in license.txt file, in main folder
  8. *
  9. */
  10. #include "StdInc.h"
  11. #include "chroniclesextractor.h"
  12. #include "../../lib/VCMIDirs.h"
  13. #include "../../lib/filesystem/CArchiveLoader.h"
  14. #include "../innoextract.h"
  15. #include "../helper.h"
  16. ChroniclesExtractor::ChroniclesExtractor(QWidget *p, std::function<void(float percent)> cb) :
  17. cb{cb}
  18. {
  19. }
  20. bool ChroniclesExtractor::createTempDir()
  21. {
  22. tempDir = QDir(pathToQString(VCMIDirs::get().userDataPath()));
  23. if(tempDir.cd("tmp"))
  24. {
  25. tempDir.removeRecursively(); // remove if already exists (e.g. previous run)
  26. tempDir.cdUp();
  27. }
  28. tempDir.mkdir("tmp");
  29. if(!tempDir.cd("tmp"))
  30. return false; // should not happen - but avoid deleting wrong folder in any case
  31. return true;
  32. }
  33. void ChroniclesExtractor::removeTempDir()
  34. {
  35. tempDir.removeRecursively();
  36. }
  37. std::vector<int> ChroniclesExtractor::getChronicleNo()
  38. {
  39. // supports "All in one" and seperate installers
  40. QStringList appDirCandidates = tempDir.entryList({"app"}, QDir::Filter::Dirs);
  41. std::vector<int> tmp;
  42. if (!appDirCandidates.empty())
  43. {
  44. QDir appDir = tempDir.filePath(appDirCandidates.front());
  45. for (size_t i = 1; i < chronicles.size(); ++i)
  46. {
  47. QString chronicleName = chronicles.at(i);
  48. QStringList chroniclesDirCandidates = appDir.entryList({chronicleName}, QDir::Filter::Dirs);
  49. if (!chroniclesDirCandidates.empty())
  50. tmp.push_back(i);
  51. }
  52. }
  53. return tmp;
  54. }
  55. bool ChroniclesExtractor::extractGogInstaller(QString file)
  56. {
  57. QString errorText = Innoextract::extract(file, tempDir.path(), [this](float progress) {
  58. float overallProgress = ((1.0 / static_cast<float>(fileCount)) * static_cast<float>(extractionFile)) + (progress / static_cast<float>(fileCount));
  59. if(cb)
  60. cb(overallProgress);
  61. });
  62. if(!errorText.isEmpty())
  63. {
  64. logGlobal->error("Gog chronicles installer extraction failure! Reason: %s", errorText.toStdString());
  65. QString hashError = Innoextract::getHashError(file, {}, {}, {});
  66. if(!hashError.isEmpty())
  67. {
  68. logGlobal->error("Hash error: %s", hashError.toStdString());
  69. }
  70. return false;
  71. }
  72. return true;
  73. }
  74. void ChroniclesExtractor::createBaseMod() const
  75. {
  76. QDir dir(pathToQString(VCMIDirs::get().userDataPath() / "Mods"));
  77. dir.mkdir("chronicles");
  78. dir.cd("chronicles");
  79. dir.mkdir("Mods");
  80. QJsonObject mod
  81. {
  82. { "modType", "Expansion" },
  83. { "name", tr("Heroes Chronicles") },
  84. { "description", tr("Heroes Chronicles") },
  85. { "author", "New World Computing, 3DO" },
  86. { "version", "1.0" },
  87. { "contact", "vcmi.eu" },
  88. { "heroes", QJsonArray({"config/portraitsChronicles.json"}) },
  89. { "settings", QJsonObject({{"mapFormat", QJsonObject({{"chronicles", QJsonObject({{
  90. {"supported", true},
  91. {"portraits", QJsonObject({
  92. {"portraitTarnumBarbarian", 163},
  93. {"portraitTarnumKnight", 164},
  94. {"portraitTarnumWizard", 165},
  95. {"portraitTarnumRanger", 166},
  96. {"portraitTarnumOverlord", 167},
  97. {"portraitTarnumBeastmaster", 168},
  98. })},
  99. }})}})}})},
  100. };
  101. QFile jsonFile(dir.filePath("mod.json"));
  102. jsonFile.open(QFile::WriteOnly);
  103. jsonFile.write(QJsonDocument(mod).toJson());
  104. for(auto & dataPath : VCMIDirs::get().dataPaths())
  105. {
  106. auto file = pathToQString(dataPath / "config" / "heroes" / "portraitsChronicles.json");
  107. auto destFolder = VCMIDirs::get().userDataPath() / "Mods" / "chronicles" / "content" / "config";
  108. auto destFile = pathToQString(destFolder / "portraitsChronicles.json");
  109. if(QFile::exists(file))
  110. {
  111. QDir().mkpath(pathToQString(destFolder));
  112. QFile::remove(destFile);
  113. Helper::performNativeCopy(file, destFile);
  114. }
  115. }
  116. }
  117. void ChroniclesExtractor::createChronicleMod(int no)
  118. {
  119. QDir dir(pathToQString(VCMIDirs::get().userDataPath() / "Mods" / "chronicles" / "Mods" / ("chronicles_" + std::to_string(no))));
  120. dir.removeRecursively();
  121. dir.mkpath(".");
  122. QString tmpChronicles = chronicles.at(no);
  123. QJsonObject mod
  124. {
  125. { "modType", "Expansion" },
  126. { "name", QString("%1 - %2").arg(no).arg(tmpChronicles) },
  127. { "description", tr("Heroes Chronicles %1 - %2").arg(no).arg(tmpChronicles) },
  128. { "author", "New World Computing, 3DO" },
  129. { "version", "1.0" },
  130. { "contact", "vcmi.eu" },
  131. };
  132. QFile jsonFile(dir.filePath("mod.json"));
  133. jsonFile.open(QFile::WriteOnly);
  134. jsonFile.write(QJsonDocument(mod).toJson());
  135. dir.cd("content");
  136. extractFiles(no);
  137. }
  138. void ChroniclesExtractor::extractFiles(int no) const
  139. {
  140. QString tmpChronicles = chronicles.at(no);
  141. std::string chroniclesDir = "chronicles_" + std::to_string(no);
  142. QDir tmpDir = tempDir.filePath(tempDir.entryList({"app"}, QDir::Filter::Dirs).front());
  143. if(!tmpDir.entryList({"data"}, QDir::Filter::Dirs).size()) // gog installer V2 has data and other folders outside "app" folder
  144. tmpDir.cdUp();
  145. tmpDir.setPath(tmpDir.filePath(tmpDir.entryList({QString(tmpChronicles)}, QDir::Filter::Dirs).front()));
  146. tmpDir.setPath(tmpDir.filePath(tmpDir.entryList({"data"}, QDir::Filter::Dirs).front()));
  147. auto basePath = VCMIDirs::get().userDataPath() / "Mods" / "chronicles" / "Mods" / chroniclesDir / "content";
  148. QDir outDirDataPortraits(pathToQString(VCMIDirs::get().userDataPath() / "Mods" / "chronicles" / "content" / "Data"));
  149. QDir outDirData(pathToQString(basePath / "Data" / chroniclesDir));
  150. QDir outDirSprites(pathToQString(basePath / "Sprites" / chroniclesDir));
  151. QDir outDirVideo(pathToQString(basePath / "Video" / chroniclesDir));
  152. QDir outDirSounds(pathToQString(basePath / "Sounds" / chroniclesDir));
  153. QDir outDirMaps(pathToQString(basePath / "Maps" / "Chronicles"));
  154. auto extract = [](QDir scrDir, QDir dest, QString file, std::vector<std::string> files = {}){
  155. if(scrDir.entryList({file}).isEmpty())
  156. return; // file does not exists (needed for "All in one" installer)
  157. CArchiveLoader archive("", scrDir.filePath(scrDir.entryList({file}).front()).toStdString(), false);
  158. for(auto & entry : archive.getEntries())
  159. if(files.empty())
  160. archive.extractToFolder(dest.absolutePath().toStdString(), "", entry.second, true);
  161. else
  162. {
  163. for(const auto & item : files)
  164. if(boost::algorithm::to_lower_copy(entry.second.name).find(boost::algorithm::to_lower_copy(item)) != std::string::npos)
  165. archive.extractToFolder(dest.absolutePath().toStdString(), "", entry.second, true);
  166. }
  167. };
  168. extract(tmpDir, outDirData, "xBitmap.lod");
  169. extract(tmpDir, outDirData, "xlBitmap.lod");
  170. extract(tmpDir, outDirSprites, "xSprite.lod");
  171. extract(tmpDir, outDirSprites, "xlSprite.lod");
  172. extract(tmpDir, outDirVideo, "xVideo.vid");
  173. extract(tmpDir, outDirSounds, "xSound.snd");
  174. tmpDir.cdUp();
  175. 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
  176. {
  177. QDir tmpDirMaps = tmpDir.filePath(tmpDir.entryList({"maps"}, QDir::Filter::Dirs).front());
  178. for(const auto & entry : tmpDirMaps.entryList())
  179. QFile(tmpDirMaps.filePath(entry)).copy(outDirData.filePath(entry));
  180. }
  181. tmpDir.cdUp();
  182. QDir tmpDirData = tmpDir.filePath(tmpDir.entryList({"data"}, QDir::Filter::Dirs).front());
  183. auto tarnumPortraits = std::vector<std::string>{"HPS137", "HPS138", "HPS139", "HPS140", "HPS141", "HPS142", "HPL137", "HPL138", "HPL139", "HPL140", "HPL141", "HPL142"};
  184. extract(tmpDirData, outDirDataPortraits, "bitmap.lod", tarnumPortraits);
  185. extract(tmpDirData, outDirData, "lbitmap.lod", std::vector<std::string>{"INTRORIM"});
  186. // special case - "All in one" installer
  187. {
  188. tmpDir.cdUp();
  189. auto mapping = std::map<std::string, int>{{ {"Intro", 1}, {"Intr2", 2}, {"Intr3", 3}, {"Intr4", 4}, {"Intro5", 7}, {"Intro6", 8} }};
  190. std::vector<std::string> videoFiles;
  191. for(auto & elem : mapping)
  192. for(const auto & ending : {".bik", ".smk"})
  193. videoFiles.push_back(elem.first + ending);
  194. extract(tmpDirData, tmpDir, "Hchron.vid", videoFiles);
  195. for(auto & ending : {".bik", ".smk"})
  196. {
  197. if(!vstd::reverseMap(mapping).count(no))
  198. continue;
  199. auto srcName = vstd::reverseMap(mapping).at(no);
  200. auto dstName = (no == 7 || no == 8) ? srcName : "Intro";
  201. Helper::performNativeCopy(tmpDir.filePath(QString::fromStdString(srcName + ending)), outDirVideo.filePath(QString::fromStdString(dstName + ending)));
  202. }
  203. }
  204. if(!outDirMaps.exists())
  205. outDirMaps.mkpath(".");
  206. QString campaignFileName = "Hc" + QString::number(no) + "_Main.h3c";
  207. QFile(outDirData.filePath(outDirData.entryList({"Main.h3c"}).front())).copy(outDirMaps.filePath(campaignFileName));
  208. }
  209. int ChroniclesExtractor::installChronicles(QStringList exe)
  210. {
  211. logGlobal->info("Installing Chronicles");
  212. extractionFile = -1;
  213. fileCount = exe.size();
  214. int result = ChroniclesInstallResultMask::Success;
  215. for(QString f : exe)
  216. {
  217. extractionFile++;
  218. logGlobal->info("Creating temporary directory");
  219. if(!createTempDir())
  220. continue;
  221. // FIXME: this is required at the moment for Android (and possibly iOS)
  222. // Incoming file names are in content URI form, e.g. content://media/internal/chronicles.exe
  223. // Qt can handle those like it does regular files
  224. // however, innoextract fails to open such files
  225. // so make a copy in directory to which vcmi always has full access and operate on it
  226. QString filepath = tempDir.filePath("chr.exe");
  227. logGlobal->info("Copying offline installer from '%s' to '%s'", f.toStdString(), filepath.toStdString());
  228. Helper::performNativeCopy(f, filepath);
  229. QFile file(filepath);
  230. logGlobal->info("Extracting offline installer");
  231. if(!extractGogInstaller(filepath))
  232. {
  233. result |= ChroniclesInstallResultMask::ExtractError;
  234. continue;
  235. }
  236. logGlobal->info("Detecting Chronicles");
  237. auto chronicleNo = getChronicleNo();
  238. if(chronicleNo.empty())
  239. {
  240. result |= ChroniclesInstallResultMask::InvalidFile;
  241. continue;
  242. }
  243. logGlobal->info("Creating base Chronicle mod");
  244. createBaseMod();
  245. for(const auto & no : chronicleNo)
  246. {
  247. logGlobal->info("Creating Chronicle mod (%i)", no);
  248. createChronicleMod(no);
  249. }
  250. logGlobal->info("Removing temporary directory");
  251. removeTempDir();
  252. }
  253. logGlobal->info("Chronicles installed");
  254. return result;
  255. }