123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463 |
- import QtQuick 2.0
- import QtQuick.Controls 2.15
- import QtQml.Models 2.15
- import FileIO 1.0
- import "../view"
- Item {
- id: clientInstaller
- signal predownloadFinished()
- signal installError()
- signal installFinished()
- signal installCanceled()
- signal installSelectLanguagesCanceled()
- readonly property bool isActive: installerState != stateIdle && installerState != stateDone
- property string progressText: ""
- property int progressPercent: 0
- property int installerState: stateIdle
- readonly property int stateIdle: 0
- readonly property int stateDownloading: 1
- readonly property int stateRevertingWinePatch: 2
- readonly property int stateUnarchiving: 3
- readonly property int stateApplyingWinePatch: 5
- readonly property int stateReapplyingWinePatch: 6
- readonly property int stateDone: 7
- property string installingVersion
- property var currentFile
- property var filesToDownload: []
- property var downloadedFiles: []
- property var installedFiles: []
- property var unappliedRemotes: []
- readonly property string voicePackPath: "/GenshinImpact_Data/StreamingAssets/Audio/GeneratedSoundBanks/Windows/"
- readonly property string persistentDir: "/GenshinImpact_Data/Persistent/"
- readonly property string audioLangPath: persistentDir + "audio_lang_14"
- readonly property var langMap: ({
- "zh-cn": "Chinese",
- "en-us": "English(US)",
- "ja-jp": "Japanese",
- "ko-kr": "Korean"
- })
- readonly property string preDownloadVersionFile: downloader.downloadDir + 'pre-download.version'
- property var checkedLanguages: ({})
- Downloader {
- id: downloader
- downloadDir: `${settings.gamePath}../_update_gi_download/`
- multiConnections: settings.multiConnections
- limitDownload: settings.limitDownload
- multiConnectionsValue: settings.multiConnectionsValue
- limitDownloadValue: settings.limitDownloadValue
- onProgressChanged: {
- let downloadingCount = downloadedFiles.length + 1
- let totalCount = filesToDownload.length + downloadedFiles.length + 1
- const na = qsTr("N/A")
- clientInstaller.progressText = downloadingCount + " " + qsTr("of") + " " + totalCount
- + ", " + qsTr("Downloaded: ") + (progress.downloaded || na) + "/" + (progress.total || na)
- + ", " + qsTr("Speed: ") + (progress.speed ? (progress.speed + "/" + qsTr("s")) : na)
- + ", " + qsTr("Estimated: ") + (progress.estimated || na)
- progressPercent = progress.percent || 0
- }
- onFinished: {
- downloadedFiles.push(currentFile)
- downloadNextFile()
- }
- onError: {
- if (!canceled) {
- ui.log(qsTr("Failed to download file") + errors)
- handleError()
- }
- }
- }
- WinePatcher {
- id: winePatcher
- gameVersion: availableVersion || localVersionLoader.version
- gamePath: settings.gamePath
- anchors.fill: parent
- onPatchReverted: {
- if (installerState === stateReapplyingWinePatch) {
- applyWinePatch()
- } else {
- installerState = stateUnarchiving
- installNextArchive()
- }
- }
- onErrorRevertingPatch: handleError()
- onPatchApplied: {
- installerState = stateDone
- installFinished()
- }
- onErrorApplyingPatch: handleError()
- onPatchCanceled: handleCancel()
- }
- ArchiveInstaller {
- id: archiveInstaller
- outputDir: settings.gamePath
- onProgressChanged: {
- let installingCount = installedFiles.length + 1
- let totalCount = installedFiles.length + downloadedFiles.length + 1
- const na = qsTr("N/A")
- let progressText = installingCount + " " + qsTr("of") + " " + totalCount
- + ", " + progress.status + ": " + currentFile.file
- if (progress.total) {
- progressText += ", " + qsTr("Unpacked: ") + (progress.unarchived || na) + "/" + progress.total
- }
- clientInstaller.progressText = progressText
- progressPercent = progress.percent || 0
- }
- onFinished: {
- installedFiles.push(currentFile)
- installHDiffPatches()
- }
- onError: handleError()
- }
- HDiffPatcher {
- id: hdiffPatcher
- outputDir: settings.gamePath
- onProgressChanged: {
- clientInstaller.progressText = qsTr("Installing ")
- + progress.value + " " + qsTr("of") + " " + progress.total
- progressPercent = progress.value * 100.0 / progress.total
- }
- onFinished: {
- if (unappliedRemotes.length !== 0) {
- unappliedRemotes = []
- return
- }
- installNextArchive()
- }
- onError: handleError()
- }
- Dialog {
- id: dialogLanguages
- anchors.centerIn: parent
- title: window.title
- standardButtons: Dialog.Ok | Dialog.Cancel
- modal: true
- focus: true
- width: block * 6
- height: block * 3 + block / 2 * listLanguages.count
- ListView {
- id: listLanguages
- property int count: 0
- interactive: false
- anchors.fill: parent
- model: ListModel {
- }
- delegate: Button {
- checkBox: true
- checkable: true
- checked: checkedLanguages[model.language] || false
- title: model.name
- width: listLanguages.width
- height: block / 2
- border.width: 0
- color: "transparent"
- horizontalAlignment: Text.AlignLeft
- onClicked: {
- checkedLanguages[model.language] = checked
- }
- }
- }
- function show() {
- listLanguages.model.clear()
- checkedLanguages = {}
- let audioLangs = readAudioLangs()
- var languages = []
- let voicePacks = webVersionLoader.json.data.game.latest.voice_packs
- for (let pack of voicePacks) {
- checkedLanguages[pack.language] = audioLangs.indexOf(langMap[pack.language]) !== -1
- languages.push(pack.language)
- listLanguages.model.append({
- language: pack.language,
- name: langMap[pack.language] || pack.language
- })
- }
- listLanguages.count = languages.length
- open()
- }
- function readAudioLangs() {
- let audioLangs
- let audioLangsText = FileIO.readTextFile(settings.gamePath + audioLangPath)
- if (audioLangsText) {
- audioLangs = audioLangsText.split("\n")
- } else {
- audioLangs = []
- for (let lang in langMap) {
- audioLangs.push(langMap[lang])
- }
- }
- return audioLangs
- }
- function writeAudioLangs() {
- let audioLangsText = ""
- for (let lang in langMap) {
- if (checkedLanguages[lang]) {
- audioLangsText += langMap[lang] + "\n"
- }
- }
- FileIO.createDirectory(settings.gamePath + persistentDir)
- FileIO.writeTextFile(settings.gamePath + audioLangPath, audioLangsText)
- }
- onAccepted: {
- writeAudioLangs()
- performInstallOrUpdate()
- }
- onRejected: installSelectLanguagesCanceled()
- }
- function handleError() {
- clientInstaller.installerState = clientInstaller.stateIdle
- installError()
- }
- function handleCancel() {
- clientInstaller.installerState = clientInstaller.stateIdle
- installCanceled()
- }
- function start() {
- if (ui.uiState == ui.uiStateInstalling) {
- dialogLanguages.show()
- } else {
- performInstallOrUpdate()
- }
- }
- function performInstallOrUpdate() {
- let isInstalling = ui.uiState == ui.uiStateInstalling
- let gamePath = settings.gamePath
- let data = webVersionLoader.json.data
- let game = isPreDownloading ? data.pre_download_game : data.game
- let availableVersion = game.latest.version
- if (availableVersion === installedVersion) {
- // shall never happen
- ui.log(qsTr("Already up to date"))
- ui.uiState = ui.uiStateInstall
- return
- }
- installingVersion = availableVersion
- ui.log("")
- let diffToInstall = null
- if (installedVersion) {
- for (let diff of game.diffs) {
- if (diff.version === installedVersion) {
- diffToInstall = diff
- break
- }
- }
- if (!diffToInstall) {
- ui.log(qsTr("No compatible patch found, performing full install"))
- }
- }
- // TODO check for available free disk space
- if (diffToInstall) {
- ui.log((isPreDownloading ? qsTr("Pre-downloading client update") : qsTr("Updating client"))
- + ` ${installedVersion} => ${availableVersion}...`)
- } else {
- ui.log((isPreDownloading ? qsTr("Pre-downloading client version") : qsTr("Installing client version"))
- + ` ${availableVersion}...`)
- }
- var files = []
- var itemToFile = (item) => {
- let fileName = item.path.replace(/.*\/([^\/]+)(\?.*)?$/, "$1")
- return { lang: item.language, url: item.path, md5: item.md5, file: fileName }
- }
- let nodeToInstall = diffToInstall || game.latest
- files.push(itemToFile(nodeToInstall))
- for (let voicePack of nodeToInstall.voice_packs) {
- let installPack = isInstalling
- ? checkedLanguages[voicePack.language]
- : FileIO.isFileExists(gamePath + voicePackPath + langMap[voicePack.language])
- if (installPack) {
- files.push(itemToFile(voicePack))
- }
- }
- filesToDownload = files
- downloadedFiles = []
- currentFile = null
- installedFiles = []
- FileIO.createDirectory(gamePath)
- installerState = stateDownloading
- downloadNextFile()
- }
- function getPreDownloadedVersion() {
- return FileIO.isFileExists(preDownloadVersionFile) ? FileIO.readTextFile(preDownloadVersionFile) : null
- }
- function downloadNextFile() {
- currentFile = filesToDownload.shift()
- if (!currentFile) {
- if (!isPreDownloading) {
- revertWinePatch()
- } else {
- installerState = stateDone
- ui.log(qsTr("Pre-download was finished successfully"))
- FileIO.writeTextFile(preDownloadVersionFile, preDownloadVersion)
- predownloadFinished()
- }
- return
- }
- downloader.download(currentFile.url, currentFile.file, currentFile.md5)
- }
- function reapplyWinePatch() {
- installerState = stateReapplyingWinePatch
- winePatcher.revertPatch()
- }
- function revertWinePatch() {
- installerState = stateRevertingWinePatch
- winePatcher.revertPatch()
- }
- function installNextArchive() {
- currentFile = downloadedFiles.shift()
- if (!currentFile) {
- FileIO.removeDirectory(downloader.downloadDir)
- localVersionLoader.version = installingVersion
- localVersionLoader.save()
- integrityCheck.reload()
- finishInstallingWithoutPatching()
- return
- }
- ui.log(qsTr("Installing") + " " + currentFile.file + "...")
- archiveInstaller.install(downloader.downloadDir + currentFile.file)
- }
- function installHDiffPatches() {
- if (hdiffPatcher.isInstallNeeded()) {
- hdiffPatcher.installAll()
- } else {
- installNextArchive()
- }
- }
- function checkForSkippedHDiffs() {
- let gamePath = settings.gamePath
- if (gamePath) {
- let voiceHdiffs = FileIO.list(gamePath + voicePackPath, "*.hdiff")
- if (voiceHdiffs.length !== 0) {
- unappliedRemotes = []
- for (let path of voiceHdiffs) {
- let remote = path.replace(gamePath, "")
- .replace(/^\//, "")
- .replace(/\.hdiff$/, "")
- unappliedRemotes.push(remote)
- }
- dialogFixHDiffs.open()
- }
- }
- }
- function finishInstallingWithoutPatching() {
- // do not start applyWinePatch() here
- // it's intended that user clicks "Patch" button
- installerState = stateDone
- installFinished()
- }
- function applyWinePatch() {
- installerState = stateApplyingWinePatch
- winePatcher.applyPatch()
- }
- function isHdiffPatchInstalled() {
- return !hdiffPatcher.isInstallNeeded()
- }
- function isWinePatchInstalled() {
- return winePatcher.isPatchInstalled()
- }
- function cancel() {
- if (downloader.isRunning) {
- downloader.cancel()
- }
- if (archiveInstaller.isRunning) {
- archiveInstaller.cancel()
- }
- if (hdiffPatcher.isRunning) {
- hdiffPatcher.cancel()
- }
- installerState = stateIdle
- }
- Dialog {
- id: dialogFixHDiffs
- anchors.centerIn: parent
- title: window.title
- standardButtons: Dialog.Yes | Dialog.No
- modal: true
- focus: true
- Text {
- id: textDialog
- anchors.fill: parent
- text: qsTr("Unapplied HDiff patches detected.\nDo you wish to install them now?")
- }
- onAccepted: hdiffPatcher.installByList(unappliedRemotes)
- onRejected: unappliedRemotes = []
- }
- Component.onCompleted: {
- checkForSkippedHDiffs()
- }
- }
|