chroniclesextractor.cpp 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  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. ChroniclesExtractor::ChroniclesExtractor(QWidget *p, std::function<void(float percent)> cb) :
  16. parent(p), cb(cb)
  17. {
  18. }
  19. bool ChroniclesExtractor::createTempDir()
  20. {
  21. tempDir = QDir(pathToQString(VCMIDirs::get().userDataPath()));
  22. if(tempDir.cd("tmp"))
  23. {
  24. tempDir.removeRecursively(); // remove if already exists (e.g. previous run)
  25. tempDir.cdUp();
  26. }
  27. tempDir.mkdir("tmp");
  28. if(!tempDir.cd("tmp"))
  29. return false; // should not happen - but avoid deleting wrong folder in any case
  30. return true;
  31. }
  32. void ChroniclesExtractor::removeTempDir()
  33. {
  34. tempDir.removeRecursively();
  35. }
  36. int ChroniclesExtractor::getChronicleNo()
  37. {
  38. QStringList appDirCandidates = tempDir.entryList({"app"}, QDir::Filter::Dirs);
  39. if (!appDirCandidates.empty())
  40. {
  41. QDir appDir = tempDir.filePath(appDirCandidates.front());
  42. for (size_t i = 1; i < chronicles.size(); ++i)
  43. {
  44. QString chronicleName = chronicles.at(i);
  45. QStringList chroniclesDirCandidates = appDir.entryList({chronicleName}, QDir::Filter::Dirs);
  46. if (!chroniclesDirCandidates.empty())
  47. return i;
  48. }
  49. }
  50. QMessageBox::critical(parent, tr("Invalid file selected"), tr("You have to select a Heroes Chronicles installer file!"));
  51. return 0;
  52. }
  53. bool ChroniclesExtractor::extractGogInstaller(QString file)
  54. {
  55. QString errorText = Innoextract::extract(file, tempDir.path(), [this](float progress) {
  56. float overallProgress = ((1.0 / static_cast<float>(fileCount)) * static_cast<float>(extractionFile)) + (progress / static_cast<float>(fileCount));
  57. if(cb)
  58. cb(overallProgress);
  59. });
  60. if(!errorText.isEmpty())
  61. {
  62. QString hashError = Innoextract::getHashError(file, {}, {}, {});
  63. QMessageBox::critical(parent, tr("Extracting error!"), errorText);
  64. if(!hashError.isEmpty())
  65. QMessageBox::critical(parent, tr("Hash error!"), hashError, QMessageBox::Ok, QMessageBox::Ok);
  66. return false;
  67. }
  68. return true;
  69. }
  70. void ChroniclesExtractor::createBaseMod() const
  71. {
  72. QDir dir(pathToQString(VCMIDirs::get().userDataPath() / "Mods"));
  73. dir.mkdir("chronicles");
  74. dir.cd("chronicles");
  75. dir.mkdir("Mods");
  76. QJsonObject mod
  77. {
  78. { "modType", "Expansion" },
  79. { "name", tr("Heroes Chronicles") },
  80. { "description", tr("Heroes Chronicles") },
  81. { "author", "3DO" },
  82. { "version", "1.0" },
  83. { "contact", "vcmi.eu" },
  84. { "heroes", QJsonArray({"config/portraitsChronicles.json"}) },
  85. { "settings", QJsonObject({{"mapFormat", QJsonObject({{"chronicles", QJsonObject({{
  86. {"supported", true},
  87. {"portraits", QJsonObject({
  88. {"portraitTarnumBarbarian", 163},
  89. {"portraitTarnumKnight", 164},
  90. {"portraitTarnumWizard", 165},
  91. {"portraitTarnumRanger", 166},
  92. {"portraitTarnumOverlord", 167},
  93. {"portraitTarnumBeastmaster", 168},
  94. })},
  95. }})}})}})},
  96. };
  97. QFile jsonFile(dir.filePath("mod.json"));
  98. jsonFile.open(QFile::WriteOnly);
  99. jsonFile.write(QJsonDocument(mod).toJson());
  100. for(auto & dataPath : VCMIDirs::get().dataPaths())
  101. {
  102. auto file = pathToQString(dataPath / "config" / "heroes" / "portraitsChronicles.json");
  103. auto destFolder = VCMIDirs::get().userDataPath() / "Mods" / "chronicles" / "content" / "config";
  104. auto destFile = pathToQString(destFolder / "portraitsChronicles.json");
  105. if(QFile::exists(file))
  106. {
  107. QDir().mkpath(pathToQString(destFolder));
  108. QFile::remove(destFile);
  109. QFile::copy(file, destFile);
  110. }
  111. }
  112. }
  113. void ChroniclesExtractor::createChronicleMod(int no)
  114. {
  115. QDir dir(pathToQString(VCMIDirs::get().userDataPath() / "Mods" / "chronicles" / "Mods" / ("chronicles_" + std::to_string(no))));
  116. dir.removeRecursively();
  117. dir.mkpath(".");
  118. QString tmpChronicles = chronicles.at(no);
  119. QJsonObject mod
  120. {
  121. { "modType", "Expansion" },
  122. { "name", QString("%1 - %2").arg(no).arg(tmpChronicles) },
  123. { "description", tr("Heroes Chronicles %1 - %2").arg(no).arg(tmpChronicles) },
  124. { "author", "3DO" },
  125. { "version", "1.0" },
  126. { "contact", "vcmi.eu" },
  127. };
  128. QFile jsonFile(dir.filePath("mod.json"));
  129. jsonFile.open(QFile::WriteOnly);
  130. jsonFile.write(QJsonDocument(mod).toJson());
  131. dir.cd("content");
  132. extractFiles(no);
  133. }
  134. void ChroniclesExtractor::extractFiles(int no) const
  135. {
  136. QString tmpChronicles = chronicles.at(no);
  137. std::string chroniclesDir = "chronicles_" + std::to_string(no);
  138. QDir tmpDir = tempDir.filePath(tempDir.entryList({"app"}, QDir::Filter::Dirs).front());
  139. tmpDir.setPath(tmpDir.filePath(tmpDir.entryList({QString(tmpChronicles)}, QDir::Filter::Dirs).front()));
  140. tmpDir.setPath(tmpDir.filePath(tmpDir.entryList({"data"}, QDir::Filter::Dirs).front()));
  141. auto basePath = VCMIDirs::get().userDataPath() / "Mods" / "chronicles" / "Mods" / chroniclesDir / "content";
  142. QDir outDirDataPortraits(pathToQString(VCMIDirs::get().userDataPath() / "Mods" / "chronicles" / "content" / "Data"));
  143. QDir outDirData(pathToQString(basePath / "Data" / chroniclesDir));
  144. QDir outDirSprites(pathToQString(basePath / "Sprites" / chroniclesDir));
  145. QDir outDirVideo(pathToQString(basePath / "Video" / chroniclesDir));
  146. QDir outDirSounds(pathToQString(basePath / "Sounds" / chroniclesDir));
  147. QDir outDirMaps(pathToQString(basePath / "Maps" / "Chronicles"));
  148. auto extract = [](QDir scrDir, QDir dest, QString file, std::vector<std::string> files = {}){
  149. CArchiveLoader archive("", scrDir.filePath(scrDir.entryList({file}).front()).toStdString(), false);
  150. for(auto & entry : archive.getEntries())
  151. if(files.empty())
  152. archive.extractToFolder(dest.absolutePath().toStdString(), "", entry.second, true);
  153. else
  154. {
  155. for(const auto & item : files)
  156. if(boost::algorithm::to_lower_copy(entry.second.name).find(boost::algorithm::to_lower_copy(item)) != std::string::npos)
  157. archive.extractToFolder(dest.absolutePath().toStdString(), "", entry.second, true);
  158. }
  159. };
  160. extract(tmpDir, outDirData, "xBitmap.lod");
  161. extract(tmpDir, outDirData, "xlBitmap.lod");
  162. extract(tmpDir, outDirSprites, "xSprite.lod");
  163. extract(tmpDir, outDirSprites, "xlSprite.lod");
  164. extract(tmpDir, outDirVideo, "xVideo.vid");
  165. extract(tmpDir, outDirSounds, "xSound.snd");
  166. tmpDir.cdUp();
  167. 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
  168. {
  169. QDir tmpDirMaps = tmpDir.filePath(tmpDir.entryList({"maps"}, QDir::Filter::Dirs).front());
  170. for(const auto & entry : tmpDirMaps.entryList())
  171. QFile(tmpDirMaps.filePath(entry)).copy(outDirData.filePath(entry));
  172. }
  173. tmpDir.cdUp();
  174. QDir tmpDirData = tmpDir.filePath(tmpDir.entryList({"data"}, QDir::Filter::Dirs).front());
  175. auto tarnumPortraits = std::vector<std::string>{"HPS137", "HPS138", "HPS139", "HPS140", "HPS141", "HPS142", "HPL137", "HPL138", "HPL139", "HPL140", "HPL141", "HPL142"};
  176. extract(tmpDirData, outDirDataPortraits, "bitmap.lod", tarnumPortraits);
  177. extract(tmpDirData, outDirData, "lbitmap.lod", std::vector<std::string>{"INTRORIM"});
  178. if(!outDirMaps.exists())
  179. outDirMaps.mkpath(".");
  180. QString campaignFileName = "Hc" + QString::number(no) + "_Main.h3c";
  181. QFile(outDirData.filePath(outDirData.entryList({"Main.h3c"}).front())).copy(outDirMaps.filePath(campaignFileName));
  182. }
  183. void ChroniclesExtractor::installChronicles(QStringList exe)
  184. {
  185. logGlobal->info("Installing Chronicles");
  186. extractionFile = -1;
  187. fileCount = exe.size();
  188. for(QString f : exe)
  189. {
  190. extractionFile++;
  191. logGlobal->info("Creating temporary directory");
  192. if(!createTempDir())
  193. continue;
  194. logGlobal->info("Copying offline installer");
  195. // FIXME: this is required at the moment for Android (and possibly iOS)
  196. // Incoming file names are in content URI form, e.g. content://media/internal/chronicles.exe
  197. // Qt can handle those like it does regular files
  198. // however, innoextract fails to open such files
  199. // so make a copy in directory to which vcmi always has full access and operate on it
  200. QString filepath = tempDir.filePath("chr.exe");
  201. QFile(f).copy(filepath);
  202. QFile file(filepath);
  203. logGlobal->info("Extracting offline installer");
  204. if(!extractGogInstaller(filepath))
  205. continue;
  206. logGlobal->info("Detecting Chronicle");
  207. int chronicleNo = getChronicleNo();
  208. if(!chronicleNo)
  209. continue;
  210. logGlobal->info("Creating base Chronicle mod");
  211. createBaseMod();
  212. logGlobal->info("Creating Chronicle mod");
  213. createChronicleMod(chronicleNo);
  214. logGlobal->info("Removing temporary directory");
  215. removeTempDir();
  216. }
  217. logGlobal->info("Chronicles installed");
  218. }