ClientInstaller.qml 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. import QtQuick 2.0
  2. import QtQuick.Controls 2.15
  3. import QtQml.Models 2.15
  4. import FileIO 1.0
  5. import "../view"
  6. Item {
  7. id: clientInstaller
  8. signal predownloadFinished()
  9. signal installError()
  10. signal installFinished()
  11. signal installCanceled()
  12. signal installSelectLanguagesCanceled()
  13. readonly property bool isActive: installerState != stateIdle && installerState != stateDone
  14. property string progressText: ""
  15. property int progressPercent: 0
  16. property int installerState: stateIdle
  17. readonly property int stateIdle: 0
  18. readonly property int stateDownloading: 1
  19. readonly property int stateRevertingWinePatch: 2
  20. readonly property int stateUnarchiving: 3
  21. readonly property int stateApplyingWinePatch: 5
  22. readonly property int stateReapplyingWinePatch: 6
  23. readonly property int stateDone: 7
  24. property string installingVersion
  25. property var currentFile
  26. property var filesToDownload: []
  27. property var downloadedFiles: []
  28. property var installedFiles: []
  29. property var unappliedRemotes: []
  30. readonly property string voicePackPath: "/GenshinImpact_Data/StreamingAssets/Audio/GeneratedSoundBanks/Windows/"
  31. readonly property string persistentDir: "/GenshinImpact_Data/Persistent/"
  32. readonly property string audioLangPath: persistentDir + "audio_lang_14"
  33. readonly property var langMap: ({
  34. "zh-cn": "Chinese",
  35. "en-us": "English(US)",
  36. "ja-jp": "Japanese",
  37. "ko-kr": "Korean"
  38. })
  39. readonly property string preDownloadVersionFile: downloader.downloadDir + 'pre-download.version'
  40. property var checkedLanguages: ({})
  41. Downloader {
  42. id: downloader
  43. downloadDir: `${settings.gamePath}../_update_gi_download/`
  44. multiConnections: settings.multiConnections
  45. limitDownload: settings.limitDownload
  46. multiConnectionsValue: settings.multiConnectionsValue
  47. limitDownloadValue: settings.limitDownloadValue
  48. onProgressChanged: {
  49. let downloadingCount = downloadedFiles.length + 1
  50. let totalCount = filesToDownload.length + downloadedFiles.length + 1
  51. const na = qsTr("N/A")
  52. clientInstaller.progressText = downloadingCount + " " + qsTr("of") + " " + totalCount
  53. + ", " + qsTr("Downloaded: ") + (progress.downloaded || na) + "/" + (progress.total || na)
  54. + ", " + qsTr("Speed: ") + (progress.speed ? (progress.speed + "/" + qsTr("s")) : na)
  55. + ", " + qsTr("Estimated: ") + (progress.estimated || na)
  56. progressPercent = progress.percent || 0
  57. }
  58. onFinished: {
  59. downloadedFiles.push(currentFile)
  60. downloadNextFile()
  61. }
  62. onError: {
  63. if (!canceled) {
  64. ui.log(qsTr("Failed to download file") + errors)
  65. handleError()
  66. }
  67. }
  68. }
  69. WinePatcher {
  70. id: winePatcher
  71. gameVersion: availableVersion || localVersionLoader.version
  72. gamePath: settings.gamePath
  73. anchors.fill: parent
  74. onPatchReverted: {
  75. if (installerState === stateReapplyingWinePatch) {
  76. applyWinePatch()
  77. } else {
  78. installerState = stateUnarchiving
  79. installNextArchive()
  80. }
  81. }
  82. onErrorRevertingPatch: handleError()
  83. onPatchApplied: {
  84. installerState = stateDone
  85. installFinished()
  86. }
  87. onErrorApplyingPatch: handleError()
  88. onPatchCanceled: handleCancel()
  89. }
  90. ArchiveInstaller {
  91. id: archiveInstaller
  92. outputDir: settings.gamePath
  93. onProgressChanged: {
  94. let installingCount = installedFiles.length + 1
  95. let totalCount = installedFiles.length + downloadedFiles.length + 1
  96. const na = qsTr("N/A")
  97. let progressText = installingCount + " " + qsTr("of") + " " + totalCount
  98. + ", " + progress.status + ": " + currentFile.file
  99. if (progress.total) {
  100. progressText += ", " + qsTr("Unpacked: ") + (progress.unarchived || na) + "/" + progress.total
  101. }
  102. clientInstaller.progressText = progressText
  103. progressPercent = progress.percent || 0
  104. }
  105. onFinished: {
  106. installedFiles.push(currentFile)
  107. installHDiffPatches()
  108. }
  109. onError: handleError()
  110. }
  111. HDiffPatcher {
  112. id: hdiffPatcher
  113. outputDir: settings.gamePath
  114. onProgressChanged: {
  115. clientInstaller.progressText = qsTr("Installing ")
  116. + progress.value + " " + qsTr("of") + " " + progress.total
  117. progressPercent = progress.value * 100.0 / progress.total
  118. }
  119. onFinished: {
  120. if (unappliedRemotes.length !== 0) {
  121. unappliedRemotes = []
  122. return
  123. }
  124. installNextArchive()
  125. }
  126. onError: handleError()
  127. }
  128. Dialog {
  129. id: dialogLanguages
  130. anchors.centerIn: parent
  131. title: window.title
  132. standardButtons: Dialog.Ok | Dialog.Cancel
  133. modal: true
  134. focus: true
  135. width: block * 6
  136. height: block * 3 + block / 2 * listLanguages.count
  137. ListView {
  138. id: listLanguages
  139. property int count: 0
  140. interactive: false
  141. anchors.fill: parent
  142. model: ListModel {
  143. }
  144. delegate: Button {
  145. checkBox: true
  146. checkable: true
  147. checked: checkedLanguages[model.language] || false
  148. title: model.name
  149. width: listLanguages.width
  150. height: block / 2
  151. border.width: 0
  152. color: "transparent"
  153. horizontalAlignment: Text.AlignLeft
  154. onClicked: {
  155. checkedLanguages[model.language] = checked
  156. }
  157. }
  158. }
  159. function show() {
  160. listLanguages.model.clear()
  161. checkedLanguages = {}
  162. let audioLangs = readAudioLangs()
  163. var languages = []
  164. let voicePacks = webVersionLoader.json.data.game.latest.voice_packs
  165. for (let pack of voicePacks) {
  166. checkedLanguages[pack.language] = audioLangs.indexOf(langMap[pack.language]) !== -1
  167. languages.push(pack.language)
  168. listLanguages.model.append({
  169. language: pack.language,
  170. name: langMap[pack.language] || pack.language
  171. })
  172. }
  173. listLanguages.count = languages.length
  174. open()
  175. }
  176. function readAudioLangs() {
  177. let audioLangs
  178. let audioLangsText = FileIO.readTextFile(settings.gamePath + audioLangPath)
  179. if (audioLangsText) {
  180. audioLangs = audioLangsText.split("\n")
  181. } else {
  182. audioLangs = []
  183. for (let lang in langMap) {
  184. audioLangs.push(langMap[lang])
  185. }
  186. }
  187. return audioLangs
  188. }
  189. function writeAudioLangs() {
  190. let audioLangsText = ""
  191. for (let lang in langMap) {
  192. if (checkedLanguages[lang]) {
  193. audioLangsText += langMap[lang] + "\n"
  194. }
  195. }
  196. FileIO.createDirectory(settings.gamePath + persistentDir)
  197. FileIO.writeTextFile(settings.gamePath + audioLangPath, audioLangsText)
  198. }
  199. onAccepted: {
  200. writeAudioLangs()
  201. performInstallOrUpdate()
  202. }
  203. onRejected: installSelectLanguagesCanceled()
  204. }
  205. function handleError() {
  206. clientInstaller.installerState = clientInstaller.stateIdle
  207. installError()
  208. }
  209. function handleCancel() {
  210. clientInstaller.installerState = clientInstaller.stateIdle
  211. installCanceled()
  212. }
  213. function start() {
  214. if (ui.uiState == ui.uiStateInstalling) {
  215. dialogLanguages.show()
  216. } else {
  217. performInstallOrUpdate()
  218. }
  219. }
  220. function performInstallOrUpdate() {
  221. let isInstalling = ui.uiState == ui.uiStateInstalling
  222. let gamePath = settings.gamePath
  223. let data = webVersionLoader.json.data
  224. let game = isPreDownloading ? data.pre_download_game : data.game
  225. let availableVersion = game.latest.version
  226. if (availableVersion === installedVersion) {
  227. // shall never happen
  228. ui.log(qsTr("Already up to date"))
  229. ui.uiState = ui.uiStateInstall
  230. return
  231. }
  232. installingVersion = availableVersion
  233. ui.log("")
  234. let diffToInstall = null
  235. if (installedVersion) {
  236. for (let diff of game.diffs) {
  237. if (diff.version === installedVersion) {
  238. diffToInstall = diff
  239. break
  240. }
  241. }
  242. if (!diffToInstall) {
  243. ui.log(qsTr("No compatible patch found, performing full install"))
  244. }
  245. }
  246. // TODO check for available free disk space
  247. if (diffToInstall) {
  248. ui.log((isPreDownloading ? qsTr("Pre-downloading client update") : qsTr("Updating client"))
  249. + ` ${installedVersion} => ${availableVersion}...`)
  250. } else {
  251. ui.log((isPreDownloading ? qsTr("Pre-downloading client version") : qsTr("Installing client version"))
  252. + ` ${availableVersion}...`)
  253. }
  254. var files = []
  255. var itemToFile = (item) => {
  256. let fileName = item.path.replace(/.*\/([^\/]+)(\?.*)?$/, "$1")
  257. return { lang: item.language, url: item.path, md5: item.md5, file: fileName }
  258. }
  259. let nodeToInstall = diffToInstall || game.latest
  260. files.push(itemToFile(nodeToInstall))
  261. for (let voicePack of nodeToInstall.voice_packs) {
  262. let installPack = isInstalling
  263. ? checkedLanguages[voicePack.language]
  264. : FileIO.isFileExists(gamePath + voicePackPath + langMap[voicePack.language])
  265. if (installPack) {
  266. files.push(itemToFile(voicePack))
  267. }
  268. }
  269. filesToDownload = files
  270. downloadedFiles = []
  271. currentFile = null
  272. installedFiles = []
  273. FileIO.createDirectory(gamePath)
  274. installerState = stateDownloading
  275. downloadNextFile()
  276. }
  277. function getPreDownloadedVersion() {
  278. return FileIO.isFileExists(preDownloadVersionFile) ? FileIO.readTextFile(preDownloadVersionFile) : null
  279. }
  280. function downloadNextFile() {
  281. currentFile = filesToDownload.shift()
  282. if (!currentFile) {
  283. if (!isPreDownloading) {
  284. revertWinePatch()
  285. } else {
  286. installerState = stateDone
  287. ui.log(qsTr("Pre-download was finished successfully"))
  288. FileIO.writeTextFile(preDownloadVersionFile, preDownloadVersion)
  289. predownloadFinished()
  290. }
  291. return
  292. }
  293. downloader.download(currentFile.url, currentFile.file, currentFile.md5)
  294. }
  295. function reapplyWinePatch() {
  296. installerState = stateReapplyingWinePatch
  297. winePatcher.revertPatch()
  298. }
  299. function revertWinePatch() {
  300. installerState = stateRevertingWinePatch
  301. winePatcher.revertPatch()
  302. }
  303. function installNextArchive() {
  304. currentFile = downloadedFiles.shift()
  305. if (!currentFile) {
  306. FileIO.removeDirectory(downloader.downloadDir)
  307. localVersionLoader.version = installingVersion
  308. localVersionLoader.save()
  309. integrityCheck.reload()
  310. finishInstallingWithoutPatching()
  311. return
  312. }
  313. ui.log(qsTr("Installing") + " " + currentFile.file + "...")
  314. archiveInstaller.install(downloader.downloadDir + currentFile.file)
  315. }
  316. function installHDiffPatches() {
  317. if (hdiffPatcher.isInstallNeeded()) {
  318. hdiffPatcher.installAll()
  319. } else {
  320. installNextArchive()
  321. }
  322. }
  323. function checkForSkippedHDiffs() {
  324. let gamePath = settings.gamePath
  325. if (gamePath) {
  326. let voiceHdiffs = FileIO.list(gamePath + voicePackPath, "*.hdiff")
  327. if (voiceHdiffs.length !== 0) {
  328. unappliedRemotes = []
  329. for (let path of voiceHdiffs) {
  330. let remote = path.replace(gamePath, "")
  331. .replace(/^\//, "")
  332. .replace(/\.hdiff$/, "")
  333. unappliedRemotes.push(remote)
  334. }
  335. dialogFixHDiffs.open()
  336. }
  337. }
  338. }
  339. function finishInstallingWithoutPatching() {
  340. // do not start applyWinePatch() here
  341. // it's intended that user clicks "Patch" button
  342. installerState = stateDone
  343. installFinished()
  344. }
  345. function applyWinePatch() {
  346. installerState = stateApplyingWinePatch
  347. winePatcher.applyPatch()
  348. }
  349. function isHdiffPatchInstalled() {
  350. return !hdiffPatcher.isInstallNeeded()
  351. }
  352. function isWinePatchInstalled() {
  353. return winePatcher.isPatchInstalled()
  354. }
  355. function cancel() {
  356. if (downloader.isRunning) {
  357. downloader.cancel()
  358. }
  359. if (archiveInstaller.isRunning) {
  360. archiveInstaller.cancel()
  361. }
  362. if (hdiffPatcher.isRunning) {
  363. hdiffPatcher.cancel()
  364. }
  365. installerState = stateIdle
  366. }
  367. Dialog {
  368. id: dialogFixHDiffs
  369. anchors.centerIn: parent
  370. title: window.title
  371. standardButtons: Dialog.Yes | Dialog.No
  372. modal: true
  373. focus: true
  374. Text {
  375. id: textDialog
  376. anchors.fill: parent
  377. text: qsTr("Unapplied HDiff patches detected.\nDo you wish to install them now?")
  378. }
  379. onAccepted: hdiffPatcher.installByList(unappliedRemotes)
  380. onRejected: unappliedRemotes = []
  381. }
  382. Component.onCompleted: {
  383. checkForSkippedHDiffs()
  384. }
  385. }