From f893ca7b0d94bfe1e78cdbc7541034e4d02d657e Mon Sep 17 00:00:00 2001 From: Lucki Date: Fri, 19 Mar 2021 13:50:58 +0100 Subject: [PATCH 01/22] Basic stuff working --- data/gamehub.gschema.xml.in | 27 + res/icons/icons.gresource.xml | 1 + res/icons/symbolic/sources/epicgames.svg | 30 + src/app.vala | 3 +- src/data/GameSource.vala | 1 + src/data/db/tables/Games.vala | 9 + .../runnables/tasks/install/InstallTask.vala | 6 + .../runnables/tasks/install/Installer.vala | 18 +- src/data/sources/epicgames/EpicAnalysis.vala | 621 +++++++ src/data/sources/epicgames/EpicChunk.vala | 191 ++ .../sources/epicgames/EpicDownloader.vala | 814 +++++++++ src/data/sources/epicgames/EpicGame.vala | 1575 +++++++++++++++++ src/data/sources/epicgames/EpicGames.vala | 831 +++++++++ .../sources/epicgames/EpicGamesServices.vala | 431 +++++ src/data/sources/epicgames/EpicInstaller.vala | 246 +++ src/data/sources/epicgames/EpicManifest.vala | 1180 ++++++++++++ src/data/sources/epicgames/EpicUtils.vala | 171 ++ src/meson.build | 11 + src/settings/Auth.vala | 25 + src/settings/Paths.vala | 24 + .../SettingsDialog/SettingsDialog.vala | 1 + .../pages/sources/EpicGames.vala | 140 ++ src/ui/views/GamesView/grid/GameCard.vala | 5 + src/ui/views/GamesView/list/GameListRow.vala | 5 + src/utils/fs/FS.vala | 7 + 25 files changed, 6365 insertions(+), 8 deletions(-) create mode 100644 res/icons/symbolic/sources/epicgames.svg create mode 100644 src/data/sources/epicgames/EpicAnalysis.vala create mode 100644 src/data/sources/epicgames/EpicChunk.vala create mode 100644 src/data/sources/epicgames/EpicDownloader.vala create mode 100644 src/data/sources/epicgames/EpicGame.vala create mode 100644 src/data/sources/epicgames/EpicGames.vala create mode 100644 src/data/sources/epicgames/EpicGamesServices.vala create mode 100644 src/data/sources/epicgames/EpicInstaller.vala create mode 100644 src/data/sources/epicgames/EpicManifest.vala create mode 100644 src/data/sources/epicgames/EpicUtils.vala create mode 100644 src/ui/dialogs/SettingsDialog/pages/sources/EpicGames.vala diff --git a/data/gamehub.gschema.xml.in b/data/gamehub.gschema.xml.in index 546d5068..729e887a 100644 --- a/data/gamehub.gschema.xml.in +++ b/data/gamehub.gschema.xml.in @@ -154,6 +154,21 @@ + + + true + Is EpicGames enabled + + + false + Is user authenticated + + + '' + EpicGames userdata + + + true @@ -215,6 +230,18 @@ + + + + ['~/Games/EpicGames', '~/EpicGames Games'] + EpicGames game directories + + + '~/Games/EpicGames' + Default EpicGames games directory + + + diff --git a/res/icons/icons.gresource.xml b/res/icons/icons.gresource.xml index 69e02036..8230ce40 100644 --- a/res/icons/icons.gresource.xml +++ b/res/icons/icons.gresource.xml @@ -3,6 +3,7 @@ symbolic/sources/sources-all.svg symbolic/sources/steam.svg + symbolic/sources/epicgames.svg symbolic/sources/gog.svg symbolic/sources/humble.svg symbolic/sources/humble-trove.svg diff --git a/res/icons/symbolic/sources/epicgames.svg b/res/icons/symbolic/sources/epicgames.svg new file mode 100644 index 00000000..7c488641 --- /dev/null +++ b/res/icons/symbolic/sources/epicgames.svg @@ -0,0 +1,30 @@ + +image/svg+xml diff --git a/src/app.vala b/src/app.vala index f79c16cb..89410580 100644 --- a/src/app.vala +++ b/src/app.vala @@ -23,6 +23,7 @@ using Gee; using GameHub.Data; using GameHub.Data.DB; using GameHub.Data.Sources.Steam; +using GameHub.Data.Sources.EpicGames; using GameHub.Data.Sources.GOG; using GameHub.Data.Sources.Humble; using GameHub.Data.Sources.Itch; @@ -141,7 +142,7 @@ namespace GameHub ImageCache.init(); Database.create(); - GameSources = { new Steam(), new GOG(), new Humble(), new Trove(), new Itch(), new User() }; + GameSources = { new Steam(), new EpicGames(), new GOG(), new Humble(), new Trove(), new Itch(), new User() }; Providers.ImageProviders = { new Providers.Images.Steam(), new Providers.Images.SteamGridDB(), new Providers.Images.JinxSGVI() }; Providers.DataProviders = { new Providers.Data.IGDB() }; diff --git a/src/data/GameSource.vala b/src/data/GameSource.vala index d1d2fd87..c4519d6c 100644 --- a/src/data/GameSource.vala +++ b/src/data/GameSource.vala @@ -21,6 +21,7 @@ using Gee; using GameHub.Utils; using GameHub.Data.Runnables; using GameHub.Data.Sources.Steam; +using GameHub.Data.Sources.EpicGames; using GameHub.Data.Sources.GOG; namespace GameHub.Data diff --git a/src/data/db/tables/Games.vala b/src/data/db/tables/Games.vala index a2a1cbc1..1ed89b50 100644 --- a/src/data/db/tables/Games.vala +++ b/src/data/db/tables/Games.vala @@ -23,6 +23,7 @@ using GameHub.Utils; using GameHub.Data.Runnables; using GameHub.Data.Sources.Steam; +using GameHub.Data.Sources.EpicGames; using GameHub.Data.Sources.GOG; using GameHub.Data.Sources.Humble; using GameHub.Data.Sources.Itch; @@ -339,6 +340,10 @@ namespace GameHub.Data.DB.Tables { g = new SteamGame.from_db((Steam) s, st); } + else if(s is EpicGames) + { + g = new EpicGame.from_db((EpicGames) s, st); + } else if(s is GOG) { g = new GOGGame.from_db((GOG) s, st); @@ -424,6 +429,10 @@ namespace GameHub.Data.DB.Tables { g = new SteamGame.from_db((Steam) s, st); } + else if(s is EpicGames) + { + g = new EpicGame.from_db((EpicGames) s, st); + } else if(s is GOG) { g = new GOGGame.from_db((GOG) s, st); diff --git a/src/data/runnables/tasks/install/InstallTask.vala b/src/data/runnables/tasks/install/InstallTask.vala index 7d483164..28a3422c 100644 --- a/src/data/runnables/tasks/install/InstallTask.vala +++ b/src/data/runnables/tasks/install/InstallTask.vala @@ -140,6 +140,12 @@ namespace GameHub.Data.Runnables.Tasks.Install if(cancelled) return; if(install_dir_imported) { + // FIXME: hack to be able to do stuff on import + if(selected_installer.can_import) + { + yield selected_installer.import(this); + } + warning("[InstallTask.install] Installation directory was imported, skipping installation"); return; } diff --git a/src/data/runnables/tasks/install/Installer.vala b/src/data/runnables/tasks/install/Installer.vala index 4550767f..dc3bc30c 100644 --- a/src/data/runnables/tasks/install/Installer.vala +++ b/src/data/runnables/tasks/install/Installer.vala @@ -26,13 +26,16 @@ namespace GameHub.Data.Runnables.Tasks.Install { public abstract class Installer: BaseObject { - public string id { get; protected set; } - public string name { get; protected set; } - public Platform platform { get; protected set; default = Platform.CURRENT; } - public int64 full_size { get; protected set; default = 0; } - public string? version { get; protected set; } - public string? language { get; protected set; } - public string? language_name { get; protected set; } + public string id { get; protected set; } + public string name { get; protected set; } + public Platform platform { get; protected set; default = Platform.CURRENT; } + public int64 full_size { get; protected set; default = 0; } + public string? version { get; protected set; } + public string? language { get; protected set; } + public string? language_name { get; protected set; } + + // allow doing something on import + public bool can_import { get; protected set; default = false; } public bool is_installable { @@ -43,6 +46,7 @@ namespace GameHub.Data.Runnables.Tasks.Install } public abstract async bool install(InstallTask task); + public virtual async bool import (InstallTask task) { return false; } // allow doing something on import } public abstract class FileInstaller: Installer diff --git a/src/data/sources/epicgames/EpicAnalysis.vala b/src/data/sources/epicgames/EpicAnalysis.vala new file mode 100644 index 00000000..8f5adb6a --- /dev/null +++ b/src/data/sources/epicgames/EpicAnalysis.vala @@ -0,0 +1,621 @@ +using Gee; + +using GameHub.Utils; + +namespace GameHub.Data.Sources.EpicGames +{ + /** + * This analysis one or two {@link Manifest}s and assembles lists on what to do download and write + * to files. + * + * @param tasks is a ordered list with instructions to open a file, write to it some {@link ChunkPart}s and close it afterwards. + */ + // FIXME: There are a lot of things related to Legendarys memory management we probably don't even need + private class Analysis + { + internal AnalysisResult? result { get; default = null; } + internal ArrayList tasks { get; default = new ArrayList(); } + internal LinkedList chunks_to_dl { get; default = new LinkedList(); } + internal Manifest.ChunkDataList chunk_data_list { get; default = null; } + internal string? base_url { get; default = null; } + + private File? resume_file { get; default = null; } + private HashMap hash_map { get; default = new HashMap(); } + private string? download_dir { get; default = null; } + + private Analysis(File install_dir, + string base_url, + File? resume_file) + { + _download_dir = install_dir.get_path(); + _base_url = base_url; + _resume_file = resume_file; + } + + internal Analysis.from_analysis(Runnables.Tasks.Install.InstallTask task, + string base_url, + Manifest new_manifest, + Manifest? old_manifest = null, + File? resume_file = null, + string[]? file_install_tags = null) + { + this(task.install_dir, base_url, resume_file); + + _result = new AnalysisResult( + new_manifest, + download_dir, + ref _hash_map, + ref _chunks_to_dl, + ref _tasks, + out _chunk_data_list, + old_manifest, + resume_file, + file_install_tags); + } + + internal class AnalysisResult + { + internal uint32 install_size { get; default = 0; } + internal uint32 reuse_size { get; default = 0; } + internal uint32 unchanged { get; default = 0; } + // internal uint32 unchanged_size { get; default = 0; } + internal uint64 dl_size { get; default = 0; } + + private ManifestComparison manifest_comparison { get; } + private uint32 added { get; default = 0; } + private uint32 biggest_file_size { get; default = 0; } + private uint32 biggest_chunk { get; default = 0; } + private uint32 changed { get; default = 0; } + private uint32 min_memory { get; default = 0; } + private uint32 num_chunks { get; default = 0; } + private uint32 num_chunks_cache { get; default = 0; } + private uint32 num_files { get; default = 0; } + private uint32 removed { get; default = 0; } + private uint32 uncompressed_dl_size { get; default = 0; } + + internal AnalysisResult(Manifest new_manifest, + string download_dir, + ref HashMap hash_map, + ref LinkedList chunks_to_dl, + ref ArrayList tasks, + out Manifest.ChunkDataList chunk_data_list, + Manifest? old_manifest = null, + File? resume_file = null, + string[]? file_install_tags = null) + { + foreach(var element in new_manifest.file_manifest_list.elements) + { + _install_size += element.file_size; + } + + _biggest_chunk = new_manifest.chunk_data_list.elements.max( + (a, b) => { + if(a.window_size < b.window_size) return -1; + + if(a.window_size == b.window_size) return 0; + + // if(a.window_size > b.window_size) return 1; + return 1; + }).window_size; + + _biggest_file_size = new_manifest.file_manifest_list.elements.max( + (a, b) => { + if(a.file_size < b.file_size) return -1; + + if(a.file_size == b.file_size) return 0; + + // if(a.file_size > b.file_size) return 1; + return 1; + }).file_size; + + var is_1mib = (biggest_chunk == 1024 * 1024); + debug(@"[Sources.EpicGames.AnalysisResult] Biggest chunk size: $biggest_chunk bytes (==1 MiB? $is_1mib)"); + + debug("[Sources.EpicGames.AnalysisResult] Creating manifest comparison…"); + _manifest_comparison = new ManifestComparison(new_manifest, + old_manifest); + + if(resume_file != null && resume_file.query_exists()) + { + info("[Sources.EpicGames.AnalysisResult] Found previously interrupted download. Download will be resumed if possible."); + try + { + var missing = 0; + var mismatch = 0; + var completed_files = new ArrayList(); + + var stream = new DataInputStream(resume_file.read()); + + string? line = null; + + while((line = stream.read_line_utf8()) != null) + { + var data = line.split(":"); + var file_hash = data[0]; + var filename = data[1]; + + var file = FS.file(download_dir, + filename); + + if(!file.query_exists()) + { + debug(@"[Sources.EpicGames.AnalysisResult] File does not exist but is in resume file: $(file.get_path())"); + missing++; + } + else if(file_hash != bytes_to_hex(new_manifest.file_manifest_list.get_file_by_path(filename).sha_hash)) + { + mismatch++; + } + else + { + completed_files.add(filename); + } + } + + if(missing > 0) + { + warning(@"[Sources.EpicGames.AnalysisResult] $missing previously completed file(s) are missing, they will be redownloaded."); + } + + if(mismatch > 0) + { + warning(@"[Sources.EpicGames.AnalysisResult] $mismatch previously completed file(s) are missing, they will be redownloaded."); + } + + // remove completed files from changed/added and move them to unchanged for the analysis. + manifest_comparison.added.remove_all(completed_files); + manifest_comparison.changed.remove_all(completed_files); + manifest_comparison.unchanged.add_all(completed_files); + + info(@"[Sources.EpicGames.AnalysisResult] Skipping $(completed_files.size) files based on resume data."); + } + catch (Error e) + { + warning(@"[Sources.EpicGames.AnalysisResult] Reading resume file failed: $(e.message), continuing as normal…"); + } + } + + // Install tags are used for selective downloading, e.g. for language packs + var additional_deletion_tasks = new ArrayList(); + + if(file_install_tags != null) + { + var files_to_skip = new ArrayList(); + + foreach(var file_manifest in new_manifest.file_manifest_list.elements) + { + foreach(var file_install_tag in file_install_tags) + { + // TODO: ??? https://github.com/derrod/legendary/blob/a2280edea8f7f8da9a080fd3fb2bafcabf9ee33d/legendary/downloader/manager.py#L146 + if(!(file_install_tag in file_manifest.install_tags)) + { + files_to_skip.add(file_manifest.filename); + } + } + } + + info(@"[Sources.EpicGames.AnalysisResult] Found $(files_to_skip.size) files to skip based on install tag."); + + manifest_comparison.added.remove_all(files_to_skip); + manifest_comparison.changed.remove_all(files_to_skip); + + files_to_skip.sort(); // TODO: Does this need a comparefunction? + foreach(var file in files_to_skip) + { + // Union + if(!(file in manifest_comparison.unchanged)) + { + manifest_comparison.unchanged.add(file); + } + + additional_deletion_tasks.add(new FileTask.delete(file, true)); + } + } + + // Legendary has exclude filters here + + if(file_install_tags.length > 0) + { + info(@"[Sources.EpicGames.AnalysisResult] Remaining files after filtering: $(manifest_comparison.added.size + manifest_comparison.changed.size)"); + + // correct install size after filtering + _install_size = 0; + foreach(var file_manifest in new_manifest.file_manifest_list.elements) + { + if(file_manifest.filename in manifest_comparison.added) + { + _install_size += file_manifest.file_size; + } + } + } + + if(!manifest_comparison.removed.is_empty) + { + _removed = manifest_comparison.removed.size; + debug(@"[Sources.EpicGames.AnalysisResult] $removed removed files"); + } + + if(!manifest_comparison.added.is_empty) + { + _added = manifest_comparison.added.size; + debug(@"[Sources.EpicGames.AnalysisResult] $added added files"); + } + + if(!manifest_comparison.changed.is_empty) + { + _changed = manifest_comparison.changed.size; + debug(@"[Sources.EpicGames.AnalysisResult] $changed changed files"); + } + + if(!manifest_comparison.unchanged.is_empty) + { + _unchanged = manifest_comparison.unchanged.size; + debug(@"[Sources.EpicGames.AnalysisResult] $unchanged unchanged files"); + } + + // count references to chunks for determining runtime cache size later + // TODO: do we care about this? + var references = new HashMultiSet(); // FIXME: correct type to count? + var file_manifest_list = new_manifest.file_manifest_list.elements; + file_manifest_list.sort( + (a, b) => { + if(a.filename.down() < b.filename.down()) return -1; + + if(a.filename.down() == b.filename.down()) return 0; + + // if(a.filename.down() > b.filename.down()) return 1; + return 1; + }); + + foreach(var file_manifest in file_manifest_list) + { + hash_map.set(file_manifest.filename, + bytes_to_hex(file_manifest.sha_hash)); + + // chunks of unchanged files are not downloaded so we can skip them + if(file_manifest.filename in manifest_comparison.unchanged) + { + // debug("skipped: %s", file_manifest.filename); + _unchanged += file_manifest.file_size; + continue; + } + + foreach(var chunk_part in file_manifest.chunk_parts) + { + references.add(chunk_part.guid_num); + } + } + + // TODO: Legendary is doing optimizations here + // var processing_optimizations = false; + + // determine reusable chunks and prepare lookup table for reusable ones + var re_usable = new HashMap >(); + var patch = true; // FIXME: hardcoded always update + + if(old_manifest != null && !manifest_comparison.changed.is_empty && patch) + { + debug("[Sources.EpicGames.AnalysisResult] Analyzing manifests for re-usable chunks…"); + foreach(var changed_file in manifest_comparison.changed) + { + var old_file = old_manifest.file_manifest_list.get_file_by_path(changed_file); + var new_file = new_manifest.file_manifest_list.get_file_by_path(changed_file); + + var existing_chunks = new HashMap >(); + uint32 offset = 0; + + foreach(var chunk_part in old_file.chunk_parts) + { + // debug(@"Old chunk: $chunk_part"); + if(!existing_chunks.has_key(chunk_part.guid_num)) + { + var list = new ArrayList >(); + existing_chunks.set(chunk_part.guid_num, + list); + } + + // TODO: possible to do this better? + var tmp = existing_chunks.get(chunk_part.guid_num); + var tmp2 = new ArrayList(); + tmp2.add_all_array({ offset, chunk_part.offset, chunk_part.offset + chunk_part.size }); + tmp.add(tmp2); + existing_chunks.set(chunk_part.guid_num, + tmp); + offset += chunk_part.size; + } + + foreach(var chunk_part in new_file.chunk_parts) + { + // debug(@"New chunk: $chunk_part"); + uint32[] key = { chunk_part.guid_num, chunk_part.offset, chunk_part.size }; + + if(!existing_chunks.has_key(chunk_part.guid_num)) continue; + + foreach(ArrayList thing in existing_chunks.get(chunk_part.guid_num)) + { + assert_nonnull(thing); + assert(thing.size == 3); + + // check if new chunk part is wholly contained in the old chunk part + if(thing.get(1) <= chunk_part.offset + && (chunk_part.offset + chunk_part.size) <= thing.get(2)) + { + references.remove(chunk_part.guid_num); + + if(!re_usable.has_key(changed_file)) + { + re_usable.set(changed_file, + new HashMap()); + } + + // TODO: possible to do this better? + var tmp = re_usable.get(changed_file); + tmp.set(key, + thing.get(0) + (chunk_part.offset - thing.get(1))); + re_usable.set(changed_file, + tmp); + _reuse_size += chunk_part.size; + } + } + } + } + } + + uint32 last_cache_size = 0; + uint32 current_cache_size = 0; + + // set to determine whether a file is currently cached or not + var cached = new ArrayList(); + + // Using this secondary set is orders of magnitude faster than checking the deque. + var chunks_in_dl_list = new ArrayList(); + + // This is just used to count all unique guids that have been cached + var dl_cache_guids = new ArrayList(); + + // run through the list of files and create the download jobs and also determine minimum + // runtime cache requirement by simulating adding/removing from cache during download. + debug("[Sources.EpicGames.AnalysisResult] Creating filetasks and chunktasks…"); + foreach(var current_file in file_manifest_list) + { + // skip unchanged and empty files + if(current_file.filename in manifest_comparison.unchanged) + { + continue; + } + else if(current_file.chunk_parts.size == 0) + { + tasks.add(new FileTask.empty_file(current_file.filename)); + continue; + } + + // TODO: does this return null if nonexisting? + var existing_chunks = re_usable.get(current_file.filename); + // Gee.HashMap existing_chunks = null; + // if(re_usable.has_key(current_file.filename)) + // { + // existing_chunks = re_usable.get(current_file.filename); + // } + var chunk_tasks = new ArrayList(); + var reused = 0; + + foreach(var chunk_part in current_file.chunk_parts) + { + var chunk_task = new ChunkTask(chunk_part.guid_num, + chunk_part.offset, + chunk_part.size); + + // re-use the chunk from the existing file if we can + uint32[] key = { chunk_part.guid_num, chunk_part.offset, chunk_part.size }; + + if(existing_chunks != null + && existing_chunks.has_key(key)) + { + // debug("reusing chunk, hash should be: " + new_manifest.chunk_data_list.get_chunk_by_number(chunk_part.guid_num).to_string()); + reused++; + chunk_task.chunk_file = current_file.filename; + chunk_task.chunk_offset = existing_chunks.get(key); + } + else + { + // add to DL list if not already in it + if(!(chunk_part.guid_num in chunks_in_dl_list)) + { + // debug("chunk " + chunk_part.guid_num.to_string() + " to download, hash should be: " + new_manifest.chunk_data_list.get_chunk_by_number(chunk_part.guid_num).to_string()); + chunks_to_dl.add(chunk_part.guid_num); + chunks_in_dl_list.add(chunk_part.guid_num); + } + + // if chunk has more than one use or is already in cache, + // check if we need to add or remove it again. + if(references.count(chunk_part.guid_num) > 1 + || chunk_part.guid_num in cached) + { + references.remove(chunk_part.guid_num); + + // delete from cache if no references left + if(!(chunk_part.guid_num in references)) + { + current_cache_size -= biggest_chunk; + cached.remove(chunk_part.guid_num); + chunk_task.cleanup = true; + } + + // add to cache if not already cached + else if(!(chunk_part.guid_num in cached)) + { + dl_cache_guids.add(chunk_part.guid_num); + cached.add(chunk_part.guid_num); + current_cache_size += biggest_chunk; + } + } + else + { + chunk_task.cleanup = true; + } + } + + chunk_tasks.add(chunk_task); + } + + if(reused > 0) + { + debug(@"[Sources.EpicGames.AnalysisResult] Reusing $reused chunks from: $(current_file.filename)"); + + // open temporary file that will contain download + old file contents + tasks.add(new FileTask.open(current_file.filename + ".tmp")); + tasks.add_all(chunk_tasks); + tasks.add(new FileTask.close(current_file.filename + ".tmp")); + + // delete old file and rename temporary + tasks.add(new FileTask.rename(current_file.filename, + current_file.filename + ".tmp", + true)); + } + else + { + tasks.add(new FileTask.open(current_file.filename)); + tasks.add_all(chunk_tasks); + tasks.add(new FileTask.close(current_file.filename)); + } + + // check if runtime cache size has changed + if(current_cache_size > last_cache_size) + { + debug(@"[Sources.EpicGames.AnalysisResult] New maximum cache size: $(current_cache_size / 1024 / 1024) MiB"); + last_cache_size = current_cache_size; + } + } + + debug(@"[Sources.EpicGames.AnalysisResult] Final cache size requirement: $(last_cache_size / 1024 / 1024) MiB"); + _min_memory = last_cache_size + (1024 * 1024 * 32); // add some padding just to be safe + + // TODO: Legendary does same caching stuff here + // https://github.com/derrod/legendary/blob/a2280edea8f7f8da9a080fd3fb2bafcabf9ee33d/legendary/downloader/manager.py#L363 + + // calculate actual dl and patch write size. + _dl_size = 0; + _uncompressed_dl_size = 0; + new_manifest.chunk_data_list.elements.foreach(chunk => { + if(chunk.guid_num in chunks_in_dl_list) + { + _dl_size += chunk.file_size; + _uncompressed_dl_size += chunk.window_size; + } + + return true; + }); + + // add jobs to remove files + foreach(var filename in manifest_comparison.removed) + { + tasks.add(new FileTask.delete(filename)); + } + + tasks.add_all(additional_deletion_tasks); + + _num_chunks_cache = dl_cache_guids.size; + chunk_data_list = new_manifest.chunk_data_list; + } + } + + // This only exists so I can put both subclasses in one list + // so that the tasks order stays in the correct position + internal abstract class Task {} + + /** + * Download manager task for a file + * + * @param filename name of the file + * @param del if this is a file to be deleted, if rename is true, delete filename before renaming + * @param empty if this is an empty file that just needs to be "touch"-ed (may not have chunk tasks) + * @param temporary_filename If rename is true: Filename to rename from. + */ + internal class FileTask: Task + { + internal string filename { get; } + internal bool del { get; default = false; } + internal bool empty { get; default = false; } + internal bool fopen { get; default = false; } + internal bool fclose { get; default = false; } + internal bool frename { get; default = false; } + internal string? temporary_filename { get; default = null; } + internal bool silent { get; default = false; } + + internal bool is_reusing + { + get + { + return temporary_filename != null; + } + } + + internal FileTask(string filename) + { + _filename = filename; + } + + internal FileTask.delete(string filename, bool silent = false) + { + this(filename); + _del = true; + _silent = silent; + } + + internal FileTask.empty_file(string filename) + { + this(filename); + _empty = true; + } + + internal FileTask.open(string filename) + { + this(filename); + _fopen = true; + } + + internal FileTask.close(string filename) + { + this(filename); + _fclose = true; + } + + internal FileTask.rename(string new_filename, + string old_filename, + bool @delete = false) + { + this(filename); + _frename = true; + _temporary_filename = old_filename; + _del = @delete; + } + } + + /** + * Download manager chunk task + * + * @param chunk_guid GUID of chunk + * @param cleanup whether or not this chunk can be removed from disk/memory after it has been written + * @param chunk_offset Offset into file or shared memory + * @param chunk_size Size to read from file or shared memory + * @param chunk_file Either cache or existing game file this chunk is read from if not using shared memory + */ + internal class ChunkTask: Task + { + internal uint32 chunk_guid { get; } + internal bool cleanup { get; set; default = false; } + internal uint32 chunk_offset { get; set; default = 0; } + internal uint32 chunk_size { get; default = 0; } + internal string? chunk_file { get; set; default = null; } + + internal ChunkTask(uint32 chunk_guid, + uint32 chunk_offset, + uint32 chunk_size) + { + _chunk_guid = chunk_guid; + _chunk_offset = chunk_offset; + _chunk_size = chunk_size; + } + } + } +} diff --git a/src/data/sources/epicgames/EpicChunk.vala b/src/data/sources/epicgames/EpicChunk.vala new file mode 100644 index 00000000..63c2b5a5 --- /dev/null +++ b/src/data/sources/epicgames/EpicChunk.vala @@ -0,0 +1,191 @@ +using Gee; +using GameHub.Utils; + +namespace GameHub.Data.Sources.EpicGames +{ + /** + Chunks are 1 MiB of data which contains one or more parts of files + */ + private class Chunk + { + private const int64 header_magic = 0xB1FE3AA2; + + private Bytes sha_hash { get; default = new Bytes(null); } + private uint8 stored_as { get; default = 0; } + private uint32 hash_type { get; default = 0; } // 0x1 = rolling hash, 0x2 = sha hash, 0x3 = both + private uint32 header_version { get; default = 3; } + private uint32 header_size { get; default = 0; } + private uint32 compressed_size { get; default = 0; } + private uint32 uncompressed_size { get; default = 1024 * 1024; } + private uint64 hash { get; default = 0; } + + + private uint32[] guid { get; default = new uint32[4]; } + private string? _guid_str = null; + private uint32? _guid_num = null; + + private Bytes? raw_bytes = null; + private Bytes? _data = null; + + internal Bytes data + { + get + { + if(_data == null) + { + if(compressed) + { + if(log_chunk) debug("[Sources.EpicGames.Chunk] chunk is compressed, uncompressing…"); + + if(log_chunk) debug("[Sources.EpicGames.Chunk] compressed chunk size: %s", raw_bytes.length.to_string()); + + try + { + var uncompressed_stream = new MemoryOutputStream.resizable(); + var zlib = new ZlibDecompressor(ZlibCompressorFormat.ZLIB); + var byte_stream = new MemoryInputStream.from_bytes(raw_bytes); + + var converter_stream = new ConverterOutputStream(uncompressed_stream, + zlib); + converter_stream.splice(byte_stream, + OutputStreamSpliceFlags.NONE); + + uncompressed_stream.close(); + _data = uncompressed_stream.steal_as_bytes(); + } + catch (Error e) + { + debug("[EpicChunk.data] error: %s", + e.message); + } + } + else + { + _data = raw_bytes; + } + + raw_bytes = null; + + if(log_chunk) debug("[Sources.EpicGames.Chunk] uncompressed chunk size: %s", _data.length.to_string()); + } + + return _data; + } + + // set + // { + // assert(value.length <= 1024 * 1024); + + // // data is now uncompressed + // if(compressed) + // { + // _stored_as ^= 0x1; + // } + + // // pad data to 1 MiB + // _data = value; + // if(value.length < 1024 * 1024) + // { + // var tmp = value.get_data(); + // tmp.resize(1024 * 1024 - value.length); + // _data = new Bytes(tmp); + // } + + // // FIXME: recalculate hashes + // // _hash = get_hash(_data); + // // _sha_hash = sha(_data); + // _hash_type = 0x3; + // } + } + + internal string guid_str + { + get + { + if(_guid_str == null) + { + _guid_str = guid_to_readable_string(guid); + } + + return _guid_str; + } + } + + internal uint32 guid_num + { + get + { + if(_guid_num == null) + { + _guid_num = guid_to_number(guid); + } + + return _guid_num; + } + } + + internal bool compressed { get { return _stored_as == 1; } } + + internal Chunk.from_byte_stream(DataInputStream stream) + { + stream.set_byte_order(DataStreamByteOrder.LITTLE_ENDIAN); + var head_start = stream.tell(); + + try + { + var magic = stream.read_uint32(); + assert(magic == header_magic); + + _header_version = stream.read_uint32(); + _header_size = stream.read_uint32(); + _compressed_size = stream.read_uint32(); + + for(var j = 0; j < 4; j++) + { + guid[j] = stream.read_uint32(); + } + + _hash = stream.read_uint64(); + _stored_as = stream.read_byte(); + + if(header_version >= 2) + { + _sha_hash = stream.read_bytes(20); + _hash_type = stream.read_byte(); + } + + if(header_version >= 3) + { + _uncompressed_size = stream.read_uint32(); + } + + assert(stream.tell() - head_start == header_size); + + raw_bytes = stream.read_bytes(compressed_size); + } + catch (Error e) + { + debug("error: %s", + e.message); + } + + if(log_chunk) debug(to_string()); + } + + // TODO: public write() {} + + // TODO: public static get_hash() {} + // https://github.com/derrod/legendary/blob/a2280edea8f7f8da9a080fd3fb2bafcabf9ee33d/legendary/utils/rolling_hash.py#L18 + + internal string to_string() + { + return "".printf( + guid_str, + stored_as.to_string(), + hash_type.to_string(), + header_version.to_string(), + compressed_size.to_string(), + uncompressed_size.to_string()); + } + } +} diff --git a/src/data/sources/epicgames/EpicDownloader.vala b/src/data/sources/epicgames/EpicDownloader.vala new file mode 100644 index 00000000..4be26240 --- /dev/null +++ b/src/data/sources/epicgames/EpicDownloader.vala @@ -0,0 +1,814 @@ +using Gee; +using Soup; + +using GameHub.Data.Runnables; +using GameHub.Utils; +using GameHub.Utils.Downloader; +using GameHub.Utils.Downloader.SoupDownloader; + +namespace GameHub.Data.Sources.EpicGames +{ + // FIXME: This whole thing is a mess because I had to come up with my own stuff here + // We need to download a number of x chunks per game and this should be properly represented in + // the download manager + private class EpicDownloader: GameHub.Utils.Downloader.SoupDownloader.SoupDownloader + { + private ArrayQueue dl_queue; + private HashTable dl_info; + private HashTable downloads; + private Session session; + + private static string[] URL_SCHEMES = { "http", "https" }; + private static string[] FILENAME_BLACKLIST = { "download" }; + + internal EpicDownloader() + { + downloads = new HashTable(str_hash, + str_equal); + dl_info = new HashTable(str_hash, + str_equal); + dl_queue = new ArrayQueue(); + session = new Session(); + session.max_conns = 32; + session.max_conns_per_host = 16; + session.user_agent = "EpicGamesLauncher/11.0.1-14907503+++Portal+Release-Live Windows/10.0.19041.1.256.64bit"; + download_manager().add_downloader(this); + } + + private EpicDownload? get_game_download(EpicGame? game) + { + if(game == null) return null; + + lock (downloads) + { + return (EpicDownload?) downloads.get(game.full_id); + } + } + + private async ArrayList fetch_parts(Installer installer) + { + var parts = new ArrayList(); + debug("preparing download"); + installer.analysis = installer.game.prepare_download(installer.task); + + // game is either up to date or hasn't changed, so we have nothing to do + if(installer.analysis.result.dl_size < 1) + { + debug("[Sources.EpicGames.EpicGame.download] Download size is 0, the game is either already up to date or has not changed."); + + if(installer.game.needs_repair && installer.game.repair_file.query_exists()) + { + installer.game.needs_verification = false; + // remove repair file + FS.rm(installer.game.repair_file.get_path()); + + // check if install tags have changed, if they did; try deleting files that are no longer required. + // TODO: update install tags + } + } + + // debug("[Sources.EpicGames.EpicGame.download] Install size: %.02d MiB", installer.analysis.result.install_size / 1024 / 1024); + // debug("[Sources.EpicGames.EpicGame.download] Download size: %.02d MiB", installer.analysis.result.dl_size / 1024 / 1024); + // debug(@"[Sources.EpicGames.EpicGame.download] Reusable size: %.02d MiB (chunks) / $(installer.analysis.result.unchanged) (skipped)", installer.analysis.result.reuse_size / 1024 / 1024); + + foreach(var chunk_guid in installer.analysis.chunks_to_dl) + { + var chunk = installer.analysis.chunk_data_list.get_chunk_by_number(chunk_guid); + var remote = File.new_for_uri(installer.analysis.base_url + "/" + chunk.path); + var local = FS.file(FS.Paths.EpicGames.Cache + "/chunks/" + installer.game.id + "/" + chunk.guid_num.to_string()); + // debug("local path: %s", local.get_path()); + FS.mkdir(local.get_parent().get_path()); + parts.add(new SoupDownload(remote, + local, + File.new_for_path(local.get_path() + "~"))); + } + + return parts; + } + + // TODO: a lot of small files, we should probably handle this in parralel + internal new async ArrayList download(Installer installer) throws Error + { + var files = new ArrayList(); + var game = installer.game; + var download = get_game_download(game); + var parts = yield fetch_parts(installer); + + // installer.task.status = new InstallTask.Status(InstallTask.State.DOWNLOADING); + if(game == null || download != null) return yield await_download(download); + + download = new EpicDownload(game.full_id, + parts); + + lock (downloads) downloads.set(game.full_id, download); + download_started(download); + + var info = new DownloadInfo.for_runnable(game, + "Downloading…"); + info.download = download; + + lock (dl_info) dl_info.set(game.full_id, info); + dl_started(info); + + if(GameHub.Application.log_downloader) + { + debug("[EpicDownloader] Installing '%s'...", + game.full_id); + } + + game.status = new Game.Status(Game.State.DOWNLOADING, + game, + download); + + debug("[DownloadableInstaller.download] Starting (%d parts)", + parts.size); + + var ds_id = download_manager().file_download_started.connect( + dl => { + if(dl.id != game.full_id) return; + + installer.task.status = new Tasks.Install.InstallTask.Status(Tasks.Install.InstallTask.State.DOWNLOADING, + dl); + // installer.download_state = new DownloadState(DownloadState.State.DOWNLOADING, dl); + dl.status_change.connect( + s => { + installer.task.notify_property("status"); + }); + }); + + try + { + uint32 current_part = 1; + foreach(var part in ((EpicDownload) download).parts) + { + debug("[DownloadableInstaller.download] Part %u: `%s`", + current_part, + part.remote.get_uri()); + + FS.mkdir(part.local.get_parent().get_path()); + + var download_description = download.id; + + if(parts.size > 1) + { + download_description = _("Part %1$u of %2$u: %3$s").printf(current_part, + parts.size, + part.id); + download.status = new EpicDownload.Status( + Download.State.DOWNLOADING, + installer.full_size, + // FIXME: total size is wrong for partial updates + (current_part * 1048576) / installer.full_size, // Chunks are mostly 1 MiB + -1, + -1); + } + + debug("Downloading " + part.remote.get_uri()); + + if(part.remote == null || part.remote.get_uri() == null || part.remote.get_uri().length == 0) + { + current_part++; + continue; + } + + var uri = part.remote.get_uri(); + + if(part.local.query_exists()) + { + // TODO: compare hash + if(GameHub.Application.log_downloader) + { + debug("[SoupDownloader] '%s' is already downloaded", + uri); + } + + files.add(part.local); + current_part++; + continue; + } + + // var tmp = File.new_for_path(part.local.get_path() + "~"); + + if(part.remote.get_uri_scheme() in URL_SCHEMES) + yield download_from_http(part, + false, + false); + else + yield download_from_filesystem(part); + + if(part.local_tmp.query_exists()) + { + part.local_tmp.move(part.local, + FileCopyFlags.OVERWRITE); + } + + // var file = yield download(part.remote, part.local, new Downloader.DownloadInfo.for_runnable(task.runnable, partDesc), false); + if(part.local != null && part.local.query_exists()) + { + files.add(part.local); + // TODO: uncompress, compare hash + // https://github.com/derrod/legendary/blob/a2280edea8f7f8da9a080fd3fb2bafcabf9ee33d/legendary/downloader/workers.py#L99 + // var chunk = new Chunk.from_file(new DataInputStream(file.read())); + + // string? file_checksum = null; + // if(part.checksum != null) + // { + // task.status = new InstallTask.Status(InstallTask.State.VERIFYING_INSTALLER_INTEGRITY); + // // FileUtils.set_contents(file.get_path() + "." + part.checksum_type_string, part.checksum); + // // file_checksum = yield Utils.compute_file_checksum(file, part.checksum_type); + // file_checksum = bytes_to_hex(chunk.sha_hash); + // } + + // if(part.checksum == null || file_checksum == null || part.checksum == file_checksum) + // { + // debug("[DownloadableInstaller.download] Downloaded `%s`; checksum: '%s' (matched)", file.get_path(), file_checksum != null ? file_checksum : "(null)"); + // files.add(file); + // } + // else + // { + // Utils.notify( + // _("%s: corrupted installer").printf(task.runnable.name), + // _("Checksum mismatch in %s").printf(file.get_basename()), + // NotificationPriority.HIGH, + // n => { + // var runnable_id = task.runnable.id; + // n.set_icon(new ThemedIcon("dialog-warning")); + // task.runnable.cast( + // game => { + // runnable_id = game.full_id; + // var icon = ImageCache.local_file(game.icon, @"games/$(game.source.id)/$(game.id)/icons/"); + // if(icon != null && icon.query_exists()) + // { + // n.set_icon(new FileIcon(icon)); + // } + // }); + // var args = new Variant("(ss)", runnable_id, file.get_path()); + // n.set_default_action_and_target_value(Application.ACTION_PREFIX + Application.ACTION_CORRUPTED_INSTALLER_PICK_ACTION, args); + // n.add_button_with_target_value(_("Show file"), Application.ACTION_PREFIX + Application.ACTION_CORRUPTED_INSTALLER_SHOW, args); + // n.add_button_with_target_value(_("Remove"), Application.ACTION_PREFIX + Application.ACTION_CORRUPTED_INSTALLER_REMOVE, args); + // n.add_button_with_target_value(_("Backup"), Application.ACTION_PREFIX + Application.ACTION_CORRUPTED_INSTALLER_BACKUP, args); + // return n; + // } + // ); + + // warning("Checksum mismatch in `%s`; expected: `%s`, actual: `%s`", file.get_basename(), part.checksum, file_checksum); + // } + } + + current_part++; + } + + // if(installers_dir != null) + // { + // FileUtils.set_contents(installers_dir.get_child(@".installer_$(id)").get_path(), ""); + // } + } + catch (IOError.CANCELLED error) + { + download.status = new FileDownload.Status(Download.State.CANCELLED); + download_cancelled(download, + error); + + if(info != null) dl_ended(info); + + throw error; + } + catch (Error error) + { + download.status = new FileDownload.Status(Download.State.FAILED); + download_failed(download, + error); + + if(info != null) dl_ended(info); + + throw error; + } + finally + { + // download_state = new DownloadState(DownloadState.State.DOWNLOADED); + download.status = new FileDownload.Status(Download.State.FINISHED); + lock (downloads) downloads.remove(game.full_id); + lock (dl_info) dl_info.remove(game.full_id); + // lock (dl_queue) dl_queue.remove(game.full_id); + } + + download_manager().disconnect(ds_id); + + download_finished(download); + dl_ended(info); + + // game.update_status(); + + return files; + } + + // public async File? download_part(File remote, File local, DownloadInfo? info = null, bool preserve_filename = true, bool queue = true) throws Error + // { + // if(remote == null || remote.get_uri() == null || remote.get_uri().length == 0) return null; + + // var uri = remote.get_uri(); + // var download = get_file_download(remote); + + // if(download != null) return yield await_download(download); + + // if(local.query_exists()) + // { + // if(GameHub.Application.log_downloader) + // { + // debug("[SoupDownloader] '%s' is already downloaded", uri); + // } + // return local; + // } + + // var tmp = File.new_for_path(local.get_path() + "~"); + + // download = new SoupDownload(remote, local, tmp); + // download.session = session; + + // lock (downloads) + // { + // downloads.set(uri, download); + // } + + // download_started(download); + + // if(info != null) + // { + // info.download = download; + + // lock (dl_info) + // { + // dl_info.set(uri, info); + // } + + // dl_started(info); + // } + + // if(GameHub.Application.log_downloader) + // { + // debug("[SoupDownloader] Downloading '%s'...", uri); + // } + + // download.status = new FileDownload.Status(Download.State.STARTING); + + // try{ + // if(remote.get_uri_scheme() in URL_SCHEMES) + // yield download_from_http(download, preserve_filename, queue); + // else + // yield download_from_filesystem(download); + // } + // catch (IOError.CANCELLED error) + // { + // download.status = new FileDownload.Status(Download.State.CANCELLED); + // download_cancelled(download, error); + // if(info != null) dl_ended(info); + // throw error; + // } + // catch (Error error) + // { + // download.status = new FileDownload.Status(Download.State.FAILED); + // download_failed(download, error); + // if(info != null) dl_ended(info); + // throw error; + // } + // finally + // { + // lock (downloads) downloads.remove(uri); + // lock (dl_info) dl_info.remove(uri); + // lock (dl_queue) dl_queue.remove(uri); + // } + + // if(download.local_tmp.query_exists()) + // { + // download.local_tmp.move(download.local, FileCopyFlags.OVERWRITE); + // } + + // if(GameHub.Application.log_downloader) + // { + // debug("[SoupDownloader] Downloaded '%s'", uri); + // } + + // download_finished(download); + // if(info != null) dl_ended(info); + + // return download.local; + // } + + private async ArrayList? await_download(EpicDownload download) throws Error + { + ArrayList files = null; + Error download_error = null; + + SourceFunc callback = await_download.callback; + var download_finished_id = download_finished.connect( + (downloader, downloaded) => { + if(((SoupDownload) downloaded).id != download.id) return; + + files = new ArrayList(); + ((EpicDownload) downloaded).parts.foreach(part => { + files.add(part.local_tmp); // FIXME: local_tmp? + + return true; + }); + + callback (); + }); + var download_cancelled_id = download_cancelled.connect( + (downloader, cancelled_download, error) => { + if(((SoupDownload) cancelled_download).id != download.id) return; + + download_error = error; + callback (); + }); + var download_failed_id = download_failed.connect( + (downloader, failed_download, error) => { + if(((SoupDownload) failed_download).id != download.id) return; + + download_error = error; + callback (); + }); + + yield; + + disconnect(download_finished_id); + disconnect(download_cancelled_id); + disconnect(download_failed_id); + + if(download_error != null) throw download_error; + + return files; + } + + // private async void await_queue(EpicDownload download) + // { + // lock (dl_queue) + // { + // if(download.remote.get_uri() in dl_queue) return; + // dl_queue.add(download.remote.get_uri()); + // } + + // var download_finished_id = download_finished.connect( + // (downloader, downloaded) => { + // lock (dl_queue) dl_queue.remove(((SoupDownload) downloaded).remote.get_uri()); + // }); + // var download_cancelled_id = download_cancelled.connect( + // (downloader, cancelled_download, error) => { + // lock (dl_queue) dl_queue.remove(((SoupDownload) cancelled_download).remote.get_uri()); + // }); + // var download_failed_id = download_failed.connect( + // (downloader, failed_download, error) => { + // lock (dl_queue) dl_queue.remove(((SoupDownload) failed_download).remote.get_uri()); + // }); + + // while(dl_queue.peek() != null && dl_queue.peek() != download.remote.get_uri() && !download.is_cancelled) { + // download.status = new FileDownload.Status(Download.State.QUEUED); + // yield Utils.sleep_async(2000); + // } + + // disconnect(download_finished_id); + // disconnect(download_cancelled_id); + // disconnect(download_failed_id); + // } + + private async void download_from_http(SoupDownload download, + bool preserve_filename = true, + bool queue = true) throws Error + { + var msg = new Message("GET", + download.remote.get_uri()); + msg.response_body.set_accumulate(false); + + download.session = session; + download.message = msg; + + // if(queue) + // { + // yield await_queue(download); + // // download.status = new FileDownload.Status(Download.State.STARTING); + // } + + if(download.is_cancelled) + { + throw new IOError.CANCELLED("Download cancelled by user"); + } + + #if !PKG_FLATPAK + var address = msg.get_address(); + var connectable = new NetworkAddress(address.name, + (uint16) address.port); + var network_monitor = NetworkMonitor.get_default(); + + if(!(yield network_monitor.can_reach_async(connectable))) + throw new IOError.HOST_UNREACHABLE("Failed to reach host"); + #endif + + GLib.Error? err = null; + + FileOutputStream? local_stream = null; + + int64 dl_bytes = 0; + int64 dl_bytes_total = 0; + + // #if SOUP_2_60 + // int64 resume_from = 0; + // var resume_dl = false; + + // if(download.local_tmp.get_basename().has_suffix("~") && download.local_tmp.query_exists()) + // { + // var info = yield download.local_tmp.query_info_async(FileAttribute.STANDARD_SIZE, FileQueryInfoFlags.NONE); + // resume_from = info.get_size(); + // if(resume_from > 0) + // { + // resume_dl = true; + // msg.request_headers.set_range(resume_from, -1); + // if(GameHub.Application.log_downloader) + // { + // debug(@"[SoupDownloader] Download part found, size: $(resume_from)"); + // } + // } + // } + // #endif + + msg.got_headers.connect( + () => { + dl_bytes_total = msg.response_headers.get_content_length(); + + if(GameHub.Application.log_downloader) + { + debug(@"[SoupDownloader] Content-Length: $(dl_bytes_total)"); + } + + try + { + if(preserve_filename) + { + string filename = null; + string disposition = null; + HashTable dparams = null; + + if(msg.response_headers.get_content_disposition(out disposition, + out dparams)) + { + if(disposition == "attachment" && dparams != null) + { + filename = dparams.get("filename"); + + if(filename != null && GameHub.Application.log_downloader) + { + debug(@"[SoupDownloader] Content-Disposition: filename=%s", + filename); + } + } + } + + if(filename == null) + { + filename = download.remote.get_basename(); + } + + if(filename != null && !(filename in FILENAME_BLACKLIST)) + { + download.local = download.local.get_parent().get_child(filename); + } + } + + if(download.local.query_exists()) + { + if(GameHub.Application.log_downloader) + { + debug(@"[SoupDownloader] '%s' exists", + download.local.get_path()); + } + + var info = download.local.query_info(FileAttribute.STANDARD_SIZE, + FileQueryInfoFlags.NONE); + + if(info.get_size() == dl_bytes_total) + { + session.cancel_message(msg, + Status.OK); + + return; + } + } + + if(GameHub.Application.log_downloader) + { + debug(@"[SoupDownloader] Downloading to '%s'", + download.local.get_path()); + } + + // #if SOUP_2_60 + // int64 rstart = -1, rend = -1; + // if(resume_dl && msg.response_headers.get_content_range(out rstart, out rend, out dl_bytes_total)) + // { + // if(GameHub.Application.log_downloader) + // { + // debug(@"[SoupDownloader] Content-Range is supported($(rstart)-$(rend)), resuming from $(resume_from)"); + // debug(@"[SoupDownloader] Content-Length: $(dl_bytes_total)"); + // } + // dl_bytes = resume_from; + // local_stream = download.local_tmp.append_to(FileCreateFlags.NONE); + // } + // else + // #endif + { + local_stream = download.local_tmp.replace(null, + false, + FileCreateFlags.REPLACE_DESTINATION); + } + } + catch (Error e) + { + warning(e.message); + } + }); + + int64 last_update = 0; + int64 dl_bytes_from_last_update = 0; + + msg.got_chunk.connect( + (msg, chunk) => { + if(session.would_redirect(msg) || local_stream == null) return; + + dl_bytes += chunk.length; + dl_bytes_from_last_update += chunk.length; + try + { + local_stream.write(chunk.data); + chunk.free(); + + int64 now = get_real_time(); + int64 diff = now - last_update; + + if(diff > 1000000) + { + int64 dl_speed = (int64) (((double) dl_bytes_from_last_update) / ((double) diff) * ((double) 1000000)); + download.status = new FileDownload.Status(Download.State.DOWNLOADING, + dl_bytes, + dl_bytes_total, + dl_speed); + last_update = now; + dl_bytes_from_last_update = 0; + } + } + catch (Error e) + { + err = e; + session.cancel_message(msg, + Status.CANCELLED); + } + }); + + session.queue_message( + msg, + (session, msg) => { + download_from_http.callback (); + }); + + yield; + + if(local_stream == null) return; + + yield local_stream.close_async(Priority.DEFAULT); + + msg.request_body.free(); + msg.response_body.free(); + + if(msg.status_code != Status.OK && msg.status_code != Status.PARTIAL_CONTENT) + { + if(msg.status_code == Status.CANCELLED) + { + throw new IOError.CANCELLED("Download cancelled by user"); + } + + if(err == null) + err = new GLib.Error(http_error_quark(), + (int) msg.status_code, + msg.reason_phrase); + + throw err; + } + } + + private async void download_from_filesystem(SoupDownload download) throws GLib.Error + { + if(download.remote == null || !download.remote.query_exists()) return; + + try + { + if(GameHub.Application.log_downloader) + { + debug("[SoupDownloader] Copying '%s' to '%s'", + download.remote.get_path(), + download.local_tmp.get_path()); + } + + yield download.remote.copy_async(download.local_tmp, + FileCopyFlags.OVERWRITE, + Priority.DEFAULT, + null, + (current, total) => { download.status = new FileDownload.Status(Download.State.DOWNLOADING, current, total); }); + } + catch (IOError.EXISTS error) {} + } + } + + public class EpicDownload: Download, PausableDownload + { + public weak Session? session; + public weak Message? message; + public bool is_cancelled = false; + public ArrayList parts { get; } + + public EpicDownload(string id, + ArrayList parts) + { + base(id); + _parts = parts; + } + + public void pause() + { + if(session != null && message != null && _status.state == Download.State.DOWNLOADING) + { + session.pause_message(message); + _status.state = Download.State.PAUSED; + status_change(_status); + } + } + + public void resume() + { + if(session != null && message != null && _status.state == Download.State.PAUSED) + { + session.unpause_message(message); + } + } + + public override void cancel() + { + is_cancelled = true; + + if(session != null && message != null) + { + session.cancel_message(message, + Soup.Status.CANCELLED); + } + } + + public class Status: Download.Status + { + public int64 bytes_total = -1; + public double dl_progress = -1; + public int64 dl_speed = -1; + public int64 eta = -1; + + public Status(Download.State state = Download.State.STARTING, + int64 total = -1, + double progress = -1, + int64 speed = -1, + int64 eta = -1) + { + base(state); + this.bytes_total = total; + this.dl_progress = progress; + this.dl_speed = speed; + this.eta = eta; + } + + public override double progress + { + get { return (double) dl_progress; } + } + + public override string? progress_string + { + owned get + { + string[] result = {}; + + if(eta >= 0) + result += C_("epic_dl_status", + "%s left;").printf(GameHub.Utils.seconds_to_string(eta)); + + if(dl_progress >= 0) + result += C_("epic_dl_status", + "%d%%").printf((int) (dl_progress * 100)); + + if(bytes_total >= 0) + result += C_("epic_dl_status", + "(%1$s / %2$s)").printf(format_size((int) (dl_progress * bytes_total)), + format_size(bytes_total)); + + if(dl_speed >= 0) + result += C_("epic_dl_status", + "[%s/s]").printf(format_size(dl_speed)); + + return string.joinv(" ", + result); + } + } + } + } +} diff --git a/src/data/sources/epicgames/EpicGame.vala b/src/data/sources/epicgames/EpicGame.vala new file mode 100644 index 00000000..e5b7ac13 --- /dev/null +++ b/src/data/sources/epicgames/EpicGame.vala @@ -0,0 +1,1575 @@ +using Gee; + +using GameHub.Data.DB; +using GameHub.Data.Runnables; +using GameHub.Data.Runnables.Tasks.Install; +using GameHub.Data.Tweaks; +using GameHub.Utils; + +namespace GameHub.Data.Sources.EpicGames +{ + // Each game gets combined through an Asset, Metadata and a Manifest. + // These three contain sub information for a game. + public class EpicGame: Game, + Traits.HasExecutableFile, Traits.SupportsCompatTools, + Traits.Game.SupportsTweaks + { + // // Traits.HasActions + // public override ArrayList? actions { get; protected set; default = new ArrayList(); } + + // Traits.HasExecutableFile + public override string? executable_path { owned get; set; } + public override string? work_dir_path { owned get; set; } + public override string? arguments { owned get; set; } + public override string? environment { owned get; set; } + + // Traits.SupportsCompatTools + public override string? compat_tool { get; set; } + public override string? compat_tool_settings { get; set; } + + // Traits.Game.SupportsTweaks + public override TweakSet? tweaks { get; set; default = null; } + + private bool game_info_updating = false; + private bool game_info_updated = false; + + // Legendary mapping + internal string app_name { get { return id; } } + internal string app_title { get { return name; } } + internal string? app_version { get { return version; } } + internal ArrayList base_urls // base urls for download, only really used when cached manifest is current + { + owned get + { + var urls = new ArrayList(); + return_val_if_fail(metadata.get_object().has_member("base_urls"), + urls); + + metadata.get_object().get_array_member("base_urls").foreach_element((array, index, node) => { + urls.add(node.get_string()); + }); + + return urls; + } + set + { + var urls = new Json.Array(); + value.foreach(url => { + urls.add_string_element(url); + + return true; + }); + + metadata.get_object().set_array_member("base_urls", + urls); + write(FS.Paths.EpicGames.Metadata, + get_metadata_filename(), + Json.to_string(metadata, + true).data); + } + } + internal Asset? asset_info { get; default = null; } + // public Json.Object? asset_info; + // public Json.Object? metadata; + private Json.Node _metadata = new Json.Node(Json.NodeType.NULL); + internal Json.Node metadata // FIXME: make a class for easier access? + { + owned get + { + if(_metadata.get_node_type() == Json.NodeType.NULL) + { + // FIXME: this will never update this way + _metadata = Parser.parse_json_file(FS.Paths.EpicGames.Metadata, + get_metadata_filename()); + + if(_metadata.get_node_type() != Json.NodeType.NULL) return _metadata; + + update_metadata(); + + if(_metadata.get_node_type() != Json.NodeType.NULL) return _metadata; + + // create new empty metadata + _metadata = new Json.Node(Json.NodeType.OBJECT); + _metadata.set_object(new Json.Object()); + } + + return _metadata; + } + } + + internal File? repair_file + { + owned get + { + return FS.file(Environment.get_tmp_dir(), + id + ".repair"); + } + } + + internal string latest_version { get { return asset_info.build_version; } } + internal bool has_updates + { + get + { + if(version == null) return false; + + return version != latest_version; + } + } + + internal bool needs_verification { get; set; default = false; } + internal bool needs_repair { get; default = false; } + internal bool requires_ownership_token { get; default = false; } + internal string launch_command + { + get + { + return manifest.meta.launch_command; + } + } + internal bool can_run_offline + { + get + { + return_val_if_fail(metadata.get_object().has_member("customAttributes"), + false); + return_val_if_fail(metadata.get_object().get_member("customAttributes").get_node_type() != Json.NodeType.OBJECT, + false); + return_val_if_fail(metadata.get_object().get_object_member("customAttributes").has_member("CanRunOffline"), + false); + return_val_if_fail(metadata.get_object().get_object_member("customAttributes").get_member("CanRunOffline").get_node_type() != Json.NodeType.OBJECT, + false); + return_val_if_fail(metadata.get_object().get_object_member("customAttributes").get_object_member("CanRunOffline").has_member("value"), + false); + + return metadata.get_object().get_object_member("customAttributes").get_object_member("CanRunOffline").get_string_member("value") == "true"; // why no boolean?! + } + } + private int64 _install_size = 0; + internal int64 install_size + { + get + { + if(_install_size == 0) + { + foreach(var element in manifest.file_manifest_list.elements) + { + _install_size += element.file_size; + } + } + + return _install_size; + } + } + // internal string egl_guid; + // internal Json.Node prereq_info; + private Manifest? _manifest = null; + internal Manifest manifest + { + owned get + { + if(_manifest == null) + { + if(version != null) + { + _manifest = EpicGames.load_manifest(load_manifest_from_disk()); + } + else + { + Bytes data; + get_cdn_manifest(out data); + _manifest = EpicGames.load_manifest(data); + } + } + + return _manifest; + } + set + { + _manifest = value; + } + } + + public ArrayList? dlc { get; protected set; default = null; } + + internal bool is_dlc + { + get + { + if(info_detailed == null) return false; + + var json = Parser.parse_json(info_detailed); + return_val_if_fail(json.get_node_type() == Json.NodeType.OBJECT, + false); + + return json.get_object().has_member("mainGameItem"); + } + } + + internal bool supports_cloud_saves + { + get + { + return metadata.get_object().has_member("customAttributes") + && metadata.get_object().get_object_member("customAttributes").has_member("CloudSaveFolder"); + } + } + + public EpicGame(EpicGames source, + Asset asset, + Json.Node? meta = null) + { + this.source = source; + id = asset.asset_id; + + // this.version = asset.build_version; // Only gets permanently saved for installed games + // this.info = asset.to_string(false); + if(meta != null) _metadata = meta; + + _asset_info = asset; + load_version(); + name = metadata.get_object().get_string_member_with_default("title", + ""); + + install_dir = null; + this.status = new Game.Status(Game.State.UNINSTALLED, + this); + this.work_dir_path = ""; + + update_game_info.begin(); + init_tweaks(); + } + + public EpicGame.from_db(EpicGames src, + Sqlite.Statement s) + { + source = src; + + // TODO: verify, add custom values + dbinit(s); + dbinit_executable(s); + dbinit_compat(s); + dbinit_tweaks(s); + + _asset_info = EpicGames.instance.get_game_asset(id); + + // update_status(); + update_game_info.begin(); + } + + public override async void update_game_info() + { + if(game_info_updating) return; + + game_info_updating = true; + + var meta_object_node = metadata.get_object(); + + if(meta_object_node.has_member("keyImages") + && meta_object_node.get_member("keyImages").get_node_type() == Json.NodeType.ARRAY) + { + meta_object_node.get_array_member("keyImages").foreach_element( + (array, index, node) => + { + if(node.get_node_type() != Json.NodeType.OBJECT) + { + return; + } + + if(!node.get_object().has_member("type") + || !node.get_object().has_member("url")) + { + return; + } + + switch(node.get_object().get_string_member("type")) + { + case "DieselGameBox": + image = node.get_object().get_string_member("url"); + break; + case "DieselGameBoxTall": + image_vertical = node.get_object().get_string_member("url"); + break; + case "Thumbnail": + icon = node.get_object().get_string_member("url"); + break; + } + }); + } + + platforms.clear(); + + if(meta_object_node.has_member("releaseInfo") + && meta_object_node.get_member("releaseInfo").get_node_type() == Json.NodeType.ARRAY) + { + meta_object_node.get_array_member("releaseInfo").foreach_element((array, index, node) => { + if(node.get_node_type() != Json.NodeType.OBJECT + || !node.get_object().has_member("appId") + || node.get_object().get_string_member("appId") != this.id + || !node.get_object().has_member("platform") + || node.get_object().get_member("platform").get_node_type() != Json.NodeType.ARRAY) + { + return; + } + + node.get_object().get_array_member("platform").foreach_element((a, i, n) => { + if(n.get_node_type() != Json.NodeType.VALUE) + { + return; + } + + foreach(var platform in Platform.PLATFORMS) + { + // Windows, Mac, Win32 + if(n.get_string().down() == platform.id()) + { + platforms.add(platform); + } + } + }); + }); + } + + if(image == null || image == "") + { + image = icon; + } + + if(image_vertical == null || image_vertical == "") + { + image_vertical = icon; + } + + // TODO: get installed status + + if(game_info_updated) + { + game_info_updating = false; + + return; + } + + save(); + update_status(); + + game_info_updated = true; + game_info_updating = false; + } + + // TODO: verify and correct this + public override void update_status() + { + if(status.state == Game.State.DOWNLOADING && status.download.status.state != Downloader.Download.State.CANCELLED) return; + + var state = Game.State.UNINSTALLED; + + // var gameinfo = get_file("gameinfo"); + // var goggame = get_file(@"goggame-$(id).info"); + var gh_marker = get_file(@".gamehub_$(id)"); + + var files = new ArrayList(); + + // files.add(goggame); + files.add(gh_marker); + + if(!(this is DLC)) + { + files.add(executable); + // files.add(gameinfo); + } + + foreach(var file in files) + { + if(file != null && file.query_exists()) + { + state = Game.State.INSTALLED; + break; + } + } + + status = new Game.Status(state, + this); + + if(state == Game.State.INSTALLED) + { + remove_tag(Tables.Tags.BUILTIN_UNINSTALLED); + add_tag(Tables.Tags.BUILTIN_INSTALLED); + } + else + { + add_tag(Tables.Tags.BUILTIN_UNINSTALLED); + remove_tag(Tables.Tags.BUILTIN_INSTALLED); + } + + load_version(); + + // actions.clear(); + // if(version != asset_info.build_version) + // { + // actions.add(new RunnableAction(this)); + // } + } + + public override async void run() + { + // TODO: this never gets called? + } + + public override async void pre_run() + { + if(is_dlc) + { + debug("[Source.EpicGame.pre_run] tried starting dlc"); + // TODO: launch main game? + } + + // TODO: offline? + assert(can_run_offline || yield EpicGames.instance.authenticate()); + + // TODO: Do we need this? + // debug("[Source.EpicGame.pre_run] refreshing login…"); + // ((EpicGames) source).login(); + + // TODO: check for updates + if(latest_version != version) + { + debug("[Source.EpicGame.pre_run] game is out of date"); + } + + // TODO: sync save files? E.g. Rocket League fails if no save was found + // the prefix has to exist already for this + } + + public override ExecTask prepare_exec_task(string[]? cmdline_override = null, + string[]? args_override = null) + { + string[] cmd = cmdline_override ?? cmdline; + string[] full_cmd = cmd; + + var variables = get_variables(); + var args = args_override ?? Utils.parse_args(arguments); + + if(args != null) + { + if("$command" in args || "${command}" in args) + { + full_cmd = {}; + } + + foreach(var arg in args) + { + if(arg == "$command" || arg == "${command}") + { + foreach(var a in cmd) + { + full_cmd += a; + } + } + else + { + if("$" in arg) + { + arg = FS.expand(arg, + null, + variables); + } + + full_cmd += arg; + } + } + } + + foreach(var arg in get_launch_parameters()) + { + full_cmd += arg; + } + + var task = Utils.exec(full_cmd).override_runtime(true).dir(work_dir.get_path()); + + cast(game => task.tweaks(game.tweaks, + game)); + + if(environment != null && environment.length > 0) + { + var env = Parser.json_object(Parser.parse_json(environment), + {}); + + if(env != null) + { + env.foreach_member((obj, name, node) => { + task.env_var(name, + node.get_string()); + }); + } + } + + return task; + } + + public override async void post_run() + { + // TODO: sync save files? + } + + // public void update_info(Json.Node json) + // { + // info = Json.to_string(json, false); + // } + + public override async void uninstall() + { + if(install_dir != null && install_dir.query_exists()) + { + // yield umount_overlays(); + + FS.rm(install_dir.get_path(), + "", + "-rf"); + update_status(); + } + + if((install_dir == null || !install_dir.query_exists()) && (executable == null || !executable.query_exists())) + { + install_dir = null; + executable = null; + save(); + update_status(); + } + } + + public override async ArrayList? load_installers() + { + if(installers != null && installers.size > 0) return installers; + + installers = new ArrayList(); + + foreach(var platform in platforms) + { + installers.add(new Installer(this, + platform)); + } + + is_installable = installers.size > 0; + + return installers; + } + + public void add_dlc(Asset asset, + Json.Node? metadata = null) + { + if(dlc == null) + { + dlc = new ArrayList(); + } + + dlc.add(new DLC(this, + asset, + metadata)); + } + + public Json.Node to_json() + { + var json = new Json.Node(Json.NodeType.OBJECT); + var urls = new Json.Node(Json.NodeType.ARRAY); + base_urls.foreach(url => { + urls.get_array().add_string_element(url); + + return true; + }); + + json.get_object().set_string_member("app_name", + id); + json.get_object().set_string_member("app_title", + name); + json.get_object().set_string_member("app_version", + version); + json.get_object().set_object_member("asset_info", + asset_info.to_json().get_object()); + json.get_object().set_array_member("base_urls", + urls.get_array()); + json.get_object().set_object_member("metadata", + metadata.get_object()); + + return json; + } + + public async bool import(File import_dir, + string egl_guid = "") + { + // if(!yield authenticate()) return false; + + // if(get_game(game, true) == null) + // { + // debug("[Source.EpicGames.import] Did not find game \"%s\" on account.", game.name); + // return false; + // } + + Manifest manifest; + _needs_verification = true; + Bytes? manifest_data = null; + + // check if the game is from an EGL installation, load manifest if possible + var egstore_path = Path.build_filename(import_dir.get_path(), + ".egstore"); + + if(File.new_for_path(egstore_path).query_exists()) + { + File? manifest_file = null; + + if(egl_guid != "") + { + try + { + var egstore_dir = Dir.open(egstore_path); + string? file_name = null; + + while((file_name = egstore_dir.read_name()) != null) + { + if(!(".mancpn" in file_name)) + { + continue; + } + + debug("[Source.EpicGames.import_game] Checking mancpn file: %s", + file_name); + var mancpn = Parser.parse_json_file(egstore_path, + file_name); + + if(mancpn.get_node_type() == Json.NodeType.OBJECT + || mancpn.get_object().has_member("AppName")) + { + debug("[Source.EpicGames.import_game] Found EGL install metadata, verifying…"); + manifest_file = FS.file(egstore_path, + file_name); + break; + } + } + } + catch (Error e) + { + debug("[Source.EpicGames.import_game] No EGL data found: %s", + e.message); + } + } + else + { + manifest_file = File.new_build_filename(egstore_path, + egl_guid + ".manifest"); + } + + if(manifest_file != null && manifest_file.query_exists()) + { + try + { + manifest_data = manifest_file.load_bytes(); + } + catch (Error e) + { + debug("[Source.EpicGames.import_game] Error reading manifest file: %s", + e.message); + } + } + else + { + debug("[Source.EpicGames.import_game] .egstore folder exists but manifest file is missing, continuing as regular import…"); + } + + // If there's no in-progress installation assume the game doesn't need to be verified + var bps_path = Path.build_filename(egstore_path, + "bps"); + var pending_path = Path.build_filename(egstore_path, + "Pending"); + + if(manifest_file != null && File.new_for_path(bps_path).query_exists()) + { + _needs_verification = false; + + if(File.new_for_path(pending_path).query_exists()) + { + try + { + Dir.open(pending_path); + _needs_verification = true; + } + catch (Error e) {} + } + + if(!needs_verification) + { + debug("[Source.EpicGames.import_game] No in-progress installation found, assuming complete…"); + } + } + } + + ArrayList tmp_urls; + + if(manifest_data == null) + { + debug("[Source.EpicGames.import_game] Downloading latest manifest for: %s", + id); + get_cdn_manifest(out manifest_data, + out tmp_urls); + + if(base_urls.is_empty) + { + base_urls = tmp_urls; + // save_metadata(); + } + } + else + { + // base urls being empty isn't an issue, they'll be fetched when updating/repairing the game + tmp_urls = base_urls; + } + + manifest = EpicGames.load_manifest(manifest_data); + save_manifest(manifest_data, + manifest.meta.build_version); + // uint install_size = 0; + // manifest.file_manifest_list.elements.foreach(file_manifest => { + // install_size += file_manifest.file_size; + // return true; + // }); + + // TODO: do we care about these? + // var prereq = new Json.Node(Json.NodeType.OBJECT); + // prereq.set_object(new Json.Object()); + // if(manifest.meta.prereq_ids != null) + // { + // var prereq_ids = new Json.Node(Json.NodeType.ARRAY); + // prereq_ids.set_array(new Json.Array()); + // manifest.meta.prereq_ids.foreach(id => { + // prereq_ids.get_array().add_string_element(id); + // return true; + // }); + + // prereq.get_object().set_member("ids", prereq_ids); + // prereq.get_object().set_string_member("name", manifest.meta.prereq_name); + // prereq.get_object().set_string_member("path", manifest.meta.prereq_path); + // prereq.get_object().set_string_member("args", manifest.meta.prereq_args); + // } + + // var metadata = Parser.parse_json(info_detailed).get_object(); + // var offline = metadata.get_object_member("customAttributes").get_boolean_member_with_default("CanRunOffline", true); + // var ot = metadata.get_object_member("customAttributes").get_boolean_member_with_default("OwnershipToken", false); + + // TODO: legendary strips all leading '/' here + executable_path = FS.file(import_dir.get_path(), + manifest.meta.launch_exe).get_path(); + + // check if most files at least exist or if user might have specified the wrong directory + var total_files = manifest.file_manifest_list.elements.size; + int found_files = 0; + manifest.file_manifest_list.elements.foreach(file_manifest => + { + var file = FS.file(import_dir.get_path(), + file_manifest.filename); + + if(file.query_exists()) + { + found_files++; + } + else + { + warning("[Source.EpicGames.import] File could not be found at: %s", + file.get_path()); + } + + return true; + }); + + var exe = FS.file(executable_path); + + if(!exe.query_exists()) + { + warning("[Source.EpicGames.import] Game executable could not be found at: %s", + exe.get_path()); + + // executable_path = null; + return false; + } + + var ratio = found_files / total_files; + + if(ratio < 0.95) + { + warning( + "[Source.EpicGames.import] Some files are missing from the game installation, install may not " + + "match latest Epic Games Store version or might be corrupted."); + _needs_verification = true; + } + else + { + GLib.info("[Source.EpicGames.import] Game install appears to be complete."); + } + + if(needs_verification) + { + GLib.info("[Source.EpicGames.import] The game installation will have to be verified before it can be updated"); + } + else + { + GLib.info( + "[Source.EpicGames.import] Installation had Epic Games Launcher metadata for version %s ".printf(version) + + "verification will not be required."); + } + + GLib.info("[Source.EpicGames.import] Game has been imported: %s", + id); + + return true; + + // Don't do this here, we're calling install afterwards anyway + // // FIXME: what even is this mess? + // game.prereq_info = prereq; + // // game.base_urls = base_urls; FIXME: + // game.install_dir = import_dir; + // game.version = manifest.meta.build_version; + // game.executable = File.new_build_filename("${install_dir}", manifest.meta.launch_exe); + // game.can_run_offline = offline; + // // game.launch_parameters = manifest.meta.launch_command; + // game.needs_verification = needs_verification; + // // game.install_size = install_size; + // game.egl_guid = egl_guid; + // // TODO: all above into meta? check what's needed + // // game.meta = manifest.meta; + } + + internal async void verify() + { + var manifest_data = get_installed_manifest(); // FIXME: cdn_manifest? + var manifest = EpicGames.load_manifest(manifest_data); + + var files = manifest.file_manifest_list.elements; + files.sort( + (a, b) => { + return strcmp(a.filename, + b.filename); + }); + + // build list of hashes + var file_list = new HashMap(); + files.foreach(file => { + file_list.set(file.filename, + file.sha_hash); + + return true; + }); + + debug(@"[Sources.EpicGames.verify_game] Verifying \"$(id)\" version \"$(latest_version)\""); + var repair_file = new ArrayList(); + + var result = yield validate_files(install_dir.get_path(), + file_list); + + result.matching.foreach(match => { + repair_file.add(match); + + return true; + }); + + result.failed.foreach(fail => { + repair_file.add(fail); + + return true; + }); + + // always write repair file + try + { + var file = FS.file(Environment.get_tmp_dir(), + id + ".repair"); + var io_stream = file.create_readwrite(FileCreateFlags.REPLACE_DESTINATION); + var output_stream = new DataOutputStream(io_stream.output_stream); + foreach(var match in repair_file) + { + output_stream.put_string(match + "\n"); + } + + io_stream.close(); + debug(@"[Sources.EpicGames.verify_game] written repair file to: $(file.get_path())"); + } + catch (Error e) {} + + if(!result.missing.is_empty || !result.failed.is_empty) + { + debug(@"[Sources.EpicGames.verify_game] Verification failed, $(result.failed.size) corrupted, $(result.missing.size) missing"); + _needs_repair = true; + } + + GLib.info("[Sources.EpicGames.verify_game] Verification finished successfully"); + } + + private string[] get_launch_parameters() + { + var game_token = ""; + + if(EpicGames.instance.is_authenticated()) + { + debug("[Sources.EpicGames.get_launch_parameters] getting auth token…"); + game_token = EpicGamesServices.instance.get_game_token().get_object().get_string_member("code"); + } + + string[] parameters = {}; + + // FIXME: gives me some random bytes, don't know why + // if(game.launch_parameters != "") + // { + // parameters = game.launch_parameters.split(" "); + // } + + parameters += "-AUTH_LOGIN=unused"; + parameters += @"-AUTH_PASSWORD=$game_token"; + parameters += "-AUTH_TYPE=exchangecode"; + parameters += @"-epicapp=$(id)"; + parameters += "-epicenv=Prod"; + + // TODO: where do we set this? + if(requires_ownership_token) + { + debug("[Sources.EpicGames.get_launch_parameters] getting ownership token…"); + var ownership_token = EpicGamesServices.instance.get_ownership_token( + asset_info.ns, + asset_info.catalog_item_id); + // TODO: write to tmp path? + write(FS.Paths.EpicGames.Cache, + @"$(asset_info.ns)$(asset_info.catalog_item_id).ovt", + ownership_token.get_data()); + parameters += "-epicovt=%s".printf(FS.file(FS.Paths.EpicGames.Cache, + @"$(asset_info.ns)$(asset_info.catalog_item_id).ovt").get_path()); + } + + // TODO: language + // var language_code = language_code; + + parameters += "-EpicPortal"; + parameters += @"-epicusername=$(EpicGames.instance.user_name)"; + parameters += @"-epicuserid=$(EpicGames.instance.user_id)"; + parameters += @"-epiclocale=en"; // FIXME: hardcoded for now + + return parameters; + } + + public int64 get_installation_size(Platform platform) + { + if(platform != Platform.WINDOWS) + { + Bytes data; + get_cdn_manifest(out data, + null, + uppercase_first_character(platform.id())); + var manifest = EpicGames.load_manifest(data); + + int64 size = 0; + foreach(var element in manifest.file_manifest_list.elements) + { + size += element.file_size; + } + + return size; + } + + return install_size; + } + + // Hack around inability to use out in async functions + private class ValidationResult + { + public ArrayList matching { get; set; default = new ArrayList(); } + public ArrayList missing { get; set; default = new ArrayList(); } + public ArrayList failed { get; set; default = new ArrayList(); } + } + + private static async ValidationResult validate_files(string path, + HashMap file_list, + ChecksumType hash_type = ChecksumType.SHA1) + requires(FS.file(path).query_exists()) + requires(file_list.size > 0) + { + var result = new ValidationResult(); + + foreach(var entry in file_list) + { + var file_path = entry.key; + var file_hash = entry.value; + + var full_path = FS.file(path, + file_path); + + if(!full_path.query_exists()) + { + result.missing.add(file_path); + continue; + } + + // debug("[Sources.EpicGames.validate_game_files] " + full_path.get_path()); + var real_hash = yield compute_file_checksum(full_path, + hash_type); + + if(real_hash != null && real_hash != bytes_to_hex(file_hash)) + { + debug("failed hash check: %s, %s != %s", + file_path, + bytes_to_hex(file_hash), + real_hash); + result.failed.add(string.join(":", + real_hash, + file_path)); + } + else if(real_hash != null) + { + result.matching.add(string.join(":", + real_hash, + file_path)); + } + else + { + debug(@"[Sources.EpicGames.validate_game_files] Could not verify \"$file_path\""); + result.missing.add(file_path); + } + } + + return result; + } + + private void get_cdn_urls(out ArrayList manifest_urls, + out ArrayList? base_urls, + string platform_override = "") + { + var platform = platform_override == "" ? "Windows" : platform_override; + var manifest_api_result = EpicGamesServices.instance.get_game_manifest( + asset_info.ns, + asset_info.catalog_item_id, + id, + platform); + + // never seen this outside the launcher itself, but if it happens: PANIC! + assert(manifest_api_result.get_object().has_member("elements")); + var elements_array = manifest_api_result.get_object().get_array_member("elements"); + assert(elements_array.get_length() <= 1); + + base_urls = new ArrayList(); + manifest_urls = new ArrayList(); + var tmp1 = new ArrayList(); + var tmp2 = new ArrayList(); + elements_array.get_object_element(0).get_array_member("manifests").foreach_element((array, index, node) => { + var uri = node.get_object().get_string_member("uri"); + var base_url = uri.substring(0, + uri.last_index_of("/")); + + if(!tmp1.contains(base_url)) + { + tmp1.add(base_url); + } + + if(node.get_object().has_member("queryParams")) + { + var parameters_array = node.get_object().get_array_member("queryParams"); + string parameter = ""; + parameters_array.foreach_element((a, i, n) => { + var name = n.get_object().get_string_member("name"); + var value = n.get_object().get_string_member("value"); + + if(i == 0) + { + parameter = name + "=" + value; + } + else + { + parameter = parameter + "&" + name + "=" + value; + } + }); + tmp2.add(uri + "?" + parameter); + } + else + { + tmp2.add(uri); + } + }); + + // Hack around inability of using references in lambdas + base_urls.add_all(tmp1); + manifest_urls.add_all(tmp2); + } + + private void get_cdn_manifest(out Bytes data, + out ArrayList? base_urls = null, + string platform_override = "") + { + ArrayList manifest_urls; + get_cdn_urls(out manifest_urls, + out base_urls, + platform_override); + + EpicGamesServices.instance.get_cdn_manifest(manifest_urls[0], + out data); + } + + private void save_manifest(Bytes bytes, + string version = this.version) + { + var name = get_manifest_filename(version); + write(FS.Paths.EpicGames.Manifests, + name, + bytes.get_data()); + } + + private Bytes get_installed_manifest() { return load_manifest_from_disk(); } + + internal Bytes? load_manifest_from_disk() + { + uint8[] data; + try + { + debug("Loading cached manifest: %s", + FS.file(FS.Paths.EpicGames.Manifests, + get_manifest_filename()).get_path()); + FileUtils.get_data(FS.file(FS.Paths.EpicGames.Manifests, + get_manifest_filename()).get_path(), + out data); + } + catch (FileError e) + { + debug("error: %s", + e.message); + + return null; + } + + return new Bytes(data); + } + + private string get_manifest_filename(string version = this.version) + { + // TODO: Escape/Normalize filename + return @"$(id)_$version.manifest"; + } + + private string get_metadata_filename() + { + // TODO: Escape/Normalize filename + return @"$id.json"; + } + + // private Json.Node get_metadata() + // { + // var json = Parser.parse_json_file(FS.Paths.EpicGames.Metadata, get_metadata_filename()); + // if(json.get_node_type() == Json.NodeType.NULL) + // { + // json = new Json.Node(Json.NodeType.OBJECT); + // json.set_object(new Json.Object()); + // } + // return json; + // } + + // internal void save_metadata() + // { + // // TODO: Save base_urls in json + // write(FS.Paths.EpicGames.Metadata, get_metadata_filename(), Json.to_string(metadata, true).data); + // } + + internal Analysis prepare_download(Runnables.Tasks.Install.InstallTask task) + { + ArrayList tmp_urls; + Bytes new_bytes; + Manifest? old_manifest = null; + + var tmp2_urls = base_urls; // copy list for manipulation + var old_bytes = version != null ? get_installed_manifest() : null; + + if(old_bytes == null) + { + debug("[Sources.EpicGames.prepare_download] Could not load old manifest, patching will not work!"); + } + else + { + old_manifest = EpicGames.load_manifest(old_bytes); + } + + get_cdn_manifest(out new_bytes, + out tmp_urls); + + tmp_urls.foreach(url => { + if(!tmp2_urls.contains(url)) + { + tmp2_urls.add(url); + } + + return true; + }); + + base_urls = tmp2_urls; + // save_metadata(); // save base urls to game metadata + + var new_manifest = EpicGames.load_manifest(new_bytes); + save_manifest(new_bytes, + new_manifest.meta.build_version); + + // check if we should use a delta manifest or not + Manifest delta_manifest; + + if(old_manifest != null && new_manifest != null) + { + var delta_manifest_data = EpicGamesServices.instance.get_delta_manifest( + base_urls[Random.int_range(0, + base_urls.size - 1)], + old_manifest.meta.build_id, + new_manifest.meta.build_id); + + if(delta_manifest_data != null) + { + delta_manifest = EpicGames.load_manifest(delta_manifest_data); + debug( + "[Sources.EpicGames.prepare_download] Using optimized delta manifest to upgrade form build" + + @"$(old_manifest.meta.build_id) to $(new_manifest.meta.build_id)"); + // TODO: combine_manifests(new_manifest, delta_manifest); + } + else + { + debug("[Sources.EpicGames.prepare_download] No Delta manifest received from CDN"); + } + } + + // TODO: DLC + + var force = false; // hardcoded for now + // var install_path = task.install_dir; + File? resume_file = null; + + if(needs_repair) + { + // use installed manifest for repairs instead of updating + // new_manifest = old_manifest; + // old_manifest = null; + + resume_file = FS.file(Environment.get_tmp_dir(), + id + ".repair"); + force = false; + } + else if(!force) + { + resume_file = FS.file(Environment.get_tmp_dir(), + id + ".resume"); + } + + var base_url = base_urls[Random.int_range(0, + base_urls.size - 1)]; + debug("[Sources.EpicGames.prepare_download] Using base_url: %s", + base_url); + + // TODO: Download optimizations + // var process_opt = false; + + // FIXME: Things get messy from here on because I had to unscramble Legendarys whole dowload manager + + // DLM + var download_task = new Analysis.from_analysis(task, + base_url, + new_manifest, + old_manifest, + resume_file); + + // TODO: prereq + // var url = base_url + "/" + chunk.path; + // TODO: + return download_task; + } + + internal void update_metadata() + { + var tmp_urls = base_urls; // save temporarily from old metadata + _metadata = EpicGamesServices.instance.get_game_info(asset_info.ns, + asset_info.catalog_item_id); + base_urls = tmp_urls; // paste them back into new metadata + // FIXME: Setting base_urls also saves + write(FS.Paths.EpicGames.Metadata, + get_metadata_filename(), + Json.to_string(metadata, + true).data); + } + + // public new async void install(InstallTask.Mode install_mode = InstallTask.Mode.INTERACTIVE, bool update = false) + // { + // if(update) + // { + // ArrayList? dirs = new ArrayList(); + // dirs.add(install_dir); + // var task = new InstallTask(this, installers, dirs, install_mode, false); + // yield task.start(); + // } + // else + // { + // if(status.state != Game.State.UNINSTALLED || !is_installable) return; + // var task = new InstallTask(this, installers, source.game_dirs, install_mode, true); + // yield task.start(); + // } + // } + + // private ArrayList get_save_games() + // { + // var savegames = EpicGamesServices.instance.get_user_cloud_saves(id, id != "" ? true : false); + // var saves = new ArrayList(); + + // debug("json dump: \n%s", Json.to_string(savegames, true)); + + // savegames.get_object().get_object_member("files").foreach_member( + // (object, name, node) => { + // var filename = node.get_object().get_string_member("fname"); + // var file = node.get_object().get_object_member("f"); + + // if(!filename.contains(".manifest")) + // { + // continue; + // } + + // var file_parts = filename.split("/"); + // saves.add(new SaveGameFile(file_parts[2], filename, file_parts[4], new DateTime.from_iso8601(file.get_object().get_string_member("lastModified")[: -1]))); + // }); + + // return saves; + // } + + // // FIXME: requires prefix present! + // private async string? get_cloud_save_path() + // { + // return_val_if_fail(metadata.get_object().has_member("customAttributes"), null); + // return_val_if_fail(metadata.get_object().get_member("customAttributes").get_node_type() != Json.NodeType.OBJECT, null); + // return_val_if_fail(metadata.get_object().get_object_member("customAttributes").has_member("CloudSaveFolder"), null); + // return_val_if_fail(metadata.get_object().get_object_member("customAttributes").get_member("CloudSaveFolder").get_node_type() != Json.NodeType.OBJECT, null); + // return_val_if_fail(metadata.get_object().get_object_member("customAttributes").get_object_member("CloudSaveFolder").has_member("value"), null); + // var save_path = metadata.get_object().get_object_member("customAttributes").get_object_member("CloudSaveFolder").get_string_member("value"); + // save_path.replace("{", "${"); // prepare for FS.expand + + // var path_vars = new HashMap(); + // path_vars.set("{installdir}", install_dir.get_path()); + // path_vars.set("{epicid}", EpicGames.instance.user_id); + // path_vars.set("{appdata}", yield convert_path_to_unix(this, yield query_registry(this, "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders", "AppData"))); + // path_vars.set("{userdir}", yield convert_path_to_unix(this, yield query_registry(this, "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders", "Personal"))); + // path_vars.set("{usersavedgames}", yield convert_path_to_unix(this, yield query_registry(this, "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders", "{4C5C32FF-BB9D-43B0-B5B4-2D72E54EAAA4}"))); + + // // not needed + // // save_path = save_path.replace("\\", "/"); + + // return FS.expand(save_path, null, path_vars); + // } + + // // FIXME: where to put this? + // private virtual async string convert_path_to_unix(Traits.SupportsCompatTools runnable, string path) + // { + // var task = Utils.exec({executable.get_path(), "winepath", "-u", path}).log(false); + // apply_env(runnable, task, null); + // var unix_path = (yield task.sync_thread(true)).output.strip(); + // debug("[Wine.convert_path_to_unix] '%s' -> '%s'", path, unix_path); + // return unix_path; + // } + + // // FIXME: where to put this? + // private virtual async string query_registry(Traits.SupportsCompatTools runnable, string path, string value) + // { + // var task = Utils.exec({executable.get_path(), "wine", "reg", "query", path, "/v", value}).log(false); + // apply_env(runnable, task, null); + // var result = (yield task.sync_thread(true)).output.strip(); + // debug("[Wine.query_registry] result: '%s'", result); + // return result; + // } + + // // TODO: make SaveGameFile an property of EpicGame + // private SaveGameFile.Status check_savegame_state(File path, SaveGameFile? save, out DateTime local, out DateTime remote) + // { + // // legendary does a os.walk here + // var latest = 0; + + // if(latest == 0 && save == null) return SaveGameFile.Status.NO_SAVE; + + // try { + // local = path.query_info("*", FileQueryInfoFlags.NONE).get_modification_date_time(); + // } catch (Error e) { + // debug("error: " + e.message); + // } + + // if(save == null) + // { + // return SaveGameFile.Status.LOCAL_NEWER; + // } + + // int year, month, day, hour, minute; + // double seconds; + // save.manifest_name.scanf("%Y.%m.%d-%H.%M.%S.manifest", &year, &month, &day, &hour, &minute, &seconds); + // remote = DateTime(TimeZone.utc(), year, month, day, hour, minute, seconds); + + // if(latest == 0) return SaveGameFile.Status.REMOTE_NEWER; + + // debug("[EpicGame.check_savegame_state] local: %s, remote: %s", local.to_string(), remote.to_string()); + + // // Ideally we check the files themselves based on manifest, + // // this is mostly a guess but should be accurate enough. + // if(local.difference(remote).abs() < TimeSpan.MINUTE) + // { + // return SaveGameFile.Status.SAME_AGE; + // } + // else if(local.compare(remote) > 0) + // { + // return SaveGameFile.Status.LOCAL_NEWER; + // } + + // return SaveGameFile.Status.REMOTE_NEWER; + // } + + private void upload_save() {} + private void download_saves() {} + + public class DLC: EpicGame + { + public EpicGame game; + + public DLC(EpicGame game, + Asset asset, + Json.Node? metadata = null) + { + base( + game.source as EpicGames, + asset, + metadata); + + icon = game.icon; + image = game.image; + + install_dir = game.install_dir; + work_dir = game.work_dir; + executable = game.executable; + + platforms = game.platforms; + + this.game = game; + update_status(); + } + } + + public class Asset + { + public string app_name; + public string asset_id; + public string build_version; + public string catalog_item_id; + public string label_name; + public string ns; + // public Json.Node asset; + public Json.Node metadata; + + // public GameAsset() {} + + public Asset.from_egs_json(Json.Node json) + { + assert(json.get_node_type() == Json.NodeType.OBJECT); + app_name = json.get_object().get_string_member_with_default("appName", + ""); + asset_id = json.get_object().get_string_member_with_default("assetId", + ""); + build_version = json.get_object().get_string_member_with_default("buildVersion", + ""); + catalog_item_id = json.get_object().get_string_member_with_default("catalogItemId", + ""); + label_name = json.get_object().get_string_member_with_default("labelName", + ""); + ns = json.get_object().get_string_member_with_default("namespace", + ""); + + // asset = json; + if(json.get_object().has_member("metadata")) + { + metadata = json.get_object().get_member("metadata"); + } + else + { + metadata = new Json.Node(Json.NodeType.OBJECT); + metadata.set_object(new Json.Object()); + } + + // json.get_object().set_object_member("metadata", metadata.get_object()); + } + + public Asset.from_json(Json.Node json) + { + assert(json.get_node_type() == Json.NodeType.OBJECT); + app_name = json.get_object().get_string_member_with_default("app_name", + ""); + asset_id = json.get_object().get_string_member_with_default("asset_id", + ""); + build_version = json.get_object().get_string_member_with_default("build_version", + ""); + catalog_item_id = json.get_object().get_string_member_with_default("catalog_item_id", + ""); + label_name = json.get_object().get_string_member_with_default("label_name", + ""); + ns = json.get_object().get_string_member_with_default("namespace", + ""); + + if(json.get_object().has_member("metadata")) + { + metadata = json.get_object().get_member("metadata"); + } + else + { + metadata = new Json.Node(Json.NodeType.OBJECT); + metadata.set_object(new Json.Object()); + } + } + + public Json.Node to_json() + { + var json = new Json.Node(Json.NodeType.OBJECT); + json.set_object(new Json.Object()); + json.get_object().set_string_member("app_name", + app_name); + json.get_object().set_string_member("asset_id", + asset_id); + json.get_object().set_string_member("build_version", + build_version); + json.get_object().set_string_member("catalog_item_id", + catalog_item_id); + json.get_object().set_string_member("label_name", + label_name); + json.get_object().set_object_member("metadata", + metadata.get_object()); + json.get_object().set_string_member("namespace", + ns); + + return json; + } + + public string to_string(bool pretty) { return Json.to_string(to_json(), + pretty); } + + public static new bool is_equal(Asset a, + Asset b) + { + if(a.asset_id == b.asset_id) + { + return true; + } + + return false; + } + } + + // public class RunnableAction: Traits.HasActions.Action + // { + // public RunnableAction(EpicGame game) + // { + // runnable = game; + // is_primary = true; + // name = "Update"; + // } + + // public new bool is_available(GameHub.Data.Compat.CompatTool? tool = null) + // { + // return true; + // } + + // public new async void invoke(GameHub.Data.Compat.CompatTool? tool = null) + // { + // yield((EpicGame)runnable).install(InstallTask.Mode.AUTO_INSTALL, true); + // } + // } + } +} diff --git a/src/data/sources/epicgames/EpicGames.vala b/src/data/sources/epicgames/EpicGames.vala new file mode 100644 index 00000000..1d82b913 --- /dev/null +++ b/src/data/sources/epicgames/EpicGames.vala @@ -0,0 +1,831 @@ +using Gee; +using Soup; +using WebKit; + +using GameHub.Data.DB; +using GameHub.Data.Runnables; +// using GameHub.Data.Tweaks; +using GameHub.Utils; + +namespace GameHub.Data.Sources.EpicGames +{ + internal bool log_chunk = false; + internal bool log_chunk_part = false; + internal bool log_chunk_data_list = false; + internal bool log_epic_games_services = false; + internal bool log_file_manifest_list = false; + internal bool log_manifest = false; + internal bool log_meta = false; + + public class EpicGames: GameSource + { + public static EpicGames instance; + + private ArrayList _games = new ArrayList(Game.is_equal); + public HashMap owned_games { get; default = new HashMap(null, null, Game.is_equal); } + private Json.Node? userdata { get; default = new Json.Node(Json.NodeType.NULL); } + private Settings.Auth.EpicGames settings; + + public override string id { get { return "epicgames"; } } + public override string name { get { return "EpicGames"; } } + public override string icon { get { return "source-epicgames-symbolic"; } } + public override ArrayList games { get { return _games; } } + public override bool enabled + { + get { return Settings.Auth.EpicGames.instance.enabled; } + set { Settings.Auth.EpicGames.instance.enabled = value; } + } + + public string? user_name + { + get + { + return_val_if_fail(userdata.get_object().has_member("displayName"), + null); + + return userdata.get_object().get_string_member("displayName"); + } + } + + internal string access_token + { + get + { + assert(userdata.get_node_type() == Json.NodeType.OBJECT); + assert(userdata.get_object().has_member("access_token")); + + return userdata.get_object().get_string_member("access_token"); + } + } + + internal string user_id + { + get + { + assert(userdata.get_node_type() == Json.NodeType.OBJECT); + assert(userdata.get_object().has_member("account_id")); + + return userdata.get_object().get_string_member("account_id"); + } + } + + private ArrayList _assets = new ArrayList(EpicGame.Asset.is_equal); + private ArrayList assets + { + get + { + if(_assets.is_empty) + { + // read from cache + var json = Parser.parse_json_file(FS.Paths.EpicGames.Cache, + "assets.json"); + + if(json.get_node_type() == Json.NodeType.ARRAY) + { + json.get_array().foreach_element( + (array, index, node) => { + var asset = new EpicGame.Asset.from_json(node); + + // debug("loaded asset: " + asset.to_string(true)); + if(!_assets.contains(asset)) + { + _assets.add(asset); + } + }); + } + } + + return _assets; + } + set + { + _assets = value; + + // save to cache + FS.mkdir(FS.Paths.EpicGames.Cache); + var json = new Json.Node(Json.NodeType.ARRAY); + json.set_array(new Json.Array()); + _assets.foreach(asset => { + json.get_array().add_object_element(asset.to_json().get_object()); + + return true; + }); + + write(FS.Paths.EpicGames.Cache, + "assets.json", + Json.to_string(json, + true).data); + } + } + + public EpicGames() + { + instance = this; + settings = Settings.Auth.EpicGames.instance; + _userdata = Parser.parse_json(settings.userdata); + + // Session we're using to access the api + new EpicGamesServices(); + } + + public override bool is_installed(bool refresh = false) + { + // Internal, this source is always installed + return true; + } + + public override async bool install() + { + // Internal, this source is always installed + return true; + } + + public override bool is_authenticated() + { + return_val_if_fail(userdata.get_node_type() == Json.NodeType.OBJECT, + false); + + if(!userdata.get_object().has_member("access_token")) return false; + + if(!userdata.get_object().has_member("expires_at")) return false; + + var now = new DateTime.now_local(); + var access_expires = new DateTime.from_iso8601(userdata.get_object().get_string_member("expires_at"), + null); + + if(access_expires.difference(now) < TimeSpan.MINUTE * 10) + { + debug("[Sources.EpicGames.is_authenticated] Access token is less than 10 minutes valid."); + + return false; + } + + return access_token != ""; + } + + public override bool can_authenticate_automatically() + { + return_val_if_fail(userdata.get_node_type() == Json.NodeType.OBJECT, + false); + + if(!userdata.get_object().has_member("refresh_token")) return false; + + if(!userdata.get_object().has_member("refresh_expires_at")) return false; + + var now = new DateTime.now_local(); + var refresh_expires = new DateTime.from_iso8601(userdata.get_object().get_string_member("refresh_expires_at"), + null); + + if(refresh_expires.difference(now) < TimeSpan.MINUTE * 10) + { + debug("[Sources.EpicGames.can_authenticate_automatically] Refresh token is less than 10 minutes valid."); + + return false; + } + + return userdata.get_object().get_string_member_with_default("refresh_token", + "") != "" && settings.authenticated; + } + + public override async bool authenticate() + { + settings.authenticated = true; + + if(is_authenticated()) return true; + + if(can_authenticate_automatically()) + { + _userdata = EpicGamesServices.instance.start_session(userdata.get_object().get_string_member("refresh_token")); + settings.userdata = Json.to_string(userdata, + false); + + return is_authenticated(); + } + + var wnd = new GameHub.UI.Windows.WebAuthWindow(this.name, + "https://www.epicgames.com/id/login?redirectUrl=https%3A%2F%2Fwww.epicgames.com%2Fid%2Fapi%2Fredirect", + "https://www.epicgames.com/id/api/redirect", + null); + + wnd.finished.connect(() => + { + wnd.webview.web_context.get_cookie_manager().get_cookies.begin( + "https://www.epicgames.com", + null, + (obj, res) => { + try + { + var webview_cookies = wnd.webview.web_context.get_cookie_manager().get_cookies.end(res); + SList cookies = new SList(); + + webview_cookies.foreach(cookie => { + cookies.append(cookie); + }); + + authenticate_with_exchange_code(authenticate_with_sid(cookies)); + } + catch (Error e) {} + + Idle.add(authenticate.callback); + }); + }); + + wnd.canceled.connect(() => Idle.add(authenticate.callback)); + + wnd.set_size_request(640, + 800); // FIXME: Doesn't work? + wnd.show_all(); + wnd.present(); + + yield; + + settings.userdata = Json.to_string(userdata, + false); + + return is_authenticated(); + } + + public async bool logout() + { + EpicGamesServices.instance.invalidate_session(); + + _userdata = new Json.Node(Json.NodeType.NULL); + settings.userdata = Json.to_string(userdata, + false); + settings.authenticated = false; + + // invalidate webkit session to allow logging in with a different account + #if WEBKIT2GTK + try + { + var webview = new WebView(); + + var cookies_file = FS.expand(FS.Paths.Cache.Cookies); + webview.web_context.get_cookie_manager().set_persistent_storage(cookies_file, + CookiePersistentStorage.TEXT); + + var website_data = yield webview.get_website_data_manager().fetch(WebsiteDataTypes.COOKIES); + foreach(var website in website_data) + { + if(website.get_name() == "epicgames.com") + { + var list = new GLib.List(); + list.append(website); + + if(yield webview.get_website_data_manager().remove(WebsiteDataTypes.COOKIES, + list)) + { + debug("[Sources.EpicGames.logout] Deleted cookies for: %s", + website.get_name()); + } + } + } + } + catch (Error e) {} + #endif + + return true; + } + + public override async ArrayList load_games(Utils.FutureResult2? game_loaded = null, + Utils.Future? cache_loaded = null) + { + if(!is_authenticated() || _games.size > 0) + { + return games; + } + + Utils.thread("EpicGamesLoading", + () => + { + _games.clear(); + + var cached = Tables.Games.get_all(this); + games_count = 0; + + if(cached.size > 0) + { + foreach(var g in cached) + { + if(g.platforms.size == 0) continue; + + if(!Settings.UI.Behavior.instance.merge_games || !Tables.Merges.is_game_merged(g)) + { + _games.add(g); + owned_games.set(g.id, + g); + + if(game_loaded != null) + { + game_loaded(g, + true); + } + } + + games_count++; + } + } + + if(cache_loaded != null) + { + cache_loaded(); + } + + get_game_and_dlc_list(); + + owned_games.foreach(tuple => + { + var game = tuple.value; + bool is_new_game = !_games.contains(game); + + if(is_new_game && (!Settings.UI.Behavior.instance.merge_games || !Tables.Merges.is_game_merged(game))) + { + _games.add(game); + + // owned_games.set(game.id, game); + if(game_loaded != null) + { + game_loaded(game, + false); + } + } + + if(is_new_game) + { + games_count++; + game.save(); + } + + return true; + }); + + Idle.add(load_games.callback); + }); + + yield; + + return games; + } + + public override ArrayList? game_dirs + { + owned get + { + ArrayList? dirs = null; + + var paths = GameHub.Settings.Paths.EpicGames.instance.game_directories; + + if(paths != null && paths.length > 0) + { + foreach(var path in paths) + { + if(path != null && path.length > 0) + { + var dir = FS.file(path); + + if(dir != null) + { + if(dirs == null) dirs = new ArrayList(); + + dirs.add(dir); + } + } + } + } + + return dirs; + } + } + + public override File? default_game_dir + { + owned get + { + var path = GameHub.Settings.Paths.EpicGames.instance.default_game_directory; + + if(path != null && path.length > 0) + { + var dir = FS.file(path); + + if(dir != null && dir.query_exists()) + { + return dir; + } + } + + var dirs = game_dirs; + + if(dirs != null && dirs.size > 0) + { + return dirs.first(); + } + + return null; + } + } + + // Legendary core replication ============================================================== + + public string authenticate_with_sid(SList cookies) + { + var session = new Session(); + session.timeout = 5; + session.max_conns = 256; + session.max_conns_per_host = 256; + + // FIXME: header setting looks ugly + // message.request_headers.append(); + // 'X-Epic-Event-Action': 'login', + // 'X-Epic-Event-Category': 'login', + // 'X-Epic-Strategy-Flags': '', + // 'X-Requested-With': 'XMLHttpRequest', + // 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' + // 'AppleWebKit/537.36 (KHTML, like Gecko) ' + // 'EpicGamesLauncher/11.0.1-14907503+++Portal+Release-Live ' + // 'UnrealEngine/4.23.0-14907503+++Portal+Release-Live ' + // 'Chrome/84.0.4147.38 Safari/537.36' + + debug("[Sources.EpicGames.LegendaryCore.with_sid] Getting xsrf"); + var message = new Message("GET", + "https://www.epicgames.com/id/api/csrf"); + message.request_headers.append("X-Epic-Event-Action", + "login"); + message.request_headers.append("X-Epic-Event-Category", + "login"); + message.request_headers.append("X-Epic-Strategy-Flags", + ""); + message.request_headers.append("X-Requested-With", + "XMLHttpRequest"); + message.request_headers.append( + "User-Agent", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + + "AppleWebKit/537.36 (KHTML, like Gecko) " + + "EpicGamesLauncher/11.0.1-14907503+++Portal+Release-Live " + + "UnrealEngine/4.23.0-14907503+++Portal+Release-Live " + + "Chrome/84.0.4147.38 Safari/537.36"); + cookies_to_request(cookies, + message); + var status = session.send_message(message); + debug("[Sources.EpicGames.LegendaryCore.with_sid] Status: %s", + status.to_string()); + assert(status == 204); + + debug("[Sources.EpicGames.LegendaryCore.with_sid] Getting exchange code"); + var cookies_from_response = cookies_from_response(message); + message = new Message("POST", + "https://www.epicgames.com/id/api/exchange/generate"); + message.request_headers.append("X-Epic-Event-Action", + "login"); + message.request_headers.append("X-Epic-Event-Category", + "login"); + message.request_headers.append("X-Epic-Strategy-Flags", + ""); + message.request_headers.append("X-Requested-With", + "XMLHttpRequest"); + message.request_headers.append( + "User-Agent", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + + "AppleWebKit/537.36 (KHTML, like Gecko) " + + "EpicGamesLauncher/11.0.1-14907503+++Portal+Release-Live " + + "UnrealEngine/4.23.0-14907503+++Portal+Release-Live " + + "Chrome/84.0.4147.38 Safari/537.36"); + cookies_to_request(cookies, + message); + cookies_to_request(cookies_from_response, + message); + + cookies_from_response.foreach(cookie => { + if(cookie.get_name() == "XSRF-TOKEN") + { + message.request_headers.append("X-XSRF-TOKEN", + cookie.get_value()); + } + }); + + status = session.send_message(message); + debug("[Sources.EpicGames.LegendaryCore.with_sid] Status: %s", + status.to_string()); + assert(status == 200); + + var json = Parser.parse_json((string) message.response_body.data); + + if(GameHub.Application.log_auth) + { + debug(Json.to_string(json, + true)); + } + + assert(json.get_node_type() == Json.NodeType.OBJECT); + assert(json.get_object().has_member("code")); + + var exchange_code = json.get_object().get_string_member("code"); + + if(GameHub.Application.log_auth) + { + debug("[Sources.EpicGames.LegendaryCore.with_sid] EGS exchange_code: %s", + exchange_code); + } + + return exchange_code; + } + + public void authenticate_with_exchange_code(string exchange_code) + { + assert(exchange_code != ""); + + _userdata = EpicGamesServices.instance.start_session(null, + exchange_code); + + return; + } + + // public bool login() + // { + // return_val_if_fail(userdata.get_node_type() == Json.NodeType.OBJECT, false); + // return_val_if_fail(userdata.get_object().has_member("expires_at"), false); + // return_val_if_fail(userdata.get_object().has_member("refresh_expires_at"), false); + + // var now = new DateTime.now_local(); + // var access_expires = new DateTime.from_iso8601(userdata.get_object().get_string_member("expires_at"), null); + // var refresh_expires = new DateTime.from_iso8601(userdata.get_object().get_string_member("refresh_expires_at"), null); + + // if(access_expires.difference(now) > TimeSpan.MINUTE * 10) + // { + // debug("[Sources.EpicGames.login] Trying to re-use existing login session…"); + // _userdata = EpicGamesServices.instance.resume_session(userdata, access_token); + + // return_val_if_fail(userdata.get_node_type() == Json.NodeType.OBJECT, false); + // return_val_if_fail(userdata.get_object().has_member("access_token"), false); + + // return userdata.get_object().get_string_member("access_token") != ""; + // } + + // if(refresh_expires.difference(now) > TimeSpan.MINUTE * 10) + // { + // return_val_if_fail(userdata.get_object().has_member("refresh_token"), false); + + // debug("[Sources.EpicGames.login] Logging in…"); + // var refresh_token = userdata.get_object().get_string_member("refresh_token"); + + // _userdata = EpicGamesServices.instance.start_session(refresh_token, null); + + // return_val_if_fail(userdata.get_node_type() == Json.NodeType.OBJECT, false); + // return_val_if_fail(userdata.get_object().has_member("access_token"), false); + + // return userdata.get_object().get_string_member("access_token") != ""; + // } + + // // TODO: invalidate + // _userdata = new Json.Node(Json.NodeType.OBJECT); + // userdata.set_object(new Json.Object()); + // settings.userdata = Json.to_string(userdata, false); + + // return false; + // } + + public ArrayList get_game_assets(bool update_assets = false, + string? platform_override = null) + { + if(platform_override != null) + { + var list = new ArrayList(); + var games_json = EpicGamesServices.instance.get_game_assets(access_token, + platform_override); + games_json.get_array().foreach_element((array, index, node) => { + assert(node.get_node_type() == Json.NodeType.OBJECT); + var asset = new EpicGame.Asset.from_egs_json(node); + list.add(asset); + }); + + return list; + } + + + if(update_assets || assets.is_empty) + { + // TODO: not logged in + var games_json = EpicGamesServices.instance.get_game_assets(access_token); + games_json.get_array().foreach_element((array, index, node) => { + assert(node.get_node_type() == Json.NodeType.OBJECT); + var asset = new EpicGame.Asset.from_egs_json(node); + + if(!assets.contains(asset)) + { + assets.add(asset); + } + else + { + assets.set(assets.index_of(asset), + asset); + } + }); + } + + return assets; + } + + public EpicGame.Asset? get_game_asset(string id, + bool update = false) + { + if(update) + { + assets = get_game_assets(update); + } + + foreach(var asset in assets) + { + if(asset.asset_id == id) + { + return asset; + } + } + + return null; + } + + public void asset_valid() {} + + public EpicGame? get_game(EpicGame game, + bool update_meta = false) + { + if(update_meta) get_game_and_dlc_list(true); + + return (EpicGame) owned_games.get(game.id); + } + + // Not needed, dlcs are always bound to games + // public void get_game_list() {} + + public void get_game_and_dlc_list(bool update_assets = true, + string? platform_override = null, + bool skip_unreal_engine = true) + { + // I don't really need the inner HashSet - a list of tuples would be enough. + // Vala should be able to handle tuples but I couldn't figure it out + var dlcs = new HashMap >(); + + var tmp_assets = get_game_assets(update_assets, + platform_override); + foreach(var asset in tmp_assets) + { + if(asset.ns == "ue" && skip_unreal_engine) continue; + + var game = (EpicGame) owned_games.get(asset.app_name); + Json.Node? metadata = null; + + if(update_assets && + (game == null + || (game != null + && game.version != asset.build_version + && platform_override != null))) + { + if(game != null + && game.version != asset.build_version + && platform_override != null) + { + debug("[LegendaryCore] Updating meta information for %s due to build version mismatch", + asset.app_name); + } + + metadata = EpicGamesServices.instance.get_game_info(asset.ns, + asset.catalog_item_id); + assert(metadata.get_node_type() == Json.NodeType.OBJECT); + + // var title = metadata.get_object().get_string_member_with_default("title", ""); + game = new EpicGame(EpicGames.instance, + asset, + metadata); + + // if(platform_override == null) game.save_metadata(); + } + + // replace asset info with the platform specific one if override is used + // FIXME: do we want this? + // if(platform_override != null) + // { + // game.version = asset.build_version; + // game.asset_info = asset; + // } + + if(game.is_dlc) + { + var json = Parser.parse_json(game.info_detailed); + return_val_if_fail(json.get_node_type() == Json.NodeType.OBJECT, + false); + + var main_id = json.get_object().get_object_member("mainGameItem").get_string_member("id"); + + // add later when we got all games + var tmp = dlcs.get(main_id); + + if(tmp == null) + { + tmp = new HashMap(); + } + + tmp.set(asset, + metadata); + dlcs.set(main_id, + tmp); + } + else + { + owned_games.set(asset.app_name, + game); + } + + // TODO: mods? + } + + // we got all games, add the dlcs to it + foreach(var game_name in dlcs) + { + if(game_name.value == null) return; + + foreach(var tuple in game_name.value) + { + var game = (EpicGame) owned_games.get(game_name.key); + game.add_dlc(tuple.key, + tuple.value); + } + } + } + + public void get_dlc_for_game() {} + public void get_installed_list() {} + public void get_installed_dlc_list() {} + public void get_installed_game() {} + // public void get_save_games() {} + // public void get_save_path() {} + // public void check_savegame_state() {} + // public void upload_save() {} + // public void download_saves() {} + public void is_offline_game() {} + public void is_noupdate_game() {} + public void is_latest() {} + public void is_game_installed() {} + public void is_dlc() {} + + internal static Manifest? load_manifest(Bytes data) + { + // FIXME: ugly json detection? + if(data[0] == '{') + { + return new Manifest.from_json(Parser.parse_json((string) data.get_data())); + } + + return new Manifest.from_bytes(data); + } + + public void get_uri_manifest() {} + public static void check_installation_conditions() {} + public void get_default_install_dir() {} + + // public Json.Node install_game(EpicGame game) + // { + // // TODO: EGL stuff? + // // if(egl_sync_enabled && !game.is_dlc) + // // { + // // if(game.egl_guid != null) + // // { + // // game.egl_guid = uuid4.replace("-", "").up(); + // // } + // // var prereq = _install_game(game); + // // egl_export(game.id); + // // return prereq; + // // else + // // { + // return _install_game(game); + // // } + // } + + // Save game metadata and info to mark it "installed" and also show the user the prerequisites + // private Json.Node _install_game(EpicGame game) + // { + // // set_installed_game(game.id, game); + // // installed_games.set(game.id, game); + // if(game.prereq_info != null) + // { + // if(game.prereq_info.get_object().has_member("installed") + // && game.prereq_info.get_object().get_boolean_member_with_default("installed", false)) + // { + // return game.prereq_info; + // } + // } + // var node = new Json.Node(Json.NodeType.OBJECT); + // node.set_object(new Json.Object()); + // return node; + // } + + // private void set_installed_game(string id, EpicGame game) + // { + // installed_games.set(id, game); + // write to file + // } + + public void uninstall_tag() {} + public void prereq_installed() {} + + // TODO: EGL stuff? + } +} diff --git a/src/data/sources/epicgames/EpicGamesServices.vala b/src/data/sources/epicgames/EpicGamesServices.vala new file mode 100644 index 00000000..f99e30da --- /dev/null +++ b/src/data/sources/epicgames/EpicGamesServices.vala @@ -0,0 +1,431 @@ +using Gee; + +using GameHub.Utils; + +using Soup; + +namespace GameHub.Data.Sources.EpicGames +{ + // https://dev.epicgames.com/docs/services/en-US/Interfaces/Auth/EASAuthentication/index.html + // https://dev.epicgames.com/docs/services/Images/Interfaces/Auth/EASAuthentication/EGSAuthFlow.webp + internal class EpicGamesServices + { + internal static EpicGamesServices instance; + + // These are coming from the Epic Launcher + private const string username = "34a02cf8f4414e29b15921876da36f9a"; + private const string password = "daafbccc737745039dffe53d94fc76cf"; + + private const string oauth_host = "account-public-service-prod03.ol.epicgames.com"; + private const string launcher_host = "launcher-public-service-prod06.ol.epicgames.com"; + private const string entitlements_host = "entitlement-public-service-prod08.ol.epicgames.com"; + private const string catalog_host = "catalog-public-service-prod06.ol.epicgames.com"; + private const string ecommerce_host = "ecommerceintegration-public-service-ecomprod02.ol.epicgames.com"; + private const string datastorage_host = "datastorage-public-service-liveegs.live.use1a.on.epicgames.com"; + private const string library_host = "library-service.live.use1a.on.epicgames.com"; + + // TODO: hardcoded for now + private string language_code = "en"; + private string country_code = "US"; + + // used with session, does not include user-agent as that's already set for the session + private HashMap auth_headers = new HashMap(); + // does not include auth header so it can be used with access token for e.g. Utils.Parser + private HashMap unauth_headers = new HashMap(); + + private Session session = new Session(); + private string user_agent = "UELauncher/11.0.1-14907503+++Portal+Release-Live Windows/10.0.19041.1.256.64bit"; + + internal EpicGamesServices() + { + instance = this; + + session.user_agent = user_agent; + unauth_headers.set("User-Agent", + user_agent); + } + + internal Json.Node start_session(string? refresh_token = null, + string? exchange_code = null) + { + var form_data = new HashTable(null, + null); + + if(refresh_token != null) + { + form_data.set("grant_type", + "refresh_token"); + form_data.set("refresh_token", + refresh_token); + form_data.set("token_type", + "eg1"); + } + else if(exchange_code != null) + { + form_data.set("grant_type", + "exchange_code"); + form_data.set("exchange_code", + exchange_code); + form_data.set("token_type", + "eg1"); + } + else + { + return_if_reached(); + } + + var message = Form.request_new_from_hash("POST", + @"https://$oauth_host/account/api/oauth/token", + form_data); + + message.request_headers.append("Authorization", + "Basic " + Base64.encode((username + ":" + password).data)); + + var status = session.send_message(message); + + assert(status < 500); + + var json = Parser.parse_json((string) message.response_body.data); + + if(GameHub.Application.log_auth) + { + debug("[start_session] " + Json.to_string(json, + true)); + } + + // invalid userdata + assert(json.get_node_type() == Json.NodeType.OBJECT); + assert(!json.get_object().has_member("error")); + + auth_headers.set("Authorization", + "Bearer %s".printf(json.get_object().get_string_member("access_token"))); + + return json; + + // { + // "access_token": "eg1~eyJraWQ…fUL5uprW9D1dvIOfLcvME", + // "expires_in": 28800, + // "expires_at": "2021-02-09T23:17:40.545Z", + // "token_type": "bearer", + // "refresh_token": "eg1~eyJraWQ…9bepwb_5ihPp4zUqypGK", + // "refresh_expires": 1987200, + // "refresh_expires_at": "2021-03-04T15:17:40.545Z", + // "account_id": "1b2a9…5b74bd2d7c", + // "client_id": "34a02c…6da36f9a", + // "internal_client": true, + // "client_service": "launcher", + // "displayName": "asdasd", + // "app": "launcher", + // "in_app_id": "1b2a9…5b74bd2d7c", + // "device_id": "3b61f…905003dc" + // } + } + + internal Json.Node resume_session(Json.Node userdata) + requires(userdata.get_node_type() == Json.NodeType.OBJECT) + { + var refreshed_json = Parser.parse_remote_json_file( + @"https://$oauth_host/account/api/oauth/verify", + "GET", + EpicGames.instance.access_token, + unauth_headers); + + if(GameHub.Application.log_auth) + { + debug("[resume_session] downloaded json " + Json.to_string(refreshed_json, + true)); + } + + assert(refreshed_json.get_node_type() == Json.NodeType.OBJECT); + assert(!refreshed_json.get_object().has_member("error")); + assert(!refreshed_json.get_object().has_member("errorMessage")); + + refreshed_json.get_object().foreach_member( + (object, name, node) => { + userdata.get_object().set_member(name, + node); + }); + + if(GameHub.Application.log_auth) + { + debug("[resume_session] updated userdata " + Json.to_string(userdata, + true)); + } + + auth_headers.set("Authorization", + "Bearer %s".printf(refreshed_json.get_object().get_string_member("access_token"))); + + return userdata; + + // { + // "token": "eg1~eyJraWQiOiB…PvnPW6aj8l6", + // "session_id": "22ed94dfc…e618bf", + // "token_type": "bearer", + // "client_id": "34a02…6f9a", + // "internal_client": true, + // "client_service": "launcher", + // "account_id": "1b2a94d…d2d7c", + // "expires_in": 28799, + // "expires_at": "2021-02-10T09:15:48.157Z", + // "auth_method": "exchange_code", + // "display_name": "asdasd", + // "app": "launcher", + // "in_app_id": "1b2a94d…d7c", + // "device_id": "3b61f…003dc" + // } + } + + internal void invalidate_session() + { + var message = new Message("DELETE", + @"https://$oauth_host/account/api/oauth/sessions/kill/$(EpicGames.instance.access_token)"); + auth_headers.foreach(header => { + message.request_headers.append(header.key, + header.value); + + return true; + }); + + session.send_message(message); + auth_headers.unset("Authorization"); + } + + internal Json.Node get_game_token() + { + uint status; + var json = Parser.parse_remote_json_file( + @"https://$oauth_host/account/api/oauth/exchange", + "GET", + EpicGames.instance.access_token, + unauth_headers, + null, + out status); + assert(status < 400); + + if(log_epic_games_services) debug("[Sources.EpicGames.EpicGamesServices.get_game_token]: \n%s", Json.to_string(json, true)); + + return json; + } + + internal Bytes get_ownership_token(string ns, + string catalog_item_id) + { + var data = new HashMap(); + var multipart = new Multipart("multipart/form-data"); + + var message = new Message( + "POST", + @"https://$ecommerce_host/ecommerceintegration/api/public/" + + @"platforms/EPIC/identities/$(EpicGames.instance.user_id)/ownershipToken"); + + data.set("nsCatalogItemId", + @"$ns:$catalog_item_id"); + auth_headers.foreach(header => { + message.request_headers.append(header.key, + header.value); + + return true; + }); + + foreach(var v in data.entries) + { + multipart.append_form_string(v.key, + v.value); + } + + multipart.to_message(message.request_headers, + message.request_body); + + var status = session.send_message(message); + assert(status < 400); + + return new Bytes(message.response_body.data); + } + + internal Json.Node get_game_assets(string platform = "Windows", + string label = "Live") + { + uint status; + var json = Parser.parse_remote_json_file( + @"https://$launcher_host/launcher/api/public/assets/$platform?label=$label", + "GET", + EpicGames.instance.access_token, + unauth_headers, + null, + out status); + + if(log_epic_games_services) debug("Game assets: %s", Json.to_string(json, true)); + + assert(status < 400); + + return json; + } + + internal Json.Node get_game_manifest(string ns, + string catalog_item_id, + string app_name, + string platform = "Windows", + string label = "Live") + { + uint status; + var json = Parser.parse_remote_json_file( + @"https://$launcher_host/launcher/api/public/assets/v2/platform" + + @"/$platform/namespace/$ns/catalogItem/$catalog_item_id/app" + + @"/$app_name/label/$label", + "GET", + EpicGames.instance.access_token, + unauth_headers, + null, + out status); + + if(log_epic_games_services) debug("[Sources.EpicGames.EpicGamesServices.get_game_manifest] json dump:\n%s", Json.to_string(json, true)); + + assert(status < 400); + + return json; + } + + internal void get_user_entitlements() {} + + internal Json.Node get_game_info(string _namespace, + string catalog_item_id) + { + uint status; + + Gee.HashMap data = new Gee.HashMap(); + data.set("id", + catalog_item_id); + data.set("includeDLCDetails", + "True"); + data.set("includeMainGameDetails", + "True"); + data.set("country", + country_code); + data.set("locale", + language_code); + + var json = Parser.parse_remote_json_file( + @"https://$catalog_host/catalog/api/shared/namespace/$_namespace/bulk/items + ?id=$catalog_item_id + &includeDLCDetails=True + &includeMainGameDetails=True + &country=$country_code + &locale=$language_code", + "GET", + EpicGames.instance.access_token, + unauth_headers, + null, + out status); + + if(log_epic_games_services) debug("[Source.EpicGames.EpicGamesServices.get_game_info] json dump: \n%s", Json.to_string(json, true)); + + assert(status < 400); + + return json.get_object().get_member(catalog_item_id); + } + + internal void get_library_items() {} + + internal Json.Node get_user_cloud_saves(string game_id = "", + bool manifests = false, + string? filenames = null) + { + var app_name = game_id; + + if(app_name.length > 0 && manifests) + { + app_name += "/manifests/"; + } + else if(app_name.length > 0) + { + app_name += "/"; + } + + Json.Node json; + uint status; + string method = "GET"; + HashMap data = null; + + if(filenames != null && filenames.length > 0) + { + method = "POST"; + data = new HashMap(); + data.set("files", + filenames); + } + + json = Parser.parse_remote_json_file( + @"https://$datastorage_host/api/v1/access/egstore/savesync/" + + @"$(EpicGames.instance.user_id)/$app_name", + method, + EpicGames.instance.access_token, + auth_headers, + data, + out status); + assert(status < 400); + assert(json.get_node_type() != Json.NodeType.NULL); + + return json; + } + + internal Json.Node create_game_cloud_saves(string game_id, + string filenames) { return get_user_cloud_saves(game_id, + false, + filenames); } + + internal void delete_game_cloud_save_files(string path) + { + var message = new Message("DELETE", + @"https://$datastorage_host/api/v1/data/egstore/$path"); + auth_headers.foreach(header => { + message.request_headers.append(header.key, + header.value); + + return true; + }); + + var status = session.send_message(message); + assert(status < 400); + } + + internal void get_cdn_manifest(string url, + out Bytes data) + { + debug("[Sources.EpicGames.get_cdn_manifest] Downloading manifest from: %s…", + url); + var message = new Message("GET", + url); + + // unauth on purpose + var status = session.send_message(message); + assert(status < 400); + data = new Bytes(message.response_body.data); + } + + /** + * Get optimized delta manifest (doesn't seem to exist for most games) + */ + internal Bytes? get_delta_manifest(string url, + string old_build_id, + string new_build_id) + { + if(old_build_id == new_build_id) return null; + + var message = new Message("GET", + @"$url/Deltas/$new_build_id/$old_build_id.delta"); + + // unauth on purpose + var status = session.send_message(message); + + return_val_if_fail(status == 200, + null); + + return new Bytes(message.request_body.data); + } + + // TODO: fetch descriptions etc + // https://github.com/SD4RK/epicstore_api/blob/master/epicstore_api/api.py#L72 + // https://store-content.ak.epicgames.com/api/de/content/products/rocket-league + // https://store-content.ak.epicgames.com/api/content/productmapping + } +} diff --git a/src/data/sources/epicgames/EpicInstaller.vala b/src/data/sources/epicgames/EpicInstaller.vala new file mode 100644 index 00000000..3520eab6 --- /dev/null +++ b/src/data/sources/epicgames/EpicInstaller.vala @@ -0,0 +1,246 @@ +using Gee; + +using GameHub.Data.Runnables; +using GameHub.Data.Runnables.Tasks.Install; +using GameHub.Utils; + +namespace GameHub.Data.Sources.EpicGames +{ + internal class Installer: Runnables.Tasks.Install.Installer + { + internal Analysis? analysis = null; + internal EpicGame game { get; private set; } + internal InstallTask? task { get; default = null; } + + internal Installer(EpicGame game, + Platform platform) + { + _game = game; + this.platform = platform; + id = game.id; + name = game.name; + full_size = game.get_installation_size(platform); // FIXME: This fetches and scans the manifest, try to get this from somewhere else e.g. the store page + can_import = true; + + if(platform != Platform.WINDOWS) + { + var list = EpicGames.instance.get_game_assets(true, + uppercase_first_character(platform.id())); + foreach(var asset in list) + { + if(asset.asset_id == id) + { + version = asset.build_version; + break; + } + } + } + else + { + version = game.latest_version; + } + } + + internal override async bool install(InstallTask task) + { + _task = task; + + debug("starting installation"); + var downloader = new EpicDownloader(); + + try + { + var downloaded_chunks = yield downloader.download(this); + + // download_task should be available here with all required information + // tasks should be in the correct order open -> write chunk -> close + var full_path = task.install_dir; + FileOutputStream? iostream = null; + + foreach(var file_task in analysis.tasks) + { + if(file_task is Analysis.FileTask) + { + // make directories + full_path = File.new_build_filename(task.install_dir.get_path(), + ((Analysis.FileTask)file_task).filename); + FS.mkdir(full_path.get_parent().get_path()); + + if(((Analysis.FileTask)file_task).empty) + { + full_path.create_readwrite(FileCreateFlags.REPLACE_DESTINATION); + continue; + } + else if(((Analysis.FileTask)file_task).fopen) + { + if(iostream != null) + { + warning("[Sources.EpicGames.Installer.install] Opening new file %s without closing previous!", + full_path.get_path()); + iostream.close(); + iostream = null; + } + + if(full_path.query_exists()) + { + iostream = yield full_path.replace_async(null, + false, + FileCreateFlags.REPLACE_DESTINATION); + } + else + { + iostream = yield full_path.create_async(FileCreateFlags.NONE); + } + + continue; + } + else if(((Analysis.FileTask)file_task).fclose) + { + if(iostream != null) + { + iostream.close(); + iostream = null; + } + else + { + warning("[Sources.EpicGames.Installer.install] Asking to close file that is not open: %s", + full_path.get_path()); + } + + continue; + } + else if(((Analysis.FileTask)file_task).frename) + { + if(iostream != null) + { + warning("[Sources.EpicGames.Installer.install] Trying to rename file without closing first!"); + iostream.close(); + iostream = null; + } + + if(((Analysis.FileTask)file_task).del) + { + FS.rm(full_path.get_path()); + } + + File.new_for_path(((Analysis.FileTask)file_task).temporary_filename).move(full_path, + FileCopyFlags.NONE); + continue; + } + else if(((Analysis.FileTask)file_task).del) + { + if(iostream != null) + { + warning("[Sources.EpicGames.Installer.install] Trying to delete file without closing first!"); + iostream.close(); + iostream = null; + } + + FS.rm(full_path.get_path()); + continue; + } + } + + assert(file_task is Analysis.ChunkTask); + assert_nonnull(iostream); + + FileInputStream? old_stream = null; + + // FIXME: this blocks the UI, do in an own thread/async + var downloaded_chunk = FS.file(FS.Paths.EpicGames.Cache + "/chunks/" + game.id + "/" + ((Analysis.ChunkTask)file_task).chunk_guid.to_string()); + + if(((Analysis.ChunkTask)file_task).chunk_file != null) + { + // reuse chunk from existing file + assert(File.new_build_filename(task.install_dir.get_path(), + ((Analysis.ChunkTask)file_task).chunk_file).query_exists()); + old_stream = File.new_build_filename(task.install_dir.get_path(), + ((Analysis.ChunkTask)file_task).chunk_file).read(); + old_stream.seek(((Analysis.ChunkTask)file_task).chunk_offset, + SeekType.SET); + var bytes = yield old_stream.read_bytes_async(((Analysis.ChunkTask)file_task).chunk_size); + // debug("chunk hash: " + Checksum.compute_for_bytes(ChecksumType.SHA1, bytes)); + yield iostream.write_bytes_async(bytes); + old_stream.close(); + old_stream = null; + } + else if(downloaded_chunk.query_exists()) + { + var chunk = new Chunk.from_byte_stream(new DataInputStream(yield downloaded_chunk.read_async())); + // debug(@"chunk data length $(chunk.data.length)"); + // debug("chunk %s hash: %s", + // ((Analysis.ChunkTask)file_task).chunk_guid.to_string(), + // Checksum.compute_for_bytes(ChecksumType.SHA1, + // chunk.data)); + var size = yield iostream.write_bytes_async(chunk.data[((Analysis.ChunkTask)file_task).chunk_offset : ((Analysis.ChunkTask)file_task).chunk_offset + ((Analysis.ChunkTask)file_task).chunk_size]); + // debug(@"written $size bytes"); + } + } + } + catch (Error e) { debug("chunk building failed: %s", e.message); } + + // TODO: clean cache path + + update_game_info(); + + task.status = new InstallTask.Status(InstallTask.State.NONE); + game.status = new Game.Status(Game.State.INSTALLED, + this.game); + + return true; + } + + // This should do three steps: Import -> verify -> repair/update + internal override async bool import(InstallTask task) + { + _task = task; + + task.status = new InstallTask.Status(InstallTask.State.INSTALLING); + game.status = new Game.Status(Game.State.INSTALLING, + this.game); + + if(!yield game.import(task.install_dir)) + { + debug("import failed"); + task.status = new InstallTask.Status(InstallTask.State.NONE); + game.status = new Game.Status(Game.State.UNINSTALLED, + this.game); + + return false; + } + + game.executable_path = game.executable.get_path(); + task.status = new InstallTask.Status(InstallTask.State.VERIFYING_INSTALLER_INTEGRITY); + game.status = new Game.Status(Game.State.VERIFYING_INSTALLER_INTEGRITY, + this.game); + + if(game.needs_verification) yield game.verify(); + + if(game.needs_repair) yield install(task); + else update_game_info(); + + task.status = new InstallTask.Status(InstallTask.State.NONE); + game.status = new Game.Status(Game.State.INSTALLED, + this.game); + + task.finish(); + + return true; + } + + private void update_game_info() + { + // update the games saved version so future manifest querys fetch the correct manifest + game.version = version; + // force update the cached manifest, the latest one should already be saved on disk here + game.manifest = EpicGames.load_manifest(game.load_manifest_from_disk()); + + game.update_metadata(); + game.install_dir = task.install_dir; + game.executable_path = FS.file(task.install_dir.get_path(), + game.manifest.meta.launch_exe).get_path(); + game.save(); + game.update_status(); + } + } +} diff --git a/src/data/sources/epicgames/EpicManifest.vala b/src/data/sources/epicgames/EpicManifest.vala new file mode 100644 index 00000000..4a2328a1 --- /dev/null +++ b/src/data/sources/epicgames/EpicManifest.vala @@ -0,0 +1,1180 @@ +using Gee; + +using GameHub.Utils; + +namespace GameHub.Data.Sources.EpicGames +{ + internal class Manifest + { + private const uint32 header_magic = 0x44BEC00C; + + private Bytes sha_hash { get; default = new Bytes(null); } + private uint8 stored_as { get; default = 0; } + private uint32 header_size { get; default = 41; } + private uint32 size_compressed { get; default = 0; } + private uint32 size_uncompressed { get; default = 0; } + private uint32 version { get; default = 18; } + + internal ChunkDataList? chunk_data_list { get; default = null; } + // TODO: CustomFields custom_fields; + // private Json.Node? custom_fields { get; default = null; } + internal FileManifestList? file_manifest_list { get; default = null; } + internal Meta? meta { get; default = null; } + + internal bool compressed { get { return (stored_as & 0x1) != 0; } } + + internal Manifest.from_bytes(Bytes bytes) + { + read_byte_header(bytes); + + var body = bytes.slice(header_size, + bytes.length); + + if(compressed) + { + if(log_manifest) debug("[Sources.EpicGames.Manifest.read_bytes] Data is compressed, uncompressing…"); + + var zlib = new ZlibDecompressor(ZlibCompressorFormat.ZLIB); + var compressed_stream = new MemoryInputStream.from_bytes(body); + var uncompressed_stream = new MemoryOutputStream.resizable(); + var converter_stream = new ConverterOutputStream(uncompressed_stream, + zlib); + + try + { + converter_stream.splice(compressed_stream, + OutputStreamSpliceFlags.NONE); + uncompressed_stream.close(); + } + catch (Error e) + { + debug("[Manifest.from_bytes]error: %s", + e.message); + } + + var data_uncompressed = uncompressed_stream.steal_as_bytes(); + assert(data_uncompressed.length == size_uncompressed); + + var decompressed_hash = Checksum.compute_for_bytes(ChecksumType.SHA1, + data_uncompressed); + + if(log_manifest) debug("[Sources.EpicGames.Manifest.read_bytes] our hash: %s", decompressed_hash); + + assert(decompressed_hash == bytes_to_hex(sha_hash)); + body = data_uncompressed; + } + + var stream = new DataInputStream(new MemoryInputStream.from_bytes(body)); + stream.set_byte_order(DataStreamByteOrder.LITTLE_ENDIAN); + + _meta = new Meta.from_byte_stream(stream); + _chunk_data_list = new ChunkDataList.from_byte_stream(stream, + meta.feature_level); + _file_manifest_list = new FileManifestList.from_byte_stream(stream); + // TODO: custom_fields = new CustomFields(stream); + + var unhandled_data = new Bytes.from_bytes(body, + (size_t) stream.tell(), + bytes.length - (size_t) stream.tell()); + + if(unhandled_data.length > 0) + { + debug(@"[Sources.EpicGames.Manifest.from_bytes] Did not read $(unhandled_data.length) remaining bytes in manifest!\n" + + "This may not be a problem."); + } + + if(log_manifest) debug(to_string()); + } + + // FIXME: json parsing is slow! + internal Manifest.from_json(Json.Node json) + { + try + { + _version = number_string_to_byte_stream(json.get_object().get_string_member_with_default("ManifestFileVersion", + "013000000000")).read_uint32(); + } + catch (Error e) { debug("error: %s", e.message); } + + _meta = new Meta.from_json(json); + _chunk_data_list = new ChunkDataList.from_json(json, + version); + _file_manifest_list = new FileManifestList.from_json(json); + _stored_as = 0; // never compress + // custom_fields = new CustomFields(); + // if(json.get_object().has_member("CustomFields")) + // { + // // TODO: custom_fields + // // custom_fields.dict = json_data.get_object().get_object_member("CustomFields"); + // // debug("unhandled: %s", Json.to_string(json_data.get_object().get_member("CustomFields"), true)); + // _custom_fields = json.get_object().get_member("CustomFields"); + // } + + // TODO: unread keys + if(log_manifest) debug(to_string()); + } + + private void read_byte_header(Bytes bytes) + { + var stream = new DataInputStream(new MemoryInputStream.from_bytes(bytes)); + stream.set_byte_order(DataStreamByteOrder.LITTLE_ENDIAN); + + try + { + var magic = stream.read_uint32(); + assert(magic == header_magic); + + _header_size = stream.read_uint32(); + _size_uncompressed = stream.read_uint32(); + _size_compressed = stream.read_uint32(); + _sha_hash = stream.read_bytes(20); + _stored_as = stream.read_byte(); + _version = stream.read_uint32(); + + assert(stream.tell() == header_size); + } + catch (Error e) + { + debug("[Manifest.read_byte_header] error: %s", + e.message); + } + } + + internal string to_string() + { + return "".printf( + version.to_string(), + stored_as.to_string(), + size_compressed.to_string(), + size_uncompressed.to_string(), + meta.to_string(), + file_manifest_list.to_string(), + chunk_data_list.to_string()); + } + + /** + * Contains metadata about the game. + * + * @param feature_level Usually same as {@link manifest_version}, but can be different e.g. if JSON manifest has been converted to binary manifest. + * @param is_file_data This was used for very old manifests that didn't use chunks at all + * @param app_id 0 for most apps, generally not used + * @param prereq_ids This is a list though I've never seen more than one entry + */ + internal class Meta + { + internal ArrayList prereq_ids { get; default = new ArrayList(); } + internal bool is_file_data { get; default = false; } + internal string app_name { get; default = ""; } + internal string build_version { get; default = ""; } + internal string launch_exe { get; default = ""; } + internal string launch_command { get; default = ""; } + internal string prereq_name { get; default = ""; } + internal string prereq_path { get; default = ""; } + internal string prereq_args { get; default = ""; } + internal uint8 data_version { get; default = 0; } + internal uint32 app_id { get; default = 0; } + internal uint32 feature_level { get; default = 18; } + internal uint32 meta_size { get; default = 0; } + + // this build id is used for something called "delta file" + internal string? _build_id = null; + internal string build_id + { + get + { + if(_build_id != null) return _build_id; + + // https://github.com/derrod/legendary/blob/master/legendary/models/manifest.py#L196 + Checksum checksum = new Checksum(ChecksumType.SHA1); + + var variant = new Variant.uint32(app_id); + variant.byteswap(); // FIXME: instead of hardcoded swapping try to set endian directly + checksum.update(variant.get_data_as_bytes().get_data(), + variant.get_data_as_bytes().get_data().length); + checksum.update(app_name.data, + -1); + checksum.update(build_version.data, + -1); + checksum.update(launch_exe.data, + -1); + checksum.update(launch_command.data, + -1); + + uint8[] hash = new uint8[ChecksumType.SHA1.get_length()]; + size_t size = ChecksumType.SHA1.get_length(); + checksum.get_digest(hash, + ref size); + + try + { + _build_id = convert(Base64.encode(hash).replace("+", + "-").replace("/", + "_").replace("=", + ""), + -1, + "ASCII", + "UTF-8"); + } + catch (Error e) + { + debug("build_id convert failed"); + } + + if(log_meta) debug(@"build_id: $build_id"); + + return _build_id; + } + } + + internal Meta.from_json(Json.Node json_data) + { + var json_obj = json_data.get_object(); + + try + { + _feature_level = number_string_to_byte_stream(json_obj.get_string_member_with_default("ManifestFileVersion", + "013000000000")).read_uint32(); + _app_id = number_string_to_byte_stream(json_obj.get_string_member_with_default("AppID", + "000000000000")).read_uint32(); + } + catch (Error e) { debug("error: %s", e.message); } + + _is_file_data = json_obj.get_boolean_member_with_default("bIsFileData", + false); + _app_name = json_obj.get_string_member_with_default("AppNameString", + ""); + _build_version = json_obj.get_string_member_with_default("BuildVersionString", + ""); + _launch_exe = json_obj.get_string_member_with_default("LaunchExeString", + ""); + _launch_command = json_obj.get_string_member_with_default("LaunchCommand", + ""); + + // TODO: we don't care about this yet + // _prereq_name = json_obj.get_string_member_with_default("PrereqName", ""); + // _prereq_path = json_obj.get_string_member_with_default("PrereqPath", ""); + // _prereq_args = json_obj.get_string_member_with_default("PrereqArgs", ""); + // if(json_obj.has_member("PrereqIds")) + // { + // json_obj.get_array_member("PrereqIds").foreach_element( + // (array, index, node) => { + // prereq_ids.add(node.get_string()); + // }); + // } + + if(log_meta) debug(to_string()); + } + + internal Meta.from_byte_stream(DataInputStream stream) + { + try + { + _meta_size = stream.read_uint32(); + _data_version = stream.read_byte(); + + // Usually same as manifest version, but can be different + // e.g. if JSON manifest has been converted to binary manifest. + _feature_level = stream.read_uint32(); + + // This was used for very old manifests that didn't use chunks at all + _is_file_data = stream.read_byte() == 1; + + // 0 for most apps, generally not used + _app_id = stream.read_uint32(); + + _app_name = read_fstring(stream); + _build_version = read_fstring(stream); + _launch_exe = read_fstring(stream); + _launch_command = read_fstring(stream); + + // This is a list though I've never seen more than one entry + var entries = stream.read_uint32(); + + for(var i = 0; i < entries; i++) + { + prereq_ids.add(read_fstring(stream)); + } + + _prereq_name = read_fstring(stream); + _prereq_path = read_fstring(stream); + _prereq_args = read_fstring(stream); + + // apparently there's a newer version that actually stores *a* build id. + if(data_version > 0) + { + _build_id = read_fstring(stream); + } + + assert(stream.tell() == meta_size); + } + catch (Error e) {} + + if(log_meta) debug(to_string()); + } + + internal string to_string() + { + return "".printf( + data_version.to_string(), + app_id.to_string(), + feature_level.to_string(), + meta_size.to_string(), + app_name, + build_version, + launch_exe, + launch_command, + build_id); + } + } + + /** + * Contains all file information. + * + * @param count How many files the game ships with. + * @param size Size all files sum up to. + */ + internal class FileManifestList + { + private HashMap? path_map = null; + + internal ArrayList elements { get; default = new ArrayList(); } + internal uint8 version { get; default = 0; } + internal uint32 count { get; default = 0; } + internal uint32 size { get; default = 0; } + + internal FileManifestList.from_byte_stream(DataInputStream stream) + { + var start = stream.tell(); + + try + { + _size = stream.read_uint32(); + _version = stream.read_byte(); + _count = stream.read_uint32(); + } + catch (Error e) {} + + for(var i = 0; i < count; i++) + { + elements.add(new FileManifest()); + } + + elements.foreach(file_manifest => { + file_manifest.filename = read_fstring(stream); + + return true; + }); + + // never seen this used in any of the manifests I checked but can't wait for something to break because of it + elements.foreach(file_manifest => { + file_manifest.symlink_target = read_fstring(stream); + + return true; + }); + + // For files this is actually the SHA1 instead of whatever it is for chunks… + elements.foreach(file_manifest => { + try + { + file_manifest.hash = stream.read_bytes(20); + } + catch (Error e) {} + + return true; + }); + + // Flags, the only one I've seen is for executables + elements.foreach(file_manifest => { + try + { + file_manifest.flags = stream.read_byte(); + } + catch (Error e) {} + + return true; + }); + + // install tags, no idea what they do, I've only seen them in the Fortnite manifest + elements.foreach(file_manifest => { + try + { + var _count = stream.read_uint32(); + + for(var i = 0; i < _count; i++) + { + file_manifest.install_tags.add(read_fstring(stream)); + } + } + catch (Error r) {} + + return true; + }); + + // Each file is made up of "Chunk Parts" that can be spread across the "chunk stream" + elements.foreach(file_manifest => { + try + { + var _count = stream.read_uint32(); + uint offset = 0; + + for(var i = 0; i < _count; i++) + { + var chunk_part = new FileManifest.ChunkPart.from_byte_stream(stream, + offset); + file_manifest.chunk_parts.add(chunk_part); + offset += chunk_part.size; + } + } + catch (Error e) {} + + return true; + }); + + // we have to calculate the actual file size ourselves + elements.foreach(file_manifest => { + uint _size = 0; + file_manifest.chunk_parts.foreach(chunk_part => { + _size += chunk_part.size; + + return true; + }); + + file_manifest.file_size = _size; + + return true; + }); + + assert(stream.tell() - start == size); + + if(log_file_manifest_list) debug(to_string()); + } + + internal FileManifestList.from_json(Json.Node json_data) + { + var json_arr = json_data.get_object().get_array_member("FileManifestList"); + _count = json_arr.get_length(); + + json_arr.foreach_element((array, index, node) => { + var file_manifest = new FileManifest(); + + var file_manifest_json = node.get_object(); + + file_manifest.filename = file_manifest_json.get_string_member_with_default("Filename", + ""); + + try + { + var hash = file_manifest_json.get_string_member("FileHash"); // 20 bytes as %03d number string + file_manifest.hash = number_string_to_byte_stream(hash).read_bytes(20); + } + catch (Error e) { debug("error: %s", e.message); } + + file_manifest.flags |= (int) file_manifest_json.get_boolean_member_with_default("bIsReadOnly", + false); + file_manifest.flags |= (int) file_manifest_json.get_boolean_member_with_default("bIsCompressed", + false) << 1; + file_manifest.flags |= (int) file_manifest_json.get_boolean_member_with_default("bIsUnixExecutable", + false) << 2; + + if(file_manifest_json.has_member("InstallTags")) + { + file_manifest_json.get_array_member("InstallTags").foreach_element((a, i, n) => { + file_manifest.install_tags.add(n.get_string()); + }); + } + + var offset = 0; + file_manifest_json.get_array_member("FileChunkParts").foreach_element((a, i, n) => + { + var chunk_part = new FileManifest.ChunkPart.from_json(n, + offset); + file_manifest.file_size += chunk_part.size; + + // TODO: not read keys + + file_manifest.chunk_parts.add(chunk_part); + }); + + // TODO: not read keys + + elements.add(file_manifest); + }); + + if(log_file_manifest_list) debug(to_string()); + } + + internal FileManifest? get_file_by_path(string path) + { + if(path_map == null) + { + path_map = new HashMap(); + + for(var i = 0; i < elements.size; i++) + { + path_map.set(elements.get(i).filename, + i); + } + } + + if(!path_map.has_key(path)) + { + debug(@"[Sources.EpicGames.FileManifestList.get_file_by_path] Invalid path: $path"); + + return null; + } + + return elements.get(path_map.get(path)); + } + + internal string to_string() + { + var result = ""; + } + + /** + * Contains information about each individual file. + * + * Each file is made up out of a number of {@link ChunkPart}s. + * + * @param chunk_parts {@link ChunkPart}s that are used in this file. + */ + internal class FileManifest + { + internal ArrayList chunk_parts { get; default = new ArrayList(); } + internal ArrayList install_tags { get; default = new ArrayList(); } + internal bool compressed { get { return (flags & 0x2) == 0x2; } } + internal bool executable { get { return (flags & 0x4) == 0x4; } } + internal bool read_only { get { return (flags & 0x1) == 0x1; } } + internal Bytes hash { get; set; default = new Bytes(null); } + internal Bytes sha_hash { get { return hash; } } + internal uchar flags { get; set; default = 0; } + internal uint32 file_size { get; set; default = 0; } + internal string filename { get; set; default = ""; } + internal string symlink_target { get; set; default = ""; } + + // Because of the weird data structure we're setting everything in the FileManifestList + internal FileManifest() {} + + internal string to_string() + { + var tag_string = ""; + var chunk_string = ""; + + foreach(var tag in install_tags) + { + tag_string = tag_string + tag; + } + + foreach(var chunk in chunk_parts) + { + chunk_string = chunk_string + chunk.to_string() + "\n"; + } + + return "".printf( + filename, + symlink_target, + bytes_to_hex(hash), + flags.to_string(), + file_size.to_string(), + tag_string, + chunk_string); + } + + /** + * ChunkPart contains simple information of Chunks used in the {@link FileManifest}. + * + * Each resulting file is build from x ChunkParts. This contains information + * where each ChunkPart belongs to in the resulting file and where to find + * it in the {@link Chunk}. + * + * @param file_offset Bytes this ChunkPart is shifted in the resulting file + * @param offset Bytes this ChunkPart is shifted in the Chunk + * @param size Size of this ChunkPart + */ + internal class ChunkPart + { + internal uint32 file_offset { get; default = 0; } + internal uint32 offset { get; default = 0; } + internal uint32 size { get; default = 0; } + internal uint32[] guid { get; default = new uint32[4]; } + + // caches for things that are "expensive" to compute + private string? _guid_str = null; + private uint32? _guid_num = null; + + internal string guid_str + { + get + { + if(_guid_str == null) + { + _guid_str = guid_to_readable_string(guid); + } + + return _guid_str; + } + } + + internal uint32 guid_num + { + get + { + if(_guid_num == null) + { + _guid_num = guid_to_number(guid); + } + + return _guid_num; + } + } + + private ChunkPart(uint32[] guid = new uint32[4], + uint32 offset = 0, + uint32 size = 0, + uint32 file_offset = 0) + { + _guid = guid; + _offset = offset; + _size = size; + _file_offset = file_offset; + } + + internal ChunkPart.from_byte_stream(DataInputStream stream, + uint32 offset) + { + var start = stream.tell(); + + try + { + var size = stream.read_uint32(); + + for(var j = 0; j < 4; j++) + { + _guid[j] = stream.read_uint32(); + } + + _offset = stream.read_uint32(); + _size = stream.read_uint32(); + _file_offset = offset; + + var diff = stream.tell() - start - size; + + if(diff > 0) + { + warning(@"[Sources.EpicGames.Manifest.ChunkPart.from_byte_stream] Did not read $diff bytes from chunk part!"); + stream.seek(diff, + SeekType.SET); + } + } + catch (Error e) + { + debug("[ChunkPart.from_byte_stream] error: %s", + e.message); + } + + if(log_chunk_part) debug(to_string()); + } + + internal ChunkPart.from_json(Json.Node json, + uint32 offset) + { + assert(json.get_node_type() == Json.NodeType.OBJECT); + + uint32 chunk_offset = 0; + uint32 chunk_size = 0; + try + { + chunk_offset = number_string_to_byte_stream(json.get_object().get_string_member("Offset")).read_uint32(); + chunk_size = number_string_to_byte_stream(json.get_object().get_string_member("Size")).read_uint32(); + } + catch (Error e) { debug("error: %s", e.message); } + + this(guid_from_hex_string(json.get_object().get_string_member("Guid")), + chunk_offset, + chunk_size, + offset + ); + + if(log_chunk_part) debug(to_string()); + } + + internal string to_string() { return @""; } + } + } + } + + /** + * Contains information about all available {@link Chunk}s. + * + * One {@link Chunk} can contain data for a file part, one file or even multiple files. + * + * @see ChunkPart + */ + internal class ChunkDataList + { + private uint8 version { get; } + private uint32 manifest_version { get; } + private uint32 size { get; } + private uint32 count { get; } + Json.Object chunk_filesize_list; // FIXME: + Json.Object chunk_hash_list; // FIXME: + Json.Object chunk_sha_list; // FIXME: + Json.Object data_group_list; // FIXME: + private HashMap guid_int_map { get; default = new HashMap(); } + private HashMap guid_str_map { get; default = new HashMap(); } + + internal ArrayList elements { get; default = new ArrayList(); } + + internal ChunkDataList.from_byte_stream(DataInputStream stream, + uint32 manifest_version = 18) + { + var start = stream.tell(); + _manifest_version = manifest_version; + + try + { + _size = stream.read_uint32(); + _version = stream.read_byte(); + _count = stream.read_uint32(); + + // the way this data is stored is rather odd, maybe there's a nicer way to write this… + for(var i = 0; i < count; i++) + { + elements.add(new ChunkInfo(manifest_version)); + } + + // guid, doesn't seem to be a standard like UUID but is fairly straightfoward, 4 bytes, 128 bit. + elements.foreach(chunk => { + for(var i = 0; i < 4; i++) + { + try + { + chunk.guid[i] = stream.read_uint32(); + } + catch (Error e) { debug("error: %s", e.message); } + } + + return true; + }); + + // hash is a 64 bit integer, no idea how it's calculated but we don't need to know that. + elements.foreach(chunk => { + try + { + chunk.hash = stream.read_uint64(); + } + catch (Error e) { debug("error: %s", e.message); } + + return true; + }); + + elements.foreach(chunk => { + try + { + chunk.sha_hash = stream.read_bytes(20); + } + catch (Error e) { debug("error: %s", e.message); } + + return true; + }); + + // group number, seems to be part of the download path + elements.foreach(chunk => { + try + { + chunk.group_num = stream.read_byte(); + } + catch (Error e) { debug("error: %s", e.message); } + + return true; + }); + + // window size is the uncompressed size + elements.foreach(chunk => { + try + { + chunk.window_size = stream.read_uint32(); + } + catch (Error e) { debug("error: %s", e.message); } + + return true; + }); + + // file size is the compressed size that will need to be downloaded + elements.foreach(chunk => { + try + { + chunk.file_size = stream.read_int64(); + } + catch (Error e) { debug("error: %s", e.message); } + + return true; + }); + + assert(stream.tell() - start == size); + } + catch (Error e) {} + + if(log_chunk_data_list) debug(to_string()); + } + + internal ChunkDataList.from_json(Json.Node json_data, + uint32 manifest_version = 13) + { + var json_obj = json_data.get_object(); + + _manifest_version = manifest_version; + _count = json_obj.get_object_member("ChunkFilesizeList").get_size(); + chunk_filesize_list = json_obj.get_object_member("ChunkFilesizeList"); + chunk_hash_list = json_obj.get_object_member("ChunkHashList"); + chunk_sha_list = json_obj.get_object_member("ChunkShaList"); + data_group_list = json_obj.get_object_member("DataGroupList"); + + chunk_filesize_list.get_members().foreach(guid => + { + var chunk_info = new ChunkInfo(manifest_version); + chunk_info.guid = guid_from_hex_string(guid); + chunk_info.window_size = 1024 * 1024; + + try + { + chunk_info.file_size = number_string_to_byte_stream(chunk_hash_list.get_string_member(guid)).read_int64(); + chunk_info.hash = number_string_to_byte_stream(chunk_hash_list.get_string_member(guid)).read_uint64(); + chunk_info.group_num = number_string_to_byte_stream(data_group_list.get_string_member(guid)).read_byte(); + + var stream = hex_string_to_byte_stream(chunk_sha_list.get_string_member(guid)); + stream.set_byte_order(DataStreamByteOrder.BIG_ENDIAN); + chunk_info.sha_hash = stream.read_bytes(20); + } + catch (Error e) { debug("error: %s", e.message); } + + elements.add(chunk_info); + }); + + if(log_chunk_data_list) debug(to_string()); + } + + /** + * Get chunk by GUID number, creates index of chunks on first call + * + * Integer GUIDs are usually faster and require less memory, use those when possible. + */ + internal ChunkInfo? get_chunk_by_number(uint32 guid) + { + if(_guid_int_map.is_empty) + { + for(var i = 0; i < _elements.size; i++) + { + _guid_int_map.set(_elements.get(i).guid_num, + i); + } + } + + if(_guid_int_map.has_key(guid)) + { + return _elements[_guid_int_map.get(guid)]; + } + + debug("[Sources.EpicManifest.ChunkDataList.get_chunk_by_number] Invalid guid!"); + assert_not_reached(); + } + + /** + * Get chunk by GUID string, creates index of chunks on first call + * + * Integer GUIDs are usually faster and require less memory, use those when possible. + */ + internal ChunkInfo? get_chunk_by_string(string guid) + { + if(_guid_str_map.is_empty) + { + for(var i = 0; i < _elements.size; i++) + { + _guid_str_map.set(_elements.get(i).guid_str, + i); + } + } + + if(_guid_str_map.has_key(guid)) + { + return _elements[_guid_str_map.get(guid)]; + } + + debug("[Sources.EpicManifest.ChunkDataList.get_chunk_by_string] Invalid guid!"); + assert_not_reached(); + } + + internal string to_string() + { + var result = ""; + } + + /** + * Contains information about one {@link Chunk}. + * + * One {@link Chunk} can contain one or multiple {@link ChunkPart}s. + * + * @param file_size is the compressed size that gets downloaded + * @param group_num is part of the download path + * @param guid doesn't seem to be a standard like UUID but is fairly straightfoward, 4 bytes, 128 bit + * @param hash is a 64 bit integer, no idea how it's calculated + * @param window_size is the uncompressed size + */ + internal class ChunkInfo + { + internal Bytes sha_hash { get; set; default = new Bytes(null); } + internal int64 file_size { get; set; default = 0; } + internal uint32[] guid { get; set; default = new uint32[4]; } + internal uint32 manifest_version { get; set; } + internal uint32 window_size { get; set; default = 0; } + internal uint64 hash { get; set; default = 0; } + + // caches for things that are "expensive" to compute + private ulong? _group_num = null; + private string? _guid_str = null; + private uint32? _guid_num = null; + + internal string guid_str + { + get + { + if(_guid_str == null) + { + _guid_str = guid_to_readable_string(guid); + } + + return _guid_str; + } + } + + internal uint32 guid_num + { + get + { + if(_guid_num == null) + { + _guid_num = guid_to_number(guid); + } + + return _guid_num; + } + } + + internal ulong group_num + { + get + { + if(_group_num == null) + { + var bytes = new ByteArray(); + + foreach(var id in guid) + { + var variant = new Variant.uint32(id); + variant.byteswap(); // FIXME: instead of hardcoded swapping try to set endian directly + bytes.append(variant.get_data_as_bytes().get_data()); + } + + _group_num = (ZLib.Utility.crc32(0, + bytes.data) & 0xffffffff) % 100; + } + + return _group_num; + } + set + { + _group_num = value; + } + } + + internal string path + { + owned get + { + return "%s/%02lu/%016llX_%s.chunk".printf( + get_chunk_dir(), + group_num, + hash, + guid_to_string(guid)); + } + } + + // Because of the weird data structure everything is set in ChunkDataList + internal ChunkInfo(uint manifest_version = 18) + { + _manifest_version = manifest_version; + } + + internal string get_chunk_dir() + { + // The lowest version I've ever seen was 12 (Unreal Tournament), but for completeness sake leave all of them in + if(manifest_version >= 15) return "ChunksV4"; + else if(manifest_version >= 6) return "ChunksV3"; + else if(manifest_version >= 3) return "ChunksV2"; + else return "Chunks"; + } + + internal string to_string() + { + return "".printf( + guid_str, + hash.to_string(), + bytes_to_hex(sha_hash), + group_num.to_string(), + window_size.to_string(), + file_size.to_string()); + } + } + } + + // TODO: private class CustomFields + // { + // int size = 0; + // int version = 0; + // int count = 0; + // // HashMap<> + // } + + /** + * Reads a string from a {@link DataInputStream}. + * + * At first it reads the length of the stream. + * When the length is negative the following string is UTF-16 - otherwise it's ASCII? + * In either case the {@link string} is returned as unescaped UTF-8 (uint8[]) + */ + // TODO: clean up and verify this mess with UTF-16 and ASCII + private static string read_fstring(DataInputStream stream) + { + string result = ""; + try + { + var length = stream.read_int32(); + // debug("[Sources.EpicGames.Manifest.read_fstring] string length: %zu", length); + + // if the length is negative the string is UTF-16 encoded, this was a pain to figure out. + if(length < 0) + { + // utf-16 chars are 2 bytes wide but the length is # of characters, not bytes + // TODO: actually make sure utf-16 characters can't be longer than 2 bytes + length *= -2; + // var tmp = stream.read_bytes(length - 2).get_data(); + // TODO: CharsetConverter oconverter = new CharsetConverter ("utf-16", "utf-8"); + // variant = new Variant.from_bytes(VariantType.STRING, stream.read_bytes(length), false); + // debug("[Sources.EpicGames.Manifest.read_fstring] string utf-16: %s", variant.get_string()); + result = convert((string) stream.read_bytes(length), + -1, + "UTF-8", + "UTF-16"); // convert to utf8 + // debug("[Sources.EpicGames.Manifest.read_fstring] string utf-8: %s", result); + // stream.seek(2, GLib.SeekType.CUR); // utf-16 strings have two byte null terminators + // TODO: seek +1 for second null char? + } + else if(length > 0) + { + // variant = new Variant.from_bytes(VariantType.STRING, stream.read_bytes(length), false); + result = (string) stream.read_bytes(length).get_data(); + // debug("[Sources.EpicGames.Manifest.read_fstring] string utf-8: %s", variant.get_string()); + // var tmp = (string) stream.read_bytes(length - 1).get_data(); + // result = convert((string) tmp, -1, "UTF-8", "ASCII"); + // result = variant.get_string(); + // stream.seek(1, GLib.SeekType.CUR); // skip string null terminator + } + else + { + result = ""; // empty string, no terminators or anything + } + } + catch (Error e) {} + + // FIXME: escape? + return result; + } + } + + /** + * Contains information about the differences between two {@link Manifest}s. + */ + internal class ManifestComparison + { + internal ArrayList added { get; default = new ArrayList(); } + internal ArrayList removed { get; default = new ArrayList(); } + internal ArrayList changed { get; default = new ArrayList(); } + internal ArrayList unchanged { get; default = new ArrayList(); } + + internal ManifestComparison(Manifest new_manifest, + Manifest? old_manifest = null) + { + if(old_manifest == null) + { + foreach(var file_manifest in new_manifest.file_manifest_list.elements) + { + added.add(file_manifest.filename); + + return; + } + } + + var old_files = new HashMap(); + + foreach(var file_manifest in old_manifest.file_manifest_list.elements) + { + old_files.set(file_manifest.filename, + file_manifest.hash); + } + + foreach(var file_manifest in new_manifest.file_manifest_list.elements) + { + Bytes? old_file_hash = null; + + if(old_files.has_key(file_manifest.filename)) + { + old_files.unset(file_manifest.filename, + out old_file_hash); + } + + if(old_file_hash != null) + { + if(file_manifest.hash == old_file_hash) + { + unchanged.add(file_manifest.filename); + } + else + { + changed.add(file_manifest.filename); + } + } + else + { + added.add(file_manifest.filename); + } + } + + // remaining old files were removed + if(old_files.size > 0) + { + removed.add_all(old_files.keys); + } + } + } +} diff --git a/src/data/sources/epicgames/EpicUtils.vala b/src/data/sources/epicgames/EpicUtils.vala new file mode 100644 index 00000000..c1f9adc0 --- /dev/null +++ b/src/data/sources/epicgames/EpicUtils.vala @@ -0,0 +1,171 @@ +using GameHub.Utils; + +namespace GameHub.Data.Sources.EpicGames +{ + /** Converts a byte sequence into a lower case hex representation + */ + private static string bytes_to_hex(Bytes bytes) { return uint8_to_hex(bytes.get_data()); } + + /** Converts a byte sequence into a lower case hex representation + */ + private static string uint8_to_hex(uint8[] bytes) + { + var builder = new StringBuilder(); + + foreach(var byte in bytes) + { + builder.append_printf("%02x", + byte); + } + + return builder.str; + } + + /** Converts a number into a byte stream from which the value can be read + * in the correct endian. + * + * The JSON manifest use a rather strange format for storing numbers. + * It's essentially %03d for each char concatenated to a string. + * …instead of just putting the fucking number in the JSON… + * Also it's still little endian. + */ + private static DataInputStream number_string_to_byte_stream(string str) + requires(str.length % 3 == 0) + { + var bytes = new ByteArray(); + + for(var i = 0; i < str.length; i += 3) + { + int segment = 0; + str.substring(i, + 3).scanf("%03hu", + out segment); + bytes.append({ (uint8) segment }); + } + + var stream = new DataInputStream(new MemoryInputStream.from_data(bytes.steal())); + stream.set_byte_order(DataStreamByteOrder.LITTLE_ENDIAN); + + return stream; + } + + /** Converts a upper case hex string into a byte stream from which the value can be read + * in the correct endian. + */ + private static DataInputStream hex_string_to_byte_stream(string str) + requires(str.length % 2 == 0) + { + var bytes = new ByteArray(); + + for(var i = 0; i < str.length; i += 2) + { + int segment = 0; + str.substring(i, + 2).scanf("%02X", + out segment); + bytes.append({ (uint8) segment }); + } + + var stream = new DataInputStream(new MemoryInputStream.from_data(bytes.steal())); + stream.set_byte_order(DataStreamByteOrder.LITTLE_ENDIAN); + + return stream; + } + + /** Reads a upper case hex string into a uint32[4]. + */ + private static uint32[] guid_from_hex_string(string str) + requires(str.length == 32) + { + uint32[] result = new uint32[4]; + var stream = hex_string_to_byte_stream(str); + stream.set_byte_order(DataStreamByteOrder.BIG_ENDIAN); + + for(var i = 0; i < 4; i++) + { + try + { + result[i] = stream.read_uint32(); + } + catch (Error e) + { + debug("error: %s", + e.message); + } + } + + return result; + } + + /** Converts a uint32 array to upper case hex string + */ + // TODO: care about little endian? + private static string guid_to_string(uint32[] guid) + { + var builder = new StringBuilder(); + + foreach(var id in guid) + { + builder.append_printf("%08X", + id); + } + + return builder.str; + } + + /** Converts a uint32 array to lower case hex string with dashes + */ + private static string guid_to_readable_string(uint32[] guid) + { + var builder = new StringBuilder(); + + foreach(var id in guid) + { + builder.append_printf("%08x-", + id); + } + + // strip last "-" + return builder.str.substring(0, + builder.str.length - 1); + } + + private static uint32 guid_to_number(uint32[] guid) { return guid[3] + (guid[2] << 32) + (guid[1] << 64) + (guid[0] << 96); } + + private static string uppercase_first_character(string str) + { + // Uppercase first character + var builder = new StringBuilder(str); + var i = 0; + unichar c; + str.get_next_char(ref i, + out c); + builder.overwrite(0, + c.to_string().up()); + + // debug("[Sources.EpicGames.Utils.uppercase] %s → %s", str, builder.str); + return builder.str; + } + + // TODO: replace with FileUtils.set_data() ? + private static void write(string path, + string name, + uint8[] bytes) + { + var file = FS.file(path, + name); + + try + { + FS.mkdir(path); + FileUtils.set_data(file.get_path(), + bytes); + } + catch (Error e) + { + warning("[Sources.EpicGames.write] Error writing `%s`: %s", + file.get_path(), + e.message); + } + } +} diff --git a/src/meson.build b/src/meson.build index 8dfa2a77..b985e2d3 100644 --- a/src/meson.build +++ b/src/meson.build @@ -45,6 +45,16 @@ gh_sources = files( 'data/sources/steam/Steam.vala', 'data/sources/steam/SteamGame.vala', + 'data/sources/epicgames/EpicAnalysis.vala', + 'data/sources/epicgames/EpicChunk.vala', + 'data/sources/epicgames/EpicDownloader.vala', + 'data/sources/epicgames/EpicGame.vala', + 'data/sources/epicgames/EpicGames.vala', + 'data/sources/epicgames/EpicGamesServices.vala', + 'data/sources/epicgames/EpicInstaller.vala', + 'data/sources/epicgames/EpicManifest.vala', + 'data/sources/epicgames/EpicUtils.vala', + 'data/sources/gog/GOG.vala', 'data/sources/gog/GOGGame.vala', @@ -117,6 +127,7 @@ gh_sources = files( 'ui/dialogs/SettingsDialog/pages/general/CompatTools.vala', 'ui/dialogs/SettingsDialog/pages/general/Tweaks.vala', 'ui/dialogs/SettingsDialog/pages/sources/Steam.vala', + 'ui/dialogs/SettingsDialog/pages/sources/EpicGames.vala', 'ui/dialogs/SettingsDialog/pages/sources/GOG.vala', 'ui/dialogs/SettingsDialog/pages/sources/Humble.vala', 'ui/dialogs/SettingsDialog/pages/sources/Itch.vala', diff --git a/src/settings/Auth.vala b/src/settings/Auth.vala index 9d31bcce..1b4e68bc 100644 --- a/src/settings/Auth.vala +++ b/src/settings/Auth.vala @@ -56,6 +56,31 @@ namespace GameHub.Settings.Auth } } + public class EpicGames: SettingsSchema + { + public bool enabled { get; set; } + public bool authenticated { get; set; } + public string userdata { get; set; } + + public EpicGames() + { + base(ProjectConfig.PROJECT_NAME + ".auth.epicgames"); + } + + private static EpicGames? _instance; + public static unowned EpicGames instance + { + get + { + if(_instance == null) + { + _instance = new EpicGames(); + } + return _instance; + } + } + } + public class GOG: SettingsSchema { public bool enabled { get; set; } diff --git a/src/settings/Paths.vala b/src/settings/Paths.vala index 40432c57..ad2ca573 100644 --- a/src/settings/Paths.vala +++ b/src/settings/Paths.vala @@ -46,6 +46,30 @@ namespace GameHub.Settings.Paths } } + public class EpicGames: GameHub.Settings.SettingsSchema + { + public string[] game_directories { get; set; } + public string default_game_directory { get; set; } + + public EpicGames() + { + base(ProjectConfig.PROJECT_NAME + ".paths.epicgames"); + } + + private static EpicGames _instance; + public static EpicGames instance + { + get + { + if(_instance == null) + { + _instance = new EpicGames(); + } + return _instance; + } + } + } + public class GOG: GameHub.Settings.SettingsSchema { public string[] game_directories { get; set; } diff --git a/src/ui/dialogs/SettingsDialog/SettingsDialog.vala b/src/ui/dialogs/SettingsDialog/SettingsDialog.vala index aaa3c87e..99e842f0 100644 --- a/src/ui/dialogs/SettingsDialog/SettingsDialog.vala +++ b/src/ui/dialogs/SettingsDialog/SettingsDialog.vala @@ -107,6 +107,7 @@ namespace GameHub.UI.Dialogs.SettingsDialog add_page("general/tweaks", new Pages.General.Tweaks(this)); add_page("sources/steam", new Pages.Sources.Steam(this)); + add_page("sources/epicgames", new Pages.Sources.EpicGames(this)); add_page("sources/gog", new Pages.Sources.GOG(this)); add_page("sources/humble", new Pages.Sources.Humble(this)); add_page("sources/itch", new Pages.Sources.Itch(this)); diff --git a/src/ui/dialogs/SettingsDialog/pages/sources/EpicGames.vala b/src/ui/dialogs/SettingsDialog/pages/sources/EpicGames.vala new file mode 100644 index 00000000..895aabd6 --- /dev/null +++ b/src/ui/dialogs/SettingsDialog/pages/sources/EpicGames.vala @@ -0,0 +1,140 @@ +using Gtk; +using GameHub.UI.Widgets; +using GameHub.UI.Widgets.Settings; + +using GameHub.Utils; + +namespace GameHub.UI.Dialogs.SettingsDialog.Pages.Sources +{ + public class EpicGames: SettingsDialogPage + { + private Settings.Auth.EpicGames epicgames_auth = Settings.Auth.EpicGames.instance; + private Settings.Paths.EpicGames epicgames_paths = Settings.Paths.EpicGames.instance; + + private Widgets.Settings.BaseSetting? account_setting; + private Button? logout_btn; + private Gtk.LinkButton? account_link; + + public EpicGames(SettingsDialog dlg) + { + Object( + dialog: dlg, + title: "EpicGames", + description: _("Disabled"), + icon_name: "source-epicgames-symbolic", + has_active_switch: true + ); + } + + construct + { + var epicgames = GameHub.Data.Sources.EpicGames.EpicGames.instance; + + epicgames_auth.bind_property("enabled", this, "active", BindingFlags.SYNC_CREATE | BindingFlags.BIDIRECTIONAL); + + if(Parser.parse_json(epicgames_auth.userdata).get_node_type() != Json.NodeType.NULL) + { + var sgrp_account = new SettingsGroup(); + + var account_actions_box = new Box(Orientation.HORIZONTAL, 12); + logout_btn = new Button.from_icon_name("system-log-out-symbolic", IconSize.BUTTON); + logout_btn.get_style_context().add_class(Gtk.STYLE_CLASS_FLAT); + logout_btn.tooltip_text = _("Logout"); + logout_btn.clicked.connect( + () => { + epicgames.logout.begin(() => update()); + request_restart(); // TODO: Requires restart until we're able to reload games from an source + }); + account_link = new LinkButton.with_label("https://epicgames.com/account/personal", _("View account")); + account_actions_box.add(logout_btn); + account_actions_box.add(account_link); + + account_setting = sgrp_account.add_setting( + new BaseSetting( + epicgames.user_name != null ? _("Authenticated as %s").printf(epicgames.user_name) : _("Authenticated"), + _("Legendary"), + account_actions_box + )); + account_setting.icon_name = "avatar-default-symbolic"; + account_setting.activatable = true; + account_setting.setting_activated.connect(() => epicgames.authenticate.begin(() => update())); + account_link.can_focus = false; + add_widget(sgrp_account); + } + + var sgrp_game_dirs = new SettingsGroupBox(_("Game directories")); + var game_dirs_list = sgrp_game_dirs.add_widget(new DirectoriesList.with_array(epicgames_paths.game_directories, epicgames_paths.default_game_directory, null, false)); + add_widget(sgrp_game_dirs); + + game_dirs_list.notify["directories"].connect( + () => { + epicgames_paths.game_directories = game_dirs_list.directories_array; + }); + + game_dirs_list.directory_selected.connect( + dir => { + epicgames_paths.default_game_directory = dir; + }); + + notify["active"].connect( + () => { + // request_restart (); + update(); + }); + + update(); + } + + private void update() + { + if(logout_btn != null) + { + logout_btn.sensitive = epicgames_auth.authenticated; + } + + // if(account_link != null) + // { + // account_link.sensitive = epicgames_auth.authenticated && epicgames.user_id.length > 0; + // } + + var epicgames = GameHub.Data.Sources.EpicGames.EpicGames.instance; + + if(!epicgames.enabled) + { + if(account_setting != null) + { + account_setting.title = _("Disabled"); + } + description = _("Disabled"); + } + else if(!epicgames.is_installed(true)) + { + if(account_setting != null) + { + account_setting.title = _("Not installed"); + } + description = _("Not installed"); + } + else if(!epicgames.is_authenticated()) + { + if(account_setting != null) + { + account_setting.title = _("Not authenticated"); + } + description = _("Not authenticated"); + } + else + { + if(this.account_setting != null) + { + account_setting.title = _("Authenticated as %s").printf(epicgames.user_name); + } + else + { + _("Authenticated"); + } + description = _("Authenticated"); + } + } + } +} diff --git a/src/ui/views/GamesView/grid/GameCard.vala b/src/ui/views/GamesView/grid/GameCard.vala index a5aaacc2..e41a6d28 100644 --- a/src/ui/views/GamesView/grid/GameCard.vala +++ b/src/ui/views/GamesView/grid/GameCard.vala @@ -464,6 +464,11 @@ namespace GameHub.UI.Views.GamesView.Grid updated_icon.visible = game is GameHub.Data.Sources.GOG.GOGGame && ((GameHub.Data.Sources.GOG.GOGGame) game).has_updates; return Source.REMOVE; }, Priority.LOW); + + Idle.add(() => { + updated_icon.visible = game is GameHub.Data.Sources.EpicGames.EpicGame && ((GameHub.Data.Sources.EpicGames.EpicGame)game).has_updates; + return Source.REMOVE; + }, Priority.LOW); } private void update_appearance() diff --git a/src/ui/views/GamesView/list/GameListRow.vala b/src/ui/views/GamesView/list/GameListRow.vala index 86212d28..5f912dee 100644 --- a/src/ui/views/GamesView/list/GameListRow.vala +++ b/src/ui/views/GamesView/list/GameListRow.vala @@ -289,6 +289,11 @@ namespace GameHub.UI.Views.GamesView.List updated_icon.visible = game is GameHub.Data.Sources.GOG.GOGGame && ((GameHub.Data.Sources.GOG.GOGGame) game).has_updates; return Source.REMOVE; }, Priority.LOW); + + Idle.add(() => { + updated_icon.visible = game is GameHub.Data.Sources.EpicGames.EpicGame && ((GameHub.Data.Sources.EpicGames.EpicGame)game).has_updates; + return Source.REMOVE; + }, Priority.LOW); } public void update_style(string[] style) diff --git a/src/utils/fs/FS.vala b/src/utils/fs/FS.vala index 01bc0a83..61f1cc68 100644 --- a/src/utils/fs/FS.vala +++ b/src/utils/fs/FS.vala @@ -84,6 +84,13 @@ namespace GameHub.Utils.FS public const string PackageInfoVDF = "appcache/packageinfo.vdf"; } + public class EpicGames + { + public const string Cache = Paths.Cache.Sources + "/epicgames"; + public const string Manifests = Paths.EpicGames.Cache + "/manifests"; + public const string Metadata = Paths.EpicGames.Cache + "/metadata"; + } + public class Humble { public const string Cache = Paths.Cache.Sources + "/humble"; From e8cac4214422b1702ffbbaaae096018b59681ea9 Mon Sep 17 00:00:00 2001 From: Lucki Date: Tue, 23 Mar 2021 16:25:37 +0100 Subject: [PATCH 02/22] Add uncrustify and Improve formatting --- src/data/sources/epicgames/EpicAnalysis.vala | 199 +- src/data/sources/epicgames/EpicChunk.vala | 43 +- .../sources/epicgames/EpicDownloader.vala | 215 +- src/data/sources/epicgames/EpicGame.vala | 411 +-- src/data/sources/epicgames/EpicGames.vala | 255 +- .../sources/epicgames/EpicGamesServices.vala | 240 +- src/data/sources/epicgames/EpicInstaller.vala | 47 +- src/data/sources/epicgames/EpicManifest.vala | 326 +- src/data/sources/epicgames/EpicUtils.vala | 50 +- uncrustify.cfg | 3128 +++++++++++++++++ 10 files changed, 3862 insertions(+), 1052 deletions(-) create mode 100644 uncrustify.cfg diff --git a/src/data/sources/epicgames/EpicAnalysis.vala b/src/data/sources/epicgames/EpicAnalysis.vala index 8f5adb6a..a98ef895 100644 --- a/src/data/sources/epicgames/EpicAnalysis.vala +++ b/src/data/sources/epicgames/EpicAnalysis.vala @@ -13,65 +13,62 @@ namespace GameHub.Data.Sources.EpicGames // FIXME: There are a lot of things related to Legendarys memory management we probably don't even need private class Analysis { - internal AnalysisResult? result { get; default = null; } - internal ArrayList tasks { get; default = new ArrayList(); } - internal LinkedList chunks_to_dl { get; default = new LinkedList(); } - internal Manifest.ChunkDataList chunk_data_list { get; default = null; } - internal string? base_url { get; default = null; } - - private File? resume_file { get; default = null; } - private HashMap hash_map { get; default = new HashMap(); } - private string? download_dir { get; default = null; } - - private Analysis(File install_dir, - string base_url, - File? resume_file) + internal AnalysisResult? result { get; default = null; } + internal ArrayList tasks { get; default = new ArrayList(); } + internal LinkedList chunks_to_dl { get; default = new LinkedList(); } + internal Manifest.ChunkDataList chunk_data_list { get; default = null; } + internal string? base_url { get; default = null; } + + private File? resume_file { get; default = null; } + private HashMap hash_map { get; default = new HashMap(); } + private string? download_dir { get; default = null; } + + private Analysis(File install_dir, string base_url, File? resume_file) { _download_dir = install_dir.get_path(); - _base_url = base_url; - _resume_file = resume_file; + _base_url = base_url; + _resume_file = resume_file; } internal Analysis.from_analysis(Runnables.Tasks.Install.InstallTask task, string base_url, Manifest new_manifest, - Manifest? old_manifest = null, - File? resume_file = null, + Manifest? old_manifest = null, + File? resume_file = null, string[]? file_install_tags = null) { this(task.install_dir, base_url, resume_file); - _result = new AnalysisResult( - new_manifest, - download_dir, - ref _hash_map, - ref _chunks_to_dl, - ref _tasks, - out _chunk_data_list, - old_manifest, - resume_file, - file_install_tags); + _result = new AnalysisResult(new_manifest, + download_dir, + ref _hash_map, + ref _chunks_to_dl, + ref _tasks, + out _chunk_data_list, + old_manifest, + resume_file, + file_install_tags); } internal class AnalysisResult { internal uint32 install_size { get; default = 0; } - internal uint32 reuse_size { get; default = 0; } - internal uint32 unchanged { get; default = 0; } + internal uint32 reuse_size { get; default = 0; } + internal uint32 unchanged { get; default = 0; } // internal uint32 unchanged_size { get; default = 0; } - internal uint64 dl_size { get; default = 0; } - - private ManifestComparison manifest_comparison { get; } - private uint32 added { get; default = 0; } - private uint32 biggest_file_size { get; default = 0; } - private uint32 biggest_chunk { get; default = 0; } - private uint32 changed { get; default = 0; } - private uint32 min_memory { get; default = 0; } - private uint32 num_chunks { get; default = 0; } - private uint32 num_chunks_cache { get; default = 0; } - private uint32 num_files { get; default = 0; } - private uint32 removed { get; default = 0; } - private uint32 uncompressed_dl_size { get; default = 0; } + internal uint64 dl_size { get; default = 0; } + + private ManifestComparison manifest_comparison { get; } + private uint32 added { get; default = 0; } + private uint32 biggest_file_size { get; default = 0; } + private uint32 biggest_chunk { get; default = 0; } + private uint32 changed { get; default = 0; } + private uint32 min_memory { get; default = 0; } + private uint32 num_chunks { get; default = 0; } + private uint32 num_chunks_cache { get; default = 0; } + private uint32 num_files { get; default = 0; } + private uint32 removed { get; default = 0; } + private uint32 uncompressed_dl_size { get; default = 0; } internal AnalysisResult(Manifest new_manifest, string download_dir, @@ -79,8 +76,8 @@ namespace GameHub.Data.Sources.EpicGames ref LinkedList chunks_to_dl, ref ArrayList tasks, out Manifest.ChunkDataList chunk_data_list, - Manifest? old_manifest = null, - File? resume_file = null, + Manifest? old_manifest = null, + File? resume_file = null, string[]? file_install_tags = null) { foreach(var element in new_manifest.file_manifest_list.elements) @@ -88,8 +85,7 @@ namespace GameHub.Data.Sources.EpicGames _install_size += element.file_size; } - _biggest_chunk = new_manifest.chunk_data_list.elements.max( - (a, b) => { + _biggest_chunk = new_manifest.chunk_data_list.elements.max((a, b) => { if(a.window_size < b.window_size) return -1; if(a.window_size == b.window_size) return 0; @@ -98,8 +94,7 @@ namespace GameHub.Data.Sources.EpicGames return 1; }).window_size; - _biggest_file_size = new_manifest.file_manifest_list.elements.max( - (a, b) => { + _biggest_file_size = new_manifest.file_manifest_list.elements.max((a, b) => { if(a.file_size < b.file_size) return -1; if(a.file_size == b.file_size) return 0; @@ -112,30 +107,26 @@ namespace GameHub.Data.Sources.EpicGames debug(@"[Sources.EpicGames.AnalysisResult] Biggest chunk size: $biggest_chunk bytes (==1 MiB? $is_1mib)"); debug("[Sources.EpicGames.AnalysisResult] Creating manifest comparison…"); - _manifest_comparison = new ManifestComparison(new_manifest, - old_manifest); + _manifest_comparison = new ManifestComparison(new_manifest, old_manifest); if(resume_file != null && resume_file.query_exists()) { info("[Sources.EpicGames.AnalysisResult] Found previously interrupted download. Download will be resumed if possible."); try { - var missing = 0; - var mismatch = 0; + var missing = 0; + var mismatch = 0; var completed_files = new ArrayList(); - - var stream = new DataInputStream(resume_file.read()); + var stream = new DataInputStream(resume_file.read()); string? line = null; while((line = stream.read_line_utf8()) != null) { - var data = line.split(":"); + var data = line.split(":"); var file_hash = data[0]; - var filename = data[1]; - - var file = FS.file(download_dir, - filename); + var filename = data[1]; + var file = FS.file(download_dir, filename); if(!file.query_exists()) { @@ -255,10 +246,10 @@ namespace GameHub.Data.Sources.EpicGames // count references to chunks for determining runtime cache size later // TODO: do we care about this? - var references = new HashMultiSet(); // FIXME: correct type to count? + var references = new HashMultiSet(); // FIXME: correct type to count? var file_manifest_list = new_manifest.file_manifest_list.elements; - file_manifest_list.sort( - (a, b) => { + + file_manifest_list.sort((a, b) => { if(a.filename.down() < b.filename.down()) return -1; if(a.filename.down() == b.filename.down()) return 0; @@ -269,8 +260,7 @@ namespace GameHub.Data.Sources.EpicGames foreach(var file_manifest in file_manifest_list) { - hash_map.set(file_manifest.filename, - bytes_to_hex(file_manifest.sha_hash)); + hash_map.set(file_manifest.filename, bytes_to_hex(file_manifest.sha_hash)); // chunks of unchanged files are not downloaded so we can skip them if(file_manifest.filename in manifest_comparison.unchanged) @@ -291,7 +281,7 @@ namespace GameHub.Data.Sources.EpicGames // determine reusable chunks and prepare lookup table for reusable ones var re_usable = new HashMap >(); - var patch = true; // FIXME: hardcoded always update + var patch = true; // FIXME: hardcoded always update if(old_manifest != null && !manifest_comparison.changed.is_empty && patch) { @@ -301,8 +291,8 @@ namespace GameHub.Data.Sources.EpicGames var old_file = old_manifest.file_manifest_list.get_file_by_path(changed_file); var new_file = new_manifest.file_manifest_list.get_file_by_path(changed_file); - var existing_chunks = new HashMap >(); - uint32 offset = 0; + var existing_chunks = new HashMap >(); + uint32 offset = 0; foreach(var chunk_part in old_file.chunk_parts) { @@ -310,17 +300,15 @@ namespace GameHub.Data.Sources.EpicGames if(!existing_chunks.has_key(chunk_part.guid_num)) { var list = new ArrayList >(); - existing_chunks.set(chunk_part.guid_num, - list); + existing_chunks.set(chunk_part.guid_num, list); } // TODO: possible to do this better? - var tmp = existing_chunks.get(chunk_part.guid_num); + var tmp = existing_chunks.get(chunk_part.guid_num); var tmp2 = new ArrayList(); tmp2.add_all_array({ offset, chunk_part.offset, chunk_part.offset + chunk_part.size }); tmp.add(tmp2); - existing_chunks.set(chunk_part.guid_num, - tmp); + existing_chunks.set(chunk_part.guid_num, tmp); offset += chunk_part.size; } @@ -344,16 +332,13 @@ namespace GameHub.Data.Sources.EpicGames if(!re_usable.has_key(changed_file)) { - re_usable.set(changed_file, - new HashMap()); + re_usable.set(changed_file, new HashMap()); } // TODO: possible to do this better? var tmp = re_usable.get(changed_file); - tmp.set(key, - thing.get(0) + (chunk_part.offset - thing.get(1))); - re_usable.set(changed_file, - tmp); + tmp.set(key, thing.get(0) + (chunk_part.offset - thing.get(1))); + re_usable.set(changed_file, tmp); _reuse_size += chunk_part.size; } } @@ -361,7 +346,7 @@ namespace GameHub.Data.Sources.EpicGames } } - uint32 last_cache_size = 0; + uint32 last_cache_size = 0; uint32 current_cache_size = 0; // set to determine whether a file is currently cached or not @@ -397,13 +382,11 @@ namespace GameHub.Data.Sources.EpicGames // existing_chunks = re_usable.get(current_file.filename); // } var chunk_tasks = new ArrayList(); - var reused = 0; + var reused = 0; foreach(var chunk_part in current_file.chunk_parts) { - var chunk_task = new ChunkTask(chunk_part.guid_num, - chunk_part.offset, - chunk_part.size); + var chunk_task = new ChunkTask(chunk_part.guid_num, chunk_part.offset, chunk_part.size); // re-use the chunk from the existing file if we can uint32[] key = { chunk_part.guid_num, chunk_part.offset, chunk_part.size }; @@ -413,7 +396,7 @@ namespace GameHub.Data.Sources.EpicGames { // debug("reusing chunk, hash should be: " + new_manifest.chunk_data_list.get_chunk_by_number(chunk_part.guid_num).to_string()); reused++; - chunk_task.chunk_file = current_file.filename; + chunk_task.chunk_file = current_file.filename; chunk_task.chunk_offset = existing_chunks.get(key); } else @@ -494,7 +477,7 @@ namespace GameHub.Data.Sources.EpicGames // https://github.com/derrod/legendary/blob/a2280edea8f7f8da9a080fd3fb2bafcabf9ee33d/legendary/downloader/manager.py#L363 // calculate actual dl and patch write size. - _dl_size = 0; + _dl_size = 0; _uncompressed_dl_size = 0; new_manifest.chunk_data_list.elements.foreach(chunk => { if(chunk.guid_num in chunks_in_dl_list) @@ -515,7 +498,7 @@ namespace GameHub.Data.Sources.EpicGames tasks.add_all(additional_deletion_tasks); _num_chunks_cache = dl_cache_guids.size; - chunk_data_list = new_manifest.chunk_data_list; + chunk_data_list = new_manifest.chunk_data_list; } } @@ -533,14 +516,14 @@ namespace GameHub.Data.Sources.EpicGames */ internal class FileTask: Task { - internal string filename { get; } - internal bool del { get; default = false; } - internal bool empty { get; default = false; } - internal bool fopen { get; default = false; } - internal bool fclose { get; default = false; } - internal bool frename { get; default = false; } + internal string filename { get; } + internal bool del { get; default = false; } + internal bool empty { get; default = false; } + internal bool fopen { get; default = false; } + internal bool fclose { get; default = false; } + internal bool frename { get; default = false; } internal string? temporary_filename { get; default = null; } - internal bool silent { get; default = false; } + internal bool silent { get; default = false; } internal bool is_reusing { @@ -558,7 +541,7 @@ namespace GameHub.Data.Sources.EpicGames internal FileTask.delete(string filename, bool silent = false) { this(filename); - _del = true; + _del = true; _silent = silent; } @@ -580,14 +563,12 @@ namespace GameHub.Data.Sources.EpicGames _fclose = true; } - internal FileTask.rename(string new_filename, - string old_filename, - bool @delete = false) + internal FileTask.rename(string new_filename, string old_filename, bool @delete = false) { this(filename); - _frename = true; + _frename = true; _temporary_filename = old_filename; - _del = @delete; + _del = @delete; } } @@ -602,19 +583,17 @@ namespace GameHub.Data.Sources.EpicGames */ internal class ChunkTask: Task { - internal uint32 chunk_guid { get; } - internal bool cleanup { get; set; default = false; } - internal uint32 chunk_offset { get; set; default = 0; } - internal uint32 chunk_size { get; default = 0; } - internal string? chunk_file { get; set; default = null; } - - internal ChunkTask(uint32 chunk_guid, - uint32 chunk_offset, - uint32 chunk_size) + internal uint32 chunk_guid { get; } + internal bool cleanup { get; set; default = false; } + internal uint32 chunk_offset { get; set; default = 0; } + internal uint32 chunk_size { get; default = 0; } + internal string? chunk_file { get; set; default = null; } + + internal ChunkTask(uint32 chunk_guid, uint32 chunk_offset, uint32 chunk_size) { - _chunk_guid = chunk_guid; + _chunk_guid = chunk_guid; _chunk_offset = chunk_offset; - _chunk_size = chunk_size; + _chunk_size = chunk_size; } } } diff --git a/src/data/sources/epicgames/EpicChunk.vala b/src/data/sources/epicgames/EpicChunk.vala index 63c2b5a5..a36794c8 100644 --- a/src/data/sources/epicgames/EpicChunk.vala +++ b/src/data/sources/epicgames/EpicChunk.vala @@ -10,19 +10,18 @@ namespace GameHub.Data.Sources.EpicGames { private const int64 header_magic = 0xB1FE3AA2; - private Bytes sha_hash { get; default = new Bytes(null); } - private uint8 stored_as { get; default = 0; } - private uint32 hash_type { get; default = 0; } // 0x1 = rolling hash, 0x2 = sha hash, 0x3 = both - private uint32 header_version { get; default = 3; } - private uint32 header_size { get; default = 0; } - private uint32 compressed_size { get; default = 0; } + private Bytes sha_hash { get; default = new Bytes(null); } + private uint8 stored_as { get; default = 0; } + private uint32 hash_type { get; default = 0; } // 0x1 = rolling hash, 0x2 = sha hash, 0x3 = both + private uint32 header_version { get; default = 3; } + private uint32 header_size { get; default = 0; } + private uint32 compressed_size { get; default = 0; } private uint32 uncompressed_size { get; default = 1024 * 1024; } - private uint64 hash { get; default = 0; } + private uint64 hash { get; default = 0; } - - private uint32[] guid { get; default = new uint32[4]; } + private uint32[] guid { get; default = new uint32[4]; } private string? _guid_str = null; - private uint32? _guid_num = null; + private uint32? _guid_num = null; private Bytes? raw_bytes = null; private Bytes? _data = null; @@ -42,21 +41,18 @@ namespace GameHub.Data.Sources.EpicGames try { var uncompressed_stream = new MemoryOutputStream.resizable(); - var zlib = new ZlibDecompressor(ZlibCompressorFormat.ZLIB); - var byte_stream = new MemoryInputStream.from_bytes(raw_bytes); + var zlib = new ZlibDecompressor(ZlibCompressorFormat.ZLIB); + var byte_stream = new MemoryInputStream.from_bytes(raw_bytes); + var converter_stream = new ConverterOutputStream(uncompressed_stream, zlib); - var converter_stream = new ConverterOutputStream(uncompressed_stream, - zlib); - converter_stream.splice(byte_stream, - OutputStreamSpliceFlags.NONE); + converter_stream.splice(byte_stream, OutputStreamSpliceFlags.NONE); uncompressed_stream.close(); _data = uncompressed_stream.steal_as_bytes(); } catch (Error e) { - debug("[EpicChunk.data] error: %s", - e.message); + debug("[EpicChunk.data] error: %s", e.message); } } else @@ -136,8 +132,8 @@ namespace GameHub.Data.Sources.EpicGames var magic = stream.read_uint32(); assert(magic == header_magic); - _header_version = stream.read_uint32(); - _header_size = stream.read_uint32(); + _header_version = stream.read_uint32(); + _header_size = stream.read_uint32(); _compressed_size = stream.read_uint32(); for(var j = 0; j < 4; j++) @@ -145,12 +141,12 @@ namespace GameHub.Data.Sources.EpicGames guid[j] = stream.read_uint32(); } - _hash = stream.read_uint64(); + _hash = stream.read_uint64(); _stored_as = stream.read_byte(); if(header_version >= 2) { - _sha_hash = stream.read_bytes(20); + _sha_hash = stream.read_bytes(20); _hash_type = stream.read_byte(); } @@ -165,8 +161,7 @@ namespace GameHub.Data.Sources.EpicGames } catch (Error e) { - debug("error: %s", - e.message); + debug("error: %s", e.message); } if(log_chunk) debug(to_string()); diff --git a/src/data/sources/epicgames/EpicDownloader.vala b/src/data/sources/epicgames/EpicDownloader.vala index 4be26240..e951b9d2 100644 --- a/src/data/sources/epicgames/EpicDownloader.vala +++ b/src/data/sources/epicgames/EpicDownloader.vala @@ -13,25 +13,22 @@ namespace GameHub.Data.Sources.EpicGames // the download manager private class EpicDownloader: GameHub.Utils.Downloader.SoupDownloader.SoupDownloader { - private ArrayQueue dl_queue; + private ArrayQueue dl_queue; private HashTable dl_info; private HashTable downloads; - private Session session; + private Session session = new Session(); - private static string[] URL_SCHEMES = { "http", "https" }; + private static string[] URL_SCHEMES = { "http", "https" }; private static string[] FILENAME_BLACKLIST = { "download" }; internal EpicDownloader() { - downloads = new HashTable(str_hash, - str_equal); - dl_info = new HashTable(str_hash, - str_equal); - dl_queue = new ArrayQueue(); - session = new Session(); - session.max_conns = 32; + downloads = new HashTable(str_hash, str_equal); + dl_info = new HashTable(str_hash, str_equal); + dl_queue = new ArrayQueue(); + session.max_conns = 32; session.max_conns_per_host = 16; - session.user_agent = "EpicGamesLauncher/11.0.1-14907503+++Portal+Release-Live Windows/10.0.19041.1.256.64bit"; + session.user_agent = "EpicGamesLauncher/11.0.1-14907503+++Portal+Release-Live Windows/10.0.19041.1.256.64bit"; download_manager().add_downloader(this); } @@ -73,38 +70,34 @@ namespace GameHub.Data.Sources.EpicGames foreach(var chunk_guid in installer.analysis.chunks_to_dl) { - var chunk = installer.analysis.chunk_data_list.get_chunk_by_number(chunk_guid); + var chunk = installer.analysis.chunk_data_list.get_chunk_by_number(chunk_guid); var remote = File.new_for_uri(installer.analysis.base_url + "/" + chunk.path); - var local = FS.file(FS.Paths.EpicGames.Cache + "/chunks/" + installer.game.id + "/" + chunk.guid_num.to_string()); + var local = FS.file(FS.Paths.EpicGames.Cache + "/chunks/" + installer.game.id + "/" + chunk.guid_num.to_string()); // debug("local path: %s", local.get_path()); FS.mkdir(local.get_parent().get_path()); - parts.add(new SoupDownload(remote, - local, - File.new_for_path(local.get_path() + "~"))); + parts.add(new SoupDownload(remote, local, File.new_for_path(local.get_path() + "~"))); } return parts; } - // TODO: a lot of small files, we should probably handle this in parralel + // TODO: a lot of small files, we should probably handle this in parallel internal new async ArrayList download(Installer installer) throws Error { - var files = new ArrayList(); - var game = installer.game; + var files = new ArrayList(); + var game = installer.game; var download = get_game_download(game); - var parts = yield fetch_parts(installer); + var parts = yield fetch_parts(installer); // installer.task.status = new InstallTask.Status(InstallTask.State.DOWNLOADING); if(game == null || download != null) return yield await_download(download); - download = new EpicDownload(game.full_id, - parts); + download = new EpicDownload(game.full_id, parts); lock (downloads) downloads.set(game.full_id, download); download_started(download); - var info = new DownloadInfo.for_runnable(game, - "Downloading…"); + var info = new DownloadInfo.for_runnable(game, "Downloading…"); info.download = download; lock (dl_info) dl_info.set(game.full_id, info); @@ -112,26 +105,21 @@ namespace GameHub.Data.Sources.EpicGames if(GameHub.Application.log_downloader) { - debug("[EpicDownloader] Installing '%s'...", - game.full_id); + debug("[EpicDownloader] Installing '%s'...", game.full_id); } - game.status = new Game.Status(Game.State.DOWNLOADING, - game, - download); + game.status = new Game.Status(Game.State.DOWNLOADING, game, download); - debug("[DownloadableInstaller.download] Starting (%d parts)", - parts.size); + debug("[DownloadableInstaller.download] Starting (%d parts)", parts.size); - var ds_id = download_manager().file_download_started.connect( - dl => { + var ds_id = download_manager().file_download_started.connect(dl => { if(dl.id != game.full_id) return; - installer.task.status = new Tasks.Install.InstallTask.Status(Tasks.Install.InstallTask.State.DOWNLOADING, - dl); + installer.task.status = new Tasks.Install.InstallTask.Status( + Tasks.Install.InstallTask.State.DOWNLOADING, + dl); // installer.download_state = new DownloadState(DownloadState.State.DOWNLOADING, dl); - dl.status_change.connect( - s => { + dl.status_change.connect(s => { installer.task.notify_property("status"); }); }); @@ -141,9 +129,7 @@ namespace GameHub.Data.Sources.EpicGames uint32 current_part = 1; foreach(var part in ((EpicDownload) download).parts) { - debug("[DownloadableInstaller.download] Part %u: `%s`", - current_part, - part.remote.get_uri()); + debug("[DownloadableInstaller.download] Part %u: `%s`", current_part, part.remote.get_uri()); FS.mkdir(part.local.get_parent().get_path()); @@ -151,10 +137,8 @@ namespace GameHub.Data.Sources.EpicGames if(parts.size > 1) { - download_description = _("Part %1$u of %2$u: %3$s").printf(current_part, - parts.size, - part.id); - download.status = new EpicDownload.Status( + download_description = _("Part %1$u of %2$u: %3$s").printf(current_part, parts.size, part.id); + download.status = new EpicDownload.Status( Download.State.DOWNLOADING, installer.full_size, // FIXME: total size is wrong for partial updates @@ -178,8 +162,7 @@ namespace GameHub.Data.Sources.EpicGames // TODO: compare hash if(GameHub.Application.log_downloader) { - debug("[SoupDownloader] '%s' is already downloaded", - uri); + debug("[SoupDownloader] '%s' is already downloaded", uri); } files.add(part.local); @@ -190,16 +173,13 @@ namespace GameHub.Data.Sources.EpicGames // var tmp = File.new_for_path(part.local.get_path() + "~"); if(part.remote.get_uri_scheme() in URL_SCHEMES) - yield download_from_http(part, - false, - false); + yield download_from_http(part, false, false); else yield download_from_filesystem(part); if(part.local_tmp.query_exists()) { - part.local_tmp.move(part.local, - FileCopyFlags.OVERWRITE); + part.local_tmp.move(part.local, FileCopyFlags.OVERWRITE); } // var file = yield download(part.remote, part.local, new Downloader.DownloadInfo.for_runnable(task.runnable, partDesc), false); @@ -266,8 +246,7 @@ namespace GameHub.Data.Sources.EpicGames catch (IOError.CANCELLED error) { download.status = new FileDownload.Status(Download.State.CANCELLED); - download_cancelled(download, - error); + download_cancelled(download, error); if(info != null) dl_ended(info); @@ -276,8 +255,7 @@ namespace GameHub.Data.Sources.EpicGames catch (Error error) { download.status = new FileDownload.Status(Download.State.FAILED); - download_failed(download, - error); + download_failed(download, error); if(info != null) dl_ended(info); @@ -396,12 +374,11 @@ namespace GameHub.Data.Sources.EpicGames private async ArrayList? await_download(EpicDownload download) throws Error { - ArrayList files = null; - Error download_error = null; + ArrayList files = null; + Error download_error = null; SourceFunc callback = await_download.callback; - var download_finished_id = download_finished.connect( - (downloader, downloaded) => { + var download_finished_id = download_finished.connect((downloader, downloaded) => { if(((SoupDownload) downloaded).id != download.id) return; files = new ArrayList(); @@ -413,15 +390,13 @@ namespace GameHub.Data.Sources.EpicGames callback (); }); - var download_cancelled_id = download_cancelled.connect( - (downloader, cancelled_download, error) => { + var download_cancelled_id = download_cancelled.connect((downloader, cancelled_download, error) => { if(((SoupDownload) cancelled_download).id != download.id) return; download_error = error; callback (); }); - var download_failed_id = download_failed.connect( - (downloader, failed_download, error) => { + var download_failed_id = download_failed.connect((downloader, failed_download, error) => { if(((SoupDownload) failed_download).id != download.id) return; download_error = error; @@ -472,10 +447,9 @@ namespace GameHub.Data.Sources.EpicGames private async void download_from_http(SoupDownload download, bool preserve_filename = true, - bool queue = true) throws Error + bool queue = true) throws Error { - var msg = new Message("GET", - download.remote.get_uri()); + var msg = new Message("GET", download.remote.get_uri()); msg.response_body.set_accumulate(false); download.session = session; @@ -493,9 +467,8 @@ namespace GameHub.Data.Sources.EpicGames } #if !PKG_FLATPAK - var address = msg.get_address(); - var connectable = new NetworkAddress(address.name, - (uint16) address.port); + var address = msg.get_address(); + var connectable = new NetworkAddress(address.name, (uint16) address.port); var network_monitor = NetworkMonitor.get_default(); if(!(yield network_monitor.can_reach_async(connectable))) @@ -506,7 +479,7 @@ namespace GameHub.Data.Sources.EpicGames FileOutputStream? local_stream = null; - int64 dl_bytes = 0; + int64 dl_bytes = 0; int64 dl_bytes_total = 0; // #if SOUP_2_60 @@ -529,8 +502,7 @@ namespace GameHub.Data.Sources.EpicGames // } // #endif - msg.got_headers.connect( - () => { + msg.got_headers.connect(() => { dl_bytes_total = msg.response_headers.get_content_length(); if(GameHub.Application.log_downloader) @@ -542,12 +514,11 @@ namespace GameHub.Data.Sources.EpicGames { if(preserve_filename) { - string filename = null; - string disposition = null; + string filename = null; + string disposition = null; HashTable dparams = null; - if(msg.response_headers.get_content_disposition(out disposition, - out dparams)) + if(msg.response_headers.get_content_disposition(out disposition, out dparams)) { if(disposition == "attachment" && dparams != null) { @@ -555,8 +526,7 @@ namespace GameHub.Data.Sources.EpicGames if(filename != null && GameHub.Application.log_downloader) { - debug(@"[SoupDownloader] Content-Disposition: filename=%s", - filename); + debug(@"[SoupDownloader] Content-Disposition: filename=%s", filename); } } } @@ -580,13 +550,11 @@ namespace GameHub.Data.Sources.EpicGames download.local.get_path()); } - var info = download.local.query_info(FileAttribute.STANDARD_SIZE, - FileQueryInfoFlags.NONE); + var info = download.local.query_info(FileAttribute.STANDARD_SIZE, FileQueryInfoFlags.NONE); if(info.get_size() == dl_bytes_total) { - session.cancel_message(msg, - Status.OK); + session.cancel_message(msg, Status.OK); return; } @@ -594,8 +562,7 @@ namespace GameHub.Data.Sources.EpicGames if(GameHub.Application.log_downloader) { - debug(@"[SoupDownloader] Downloading to '%s'", - download.local.get_path()); + debug(@"[SoupDownloader] Downloading to '%s'", download.local.get_path()); } // #if SOUP_2_60 @@ -613,9 +580,7 @@ namespace GameHub.Data.Sources.EpicGames // else // #endif { - local_stream = download.local_tmp.replace(null, - false, - FileCreateFlags.REPLACE_DESTINATION); + local_stream = download.local_tmp.replace(null, false, FileCreateFlags.REPLACE_DESTINATION); } } catch (Error e) @@ -624,45 +589,42 @@ namespace GameHub.Data.Sources.EpicGames } }); - int64 last_update = 0; + int64 last_update = 0; int64 dl_bytes_from_last_update = 0; - msg.got_chunk.connect( - (msg, chunk) => { + msg.got_chunk.connect((msg, chunk) => { if(session.would_redirect(msg) || local_stream == null) return; - dl_bytes += chunk.length; + dl_bytes += chunk.length; dl_bytes_from_last_update += chunk.length; try { local_stream.write(chunk.data); chunk.free(); - int64 now = get_real_time(); + int64 now = get_real_time(); int64 diff = now - last_update; if(diff > 1000000) { - int64 dl_speed = (int64) (((double) dl_bytes_from_last_update) / ((double) diff) * ((double) 1000000)); + int64 dl_speed = (int64) (((double) dl_bytes_from_last_update) / ((double) diff) * ((double) 1000000)); download.status = new FileDownload.Status(Download.State.DOWNLOADING, dl_bytes, dl_bytes_total, dl_speed); - last_update = now; + last_update = now; dl_bytes_from_last_update = 0; } } catch (Error e) { err = e; - session.cancel_message(msg, - Status.CANCELLED); + session.cancel_message(msg, Status.CANCELLED); } }); - session.queue_message( - msg, - (session, msg) => { + session.queue_message(msg, + (session, msg) => { download_from_http.callback (); }); @@ -683,9 +645,7 @@ namespace GameHub.Data.Sources.EpicGames } if(err == null) - err = new GLib.Error(http_error_quark(), - (int) msg.status_code, - msg.reason_phrase); + err = new GLib.Error(http_error_quark(), (int) msg.status_code, msg.reason_phrase); throw err; } @@ -716,13 +676,12 @@ namespace GameHub.Data.Sources.EpicGames public class EpicDownload: Download, PausableDownload { - public weak Session? session; - public weak Message? message; - public bool is_cancelled = false; + public weak Session? session; + public weak Message? message; + public bool is_cancelled = false; public ArrayList parts { get; } - public EpicDownload(string id, - ArrayList parts) + public EpicDownload(string id, ArrayList parts) { base(id); _parts = parts; @@ -752,29 +711,28 @@ namespace GameHub.Data.Sources.EpicGames if(session != null && message != null) { - session.cancel_message(message, - Soup.Status.CANCELLED); + session.cancel_message(message, Soup.Status.CANCELLED); } } public class Status: Download.Status { - public int64 bytes_total = -1; + public int64 bytes_total = -1; public double dl_progress = -1; - public int64 dl_speed = -1; - public int64 eta = -1; - - public Status(Download.State state = Download.State.STARTING, - int64 total = -1, - double progress = -1, - int64 speed = -1, - int64 eta = -1) + public int64 dl_speed = -1; + public int64 eta = -1; + + public Status(Download.State state = Download.State.STARTING, + int64 total = -1, + double progress = -1, + int64 speed = -1, + int64 eta = -1) { base(state); this.bytes_total = total; this.dl_progress = progress; - this.dl_speed = speed; - this.eta = eta; + this.dl_speed = speed; + this.eta = eta; } public override double progress @@ -789,24 +747,19 @@ namespace GameHub.Data.Sources.EpicGames string[] result = {}; if(eta >= 0) - result += C_("epic_dl_status", - "%s left;").printf(GameHub.Utils.seconds_to_string(eta)); + result += C_("epic_dl_status", "%s left;").printf(GameHub.Utils.seconds_to_string(eta)); if(dl_progress >= 0) - result += C_("epic_dl_status", - "%d%%").printf((int) (dl_progress * 100)); + result += C_("epic_dl_status", "%d%%").printf((int) (dl_progress * 100)); if(bytes_total >= 0) - result += C_("epic_dl_status", - "(%1$s / %2$s)").printf(format_size((int) (dl_progress * bytes_total)), - format_size(bytes_total)); + result += C_("epic_dl_status", "(%1$s / %2$s)").printf(format_size((int) (dl_progress * bytes_total)), + format_size(bytes_total)); if(dl_speed >= 0) - result += C_("epic_dl_status", - "[%s/s]").printf(format_size(dl_speed)); + result += C_("epic_dl_status", "[%s/s]").printf(format_size(dl_speed)); - return string.joinv(" ", - result); + return string.joinv(" ", result); } } } diff --git a/src/data/sources/epicgames/EpicGame.vala b/src/data/sources/epicgames/EpicGame.vala index e5b7ac13..aa3537f0 100644 --- a/src/data/sources/epicgames/EpicGame.vala +++ b/src/data/sources/epicgames/EpicGame.vala @@ -19,31 +19,30 @@ namespace GameHub.Data.Sources.EpicGames // Traits.HasExecutableFile public override string? executable_path { owned get; set; } - public override string? work_dir_path { owned get; set; } - public override string? arguments { owned get; set; } - public override string? environment { owned get; set; } + public override string? work_dir_path { owned get; set; } + public override string? arguments { owned get; set; } + public override string? environment { owned get; set; } // Traits.SupportsCompatTools - public override string? compat_tool { get; set; } + public override string? compat_tool { get; set; } public override string? compat_tool_settings { get; set; } // Traits.Game.SupportsTweaks public override TweakSet? tweaks { get; set; default = null; } private bool game_info_updating = false; - private bool game_info_updated = false; + private bool game_info_updated = false; // Legendary mapping - internal string app_name { get { return id; } } - internal string app_title { get { return name; } } - internal string? app_version { get { return version; } } + internal string app_name { get { return id; } } + internal string app_title { get { return name; } } + internal string? app_version { get { return version; } } internal ArrayList base_urls // base urls for download, only really used when cached manifest is current { owned get { var urls = new ArrayList(); - return_val_if_fail(metadata.get_object().has_member("base_urls"), - urls); + return_val_if_fail(metadata.get_object().has_member("base_urls"), urls); metadata.get_object().get_array_member("base_urls").foreach_element((array, index, node) => { urls.add(node.get_string()); @@ -60,18 +59,16 @@ namespace GameHub.Data.Sources.EpicGames return true; }); - metadata.get_object().set_array_member("base_urls", - urls); + metadata.get_object().set_array_member("base_urls", urls); write(FS.Paths.EpicGames.Metadata, get_metadata_filename(), - Json.to_string(metadata, - true).data); + Json.to_string(metadata, true).data); } } internal Asset? asset_info { get; default = null; } // public Json.Object? asset_info; // public Json.Object? metadata; - private Json.Node _metadata = new Json.Node(Json.NodeType.NULL); + private Json.Node _metadata = new Json.Node(Json.NodeType.NULL); internal Json.Node metadata // FIXME: make a class for easier access? { owned get @@ -79,8 +76,7 @@ namespace GameHub.Data.Sources.EpicGames if(_metadata.get_node_type() == Json.NodeType.NULL) { // FIXME: this will never update this way - _metadata = Parser.parse_json_file(FS.Paths.EpicGames.Metadata, - get_metadata_filename()); + _metadata = Parser.parse_json_file(FS.Paths.EpicGames.Metadata, get_metadata_filename()); if(_metadata.get_node_type() != Json.NodeType.NULL) return _metadata; @@ -101,13 +97,12 @@ namespace GameHub.Data.Sources.EpicGames { owned get { - return FS.file(Environment.get_tmp_dir(), - id + ".repair"); + return FS.file(Environment.get_tmp_dir(), id + ".repair"); } } internal string latest_version { get { return asset_info.build_version; } } - internal bool has_updates + internal bool has_updates { get { @@ -117,9 +112,9 @@ namespace GameHub.Data.Sources.EpicGames } } - internal bool needs_verification { get; set; default = false; } - internal bool needs_repair { get; default = false; } - internal bool requires_ownership_token { get; default = false; } + internal bool needs_verification { get; set; default = false; } + internal bool needs_repair { get; default = false; } + internal bool requires_ownership_token { get; default = false; } internal string launch_command { get @@ -131,21 +126,16 @@ namespace GameHub.Data.Sources.EpicGames { get { - return_val_if_fail(metadata.get_object().has_member("customAttributes"), - false); - return_val_if_fail(metadata.get_object().get_member("customAttributes").get_node_type() != Json.NodeType.OBJECT, - false); - return_val_if_fail(metadata.get_object().get_object_member("customAttributes").has_member("CanRunOffline"), - false); - return_val_if_fail(metadata.get_object().get_object_member("customAttributes").get_member("CanRunOffline").get_node_type() != Json.NodeType.OBJECT, - false); - return_val_if_fail(metadata.get_object().get_object_member("customAttributes").get_object_member("CanRunOffline").has_member("value"), - false); + return_val_if_fail(metadata.get_object().has_member("customAttributes"), false); + return_val_if_fail(metadata.get_object().get_member("customAttributes").get_node_type() != Json.NodeType.OBJECT, false); + return_val_if_fail(metadata.get_object().get_object_member("customAttributes").has_member("CanRunOffline"), false); + return_val_if_fail(metadata.get_object().get_object_member("customAttributes").get_member("CanRunOffline").get_node_type() != Json.NodeType.OBJECT, false); + return_val_if_fail(metadata.get_object().get_object_member("customAttributes").get_object_member("CanRunOffline").has_member("value"), false); return metadata.get_object().get_object_member("customAttributes").get_object_member("CanRunOffline").get_string_member("value") == "true"; // why no boolean?! } } - private int64 _install_size = 0; + private int64 _install_size = 0; internal int64 install_size { get @@ -163,7 +153,7 @@ namespace GameHub.Data.Sources.EpicGames } // internal string egl_guid; // internal Json.Node prereq_info; - private Manifest? _manifest = null; + private Manifest? _manifest = null; internal Manifest manifest { owned get @@ -199,8 +189,7 @@ namespace GameHub.Data.Sources.EpicGames if(info_detailed == null) return false; var json = Parser.parse_json(info_detailed); - return_val_if_fail(json.get_node_type() == Json.NodeType.OBJECT, - false); + return_val_if_fail(json.get_node_type() == Json.NodeType.OBJECT, false); return json.get_object().has_member("mainGameItem"); } @@ -215,12 +204,10 @@ namespace GameHub.Data.Sources.EpicGames } } - public EpicGame(EpicGames source, - Asset asset, - Json.Node? meta = null) + public EpicGame(EpicGames source, Asset asset, Json.Node? meta = null) { this.source = source; - id = asset.asset_id; + id = asset.asset_id; // this.version = asset.build_version; // Only gets permanently saved for installed games // this.info = asset.to_string(false); @@ -228,20 +215,17 @@ namespace GameHub.Data.Sources.EpicGames _asset_info = asset; load_version(); - name = metadata.get_object().get_string_member_with_default("title", - ""); + name = metadata.get_object().get_string_member_with_default("title", ""); - install_dir = null; - this.status = new Game.Status(Game.State.UNINSTALLED, - this); + install_dir = null; + this.status = new Game.Status(Game.State.UNINSTALLED, this); this.work_dir_path = ""; update_game_info.begin(); init_tweaks(); } - public EpicGame.from_db(EpicGames src, - Sqlite.Statement s) + public EpicGame.from_db(EpicGames src, Sqlite.Statement s) { source = src; @@ -268,8 +252,7 @@ namespace GameHub.Data.Sources.EpicGames if(meta_object_node.has_member("keyImages") && meta_object_node.get_member("keyImages").get_node_type() == Json.NodeType.ARRAY) { - meta_object_node.get_array_member("keyImages").foreach_element( - (array, index, node) => + meta_object_node.get_array_member("keyImages").foreach_element((array, index, node) => { if(node.get_node_type() != Json.NodeType.OBJECT) { @@ -352,7 +335,7 @@ namespace GameHub.Data.Sources.EpicGames save(); update_status(); - game_info_updated = true; + game_info_updated = true; game_info_updating = false; } @@ -387,8 +370,7 @@ namespace GameHub.Data.Sources.EpicGames } } - status = new Game.Status(state, - this); + status = new Game.Status(state, this); if(state == Game.State.INSTALLED) { @@ -441,13 +423,13 @@ namespace GameHub.Data.Sources.EpicGames } public override ExecTask prepare_exec_task(string[]? cmdline_override = null, - string[]? args_override = null) + string[]? args_override = null) { - string[] cmd = cmdline_override ?? cmdline; + string[] cmd = cmdline_override ?? cmdline; string[] full_cmd = cmd; var variables = get_variables(); - var args = args_override ?? Utils.parse_args(arguments); + var args = args_override ?? Utils.parse_args(arguments); if(args != null) { @@ -469,9 +451,7 @@ namespace GameHub.Data.Sources.EpicGames { if("$" in arg) { - arg = FS.expand(arg, - null, - variables); + arg = FS.expand(arg, null, variables); } full_cmd += arg; @@ -486,19 +466,16 @@ namespace GameHub.Data.Sources.EpicGames var task = Utils.exec(full_cmd).override_runtime(true).dir(work_dir.get_path()); - cast(game => task.tweaks(game.tweaks, - game)); + cast(game => task.tweaks(game.tweaks, game)); if(environment != null && environment.length > 0) { - var env = Parser.json_object(Parser.parse_json(environment), - {}); + var env = Parser.json_object(Parser.parse_json(environment), {}); if(env != null) { env.foreach_member((obj, name, node) => { - task.env_var(name, - node.get_string()); + task.env_var(name, node.get_string()); }); } } @@ -522,16 +499,14 @@ namespace GameHub.Data.Sources.EpicGames { // yield umount_overlays(); - FS.rm(install_dir.get_path(), - "", - "-rf"); + FS.rm(install_dir.get_path(), "", "-rf"); update_status(); } if((install_dir == null || !install_dir.query_exists()) && (executable == null || !executable.query_exists())) { install_dir = null; - executable = null; + executable = null; save(); update_status(); } @@ -545,8 +520,7 @@ namespace GameHub.Data.Sources.EpicGames foreach(var platform in platforms) { - installers.add(new Installer(this, - platform)); + installers.add(new Installer(this, platform)); } is_installable = installers.size > 0; @@ -554,17 +528,14 @@ namespace GameHub.Data.Sources.EpicGames return installers; } - public void add_dlc(Asset asset, - Json.Node? metadata = null) + public void add_dlc(Asset asset, Json.Node? metadata = null) { if(dlc == null) { dlc = new ArrayList(); } - dlc.add(new DLC(this, - asset, - metadata)); + dlc.add(new DLC(this, asset, metadata)); } public Json.Node to_json() @@ -577,24 +548,17 @@ namespace GameHub.Data.Sources.EpicGames return true; }); - json.get_object().set_string_member("app_name", - id); - json.get_object().set_string_member("app_title", - name); - json.get_object().set_string_member("app_version", - version); - json.get_object().set_object_member("asset_info", - asset_info.to_json().get_object()); - json.get_object().set_array_member("base_urls", - urls.get_array()); - json.get_object().set_object_member("metadata", - metadata.get_object()); + json.get_object().set_string_member("app_name", id); + json.get_object().set_string_member("app_title", name); + json.get_object().set_string_member("app_version", version); + json.get_object().set_object_member("asset_info", asset_info.to_json().get_object()); + json.get_object().set_array_member("base_urls", urls.get_array()); + json.get_object().set_object_member("metadata", metadata.get_object()); return json; } - public async bool import(File import_dir, - string egl_guid = "") + public async bool import(File import_dir, string egl_guid = "") { // if(!yield authenticate()) return false; @@ -605,12 +569,11 @@ namespace GameHub.Data.Sources.EpicGames // } Manifest manifest; - _needs_verification = true; + _needs_verification = true; Bytes? manifest_data = null; // check if the game is from an EGL installation, load manifest if possible - var egstore_path = Path.build_filename(import_dir.get_path(), - ".egstore"); + var egstore_path = Path.build_filename(import_dir.get_path(), ".egstore"); if(File.new_for_path(egstore_path).query_exists()) { @@ -632,29 +595,25 @@ namespace GameHub.Data.Sources.EpicGames debug("[Source.EpicGames.import_game] Checking mancpn file: %s", file_name); - var mancpn = Parser.parse_json_file(egstore_path, - file_name); + var mancpn = Parser.parse_json_file(egstore_path, file_name); if(mancpn.get_node_type() == Json.NodeType.OBJECT || mancpn.get_object().has_member("AppName")) { debug("[Source.EpicGames.import_game] Found EGL install metadata, verifying…"); - manifest_file = FS.file(egstore_path, - file_name); + manifest_file = FS.file(egstore_path, file_name); break; } } } catch (Error e) { - debug("[Source.EpicGames.import_game] No EGL data found: %s", - e.message); + debug("[Source.EpicGames.import_game] No EGL data found: %s", e.message); } } else { - manifest_file = File.new_build_filename(egstore_path, - egl_guid + ".manifest"); + manifest_file = File.new_build_filename(egstore_path, egl_guid + ".manifest"); } if(manifest_file != null && manifest_file.query_exists()) @@ -665,8 +624,7 @@ namespace GameHub.Data.Sources.EpicGames } catch (Error e) { - debug("[Source.EpicGames.import_game] Error reading manifest file: %s", - e.message); + debug("[Source.EpicGames.import_game] Error reading manifest file: %s", e.message); } } else @@ -675,10 +633,8 @@ namespace GameHub.Data.Sources.EpicGames } // If there's no in-progress installation assume the game doesn't need to be verified - var bps_path = Path.build_filename(egstore_path, - "bps"); - var pending_path = Path.build_filename(egstore_path, - "Pending"); + var bps_path = Path.build_filename(egstore_path, "bps"); + var pending_path = Path.build_filename(egstore_path, "Pending"); if(manifest_file != null && File.new_for_path(bps_path).query_exists()) { @@ -705,10 +661,8 @@ namespace GameHub.Data.Sources.EpicGames if(manifest_data == null) { - debug("[Source.EpicGames.import_game] Downloading latest manifest for: %s", - id); - get_cdn_manifest(out manifest_data, - out tmp_urls); + debug("[Source.EpicGames.import_game] Downloading latest manifest for: %s", id); + get_cdn_manifest(out manifest_data, out tmp_urls); if(base_urls.is_empty) { @@ -723,8 +677,7 @@ namespace GameHub.Data.Sources.EpicGames } manifest = EpicGames.load_manifest(manifest_data); - save_manifest(manifest_data, - manifest.meta.build_version); + save_manifest(manifest_data, manifest.meta.build_version); // uint install_size = 0; // manifest.file_manifest_list.elements.foreach(file_manifest => { // install_size += file_manifest.file_size; @@ -754,16 +707,14 @@ namespace GameHub.Data.Sources.EpicGames // var ot = metadata.get_object_member("customAttributes").get_boolean_member_with_default("OwnershipToken", false); // TODO: legendary strips all leading '/' here - executable_path = FS.file(import_dir.get_path(), - manifest.meta.launch_exe).get_path(); + executable_path = FS.file(import_dir.get_path(), manifest.meta.launch_exe).get_path(); // check if most files at least exist or if user might have specified the wrong directory var total_files = manifest.file_manifest_list.elements.size; int found_files = 0; manifest.file_manifest_list.elements.foreach(file_manifest => { - var file = FS.file(import_dir.get_path(), - file_manifest.filename); + var file = FS.file(import_dir.get_path(), file_manifest.filename); if(file.query_exists()) { @@ -771,8 +722,7 @@ namespace GameHub.Data.Sources.EpicGames } else { - warning("[Source.EpicGames.import] File could not be found at: %s", - file.get_path()); + warning("[Source.EpicGames.import] File could not be found at: %s", file.get_path()); } return true; @@ -782,8 +732,7 @@ namespace GameHub.Data.Sources.EpicGames if(!exe.query_exists()) { - warning("[Source.EpicGames.import] Game executable could not be found at: %s", - exe.get_path()); + warning("[Source.EpicGames.import] Game executable could not be found at: %s", exe.get_path()); // executable_path = null; return false; @@ -814,8 +763,7 @@ namespace GameHub.Data.Sources.EpicGames "verification will not be required."); } - GLib.info("[Source.EpicGames.import] Game has been imported: %s", - id); + GLib.info("[Source.EpicGames.import] Game has been imported: %s", id); return true; @@ -838,29 +786,24 @@ namespace GameHub.Data.Sources.EpicGames internal async void verify() { var manifest_data = get_installed_manifest(); // FIXME: cdn_manifest? - var manifest = EpicGames.load_manifest(manifest_data); + var manifest = EpicGames.load_manifest(manifest_data); var files = manifest.file_manifest_list.elements; - files.sort( - (a, b) => { - return strcmp(a.filename, - b.filename); + files.sort((a, b) => { + return strcmp(a.filename, b.filename); }); // build list of hashes var file_list = new HashMap(); files.foreach(file => { - file_list.set(file.filename, - file.sha_hash); + file_list.set(file.filename, file.sha_hash); return true; }); debug(@"[Sources.EpicGames.verify_game] Verifying \"$(id)\" version \"$(latest_version)\""); var repair_file = new ArrayList(); - - var result = yield validate_files(install_dir.get_path(), - file_list); + var result = yield validate_files(install_dir.get_path(), file_list); result.matching.foreach(match => { repair_file.add(match); @@ -877,9 +820,8 @@ namespace GameHub.Data.Sources.EpicGames // always write repair file try { - var file = FS.file(Environment.get_tmp_dir(), - id + ".repair"); - var io_stream = file.create_readwrite(FileCreateFlags.REPLACE_DESTINATION); + var file = FS.file(Environment.get_tmp_dir(), id + ".repair"); + var io_stream = file.create_readwrite(FileCreateFlags.REPLACE_DESTINATION); var output_stream = new DataOutputStream(io_stream.output_stream); foreach(var match in repair_file) { @@ -928,15 +870,11 @@ namespace GameHub.Data.Sources.EpicGames if(requires_ownership_token) { debug("[Sources.EpicGames.get_launch_parameters] getting ownership token…"); - var ownership_token = EpicGamesServices.instance.get_ownership_token( - asset_info.ns, - asset_info.catalog_item_id); + var ownership_token = EpicGamesServices.instance.get_ownership_token(asset_info.ns, + asset_info.catalog_item_id); // TODO: write to tmp path? - write(FS.Paths.EpicGames.Cache, - @"$(asset_info.ns)$(asset_info.catalog_item_id).ovt", - ownership_token.get_data()); - parameters += "-epicovt=%s".printf(FS.file(FS.Paths.EpicGames.Cache, - @"$(asset_info.ns)$(asset_info.catalog_item_id).ovt").get_path()); + write(FS.Paths.EpicGames.Cache, @"$(asset_info.ns)$(asset_info.catalog_item_id).ovt", ownership_token.get_data()); + parameters += "-epicovt=%s".printf(FS.file(FS.Paths.EpicGames.Cache, @"$(asset_info.ns)$(asset_info.catalog_item_id).ovt").get_path()); } // TODO: language @@ -955,9 +893,7 @@ namespace GameHub.Data.Sources.EpicGames if(platform != Platform.WINDOWS) { Bytes data; - get_cdn_manifest(out data, - null, - uppercase_first_character(platform.id())); + get_cdn_manifest(out data, null, uppercase_first_character(platform.id())); var manifest = EpicGames.load_manifest(data); int64 size = 0; @@ -976,8 +912,8 @@ namespace GameHub.Data.Sources.EpicGames private class ValidationResult { public ArrayList matching { get; set; default = new ArrayList(); } - public ArrayList missing { get; set; default = new ArrayList(); } - public ArrayList failed { get; set; default = new ArrayList(); } + public ArrayList missing { get; set; default = new ArrayList(); } + public ArrayList failed { get; set; default = new ArrayList(); } } private static async ValidationResult validate_files(string path, @@ -993,8 +929,7 @@ namespace GameHub.Data.Sources.EpicGames var file_path = entry.key; var file_hash = entry.value; - var full_path = FS.file(path, - file_path); + var full_path = FS.file(path, file_path); if(!full_path.query_exists()) { @@ -1003,24 +938,16 @@ namespace GameHub.Data.Sources.EpicGames } // debug("[Sources.EpicGames.validate_game_files] " + full_path.get_path()); - var real_hash = yield compute_file_checksum(full_path, - hash_type); + var real_hash = yield compute_file_checksum(full_path, hash_type); if(real_hash != null && real_hash != bytes_to_hex(file_hash)) { - debug("failed hash check: %s, %s != %s", - file_path, - bytes_to_hex(file_hash), - real_hash); - result.failed.add(string.join(":", - real_hash, - file_path)); + debug("failed hash check: %s, %s != %s", file_path, bytes_to_hex(file_hash), real_hash); + result.failed.add(string.join(":", real_hash, file_path)); } else if(real_hash != null) { - result.matching.add(string.join(":", - real_hash, - file_path)); + result.matching.add(string.join(":", real_hash, file_path)); } else { @@ -1036,26 +963,24 @@ namespace GameHub.Data.Sources.EpicGames out ArrayList? base_urls, string platform_override = "") { - var platform = platform_override == "" ? "Windows" : platform_override; - var manifest_api_result = EpicGamesServices.instance.get_game_manifest( - asset_info.ns, - asset_info.catalog_item_id, - id, - platform); + var platform = platform_override == "" ? "Windows" : platform_override; + var manifest_api_result = EpicGamesServices.instance.get_game_manifest(asset_info.ns, + asset_info.catalog_item_id, + id, + platform); // never seen this outside the launcher itself, but if it happens: PANIC! assert(manifest_api_result.get_object().has_member("elements")); var elements_array = manifest_api_result.get_object().get_array_member("elements"); assert(elements_array.get_length() <= 1); - base_urls = new ArrayList(); + base_urls = new ArrayList(); manifest_urls = new ArrayList(); var tmp1 = new ArrayList(); var tmp2 = new ArrayList(); elements_array.get_object_element(0).get_array_member("manifests").foreach_element((array, index, node) => { - var uri = node.get_object().get_string_member("uri"); - var base_url = uri.substring(0, - uri.last_index_of("/")); + var uri = node.get_object().get_string_member("uri"); + var base_url = uri.substring(0, uri.last_index_of("/")); if(!tmp1.contains(base_url)) { @@ -1065,9 +990,9 @@ namespace GameHub.Data.Sources.EpicGames if(node.get_object().has_member("queryParams")) { var parameters_array = node.get_object().get_array_member("queryParams"); - string parameter = ""; + string parameter = ""; parameters_array.foreach_element((a, i, n) => { - var name = n.get_object().get_string_member("name"); + var name = n.get_object().get_string_member("name"); var value = n.get_object().get_string_member("value"); if(i == 0) @@ -1093,25 +1018,18 @@ namespace GameHub.Data.Sources.EpicGames } private void get_cdn_manifest(out Bytes data, - out ArrayList? base_urls = null, + out ArrayList? base_urls = null, string platform_override = "") { ArrayList manifest_urls; - get_cdn_urls(out manifest_urls, - out base_urls, - platform_override); - - EpicGamesServices.instance.get_cdn_manifest(manifest_urls[0], - out data); + get_cdn_urls(out manifest_urls, out base_urls, platform_override); + EpicGamesServices.instance.get_cdn_manifest(manifest_urls[0], out data); } - private void save_manifest(Bytes bytes, - string version = this.version) + private void save_manifest(Bytes bytes, string version = this.version) { var name = get_manifest_filename(version); - write(FS.Paths.EpicGames.Manifests, - name, - bytes.get_data()); + write(FS.Paths.EpicGames.Manifests, name, bytes.get_data()); } private Bytes get_installed_manifest() { return load_manifest_from_disk(); } @@ -1121,17 +1039,12 @@ namespace GameHub.Data.Sources.EpicGames uint8[] data; try { - debug("Loading cached manifest: %s", - FS.file(FS.Paths.EpicGames.Manifests, - get_manifest_filename()).get_path()); - FileUtils.get_data(FS.file(FS.Paths.EpicGames.Manifests, - get_manifest_filename()).get_path(), - out data); + debug("Loading cached manifest: %s", FS.file(FS.Paths.EpicGames.Manifests, get_manifest_filename()).get_path()); + FileUtils.get_data(FS.file(FS.Paths.EpicGames.Manifests, get_manifest_filename()).get_path(), out data); } catch (FileError e) { - debug("error: %s", - e.message); + debug("error: %s", e.message); return null; } @@ -1171,7 +1084,7 @@ namespace GameHub.Data.Sources.EpicGames internal Analysis prepare_download(Runnables.Tasks.Install.InstallTask task) { ArrayList tmp_urls; - Bytes new_bytes; + Bytes new_bytes; Manifest? old_manifest = null; var tmp2_urls = base_urls; // copy list for manipulation @@ -1186,8 +1099,7 @@ namespace GameHub.Data.Sources.EpicGames old_manifest = EpicGames.load_manifest(old_bytes); } - get_cdn_manifest(out new_bytes, - out tmp_urls); + get_cdn_manifest(out new_bytes, out tmp_urls); tmp_urls.foreach(url => { if(!tmp2_urls.contains(url)) @@ -1202,8 +1114,7 @@ namespace GameHub.Data.Sources.EpicGames // save_metadata(); // save base urls to game metadata var new_manifest = EpicGames.load_manifest(new_bytes); - save_manifest(new_bytes, - new_manifest.meta.build_version); + save_manifest(new_bytes, new_manifest.meta.build_version); // check if we should use a delta manifest or not Manifest delta_manifest; @@ -1211,8 +1122,7 @@ namespace GameHub.Data.Sources.EpicGames if(old_manifest != null && new_manifest != null) { var delta_manifest_data = EpicGamesServices.instance.get_delta_manifest( - base_urls[Random.int_range(0, - base_urls.size - 1)], + base_urls[Random.int_range(0, base_urls.size - 1)], old_manifest.meta.build_id, new_manifest.meta.build_id); @@ -1242,18 +1152,15 @@ namespace GameHub.Data.Sources.EpicGames // new_manifest = old_manifest; // old_manifest = null; - resume_file = FS.file(Environment.get_tmp_dir(), - id + ".repair"); - force = false; + resume_file = FS.file(Environment.get_tmp_dir(), id + ".repair"); + force = false; } else if(!force) { - resume_file = FS.file(Environment.get_tmp_dir(), - id + ".resume"); + resume_file = FS.file(Environment.get_tmp_dir(), id + ".resume"); } - var base_url = base_urls[Random.int_range(0, - base_urls.size - 1)]; + var base_url = base_urls[Random.int_range(0, base_urls.size - 1)]; debug("[Sources.EpicGames.prepare_download] Using base_url: %s", base_url); @@ -1278,14 +1185,12 @@ namespace GameHub.Data.Sources.EpicGames internal void update_metadata() { var tmp_urls = base_urls; // save temporarily from old metadata - _metadata = EpicGamesServices.instance.get_game_info(asset_info.ns, - asset_info.catalog_item_id); + _metadata = EpicGamesServices.instance.get_game_info(asset_info.ns, asset_info.catalog_item_id); base_urls = tmp_urls; // paste them back into new metadata // FIXME: Setting base_urls also saves write(FS.Paths.EpicGames.Metadata, get_metadata_filename(), - Json.to_string(metadata, - true).data); + Json.to_string(metadata, true).data); } // public new async void install(InstallTask.Mode install_mode = InstallTask.Mode.INTERACTIVE, bool update = false) @@ -1422,21 +1327,16 @@ namespace GameHub.Data.Sources.EpicGames { public EpicGame game; - public DLC(EpicGame game, - Asset asset, - Json.Node? metadata = null) + public DLC(EpicGame game, Asset asset, Json.Node? metadata = null) { - base( - game.source as EpicGames, - asset, - metadata); + base(game.source as EpicGames, asset, metadata); - icon = game.icon; + icon = game.icon; image = game.image; install_dir = game.install_dir; - work_dir = game.work_dir; - executable = game.executable; + work_dir = game.work_dir; + executable = game.executable; platforms = game.platforms; @@ -1461,18 +1361,13 @@ namespace GameHub.Data.Sources.EpicGames public Asset.from_egs_json(Json.Node json) { assert(json.get_node_type() == Json.NodeType.OBJECT); - app_name = json.get_object().get_string_member_with_default("appName", - ""); - asset_id = json.get_object().get_string_member_with_default("assetId", - ""); - build_version = json.get_object().get_string_member_with_default("buildVersion", - ""); - catalog_item_id = json.get_object().get_string_member_with_default("catalogItemId", - ""); - label_name = json.get_object().get_string_member_with_default("labelName", - ""); - ns = json.get_object().get_string_member_with_default("namespace", - ""); + + app_name = json.get_object().get_string_member_with_default("appName", ""); + asset_id = json.get_object().get_string_member_with_default("assetId", ""); + build_version = json.get_object().get_string_member_with_default("buildVersion", ""); + catalog_item_id = json.get_object().get_string_member_with_default("catalogItemId", ""); + label_name = json.get_object().get_string_member_with_default("labelName", ""); + ns = json.get_object().get_string_member_with_default("namespace", ""); // asset = json; if(json.get_object().has_member("metadata")) @@ -1491,18 +1386,13 @@ namespace GameHub.Data.Sources.EpicGames public Asset.from_json(Json.Node json) { assert(json.get_node_type() == Json.NodeType.OBJECT); - app_name = json.get_object().get_string_member_with_default("app_name", - ""); - asset_id = json.get_object().get_string_member_with_default("asset_id", - ""); - build_version = json.get_object().get_string_member_with_default("build_version", - ""); - catalog_item_id = json.get_object().get_string_member_with_default("catalog_item_id", - ""); - label_name = json.get_object().get_string_member_with_default("label_name", - ""); - ns = json.get_object().get_string_member_with_default("namespace", - ""); + + app_name = json.get_object().get_string_member_with_default("app_name", ""); + asset_id = json.get_object().get_string_member_with_default("asset_id", ""); + build_version = json.get_object().get_string_member_with_default("build_version", ""); + catalog_item_id = json.get_object().get_string_member_with_default("catalog_item_id", ""); + label_name = json.get_object().get_string_member_with_default("label_name", ""); + ns = json.get_object().get_string_member_with_default("namespace", ""); if(json.get_object().has_member("metadata")) { @@ -1519,29 +1409,20 @@ namespace GameHub.Data.Sources.EpicGames { var json = new Json.Node(Json.NodeType.OBJECT); json.set_object(new Json.Object()); - json.get_object().set_string_member("app_name", - app_name); - json.get_object().set_string_member("asset_id", - asset_id); - json.get_object().set_string_member("build_version", - build_version); - json.get_object().set_string_member("catalog_item_id", - catalog_item_id); - json.get_object().set_string_member("label_name", - label_name); - json.get_object().set_object_member("metadata", - metadata.get_object()); - json.get_object().set_string_member("namespace", - ns); + json.get_object().set_string_member("app_name", app_name); + json.get_object().set_string_member("asset_id", asset_id); + json.get_object().set_string_member("build_version", build_version); + json.get_object().set_string_member("catalog_item_id", catalog_item_id); + json.get_object().set_string_member("label_name", label_name); + json.get_object().set_object_member("metadata", metadata.get_object()); + json.get_object().set_string_member("namespace", ns); return json; } - public string to_string(bool pretty) { return Json.to_string(to_json(), - pretty); } + public string to_string(bool pretty) { return Json.to_string(to_json(), pretty); } - public static new bool is_equal(Asset a, - Asset b) + public static new bool is_equal(Asset a, Asset b) { if(a.asset_id == b.asset_id) { diff --git a/src/data/sources/epicgames/EpicGames.vala b/src/data/sources/epicgames/EpicGames.vala index 1d82b913..a3c1f061 100644 --- a/src/data/sources/epicgames/EpicGames.vala +++ b/src/data/sources/epicgames/EpicGames.vala @@ -9,27 +9,28 @@ using GameHub.Utils; namespace GameHub.Data.Sources.EpicGames { - internal bool log_chunk = false; - internal bool log_chunk_part = false; - internal bool log_chunk_data_list = false; + internal bool log_chunk = false; + internal bool log_chunk_part = false; + internal bool log_chunk_data_list = false; internal bool log_epic_games_services = false; - internal bool log_file_manifest_list = false; - internal bool log_manifest = false; - internal bool log_meta = false; + internal bool log_file_manifest_list = false; + internal bool log_manifest = false; + internal bool log_meta = false; public class EpicGames: GameSource { public static EpicGames instance; - private ArrayList _games = new ArrayList(Game.is_equal); - public HashMap owned_games { get; default = new HashMap(null, null, Game.is_equal); } - private Json.Node? userdata { get; default = new Json.Node(Json.NodeType.NULL); } private Settings.Auth.EpicGames settings; - public override string id { get { return "epicgames"; } } - public override string name { get { return "EpicGames"; } } - public override string icon { get { return "source-epicgames-symbolic"; } } - public override ArrayList games { get { return _games; } } + private Json.Node? userdata { get; default = new Json.Node(Json.NodeType.NULL); } + + public override string id { get { return "epicgames"; } } + public override string name { get { return "EpicGames"; } } + public override string icon { get { return "source-epicgames-symbolic"; } } + public override ArrayList games { get; default = new ArrayList(Game.is_equal); } + public HashMap owned_games { get; default = new HashMap(null, null, Game.is_equal); } + public override bool enabled { get { return Settings.Auth.EpicGames.instance.enabled; } @@ -40,8 +41,7 @@ namespace GameHub.Data.Sources.EpicGames { get { - return_val_if_fail(userdata.get_object().has_member("displayName"), - null); + return_val_if_fail(userdata.get_object().has_member("displayName"), null); return userdata.get_object().get_string_member("displayName"); } @@ -77,13 +77,11 @@ namespace GameHub.Data.Sources.EpicGames if(_assets.is_empty) { // read from cache - var json = Parser.parse_json_file(FS.Paths.EpicGames.Cache, - "assets.json"); + var json = Parser.parse_json_file(FS.Paths.EpicGames.Cache, "assets.json"); if(json.get_node_type() == Json.NodeType.ARRAY) { - json.get_array().foreach_element( - (array, index, node) => { + json.get_array().foreach_element((array, index, node) => { var asset = new EpicGame.Asset.from_json(node); // debug("loaded asset: " + asset.to_string(true)); @@ -113,15 +111,14 @@ namespace GameHub.Data.Sources.EpicGames write(FS.Paths.EpicGames.Cache, "assets.json", - Json.to_string(json, - true).data); + Json.to_string(json, true).data); } } public EpicGames() { - instance = this; - settings = Settings.Auth.EpicGames.instance; + instance = this; + settings = Settings.Auth.EpicGames.instance; _userdata = Parser.parse_json(settings.userdata); // Session we're using to access the api @@ -142,16 +139,14 @@ namespace GameHub.Data.Sources.EpicGames public override bool is_authenticated() { - return_val_if_fail(userdata.get_node_type() == Json.NodeType.OBJECT, - false); + return_val_if_fail(userdata.get_node_type() == Json.NodeType.OBJECT, false); if(!userdata.get_object().has_member("access_token")) return false; if(!userdata.get_object().has_member("expires_at")) return false; - var now = new DateTime.now_local(); - var access_expires = new DateTime.from_iso8601(userdata.get_object().get_string_member("expires_at"), - null); + var now = new DateTime.now_local(); + var access_expires = new DateTime.from_iso8601(userdata.get_object().get_string_member("expires_at"), null); if(access_expires.difference(now) < TimeSpan.MINUTE * 10) { @@ -165,16 +160,14 @@ namespace GameHub.Data.Sources.EpicGames public override bool can_authenticate_automatically() { - return_val_if_fail(userdata.get_node_type() == Json.NodeType.OBJECT, - false); + return_val_if_fail(userdata.get_node_type() == Json.NodeType.OBJECT, false); if(!userdata.get_object().has_member("refresh_token")) return false; if(!userdata.get_object().has_member("refresh_expires_at")) return false; - var now = new DateTime.now_local(); - var refresh_expires = new DateTime.from_iso8601(userdata.get_object().get_string_member("refresh_expires_at"), - null); + var now = new DateTime.now_local(); + var refresh_expires = new DateTime.from_iso8601(userdata.get_object().get_string_member("refresh_expires_at"), null); if(refresh_expires.difference(now) < TimeSpan.MINUTE * 10) { @@ -183,8 +176,7 @@ namespace GameHub.Data.Sources.EpicGames return false; } - return userdata.get_object().get_string_member_with_default("refresh_token", - "") != "" && settings.authenticated; + return userdata.get_object().get_string_member_with_default("refresh_token", "") != "" && settings.authenticated; } public override async bool authenticate() @@ -195,17 +187,17 @@ namespace GameHub.Data.Sources.EpicGames if(can_authenticate_automatically()) { - _userdata = EpicGamesServices.instance.start_session(userdata.get_object().get_string_member("refresh_token")); - settings.userdata = Json.to_string(userdata, - false); + _userdata = EpicGamesServices.instance.start_session(userdata.get_object().get_string_member("refresh_token")); + settings.userdata = Json.to_string(userdata, false); return is_authenticated(); } - var wnd = new GameHub.UI.Windows.WebAuthWindow(this.name, - "https://www.epicgames.com/id/login?redirectUrl=https%3A%2F%2Fwww.epicgames.com%2Fid%2Fapi%2Fredirect", - "https://www.epicgames.com/id/api/redirect", - null); + var wnd = new GameHub.UI.Windows.WebAuthWindow( + this.name, + "https://www.epicgames.com/id/login?redirectUrl=https%3A%2F%2Fwww.epicgames.com%2Fid%2Fapi%2Fredirect", + "https://www.epicgames.com/id/api/redirect", + null); wnd.finished.connect(() => { @@ -215,7 +207,7 @@ namespace GameHub.Data.Sources.EpicGames (obj, res) => { try { - var webview_cookies = wnd.webview.web_context.get_cookie_manager().get_cookies.end(res); + var webview_cookies = wnd.webview.web_context.get_cookie_manager().get_cookies.end(res); SList cookies = new SList(); webview_cookies.foreach(cookie => { @@ -232,15 +224,13 @@ namespace GameHub.Data.Sources.EpicGames wnd.canceled.connect(() => Idle.add(authenticate.callback)); - wnd.set_size_request(640, - 800); // FIXME: Doesn't work? + wnd.set_size_request(640, 800); // FIXME: Doesn't work? wnd.show_all(); wnd.present(); yield; - settings.userdata = Json.to_string(userdata, - false); + settings.userdata = Json.to_string(userdata, false); return is_authenticated(); } @@ -249,9 +239,8 @@ namespace GameHub.Data.Sources.EpicGames { EpicGamesServices.instance.invalidate_session(); - _userdata = new Json.Node(Json.NodeType.NULL); - settings.userdata = Json.to_string(userdata, - false); + _userdata = new Json.Node(Json.NodeType.NULL); + settings.userdata = Json.to_string(userdata, false); settings.authenticated = false; // invalidate webkit session to allow logging in with a different account @@ -261,8 +250,7 @@ namespace GameHub.Data.Sources.EpicGames var webview = new WebView(); var cookies_file = FS.expand(FS.Paths.Cache.Cookies); - webview.web_context.get_cookie_manager().set_persistent_storage(cookies_file, - CookiePersistentStorage.TEXT); + webview.web_context.get_cookie_manager().set_persistent_storage(cookies_file, CookiePersistentStorage.TEXT); var website_data = yield webview.get_website_data_manager().fetch(WebsiteDataTypes.COOKIES); foreach(var website in website_data) @@ -272,11 +260,9 @@ namespace GameHub.Data.Sources.EpicGames var list = new GLib.List(); list.append(website); - if(yield webview.get_website_data_manager().remove(WebsiteDataTypes.COOKIES, - list)) + if(yield webview.get_website_data_manager().remove(WebsiteDataTypes.COOKIES, list)) { - debug("[Sources.EpicGames.logout] Deleted cookies for: %s", - website.get_name()); + debug("[Sources.EpicGames.logout] Deleted cookies for: %s", website.get_name()); } } } @@ -287,7 +273,7 @@ namespace GameHub.Data.Sources.EpicGames return true; } - public override async ArrayList load_games(Utils.FutureResult2? game_loaded = null, + public override async ArrayList load_games(Utils.FutureResult2? game_loaded = null, Utils.Future? cache_loaded = null) { if(!is_authenticated() || _games.size > 0) @@ -300,7 +286,7 @@ namespace GameHub.Data.Sources.EpicGames { _games.clear(); - var cached = Tables.Games.get_all(this); + var cached = Tables.Games.get_all(this); games_count = 0; if(cached.size > 0) @@ -312,13 +298,11 @@ namespace GameHub.Data.Sources.EpicGames if(!Settings.UI.Behavior.instance.merge_games || !Tables.Merges.is_game_merged(g)) { _games.add(g); - owned_games.set(g.id, - g); + owned_games.set(g.id, g); if(game_loaded != null) { - game_loaded(g, - true); + game_loaded(g, true); } } @@ -345,8 +329,7 @@ namespace GameHub.Data.Sources.EpicGames // owned_games.set(game.id, game); if(game_loaded != null) { - game_loaded(game, - false); + game_loaded(game, false); } } @@ -429,90 +412,60 @@ namespace GameHub.Data.Sources.EpicGames public string authenticate_with_sid(SList cookies) { var session = new Session(); - session.timeout = 5; - session.max_conns = 256; + session.timeout = 5; + session.max_conns = 256; session.max_conns_per_host = 256; // FIXME: header setting looks ugly - // message.request_headers.append(); - // 'X-Epic-Event-Action': 'login', - // 'X-Epic-Event-Category': 'login', - // 'X-Epic-Strategy-Flags': '', - // 'X-Requested-With': 'XMLHttpRequest', - // 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' - // 'AppleWebKit/537.36 (KHTML, like Gecko) ' - // 'EpicGamesLauncher/11.0.1-14907503+++Portal+Release-Live ' - // 'UnrealEngine/4.23.0-14907503+++Portal+Release-Live ' - // 'Chrome/84.0.4147.38 Safari/537.36' - debug("[Sources.EpicGames.LegendaryCore.with_sid] Getting xsrf"); - var message = new Message("GET", - "https://www.epicgames.com/id/api/csrf"); - message.request_headers.append("X-Epic-Event-Action", - "login"); - message.request_headers.append("X-Epic-Event-Category", - "login"); - message.request_headers.append("X-Epic-Strategy-Flags", - ""); - message.request_headers.append("X-Requested-With", - "XMLHttpRequest"); - message.request_headers.append( - "User-Agent", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + - "AppleWebKit/537.36 (KHTML, like Gecko) " + - "EpicGamesLauncher/11.0.1-14907503+++Portal+Release-Live " + - "UnrealEngine/4.23.0-14907503+++Portal+Release-Live " + - "Chrome/84.0.4147.38 Safari/537.36"); - cookies_to_request(cookies, - message); + var message = new Message("GET", "https://www.epicgames.com/id/api/csrf"); + message.request_headers.append("X-Epic-Event-Action", "login"); + message.request_headers.append("X-Epic-Event-Category", "login"); + message.request_headers.append("X-Epic-Strategy-Flags", ""); + message.request_headers.append("X-Requested-With", "XMLHttpRequest"); + message.request_headers.append("User-Agent", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + + "AppleWebKit/537.36 (KHTML, like Gecko) " + + "EpicGamesLauncher/11.0.1-14907503+++Portal+Release-Live " + + "UnrealEngine/4.23.0-14907503+++Portal+Release-Live " + + "Chrome/84.0.4147.38 Safari/537.36"); + cookies_to_request(cookies, message); var status = session.send_message(message); - debug("[Sources.EpicGames.LegendaryCore.with_sid] Status: %s", - status.to_string()); + debug("[Sources.EpicGames.LegendaryCore.with_sid] Status: %s", status.to_string()); assert(status == 204); debug("[Sources.EpicGames.LegendaryCore.with_sid] Getting exchange code"); var cookies_from_response = cookies_from_response(message); - message = new Message("POST", - "https://www.epicgames.com/id/api/exchange/generate"); - message.request_headers.append("X-Epic-Event-Action", - "login"); - message.request_headers.append("X-Epic-Event-Category", - "login"); - message.request_headers.append("X-Epic-Strategy-Flags", - ""); - message.request_headers.append("X-Requested-With", - "XMLHttpRequest"); - message.request_headers.append( - "User-Agent", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + - "AppleWebKit/537.36 (KHTML, like Gecko) " + - "EpicGamesLauncher/11.0.1-14907503+++Portal+Release-Live " + - "UnrealEngine/4.23.0-14907503+++Portal+Release-Live " + - "Chrome/84.0.4147.38 Safari/537.36"); - cookies_to_request(cookies, - message); - cookies_to_request(cookies_from_response, - message); + message = new Message("POST", "https://www.epicgames.com/id/api/exchange/generate"); + message.request_headers.append("X-Epic-Event-Action", "login"); + message.request_headers.append("X-Epic-Event-Category", "login"); + message.request_headers.append("X-Epic-Strategy-Flags", ""); + message.request_headers.append("X-Requested-With", "XMLHttpRequest"); + message.request_headers.append("User-Agent", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + + "AppleWebKit/537.36 (KHTML, like Gecko) " + + "EpicGamesLauncher/11.0.1-14907503+++Portal+Release-Live " + + "UnrealEngine/4.23.0-14907503+++Portal+Release-Live " + + "Chrome/84.0.4147.38 Safari/537.36"); + cookies_to_request(cookies, message); + cookies_to_request(cookies_from_response, message); cookies_from_response.foreach(cookie => { if(cookie.get_name() == "XSRF-TOKEN") { - message.request_headers.append("X-XSRF-TOKEN", - cookie.get_value()); + message.request_headers.append("X-XSRF-TOKEN", cookie.get_value()); } }); status = session.send_message(message); - debug("[Sources.EpicGames.LegendaryCore.with_sid] Status: %s", - status.to_string()); + debug("[Sources.EpicGames.LegendaryCore.with_sid] Status: %s", status.to_string()); assert(status == 200); var json = Parser.parse_json((string) message.response_body.data); if(GameHub.Application.log_auth) { - debug(Json.to_string(json, - true)); + debug(Json.to_string(json, true)); } assert(json.get_node_type() == Json.NodeType.OBJECT); @@ -533,8 +486,7 @@ namespace GameHub.Data.Sources.EpicGames { assert(exchange_code != ""); - _userdata = EpicGamesServices.instance.start_session(null, - exchange_code); + _userdata = EpicGamesServices.instance.start_session(null, exchange_code); return; } @@ -583,14 +535,14 @@ namespace GameHub.Data.Sources.EpicGames // return false; // } - public ArrayList get_game_assets(bool update_assets = false, + public ArrayList get_game_assets(bool update_assets = false, string? platform_override = null) { if(platform_override != null) { - var list = new ArrayList(); - var games_json = EpicGamesServices.instance.get_game_assets(access_token, - platform_override); + var list = new ArrayList(); + var games_json = EpicGamesServices.instance.get_game_assets(access_token, platform_override); + games_json.get_array().foreach_element((array, index, node) => { assert(node.get_node_type() == Json.NodeType.OBJECT); var asset = new EpicGame.Asset.from_egs_json(node); @@ -605,6 +557,7 @@ namespace GameHub.Data.Sources.EpicGames { // TODO: not logged in var games_json = EpicGamesServices.instance.get_game_assets(access_token); + games_json.get_array().foreach_element((array, index, node) => { assert(node.get_node_type() == Json.NodeType.OBJECT); var asset = new EpicGame.Asset.from_egs_json(node); @@ -615,8 +568,7 @@ namespace GameHub.Data.Sources.EpicGames } else { - assets.set(assets.index_of(asset), - asset); + assets.set(assets.index_of(asset), asset); } }); } @@ -624,8 +576,7 @@ namespace GameHub.Data.Sources.EpicGames return assets; } - public EpicGame.Asset? get_game_asset(string id, - bool update = false) + public EpicGame.Asset? get_game_asset(string id, bool update = false) { if(update) { @@ -645,8 +596,7 @@ namespace GameHub.Data.Sources.EpicGames public void asset_valid() {} - public EpicGame? get_game(EpicGame game, - bool update_meta = false) + public EpicGame? get_game(EpicGame game, bool update_meta = false) { if(update_meta) get_game_and_dlc_list(true); @@ -656,16 +606,15 @@ namespace GameHub.Data.Sources.EpicGames // Not needed, dlcs are always bound to games // public void get_game_list() {} - public void get_game_and_dlc_list(bool update_assets = true, - string? platform_override = null, + public void get_game_and_dlc_list(bool update_assets = true, + string? platform_override = null, bool skip_unreal_engine = true) { - // I don't really need the inner HashSet - a list of tuples would be enough. + // I don't really need the inner HashMap - a list of tuples would be enough. // Vala should be able to handle tuples but I couldn't figure it out var dlcs = new HashMap >(); - var tmp_assets = get_game_assets(update_assets, - platform_override); + var tmp_assets = get_game_assets(update_assets, platform_override); foreach(var asset in tmp_assets) { if(asset.ns == "ue" && skip_unreal_engine) continue; @@ -687,14 +636,11 @@ namespace GameHub.Data.Sources.EpicGames asset.app_name); } - metadata = EpicGamesServices.instance.get_game_info(asset.ns, - asset.catalog_item_id); + metadata = EpicGamesServices.instance.get_game_info(asset.ns, asset.catalog_item_id); assert(metadata.get_node_type() == Json.NodeType.OBJECT); // var title = metadata.get_object().get_string_member_with_default("title", ""); - game = new EpicGame(EpicGames.instance, - asset, - metadata); + game = new EpicGame(EpicGames.instance, asset, metadata); // if(platform_override == null) game.save_metadata(); } @@ -710,8 +656,7 @@ namespace GameHub.Data.Sources.EpicGames if(game.is_dlc) { var json = Parser.parse_json(game.info_detailed); - return_val_if_fail(json.get_node_type() == Json.NodeType.OBJECT, - false); + return_val_if_fail(json.get_node_type() == Json.NodeType.OBJECT, false); var main_id = json.get_object().get_object_member("mainGameItem").get_string_member("id"); @@ -723,15 +668,12 @@ namespace GameHub.Data.Sources.EpicGames tmp = new HashMap(); } - tmp.set(asset, - metadata); - dlcs.set(main_id, - tmp); + tmp.set(asset, metadata); + dlcs.set(main_id, tmp); } else { - owned_games.set(asset.app_name, - game); + owned_games.set(asset.app_name, game); } // TODO: mods? @@ -745,8 +687,7 @@ namespace GameHub.Data.Sources.EpicGames foreach(var tuple in game_name.value) { var game = (EpicGame) owned_games.get(game_name.key); - game.add_dlc(tuple.key, - tuple.value); + game.add_dlc(tuple.key, tuple.value); } } } diff --git a/src/data/sources/epicgames/EpicGamesServices.vala b/src/data/sources/epicgames/EpicGamesServices.vala index f99e30da..0792174a 100644 --- a/src/data/sources/epicgames/EpicGamesServices.vala +++ b/src/data/sources/epicgames/EpicGamesServices.vala @@ -16,70 +16,58 @@ namespace GameHub.Data.Sources.EpicGames private const string username = "34a02cf8f4414e29b15921876da36f9a"; private const string password = "daafbccc737745039dffe53d94fc76cf"; - private const string oauth_host = "account-public-service-prod03.ol.epicgames.com"; - private const string launcher_host = "launcher-public-service-prod06.ol.epicgames.com"; + private const string oauth_host = "account-public-service-prod03.ol.epicgames.com"; + private const string launcher_host = "launcher-public-service-prod06.ol.epicgames.com"; private const string entitlements_host = "entitlement-public-service-prod08.ol.epicgames.com"; - private const string catalog_host = "catalog-public-service-prod06.ol.epicgames.com"; - private const string ecommerce_host = "ecommerceintegration-public-service-ecomprod02.ol.epicgames.com"; - private const string datastorage_host = "datastorage-public-service-liveegs.live.use1a.on.epicgames.com"; - private const string library_host = "library-service.live.use1a.on.epicgames.com"; + private const string catalog_host = "catalog-public-service-prod06.ol.epicgames.com"; + private const string ecommerce_host = "ecommerceintegration-public-service-ecomprod02.ol.epicgames.com"; + private const string datastorage_host = "datastorage-public-service-liveegs.live.use1a.on.epicgames.com"; + private const string library_host = "library-service.live.use1a.on.epicgames.com"; // TODO: hardcoded for now private string language_code = "en"; - private string country_code = "US"; + private string country_code = "US"; // used with session, does not include user-agent as that's already set for the session private HashMap auth_headers = new HashMap(); // does not include auth header so it can be used with access token for e.g. Utils.Parser private HashMap unauth_headers = new HashMap(); - private Session session = new Session(); - private string user_agent = "UELauncher/11.0.1-14907503+++Portal+Release-Live Windows/10.0.19041.1.256.64bit"; + private Session session = new Session(); + private string user_agent = "UELauncher/11.0.1-14907503+++Portal+Release-Live Windows/10.0.19041.1.256.64bit"; internal EpicGamesServices() { instance = this; session.user_agent = user_agent; - unauth_headers.set("User-Agent", - user_agent); + unauth_headers.set("User-Agent", user_agent); } - internal Json.Node start_session(string? refresh_token = null, - string? exchange_code = null) + internal Json.Node start_session(string? refresh_token = null, string? exchange_code = null) { - var form_data = new HashTable(null, - null); + var form_data = new HashTable(null, null); if(refresh_token != null) { - form_data.set("grant_type", - "refresh_token"); - form_data.set("refresh_token", - refresh_token); - form_data.set("token_type", - "eg1"); + form_data.set("grant_type", "refresh_token"); + form_data.set("refresh_token", refresh_token); + form_data.set("token_type", "eg1"); } else if(exchange_code != null) { - form_data.set("grant_type", - "exchange_code"); - form_data.set("exchange_code", - exchange_code); - form_data.set("token_type", - "eg1"); + form_data.set("grant_type", "exchange_code"); + form_data.set("exchange_code", exchange_code); + form_data.set("token_type", "eg1"); } else { return_if_reached(); } - var message = Form.request_new_from_hash("POST", - @"https://$oauth_host/account/api/oauth/token", - form_data); + var message = Form.request_new_from_hash("POST", @"https://$oauth_host/account/api/oauth/token", form_data); - message.request_headers.append("Authorization", - "Basic " + Base64.encode((username + ":" + password).data)); + message.request_headers.append("Authorization", "Basic " + Base64.encode((username + ":" + password).data)); var status = session.send_message(message); @@ -89,16 +77,14 @@ namespace GameHub.Data.Sources.EpicGames if(GameHub.Application.log_auth) { - debug("[start_session] " + Json.to_string(json, - true)); + debug("[start_session] " + Json.to_string(json, true)); } // invalid userdata assert(json.get_node_type() == Json.NodeType.OBJECT); assert(!json.get_object().has_member("error")); - auth_headers.set("Authorization", - "Bearer %s".printf(json.get_object().get_string_member("access_token"))); + auth_headers.set("Authorization", "Bearer %s".printf(json.get_object().get_string_member("access_token"))); return json; @@ -121,6 +107,8 @@ namespace GameHub.Data.Sources.EpicGames // } } + // This function is intended for server - side use only. + // https://dev.epicgames.com/docs/services/en-US/API/Members/Functions/Auth/EOS_Auth_VerifyUserAuth/index.html internal Json.Node resume_session(Json.Node userdata) requires(userdata.get_node_type() == Json.NodeType.OBJECT) { @@ -132,28 +120,23 @@ namespace GameHub.Data.Sources.EpicGames if(GameHub.Application.log_auth) { - debug("[resume_session] downloaded json " + Json.to_string(refreshed_json, - true)); + debug("[resume_session] downloaded json " + Json.to_string(refreshed_json, true)); } assert(refreshed_json.get_node_type() == Json.NodeType.OBJECT); assert(!refreshed_json.get_object().has_member("error")); assert(!refreshed_json.get_object().has_member("errorMessage")); - refreshed_json.get_object().foreach_member( - (object, name, node) => { - userdata.get_object().set_member(name, - node); + refreshed_json.get_object().foreach_member((object, name, node) => { + userdata.get_object().set_member(name, node); }); if(GameHub.Application.log_auth) { - debug("[resume_session] updated userdata " + Json.to_string(userdata, - true)); + debug("[resume_session] updated userdata " + Json.to_string(userdata, true)); } - auth_headers.set("Authorization", - "Bearer %s".printf(refreshed_json.get_object().get_string_member("access_token"))); + auth_headers.set("Authorization", "Bearer %s".printf(refreshed_json.get_object().get_string_member("access_token"))); return userdata; @@ -177,11 +160,9 @@ namespace GameHub.Data.Sources.EpicGames internal void invalidate_session() { - var message = new Message("DELETE", - @"https://$oauth_host/account/api/oauth/sessions/kill/$(EpicGames.instance.access_token)"); + var message = new Message("DELETE", @"https://$oauth_host/account/api/oauth/sessions/kill/$(EpicGames.instance.access_token)"); auth_headers.foreach(header => { - message.request_headers.append(header.key, - header.value); + message.request_headers.append(header.key, header.value); return true; }); @@ -193,7 +174,7 @@ namespace GameHub.Data.Sources.EpicGames internal Json.Node get_game_token() { uint status; - var json = Parser.parse_remote_json_file( + var json = Parser.parse_remote_json_file( @"https://$oauth_host/account/api/oauth/exchange", "GET", EpicGames.instance.access_token, @@ -207,10 +188,9 @@ namespace GameHub.Data.Sources.EpicGames return json; } - internal Bytes get_ownership_token(string ns, - string catalog_item_id) + internal Bytes get_ownership_token(string ns, string catalog_item_id) { - var data = new HashMap(); + var data = new HashMap(); var multipart = new Multipart("multipart/form-data"); var message = new Message( @@ -218,23 +198,19 @@ namespace GameHub.Data.Sources.EpicGames @"https://$ecommerce_host/ecommerceintegration/api/public/" + @"platforms/EPIC/identities/$(EpicGames.instance.user_id)/ownershipToken"); - data.set("nsCatalogItemId", - @"$ns:$catalog_item_id"); + data.set("nsCatalogItemId", @"$ns:$catalog_item_id"); auth_headers.foreach(header => { - message.request_headers.append(header.key, - header.value); + message.request_headers.append(header.key, header.value); return true; }); foreach(var v in data.entries) { - multipart.append_form_string(v.key, - v.value); + multipart.append_form_string(v.key, v.value); } - multipart.to_message(message.request_headers, - message.request_body); + multipart.to_message(message.request_headers, message.request_body); var status = session.send_message(message); assert(status < 400); @@ -242,11 +218,10 @@ namespace GameHub.Data.Sources.EpicGames return new Bytes(message.response_body.data); } - internal Json.Node get_game_assets(string platform = "Windows", - string label = "Live") + internal Json.Node get_game_assets(string platform = "Windows", string label = "Live") { uint status; - var json = Parser.parse_remote_json_file( + var json = Parser.parse_remote_json_file( @"https://$launcher_host/launcher/api/public/assets/$platform?label=$label", "GET", EpicGames.instance.access_token, @@ -265,10 +240,10 @@ namespace GameHub.Data.Sources.EpicGames string catalog_item_id, string app_name, string platform = "Windows", - string label = "Live") + string label = "Live") { uint status; - var json = Parser.parse_remote_json_file( + var json = Parser.parse_remote_json_file( @"https://$launcher_host/launcher/api/public/assets/v2/platform" + @"/$platform/namespace/$ns/catalogItem/$catalog_item_id/app" + @"/$app_name/label/$label", @@ -287,24 +262,18 @@ namespace GameHub.Data.Sources.EpicGames internal void get_user_entitlements() {} - internal Json.Node get_game_info(string _namespace, - string catalog_item_id) + internal Json.Node get_game_info(string _namespace, string catalog_item_id) { - uint status; - Gee.HashMap data = new Gee.HashMap(); - data.set("id", - catalog_item_id); - data.set("includeDLCDetails", - "True"); - data.set("includeMainGameDetails", - "True"); - data.set("country", - country_code); - data.set("locale", - language_code); - - var json = Parser.parse_remote_json_file( + + data.set("id", catalog_item_id); + data.set("includeDLCDetails", "True"); + data.set("includeMainGameDetails", "True"); + data.set("country", country_code); + data.set("locale", language_code); + + uint status; + var json = Parser.parse_remote_json_file( @"https://$catalog_host/catalog/api/shared/namespace/$_namespace/bulk/items ?id=$catalog_item_id &includeDLCDetails=True @@ -324,11 +293,64 @@ namespace GameHub.Data.Sources.EpicGames return json.get_object().get_member(catalog_item_id); } - internal void get_library_items() {} + internal ArrayList get_library_items(bool include_metadata = true) + { + ArrayList records = new ArrayList(); + + uint status; + var json = Parser.parse_remote_json_file( + @"https://$library_host/library/api/public/items" + + @"?includeMetadata=$include_metadata", + "GET", + EpicGames.instance.access_token, + unauth_headers, + null, + out status); + + if(log_epic_games_services) debug("[Source.EpicGames.EpicGamesServices.get_library_items] json dump: \n%s", Json.to_string(json, true)); - internal Json.Node get_user_cloud_saves(string game_id = "", - bool manifests = false, - string? filenames = null) + assert(status < 400); + assert(json.get_node_type() == Json.NodeType.OBJECT); + assert(json.get_object().has_member("records")); + assert(json.get_object().get_member("records").get_node_type() == Json.NodeType.ARRAY); + + json.get_object().get_array_member("records").foreach_element((array, index, node) => { + records.add(node); + }); + + + while(json.get_object().has_member("responseMetadata") + && json.get_object().get_member("responseMetadata").get_node_type() == Json.NodeType.OBJECT + && json.get_object().get_object_member("responseMetadata").has_member("nextCursor") + && json.get_object().get_object_member("responseMetadata").get_member("nextCursor").get_node_type() == Json.NodeType.OBJECT) + { + // TODO: verify if this is a string + var cursor = json.get_object().get_object_member("responseMetadata").get_string_member("nextCursor"); + + json = Parser.parse_remote_json_file( + @"https://$library_host/library/api/public/items" + + @"?includeMetadata=$include_metadata" + + @"&cursor=$cursor", + "GET", + EpicGames.instance.access_token, + unauth_headers, + null, + out status); + + assert(status < 400); + assert(json.get_node_type() == Json.NodeType.OBJECT); + assert(json.get_object().has_member("records")); + assert(json.get_object().get_member("records").get_node_type() == Json.NodeType.ARRAY); + + json.get_object().get_array_member("records").foreach_element((array, index, node) => { + records.add(node); + }); + } + + return records; + } + + internal Json.Node get_user_cloud_saves(string game_id = "", bool manifests = false, string? filenames = null) { var app_name = game_id; @@ -341,20 +363,18 @@ namespace GameHub.Data.Sources.EpicGames app_name += "/"; } - Json.Node json; - uint status; - string method = "GET"; - HashMap data = null; + string method = "GET"; + HashMap data = null; if(filenames != null && filenames.length > 0) { method = "POST"; - data = new HashMap(); - data.set("files", - filenames); + data = new HashMap(); + data.set("files", filenames); } - json = Parser.parse_remote_json_file( + uint status; + var json = Parser.parse_remote_json_file( @"https://$datastorage_host/api/v1/access/egstore/savesync/" + @"$(EpicGames.instance.user_id)/$app_name", method, @@ -368,18 +388,13 @@ namespace GameHub.Data.Sources.EpicGames return json; } - internal Json.Node create_game_cloud_saves(string game_id, - string filenames) { return get_user_cloud_saves(game_id, - false, - filenames); } + internal Json.Node create_game_cloud_saves(string game_id, string filenames) { return get_user_cloud_saves(game_id, false, filenames); } internal void delete_game_cloud_save_files(string path) { - var message = new Message("DELETE", - @"https://$datastorage_host/api/v1/data/egstore/$path"); + var message = new Message("DELETE", @"https://$datastorage_host/api/v1/data/egstore/$path"); auth_headers.foreach(header => { - message.request_headers.append(header.key, - header.value); + message.request_headers.append(header.key, header.value); return true; }); @@ -388,13 +403,10 @@ namespace GameHub.Data.Sources.EpicGames assert(status < 400); } - internal void get_cdn_manifest(string url, - out Bytes data) + internal void get_cdn_manifest(string url, out Bytes data) { - debug("[Sources.EpicGames.get_cdn_manifest] Downloading manifest from: %s…", - url); - var message = new Message("GET", - url); + debug("[Sources.EpicGames.get_cdn_manifest] Downloading manifest from: %s…", url); + var message = new Message("GET", url); // unauth on purpose var status = session.send_message(message); @@ -405,20 +417,16 @@ namespace GameHub.Data.Sources.EpicGames /** * Get optimized delta manifest (doesn't seem to exist for most games) */ - internal Bytes? get_delta_manifest(string url, - string old_build_id, - string new_build_id) + internal Bytes? get_delta_manifest(string url, string old_build_id, string new_build_id) { if(old_build_id == new_build_id) return null; - var message = new Message("GET", - @"$url/Deltas/$new_build_id/$old_build_id.delta"); + var message = new Message("GET", @"$url/Deltas/$new_build_id/$old_build_id.delta"); // unauth on purpose var status = session.send_message(message); - return_val_if_fail(status == 200, - null); + return_val_if_fail(status == 200, null); return new Bytes(message.request_body.data); } diff --git a/src/data/sources/epicgames/EpicInstaller.vala b/src/data/sources/epicgames/EpicInstaller.vala index 3520eab6..e8b4dbf4 100644 --- a/src/data/sources/epicgames/EpicInstaller.vala +++ b/src/data/sources/epicgames/EpicInstaller.vala @@ -8,24 +8,22 @@ namespace GameHub.Data.Sources.EpicGames { internal class Installer: Runnables.Tasks.Install.Installer { - internal Analysis? analysis = null; - internal EpicGame game { get; private set; } - internal InstallTask? task { get; default = null; } + internal Analysis? analysis { get; set; default = null; } + internal EpicGame game { get; private set; } + internal InstallTask? task { get; default = null; } - internal Installer(EpicGame game, - Platform platform) + internal Installer(EpicGame game, Platform platform) { - _game = game; + _game = game; this.platform = platform; - id = game.id; - name = game.name; - full_size = game.get_installation_size(platform); // FIXME: This fetches and scans the manifest, try to get this from somewhere else e.g. the store page - can_import = true; + id = game.id; + name = game.name; + full_size = game.get_installation_size(platform); // FIXME: This fetches and scans the manifest, try to get this from somewhere else e.g. the store page + can_import = true; if(platform != Platform.WINDOWS) { - var list = EpicGames.instance.get_game_assets(true, - uppercase_first_character(platform.id())); + var list = EpicGames.instance.get_game_assets(true, uppercase_first_character(platform.id())); foreach(var asset in list) { if(asset.asset_id == id) @@ -156,8 +154,7 @@ namespace GameHub.Data.Sources.EpicGames ((Analysis.ChunkTask)file_task).chunk_file).query_exists()); old_stream = File.new_build_filename(task.install_dir.get_path(), ((Analysis.ChunkTask)file_task).chunk_file).read(); - old_stream.seek(((Analysis.ChunkTask)file_task).chunk_offset, - SeekType.SET); + old_stream.seek(((Analysis.ChunkTask)file_task).chunk_offset, SeekType.SET); var bytes = yield old_stream.read_bytes_async(((Analysis.ChunkTask)file_task).chunk_size); // debug("chunk hash: " + Checksum.compute_for_bytes(ChecksumType.SHA1, bytes)); yield iostream.write_bytes_async(bytes); @@ -184,8 +181,7 @@ namespace GameHub.Data.Sources.EpicGames update_game_info(); task.status = new InstallTask.Status(InstallTask.State.NONE); - game.status = new Game.Status(Game.State.INSTALLED, - this.game); + game.status = new Game.Status(Game.State.INSTALLED, this.game); return true; } @@ -196,23 +192,20 @@ namespace GameHub.Data.Sources.EpicGames _task = task; task.status = new InstallTask.Status(InstallTask.State.INSTALLING); - game.status = new Game.Status(Game.State.INSTALLING, - this.game); + game.status = new Game.Status(Game.State.INSTALLING, this.game); if(!yield game.import(task.install_dir)) { debug("import failed"); task.status = new InstallTask.Status(InstallTask.State.NONE); - game.status = new Game.Status(Game.State.UNINSTALLED, - this.game); + game.status = new Game.Status(Game.State.UNINSTALLED, this.game); return false; } game.executable_path = game.executable.get_path(); - task.status = new InstallTask.Status(InstallTask.State.VERIFYING_INSTALLER_INTEGRITY); - game.status = new Game.Status(Game.State.VERIFYING_INSTALLER_INTEGRITY, - this.game); + task.status = new InstallTask.Status(InstallTask.State.VERIFYING_INSTALLER_INTEGRITY); + game.status = new Game.Status(Game.State.VERIFYING_INSTALLER_INTEGRITY, this.game); if(game.needs_verification) yield game.verify(); @@ -220,8 +213,7 @@ namespace GameHub.Data.Sources.EpicGames else update_game_info(); task.status = new InstallTask.Status(InstallTask.State.NONE); - game.status = new Game.Status(Game.State.INSTALLED, - this.game); + game.status = new Game.Status(Game.State.INSTALLED, this.game); task.finish(); @@ -236,9 +228,8 @@ namespace GameHub.Data.Sources.EpicGames game.manifest = EpicGames.load_manifest(game.load_manifest_from_disk()); game.update_metadata(); - game.install_dir = task.install_dir; - game.executable_path = FS.file(task.install_dir.get_path(), - game.manifest.meta.launch_exe).get_path(); + game.install_dir = task.install_dir; + game.executable_path = FS.file(task.install_dir.get_path(), game.manifest.meta.launch_exe).get_path(); game.save(); game.update_status(); } diff --git a/src/data/sources/epicgames/EpicManifest.vala b/src/data/sources/epicgames/EpicManifest.vala index 4a2328a1..41ed5a84 100644 --- a/src/data/sources/epicgames/EpicManifest.vala +++ b/src/data/sources/epicgames/EpicManifest.vala @@ -8,18 +8,18 @@ namespace GameHub.Data.Sources.EpicGames { private const uint32 header_magic = 0x44BEC00C; - private Bytes sha_hash { get; default = new Bytes(null); } - private uint8 stored_as { get; default = 0; } - private uint32 header_size { get; default = 41; } - private uint32 size_compressed { get; default = 0; } + private Bytes sha_hash { get; default = new Bytes(null); } + private uint8 stored_as { get; default = 0; } + private uint32 header_size { get; default = 41; } + private uint32 size_compressed { get; default = 0; } private uint32 size_uncompressed { get; default = 0; } - private uint32 version { get; default = 18; } + private uint32 version { get; default = 18; } internal ChunkDataList? chunk_data_list { get; default = null; } // TODO: CustomFields custom_fields; // private Json.Node? custom_fields { get; default = null; } internal FileManifestList? file_manifest_list { get; default = null; } - internal Meta? meta { get; default = null; } + internal Meta? meta { get; default = null; } internal bool compressed { get { return (stored_as & 0x1) != 0; } } @@ -27,36 +27,31 @@ namespace GameHub.Data.Sources.EpicGames { read_byte_header(bytes); - var body = bytes.slice(header_size, - bytes.length); + var body = bytes.slice(header_size, bytes.length); if(compressed) { if(log_manifest) debug("[Sources.EpicGames.Manifest.read_bytes] Data is compressed, uncompressing…"); - var zlib = new ZlibDecompressor(ZlibCompressorFormat.ZLIB); - var compressed_stream = new MemoryInputStream.from_bytes(body); + var zlib = new ZlibDecompressor(ZlibCompressorFormat.ZLIB); + var compressed_stream = new MemoryInputStream.from_bytes(body); var uncompressed_stream = new MemoryOutputStream.resizable(); - var converter_stream = new ConverterOutputStream(uncompressed_stream, - zlib); + var converter_stream = new ConverterOutputStream(uncompressed_stream, zlib); try { - converter_stream.splice(compressed_stream, - OutputStreamSpliceFlags.NONE); + converter_stream.splice(compressed_stream, OutputStreamSpliceFlags.NONE); uncompressed_stream.close(); } catch (Error e) { - debug("[Manifest.from_bytes]error: %s", - e.message); + debug("[Manifest.from_bytes]error: %s", e.message); } var data_uncompressed = uncompressed_stream.steal_as_bytes(); assert(data_uncompressed.length == size_uncompressed); - var decompressed_hash = Checksum.compute_for_bytes(ChecksumType.SHA1, - data_uncompressed); + var decompressed_hash = Checksum.compute_for_bytes(ChecksumType.SHA1, data_uncompressed); if(log_manifest) debug("[Sources.EpicGames.Manifest.read_bytes] our hash: %s", decompressed_hash); @@ -67,15 +62,12 @@ namespace GameHub.Data.Sources.EpicGames var stream = new DataInputStream(new MemoryInputStream.from_bytes(body)); stream.set_byte_order(DataStreamByteOrder.LITTLE_ENDIAN); - _meta = new Meta.from_byte_stream(stream); - _chunk_data_list = new ChunkDataList.from_byte_stream(stream, - meta.feature_level); + _meta = new Meta.from_byte_stream(stream); + _chunk_data_list = new ChunkDataList.from_byte_stream(stream, meta.feature_level); _file_manifest_list = new FileManifestList.from_byte_stream(stream); // TODO: custom_fields = new CustomFields(stream); - var unhandled_data = new Bytes.from_bytes(body, - (size_t) stream.tell(), - bytes.length - (size_t) stream.tell()); + var unhandled_data = new Bytes.from_bytes(body, (size_t) stream.tell(), bytes.length - (size_t) stream.tell()); if(unhandled_data.length > 0) { @@ -91,16 +83,14 @@ namespace GameHub.Data.Sources.EpicGames { try { - _version = number_string_to_byte_stream(json.get_object().get_string_member_with_default("ManifestFileVersion", - "013000000000")).read_uint32(); + _version = number_string_to_byte_stream(json.get_object().get_string_member_with_default("ManifestFileVersion", "013000000000")).read_uint32(); } catch (Error e) { debug("error: %s", e.message); } - _meta = new Meta.from_json(json); - _chunk_data_list = new ChunkDataList.from_json(json, - version); + _meta = new Meta.from_json(json); + _chunk_data_list = new ChunkDataList.from_json(json, version); _file_manifest_list = new FileManifestList.from_json(json); - _stored_as = 0; // never compress + _stored_as = 0; // never compress // custom_fields = new CustomFields(); // if(json.get_object().has_member("CustomFields")) // { @@ -124,19 +114,18 @@ namespace GameHub.Data.Sources.EpicGames var magic = stream.read_uint32(); assert(magic == header_magic); - _header_size = stream.read_uint32(); + _header_size = stream.read_uint32(); _size_uncompressed = stream.read_uint32(); - _size_compressed = stream.read_uint32(); - _sha_hash = stream.read_bytes(20); - _stored_as = stream.read_byte(); - _version = stream.read_uint32(); + _size_compressed = stream.read_uint32(); + _sha_hash = stream.read_bytes(20); + _stored_as = stream.read_byte(); + _version = stream.read_uint32(); assert(stream.tell() == header_size); } catch (Error e) { - debug("[Manifest.read_byte_header] error: %s", - e.message); + debug("[Manifest.read_byte_header] error: %s", e.message); } } @@ -162,23 +151,23 @@ namespace GameHub.Data.Sources.EpicGames */ internal class Meta { - internal ArrayList prereq_ids { get; default = new ArrayList(); } - internal bool is_file_data { get; default = false; } - internal string app_name { get; default = ""; } - internal string build_version { get; default = ""; } - internal string launch_exe { get; default = ""; } - internal string launch_command { get; default = ""; } - internal string prereq_name { get; default = ""; } - internal string prereq_path { get; default = ""; } - internal string prereq_args { get; default = ""; } - internal uint8 data_version { get; default = 0; } - internal uint32 app_id { get; default = 0; } - internal uint32 feature_level { get; default = 18; } - internal uint32 meta_size { get; default = 0; } + internal ArrayList prereq_ids { get; default = new ArrayList(); } + internal bool is_file_data { get; default = false; } + internal string app_name { get; default = ""; } + internal string build_version { get; default = ""; } + internal string launch_exe { get; default = ""; } + internal string launch_command { get; default = ""; } + internal string prereq_name { get; default = ""; } + internal string prereq_path { get; default = ""; } + internal string prereq_args { get; default = ""; } + internal uint8 data_version { get; default = 0; } + internal uint32 app_id { get; default = 0; } + internal uint32 feature_level { get; default = 18; } + internal uint32 meta_size { get; default = 0; } // this build id is used for something called "delta file" internal string? _build_id = null; - internal string build_id + internal string build_id { get { @@ -191,26 +180,18 @@ namespace GameHub.Data.Sources.EpicGames variant.byteswap(); // FIXME: instead of hardcoded swapping try to set endian directly checksum.update(variant.get_data_as_bytes().get_data(), variant.get_data_as_bytes().get_data().length); - checksum.update(app_name.data, - -1); - checksum.update(build_version.data, - -1); - checksum.update(launch_exe.data, - -1); - checksum.update(launch_command.data, - -1); + checksum.update(app_name.data, -1); + checksum.update(build_version.data, -1); + checksum.update(launch_exe.data, -1); + checksum.update(launch_command.data, -1); uint8[] hash = new uint8[ChecksumType.SHA1.get_length()]; - size_t size = ChecksumType.SHA1.get_length(); - checksum.get_digest(hash, - ref size); + size_t size = ChecksumType.SHA1.get_length(); + checksum.get_digest(hash, ref size); try { - _build_id = convert(Base64.encode(hash).replace("+", - "-").replace("/", - "_").replace("=", - ""), + _build_id = convert(Base64.encode(hash).replace("+", "-").replace("/", "_").replace("=", ""), -1, "ASCII", "UTF-8"); @@ -232,24 +213,16 @@ namespace GameHub.Data.Sources.EpicGames try { - _feature_level = number_string_to_byte_stream(json_obj.get_string_member_with_default("ManifestFileVersion", - "013000000000")).read_uint32(); - _app_id = number_string_to_byte_stream(json_obj.get_string_member_with_default("AppID", - "000000000000")).read_uint32(); + _is_file_data = json_obj.get_boolean_member_with_default("bIsFileData", false); + _app_name = json_obj.get_string_member_with_default("AppNameString", ""); + _build_version = json_obj.get_string_member_with_default("BuildVersionString", ""); + _launch_exe = json_obj.get_string_member_with_default("LaunchExeString", ""); + _launch_command = json_obj.get_string_member_with_default("LaunchCommand", ""); + _feature_level = number_string_to_byte_stream(json_obj.get_string_member_with_default("ManifestFileVersion", "013000000000")).read_uint32(); + _app_id = number_string_to_byte_stream(json_obj.get_string_member_with_default("AppID", "000000000000")).read_uint32(); } catch (Error e) { debug("error: %s", e.message); } - _is_file_data = json_obj.get_boolean_member_with_default("bIsFileData", - false); - _app_name = json_obj.get_string_member_with_default("AppNameString", - ""); - _build_version = json_obj.get_string_member_with_default("BuildVersionString", - ""); - _launch_exe = json_obj.get_string_member_with_default("LaunchExeString", - ""); - _launch_command = json_obj.get_string_member_with_default("LaunchCommand", - ""); - // TODO: we don't care about this yet // _prereq_name = json_obj.get_string_member_with_default("PrereqName", ""); // _prereq_path = json_obj.get_string_member_with_default("PrereqPath", ""); @@ -269,7 +242,7 @@ namespace GameHub.Data.Sources.EpicGames { try { - _meta_size = stream.read_uint32(); + _meta_size = stream.read_uint32(); _data_version = stream.read_byte(); // Usually same as manifest version, but can be different @@ -282,9 +255,9 @@ namespace GameHub.Data.Sources.EpicGames // 0 for most apps, generally not used _app_id = stream.read_uint32(); - _app_name = read_fstring(stream); - _build_version = read_fstring(stream); - _launch_exe = read_fstring(stream); + _app_name = read_fstring(stream); + _build_version = read_fstring(stream); + _launch_exe = read_fstring(stream); _launch_command = read_fstring(stream); // This is a list though I've never seen more than one entry @@ -338,9 +311,9 @@ namespace GameHub.Data.Sources.EpicGames private HashMap? path_map = null; internal ArrayList elements { get; default = new ArrayList(); } - internal uint8 version { get; default = 0; } - internal uint32 count { get; default = 0; } - internal uint32 size { get; default = 0; } + internal uint8 version { get; default = 0; } + internal uint32 count { get; default = 0; } + internal uint32 size { get; default = 0; } internal FileManifestList.from_byte_stream(DataInputStream stream) { @@ -348,9 +321,9 @@ namespace GameHub.Data.Sources.EpicGames try { - _size = stream.read_uint32(); + _size = stream.read_uint32(); _version = stream.read_byte(); - _count = stream.read_uint32(); + _count = stream.read_uint32(); } catch (Error e) {} @@ -419,8 +392,7 @@ namespace GameHub.Data.Sources.EpicGames for(var i = 0; i < _count; i++) { - var chunk_part = new FileManifest.ChunkPart.from_byte_stream(stream, - offset); + var chunk_part = new FileManifest.ChunkPart.from_byte_stream(stream, offset); file_manifest.chunk_parts.add(chunk_part); offset += chunk_part.size; } @@ -459,22 +431,18 @@ namespace GameHub.Data.Sources.EpicGames var file_manifest_json = node.get_object(); - file_manifest.filename = file_manifest_json.get_string_member_with_default("Filename", - ""); + file_manifest.filename = file_manifest_json.get_string_member_with_default("Filename", ""); try { - var hash = file_manifest_json.get_string_member("FileHash"); // 20 bytes as %03d number string + var hash = file_manifest_json.get_string_member("FileHash"); // 20 bytes as %03d number string file_manifest.hash = number_string_to_byte_stream(hash).read_bytes(20); } catch (Error e) { debug("error: %s", e.message); } - file_manifest.flags |= (int) file_manifest_json.get_boolean_member_with_default("bIsReadOnly", - false); - file_manifest.flags |= (int) file_manifest_json.get_boolean_member_with_default("bIsCompressed", - false) << 1; - file_manifest.flags |= (int) file_manifest_json.get_boolean_member_with_default("bIsUnixExecutable", - false) << 2; + file_manifest.flags |= (int) file_manifest_json.get_boolean_member_with_default("bIsReadOnly", false); + file_manifest.flags |= (int) file_manifest_json.get_boolean_member_with_default("bIsCompressed", false) << 1; + file_manifest.flags |= (int) file_manifest_json.get_boolean_member_with_default("bIsUnixExecutable", false) << 2; if(file_manifest_json.has_member("InstallTags")) { @@ -486,8 +454,7 @@ namespace GameHub.Data.Sources.EpicGames var offset = 0; file_manifest_json.get_array_member("FileChunkParts").foreach_element((a, i, n) => { - var chunk_part = new FileManifest.ChunkPart.from_json(n, - offset); + var chunk_part = new FileManifest.ChunkPart.from_json(n, offset); file_manifest.file_size += chunk_part.size; // TODO: not read keys @@ -511,8 +478,7 @@ namespace GameHub.Data.Sources.EpicGames for(var i = 0; i < elements.size; i++) { - path_map.set(elements.get(i).filename, - i); + path_map.set(elements.get(i).filename, i); } } @@ -550,13 +516,13 @@ namespace GameHub.Data.Sources.EpicGames */ internal class FileManifest { - internal ArrayList chunk_parts { get; default = new ArrayList(); } - internal ArrayList install_tags { get; default = new ArrayList(); } - internal bool compressed { get { return (flags & 0x2) == 0x2; } } - internal bool executable { get { return (flags & 0x4) == 0x4; } } - internal bool read_only { get { return (flags & 0x1) == 0x1; } } - internal Bytes hash { get; set; default = new Bytes(null); } - internal Bytes sha_hash { get { return hash; } } + internal ArrayList chunk_parts { get; default = new ArrayList(); } + internal ArrayList install_tags { get; default = new ArrayList(); } + internal bool compressed { get { return (flags & 0x2) == 0x2; } } + internal bool executable { get { return (flags & 0x4) == 0x4; } } + internal bool read_only { get { return (flags & 0x1) == 0x1; } } + internal Bytes hash { get; set; default = new Bytes(null); } + internal Bytes sha_hash { get { return hash; } } internal uchar flags { get; set; default = 0; } internal uint32 file_size { get; set; default = 0; } internal string filename { get; set; default = ""; } @@ -567,7 +533,7 @@ namespace GameHub.Data.Sources.EpicGames internal string to_string() { - var tag_string = ""; + var tag_string = ""; var chunk_string = ""; foreach(var tag in install_tags) @@ -604,13 +570,13 @@ namespace GameHub.Data.Sources.EpicGames internal class ChunkPart { internal uint32 file_offset { get; default = 0; } - internal uint32 offset { get; default = 0; } - internal uint32 size { get; default = 0; } - internal uint32[] guid { get; default = new uint32[4]; } + internal uint32 offset { get; default = 0; } + internal uint32 size { get; default = 0; } + internal uint32[] guid { get; default = new uint32[4]; } // caches for things that are "expensive" to compute private string? _guid_str = null; - private uint32? _guid_num = null; + private uint32? _guid_num = null; internal string guid_str { @@ -638,19 +604,18 @@ namespace GameHub.Data.Sources.EpicGames } } - private ChunkPart(uint32[] guid = new uint32[4], - uint32 offset = 0, - uint32 size = 0, + private ChunkPart(uint32[] guid = new uint32[4], + uint32 offset = 0, + uint32 size = 0, uint32 file_offset = 0) { - _guid = guid; - _offset = offset; - _size = size; + _guid = guid; + _offset = offset; + _size = size; _file_offset = file_offset; } - internal ChunkPart.from_byte_stream(DataInputStream stream, - uint32 offset) + internal ChunkPart.from_byte_stream(DataInputStream stream, uint32 offset) { var start = stream.tell(); @@ -663,8 +628,8 @@ namespace GameHub.Data.Sources.EpicGames _guid[j] = stream.read_uint32(); } - _offset = stream.read_uint32(); - _size = stream.read_uint32(); + _offset = stream.read_uint32(); + _size = stream.read_uint32(); _file_offset = offset; var diff = stream.tell() - start - size; @@ -672,30 +637,27 @@ namespace GameHub.Data.Sources.EpicGames if(diff > 0) { warning(@"[Sources.EpicGames.Manifest.ChunkPart.from_byte_stream] Did not read $diff bytes from chunk part!"); - stream.seek(diff, - SeekType.SET); + stream.seek(diff, SeekType.SET); } } catch (Error e) { - debug("[ChunkPart.from_byte_stream] error: %s", - e.message); + debug("[ChunkPart.from_byte_stream] error: %s", e.message); } if(log_chunk_part) debug(to_string()); } - internal ChunkPart.from_json(Json.Node json, - uint32 offset) + internal ChunkPart.from_json(Json.Node json, uint32 offset) { assert(json.get_node_type() == Json.NodeType.OBJECT); uint32 chunk_offset = 0; - uint32 chunk_size = 0; + uint32 chunk_size = 0; try { chunk_offset = number_string_to_byte_stream(json.get_object().get_string_member("Offset")).read_uint32(); - chunk_size = number_string_to_byte_stream(json.get_object().get_string_member("Size")).read_uint32(); + chunk_size = number_string_to_byte_stream(json.get_object().get_string_member("Size")).read_uint32(); } catch (Error e) { debug("error: %s", e.message); } @@ -723,29 +685,28 @@ namespace GameHub.Data.Sources.EpicGames internal class ChunkDataList { private uint8 version { get; } - private uint32 manifest_version { get; } - private uint32 size { get; } - private uint32 count { get; } - Json.Object chunk_filesize_list; // FIXME: - Json.Object chunk_hash_list; // FIXME: - Json.Object chunk_sha_list; // FIXME: - Json.Object data_group_list; // FIXME: + private uint32 manifest_version { get; } + private uint32 size { get; } + private uint32 count { get; } + Json.Object chunk_filesize_list; // FIXME: + Json.Object chunk_hash_list; // FIXME: + Json.Object chunk_sha_list; // FIXME: + Json.Object data_group_list; // FIXME: private HashMap guid_int_map { get; default = new HashMap(); } private HashMap guid_str_map { get; default = new HashMap(); } internal ArrayList elements { get; default = new ArrayList(); } - internal ChunkDataList.from_byte_stream(DataInputStream stream, - uint32 manifest_version = 18) + internal ChunkDataList.from_byte_stream(DataInputStream stream, uint32 manifest_version = 18) { var start = stream.tell(); _manifest_version = manifest_version; try { - _size = stream.read_uint32(); + _size = stream.read_uint32(); _version = stream.read_byte(); - _count = stream.read_uint32(); + _count = stream.read_uint32(); // the way this data is stored is rather odd, maybe there's a nicer way to write this… for(var i = 0; i < count; i++) @@ -828,17 +789,16 @@ namespace GameHub.Data.Sources.EpicGames if(log_chunk_data_list) debug(to_string()); } - internal ChunkDataList.from_json(Json.Node json_data, - uint32 manifest_version = 13) + internal ChunkDataList.from_json(Json.Node json_data, uint32 manifest_version = 13) { var json_obj = json_data.get_object(); - _manifest_version = manifest_version; - _count = json_obj.get_object_member("ChunkFilesizeList").get_size(); + _manifest_version = manifest_version; + _count = json_obj.get_object_member("ChunkFilesizeList").get_size(); chunk_filesize_list = json_obj.get_object_member("ChunkFilesizeList"); - chunk_hash_list = json_obj.get_object_member("ChunkHashList"); - chunk_sha_list = json_obj.get_object_member("ChunkShaList"); - data_group_list = json_obj.get_object_member("DataGroupList"); + chunk_hash_list = json_obj.get_object_member("ChunkHashList"); + chunk_sha_list = json_obj.get_object_member("ChunkShaList"); + data_group_list = json_obj.get_object_member("DataGroupList"); chunk_filesize_list.get_members().foreach(guid => { @@ -865,18 +825,17 @@ namespace GameHub.Data.Sources.EpicGames } /** - * Get chunk by GUID number, creates index of chunks on first call - * - * Integer GUIDs are usually faster and require less memory, use those when possible. - */ + * Get chunk by GUID number, creates index of chunks on first call + * + * Integer GUIDs are usually faster and require less memory, use those when possible. + */ internal ChunkInfo? get_chunk_by_number(uint32 guid) { if(_guid_int_map.is_empty) { for(var i = 0; i < _elements.size; i++) { - _guid_int_map.set(_elements.get(i).guid_num, - i); + _guid_int_map.set(_elements.get(i).guid_num, i); } } @@ -900,8 +859,7 @@ namespace GameHub.Data.Sources.EpicGames { for(var i = 0; i < _elements.size; i++) { - _guid_str_map.set(_elements.get(i).guid_str, - i); + _guid_str_map.set(_elements.get(i).guid_str, i); } } @@ -943,17 +901,17 @@ namespace GameHub.Data.Sources.EpicGames */ internal class ChunkInfo { - internal Bytes sha_hash { get; set; default = new Bytes(null); } - internal int64 file_size { get; set; default = 0; } - internal uint32[] guid { get; set; default = new uint32[4]; } + internal Bytes sha_hash { get; set; default = new Bytes(null); } + internal int64 file_size { get; set; default = 0; } + internal uint32[] guid { get; set; default = new uint32[4]; } internal uint32 manifest_version { get; set; } - internal uint32 window_size { get; set; default = 0; } - internal uint64 hash { get; set; default = 0; } + internal uint32 window_size { get; set; default = 0; } + internal uint64 hash { get; set; default = 0; } // caches for things that are "expensive" to compute - private ulong? _group_num = null; + private ulong? _group_num = null; private string? _guid_str = null; - private uint32? _guid_num = null; + private uint32? _guid_num = null; internal string guid_str { @@ -996,8 +954,7 @@ namespace GameHub.Data.Sources.EpicGames bytes.append(variant.get_data_as_bytes().get_data()); } - _group_num = (ZLib.Utility.crc32(0, - bytes.data) & 0xffffffff) % 100; + _group_num = (ZLib.Utility.crc32(0, bytes.data) & 0xffffffff) % 100; } return _group_num; @@ -1012,11 +969,10 @@ namespace GameHub.Data.Sources.EpicGames { owned get { - return "%s/%02lu/%016llX_%s.chunk".printf( - get_chunk_dir(), - group_num, - hash, - guid_to_string(guid)); + return "%s/%02lu/%016llX_%s.chunk".printf(get_chunk_dir(), + group_num, + hash, + guid_to_string(guid)); } } @@ -1082,10 +1038,7 @@ namespace GameHub.Data.Sources.EpicGames // TODO: CharsetConverter oconverter = new CharsetConverter ("utf-16", "utf-8"); // variant = new Variant.from_bytes(VariantType.STRING, stream.read_bytes(length), false); // debug("[Sources.EpicGames.Manifest.read_fstring] string utf-16: %s", variant.get_string()); - result = convert((string) stream.read_bytes(length), - -1, - "UTF-8", - "UTF-16"); // convert to utf8 + result = convert((string) stream.read_bytes(length), -1, "UTF-8", "UTF-16"); // convert to utf8 // debug("[Sources.EpicGames.Manifest.read_fstring] string utf-8: %s", result); // stream.seek(2, GLib.SeekType.CUR); // utf-16 strings have two byte null terminators // TODO: seek +1 for second null char? @@ -1117,13 +1070,12 @@ namespace GameHub.Data.Sources.EpicGames */ internal class ManifestComparison { - internal ArrayList added { get; default = new ArrayList(); } - internal ArrayList removed { get; default = new ArrayList(); } - internal ArrayList changed { get; default = new ArrayList(); } + internal ArrayList added { get; default = new ArrayList(); } + internal ArrayList removed { get; default = new ArrayList(); } + internal ArrayList changed { get; default = new ArrayList(); } internal ArrayList unchanged { get; default = new ArrayList(); } - internal ManifestComparison(Manifest new_manifest, - Manifest? old_manifest = null) + internal ManifestComparison(Manifest new_manifest, Manifest? old_manifest = null) { if(old_manifest == null) { @@ -1139,8 +1091,7 @@ namespace GameHub.Data.Sources.EpicGames foreach(var file_manifest in old_manifest.file_manifest_list.elements) { - old_files.set(file_manifest.filename, - file_manifest.hash); + old_files.set(file_manifest.filename, file_manifest.hash); } foreach(var file_manifest in new_manifest.file_manifest_list.elements) @@ -1149,8 +1100,7 @@ namespace GameHub.Data.Sources.EpicGames if(old_files.has_key(file_manifest.filename)) { - old_files.unset(file_manifest.filename, - out old_file_hash); + old_files.unset(file_manifest.filename, out old_file_hash); } if(old_file_hash != null) diff --git a/src/data/sources/epicgames/EpicUtils.vala b/src/data/sources/epicgames/EpicUtils.vala index c1f9adc0..9a7dea9e 100644 --- a/src/data/sources/epicgames/EpicUtils.vala +++ b/src/data/sources/epicgames/EpicUtils.vala @@ -14,8 +14,7 @@ namespace GameHub.Data.Sources.EpicGames foreach(var byte in bytes) { - builder.append_printf("%02x", - byte); + builder.append_printf("%02x", byte); } return builder.str; @@ -37,9 +36,7 @@ namespace GameHub.Data.Sources.EpicGames for(var i = 0; i < str.length; i += 3) { int segment = 0; - str.substring(i, - 3).scanf("%03hu", - out segment); + str.substring(i, 3).scanf("%03hu", out segment); bytes.append({ (uint8) segment }); } @@ -60,9 +57,7 @@ namespace GameHub.Data.Sources.EpicGames for(var i = 0; i < str.length; i += 2) { int segment = 0; - str.substring(i, - 2).scanf("%02X", - out segment); + str.substring(i, 2).scanf("%02X", out segment); bytes.append({ (uint8) segment }); } @@ -78,7 +73,7 @@ namespace GameHub.Data.Sources.EpicGames requires(str.length == 32) { uint32[] result = new uint32[4]; - var stream = hex_string_to_byte_stream(str); + var stream = hex_string_to_byte_stream(str); stream.set_byte_order(DataStreamByteOrder.BIG_ENDIAN); for(var i = 0; i < 4; i++) @@ -89,8 +84,7 @@ namespace GameHub.Data.Sources.EpicGames } catch (Error e) { - debug("error: %s", - e.message); + debug("error: %s", e.message); } } @@ -106,8 +100,7 @@ namespace GameHub.Data.Sources.EpicGames foreach(var id in guid) { - builder.append_printf("%08X", - id); + builder.append_printf("%08X", id); } return builder.str; @@ -121,13 +114,11 @@ namespace GameHub.Data.Sources.EpicGames foreach(var id in guid) { - builder.append_printf("%08x-", - id); + builder.append_printf("%08x-", id); } // strip last "-" - return builder.str.substring(0, - builder.str.length - 1); + return builder.str.substring(0, builder.str.length - 1); } private static uint32 guid_to_number(uint32[] guid) { return guid[3] + (guid[2] << 32) + (guid[1] << 64) + (guid[0] << 96); } @@ -135,37 +126,30 @@ namespace GameHub.Data.Sources.EpicGames private static string uppercase_first_character(string str) { // Uppercase first character - var builder = new StringBuilder(str); - var i = 0; + var builder = new StringBuilder(str); + var i = 0; unichar c; - str.get_next_char(ref i, - out c); - builder.overwrite(0, - c.to_string().up()); + + str.get_next_char(ref i, out c); + builder.overwrite(0, c.to_string().up()); // debug("[Sources.EpicGames.Utils.uppercase] %s → %s", str, builder.str); return builder.str; } // TODO: replace with FileUtils.set_data() ? - private static void write(string path, - string name, - uint8[] bytes) + private static void write(string path, string name, uint8[] bytes) { - var file = FS.file(path, - name); + var file = FS.file(path, name); try { FS.mkdir(path); - FileUtils.set_data(file.get_path(), - bytes); + FileUtils.set_data(file.get_path(), bytes); } catch (Error e) { - warning("[Sources.EpicGames.write] Error writing `%s`: %s", - file.get_path(), - e.message); + warning("[Sources.EpicGames.write] Error writing `%s`: %s", file.get_path(), e.message); } } } diff --git a/uncrustify.cfg b/uncrustify.cfg new file mode 100644 index 00000000..22969a72 --- /dev/null +++ b/uncrustify.cfg @@ -0,0 +1,3128 @@ +# Uncrustify_d-0.72.0_f + +# +# General options +# + +# The type of line endings. +# +# Default: auto +newlines = auto # lf/crlf/cr/auto + +# The original size of tabs in the input. +# +# Default: 8 +input_tab_size = 8 # unsigned number + +# The size of tabs in the output (only used if align_with_tabs=true). +# +# Default: 8 +output_tab_size = 8 # unsigned number + +# The ASCII value of the string escape char, usually 92 (\) or (Pawn) 94 (^). +# +# Default: 92 +string_escape_char = 92 # unsigned number + +# Alternate string escape char (usually only used for Pawn). +# Only works right before the quote char. +string_escape_char2 = 0 # unsigned number + +# Replace tab characters found in string literals with the escape sequence \t +# instead. +string_replace_tab_chars = false # true/false + +# Allow interpreting '>=' and '>>=' as part of a template in code like +# 'void f(list>=val);'. If true, 'assert(x<0 && y>=3)' will be broken. +# Improvements to template detection may make this option obsolete. +tok_split_gte = false # true/false + +# Disable formatting of NL_CONT ('\\n') ended lines (e.g. multiline macros) +disable_processing_nl_cont = false # true/false + +# Specify the marker used in comments to disable processing of part of the +# file. +# The comment should be used alone in one line. +# +# Default: *INDENT-OFF* +disable_processing_cmt = " *INDENT-OFF*" # string + +# Specify the marker used in comments to (re)enable processing in a file. +# The comment should be used alone in one line. +# +# Default: *INDENT-ON* +enable_processing_cmt = " *INDENT-ON*" # string + +# Enable parsing of digraphs. +enable_digraphs = false # true/false + +# Add or remove the UTF-8 BOM (recommend 'remove'). +utf8_bom = ignore # ignore/add/remove/force + +# If the file contains bytes with values between 128 and 255, but is not +# UTF-8, then output as UTF-8. +utf8_byte = false # true/false + +# Force the output encoding to UTF-8. +utf8_force = false # true/false + +# Add or remove space between 'do' and '{'. +sp_do_brace_open = add # ignore/add/remove/force + +# Add or remove space between '}' and 'while'. +sp_brace_close_while = add # ignore/add/remove/force + +# Add or remove space between 'while' and '('. +sp_while_paren_open = add # ignore/add/remove/force + +# +# Spacing options +# + +# Add or remove space around non-assignment symbolic operators ('+', '/', '%', +# '<<', and so forth). +sp_arith = add # ignore/add/remove/force + +# Add or remove space around arithmetic operators '+' and '-'. +# +# Overrides sp_arith. +sp_arith_additive = add # ignore/add/remove/force + +# Add or remove space around assignment operator '=', '+=', etc. +sp_assign = add # ignore/add/remove/force + +# Add or remove space around '=' in C++11 lambda capture specifications. +# +# Overrides sp_assign. +sp_cpp_lambda_assign = ignore # ignore/add/remove/force + +# Add or remove space after the capture specification of a C++11 lambda when +# an argument list is present, as in '[] (int x){ ... }'. +sp_cpp_lambda_square_paren = ignore # ignore/add/remove/force + +# Add or remove space after the capture specification of a C++11 lambda with +# no argument list is present, as in '[] { ... }'. +sp_cpp_lambda_square_brace = ignore # ignore/add/remove/force + +# Add or remove space after the argument list of a C++11 lambda, as in +# '[](int x) { ... }'. +sp_cpp_lambda_paren_brace = ignore # ignore/add/remove/force + +# Add or remove space between a lambda body and its call operator of an +# immediately invoked lambda, as in '[]( ... ){ ... } ( ... )'. +sp_cpp_lambda_fparen = ignore # ignore/add/remove/force + +# Add or remove space around assignment operator '=' in a prototype. +# +# If set to ignore, use sp_assign. +sp_assign_default = ignore # ignore/add/remove/force + +# Add or remove space before assignment operator '=', '+=', etc. +# +# Overrides sp_assign. +sp_before_assign = ignore # ignore/add/remove/force + +# Add or remove space after assignment operator '=', '+=', etc. +# +# Overrides sp_assign. +sp_after_assign = ignore # ignore/add/remove/force + +# Add or remove space in 'NS_ENUM ('. +sp_enum_paren = ignore # ignore/add/remove/force + +# Add or remove space around assignment '=' in enum. +sp_enum_assign = ignore # ignore/add/remove/force + +# Add or remove space before assignment '=' in enum. +# +# Overrides sp_enum_assign. +sp_enum_before_assign = ignore # ignore/add/remove/force + +# Add or remove space after assignment '=' in enum. +# +# Overrides sp_enum_assign. +sp_enum_after_assign = ignore # ignore/add/remove/force + +# Add or remove space around assignment ':' in enum. +sp_enum_colon = ignore # ignore/add/remove/force + +# Add or remove space around preprocessor '##' concatenation operator. +# +# Default: add +sp_pp_concat = add # ignore/add/remove/force + +# Add or remove space after preprocessor '#' stringify operator. +# Also affects the '#@' charizing operator. +sp_pp_stringify = ignore # ignore/add/remove/force + +# Add or remove space before preprocessor '#' stringify operator +# as in '#define x(y) L#y'. +sp_before_pp_stringify = ignore # ignore/add/remove/force + +# Add or remove space around boolean operators '&&' and '||'. +sp_bool = add # ignore/add/remove/force + +# Add or remove space around compare operator '<', '>', '==', etc. +sp_compare = add # ignore/add/remove/force + +# Add or remove space inside '(' and ')'. +sp_inside_paren = remove # ignore/add/remove/force + +# Add or remove space between nested parentheses, i.e. '((' vs. ') )'. +sp_paren_paren = remove # ignore/add/remove/force + +# Add or remove space between back-to-back parentheses, i.e. ')(' vs. ') ('. +sp_cparen_oparen = ignore # ignore/add/remove/force + +# Whether to balance spaces inside nested parentheses. +sp_balance_nested_parens = false # true/false + +# Add or remove space between ')' and '{'. +sp_paren_brace = add # ignore/add/remove/force + +# Add or remove space between nested braces, i.e. '{{' vs '{ {'. +sp_brace_brace = remove # ignore/add/remove/force + +# Add or remove space before pointer star '*'. +sp_before_ptr_star = ignore # ignore/add/remove/force + +# Add or remove space before pointer star '*' that isn't followed by a +# variable name. If set to ignore, sp_before_ptr_star is used instead. +sp_before_unnamed_ptr_star = ignore # ignore/add/remove/force + +# Add or remove space between pointer stars '*'. +sp_between_ptr_star = ignore # ignore/add/remove/force + +# Add or remove space after pointer star '*', if followed by a word. +# +# Overrides sp_type_func. +sp_after_ptr_star = ignore # ignore/add/remove/force + +# Add or remove space after pointer caret '^', if followed by a word. +sp_after_ptr_block_caret = ignore # ignore/add/remove/force + +# Add or remove space after pointer star '*', if followed by a qualifier. +sp_after_ptr_star_qualifier = ignore # ignore/add/remove/force + +# Add or remove space after a pointer star '*', if followed by a function +# prototype or function definition. +# +# Overrides sp_after_ptr_star and sp_type_func. +sp_after_ptr_star_func = ignore # ignore/add/remove/force + +# Add or remove space after a pointer star '*', if followed by an open +# parenthesis, as in 'void* (*)(). +sp_ptr_star_paren = ignore # ignore/add/remove/force + +# Add or remove space before a pointer star '*', if followed by a function +# prototype or function definition. +sp_before_ptr_star_func = ignore # ignore/add/remove/force + +# Add or remove space before a reference sign '&'. +sp_before_byref = add # ignore/add/remove/force + +# Add or remove space before a reference sign '&' that isn't followed by a +# variable name. If set to ignore, sp_before_byref is used instead. +sp_before_unnamed_byref = ignore # ignore/add/remove/force + +# Add or remove space after reference sign '&', if followed by a word. +# +# Overrides sp_type_func. +sp_after_byref = remove # ignore/add/remove/force + +# Add or remove space after a reference sign '&', if followed by a function +# prototype or function definition. +# +# Overrides sp_after_byref and sp_type_func. +sp_after_byref_func = ignore # ignore/add/remove/force + +# Add or remove space before a reference sign '&', if followed by a function +# prototype or function definition. +sp_before_byref_func = ignore # ignore/add/remove/force + +# Add or remove space between type and word. In cases where total removal of +# whitespace would be a syntax error, a value of 'remove' is treated the same +# as 'force'. +# +# This also affects some other instances of space following a type that are +# not covered by other options; for example, between the return type and +# parenthesis of a function type template argument, between the type and +# parenthesis of an array parameter, or between 'decltype(...)' and the +# following word. +# +# Default: force +sp_after_type = force # ignore/add/remove/force + +# Add or remove space between 'decltype(...)' and word. +# +# Overrides sp_after_type. +sp_after_decltype = ignore # ignore/add/remove/force + +# (D) Add or remove space before the parenthesis in the D constructs +# 'template Foo(' and 'class Foo('. +sp_before_template_paren = ignore # ignore/add/remove/force + +# Add or remove space between 'template' and '<'. +# If set to ignore, sp_before_angle is used. +sp_template_angle = ignore # ignore/add/remove/force + +# Add or remove space before '<'. +sp_before_angle = ignore # ignore/add/remove/force + +# Add or remove space inside '<' and '>'. +sp_inside_angle = ignore # ignore/add/remove/force + +# Add or remove space inside '<>'. +sp_inside_angle_empty = ignore # ignore/add/remove/force + +# Add or remove space between '>' and ':'. +sp_angle_colon = ignore # ignore/add/remove/force + +# Add or remove space after '>'. +sp_after_angle = ignore # ignore/add/remove/force + +# Add or remove space between '>' and '(' as found in 'new List(foo);'. +sp_angle_paren = remove # ignore/add/remove/force + +# Add or remove space between '>' and '()' as found in 'new List();'. +sp_angle_paren_empty = remove # ignore/add/remove/force + +# Add or remove space between '>' and a word as in 'List m;' or +# 'template static ...'. +sp_angle_word = ignore # ignore/add/remove/force + +# Add or remove space between '>' and '>' in '>>' (template stuff). +# +# Default: add +sp_angle_shift = ignore # ignore/add/remove/force + +# (C++11) Permit removal of the space between '>>' in 'foo >'. Note +# that sp_angle_shift cannot remove the space without this option. +sp_permit_cpp11_shift = true # true/false + +# Add or remove space before '(' of control statements ('if', 'for', 'switch', +# 'while', etc.). +sp_before_sparen = remove # ignore/add/remove/force + +# Add or remove space inside '(' and ')' of control statements. +sp_inside_sparen = add # ignore/add/remove/force + +# Add or remove space after '(' of control statements. +# +# Overrides sp_inside_sparen. +sp_inside_sparen_open = remove # ignore/add/remove/force + +# Add or remove space before ')' of control statements. +# +# Overrides sp_inside_sparen. +sp_inside_sparen_close = remove # ignore/add/remove/force + +# Add or remove space after ')' of control statements. +sp_after_sparen = add # ignore/add/remove/force + +# Add or remove space between ')' and '{' of of control statements. +sp_sparen_brace = add # ignore/add/remove/force + +# (D) Add or remove space between 'invariant' and '('. +sp_invariant_paren = ignore # ignore/add/remove/force + +# (D) Add or remove space after the ')' in 'invariant (C) c'. +sp_after_invariant_paren = ignore # ignore/add/remove/force + +# Add or remove space before empty statement ';' on 'if', 'for' and 'while'. +sp_special_semi = remove # ignore/add/remove/force + +# Add or remove space before ';'. +# +# Default: remove +sp_before_semi = remove # ignore/add/remove/force + +# Add or remove space before ';' in non-empty 'for' statements. +sp_before_semi_for = remove # ignore/add/remove/force + +# Add or remove space before a semicolon of an empty part of a for statement. +sp_before_semi_for_empty = ignore # ignore/add/remove/force + +# Add or remove space after ';', except when followed by a comment. +# +# Default: add +sp_after_semi = add # ignore/add/remove/force + +# Add or remove space after ';' in non-empty 'for' statements. +# +# Default: force +sp_after_semi_for = force # ignore/add/remove/force + +# Add or remove space after the final semicolon of an empty part of a for +# statement, as in 'for ( ; ; )'. +sp_after_semi_for_empty = ignore # ignore/add/remove/force + +# Add or remove space before '[' (except '[]'). +sp_before_square = ignore # ignore/add/remove/force + +# Add or remove space before '[' for a variable definition. +# +# Default: remove +sp_before_vardef_square = remove # ignore/add/remove/force + +# Add or remove space before '[' for asm block. +sp_before_square_asm_block = ignore # ignore/add/remove/force + +# Add or remove space before '[]'. +sp_before_squares = ignore # ignore/add/remove/force + +# Add or remove space before C++17 structured bindings. +sp_cpp_before_struct_binding = ignore # ignore/add/remove/force + +# Add or remove space inside a non-empty '[' and ']'. +sp_inside_square = ignore # ignore/add/remove/force + +# Add or remove space inside '[]'. +sp_inside_square_empty = ignore # ignore/add/remove/force + +# (OC) Add or remove space inside a non-empty Objective-C boxed array '@[' and +# ']'. If set to ignore, sp_inside_square is used. +sp_inside_square_oc_array = ignore # ignore/add/remove/force + +# Add or remove space after ',', i.e. 'a,b' vs. 'a, b'. +sp_after_comma = force # ignore/add/remove/force + +# Add or remove space before ','. +# +# Default: remove +sp_before_comma = remove # ignore/add/remove/force + +# (C#) Add or remove space between ',' and ']' in multidimensional array type +# like 'int[,,]'. +sp_after_mdatype_commas = ignore # ignore/add/remove/force + +# (C#) Add or remove space between '[' and ',' in multidimensional array type +# like 'int[,,]'. +sp_before_mdatype_commas = ignore # ignore/add/remove/force + +# (C#) Add or remove space between ',' in multidimensional array type +# like 'int[,,]'. +sp_between_mdatype_commas = ignore # ignore/add/remove/force + +# Add or remove space between an open parenthesis and comma, +# i.e. '(,' vs. '( ,'. +# +# Default: force +sp_paren_comma = force # ignore/add/remove/force + +# Add or remove space before the variadic '...' when preceded by a +# non-punctuator. +sp_before_ellipsis = ignore # ignore/add/remove/force + +# Add or remove space between a type and '...'. +sp_type_ellipsis = ignore # ignore/add/remove/force + +# (D) Add or remove space between a type and '?'. +sp_type_question = remove # ignore/add/remove/force + +# Add or remove space between ')' and '...'. +sp_paren_ellipsis = ignore # ignore/add/remove/force + +# Add or remove space between ')' and a qualifier such as 'const'. +sp_paren_qualifier = ignore # ignore/add/remove/force + +# Add or remove space between ')' and 'noexcept'. +sp_paren_noexcept = ignore # ignore/add/remove/force + +# Add or remove space after class ':'. +sp_after_class_colon = ignore # ignore/add/remove/force + +# Add or remove space before class ':'. +sp_before_class_colon = remove # ignore/add/remove/force + +# Add or remove space after class constructor ':'. +sp_after_constr_colon = ignore # ignore/add/remove/force + +# Add or remove space before class constructor ':'. +sp_before_constr_colon = ignore # ignore/add/remove/force + +# Add or remove space before case ':'. +# +# Default: remove +sp_before_case_colon = remove # ignore/add/remove/force + +# Add or remove space between 'operator' and operator sign. +sp_after_operator = ignore # ignore/add/remove/force + +# Add or remove space between the operator symbol and the open parenthesis, as +# in 'operator ++('. +sp_after_operator_sym = ignore # ignore/add/remove/force + +# Overrides sp_after_operator_sym when the operator has no arguments, as in +# 'operator *()'. +sp_after_operator_sym_empty = ignore # ignore/add/remove/force + +# Add or remove space after C/D cast, i.e. 'cast(int)a' vs. 'cast(int) a' or +# '(int)a' vs. '(int) a'. +sp_after_cast = add # ignore/add/remove/force + +# Add or remove spaces inside cast parentheses. +sp_inside_paren_cast = ignore # ignore/add/remove/force + +# Add or remove space between the type and open parenthesis in a C++ cast, +# i.e. 'int(exp)' vs. 'int (exp)'. +sp_cpp_cast_paren = ignore # ignore/add/remove/force + +# Add or remove space between 'sizeof' and '('. +sp_sizeof_paren = ignore # ignore/add/remove/force + +# Add or remove space between 'sizeof' and '...'. +sp_sizeof_ellipsis = ignore # ignore/add/remove/force + +# Add or remove space between 'sizeof...' and '('. +sp_sizeof_ellipsis_paren = ignore # ignore/add/remove/force + +# Add or remove space between 'decltype' and '('. +sp_decltype_paren = ignore # ignore/add/remove/force + +# (Pawn) Add or remove space after the tag keyword. +sp_after_tag = ignore # ignore/add/remove/force + +# Add or remove space inside enum '{' and '}'. +sp_inside_braces_enum = ignore # ignore/add/remove/force + +# Add or remove space inside struct/union '{' and '}'. +sp_inside_braces_struct = ignore # ignore/add/remove/force + +# (OC) Add or remove space inside Objective-C boxed dictionary '{' and '}' +sp_inside_braces_oc_dict = ignore # ignore/add/remove/force + +# Add or remove space after open brace in an unnamed temporary +# direct-list-initialization. +sp_after_type_brace_init_lst_open = ignore # ignore/add/remove/force + +# Add or remove space before close brace in an unnamed temporary +# direct-list-initialization. +sp_before_type_brace_init_lst_close = ignore # ignore/add/remove/force + +# Add or remove space inside an unnamed temporary direct-list-initialization. +sp_inside_type_brace_init_lst = ignore # ignore/add/remove/force + +# Add or remove space inside '{' and '}'. +sp_inside_braces = add # ignore/add/remove/force + +# Add or remove space inside '{}'. +sp_inside_braces_empty = remove # ignore/add/remove/force + +# Add or remove space around trailing return operator '->'. +sp_trailing_return = ignore # ignore/add/remove/force + +# Add or remove space between return type and function name. A minimum of 1 +# is forced except for pointer return types. +sp_type_func = ignore # ignore/add/remove/force + +# Add or remove space between type and open brace of an unnamed temporary +# direct-list-initialization. +sp_type_brace_init_lst = ignore # ignore/add/remove/force + +# Add or remove space between function name and '(' on function declaration. +sp_func_proto_paren = remove # ignore/add/remove/force + +# Add or remove space between function name and '()' on function declaration +# without parameters. +sp_func_proto_paren_empty = remove # ignore/add/remove/force + +# Add or remove space between function name and '(' with a typedef specifier. +sp_func_type_paren = remove # ignore/add/remove/force + +# Add or remove space between alias name and '(' of a non-pointer function type typedef. +sp_func_def_paren = remove # ignore/add/remove/force + +# Add or remove space between function name and '()' on function definition +# without parameters. +sp_func_def_paren_empty = remove # ignore/add/remove/force + +# Add or remove space inside empty function '()'. +# Overrides sp_after_angle unless use_sp_after_angle_always is set to true. +sp_inside_fparens = remove # ignore/add/remove/force + +# Add or remove space inside function '(' and ')'. +sp_inside_fparen = remove # ignore/add/remove/force + +# Add or remove space inside the first parentheses in a function type, as in +# 'void (*x)(...)'. +sp_inside_tparen = ignore # ignore/add/remove/force + +# Add or remove space between the ')' and '(' in a function type, as in +# 'void (*x)(...)'. +sp_after_tparen_close = ignore # ignore/add/remove/force + +# Add or remove space between ']' and '(' when part of a function call. +sp_square_fparen = ignore # ignore/add/remove/force + +# Add or remove space between ')' and '{' of function. +sp_fparen_brace = add # ignore/add/remove/force + +# Add or remove space between ')' and '{' of a function call in object +# initialization. +# +# Overrides sp_fparen_brace. +sp_fparen_brace_initializer = ignore # ignore/add/remove/force + +# (Java) Add or remove space between ')' and '{{' of double brace initializer. +sp_fparen_dbrace = ignore # ignore/add/remove/force + +# Add or remove space between function name and '(' on function calls. +sp_func_call_paren = remove # ignore/add/remove/force + +# Add or remove space between function name and '()' on function calls without +# parameters. If set to ignore (the default), sp_func_call_paren is used. +sp_func_call_paren_empty = remove # ignore/add/remove/force + +# Add or remove space between the user function name and '(' on function +# calls. You need to set a keyword to be a user function in the config file, +# like: +# set func_call_user tr _ i18n +sp_func_call_user_paren = ignore # ignore/add/remove/force + +# Add or remove space inside user function '(' and ')'. +sp_func_call_user_inside_fparen = remove # ignore/add/remove/force + +# Add or remove space between nested parentheses with user functions, +# i.e. '((' vs. '( ('. +sp_func_call_user_paren_paren = remove # ignore/add/remove/force + +# Add or remove space between a constructor/destructor and the open +# parenthesis. +sp_func_class_paren = ignore # ignore/add/remove/force + +# Add or remove space between a constructor without parameters or destructor +# and '()'. +sp_func_class_paren_empty = ignore # ignore/add/remove/force + +# Add or remove space between 'return' and '('. +sp_return_paren = ignore # ignore/add/remove/force + +# Add or remove space between 'return' and '{'. +sp_return_brace = ignore # ignore/add/remove/force + +# Add or remove space between '__attribute__' and '('. +sp_attribute_paren = ignore # ignore/add/remove/force + +# Add or remove space between 'defined' and '(' in '#if defined (FOO)'. +sp_defined_paren = ignore # ignore/add/remove/force + +# Add or remove space between 'throw' and '(' in 'throw (something)'. +sp_throw_paren = add # ignore/add/remove/force + +# Add or remove space between 'throw' and anything other than '(' as in +# '@throw [...];'. +sp_after_throw = ignore # ignore/add/remove/force + +# Add or remove space between 'catch' and '(' in 'catch (something) { }'. +# If set to ignore, sp_before_sparen is used. +sp_catch_paren = add # ignore/add/remove/force + +# (OC) Add or remove space between '@catch' and '(' +# in '@catch (something) { }'. If set to ignore, sp_catch_paren is used. +sp_oc_catch_paren = ignore # ignore/add/remove/force + +# (OC) Add or remove space before Objective-C protocol list +# as in '@protocol Protocol' or '@interface MyClass : NSObject'. +sp_before_oc_proto_list = ignore # ignore/add/remove/force + +# (OC) Add or remove space between class name and '(' +# in '@interface className(categoryName):BaseClass' +sp_oc_classname_paren = ignore # ignore/add/remove/force + +# (D) Add or remove space between 'version' and '(' +# in 'version (something) { }'. If set to ignore, sp_before_sparen is used. +sp_version_paren = ignore # ignore/add/remove/force + +# (D) Add or remove space between 'scope' and '(' +# in 'scope (something) { }'. If set to ignore, sp_before_sparen is used. +sp_scope_paren = ignore # ignore/add/remove/force + +# Add or remove space between 'super' and '(' in 'super (something)'. +# +# Default: remove +sp_super_paren = remove # ignore/add/remove/force + +# Add or remove space between 'this' and '(' in 'this (something)'. +# +# Default: remove +sp_this_paren = remove # ignore/add/remove/force + +# Add or remove space between a macro name and its definition. +sp_macro = ignore # ignore/add/remove/force + +# Add or remove space between a macro function ')' and its definition. +sp_macro_func = ignore # ignore/add/remove/force + +# Add or remove space between 'else' and '{' if on the same line. +sp_else_brace = add # ignore/add/remove/force + +# Add or remove space between '}' and 'else' if on the same line. +sp_brace_else = add # ignore/add/remove/force + +# Add or remove space between '}' and the name of a typedef on the same line. +sp_brace_typedef = ignore # ignore/add/remove/force + +# Add or remove space before the '{' of a 'catch' statement, if the '{' and +# 'catch' are on the same line, as in 'catch (decl) {'. +sp_catch_brace = add # ignore/add/remove/force + +# (OC) Add or remove space before the '{' of a '@catch' statement, if the '{' +# and '@catch' are on the same line, as in '@catch (decl) {'. +# If set to ignore, sp_catch_brace is used. +sp_oc_catch_brace = ignore # ignore/add/remove/force + +# Add or remove space between '}' and 'catch' if on the same line. +sp_brace_catch = add # ignore/add/remove/force + +# (OC) Add or remove space between '}' and '@catch' if on the same line. +# If set to ignore, sp_brace_catch is used. +sp_oc_brace_catch = ignore # ignore/add/remove/force + +# Add or remove space between 'finally' and '{' if on the same line. +sp_finally_brace = add # ignore/add/remove/force + +# Add or remove space between '}' and 'finally' if on the same line. +sp_brace_finally = add # ignore/add/remove/force + +# Add or remove space between 'try' and '{' if on the same line. +sp_try_brace = add # ignore/add/remove/force + +# Add or remove space between get/set and '{' if on the same line. +sp_getset_brace = add # ignore/add/remove/force + +# Add or remove space between a variable and '{' for C++ uniform +# initialization. +sp_word_brace_init_lst = ignore # ignore/add/remove/force + +# Add or remove space between a variable and '{' for a namespace. +# +# Default: add +sp_word_brace_ns = add # ignore/add/remove/force + +# Add or remove space before the '::' operator. +sp_before_dc = ignore # ignore/add/remove/force + +# Add or remove space after the '::' operator. +sp_after_dc = ignore # ignore/add/remove/force + +# (D) Add or remove around the D named array initializer ':' operator. +sp_d_array_colon = ignore # ignore/add/remove/force + +# Add or remove space after the '!' (not) unary operator. +# +# Default: remove +sp_not = remove # ignore/add/remove/force + +# Add or remove space after the '~' (invert) unary operator. +# +# Default: remove +sp_inv = remove # ignore/add/remove/force + +# Add or remove space after the '&' (address-of) unary operator. This does not +# affect the spacing after a '&' that is part of a type. +# +# Default: remove +sp_addr = remove # ignore/add/remove/force + +# Add or remove space around the '.' or '->' operators. +# +# Default: remove +sp_member = remove # ignore/add/remove/force + +# Add or remove space after the '*' (dereference) unary operator. This does +# not affect the spacing after a '*' that is part of a type. +# +# Default: remove +sp_deref = remove # ignore/add/remove/force + +# Add or remove space after '+' or '-', as in 'x = -5' or 'y = +7'. +# +# Default: remove +sp_sign = remove # ignore/add/remove/force + +# Add or remove space between '++' and '--' the word to which it is being +# applied, as in '(--x)' or 'y++;'. +# +# Default: remove +sp_incdec = remove # ignore/add/remove/force + +# Add or remove space before a backslash-newline at the end of a line. +# +# Default: add +sp_before_nl_cont = add # ignore/add/remove/force + +# (OC) Add or remove space after the scope '+' or '-', as in '-(void) foo;' +# or '+(int) bar;'. +sp_after_oc_scope = ignore # ignore/add/remove/force + +# (OC) Add or remove space after the colon in message specs, +# i.e. '-(int) f:(int) x;' vs. '-(int) f: (int) x;'. +sp_after_oc_colon = ignore # ignore/add/remove/force + +# (OC) Add or remove space before the colon in message specs, +# i.e. '-(int) f: (int) x;' vs. '-(int) f : (int) x;'. +sp_before_oc_colon = ignore # ignore/add/remove/force + +# (OC) Add or remove space after the colon in immutable dictionary expression +# 'NSDictionary *test = @{@"foo" :@"bar"};'. +sp_after_oc_dict_colon = ignore # ignore/add/remove/force + +# (OC) Add or remove space before the colon in immutable dictionary expression +# 'NSDictionary *test = @{@"foo" :@"bar"};'. +sp_before_oc_dict_colon = ignore # ignore/add/remove/force + +# (OC) Add or remove space after the colon in message specs, +# i.e. '[object setValue:1];' vs. '[object setValue: 1];'. +sp_after_send_oc_colon = ignore # ignore/add/remove/force + +# (OC) Add or remove space before the colon in message specs, +# i.e. '[object setValue:1];' vs. '[object setValue :1];'. +sp_before_send_oc_colon = ignore # ignore/add/remove/force + +# (OC) Add or remove space after the (type) in message specs, +# i.e. '-(int)f: (int) x;' vs. '-(int)f: (int)x;'. +sp_after_oc_type = ignore # ignore/add/remove/force + +# (OC) Add or remove space after the first (type) in message specs, +# i.e. '-(int) f:(int)x;' vs. '-(int)f:(int)x;'. +sp_after_oc_return_type = ignore # ignore/add/remove/force + +# (OC) Add or remove space between '@selector' and '(', +# i.e. '@selector(msgName)' vs. '@selector (msgName)'. +# Also applies to '@protocol()' constructs. +sp_after_oc_at_sel = ignore # ignore/add/remove/force + +# (OC) Add or remove space between '@selector(x)' and the following word, +# i.e. '@selector(foo) a:' vs. '@selector(foo)a:'. +sp_after_oc_at_sel_parens = ignore # ignore/add/remove/force + +# (OC) Add or remove space inside '@selector' parentheses, +# i.e. '@selector(foo)' vs. '@selector( foo )'. +# Also applies to '@protocol()' constructs. +sp_inside_oc_at_sel_parens = ignore # ignore/add/remove/force + +# (OC) Add or remove space before a block pointer caret, +# i.e. '^int (int arg){...}' vs. ' ^int (int arg){...}'. +sp_before_oc_block_caret = ignore # ignore/add/remove/force + +# (OC) Add or remove space after a block pointer caret, +# i.e. '^int (int arg){...}' vs. '^ int (int arg){...}'. +sp_after_oc_block_caret = ignore # ignore/add/remove/force + +# (OC) Add or remove space between the receiver and selector in a message, +# as in '[receiver selector ...]'. +sp_after_oc_msg_receiver = ignore # ignore/add/remove/force + +# (OC) Add or remove space after '@property'. +sp_after_oc_property = ignore # ignore/add/remove/force + +# (OC) Add or remove space between '@synchronized' and the open parenthesis, +# i.e. '@synchronized(foo)' vs. '@synchronized (foo)'. +sp_after_oc_synchronized = ignore # ignore/add/remove/force + +# Add or remove space around the ':' in 'b ? t : f'. +sp_cond_colon = add # ignore/add/remove/force + +# Add or remove space before the ':' in 'b ? t : f'. +# +# Overrides sp_cond_colon. +sp_cond_colon_before = ignore # ignore/add/remove/force + +# Add or remove space after the ':' in 'b ? t : f'. +# +# Overrides sp_cond_colon. +sp_cond_colon_after = ignore # ignore/add/remove/force + +# Add or remove space around the '?' in 'b ? t : f'. +sp_cond_question = ignore # ignore/add/remove/force + +# Add or remove space before the '?' in 'b ? t : f'. +# +# Overrides sp_cond_question. +sp_cond_question_before = ignore # ignore/add/remove/force + +# Add or remove space after the '?' in 'b ? t : f'. +# +# Overrides sp_cond_question. +sp_cond_question_after = ignore # ignore/add/remove/force + +# In the abbreviated ternary form '(a ?: b)', add or remove space between '?' +# and ':'. +# +# Overrides all other sp_cond_* options. +sp_cond_ternary_short = ignore # ignore/add/remove/force + +# Fix the spacing between 'case' and the label. Only 'ignore' and 'force' make +# sense here. +sp_case_label = ignore # ignore/add/remove/force + +# (D) Add or remove space around the D '..' operator. +sp_range = ignore # ignore/add/remove/force + +# Add or remove space after ':' in a Java/C++11 range-based 'for', +# as in 'for (Type var : expr)'. +sp_after_for_colon = ignore # ignore/add/remove/force + +# Add or remove space before ':' in a Java/C++11 range-based 'for', +# as in 'for (Type var : expr)'. +sp_before_for_colon = ignore # ignore/add/remove/force + +# (D) Add or remove space between 'extern' and '(' as in 'extern (C)'. +sp_extern_paren = ignore # ignore/add/remove/force + +# Add or remove space after the opening of a C++ comment, +# i.e. '// A' vs. '//A'. +sp_cmt_cpp_start = ignore # ignore/add/remove/force + +# If true, space is added with sp_cmt_cpp_start will be added after doxygen +# sequences like '///', '///<', '//!' and '//!<'. +sp_cmt_cpp_doxygen = false # true/false + +# If true, space is added with sp_cmt_cpp_start will be added after Qt +# translator or meta-data comments like '//:', '//=', and '//~'. +sp_cmt_cpp_qttr = false # true/false + +# Add or remove space between #else or #endif and a trailing comment. +sp_endif_cmt = ignore # ignore/add/remove/force + +# Add or remove space after 'new', 'delete' and 'delete[]'. +sp_after_new = ignore # ignore/add/remove/force + +# Add or remove space between 'new' and '(' in 'new()'. +sp_between_new_paren = ignore # ignore/add/remove/force + +# Add or remove space between ')' and type in 'new(foo) BAR'. +sp_after_newop_paren = ignore # ignore/add/remove/force + +# Add or remove space inside parenthesis of the new operator +# as in 'new(foo) BAR'. +sp_inside_newop_paren = ignore # ignore/add/remove/force + +# Add or remove space after the open parenthesis of the new operator, +# as in 'new(foo) BAR'. +# +# Overrides sp_inside_newop_paren. +sp_inside_newop_paren_open = ignore # ignore/add/remove/force + +# Add or remove space before the close parenthesis of the new operator, +# as in 'new(foo) BAR'. +# +# Overrides sp_inside_newop_paren. +sp_inside_newop_paren_close = ignore # ignore/add/remove/force + +# Add or remove space before a trailing or embedded comment. +sp_before_tr_emb_cmt = ignore # ignore/add/remove/force + +# Number of spaces before a trailing or embedded comment. +sp_num_before_tr_emb_cmt = 0 # unsigned number + +# (Java) Add or remove space between an annotation and the open parenthesis. +sp_annotation_paren = ignore # ignore/add/remove/force + +# If true, vbrace tokens are dropped to the previous token and skipped. +sp_skip_vbrace_tokens = false # true/false + +# Add or remove space after 'noexcept'. +sp_after_noexcept = ignore # ignore/add/remove/force + +# Add or remove space after '_'. +sp_vala_after_translation = remove # ignore/add/remove/force + +# If true, a is inserted after #define. +force_tab_after_define = false # true/false + +# +# Indenting options +# + +# The number of columns to indent per level. Usually 2, 3, 4, or 8. +# +# Default: 8 +indent_columns = 8 # unsigned number + +# The continuation indent. If non-zero, this overrides the indent of '(', '[' +# and '=' continuation indents. Negative values are OK; negative value is +# absolute and not increased for each '(' or '[' level. +# +# For FreeBSD, this is set to 4. +indent_continue = 0 # number + +# The continuation indent, only for class header line(s). If non-zero, this +# overrides the indent of 'class' continuation indents. +indent_continue_class_head = 0 # unsigned number + +# Whether to indent empty lines (i.e. lines which contain only spaces before +# the newline character). +indent_single_newlines = false # true/false + +# The continuation indent for func_*_param if they are true. If non-zero, this +# overrides the indent. +indent_param = 0 # unsigned number + +# How to use tabs when indenting code. +# +# 0: Spaces only +# 1: Indent with tabs to brace level, align with spaces (default) +# 2: Indent and align with tabs, using spaces when not on a tabstop +# +# Default: 1 +indent_with_tabs = 1 # unsigned number + +# Whether to indent comments that are not at a brace level with tabs on a +# tabstop. Requires indent_with_tabs=2. If false, will use spaces. +indent_cmt_with_tabs = false # true/false + +# Whether to indent strings broken by '\' so that they line up. +indent_align_string = false # true/false + +# The number of spaces to indent multi-line XML strings. +# Requires indent_align_string=true. +indent_xml_string = 0 # unsigned number + +# Spaces to indent '{' from level. +indent_brace = 0 # unsigned number + +# Whether braces are indented to the body level. +indent_braces = false # true/false + +# Whether to disable indenting function braces if indent_braces=true. +indent_braces_no_func = false # true/false + +# Whether to disable indenting class braces if indent_braces=true. +indent_braces_no_class = false # true/false + +# Whether to disable indenting struct braces if indent_braces=true. +indent_braces_no_struct = false # true/false + +# Whether to indent based on the size of the brace parent, +# i.e. 'if' => 3 spaces, 'for' => 4 spaces, etc. +indent_brace_parent = false # true/false + +# Whether to indent based on the open parenthesis instead of the open brace +# in '({\n'. +indent_paren_open_brace = false # true/false + +# (C#) Whether to indent the brace of a C# delegate by another level. +indent_cs_delegate_brace = false # true/false + +# (C#) Whether to indent a C# delegate (to handle delegates with no brace) by +# another level. +indent_cs_delegate_body = false # true/false + +# Whether to indent the body of a 'namespace'. +indent_namespace = true # true/false + +# Whether to indent only the first namespace, and not any nested namespaces. +# Requires indent_namespace=true. +indent_namespace_single_indent = false # true/false + +# The number of spaces to indent a namespace block. +# If set to zero, use the value indent_columns +indent_namespace_level = 0 # unsigned number + +# If the body of the namespace is longer than this number, it won't be +# indented. Requires indent_namespace=true. 0 means no limit. +indent_namespace_limit = 0 # unsigned number + +# Whether the 'extern "C"' body is indented. +indent_extern = false # true/false + +# Whether the 'class' body is indented. +indent_class = true # true/false + +# Whether to indent the stuff after a leading base class colon. +indent_class_colon = false # true/false + +# Whether to indent based on a class colon instead of the stuff after the +# colon. Requires indent_class_colon=true. +indent_class_on_colon = false # true/false + +# Whether to indent the stuff after a leading class initializer colon. +indent_constr_colon = false # true/false + +# Virtual indent from the ':' for member initializers. +# +# Default: 2 +indent_ctor_init_leading = 2 # unsigned number + +# Additional indent for constructor initializer list. +# Negative values decrease indent down to the first column. +indent_ctor_init = 0 # number + +# Whether to indent 'if' following 'else' as a new block under the 'else'. +# If false, 'else\nif' is treated as 'else if' for indenting purposes. +indent_else_if = false # true/false + +# Amount to indent variable declarations after a open brace. +# +# <0: Relative +# >=0: Absolute +indent_var_def_blk = 0 # number + +# Whether to indent continued variable declarations instead of aligning. +indent_var_def_cont = false # true/false + +# Whether to indent continued shift expressions ('<<' and '>>') instead of +# aligning. Set align_left_shift=false when enabling this. +indent_shift = false # true/false + +# Whether to force indentation of function definitions to start in column 1. +indent_func_def_force_col1 = false # true/false + +# Whether to indent continued function call parameters one indent level, +# rather than aligning parameters under the open parenthesis. +indent_func_call_param = false # true/false + +# Whether to indent continued function definition parameters one indent level, +# rather than aligning parameters under the open parenthesis. +indent_func_def_param = false # true/false + +# for function definitions, only if indent_func_def_param is false +# Allows to align params when appropriate and indent them when not +# behave as if it was true if paren position is more than this value +# if paren position is more than the option value +indent_func_def_param_paren_pos_threshold = 0 # unsigned number + +# Whether to indent continued function call prototype one indent level, +# rather than aligning parameters under the open parenthesis. +indent_func_proto_param = false # true/false + +# Whether to indent continued function call declaration one indent level, +# rather than aligning parameters under the open parenthesis. +indent_func_class_param = false # true/false + +# Whether to indent continued class variable constructors one indent level, +# rather than aligning parameters under the open parenthesis. +indent_func_ctor_var_param = false # true/false + +# Whether to indent continued template parameter list one indent level, +# rather than aligning parameters under the open parenthesis. +indent_template_param = false # true/false + +# Double the indent for indent_func_xxx_param options. +# Use both values of the options indent_columns and indent_param. +indent_func_param_double = false # true/false + +# Indentation column for standalone 'const' qualifier on a function +# prototype. +indent_func_const = 0 # unsigned number + +# Indentation column for standalone 'throw' qualifier on a function +# prototype. +indent_func_throw = 0 # unsigned number + +# How to indent within a macro followed by a brace on the same line +# This allows reducing the indent in macros that have (for example) +# `do { ... } while (0)` blocks bracketing them. +# +# true: add an indent for the brace on the same line as the macro +# false: do not add an indent for the brace on the same line as the macro +# +# Default: true +indent_macro_brace = true # true/false + +# The number of spaces to indent a continued '->' or '.'. +# Usually set to 0, 1, or indent_columns. +indent_member = 0 # unsigned number + +# Whether lines broken at '.' or '->' should be indented by a single indent. +# The indent_member option will not be effective if this is set to true. +indent_member_single = true # true/false + +# Spaces to indent single line ('//') comments on lines before code. +indent_sing_line_comments = 0 # unsigned number + +# When opening a paren for a control statement (if, for, while, etc), increase +# the indent level by this value. Negative values decrease the indent level. +indent_sparen_extra = 0 # number + +# Whether to indent trailing single line ('//') comments relative to the code +# instead of trying to keep the same absolute column. +indent_relative_single_line_comments = false # true/false + +# Spaces to indent 'case' from 'switch'. Usually 0 or indent_columns. +indent_switch_case = indent_columns # unsigned number + +# indent 'break' with 'case' from 'switch'. +indent_switch_break_with_case = false # true/false + +# Whether to indent preprocessor statements inside of switch statements. +# +# Default: true +indent_switch_pp = true # true/false + +# Spaces to shift the 'case' line, without affecting any other lines. +# Usually 0. +indent_case_shift = 0 # unsigned number + +# Spaces to indent '{' from 'case'. By default, the brace will appear under +# the 'c' in case. Usually set to 0 or indent_columns. Negative values are OK. +indent_case_brace = 0 # number + +# Whether to indent comments found in first column. +indent_col1_comment = false # true/false + +# Whether to indent multi string literal in first column. +indent_col1_multi_string_literal = false # true/false + +# How to indent goto labels. +# +# >0: Absolute column where 1 is the leftmost column +# <=0: Subtract from brace indent +# +# Default: 1 +indent_label = 1 # number + +# How to indent access specifiers that are followed by a +# colon. +# +# >0: Absolute column where 1 is the leftmost column +# <=0: Subtract from brace indent +# +# Default: 1 +indent_access_spec = 1 # number + +# Whether to indent the code after an access specifier by one level. +# If true, this option forces 'indent_access_spec=0'. +indent_access_spec_body = false # true/false + +# If an open parenthesis is followed by a newline, whether to indent the next +# line so that it lines up after the open parenthesis (not recommended). +indent_paren_nl = false # true/false + +# How to indent a close parenthesis after a newline. +# +# 0: Indent to body level (default) +# 1: Align under the open parenthesis +# 2: Indent to the brace level +indent_paren_close = 2 # unsigned number + +# Whether to indent the open parenthesis of a function definition, +# if the parenthesis is on its own line. +indent_paren_after_func_def = false # true/false + +# Whether to indent the open parenthesis of a function declaration, +# if the parenthesis is on its own line. +indent_paren_after_func_decl = false # true/false + +# Whether to indent the open parenthesis of a function call, +# if the parenthesis is on its own line. +indent_paren_after_func_call = false # true/false + +# Whether to indent a comma when inside a parenthesis. +# If true, aligns under the open parenthesis. +indent_comma_paren = false # true/false + +# Whether to indent a Boolean operator when inside a parenthesis. +# If true, aligns under the open parenthesis. +indent_bool_paren = false # true/false + +# Whether to indent a semicolon when inside a for parenthesis. +# If true, aligns under the open for parenthesis. +indent_semicolon_for_paren = false # true/false + +# Whether to align the first expression to following ones +# if indent_bool_paren=true. +indent_first_bool_expr = false # true/false + +# Whether to align the first expression to following ones +# if indent_semicolon_for_paren=true. +indent_first_for_expr = false # true/false + +# If an open square is followed by a newline, whether to indent the next line +# so that it lines up after the open square (not recommended). +indent_square_nl = false # true/false + +# (ESQL/C) Whether to preserve the relative indent of 'EXEC SQL' bodies. +indent_preserve_sql = false # true/false + +# Whether to align continued statements at the '='. If false or if the '=' is +# followed by a newline, the next line is indent one tab. +# +# Default: true +indent_align_assign = true # true/false + +# If true, the indentation of the chunks after a '=' sequence will be set at +# LHS token indentation column before '='. +indent_off_after_assign = false # true/false + +# Whether to align continued statements at the '('. If false or the '(' is +# followed by a newline, the next line indent is one tab. +# +# Default: true +indent_align_paren = true # true/false + +# (OC) Whether to indent Objective-C code inside message selectors. +indent_oc_inside_msg_sel = false # true/false + +# (OC) Whether to indent Objective-C blocks at brace level instead of usual +# rules. +indent_oc_block = false # true/false + +# (OC) Indent for Objective-C blocks in a message relative to the parameter +# name. +# +# =0: Use indent_oc_block rules +# >0: Use specified number of spaces to indent +indent_oc_block_msg = 0 # unsigned number + +# (OC) Minimum indent for subsequent parameters +indent_oc_msg_colon = 0 # unsigned number + +# (OC) Whether to prioritize aligning with initial colon (and stripping spaces +# from lines, if necessary). +# +# Default: true +indent_oc_msg_prioritize_first_colon = true # true/false + +# (OC) Whether to indent blocks the way that Xcode does by default +# (from the keyword if the parameter is on its own line; otherwise, from the +# previous indentation level). Requires indent_oc_block_msg=true. +indent_oc_block_msg_xcode_style = false # true/false + +# (OC) Whether to indent blocks from where the brace is, relative to a +# message keyword. Requires indent_oc_block_msg=true. +indent_oc_block_msg_from_keyword = false # true/false + +# (OC) Whether to indent blocks from where the brace is, relative to a message +# colon. Requires indent_oc_block_msg=true. +indent_oc_block_msg_from_colon = false # true/false + +# (OC) Whether to indent blocks from where the block caret is. +# Requires indent_oc_block_msg=true. +indent_oc_block_msg_from_caret = false # true/false + +# (OC) Whether to indent blocks from where the brace caret is. +# Requires indent_oc_block_msg=true. +indent_oc_block_msg_from_brace = false # true/false + +# When indenting after virtual brace open and newline add further spaces to +# reach this minimum indent. +indent_min_vbrace_open = 0 # unsigned number + +# Whether to add further spaces after regular indent to reach next tabstop +# when indenting after virtual brace open and newline. +indent_vbrace_open_on_tabstop = false # true/false + +# How to indent after a brace followed by another token (not a newline). +# true: indent all contained lines to match the token +# false: indent all contained lines to match the brace +# +# Default: true +indent_token_after_brace = true # true/false + +# Whether to indent the body of a C++11 lambda. +indent_cpp_lambda_body = true # true/false + +# How to indent compound literals that are being returned. +# true: add both the indent from return & the compound literal open brace (ie: +# 2 indent levels) +# false: only indent 1 level, don't add the indent for the open brace, only add +# the indent for the return. +# +# Default: true +indent_compound_literal_return = true # true/false + +# (C#) Whether to indent a 'using' block if no braces are used. +# +# Default: true +indent_using_block = true # true/false + +# How to indent the continuation of ternary operator. +# +# 0: Off (default) +# 1: When the `if_false` is a continuation, indent it under `if_false` +# 2: When the `:` is a continuation, indent it under `?` +indent_ternary_operator = 0 # unsigned number + +# Whether to indent the statments inside ternary operator. +indent_inside_ternary_operator = false # true/false + +# If true, the indentation of the chunks after a `return` sequence will be set at return indentation column. +indent_off_after_return = false # true/false + +# If true, the indentation of the chunks after a `return new` sequence will be set at return indentation column. +indent_off_after_return_new = false # true/false + +# If true, the tokens after return are indented with regular single indentation. By default (false) the indentation is after the return token. +indent_single_after_return = false # true/false + +# Whether to ignore indent and alignment for 'asm' blocks (i.e. assume they +# have their own indentation). +indent_ignore_asm_block = false # true/false + +# Don't indent the close parenthesis of a function definition, +# if the parenthesis is on its own line. +donot_indent_func_def_close_paren = true # true/false + +# +# Newline adding and removing options +# + +# Whether to collapse empty blocks between '{' and '}'. +# If true, overrides nl_inside_empty_func +nl_collapse_empty_body = true # true/false + +# Don't split one-line braced assignments, as in 'foo_t f = { 1, 2 };'. +nl_assign_leave_one_liners = true # true/false + +# Don't split one-line braced statements inside a 'class xx { }' body. +nl_class_leave_one_liners = true # true/false + +# Don't split one-line enums, as in 'enum foo { BAR = 15 };' +nl_enum_leave_one_liners = true # true/false + +# Don't split one-line get or set functions. +nl_getset_leave_one_liners = true # true/false + +# (C#) Don't split one-line property get or set functions. +nl_cs_property_leave_one_liners = true # true/false + +# Don't split one-line function definitions, as in 'int foo() { return 0; }'. +# might modify nl_func_type_name +nl_func_leave_one_liners = true # true/false + +# Don't split one-line C++11 lambdas, as in '[]() { return 0; }'. +nl_cpp_lambda_leave_one_liners = true # true/false + +# Don't split one-line if/else statements, as in 'if(...) b++;'. +nl_if_leave_one_liners = true # true/false + +# Don't split one-line while statements, as in 'while(...) b++;'. +nl_while_leave_one_liners = true # true/false + +# Don't split one-line for statements, as in 'for(...) b++;'. +nl_for_leave_one_liners = true # true/false + +# (OC) Don't split one-line Objective-C messages. +nl_oc_msg_leave_one_liner = false # true/false + +# (OC) Add or remove newline between method declaration and '{'. +nl_oc_mdef_brace = remove # ignore/add/remove/force + +# (OC) Add or remove newline between Objective-C block signature and '{'. +nl_oc_block_brace = ignore # ignore/add/remove/force + +# (OC) Add or remove blank line before '@interface' statement. +nl_oc_before_interface = ignore # ignore/add/remove/force + +# (OC) Add or remove blank line before '@implementation' statement. +nl_oc_before_implementation = ignore # ignore/add/remove/force + +# (OC) Add or remove blank line before '@end' statement. +nl_oc_before_end = ignore # ignore/add/remove/force + +# (OC) Add or remove newline between '@interface' and '{'. +nl_oc_interface_brace = ignore # ignore/add/remove/force + +# (OC) Add or remove newline between '@implementation' and '{'. +nl_oc_implementation_brace = ignore # ignore/add/remove/force + +# Add or remove newlines at the start of the file. +nl_start_of_file = ignore # ignore/add/remove/force + +# The minimum number of newlines at the start of the file (only used if +# nl_start_of_file is 'add' or 'force'). +nl_start_of_file_min = 0 # unsigned number + +# Add or remove newline at the end of the file. +nl_end_of_file = ignore # ignore/add/remove/force + +# The minimum number of newlines at the end of the file (only used if +# nl_end_of_file is 'add' or 'force'). +nl_end_of_file_min = 0 # unsigned number + +# Add or remove newline between '=' and '{'. +nl_assign_brace = ignore # ignore/add/remove/force + +# (D) Add or remove newline between '=' and '['. +nl_assign_square = ignore # ignore/add/remove/force + +# Add or remove newline between '[]' and '{'. +nl_tsquare_brace = ignore # ignore/add/remove/force + +# (D) Add or remove newline after '= ['. Will also affect the newline before +# the ']'. +nl_after_square_assign = ignore # ignore/add/remove/force + +# Add or remove newline between a function call's ')' and '{', as in +# 'list_for_each(item, &list) { }'. +nl_fcall_brace = add # ignore/add/remove/force + +# Add or remove newline between 'enum' and '{'. +nl_enum_brace = ignore # ignore/add/remove/force + +# Add or remove newline between 'enum' and 'class'. +nl_enum_class = ignore # ignore/add/remove/force + +# Add or remove newline between 'enum class' and the identifier. +nl_enum_class_identifier = ignore # ignore/add/remove/force + +# Add or remove newline between 'enum class' type and ':'. +nl_enum_identifier_colon = ignore # ignore/add/remove/force + +# Add or remove newline between 'enum class identifier :' and type. +nl_enum_colon_type = ignore # ignore/add/remove/force + +# Add or remove newline between 'struct and '{'. +nl_struct_brace = add # ignore/add/remove/force + +# Add or remove newline between 'union' and '{'. +nl_union_brace = ignore # ignore/add/remove/force + +# Add or remove newline between 'if' and '{'. +nl_if_brace = add # ignore/add/remove/force + +# Add or remove newline between '}' and 'else'. +nl_brace_else = add # ignore/add/remove/force + +# Add or remove newline between 'else if' and '{'. If set to ignore, +# nl_if_brace is used instead. +nl_elseif_brace = ignore # ignore/add/remove/force + +# Add or remove newline between 'else' and '{'. +nl_else_brace = add # ignore/add/remove/force + +# Add or remove newline between 'else' and 'if'. +nl_else_if = remove # ignore/add/remove/force + +# Add or remove newline before '{' opening brace +nl_before_opening_brace_func_class_def = add # ignore/add/remove/force + +# Add or remove newline before 'if'/'else if' closing parenthesis. +nl_before_if_closing_paren = remove # ignore/add/remove/force + +# Add or remove newline between '}' and 'finally'. +nl_brace_finally = add # ignore/add/remove/force + +# Add or remove newline between 'finally' and '{'. +nl_finally_brace = ignore # ignore/add/remove/force + +# Add or remove newline between 'try' and '{'. +nl_try_brace = add # ignore/add/remove/force + +# Add or remove newline between get/set and '{'. +nl_getset_brace = add # ignore/add/remove/force + +# Add or remove newline between 'for' and '{'. +nl_for_brace = add # ignore/add/remove/force + +# Add or remove newline before the '{' of a 'catch' statement, as in +# 'catch (decl) {'. +nl_catch_brace = add # ignore/add/remove/force + +# (OC) Add or remove newline before the '{' of a '@catch' statement, as in +# '@catch (decl) {'. If set to ignore, nl_catch_brace is used. +nl_oc_catch_brace = ignore # ignore/add/remove/force + +# Add or remove newline between '}' and 'catch'. +nl_brace_catch = add # ignore/add/remove/force + +# (OC) Add or remove newline between '}' and '@catch'. If set to ignore, +# nl_brace_catch is used. +nl_oc_brace_catch = ignore # ignore/add/remove/force + +# Add or remove newline between '}' and ']'. +nl_brace_square = ignore # ignore/add/remove/force + +# Add or remove newline between '}' and ')' in a function invocation. +nl_brace_fparen = ignore # ignore/add/remove/force + +# Add or remove newline between 'while' and '{'. +nl_while_brace = add # ignore/add/remove/force + +# (D) Add or remove newline between 'scope (x)' and '{'. +nl_scope_brace = ignore # ignore/add/remove/force + +# (D) Add or remove newline between 'unittest' and '{'. +nl_unittest_brace = ignore # ignore/add/remove/force + +# (D) Add or remove newline between 'version (x)' and '{'. +nl_version_brace = ignore # ignore/add/remove/force + +# (C#) Add or remove newline between 'using' and '{'. +nl_using_brace = ignore # ignore/add/remove/force + +# Add or remove newline between two open or close braces. Due to general +# newline/brace handling, REMOVE may not work. +nl_brace_brace = add # ignore/add/remove/force + +# Add or remove newline between 'do' and '{'. +nl_do_brace = add # ignore/add/remove/force + +# Add or remove newline between '}' and 'while' of 'do' statement. +nl_brace_while = add # ignore/add/remove/force + +# Add or remove newline between 'switch' and '{'. +nl_switch_brace = add # ignore/add/remove/force + +# Add or remove newline between 'synchronized' and '{'. +nl_synchronized_brace = ignore # ignore/add/remove/force + +# Add a newline between ')' and '{' if the ')' is on a different line than the +# if/for/etc. +# +# Overrides nl_for_brace, nl_if_brace, nl_switch_brace, nl_while_switch and +# nl_catch_brace. +nl_multi_line_cond = false # true/false + +# Add a newline after '(' if an if/for/while/switch condition spans multiple +# lines +nl_multi_line_sparen_open = remove # ignore/add/remove/force + +# Add a newline before ')' if an if/for/while/switch condition spans multiple +# lines. Overrides nl_before_if_closing_paren if both are specified. +nl_multi_line_sparen_close = remove # ignore/add/remove/force + +# Force a newline in a define after the macro name for multi-line defines. +nl_multi_line_define = false # true/false + +# Whether to add a newline before 'case', and a blank line before a 'case' +# statement that follows a ';' or '}'. +nl_before_case = false # true/false + +# Whether to add a newline after a 'case' statement. +nl_after_case = false # true/false + +# Add or remove newline between a case ':' and '{'. +# +# Overrides nl_after_case. +nl_case_colon_brace = ignore # ignore/add/remove/force + +# Add or remove newline between ')' and 'throw'. +nl_before_throw = ignore # ignore/add/remove/force + +# Add or remove newline between 'namespace' and '{'. +nl_namespace_brace = add # ignore/add/remove/force + +# Add or remove newline after 'template<...>' of a template class. +nl_template_class = ignore # ignore/add/remove/force + +# Add or remove newline after 'template<...>' of a template class declaration. +# +# Overrides nl_template_class. +nl_template_class_decl = ignore # ignore/add/remove/force + +# Add or remove newline after 'template<>' of a specialized class declaration. +# +# Overrides nl_template_class_decl. +nl_template_class_decl_special = ignore # ignore/add/remove/force + +# Add or remove newline after 'template<...>' of a template class definition. +# +# Overrides nl_template_class. +nl_template_class_def = ignore # ignore/add/remove/force + +# Add or remove newline after 'template<>' of a specialized class definition. +# +# Overrides nl_template_class_def. +nl_template_class_def_special = ignore # ignore/add/remove/force + +# Add or remove newline after 'template<...>' of a template function. +nl_template_func = ignore # ignore/add/remove/force + +# Add or remove newline after 'template<...>' of a template function +# declaration. +# +# Overrides nl_template_func. +nl_template_func_decl = ignore # ignore/add/remove/force + +# Add or remove newline after 'template<>' of a specialized function +# declaration. +# +# Overrides nl_template_func_decl. +nl_template_func_decl_special = ignore # ignore/add/remove/force + +# Add or remove newline after 'template<...>' of a template function +# definition. +# +# Overrides nl_template_func. +nl_template_func_def = ignore # ignore/add/remove/force + +# Add or remove newline after 'template<>' of a specialized function +# definition. +# +# Overrides nl_template_func_def. +nl_template_func_def_special = ignore # ignore/add/remove/force + +# Add or remove newline after 'template<...>' of a template variable. +nl_template_var = ignore # ignore/add/remove/force + +# Add or remove newline between 'template<...>' and 'using' of a templated +# type alias. +nl_template_using = ignore # ignore/add/remove/force + +# Add or remove newline between 'class' and '{'. +nl_class_brace = add # ignore/add/remove/force + +# Add or remove newline before or after (depending on pos_class_comma, +# may not be IGNORE) each',' in the base class list. +nl_class_init_args = ignore # ignore/add/remove/force + +# Add or remove newline after each ',' in the constructor member +# initialization. Related to nl_constr_colon, pos_constr_colon and +# pos_constr_comma. +nl_constr_init_args = ignore # ignore/add/remove/force + +# Add or remove newline before first element, after comma, and after last +# element, in 'enum'. +nl_enum_own_lines = ignore # ignore/add/remove/force + +# Add or remove newline between return type and function name in a function +# definition. +# might be modified by nl_func_leave_one_liners +nl_func_type_name = ignore # ignore/add/remove/force + +# Add or remove newline between return type and function name inside a class +# definition. If set to ignore, nl_func_type_name or nl_func_proto_type_name +# is used instead. +nl_func_type_name_class = ignore # ignore/add/remove/force + +# Add or remove newline between class specification and '::' +# in 'void A::f() { }'. Only appears in separate member implementation (does +# not appear with in-line implementation). +nl_func_class_scope = ignore # ignore/add/remove/force + +# Add or remove newline between function scope and name, as in +# 'void A :: f() { }'. +nl_func_scope_name = ignore # ignore/add/remove/force + +# Add or remove newline between return type and function name in a prototype. +nl_func_proto_type_name = ignore # ignore/add/remove/force + +# Add or remove newline between a function name and the opening '(' in the +# declaration. +nl_func_paren = ignore # ignore/add/remove/force + +# Overrides nl_func_paren for functions with no parameters. +nl_func_paren_empty = ignore # ignore/add/remove/force + +# Add or remove newline between a function name and the opening '(' in the +# definition. +nl_func_def_paren = ignore # ignore/add/remove/force + +# Overrides nl_func_def_paren for functions with no parameters. +nl_func_def_paren_empty = ignore # ignore/add/remove/force + +# Add or remove newline between a function name and the opening '(' in the +# call. +nl_func_call_paren = ignore # ignore/add/remove/force + +# Overrides nl_func_call_paren for functions with no parameters. +nl_func_call_paren_empty = ignore # ignore/add/remove/force + +# Add or remove newline after '(' in a function declaration. +nl_func_decl_start = ignore # ignore/add/remove/force + +# Add or remove newline after '(' in a function definition. +nl_func_def_start = ignore # ignore/add/remove/force + +# Overrides nl_func_decl_start when there is only one parameter. +nl_func_decl_start_single = remove # ignore/add/remove/force + +# Overrides nl_func_def_start when there is only one parameter. +nl_func_def_start_single = remove # ignore/add/remove/force + +# Whether to add a newline after '(' in a function declaration if '(' and ')' +# are in different lines. If false, nl_func_decl_start is used instead. +nl_func_decl_start_multi_line = false # true/false + +# Whether to add a newline after '(' in a function definition if '(' and ')' +# are in different lines. If false, nl_func_def_start is used instead. +nl_func_def_start_multi_line = false # true/false + +# Add or remove newline after each ',' in a function declaration. +nl_func_decl_args = ignore # ignore/add/remove/force + +# Add or remove newline after each ',' in a function definition. +nl_func_def_args = ignore # ignore/add/remove/force + +# Add or remove newline after each ',' in a function call. +nl_func_call_args = ignore # ignore/add/remove/force + +# Whether to add a newline after each ',' in a function declaration if '(' +# and ')' are in different lines. If false, nl_func_decl_args is used instead. +nl_func_decl_args_multi_line = true # true/false + +# Whether to add a newline after each ',' in a function definition if '(' +# and ')' are in different lines. If false, nl_func_def_args is used instead. +nl_func_def_args_multi_line = true # true/false + +# Add or remove newline before the ')' in a function declaration. +nl_func_decl_end = remove # ignore/add/remove/force + +# Add or remove newline before the ')' in a function definition. +nl_func_def_end = remove # ignore/add/remove/force + +# Overrides nl_func_decl_end when there is only one parameter. +nl_func_decl_end_single = remove # ignore/add/remove/force + +# Overrides nl_func_def_end when there is only one parameter. +nl_func_def_end_single = remove # ignore/add/remove/force + +# Whether to add a newline before ')' in a function declaration if '(' and ')' +# are in different lines. If false, nl_func_decl_end is used instead. +nl_func_decl_end_multi_line = false # true/false + +# Whether to add a newline before ')' in a function definition if '(' and ')' +# are in different lines. If false, nl_func_def_end is used instead. +nl_func_def_end_multi_line = false # true/false + +# Add or remove newline between '()' in a function declaration. +nl_func_decl_empty = remove # ignore/add/remove/force + +# Add or remove newline between '()' in a function definition. +nl_func_def_empty = remove # ignore/add/remove/force + +# Add or remove newline between '()' in a function call. +nl_func_call_empty = remove # ignore/add/remove/force + +# Whether to add a newline after '(' in a function call, +# has preference over nl_func_call_start_multi_line. +nl_func_call_start = ignore # ignore/add/remove/force + +# Whether to add a newline before ')' in a function call. +nl_func_call_end = remove # ignore/add/remove/force + +# Whether to add a newline after '(' in a function call if '(' and ')' are in +# different lines. +nl_func_call_start_multi_line = false # true/false + +# Whether to add a newline after each ',' in a function call if '(' and ')' +# are in different lines. +nl_func_call_args_multi_line = true # true/false + +# Whether to add a newline before ')' in a function call if '(' and ')' are in +# different lines. +nl_func_call_end_multi_line = false # true/false + +# Whether to respect nl_func_call_XXX option incase of closure args. +nl_func_call_args_multi_line_ignore_closures = false # true/false + +# Whether to add a newline after '<' of a template parameter list. +nl_template_start = false # true/false + +# Whether to add a newline after each ',' in a template parameter list. +nl_template_args = false # true/false + +# Whether to add a newline before '>' of a template parameter list. +nl_template_end = false # true/false + +# (OC) Whether to put each Objective-C message parameter on a separate line. +# See nl_oc_msg_leave_one_liner. +nl_oc_msg_args = false # true/false + +# Add or remove newline between function signature and '{'. +nl_fdef_brace = add # ignore/add/remove/force + +# Add or remove newline between function signature and '{', +# if signature ends with ')'. Overrides nl_fdef_brace. +nl_fdef_brace_cond = ignore # ignore/add/remove/force + +# Add or remove newline between C++11 lambda signature and '{'. +nl_cpp_ldef_brace = remove # ignore/add/remove/force + +# Add or remove newline between 'return' and the return expression. +nl_return_expr = remove # ignore/add/remove/force + +# Whether to add a newline after semicolons, except in 'for' statements. +nl_after_semicolon = add # true/false + +# (Java) Add or remove newline between the ')' and '{{' of the double brace +# initializer. +nl_paren_dbrace_open = ignore # ignore/add/remove/force + +# Whether to add a newline after the type in an unnamed temporary +# direct-list-initialization. +nl_type_brace_init_lst = ignore # ignore/add/remove/force + +# Whether to add a newline after the open brace in an unnamed temporary +# direct-list-initialization. +nl_type_brace_init_lst_open = ignore # ignore/add/remove/force + +# Whether to add a newline before the close brace in an unnamed temporary +# direct-list-initialization. +nl_type_brace_init_lst_close = ignore # ignore/add/remove/force + +# Whether to add a newline after '{'. This also adds a newline before the +# matching '}'. +nl_after_brace_open = false # true/false + +# Whether to add a newline between the open brace and a trailing single-line +# comment. Requires nl_after_brace_open=true. +nl_after_brace_open_cmt = false # true/false + +# Whether to add a newline after a virtual brace open with a non-empty body. +# These occur in un-braced if/while/do/for statement bodies. +nl_after_vbrace_open = false # true/false + +# Whether to add a newline after a virtual brace open with an empty body. +# These occur in un-braced if/while/do/for statement bodies. +nl_after_vbrace_open_empty = false # true/false + +# Whether to add a newline after '}'. Does not apply if followed by a +# necessary ';'. +nl_after_brace_close = true # true/false + +# Whether to add a newline after a virtual brace close, +# as in 'if (foo) a++; return;'. +nl_after_vbrace_close = false # true/false + +# Add or remove newline between the close brace and identifier, +# as in 'struct { int a; } b;'. Affects enumerations, unions and +# structures. If set to ignore, uses nl_after_brace_close. +nl_brace_struct_var = ignore # ignore/add/remove/force + +# Whether to alter newlines in '#define' macros. +nl_define_macro = false # true/false + +# Whether to alter newlines between consecutive parenthesis closes. The number +# of closing parentheses in a line will depend on respective open parenthesis +# lines. +nl_squeeze_paren_close = false # true/false + +# Whether to remove blanks after '#ifxx' and '#elxx', or before '#elxx' and +# '#endif'. Does not affect top-level #ifdefs. +nl_squeeze_ifdef = true # true/false + +# Makes the nl_squeeze_ifdef option affect the top-level #ifdefs as well. +nl_squeeze_ifdef_top_level = true # true/false + +# Add or remove blank line before 'if'. +nl_before_if = add # ignore/add/remove/force + +# Add or remove blank line after 'if' statement. Add/Force work only if the +# next token is not a closing brace. +nl_after_if = add # ignore/add/remove/force + +# Add or remove blank line before 'for'. +nl_before_for = ignore # ignore/add/remove/force + +# Add or remove blank line after 'for' statement. +nl_after_for = add # ignore/add/remove/force + +# Add or remove blank line before 'while'. +nl_before_while = add # ignore/add/remove/force + +# Add or remove blank line after 'while' statement. +nl_after_while = add # ignore/add/remove/force + +# Add or remove blank line before 'switch'. +nl_before_switch = add # ignore/add/remove/force + +# Add or remove blank line after 'switch' statement. +nl_after_switch = add # ignore/add/remove/force + +# Add or remove blank line before 'synchronized'. +nl_before_synchronized = ignore # ignore/add/remove/force + +# Add or remove blank line after 'synchronized' statement. +nl_after_synchronized = ignore # ignore/add/remove/force + +# Add or remove blank line before 'do'. +nl_before_do = add # ignore/add/remove/force + +# Add or remove blank line after 'do/while' statement. +nl_after_do = add # ignore/add/remove/force + +# Whether to put a blank line before 'return' statements, unless after an open +# brace. +nl_before_return = true # true/false + +# Whether to put a blank line after 'return' statements, unless followed by a +# close brace. +nl_after_return = true # true/false + +# Whether to put a blank line before a member '.' or '->' operators. +nl_before_member = ignore # ignore/add/remove/force + +# (Java) Whether to put a blank line after a member '.' or '->' operators. +nl_after_member = ignore # ignore/add/remove/force + +# Whether to double-space commented-entries in 'struct'/'union'/'enum'. +nl_ds_struct_enum_cmt = false # true/false + +# Whether to force a newline before '}' of a 'struct'/'union'/'enum'. +# (Lower priority than eat_blanks_before_close_brace.) +nl_ds_struct_enum_close_brace = false # true/false + +# Add or remove newline before or after (depending on pos_class_colon) a class +# colon, as in 'class Foo : public Bar'. +nl_class_colon = ignore # ignore/add/remove/force + +# Add or remove newline around a class constructor colon. The exact position +# depends on nl_constr_init_args, pos_constr_colon and pos_constr_comma. +nl_constr_colon = ignore # ignore/add/remove/force + +# Whether to collapse a two-line namespace, like 'namespace foo\n{ decl; }' +# into a single line. If true, prevents other brace newline rules from turning +# such code into four lines. +nl_namespace_two_to_one_liner = false # true/false + +# Whether to remove a newline in simple unbraced if statements, turning them +# into one-liners, as in 'if(b)\n i++;' => 'if(b) i++;'. +nl_create_if_one_liner = false # true/false + +# Whether to remove a newline in simple unbraced for statements, turning them +# into one-liners, as in 'for (...)\n stmt;' => 'for (...) stmt;'. +nl_create_for_one_liner = false # true/false + +# Whether to remove a newline in simple unbraced while statements, turning +# them into one-liners, as in 'while (expr)\n stmt;' => 'while (expr) stmt;'. +nl_create_while_one_liner = false # true/false + +# Whether to collapse a function definition whose body (not counting braces) +# is only one line so that the entire definition (prototype, braces, body) is +# a single line. +nl_create_func_def_one_liner = true # true/false + +# Whether to collapse a function definition whose body (not counting braces) +# is only one line so that the entire definition (prototype, braces, body) is +# a single line. +nl_create_list_one_liner = true # true/false + +# Whether to split one-line simple unbraced if statements into two lines by +# adding a newline, as in 'if(b) i++;'. +nl_split_if_one_liner = false # true/false + +# Whether to split one-line simple unbraced for statements into two lines by +# adding a newline, as in 'for (...) stmt;'. +nl_split_for_one_liner = false # true/false + +# Whether to split one-line simple unbraced while statements into two lines by +# adding a newline, as in 'while (expr) stmt;'. +nl_split_while_one_liner = false # true/false + +# Don't add a newline before a cpp-comment in a parameter list of a function +# call. +donot_add_nl_before_cpp_comment = false # true/false + +# +# Blank line options +# + +# The maximum number of consecutive newlines (3 = 2 blank lines). +nl_max = 0 # unsigned number + +# The maximum number of consecutive newlines in a function. +nl_max_blank_in_func = 0 # unsigned number + +# The number of newlines inside an empty function body. +# This option is overridden by nl_collapse_empty_body=true +nl_inside_empty_func = 0 # unsigned number + +# The number of newlines before a function prototype. +nl_before_func_body_proto = 0 # unsigned number + +# The number of newlines before a multi-line function definition. +nl_before_func_body_def = 0 # unsigned number + +# The number of newlines before a class constructor/destructor prototype. +nl_before_func_class_proto = 0 # unsigned number + +# The number of newlines before a class constructor/destructor definition. +nl_before_func_class_def = 0 # unsigned number + +# The number of newlines after a function prototype. +nl_after_func_proto = 0 # unsigned number + +# The number of newlines after a function prototype, if not followed by +# another function prototype. +nl_after_func_proto_group = 0 # unsigned number + +# The number of newlines after a class constructor/destructor prototype. +nl_after_func_class_proto = 0 # unsigned number + +# The number of newlines after a class constructor/destructor prototype, +# if not followed by another constructor/destructor prototype. +nl_after_func_class_proto_group = 0 # unsigned number + +# Whether one-line method definitions inside a class body should be treated +# as if they were prototypes for the purposes of adding newlines. +# +# Requires nl_class_leave_one_liners=true. Overrides nl_before_func_body_def +# and nl_before_func_class_def for one-liners. +nl_class_leave_one_liner_groups = false # true/false + +# The number of newlines after '}' of a multi-line function body. +nl_after_func_body = 0 # unsigned number + +# The number of newlines after '}' of a multi-line function body in a class +# declaration. Also affects class constructors/destructors. +# +# Overrides nl_after_func_body. +nl_after_func_body_class = 0 # unsigned number + +# The number of newlines after '}' of a single line function body. Also +# affects class constructors/destructors. +# +# Overrides nl_after_func_body and nl_after_func_body_class. +nl_after_func_body_one_liner = 0 # unsigned number + +# The number of blank lines after a block of variable definitions at the top +# of a function body. +# +# 0: No change (default). +nl_func_var_def_blk = 0 # unsigned number + +# The number of newlines before a block of typedefs. If nl_after_access_spec +# is non-zero, that option takes precedence. +# +# 0: No change (default). +nl_typedef_blk_start = 0 # unsigned number + +# The number of newlines after a block of typedefs. +# +# 0: No change (default). +nl_typedef_blk_end = 0 # unsigned number + +# The maximum number of consecutive newlines within a block of typedefs. +# +# 0: No change (default). +nl_typedef_blk_in = 0 # unsigned number + +# The number of newlines before a block of variable definitions not at the top +# of a function body. If nl_after_access_spec is non-zero, that option takes +# precedence. +# +# 0: No change (default). +nl_var_def_blk_start = 1 # unsigned number + +# The number of newlines after a block of variable definitions not at the top +# of a function body. +# +# 0: No change (default). +nl_var_def_blk_end = 1 # unsigned number + +# The maximum number of consecutive newlines within a block of variable +# definitions. +# +# 0: No change (default). +nl_var_def_blk_in = 0 # unsigned number + +# The minimum number of newlines before a multi-line comment. +# Doesn't apply if after a brace open or another multi-line comment. +nl_before_block_comment = 0 # unsigned number + +# The minimum number of newlines before a single-line C comment. +# Doesn't apply if after a brace open or other single-line C comments. +nl_before_c_comment = 0 # unsigned number + +# The minimum number of newlines before a CPP comment. +# Doesn't apply if after a brace open or other CPP comments. +nl_before_cpp_comment = 0 # unsigned number + +# Whether to force a newline after a multi-line comment. +nl_after_multiline_comment = false # true/false + +# Whether to force a newline after a label's colon. +nl_after_label_colon = false # true/false + +# The number of newlines after '}' or ';' of a struct/enum/union definition. +nl_after_struct = 0 # unsigned number + +# The number of newlines before a class definition. +nl_before_class = 0 # unsigned number + +# The number of newlines after '}' or ';' of a class definition. +nl_after_class = 0 # unsigned number + +# The number of newlines before a namespace. +nl_before_namespace = 0 # unsigned number + +# The number of newlines after '{' of a namespace. This also adds newlines +# before the matching '}'. +# +# 0: Apply eat_blanks_after_open_brace or eat_blanks_before_close_brace if +# applicable, otherwise no change. +# +# Overrides eat_blanks_after_open_brace and eat_blanks_before_close_brace. +nl_inside_namespace = 0 # unsigned number + +# The number of newlines after '}' of a namespace. +nl_after_namespace = 0 # unsigned number + +# The number of newlines before an access specifier label. This also includes +# the Qt-specific 'signals:' and 'slots:'. Will not change the newline count +# if after a brace open. +# +# 0: No change (default). +nl_before_access_spec = 0 # unsigned number + +# The number of newlines after an access specifier label. This also includes +# the Qt-specific 'signals:' and 'slots:'. Will not change the newline count +# if after a brace open. +# +# 0: No change (default). +# +# Overrides nl_typedef_blk_start and nl_var_def_blk_start. +nl_after_access_spec = 0 # unsigned number + +# The number of newlines between a function definition and the function +# comment, as in '// comment\n void foo() {...}'. +# +# 0: No change (default). +nl_comment_func_def = 0 # unsigned number + +# The number of newlines after a try-catch-finally block that isn't followed +# by a brace close. +# +# 0: No change (default). +nl_after_try_catch_finally = 2 # unsigned number + +# (C#) The number of newlines before and after a property, indexer or event +# declaration. +# +# 0: No change (default). +nl_around_cs_property = 0 # unsigned number + +# (C#) The number of newlines between the get/set/add/remove handlers. +# +# 0: No change (default). +nl_between_get_set = 0 # unsigned number + +# (C#) Add or remove newline between property and the '{'. +nl_property_brace = remove # ignore/add/remove/force + +# Whether to remove blank lines after '{'. +eat_blanks_after_open_brace = true # true/false + +# Whether to remove blank lines before '}'. +eat_blanks_before_close_brace = true # true/false + +# How aggressively to remove extra newlines not in preprocessor. +# +# 0: No change (default) +# 1: Remove most newlines not handled by other config +# 2: Remove all newlines and reformat completely by config +nl_remove_extra_newlines = 0 # unsigned number + +# (Java) Add or remove newline after an annotation statement. Only affects +# annotations that are after a newline. +nl_after_annotation = ignore # ignore/add/remove/force + +# (Java) Add or remove newline between two annotations. +nl_between_annotation = ignore # ignore/add/remove/force + +# The number of newlines before a whole-file #ifdef. +# +# 0: No change (default). +nl_before_whole_file_ifdef = 0 # unsigned number + +# The number of newlines after a whole-file #ifdef. +# +# 0: No change (default). +nl_after_whole_file_ifdef = 0 # unsigned number + +# The number of newlines before a whole-file #endif. +# +# 0: No change (default). +nl_before_whole_file_endif = 0 # unsigned number + +# The number of newlines after a whole-file #endif. +# +# 0: No change (default). +nl_after_whole_file_endif = 0 # unsigned number + +# +# Positioning options +# + +# The position of arithmetic operators in wrapped expressions. +pos_arith = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of assignment in wrapped expressions. Do not affect '=' +# followed by '{'. +pos_assign = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of Boolean operators in wrapped expressions. +pos_bool = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of comparison operators in wrapped expressions. +pos_compare = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of conditional operators, as in the '?' and ':' of +# 'expr ? stmt : stmt', in wrapped expressions. +pos_conditional = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of the comma in wrapped expressions. +pos_comma = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of the comma in enum entries. +pos_enum_comma = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of the comma in the base class list if there is more than one +# line. Affects nl_class_init_args. +pos_class_comma = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of the comma in the constructor initialization list. +# Related to nl_constr_colon, nl_constr_init_args and pos_constr_colon. +pos_constr_comma = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of trailing/leading class colon, between class and base class +# list. Affects nl_class_colon. +pos_class_colon = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of colons between constructor and member initialization. +# Related to nl_constr_colon, nl_constr_init_args and pos_constr_comma. +pos_constr_colon = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# The position of shift operators in wrapped expressions. +pos_shift = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force + +# +# Line splitting options +# + +# Try to limit code width to N columns. +code_width = 0 # unsigned number + +# Whether to fully split long 'for' statements at semi-colons. +ls_for_split_full = false # true/false + +# Whether to fully split long function prototypes/calls at commas. +# The option ls_code_width has priority over the option ls_func_split_full. +ls_func_split_full = false # true/false + +# Whether to split lines as close to code_width as possible and ignore some +# groupings. +# The option ls_code_width has priority over the option ls_func_split_full. +ls_code_width = false # true/false + +# +# Code alignment options (not left column spaces/tabs) +# + +# Whether to keep non-indenting tabs. +align_keep_tabs = false # true/false + +# Whether to use tabs for aligning. +align_with_tabs = false # true/false + +# Whether to bump out to the next tab when aligning. +align_on_tabstop = false # true/false + +# Whether to right-align numbers. +align_number_right = false # true/false + +# Whether to keep whitespace not required for alignment. +align_keep_extra_space = false # true/false + +# Whether to align variable definitions in prototypes and functions. +align_func_params = true # true/false + +# The span for aligning parameter definitions in function on parameter name. +# +# 0: Don't align (default). +align_func_params_span = 0 # unsigned number + +# The threshold for aligning function parameter definitions. +# Use a negative number for absolute thresholds. +# +# 0: No limit (default). +align_func_params_thresh = 0 # number + +# The gap for aligning function parameter definitions. +align_func_params_gap = 0 # unsigned number + +# The span for aligning constructor value. +# +# 0: Don't align (default). +align_constr_value_span = 0 # unsigned number + +# The threshold for aligning constructor value. +# Use a negative number for absolute thresholds. +# +# 0: No limit (default). +align_constr_value_thresh = 0 # number + +# The gap for aligning constructor value. +align_constr_value_gap = 0 # unsigned number + +# Whether to align parameters in single-line functions that have the same +# name. The function names must already be aligned with each other. +align_same_func_call_params = false # true/false + +# The span for aligning function-call parameters for single line functions. +# +# 0: Don't align (default). +align_same_func_call_params_span = 0 # unsigned number + +# The threshold for aligning function-call parameters for single line +# functions. +# Use a negative number for absolute thresholds. +# +# 0: No limit (default). +align_same_func_call_params_thresh = 0 # number + +# The span for aligning variable definitions. +# +# 0: Don't align (default). +align_var_def_span = 1 # unsigned number + +# How to consider (or treat) the '*' in the alignment of variable definitions. +# +# 0: Part of the type 'void * foo;' (default) +# 1: Part of the variable 'void *foo;' +# 2: Dangling 'void *foo;' +# Dangling: the '*' will not be taken into account when aligning. +align_var_def_star_style = 0 # unsigned number + +# How to consider (or treat) the '&' in the alignment of variable definitions. +# +# 0: Part of the type 'long & foo;' (default) +# 1: Part of the variable 'long &foo;' +# 2: Dangling 'long &foo;' +# Dangling: the '&' will not be taken into account when aligning. +align_var_def_amp_style = 0 # unsigned number + +# The threshold for aligning variable definitions. +# Use a negative number for absolute thresholds. +# +# 0: No limit (default). +align_var_def_thresh = 0 # number + +# The gap for aligning variable definitions. +align_var_def_gap = 0 # unsigned number + +# Whether to align the colon in struct bit fields. +align_var_def_colon = false # true/false + +# The gap for aligning the colon in struct bit fields. +align_var_def_colon_gap = 0 # unsigned number + +# Whether to align any attribute after the variable name. +align_var_def_attribute = false # true/false + +# Whether to align inline struct/enum/union variable definitions. +align_var_def_inline = false # true/false + +# The span for aligning on '=' in assignments. +# +# 0: Don't align (default). +align_assign_span = 1 # unsigned number + +# The span for aligning on '=' in function prototype modifier. +# +# 0: Don't align (default). +align_assign_func_proto_span = 0 # unsigned number + +# The threshold for aligning on '=' in assignments. +# Use a negative number for absolute thresholds. +# +# 0: No limit (default). +align_assign_thresh = 0 # number + +# How to apply align_assign_span to function declaration "assignments", i.e. +# 'virtual void foo() = 0' or '~foo() = {default|delete}'. +# +# 0: Align with other assignments (default) +# 1: Align with each other, ignoring regular assignments +# 2: Don't align +align_assign_decl_func = 0 # unsigned number + +# The span for aligning on '=' in enums. +# +# 0: Don't align (default). +align_enum_equ_span = 1 # unsigned number + +# The threshold for aligning on '=' in enums. +# Use a negative number for absolute thresholds. +# +# 0: no limit (default). +align_enum_equ_thresh = 0 # number + +# The span for aligning class member definitions. +# +# 0: Don't align (default). +align_var_class_span = 1 # unsigned number + +# The threshold for aligning class member definitions. +# Use a negative number for absolute thresholds. +# +# 0: No limit (default). +align_var_class_thresh = 0 # number + +# The gap for aligning class member definitions. +align_var_class_gap = 0 # unsigned number + +# The span for aligning struct/union member definitions. +# +# 0: Don't align (default). +align_var_struct_span = 1 # unsigned number + +# The threshold for aligning struct/union member definitions. +# Use a negative number for absolute thresholds. +# +# 0: No limit (default). +align_var_struct_thresh = 0 # number + +# The gap for aligning struct/union member definitions. +align_var_struct_gap = 0 # unsigned number + +# The span for aligning struct initializer values. +# +# 0: Don't align (default). +align_struct_init_span = 1 # unsigned number + +# The span for aligning single-line typedefs. +# +# 0: Don't align (default). +align_typedef_span = 0 # unsigned number + +# The minimum space between the type and the synonym of a typedef. +align_typedef_gap = 0 # unsigned number + +# How to align typedef'd functions with other typedefs. +# +# 0: Don't mix them at all (default) +# 1: Align the open parenthesis with the types +# 2: Align the function type name with the other type names +align_typedef_func = 0 # unsigned number + +# How to consider (or treat) the '*' in the alignment of typedefs. +# +# 0: Part of the typedef type, 'typedef int * pint;' (default) +# 1: Part of type name: 'typedef int *pint;' +# 2: Dangling: 'typedef int *pint;' +# Dangling: the '*' will not be taken into account when aligning. +align_typedef_star_style = 0 # unsigned number + +# How to consider (or treat) the '&' in the alignment of typedefs. +# +# 0: Part of the typedef type, 'typedef int & intref;' (default) +# 1: Part of type name: 'typedef int &intref;' +# 2: Dangling: 'typedef int &intref;' +# Dangling: the '&' will not be taken into account when aligning. +align_typedef_amp_style = 0 # unsigned number + +# The span for aligning comments that end lines. +# +# 0: Don't align (default). +align_right_cmt_span = 1 # unsigned number + +# Minimum number of columns between preceding text and a trailing comment in +# order for the comment to qualify for being aligned. Must be non-zero to have +# an effect. +align_right_cmt_gap = 0 # unsigned number + +# If aligning comments, whether to mix with comments after '}' and #endif with +# less than three spaces before the comment. +align_right_cmt_mix = false # true/false + +# Whether to only align trailing comments that are at the same brace level. +align_right_cmt_same_level = false # true/false + +# Minimum column at which to align trailing comments. Comments which are +# aligned beyond this column, but which can be aligned in a lesser column, +# may be "pulled in". +# +# 0: Ignore (default). +align_right_cmt_at_col = 0 # unsigned number + +# The span for aligning function prototypes. +# +# 0: Don't align (default). +align_func_proto_span = 0 # unsigned number + +# The threshold for aligning function prototypes. +# Use a negative number for absolute thresholds. +# +# 0: No limit (default). +align_func_proto_thresh = 0 # number + +# Minimum gap between the return type and the function name. +align_func_proto_gap = 0 # unsigned number + +# Whether to align function prototypes on the 'operator' keyword instead of +# what follows. +align_on_operator = false # true/false + +# Whether to mix aligning prototype and variable declarations. If true, +# align_var_def_XXX options are used instead of align_func_proto_XXX options. +align_mix_var_proto = false # true/false + +# Whether to align single-line functions with function prototypes. +# Uses align_func_proto_span. +align_single_line_func = false # true/false + +# Whether to align the open brace of single-line functions. +# Requires align_single_line_func=true. Uses align_func_proto_span. +align_single_line_brace = false # true/false + +# Gap for align_single_line_brace. +align_single_line_brace_gap = 0 # unsigned number + +# (OC) The span for aligning Objective-C message specifications. +# +# 0: Don't align (default). +align_oc_msg_spec_span = 0 # unsigned number + +# Whether to align macros wrapped with a backslash and a newline. This will +# not work right if the macro contains a multi-line comment. +align_nl_cont = false # true/false + +# Whether to align macro functions and variables together. +align_pp_define_together = false # true/false + +# The span for aligning on '#define' bodies. +# +# =0: Don't align (default) +# >0: Number of lines (including comments) between blocks +align_pp_define_span = 0 # unsigned number + +# The minimum space between label and value of a preprocessor define. +align_pp_define_gap = 0 # unsigned number + +# Whether to align lines that start with '<<' with previous '<<'. +# +# Default: true +align_left_shift = true # true/false + +# Whether to align comma-separated statements following '<<' (as used to +# initialize Eigen matrices). +align_eigen_comma_init = false # true/false + +# Whether to align text after 'asm volatile ()' colons. +align_asm_colon = false # true/false + +# (OC) Span for aligning parameters in an Objective-C message call +# on the ':'. +# +# 0: Don't align. +align_oc_msg_colon_span = 0 # unsigned number + +# (OC) Whether to always align with the first parameter, even if it is too +# short. +align_oc_msg_colon_first = false # true/false + +# (OC) Whether to align parameters in an Objective-C '+' or '-' declaration +# on the ':'. +align_oc_decl_colon = false # true/false + +# (OC) Whether to not align parameters in an Objectve-C message call if first +# colon is not on next line of the message call (the same way Xcode does +# aligment) +align_oc_msg_colon_xcode_like = false # true/false + +# +# Comment modification options +# + +# Try to wrap comments at N columns. +cmt_width = 0 # unsigned number + +# How to reflow comments. +# +# 0: No reflowing (apart from the line wrapping due to cmt_width) (default) +# 1: No touching at all +# 2: Full reflow +cmt_reflow_mode = 1 # unsigned number + +# Whether to convert all tabs to spaces in comments. If false, tabs in +# comments are left alone, unless used for indenting. +cmt_convert_tab_to_spaces = false # true/false + +# Whether to apply changes to multi-line comments, including cmt_width, +# keyword substitution and leading chars. +# +# Default: true +cmt_indent_multi = false # true/false + +# Whether to group c-comments that look like they are in a block. +cmt_c_group = false # true/false + +# Whether to put an empty '/*' on the first line of the combined c-comment. +cmt_c_nl_start = false # true/false + +# Whether to add a newline before the closing '*/' of the combined c-comment. +cmt_c_nl_end = false # true/false + +# Whether to change cpp-comments into c-comments. +cmt_cpp_to_c = false # true/false + +# Whether to group cpp-comments that look like they are in a block. Only +# meaningful if cmt_cpp_to_c=true. +cmt_cpp_group = false # true/false + +# Whether to put an empty '/*' on the first line of the combined cpp-comment +# when converting to a c-comment. +# +# Requires cmt_cpp_to_c=true and cmt_cpp_group=true. +cmt_cpp_nl_start = false # true/false + +# Whether to add a newline before the closing '*/' of the combined cpp-comment +# when converting to a c-comment. +# +# Requires cmt_cpp_to_c=true and cmt_cpp_group=true. +cmt_cpp_nl_end = false # true/false + +# Whether to put a star on subsequent comment lines. +cmt_star_cont = false # true/false + +# The number of spaces to insert at the start of subsequent comment lines. +cmt_sp_before_star_cont = 0 # unsigned number + +# The number of spaces to insert after the star on subsequent comment lines. +cmt_sp_after_star_cont = 0 # unsigned number + +# For multi-line comments with a '*' lead, remove leading spaces if the first +# and last lines of the comment are the same length. +# +# Default: true +cmt_multi_check_last = true # true/false + +# For multi-line comments with a '*' lead, remove leading spaces if the first +# and last lines of the comment are the same length AND if the length is +# bigger as the first_len minimum. +# +# Default: 4 +cmt_multi_first_len_minimum = 4 # unsigned number + +# Path to a file that contains text to insert at the beginning of a file if +# the file doesn't start with a C/C++ comment. If the inserted text contains +# '$(filename)', that will be replaced with the current file's name. +cmt_insert_file_header = "" # string + +# Path to a file that contains text to insert at the end of a file if the +# file doesn't end with a C/C++ comment. If the inserted text contains +# '$(filename)', that will be replaced with the current file's name. +cmt_insert_file_footer = "" # string + +# Path to a file that contains text to insert before a function definition if +# the function isn't preceded by a C/C++ comment. If the inserted text +# contains '$(function)', '$(javaparam)' or '$(fclass)', these will be +# replaced with, respectively, the name of the function, the javadoc '@param' +# and '@return' stuff, or the name of the class to which the member function +# belongs. +cmt_insert_func_header = "" # string + +# Path to a file that contains text to insert before a class if the class +# isn't preceded by a C/C++ comment. If the inserted text contains '$(class)', +# that will be replaced with the class name. +cmt_insert_class_header = "" # string + +# Path to a file that contains text to insert before an Objective-C message +# specification, if the method isn't preceded by a C/C++ comment. If the +# inserted text contains '$(message)' or '$(javaparam)', these will be +# replaced with, respectively, the name of the function, or the javadoc +# '@param' and '@return' stuff. +cmt_insert_oc_msg_header = "" # string + +# Whether a comment should be inserted if a preprocessor is encountered when +# stepping backwards from a function name. +# +# Applies to cmt_insert_oc_msg_header, cmt_insert_func_header and +# cmt_insert_class_header. +cmt_insert_before_preproc = false # true/false + +# Whether a comment should be inserted if a function is declared inline to a +# class definition. +# +# Applies to cmt_insert_func_header. +# +# Default: true +cmt_insert_before_inlines = true # true/false + +# Whether a comment should be inserted if the function is a class constructor +# or destructor. +# +# Applies to cmt_insert_func_header. +cmt_insert_before_ctor_dtor = false # true/false + +# +# Code modifying options (non-whitespace) +# + +# Add or remove braces on a single-line 'do' statement. +mod_full_brace_do = ignore # ignore/add/remove/force + +# Add or remove braces on a single-line 'for' statement. +mod_full_brace_for = ignore # ignore/add/remove/force + +# (Pawn) Add or remove braces on a single-line function definition. +mod_full_brace_function = ignore # ignore/add/remove/force + +# Add or remove braces on a single-line 'if' statement. Braces will not be +# removed if the braced statement contains an 'else'. +mod_full_brace_if = ignore # ignore/add/remove/force + +# Whether to enforce that all blocks of an 'if'/'else if'/'else' chain either +# have, or do not have, braces. If true, braces will be added if any block +# needs braces, and will only be removed if they can be removed from all +# blocks. +# +# Overrides mod_full_brace_if. +mod_full_brace_if_chain = false # true/false + +# Whether to add braces to all blocks of an 'if'/'else if'/'else' chain. +# If true, mod_full_brace_if_chain will only remove braces from an 'if' that +# does not have an 'else if' or 'else'. +mod_full_brace_if_chain_only = false # true/false + +# Add or remove braces on single-line 'while' statement. +mod_full_brace_while = ignore # ignore/add/remove/force + +# Add or remove braces on single-line 'using ()' statement. +mod_full_brace_using = ignore # ignore/add/remove/force + +# Don't remove braces around statements that span N newlines +mod_full_brace_nl = 0 # unsigned number + +# Whether to prevent removal of braces from 'if'/'for'/'while'/etc. blocks +# which span multiple lines. +# +# Affects: +# mod_full_brace_for +# mod_full_brace_if +# mod_full_brace_if_chain +# mod_full_brace_if_chain_only +# mod_full_brace_while +# mod_full_brace_using +# +# Does not affect: +# mod_full_brace_do +# mod_full_brace_function +mod_full_brace_nl_block_rem_mlcond = true # true/false + +# Add or remove unnecessary parenthesis on 'return' statement. +mod_paren_on_return = ignore # ignore/add/remove/force + +# (Pawn) Whether to change optional semicolons to real semicolons. +mod_pawn_semicolon = false # true/false + +# Whether to fully parenthesize Boolean expressions in 'while' and 'if' +# statement, as in 'if (a && b > c)' => 'if (a && (b > c))'. +mod_full_paren_if_bool = false # true/false + +# Whether to remove superfluous semicolons. +mod_remove_extra_semicolon = false # true/false + +# If a function body exceeds the specified number of newlines and doesn't have +# a comment after the close brace, a comment will be added. +mod_add_long_function_closebrace_comment = 0 # unsigned number + +# If a namespace body exceeds the specified number of newlines and doesn't +# have a comment after the close brace, a comment will be added. +mod_add_long_namespace_closebrace_comment = 0 # unsigned number + +# If a class body exceeds the specified number of newlines and doesn't have a +# comment after the close brace, a comment will be added. +mod_add_long_class_closebrace_comment = 0 # unsigned number + +# If a switch body exceeds the specified number of newlines and doesn't have a +# comment after the close brace, a comment will be added. +mod_add_long_switch_closebrace_comment = 0 # unsigned number + +# If an #ifdef body exceeds the specified number of newlines and doesn't have +# a comment after the #endif, a comment will be added. +mod_add_long_ifdef_endif_comment = 0 # unsigned number + +# If an #ifdef or #else body exceeds the specified number of newlines and +# doesn't have a comment after the #else, a comment will be added. +mod_add_long_ifdef_else_comment = 0 # unsigned number + +# Whether to take care of the case by the mod_sort_xx options. +mod_sort_case_sensitive = false # true/false + +# Whether to sort consecutive single-line 'import' statements. +mod_sort_import = false # true/false + +# (C#) Whether to sort consecutive single-line 'using' statements. +mod_sort_using = false # true/false + +# Whether to sort consecutive single-line '#include' statements (C/C++) and +# '#import' statements (Objective-C). Be aware that this has the potential to +# break your code if your includes/imports have ordering dependencies. +mod_sort_include = false # true/false + +# Whether to prioritize '#include' and '#import' statements that contain +# filename without extension when sorting is enabled. +mod_sort_incl_import_prioritize_filename = false # true/false + +# Whether to prioritize '#include' and '#import' statements that does not +# contain extensions when sorting is enabled. +mod_sort_incl_import_prioritize_extensionless = false # true/false + +# Whether to prioritize '#include' and '#import' statements that contain +# angle over quotes when sorting is enabled. +mod_sort_incl_import_prioritize_angle_over_quotes = false # true/false + +# Whether to ignore file extension in '#include' and '#import' statements +# for sorting comparison. +mod_sort_incl_import_ignore_extension = false # true/false + +# Whether to group '#include' and '#import' statements when sorting is enabled. +mod_sort_incl_import_grouping_enabled = false # true/false + +# Whether to move a 'break' that appears after a fully braced 'case' before +# the close brace, as in 'case X: { ... } break;' => 'case X: { ... break; }'. +mod_move_case_break = false # true/false + +# Add or remove braces around a fully braced case statement. Will only remove +# braces if there are no variable declarations in the block. +mod_case_brace = ignore # ignore/add/remove/force + +# Whether to remove a void 'return;' that appears as the last statement in a +# function. +mod_remove_empty_return = false # true/false + +# Add or remove the comma after the last value of an enumeration. +mod_enum_last_comma = ignore # ignore/add/remove/force + +# (OC) Whether to organize the properties. If true, properties will be +# rearranged according to the mod_sort_oc_property_*_weight factors. +mod_sort_oc_properties = false # true/false + +# (OC) Weight of a class property modifier. +mod_sort_oc_property_class_weight = 0 # number + +# (OC) Weight of 'atomic' and 'nonatomic'. +mod_sort_oc_property_thread_safe_weight = 0 # number + +# (OC) Weight of 'readwrite' when organizing properties. +mod_sort_oc_property_readwrite_weight = 0 # number + +# (OC) Weight of a reference type specifier ('retain', 'copy', 'assign', +# 'weak', 'strong') when organizing properties. +mod_sort_oc_property_reference_weight = 0 # number + +# (OC) Weight of getter type ('getter=') when organizing properties. +mod_sort_oc_property_getter_weight = 0 # number + +# (OC) Weight of setter type ('setter=') when organizing properties. +mod_sort_oc_property_setter_weight = 0 # number + +# (OC) Weight of nullability type ('nullable', 'nonnull', 'null_unspecified', +# 'null_resettable') when organizing properties. +mod_sort_oc_property_nullability_weight = 0 # number + +# +# Preprocessor options +# + +# Add or remove indentation of preprocessor directives inside #if blocks +# at brace level 0 (file-level). +pp_indent = ignore # ignore/add/remove/force + +# Whether to indent #if/#else/#endif at the brace level. If false, these are +# indented from column 1. +pp_indent_at_level = false # true/false + +# Specifies the number of columns to indent preprocessors per level +# at brace level 0 (file-level). If pp_indent_at_level=false, also specifies +# the number of columns to indent preprocessors per level +# at brace level > 0 (function-level). +# +# Default: 1 +pp_indent_count = 1 # unsigned number + +# Add or remove space after # based on pp_level of #if blocks. +pp_space = ignore # ignore/add/remove/force + +# Sets the number of spaces per level added with pp_space. +pp_space_count = 0 # unsigned number + +# The indent for '#region' and '#endregion' in C# and '#pragma region' in +# C/C++. Negative values decrease indent down to the first column. +pp_indent_region = 0 # number + +# Whether to indent the code between #region and #endregion. +pp_region_indent_code = false # true/false + +# If pp_indent_at_level=true, sets the indent for #if, #else and #endif when +# not at file-level. Negative values decrease indent down to the first column. +# +# =0: Indent preprocessors using output_tab_size +# >0: Column at which all preprocessors will be indented +pp_indent_if = 0 # number + +# Whether to indent the code between #if, #else and #endif. +pp_if_indent_code = false # true/false + +# Whether to indent '#define' at the brace level. If false, these are +# indented from column 1. +pp_define_at_level = false # true/false + +# Whether to ignore the '#define' body while formatting. +pp_ignore_define_body = false # true/false + +# Whether to indent case statements between #if, #else, and #endif. +# Only applies to the indent of the preprocesser that the case statements +# directly inside of. +# +# Default: true +pp_indent_case = true # true/false + +# Whether to indent whole function definitions between #if, #else, and #endif. +# Only applies to the indent of the preprocesser that the function definition +# is directly inside of. +# +# Default: true +pp_indent_func_def = true # true/false + +# Whether to indent extern C blocks between #if, #else, and #endif. +# Only applies to the indent of the preprocesser that the extern block is +# directly inside of. +# +# Default: true +pp_indent_extern = true # true/false + +# Whether to indent braces directly inside #if, #else, and #endif. +# Only applies to the indent of the preprocesser that the braces are directly +# inside of. +# +# Default: true +pp_indent_brace = true # true/false + +# +# Sort includes options +# + +# The regex for include category with priority 0. +include_category_0 = "" # string + +# The regex for include category with priority 1. +include_category_1 = "" # string + +# The regex for include category with priority 2. +include_category_2 = "" # string + +# +# Use or Do not Use options +# + +# true: indent_func_call_param will be used (default) +# false: indent_func_call_param will NOT be used +# +# Default: true +use_indent_func_call_param = true # true/false + +# The value of the indentation for a continuation line is calculated +# differently if the statement is: +# - a declaration: your case with QString fileName ... +# - an assignment: your case with pSettings = new QSettings( ... +# +# At the second case the indentation value might be used twice: +# - at the assignment +# - at the function call (if present) +# +# To prevent the double use of the indentation value, use this option with the +# value 'true'. +# +# true: indent_continue will be used only once +# false: indent_continue will be used every time (default) +use_indent_continue_only_once = false # true/false + +# The value might be used twice: +# - at the assignment +# - at the opening brace +# +# To prevent the double use of the indentation value, use this option with the +# value 'true'. +# +# true: indentation will be used only once +# false: indentation will be used every time (default) +indent_cpp_lambda_only_once = false # true/false + +# Whether sp_after_angle takes precedence over sp_inside_fparen. This was the +# historic behavior, but is probably not the desired behavior, so this is off +# by default. +use_sp_after_angle_always = false # true/false + +# Whether to apply special formatting for Qt SIGNAL/SLOT macros. Essentially, +# this tries to format these so that they match Qt's normalized form (i.e. the +# result of QMetaObject::normalizedSignature), which can slightly improve the +# performance of the QObject::connect call, rather than how they would +# otherwise be formatted. +# +# See options_for_QT.cpp for details. +# +# Default: true +use_options_overriding_for_qt_macros = true # true/false + +# If true: the form feed character is removed from the list +# of whitespace characters. +# See https://en.cppreference.com/w/cpp/string/byte/isspace +use_form_feed_no_more_as_whitespace_character = false # true/false + +# +# Warn levels - 1: error, 2: warning (default), 3: note +# + +# (C#) Warning is given if doing tab-to-\t replacement and we have found one +# in a C# verbatim string literal. +# +# Default: 2 +warn_level_tabs_found_in_verbatim_string_literals = 2 # unsigned number + +# Limit the number of loops. +# Used by uncrustify.cpp to exit from infinite loop. +# 0: no limit. +debug_max_number_of_loops = 0 # number + +# Set the number of the line to protocol; +# Used in the function prot_the_line if the 2. parameter is zero. +# 0: nothing protocol. +debug_line_number_to_protocol = 0 # number + +# Set the number of second(s) before terminating formatting the current file, +# 0: no timeout. +# only for linux +debug_timeout = 0 # number + +# Meaning of the settings: +# Ignore - do not do any changes +# Add - makes sure there is 1 or more space/brace/newline/etc +# Force - makes sure there is exactly 1 space/brace/newline/etc, +# behaves like Add in some contexts +# Remove - removes space/brace/newline/etc +# +# +# - Token(s) can be treated as specific type(s) with the 'set' option: +# `set tokenType tokenString [tokenString...]` +# +# Example: +# `set BOOL __AND__ __OR__` +# +# tokenTypes are defined in src/token_enum.h, use them without the +# 'CT_' prefix: 'CT_BOOL' => 'BOOL' +# +# +# - Token(s) can be treated as type(s) with the 'type' option. +# `type tokenString [tokenString...]` +# +# Example: +# `type int c_uint_8 Rectangle` +# +# This can also be achieved with `set TYPE int c_uint_8 Rectangle` +# +# +# To embed whitespace in tokenStrings use the '\' escape character, or quote +# the tokenStrings. These quotes are supported: "'` +# +# +# - Support for the auto detection of languages through the file ending can be +# added using the 'file_ext' command. +# `file_ext langType langString [langString..]` +# +# Example: +# `file_ext CPP .ch .cxx .cpp.in` +# +# langTypes are defined in uncrusify_types.h in the lang_flag_e enum, use +# them without the 'LANG_' prefix: 'LANG_CPP' => 'CPP' +# +# +# - Custom macro-based indentation can be set up using 'macro-open', +# 'macro-else' and 'macro-close'. +# `(macro-open | macro-else | macro-close) tokenString` +# +# Example: +# `macro-open BEGIN_TEMPLATE_MESSAGE_MAP` +# `macro-open BEGIN_MESSAGE_MAP` +# `macro-close END_MESSAGE_MAP` +# +# +# option(s) with 'not default' value: 0 +# From 2de84558846a9f641871111332c0e22f674d45b7 Mon Sep 17 00:00:00 2001 From: Lucki Date: Thu, 25 Mar 2021 12:56:42 +0100 Subject: [PATCH 03/22] Try some DLC handling --- .../sources/epicgames/EpicDownloader.vala | 8 +-- src/data/sources/epicgames/EpicGame.vala | 7 +++ src/data/sources/epicgames/EpicGames.vala | 60 ++++++++++++------- src/data/sources/epicgames/EpicInstaller.vala | 32 ++++++---- 4 files changed, 70 insertions(+), 37 deletions(-) diff --git a/src/data/sources/epicgames/EpicDownloader.vala b/src/data/sources/epicgames/EpicDownloader.vala index e951b9d2..7e231d87 100644 --- a/src/data/sources/epicgames/EpicDownloader.vala +++ b/src/data/sources/epicgames/EpicDownloader.vala @@ -46,7 +46,7 @@ namespace GameHub.Data.Sources.EpicGames { var parts = new ArrayList(); debug("preparing download"); - installer.analysis = installer.game.prepare_download(installer.task); + installer.analysis = installer.game.prepare_download(installer.install_task); // game is either up to date or hasn't changed, so we have nothing to do if(installer.analysis.result.dl_size < 1) @@ -115,12 +115,12 @@ namespace GameHub.Data.Sources.EpicGames var ds_id = download_manager().file_download_started.connect(dl => { if(dl.id != game.full_id) return; - installer.task.status = new Tasks.Install.InstallTask.Status( + installer.install_task.status = new Tasks.Install.InstallTask.Status( Tasks.Install.InstallTask.State.DOWNLOADING, dl); // installer.download_state = new DownloadState(DownloadState.State.DOWNLOADING, dl); dl.status_change.connect(s => { - installer.task.notify_property("status"); + installer.install_task.notify_property("status"); }); }); @@ -142,7 +142,7 @@ namespace GameHub.Data.Sources.EpicGames Download.State.DOWNLOADING, installer.full_size, // FIXME: total size is wrong for partial updates - (current_part * 1048576) / installer.full_size, // Chunks are mostly 1 MiB + current_part / parts.size, // Chunks are mostly 1 MiB -1, -1); } diff --git a/src/data/sources/epicgames/EpicGame.vala b/src/data/sources/epicgames/EpicGame.vala index aa3537f0..0e34b2a0 100644 --- a/src/data/sources/epicgames/EpicGame.vala +++ b/src/data/sources/epicgames/EpicGame.vala @@ -1343,6 +1343,13 @@ namespace GameHub.Data.Sources.EpicGames this.game = game; update_status(); } + + public override void update_status() + { + if(game == null) return; + + base.update_status(); + } } public class Asset diff --git a/src/data/sources/epicgames/EpicGames.vala b/src/data/sources/epicgames/EpicGames.vala index a3c1f061..bf6472b2 100644 --- a/src/data/sources/epicgames/EpicGames.vala +++ b/src/data/sources/epicgames/EpicGames.vala @@ -29,7 +29,6 @@ namespace GameHub.Data.Sources.EpicGames public override string name { get { return "EpicGames"; } } public override string icon { get { return "source-epicgames-symbolic"; } } public override ArrayList games { get; default = new ArrayList(Game.is_equal); } - public HashMap owned_games { get; default = new HashMap(null, null, Game.is_equal); } public override bool enabled { @@ -298,7 +297,6 @@ namespace GameHub.Data.Sources.EpicGames if(!Settings.UI.Behavior.instance.merge_games || !Tables.Merges.is_game_merged(g)) { _games.add(g); - owned_games.set(g.id, g); if(game_loaded != null) { @@ -315,7 +313,7 @@ namespace GameHub.Data.Sources.EpicGames cache_loaded(); } - get_game_and_dlc_list(); + var owned_games = get_game_and_dlc_list(true); owned_games.foreach(tuple => { @@ -326,7 +324,6 @@ namespace GameHub.Data.Sources.EpicGames { _games.add(game); - // owned_games.set(game.id, game); if(game_loaded != null) { game_loaded(game, false); @@ -596,20 +593,42 @@ namespace GameHub.Data.Sources.EpicGames public void asset_valid() {} - public EpicGame? get_game(EpicGame game, bool update_meta = false) + public EpicGame? get_game(string id, bool update_meta = false) { - if(update_meta) get_game_and_dlc_list(true); + if(update_meta) + { + var owned_games = get_game_and_dlc_list(true); - return (EpicGame) owned_games.get(game.id); + _games.foreach(game => { + if(owned_games.has_key(game.id)) + { + game = owned_games.get(game.id); + owned_games.unset(game.id); + } + + return true; + }); + + if(!owned_games.is_empty) + { + _games.add_all(owned_games.values); + } + } + + return (EpicGame) _games.first_match(game => { + return game.id == id; + }); } // Not needed, dlcs are always bound to games // public void get_game_list() {} - public void get_game_and_dlc_list(bool update_assets = true, - string? platform_override = null, - bool skip_unreal_engine = true) + public HashMap get_game_and_dlc_list(bool update_assets = true, + string? platform_override = null, + bool skip_unreal_engine = true) { + HashMap owned_games = new HashMap(); + // I don't really need the inner HashMap - a list of tuples would be enough. // Vala should be able to handle tuples but I couldn't figure it out var dlcs = new HashMap >(); @@ -617,16 +636,15 @@ namespace GameHub.Data.Sources.EpicGames var tmp_assets = get_game_assets(update_assets, platform_override); foreach(var asset in tmp_assets) { + Json.Node? metadata = null; + if(asset.ns == "ue" && skip_unreal_engine) continue; - var game = (EpicGame) owned_games.get(asset.app_name); - Json.Node? metadata = null; + var game = get_game(asset.app_name); - if(update_assets && - (game == null - || (game != null - && game.version != asset.build_version - && platform_override != null))) + if(update_assets && (game == null || (game != null + && game.version != asset.build_version + && platform_override != null))) { if(game != null && game.version != asset.build_version @@ -673,7 +691,7 @@ namespace GameHub.Data.Sources.EpicGames } else { - owned_games.set(asset.app_name, game); + owned_games.set(game.id, game); } // TODO: mods? @@ -682,14 +700,16 @@ namespace GameHub.Data.Sources.EpicGames // we got all games, add the dlcs to it foreach(var game_name in dlcs) { - if(game_name.value == null) return; + if(game_name.value == null) continue; foreach(var tuple in game_name.value) { - var game = (EpicGame) owned_games.get(game_name.key); + var game = owned_games.get(game_name.key); game.add_dlc(tuple.key, tuple.value); } } + + return owned_games; } public void get_dlc_for_game() {} diff --git a/src/data/sources/epicgames/EpicInstaller.vala b/src/data/sources/epicgames/EpicInstaller.vala index e8b4dbf4..59c11450 100644 --- a/src/data/sources/epicgames/EpicInstaller.vala +++ b/src/data/sources/epicgames/EpicInstaller.vala @@ -8,9 +8,9 @@ namespace GameHub.Data.Sources.EpicGames { internal class Installer: Runnables.Tasks.Install.Installer { - internal Analysis? analysis { get; set; default = null; } - internal EpicGame game { get; private set; } - internal InstallTask? task { get; default = null; } + internal Analysis? analysis { get; set; default = null; } + internal EpicGame game { get; private set; } + internal InstallTask? install_task { get; default = null; } internal Installer(EpicGame game, Platform platform) { @@ -41,7 +41,14 @@ namespace GameHub.Data.Sources.EpicGames internal override async bool install(InstallTask task) { - _task = task; + _install_task = task; + + if(game is EpicGame.DLC) + { + if(((EpicGame.DLC)game).game.install_dir == null) return false; + + _install_task.install_dir = ((EpicGame.DLC)game).game.install_dir; + } debug("starting installation"); var downloader = new EpicDownloader(); @@ -52,7 +59,7 @@ namespace GameHub.Data.Sources.EpicGames // download_task should be available here with all required information // tasks should be in the correct order open -> write chunk -> close - var full_path = task.install_dir; + var full_path = install_task.install_dir; FileOutputStream? iostream = null; foreach(var file_task in analysis.tasks) @@ -60,7 +67,7 @@ namespace GameHub.Data.Sources.EpicGames if(file_task is Analysis.FileTask) { // make directories - full_path = File.new_build_filename(task.install_dir.get_path(), + full_path = File.new_build_filename(install_task.install_dir.get_path(), ((Analysis.FileTask)file_task).filename); FS.mkdir(full_path.get_parent().get_path()); @@ -150,9 +157,9 @@ namespace GameHub.Data.Sources.EpicGames if(((Analysis.ChunkTask)file_task).chunk_file != null) { // reuse chunk from existing file - assert(File.new_build_filename(task.install_dir.get_path(), + assert(File.new_build_filename(install_task.install_dir.get_path(), ((Analysis.ChunkTask)file_task).chunk_file).query_exists()); - old_stream = File.new_build_filename(task.install_dir.get_path(), + old_stream = File.new_build_filename(install_task.install_dir.get_path(), ((Analysis.ChunkTask)file_task).chunk_file).read(); old_stream.seek(((Analysis.ChunkTask)file_task).chunk_offset, SeekType.SET); var bytes = yield old_stream.read_bytes_async(((Analysis.ChunkTask)file_task).chunk_size); @@ -167,8 +174,7 @@ namespace GameHub.Data.Sources.EpicGames // debug(@"chunk data length $(chunk.data.length)"); // debug("chunk %s hash: %s", // ((Analysis.ChunkTask)file_task).chunk_guid.to_string(), - // Checksum.compute_for_bytes(ChecksumType.SHA1, - // chunk.data)); + // Checksum.compute_for_bytes(ChecksumType.SHA1, chunk.data)); var size = yield iostream.write_bytes_async(chunk.data[((Analysis.ChunkTask)file_task).chunk_offset : ((Analysis.ChunkTask)file_task).chunk_offset + ((Analysis.ChunkTask)file_task).chunk_size]); // debug(@"written $size bytes"); } @@ -189,7 +195,7 @@ namespace GameHub.Data.Sources.EpicGames // This should do three steps: Import -> verify -> repair/update internal override async bool import(InstallTask task) { - _task = task; + _install_task = task; task.status = new InstallTask.Status(InstallTask.State.INSTALLING); game.status = new Game.Status(Game.State.INSTALLING, this.game); @@ -228,8 +234,8 @@ namespace GameHub.Data.Sources.EpicGames game.manifest = EpicGames.load_manifest(game.load_manifest_from_disk()); game.update_metadata(); - game.install_dir = task.install_dir; - game.executable_path = FS.file(task.install_dir.get_path(), game.manifest.meta.launch_exe).get_path(); + game.install_dir = install_task.install_dir; + game.executable_path = FS.file(install_task.install_dir.get_path(), game.manifest.meta.launch_exe).get_path(); game.save(); game.update_status(); } From 2352c507748651be3f1ff3e618f550189cdc2343 Mon Sep 17 00:00:00 2001 From: Lucki Date: Thu, 25 Mar 2021 14:20:04 +0100 Subject: [PATCH 04/22] Misc --- src/data/sources/epicgames/EpicChunk.vala | 2 +- .../sources/epicgames/EpicDownloader.vala | 5 +- src/data/sources/epicgames/EpicGame.vala | 24 +------ src/data/sources/epicgames/EpicGames.vala | 16 ++--- .../sources/epicgames/EpicGamesServices.vala | 7 +++ src/data/sources/epicgames/EpicManifest.vala | 63 +++++++++---------- src/data/sources/epicgames/EpicUtils.vala | 1 - .../pages/sources/EpicGames.vala | 16 ++--- 8 files changed, 58 insertions(+), 76 deletions(-) diff --git a/src/data/sources/epicgames/EpicChunk.vala b/src/data/sources/epicgames/EpicChunk.vala index a36794c8..7e4707db 100644 --- a/src/data/sources/epicgames/EpicChunk.vala +++ b/src/data/sources/epicgames/EpicChunk.vala @@ -87,7 +87,7 @@ namespace GameHub.Data.Sources.EpicGames // _data = new Bytes(tmp); // } - // // FIXME: recalculate hashes + // // TODO: recalculate hashes // // _hash = get_hash(_data); // // _sha_hash = sha(_data); // _hash_type = 0x3; diff --git a/src/data/sources/epicgames/EpicDownloader.vala b/src/data/sources/epicgames/EpicDownloader.vala index 7e231d87..9528900b 100644 --- a/src/data/sources/epicgames/EpicDownloader.vala +++ b/src/data/sources/epicgames/EpicDownloader.vala @@ -140,9 +140,8 @@ namespace GameHub.Data.Sources.EpicGames download_description = _("Part %1$u of %2$u: %3$s").printf(current_part, parts.size, part.id); download.status = new EpicDownload.Status( Download.State.DOWNLOADING, - installer.full_size, - // FIXME: total size is wrong for partial updates - current_part / parts.size, // Chunks are mostly 1 MiB + (int64) installer.analysis.result.dl_size, + current_part / parts.size, -1, -1); } diff --git a/src/data/sources/epicgames/EpicGame.vala b/src/data/sources/epicgames/EpicGame.vala index 0e34b2a0..0d3075cc 100644 --- a/src/data/sources/epicgames/EpicGame.vala +++ b/src/data/sources/epicgames/EpicGame.vala @@ -323,8 +323,6 @@ namespace GameHub.Data.Sources.EpicGames image_vertical = icon; } - // TODO: get installed status - if(game_info_updated) { game_info_updating = false; @@ -408,10 +406,6 @@ namespace GameHub.Data.Sources.EpicGames // TODO: offline? assert(can_run_offline || yield EpicGames.instance.authenticate()); - // TODO: Do we need this? - // debug("[Source.EpicGame.pre_run] refreshing login…"); - // ((EpicGames) source).login(); - // TODO: check for updates if(latest_version != version) { @@ -766,21 +760,6 @@ namespace GameHub.Data.Sources.EpicGames GLib.info("[Source.EpicGames.import] Game has been imported: %s", id); return true; - - // Don't do this here, we're calling install afterwards anyway - // // FIXME: what even is this mess? - // game.prereq_info = prereq; - // // game.base_urls = base_urls; FIXME: - // game.install_dir = import_dir; - // game.version = manifest.meta.build_version; - // game.executable = File.new_build_filename("${install_dir}", manifest.meta.launch_exe); - // game.can_run_offline = offline; - // // game.launch_parameters = manifest.meta.launch_command; - // game.needs_verification = needs_verification; - // // game.install_size = install_size; - // game.egl_guid = egl_guid; - // // TODO: all above into meta? check what's needed - // // game.meta = manifest.meta; } internal async void verify() @@ -874,6 +853,7 @@ namespace GameHub.Data.Sources.EpicGames asset_info.catalog_item_id); // TODO: write to tmp path? write(FS.Paths.EpicGames.Cache, @"$(asset_info.ns)$(asset_info.catalog_item_id).ovt", ownership_token.get_data()); + // FIXME: needs wine path format? parameters += "-epicovt=%s".printf(FS.file(FS.Paths.EpicGames.Cache, @"$(asset_info.ns)$(asset_info.catalog_item_id).ovt").get_path()); } @@ -1278,7 +1258,7 @@ namespace GameHub.Data.Sources.EpicGames // return result; // } - // // TODO: make SaveGameFile an property of EpicGame + // // TODO: make SaveGameFile a property of EpicGame // private SaveGameFile.Status check_savegame_state(File path, SaveGameFile? save, out DateTime local, out DateTime remote) // { // // legendary does a os.walk here diff --git a/src/data/sources/epicgames/EpicGames.vala b/src/data/sources/epicgames/EpicGames.vala index bf6472b2..63c3b3e4 100644 --- a/src/data/sources/epicgames/EpicGames.vala +++ b/src/data/sources/epicgames/EpicGames.vala @@ -46,12 +46,13 @@ namespace GameHub.Data.Sources.EpicGames } } - internal string access_token + internal string? access_token { get { - assert(userdata.get_node_type() == Json.NodeType.OBJECT); - assert(userdata.get_object().has_member("access_token")); + return_val_if_fail(userdata.get_node_type() == Json.NodeType.OBJECT, null); + return_val_if_fail(userdata.get_object().has_member("access_token"), null); + return_val_if_fail(userdata.get_object().get_member("access_token").get_node_type() == Json.NodeType.VALUE, null); return userdata.get_object().get_string_member("access_token"); } @@ -149,12 +150,12 @@ namespace GameHub.Data.Sources.EpicGames if(access_expires.difference(now) < TimeSpan.MINUTE * 10) { - debug("[Sources.EpicGames.is_authenticated] Access token is less than 10 minutes valid."); + if(Application.log_auth) debug("[Sources.EpicGames.is_authenticated] Access token is less than 10 minutes valid."); return false; } - return access_token != ""; + return access_token != null && access_token.length > 0; } public override bool can_authenticate_automatically() @@ -535,7 +536,7 @@ namespace GameHub.Data.Sources.EpicGames public ArrayList get_game_assets(bool update_assets = false, string? platform_override = null) { - if(platform_override != null) + if(platform_override != null && access_token != null && access_token.length > 0) { var list = new ArrayList(); var games_json = EpicGamesServices.instance.get_game_assets(access_token, platform_override); @@ -550,9 +551,8 @@ namespace GameHub.Data.Sources.EpicGames } - if(update_assets || assets.is_empty) + if(update_assets || assets.is_empty && access_token != null && access_token.length > 0) { - // TODO: not logged in var games_json = EpicGamesServices.instance.get_game_assets(access_token); games_json.get_array().foreach_element((array, index, node) => { diff --git a/src/data/sources/epicgames/EpicGamesServices.vala b/src/data/sources/epicgames/EpicGamesServices.vala index 0792174a..065e349c 100644 --- a/src/data/sources/epicgames/EpicGamesServices.vala +++ b/src/data/sources/epicgames/EpicGamesServices.vala @@ -111,6 +111,7 @@ namespace GameHub.Data.Sources.EpicGames // https://dev.epicgames.com/docs/services/en-US/API/Members/Functions/Auth/EOS_Auth_VerifyUserAuth/index.html internal Json.Node resume_session(Json.Node userdata) requires(userdata.get_node_type() == Json.NodeType.OBJECT) + requires(EpicGames.instance.access_token != null && EpicGames.instance.access_token.length > 0) { var refreshed_json = Parser.parse_remote_json_file( @"https://$oauth_host/account/api/oauth/verify", @@ -172,6 +173,7 @@ namespace GameHub.Data.Sources.EpicGames } internal Json.Node get_game_token() + requires(EpicGames.instance.access_token != null && EpicGames.instance.access_token.length > 0) { uint status; var json = Parser.parse_remote_json_file( @@ -219,6 +221,7 @@ namespace GameHub.Data.Sources.EpicGames } internal Json.Node get_game_assets(string platform = "Windows", string label = "Live") + requires(EpicGames.instance.access_token != null && EpicGames.instance.access_token.length > 0) { uint status; var json = Parser.parse_remote_json_file( @@ -241,6 +244,7 @@ namespace GameHub.Data.Sources.EpicGames string app_name, string platform = "Windows", string label = "Live") + requires(EpicGames.instance.access_token != null && EpicGames.instance.access_token.length > 0) { uint status; var json = Parser.parse_remote_json_file( @@ -263,6 +267,7 @@ namespace GameHub.Data.Sources.EpicGames internal void get_user_entitlements() {} internal Json.Node get_game_info(string _namespace, string catalog_item_id) + requires(EpicGames.instance.access_token != null && EpicGames.instance.access_token.length > 0) { Gee.HashMap data = new Gee.HashMap(); @@ -294,6 +299,7 @@ namespace GameHub.Data.Sources.EpicGames } internal ArrayList get_library_items(bool include_metadata = true) + requires(EpicGames.instance.access_token != null && EpicGames.instance.access_token.length > 0) { ArrayList records = new ArrayList(); @@ -351,6 +357,7 @@ namespace GameHub.Data.Sources.EpicGames } internal Json.Node get_user_cloud_saves(string game_id = "", bool manifests = false, string? filenames = null) + requires(EpicGames.instance.access_token != null && EpicGames.instance.access_token.length > 0) { var app_name = game_id; diff --git a/src/data/sources/epicgames/EpicManifest.vala b/src/data/sources/epicgames/EpicManifest.vala index 41ed5a84..ba436393 100644 --- a/src/data/sources/epicgames/EpicManifest.vala +++ b/src/data/sources/epicgames/EpicManifest.vala @@ -684,14 +684,10 @@ namespace GameHub.Data.Sources.EpicGames */ internal class ChunkDataList { - private uint8 version { get; } + private uint8 version { get; } private uint32 manifest_version { get; } private uint32 size { get; } private uint32 count { get; } - Json.Object chunk_filesize_list; // FIXME: - Json.Object chunk_hash_list; // FIXME: - Json.Object chunk_sha_list; // FIXME: - Json.Object data_group_list; // FIXME: private HashMap guid_int_map { get; default = new HashMap(); } private HashMap guid_str_map { get; default = new HashMap(); } @@ -793,12 +789,12 @@ namespace GameHub.Data.Sources.EpicGames { var json_obj = json_data.get_object(); - _manifest_version = manifest_version; - _count = json_obj.get_object_member("ChunkFilesizeList").get_size(); - chunk_filesize_list = json_obj.get_object_member("ChunkFilesizeList"); - chunk_hash_list = json_obj.get_object_member("ChunkHashList"); - chunk_sha_list = json_obj.get_object_member("ChunkShaList"); - data_group_list = json_obj.get_object_member("DataGroupList"); + _manifest_version = manifest_version; + _count = json_obj.get_object_member("ChunkFilesizeList").get_size(); + var chunk_filesize_list = json_obj.get_object_member("ChunkFilesizeList"); + var chunk_hash_list = json_obj.get_object_member("ChunkHashList"); + var chunk_sha_list = json_obj.get_object_member("ChunkShaList"); + var data_group_list = json_obj.get_object_member("DataGroupList"); chunk_filesize_list.get_members().foreach(guid => { @@ -945,16 +941,29 @@ namespace GameHub.Data.Sources.EpicGames { if(_group_num == null) { - var bytes = new ByteArray(); + // var bytes = new ByteArray(); + var memory = new MemoryOutputStream.resizable(); + + try + { + var stream = new DataOutputStream(memory); + stream.set_byte_order(DataStreamByteOrder.LITTLE_ENDIAN); + + foreach(var id in guid) + { + stream.put_uint32(id); + } - foreach(var id in guid) + stream.close(); + memory.close(); + } + catch (Error e) { - var variant = new Variant.uint32(id); - variant.byteswap(); // FIXME: instead of hardcoded swapping try to set endian directly - bytes.append(variant.get_data_as_bytes().get_data()); + debug("error: %s", e.message); + assert_not_reached(); } - _group_num = (ZLib.Utility.crc32(0, bytes.data) & 0xffffffff) % 100; + _group_num = (ZLib.Utility.crc32(0, memory.steal_data()) & 0xffffffff) % 100; } return _group_num; @@ -1019,7 +1028,7 @@ namespace GameHub.Data.Sources.EpicGames * When the length is negative the following string is UTF-16 - otherwise it's ASCII? * In either case the {@link string} is returned as unescaped UTF-8 (uint8[]) */ - // TODO: clean up and verify this mess with UTF-16 and ASCII + // TODO: verify this with UTF-16 and ASCII private static string read_fstring(DataInputStream stream) { string result = ""; @@ -1032,30 +1041,16 @@ namespace GameHub.Data.Sources.EpicGames if(length < 0) { // utf-16 chars are 2 bytes wide but the length is # of characters, not bytes - // TODO: actually make sure utf-16 characters can't be longer than 2 bytes length *= -2; - // var tmp = stream.read_bytes(length - 2).get_data(); - // TODO: CharsetConverter oconverter = new CharsetConverter ("utf-16", "utf-8"); - // variant = new Variant.from_bytes(VariantType.STRING, stream.read_bytes(length), false); - // debug("[Sources.EpicGames.Manifest.read_fstring] string utf-16: %s", variant.get_string()); - result = convert((string) stream.read_bytes(length), -1, "UTF-8", "UTF-16"); // convert to utf8 - // debug("[Sources.EpicGames.Manifest.read_fstring] string utf-8: %s", result); - // stream.seek(2, GLib.SeekType.CUR); // utf-16 strings have two byte null terminators - // TODO: seek +1 for second null char? + result = convert((string) stream.read_bytes(length), -1, "UTF-8", "UTF-16"); // convert to utf8 } else if(length > 0) { - // variant = new Variant.from_bytes(VariantType.STRING, stream.read_bytes(length), false); result = (string) stream.read_bytes(length).get_data(); - // debug("[Sources.EpicGames.Manifest.read_fstring] string utf-8: %s", variant.get_string()); - // var tmp = (string) stream.read_bytes(length - 1).get_data(); - // result = convert((string) tmp, -1, "UTF-8", "ASCII"); - // result = variant.get_string(); - // stream.seek(1, GLib.SeekType.CUR); // skip string null terminator } else { - result = ""; // empty string, no terminators or anything + result = ""; // empty string } } catch (Error e) {} diff --git a/src/data/sources/epicgames/EpicUtils.vala b/src/data/sources/epicgames/EpicUtils.vala index 9a7dea9e..c63502c2 100644 --- a/src/data/sources/epicgames/EpicUtils.vala +++ b/src/data/sources/epicgames/EpicUtils.vala @@ -137,7 +137,6 @@ namespace GameHub.Data.Sources.EpicGames return builder.str; } - // TODO: replace with FileUtils.set_data() ? private static void write(string path, string name, uint8[] bytes) { var file = FS.file(path, name); diff --git a/src/ui/dialogs/SettingsDialog/pages/sources/EpicGames.vala b/src/ui/dialogs/SettingsDialog/pages/sources/EpicGames.vala index 895aabd6..ebf4bef8 100644 --- a/src/ui/dialogs/SettingsDialog/pages/sources/EpicGames.vala +++ b/src/ui/dialogs/SettingsDialog/pages/sources/EpicGames.vala @@ -8,7 +8,7 @@ namespace GameHub.UI.Dialogs.SettingsDialog.Pages.Sources { public class EpicGames: SettingsDialogPage { - private Settings.Auth.EpicGames epicgames_auth = Settings.Auth.EpicGames.instance; + private Settings.Auth.EpicGames epicgames_auth = Settings.Auth.EpicGames.instance; private Settings.Paths.EpicGames epicgames_paths = Settings.Paths.EpicGames.instance; private Widgets.Settings.BaseSetting? account_setting; @@ -22,8 +22,7 @@ namespace GameHub.UI.Dialogs.SettingsDialog.Pages.Sources title: "EpicGames", description: _("Disabled"), icon_name: "source-epicgames-symbolic", - has_active_switch: true - ); + has_active_switch: true); } construct @@ -43,7 +42,7 @@ namespace GameHub.UI.Dialogs.SettingsDialog.Pages.Sources logout_btn.clicked.connect( () => { epicgames.logout.begin(() => update()); - request_restart(); // TODO: Requires restart until we're able to reload games from an source + request_restart(); // TODO: Requires restart until we're able to reload games from a source }); account_link = new LinkButton.with_label("https://epicgames.com/account/personal", _("View account")); account_actions_box.add(logout_btn); @@ -53,9 +52,8 @@ namespace GameHub.UI.Dialogs.SettingsDialog.Pages.Sources new BaseSetting( epicgames.user_name != null ? _("Authenticated as %s").printf(epicgames.user_name) : _("Authenticated"), _("Legendary"), - account_actions_box - )); - account_setting.icon_name = "avatar-default-symbolic"; + account_actions_box)); + account_setting.icon_name = "avatar-default-symbolic"; account_setting.activatable = true; account_setting.setting_activated.connect(() => epicgames.authenticate.begin(() => update())); account_link.can_focus = false; @@ -105,6 +103,7 @@ namespace GameHub.UI.Dialogs.SettingsDialog.Pages.Sources { account_setting.title = _("Disabled"); } + description = _("Disabled"); } else if(!epicgames.is_installed(true)) @@ -113,6 +112,7 @@ namespace GameHub.UI.Dialogs.SettingsDialog.Pages.Sources { account_setting.title = _("Not installed"); } + description = _("Not installed"); } else if(!epicgames.is_authenticated()) @@ -121,6 +121,7 @@ namespace GameHub.UI.Dialogs.SettingsDialog.Pages.Sources { account_setting.title = _("Not authenticated"); } + description = _("Not authenticated"); } else @@ -133,6 +134,7 @@ namespace GameHub.UI.Dialogs.SettingsDialog.Pages.Sources { _("Authenticated"); } + description = _("Authenticated"); } } From 07838bfe01ba27d9def372bac5194677393c95f0 Mon Sep 17 00:00:00 2001 From: Lucki Date: Wed, 21 Apr 2021 15:53:13 +0200 Subject: [PATCH 05/22] correct function call --- src/data/sources/epicgames/EpicGame.vala | 2 +- src/data/sources/epicgames/EpicGames.vala | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/data/sources/epicgames/EpicGame.vala b/src/data/sources/epicgames/EpicGame.vala index 0d3075cc..d59c3bd0 100644 --- a/src/data/sources/epicgames/EpicGame.vala +++ b/src/data/sources/epicgames/EpicGame.vala @@ -65,7 +65,7 @@ namespace GameHub.Data.Sources.EpicGames Json.to_string(metadata, true).data); } } - internal Asset? asset_info { get; default = null; } + internal Asset? asset_info { get; set; default = null; } // public Json.Object? asset_info; // public Json.Object? metadata; private Json.Node _metadata = new Json.Node(Json.NodeType.NULL); diff --git a/src/data/sources/epicgames/EpicGames.vala b/src/data/sources/epicgames/EpicGames.vala index 63c3b3e4..aaaf08e4 100644 --- a/src/data/sources/epicgames/EpicGames.vala +++ b/src/data/sources/epicgames/EpicGames.vala @@ -539,7 +539,7 @@ namespace GameHub.Data.Sources.EpicGames if(platform_override != null && access_token != null && access_token.length > 0) { var list = new ArrayList(); - var games_json = EpicGamesServices.instance.get_game_assets(access_token, platform_override); + var games_json = EpicGamesServices.instance.get_game_assets(platform_override); games_json.get_array().foreach_element((array, index, node) => { assert(node.get_node_type() == Json.NodeType.OBJECT); @@ -550,10 +550,9 @@ namespace GameHub.Data.Sources.EpicGames return list; } - - if(update_assets || assets.is_empty && access_token != null && access_token.length > 0) + if((update_assets || assets.is_empty) && access_token != null && access_token.length > 0) { - var games_json = EpicGamesServices.instance.get_game_assets(access_token); + var games_json = EpicGamesServices.instance.get_game_assets(); games_json.get_array().foreach_element((array, index, node) => { assert(node.get_node_type() == Json.NodeType.OBJECT); @@ -567,6 +566,9 @@ namespace GameHub.Data.Sources.EpicGames { assets.set(assets.index_of(asset), asset); } + + // Also update asset info in EpicGame because we rely on this being up-to-date + get_game(asset.asset_id).asset_info = asset; }); } From f7ad1eb0427a0ee0f33d29b7152648ad4a81ac0b Mon Sep 17 00:00:00 2001 From: Lucki Date: Sun, 25 Apr 2021 01:50:58 +0200 Subject: [PATCH 06/22] Fix segfault caused by infinite loop Also: * some improved DLC handling * actually update the cached asset list * always save fetched metadata to disk --- src/data/db/tables/Games.vala | 1 + src/data/sources/epicgames/EpicGame.vala | 47 +++++++++++------ src/data/sources/epicgames/EpicGames.vala | 62 ++++++++++++++++------- 3 files changed, 77 insertions(+), 33 deletions(-) diff --git a/src/data/db/tables/Games.vala b/src/data/db/tables/Games.vala index 1ed89b50..b7d64d43 100644 --- a/src/data/db/tables/Games.vala +++ b/src/data/db/tables/Games.vala @@ -155,6 +155,7 @@ namespace GameHub.Data.DB.Tables } if(game is Sources.GOG.GOGGame.DLC) return false; + if(game is Sources.EpicGames.EpicGame.DLC) return false; unowned Sqlite.Database? db = Database.instance.db; if(db == null) return false; diff --git a/src/data/sources/epicgames/EpicGame.vala b/src/data/sources/epicgames/EpicGame.vala index d59c3bd0..b77927b5 100644 --- a/src/data/sources/epicgames/EpicGame.vala +++ b/src/data/sources/epicgames/EpicGame.vala @@ -42,6 +42,7 @@ namespace GameHub.Data.Sources.EpicGames owned get { var urls = new ArrayList(); + return_val_if_fail(_metadata.get_node_type() == Json.NodeType.OBJECT, urls); // prevent loop return_val_if_fail(metadata.get_object().has_member("base_urls"), urls); metadata.get_object().get_array_member("base_urls").foreach_element((array, index, node) => { @@ -52,22 +53,22 @@ namespace GameHub.Data.Sources.EpicGames } set { - var urls = new Json.Array(); + var urls = new Json.Node(Json.NodeType.ARRAY); + urls.set_array(new Json.Array()); value.foreach(url => { - urls.add_string_element(url); + urls.get_array().add_string_element(url); return true; }); - metadata.get_object().set_array_member("base_urls", urls); + metadata.get_object().set_array_member("base_urls", urls.get_array()); write(FS.Paths.EpicGames.Metadata, get_metadata_filename(), Json.to_string(metadata, true).data); } } internal Asset? asset_info { get; set; default = null; } - // public Json.Object? asset_info; - // public Json.Object? metadata; + private Json.Node _metadata = new Json.Node(Json.NodeType.NULL); internal Json.Node metadata // FIXME: make a class for easier access? { @@ -76,6 +77,7 @@ namespace GameHub.Data.Sources.EpicGames if(_metadata.get_node_type() == Json.NodeType.NULL) { // FIXME: this will never update this way + var f = FS.file(FS.Paths.EpicGames.Metadata, get_metadata_filename()); _metadata = Parser.parse_json_file(FS.Paths.EpicGames.Metadata, get_metadata_filename()); if(_metadata.get_node_type() != Json.NodeType.NULL) return _metadata; @@ -91,6 +93,16 @@ namespace GameHub.Data.Sources.EpicGames return _metadata; } + set + { + return_if_fail(value.get_node_type() == Json.NodeType.OBJECT); + + // TODO: save and rejoin base_urls? + _metadata = value; + write(FS.Paths.EpicGames.Metadata, + get_metadata_filename(), + Json.to_string(_metadata, true).data); + } } internal File? repair_file @@ -186,12 +198,9 @@ namespace GameHub.Data.Sources.EpicGames { get { - if(info_detailed == null) return false; - - var json = Parser.parse_json(info_detailed); - return_val_if_fail(json.get_node_type() == Json.NodeType.OBJECT, false); + return_val_if_fail(metadata.get_node_type() == Json.NodeType.OBJECT, false); - return json.get_object().has_member("mainGameItem"); + return metadata.get_object().has_member("mainGameItem"); } } @@ -204,18 +213,18 @@ namespace GameHub.Data.Sources.EpicGames } } - public EpicGame(EpicGames source, Asset asset, Json.Node? meta = null) + public EpicGame(EpicGames source, Asset asset, Json.Node? metadata = null) { this.source = source; id = asset.asset_id; // this.version = asset.build_version; // Only gets permanently saved for installed games // this.info = asset.to_string(false); - if(meta != null) _metadata = meta; + if(metadata != null) this.metadata = metadata; _asset_info = asset; load_version(); - name = metadata.get_object().get_string_member_with_default("title", ""); + name = this.metadata.get_object().get_string_member_with_default("title", ""); install_dir = null; this.status = new Game.Status(Game.State.UNINSTALLED, this); @@ -524,7 +533,7 @@ namespace GameHub.Data.Sources.EpicGames public void add_dlc(Asset asset, Json.Node? metadata = null) { - if(dlc == null) + if(dlc == null || dlc.size == 0) { dlc = new ArrayList(); } @@ -1166,8 +1175,16 @@ namespace GameHub.Data.Sources.EpicGames { var tmp_urls = base_urls; // save temporarily from old metadata _metadata = EpicGamesServices.instance.get_game_info(asset_info.ns, asset_info.catalog_item_id); - base_urls = tmp_urls; // paste them back into new metadata + + // prevent loop by accessing metadata again in set_base_urls + if(_metadata.get_node_type() == Json.NodeType.NULL) + { + _metadata = new Json.Node(Json.NodeType.OBJECT); + _metadata.set_object(new Json.Object()); + } + // FIXME: Setting base_urls also saves + base_urls = tmp_urls; // paste them back into new metadata write(FS.Paths.EpicGames.Metadata, get_metadata_filename(), Json.to_string(metadata, true).data); diff --git a/src/data/sources/epicgames/EpicGames.vala b/src/data/sources/epicgames/EpicGames.vala index aaaf08e4..5f10f166 100644 --- a/src/data/sources/epicgames/EpicGames.vala +++ b/src/data/sources/epicgames/EpicGames.vala @@ -12,7 +12,7 @@ namespace GameHub.Data.Sources.EpicGames internal bool log_chunk = false; internal bool log_chunk_part = false; internal bool log_chunk_data_list = false; - internal bool log_epic_games_services = false; + internal bool log_epic_games_services = true; internal bool log_file_manifest_list = false; internal bool log_manifest = false; internal bool log_meta = false; @@ -25,10 +25,10 @@ namespace GameHub.Data.Sources.EpicGames private Json.Node? userdata { get; default = new Json.Node(Json.NodeType.NULL); } - public override string id { get { return "epicgames"; } } - public override string name { get { return "EpicGames"; } } - public override string icon { get { return "source-epicgames-symbolic"; } } - public override ArrayList games { get; default = new ArrayList(Game.is_equal); } + public override string id { get { return "epicgames"; } } + public override string name { get { return "EpicGames"; } } + public override string icon { get { return "source-epicgames-symbolic"; } } + public override ArrayList games { get; default = new ArrayList(Game.is_equal); } public override bool enabled { @@ -568,8 +568,13 @@ namespace GameHub.Data.Sources.EpicGames } // Also update asset info in EpicGame because we rely on this being up-to-date - get_game(asset.asset_id).asset_info = asset; + var game = get_game(asset.asset_id); + + if(game != null) game.asset_info = asset; }); + + // trigger disk save + assets = assets; } return assets; @@ -635,13 +640,15 @@ namespace GameHub.Data.Sources.EpicGames // Vala should be able to handle tuples but I couldn't figure it out var dlcs = new HashMap >(); - var tmp_assets = get_game_assets(update_assets, platform_override); - foreach(var asset in tmp_assets) + var owned_assets = get_game_assets(update_assets, platform_override); + foreach(var asset in owned_assets) { Json.Node? metadata = null; if(asset.ns == "ue" && skip_unreal_engine) continue; + // FIXME: We're only loading games from the DB so we're never finding DLCs here + // This results into game == null so we're fetching metadata every time for DLCs var game = get_game(asset.app_name); if(update_assets && (game == null || (game != null @@ -659,8 +666,11 @@ namespace GameHub.Data.Sources.EpicGames metadata = EpicGamesServices.instance.get_game_info(asset.ns, asset.catalog_item_id); assert(metadata.get_node_type() == Json.NodeType.OBJECT); - // var title = metadata.get_object().get_string_member_with_default("title", ""); - game = new EpicGame(EpicGames.instance, asset, metadata); + // Don't add DLCs + if(!metadata.get_object().has_member("mainGameItem")) + { + game = new EpicGame(EpicGames.instance, asset, metadata); + } // if(platform_override == null) game.save_metadata(); } @@ -673,15 +683,18 @@ namespace GameHub.Data.Sources.EpicGames // game.asset_info = asset; // } - if(game.is_dlc) + // temporay save DLCs to list and assign later to main games + // so were surely have all main games loaded + if(game == null) { - var json = Parser.parse_json(game.info_detailed); - return_val_if_fail(json.get_node_type() == Json.NodeType.OBJECT, false); - - var main_id = json.get_object().get_object_member("mainGameItem").get_string_member("id"); + assert(metadata.get_node_type() == Json.NodeType.OBJECT); + assert(metadata.get_object().has_member("mainGameItem")); + assert(metadata.get_object().get_member("mainGameItem").get_node_type() == Json.NodeType.OBJECT); + assert(metadata.get_object().get_object_member("mainGameItem").has_member("id")); + assert(metadata.get_object().get_object_member("mainGameItem").get_member("id").get_node_type() == Json.NodeType.VALUE); - // add later when we got all games - var tmp = dlcs.get(main_id); + var main_id = metadata.get_object().get_object_member("mainGameItem").get_string_member("id"); + var tmp = dlcs.get(main_id); if(tmp == null) { @@ -699,7 +712,7 @@ namespace GameHub.Data.Sources.EpicGames // TODO: mods? } - // we got all games, add the dlcs to it + // we got all games, add the DLCs to it foreach(var game_name in dlcs) { if(game_name.value == null) continue; @@ -707,6 +720,19 @@ namespace GameHub.Data.Sources.EpicGames foreach(var tuple in game_name.value) { var game = owned_games.get(game_name.key); + + if(game == null) + { + // try harder by matching against catalog id + game = owned_games.first_match(entry => { + return entry.value.asset_info.catalog_item_id == game_name.key; + }).value; + } + + // FIXME: If it's possible to own a DLC without the main game we shouldn't fail here + assert_nonnull(game); + assert_nonnull(tuple.key); + game.add_dlc(tuple.key, tuple.value); } } From 3a4495eb131fbd032e774349c3966968a293529e Mon Sep 17 00:00:00 2001 From: Lucki Date: Mon, 26 Apr 2021 17:53:35 +0200 Subject: [PATCH 07/22] Implement updating --- src/data/sources/epicgames/EpicAnalysis.vala | 111 +++++++++++------- src/data/sources/epicgames/EpicGame.vala | 74 ++++++------ src/data/sources/epicgames/EpicGames.vala | 1 + src/data/sources/epicgames/EpicInstaller.vala | 49 +++++++- src/data/sources/epicgames/EpicManifest.vala | 3 +- .../GameDetailsView/GameDetailsPage.vala | 16 ++- 6 files changed, 166 insertions(+), 88 deletions(-) diff --git a/src/data/sources/epicgames/EpicAnalysis.vala b/src/data/sources/epicgames/EpicAnalysis.vala index a98ef895..8acdf812 100644 --- a/src/data/sources/epicgames/EpicAnalysis.vala +++ b/src/data/sources/epicgames/EpicAnalysis.vala @@ -104,7 +104,8 @@ namespace GameHub.Data.Sources.EpicGames }).file_size; var is_1mib = (biggest_chunk == 1024 * 1024); - debug(@"[Sources.EpicGames.AnalysisResult] Biggest chunk size: $biggest_chunk bytes (==1 MiB? $is_1mib)"); + + if(log_analysis) debug(@"[Sources.EpicGames.AnalysisResult] Biggest chunk size: $biggest_chunk bytes (==1 MiB? $is_1mib)"); debug("[Sources.EpicGames.AnalysisResult] Creating manifest comparison…"); _manifest_comparison = new ManifestComparison(new_manifest, old_manifest); @@ -280,18 +281,19 @@ namespace GameHub.Data.Sources.EpicGames // var processing_optimizations = false; // determine reusable chunks and prepare lookup table for reusable ones - var re_usable = new HashMap >(); + var re_usable = new HashMap >(); var patch = true; // FIXME: hardcoded always update if(old_manifest != null && !manifest_comparison.changed.is_empty && patch) { - debug("[Sources.EpicGames.AnalysisResult] Analyzing manifests for re-usable chunks…"); + if(log_analysis) debug("[Sources.EpicGames.AnalysisResult] Analyzing manifests for re-usable chunks…"); + foreach(var changed_file in manifest_comparison.changed) { var old_file = old_manifest.file_manifest_list.get_file_by_path(changed_file); var new_file = new_manifest.file_manifest_list.get_file_by_path(changed_file); - var existing_chunks = new HashMap >(); + var existing_chunks = new HashMap>(); uint32 offset = 0; foreach(var chunk_part in old_file.chunk_parts) @@ -299,53 +301,50 @@ namespace GameHub.Data.Sources.EpicGames // debug(@"Old chunk: $chunk_part"); if(!existing_chunks.has_key(chunk_part.guid_num)) { - var list = new ArrayList >(); + var list = new ArrayList(); existing_chunks.set(chunk_part.guid_num, list); } - // TODO: possible to do this better? - var tmp = existing_chunks.get(chunk_part.guid_num); - var tmp2 = new ArrayList(); - tmp2.add_all_array({ offset, chunk_part.offset, chunk_part.offset + chunk_part.size }); - tmp.add(tmp2); - existing_chunks.set(chunk_part.guid_num, tmp); + existing_chunks.get(chunk_part.guid_num).add(new OldChunkKey(offset, chunk_part.offset, chunk_part.offset + chunk_part.size)); offset += chunk_part.size; } foreach(var chunk_part in new_file.chunk_parts) { // debug(@"New chunk: $chunk_part"); - uint32[] key = { chunk_part.guid_num, chunk_part.offset, chunk_part.size }; + var key = new ChunkKey(chunk_part.guid_num, chunk_part.offset, chunk_part.size); if(!existing_chunks.has_key(chunk_part.guid_num)) continue; - foreach(ArrayList thing in existing_chunks.get(chunk_part.guid_num)) + foreach(var thing in existing_chunks.get(chunk_part.guid_num)) { - assert_nonnull(thing); - assert(thing.size == 3); - // check if new chunk part is wholly contained in the old chunk part - if(thing.get(1) <= chunk_part.offset - && (chunk_part.offset + chunk_part.size) <= thing.get(2)) + if(thing.chunk_part_offset <= chunk_part.offset + && (chunk_part.offset + chunk_part.size) <= thing.chunk_part_end) { references.remove(chunk_part.guid_num); if(!re_usable.has_key(changed_file)) { - re_usable.set(changed_file, new HashMap()); + re_usable.set(changed_file, + new HashMap( + key => { return key.hash(); }, + (a, b) => { return a.equal_to(b); })); } - // TODO: possible to do this better? - var tmp = re_usable.get(changed_file); - tmp.set(key, thing.get(0) + (chunk_part.offset - thing.get(1))); - re_usable.set(changed_file, tmp); + re_usable.get(changed_file).set(key, thing.file_offset + (chunk_part.offset - thing.chunk_part_offset)); _reuse_size += chunk_part.size; + break; } } } } } + if(log_analysis) debug("re-usable size: " + reuse_size.to_string()); + + if(log_analysis) debug("files with re-usable parts: " + re_usable.size.to_string()); + uint32 last_cache_size = 0; uint32 current_cache_size = 0; @@ -374,27 +373,21 @@ namespace GameHub.Data.Sources.EpicGames continue; } - // TODO: does this return null if nonexisting? var existing_chunks = re_usable.get(current_file.filename); - // Gee.HashMap existing_chunks = null; - // if(re_usable.has_key(current_file.filename)) - // { - // existing_chunks = re_usable.get(current_file.filename); - // } - var chunk_tasks = new ArrayList(); - var reused = 0; + var chunk_tasks = new ArrayList(); + var reused = 0; foreach(var chunk_part in current_file.chunk_parts) { var chunk_task = new ChunkTask(chunk_part.guid_num, chunk_part.offset, chunk_part.size); // re-use the chunk from the existing file if we can - uint32[] key = { chunk_part.guid_num, chunk_part.offset, chunk_part.size }; + var key = new ChunkKey(chunk_part.guid_num, chunk_part.offset, chunk_part.size); - if(existing_chunks != null - && existing_chunks.has_key(key)) + if(existing_chunks != null && existing_chunks.has_key(key)) { - // debug("reusing chunk, hash should be: " + new_manifest.chunk_data_list.get_chunk_by_number(chunk_part.guid_num).to_string()); + if(log_analysis) debug("reusing chunk: " + new_manifest.chunk_data_list.get_chunk_by_number(chunk_part.guid_num).to_string()); + reused++; chunk_task.chunk_file = current_file.filename; chunk_task.chunk_offset = existing_chunks.get(key); @@ -443,7 +436,7 @@ namespace GameHub.Data.Sources.EpicGames if(reused > 0) { - debug(@"[Sources.EpicGames.AnalysisResult] Reusing $reused chunks from: $(current_file.filename)"); + if(log_analysis) debug(@"[Sources.EpicGames.AnalysisResult] Reusing $reused chunks from: $(current_file.filename)"); // open temporary file that will contain download + old file contents tasks.add(new FileTask.open(current_file.filename + ".tmp")); @@ -465,12 +458,14 @@ namespace GameHub.Data.Sources.EpicGames // check if runtime cache size has changed if(current_cache_size > last_cache_size) { - debug(@"[Sources.EpicGames.AnalysisResult] New maximum cache size: $(current_cache_size / 1024 / 1024) MiB"); + if(log_analysis) debug(@"[Sources.EpicGames.AnalysisResult] New maximum cache size: $(current_cache_size / 1024 / 1024) MiB"); + last_cache_size = current_cache_size; } } - debug(@"[Sources.EpicGames.AnalysisResult] Final cache size requirement: $(last_cache_size / 1024 / 1024) MiB"); + if(log_analysis) debug(@"[Sources.EpicGames.AnalysisResult] Final cache size requirement: $(last_cache_size / 1024 / 1024) MiB"); + _min_memory = last_cache_size + (1024 * 1024 * 32); // add some padding just to be safe // TODO: Legendary does same caching stuff here @@ -500,6 +495,38 @@ namespace GameHub.Data.Sources.EpicGames _num_chunks_cache = dl_cache_guids.size; chunk_data_list = new_manifest.chunk_data_list; } + + class ChunkKey + { + public uint32 guid_num; + public uint32 offset; + public uint32 size; + + public ChunkKey(uint32 guid_num, uint32 offset, uint32 size) + { + this.guid_num = guid_num; + this.offset = offset; + this.size = size; + } + + public uint hash() { var hash = (guid_num.to_string() + offset.to_string() + size.to_string()).hash(); return hash; } + + public bool equal_to(ChunkKey chunk_key) { return chunk_key.hash() == hash(); } + } + + class OldChunkKey + { + public uint32 file_offset; + public uint32 chunk_part_offset; + public uint32 chunk_part_end; + + public OldChunkKey(uint32 file_offset, uint32 chunk_part_offset, uint32 chunk_part_end) + { + this.file_offset = file_offset; + this.chunk_part_offset = chunk_part_offset; + this.chunk_part_end = chunk_part_end; + } + } } // This only exists so I can put both subclasses in one list @@ -516,7 +543,7 @@ namespace GameHub.Data.Sources.EpicGames */ internal class FileTask: Task { - internal string filename { get; } + internal string filename { get; } internal bool del { get; default = false; } internal bool empty { get; default = false; } internal bool fopen { get; default = false; } @@ -563,12 +590,12 @@ namespace GameHub.Data.Sources.EpicGames _fclose = true; } - internal FileTask.rename(string new_filename, string old_filename, bool @delete = false) + internal FileTask.rename(string new_filename, string old_filename, bool dele = false) { - this(filename); + this(new_filename); _frename = true; _temporary_filename = old_filename; - _del = @delete; + _del = dele; } } diff --git a/src/data/sources/epicgames/EpicGame.vala b/src/data/sources/epicgames/EpicGame.vala index b77927b5..577117c9 100644 --- a/src/data/sources/epicgames/EpicGame.vala +++ b/src/data/sources/epicgames/EpicGame.vala @@ -14,7 +14,7 @@ namespace GameHub.Data.Sources.EpicGames Traits.HasExecutableFile, Traits.SupportsCompatTools, Traits.Game.SupportsTweaks { - // // Traits.HasActions + // Traits.HasActions // public override ArrayList? actions { get; protected set; default = new ArrayList(); } // Traits.HasExecutableFile @@ -105,6 +105,7 @@ namespace GameHub.Data.Sources.EpicGames } } + internal File? resume_file { get; default = null; } internal File? repair_file { owned get @@ -393,10 +394,12 @@ namespace GameHub.Data.Sources.EpicGames load_version(); // actions.clear(); - // if(version != asset_info.build_version) - // { - // actions.add(new RunnableAction(this)); - // } + // var action = new RunnableAction(this); + + // // if(!action.is_hidden) + // // { + // actions.add(action); + // // } } public override async void run() @@ -1131,9 +1134,9 @@ namespace GameHub.Data.Sources.EpicGames // TODO: DLC - var force = false; // hardcoded for now + var force_update = true; // hardcoded for now // var install_path = task.install_dir; - File? resume_file = null; + _resume_file = null; if(needs_repair) { @@ -1141,12 +1144,12 @@ namespace GameHub.Data.Sources.EpicGames // new_manifest = old_manifest; // old_manifest = null; - resume_file = FS.file(Environment.get_tmp_dir(), id + ".repair"); - force = false; + _resume_file = FS.file(Environment.get_tmp_dir(), id + ".repair"); + force_update = false; } - else if(!force) + else if(force_update) { - resume_file = FS.file(Environment.get_tmp_dir(), id + ".resume"); + _resume_file = FS.file(Environment.get_tmp_dir(), id + ".resume"); } var base_url = base_urls[Random.int_range(0, base_urls.size - 1)]; @@ -1190,22 +1193,24 @@ namespace GameHub.Data.Sources.EpicGames Json.to_string(metadata, true).data); } - // public new async void install(InstallTask.Mode install_mode = InstallTask.Mode.INTERACTIVE, bool update = false) - // { - // if(update) - // { - // ArrayList? dirs = new ArrayList(); - // dirs.add(install_dir); - // var task = new InstallTask(this, installers, dirs, install_mode, false); - // yield task.start(); - // } - // else - // { - // if(status.state != Game.State.UNINSTALLED || !is_installable) return; - // var task = new InstallTask(this, installers, source.game_dirs, install_mode, true); - // yield task.start(); - // } - // } + public override async void install(InstallTask.Mode install_mode = InstallTask.Mode.INTERACTIVE) + { + if(status.state == Game.State.INSTALLED) + { + ArrayList? dirs = new ArrayList(); + dirs.add(install_dir); + var task = new InstallTask(this, installers, dirs, InstallTask.Mode.AUTO_INSTALL, false); + yield task.start(); + } + else + { + // Uninstalled, fresh install + if(status.state != Game.State.UNINSTALLED || !is_installable) return; + + var task = new InstallTask(this, installers, source.game_dirs, install_mode, true); + yield task.start(); + } + } // private ArrayList get_save_games() // { @@ -1441,20 +1446,15 @@ namespace GameHub.Data.Sources.EpicGames // { // public RunnableAction(EpicGame game) // { - // runnable = game; + // runnable = game; // is_primary = true; - // name = "Update"; + // name = "Update"; + // is_hidden = !game.has_updates; // } - // public new bool is_available(GameHub.Data.Compat.CompatTool? tool = null) - // { - // return true; - // } + // public new bool is_available(GameHub.Data.Compat.CompatTool? tool = null) { return ((EpicGame) runnable).has_updates; } - // public new async void invoke(GameHub.Data.Compat.CompatTool? tool = null) - // { - // yield((EpicGame)runnable).install(InstallTask.Mode.AUTO_INSTALL, true); - // } + // public new async void invoke(GameHub.Data.Compat.CompatTool? tool = null) { yield((EpicGame) runnable).install(InstallTask.Mode.AUTO_INSTALL); } // } } } diff --git a/src/data/sources/epicgames/EpicGames.vala b/src/data/sources/epicgames/EpicGames.vala index 5f10f166..7aebcfb4 100644 --- a/src/data/sources/epicgames/EpicGames.vala +++ b/src/data/sources/epicgames/EpicGames.vala @@ -9,6 +9,7 @@ using GameHub.Utils; namespace GameHub.Data.Sources.EpicGames { + internal bool log_analysis = false; internal bool log_chunk = false; internal bool log_chunk_part = false; internal bool log_chunk_data_list = false; diff --git a/src/data/sources/epicgames/EpicInstaller.vala b/src/data/sources/epicgames/EpicInstaller.vala index 59c11450..4f0a9dde 100644 --- a/src/data/sources/epicgames/EpicInstaller.vala +++ b/src/data/sources/epicgames/EpicInstaller.vala @@ -112,6 +112,37 @@ namespace GameHub.Data.Sources.EpicGames full_path.get_path()); } + // write last completed file to simple resume file + if(game.resume_file != null) + { + var path = full_path.get_path(); + + if(path[path.length - 4:path.length] == ".tmp") + { + path = path[0 : path.length - 4]; + } + + var file_hash = yield Utils.compute_file_checksum(full_path, ChecksumType.SHA1); + // var tmp = ""; + + // if(((Analysis.FileTask)file_task).filename[((Analysis.FileTask)file_task).filename.length - 4 : ((Analysis.FileTask)file_task).filename.length] == ".tmp") + // { + // tmp = ((Analysis.FileTask)file_task).filename[0 : ((Analysis.FileTask)file_task).filename.length - 4]; + // } + // else + // { + // tmp = ((Analysis.FileTask)file_task).filename; + // } + + // debug(tmp); + // assert(file_hash == bytes_to_hex(analysis.result.manifest.file_manifest_list.get_file_by_path(tmp).sha_hash)); + + var output_stream = game.resume_file.append_to(FileCreateFlags.NONE); + output_stream.write((string.join(":", file_hash, path) + "\n").data); + + output_stream.close(); + } + continue; } else if(((Analysis.FileTask)file_task).frename) @@ -128,8 +159,8 @@ namespace GameHub.Data.Sources.EpicGames FS.rm(full_path.get_path()); } - File.new_for_path(((Analysis.FileTask)file_task).temporary_filename).move(full_path, - FileCopyFlags.NONE); + File.new_build_filename(install_task.install_dir.get_path(), + ((Analysis.FileTask)file_task).temporary_filename).move(full_path, FileCopyFlags.NONE); continue; } else if(((Analysis.FileTask)file_task).del) @@ -149,21 +180,19 @@ namespace GameHub.Data.Sources.EpicGames assert(file_task is Analysis.ChunkTask); assert_nonnull(iostream); - FileInputStream? old_stream = null; - // FIXME: this blocks the UI, do in an own thread/async var downloaded_chunk = FS.file(FS.Paths.EpicGames.Cache + "/chunks/" + game.id + "/" + ((Analysis.ChunkTask)file_task).chunk_guid.to_string()); if(((Analysis.ChunkTask)file_task).chunk_file != null) { // reuse chunk from existing file + FileInputStream? old_stream = null; assert(File.new_build_filename(install_task.install_dir.get_path(), ((Analysis.ChunkTask)file_task).chunk_file).query_exists()); old_stream = File.new_build_filename(install_task.install_dir.get_path(), ((Analysis.ChunkTask)file_task).chunk_file).read(); old_stream.seek(((Analysis.ChunkTask)file_task).chunk_offset, SeekType.SET); var bytes = yield old_stream.read_bytes_async(((Analysis.ChunkTask)file_task).chunk_size); - // debug("chunk hash: " + Checksum.compute_for_bytes(ChecksumType.SHA1, bytes)); yield iostream.write_bytes_async(bytes); old_stream.close(); old_stream = null; @@ -178,9 +207,17 @@ namespace GameHub.Data.Sources.EpicGames var size = yield iostream.write_bytes_async(chunk.data[((Analysis.ChunkTask)file_task).chunk_offset : ((Analysis.ChunkTask)file_task).chunk_offset + ((Analysis.ChunkTask)file_task).chunk_size]); // debug(@"written $size bytes"); } + else + { + assert_not_reached(); + } } } - catch (Error e) { debug("chunk building failed: %s", e.message); } + catch (Error e) + { + debug("chunk building failed: %s", e.message); + assert_not_reached(); + } // TODO: clean cache path diff --git a/src/data/sources/epicgames/EpicManifest.vala b/src/data/sources/epicgames/EpicManifest.vala index ba436393..16ee0ea9 100644 --- a/src/data/sources/epicgames/EpicManifest.vala +++ b/src/data/sources/epicgames/EpicManifest.vala @@ -1100,7 +1100,8 @@ namespace GameHub.Data.Sources.EpicGames if(old_file_hash != null) { - if(file_manifest.hash == old_file_hash) + // Comparing Bytes doesn't work, using their string representation + if(bytes_to_hex(file_manifest.hash) == bytes_to_hex(old_file_hash)) { unchanged.add(file_manifest.filename); } diff --git a/src/ui/views/GameDetailsView/GameDetailsPage.vala b/src/ui/views/GameDetailsView/GameDetailsPage.vala index 7e9325d7..ff31f5b9 100644 --- a/src/ui/views/GameDetailsView/GameDetailsPage.vala +++ b/src/ui/views/GameDetailsView/GameDetailsPage.vala @@ -66,6 +66,7 @@ namespace GameHub.UI.Views.GameDetailsView private ActionButton action_install; private ActionButton action_run; + private ActionButton action_update; private ActionButton action_properties; private ActionButton action_open_directory; private ActionButton action_open_installer_collection_directory; @@ -230,6 +231,7 @@ namespace GameHub.UI.Views.GameDetailsView action_install = add_action("go-down", null, _("Install"), install_game, true); action_run = add_action("media-playback-start", null, _("Run"), run_game, true); + action_update = add_action("go-down", null, _("Update"), game_update); action_open_directory = add_action("folder", null, _("Open installation directory"), open_game_directory); action_open_store_page = add_action("web-browser", null, _("Open store page"), open_game_store_page); action_uninstall = add_action("edit-delete", null, (game is Sources.User.UserGame) ? _("Remove") : _("Uninstall"), uninstall_game); @@ -290,12 +292,14 @@ namespace GameHub.UI.Views.GameDetailsView } action_install.visible = s.state != Game.State.INSTALLED; action_install.sensitive = s.state == Game.State.UNINSTALLED && game.is_installable; + action_update.visible = s.state == Game.State.INSTALLED && game is GameHub.Data.Sources.EpicGames.EpicGame; + action_update.sensitive = s.state == Game.State.INSTALLED && game is GameHub.Data.Sources.EpicGames.EpicGame && ((GameHub.Data.Sources.EpicGames.EpicGame) game).has_updates; action_run.visible = s.state == Game.State.INSTALLED; action_run.sensitive = game.can_be_launched(); action_open_directory.visible = s.state == Game.State.INSTALLED && game.install_dir != null && game.install_dir.query_exists(); action_open_store_page.visible = game.store_page != null; - action_uninstall.visible = s.state == Game.State.INSTALLED && !(game is GameHub.Data.Sources.GOG.GOGGame.DLC); - action_properties.visible = !(game is GameHub.Data.Sources.GOG.GOGGame.DLC); + action_uninstall.visible = s.state == Game.State.INSTALLED && !(game is GameHub.Data.Sources.GOG.GOGGame.DLC) && !(game is GameHub.Data.Sources.EpicGames.EpicGame.DLC); + action_properties.visible = !(game is GameHub.Data.Sources.GOG.GOGGame.DLC) && !(game is GameHub.Data.Sources.EpicGames.EpicGame.DLC); } public void update() @@ -399,6 +403,14 @@ namespace GameHub.UI.Views.GameDetailsView } } + private void game_update() + { + if(game != null && game.status.state == Game.State.INSTALLED) + { + game.install.begin(); + } + } + private void game_properties() { if(game != null) From c527f4af8bced55cc435ab380bb2895101e09d44 Mon Sep 17 00:00:00 2001 From: Lucki Date: Thu, 6 May 2021 18:35:37 +0200 Subject: [PATCH 08/22] Add store descriptions and links --- src/data/sources/epicgames/EpicGame.vala | 53 +++++++++++-- .../sources/epicgames/EpicGamesServices.vala | 74 ++++++++++++++++++- 2 files changed, 121 insertions(+), 6 deletions(-) diff --git a/src/data/sources/epicgames/EpicGame.vala b/src/data/sources/epicgames/EpicGame.vala index 577117c9..fb7bb161 100644 --- a/src/data/sources/epicgames/EpicGame.vala +++ b/src/data/sources/epicgames/EpicGame.vala @@ -340,6 +340,49 @@ namespace GameHub.Data.Sources.EpicGames return; } + Json.Node json; + + if(info_detailed == null || info_detailed.length == 0) + { + json = EpicGamesServices.instance.get_store_details(asset_info.ns, asset_info.asset_id); + + if(json.get_node_type() != Json.NodeType.NULL) + { + info_detailed = Json.to_string(json, false); + } + } + + json = Parser.parse_json(info_detailed); + + if(json != null && json.get_node_type() != Json.NodeType.NULL) + { + var slug = json.get_object().get_string_member_with_default("_slug", ""); + var page = json.get_object().get_array_member("pages").get_object_element(0); + var about = page.get_object_member("data").get_object_member("about"); + // var social = page.get_object_member("data").get_object_member("socialLinks"); + + if(slug != "") + { + // FIXME: Globify language and merge with …Services + var language_code = Intl.setlocale(LocaleCategory.ALL, null).down().substring(0, 2); + store_page = @"https://www.epicgames.com/store/$language_code/p/$slug"; + } + + if(about != null) + { + description = about.get_string_member_with_default("shortDescription", ""); + var long_description = about.get_string_member("description"); + + if(long_description != null && long_description.length > 0) + { + if(description.length > 0) description += "

"; + + long_description.replace("\n", "
"); + description += long_description; + } + } + } + save(); update_status(); @@ -776,7 +819,7 @@ namespace GameHub.Data.Sources.EpicGames internal async void verify() { - var manifest_data = get_installed_manifest(); // FIXME: cdn_manifest? + var manifest_data = get_installed_manifest(); // FIXME: cdn_manifest? var manifest = EpicGames.load_manifest(manifest_data); var files = manifest.file_manifest_list.elements; @@ -875,7 +918,7 @@ namespace GameHub.Data.Sources.EpicGames parameters += "-EpicPortal"; parameters += @"-epicusername=$(EpicGames.instance.user_name)"; parameters += @"-epicuserid=$(EpicGames.instance.user_id)"; - parameters += @"-epiclocale=en"; // FIXME: hardcoded for now + parameters += @"-epiclocale=en"; // FIXME: hardcoded for now return parameters; } @@ -1079,7 +1122,7 @@ namespace GameHub.Data.Sources.EpicGames Bytes new_bytes; Manifest? old_manifest = null; - var tmp2_urls = base_urls; // copy list for manipulation + var tmp2_urls = base_urls; // copy list for manipulation var old_bytes = version != null ? get_installed_manifest() : null; if(old_bytes == null) @@ -1134,7 +1177,7 @@ namespace GameHub.Data.Sources.EpicGames // TODO: DLC - var force_update = true; // hardcoded for now + var force_update = true; // hardcoded for now // var install_path = task.install_dir; _resume_file = null; @@ -1176,7 +1219,7 @@ namespace GameHub.Data.Sources.EpicGames internal void update_metadata() { - var tmp_urls = base_urls; // save temporarily from old metadata + var tmp_urls = base_urls; // save temporarily from old metadata _metadata = EpicGamesServices.instance.get_game_info(asset_info.ns, asset_info.catalog_item_id); // prevent loop by accessing metadata again in set_base_urls diff --git a/src/data/sources/epicgames/EpicGamesServices.vala b/src/data/sources/epicgames/EpicGamesServices.vala index 065e349c..3b719b52 100644 --- a/src/data/sources/epicgames/EpicGamesServices.vala +++ b/src/data/sources/epicgames/EpicGamesServices.vala @@ -24,9 +24,12 @@ namespace GameHub.Data.Sources.EpicGames private const string datastorage_host = "datastorage-public-service-liveegs.live.use1a.on.epicgames.com"; private const string library_host = "library-service.live.use1a.on.epicgames.com"; + private const string store_host = "store-content.ak.epicgames.com"; + // TODO: hardcoded for now private string language_code = "en"; private string country_code = "US"; + // var language_code = Intl.setlocale(LocaleCategory.ALL, null).down().substring(0, 2); // used with session, does not include user-agent as that's already set for the session private HashMap auth_headers = new HashMap(); @@ -36,6 +39,17 @@ namespace GameHub.Data.Sources.EpicGames private Session session = new Session(); private string user_agent = "UELauncher/11.0.1-14907503+++Portal+Release-Live Windows/10.0.19041.1.256.64bit"; + Json.Node? _productmapping = null; + Json.Node productmapping + { + get + { + if(_productmapping == null) update_store_productmapping(); + + return _productmapping; + } + } + internal EpicGamesServices() { instance = this; @@ -107,7 +121,7 @@ namespace GameHub.Data.Sources.EpicGames // } } - // This function is intended for server - side use only. + // This function is intended for server-side use only. // https://dev.epicgames.com/docs/services/en-US/API/Members/Functions/Auth/EOS_Auth_VerifyUserAuth/index.html internal Json.Node resume_session(Json.Node userdata) requires(userdata.get_node_type() == Json.NodeType.OBJECT) @@ -442,5 +456,63 @@ namespace GameHub.Data.Sources.EpicGames // https://github.com/SD4RK/epicstore_api/blob/master/epicstore_api/api.py#L72 // https://store-content.ak.epicgames.com/api/de/content/products/rocket-league // https://store-content.ak.epicgames.com/api/content/productmapping + + /** + * Retrieve store information. + * + * Tries to match against https://store-content.ak.epicgames.com/api/content/productmapping + * which mostly has the namespace as identifier. However, some only have the appid to match + * against. + * + * Also it's possible the store page doesn't exist (anymore). + * + * @param ns Namespace of an asset + * @param appid Fallback in case the other ID is used + */ + internal Json.Node get_store_details(string ns, string appid) + { + var slug = appid; + + if(productmapping.get_object().has_member(ns)) + { + assert(productmapping.get_object().get_member(ns).get_node_type() == Json.NodeType.VALUE); + slug = productmapping.get_object().get_string_member(ns); + } + + // debug("getting store info for %s - %s - %s", ns, appid, slug); + + uint status; + var json = Parser.parse_remote_json_file( + @"https://$store_host/api/$language_code/content/products/$slug", + "GET", + null, + unauth_headers, + null, + out status); + return_val_if_fail(status < 400, new Json.Node(Json.NodeType.NULL)); // Removed games will fail + assert(json.get_node_type() != Json.NodeType.NULL); + + if(log_epic_games_services) debug("[Source.EpicGames.EpicGamesServices.get_store_details] json dump: \n%s", Json.to_string(json, true)); + + return json; + } + + private void update_store_productmapping() + { + uint status; + var json = Parser.parse_remote_json_file( + @"https://$store_host/api/content/productmapping", + "GET", + null, + unauth_headers, + null, + out status); + assert(status < 400); + assert(json.get_node_type() == Json.NodeType.OBJECT); + + if(log_epic_games_services) debug("[Source.EpicGames.EpicGamesServices.update_store_productmapping] json dump: \n%s", Json.to_string(json, true)); + + _productmapping = json; + } } } From 4452c3c09f915d00b37b4bc0ecee62ad521ff0f4 Mon Sep 17 00:00:00 2001 From: Lucki Date: Sat, 8 May 2021 15:18:38 +0200 Subject: [PATCH 09/22] Implement delta manifest handling --- src/data/sources/epicgames/EpicGame.vala | 15 +++--- .../sources/epicgames/EpicGamesServices.vala | 15 ++++-- src/data/sources/epicgames/EpicManifest.vala | 49 +++++++++++++++++-- 3 files changed, 63 insertions(+), 16 deletions(-) diff --git a/src/data/sources/epicgames/EpicGame.vala b/src/data/sources/epicgames/EpicGame.vala index fb7bb161..f50c06ff 100644 --- a/src/data/sources/epicgames/EpicGame.vala +++ b/src/data/sources/epicgames/EpicGame.vala @@ -1156,18 +1156,19 @@ namespace GameHub.Data.Sources.EpicGames if(old_manifest != null && new_manifest != null) { - var delta_manifest_data = EpicGamesServices.instance.get_delta_manifest( + Bytes delta_manifest_data = null; + var delta_available = EpicGamesServices.instance.get_delta_manifest( base_urls[Random.int_range(0, base_urls.size - 1)], old_manifest.meta.build_id, - new_manifest.meta.build_id); + new_manifest.meta.build_id, + out delta_manifest_data); - if(delta_manifest_data != null) + if(delta_available && delta_manifest_data != null) { delta_manifest = EpicGames.load_manifest(delta_manifest_data); - debug( - "[Sources.EpicGames.prepare_download] Using optimized delta manifest to upgrade form build" + - @"$(old_manifest.meta.build_id) to $(new_manifest.meta.build_id)"); - // TODO: combine_manifests(new_manifest, delta_manifest); + debug("[Sources.EpicGames.prepare_download] Using optimized delta manifest to upgrade from build " + + @"$(old_manifest.meta.build_id) to $(new_manifest.meta.build_id)"); + new_manifest.combine_manifest(delta_manifest); } else { diff --git a/src/data/sources/epicgames/EpicGamesServices.vala b/src/data/sources/epicgames/EpicGamesServices.vala index 3b719b52..55c31a3e 100644 --- a/src/data/sources/epicgames/EpicGamesServices.vala +++ b/src/data/sources/epicgames/EpicGamesServices.vala @@ -438,18 +438,23 @@ namespace GameHub.Data.Sources.EpicGames /** * Get optimized delta manifest (doesn't seem to exist for most games) */ - internal Bytes? get_delta_manifest(string url, string old_build_id, string new_build_id) + internal bool get_delta_manifest(string url, string old_build_id, string new_build_id, out Bytes data) { - if(old_build_id == new_build_id) return null; + if(old_build_id == new_build_id) return false; - var message = new Message("GET", @"$url/Deltas/$new_build_id/$old_build_id.delta"); + var delta_url = @"$url/Deltas/$new_build_id/$old_build_id.delta"; + + if(log_epic_games_services) debug("Delta url: " + delta_url); + + var message = new Message("GET", delta_url); // unauth on purpose var status = session.send_message(message); + return_val_if_fail(status < 400, false); - return_val_if_fail(status == 200, null); + data = new Bytes(message.response_body.data); - return new Bytes(message.request_body.data); + return true; } // TODO: fetch descriptions etc diff --git a/src/data/sources/epicgames/EpicManifest.vala b/src/data/sources/epicgames/EpicManifest.vala index 16ee0ea9..8633e985 100644 --- a/src/data/sources/epicgames/EpicManifest.vala +++ b/src/data/sources/epicgames/EpicManifest.vala @@ -308,11 +308,10 @@ namespace GameHub.Data.Sources.EpicGames */ internal class FileManifestList { - private HashMap? path_map = null; - internal ArrayList elements { get; default = new ArrayList(); } + internal HashMap? path_map { get; set; default = null; } internal uint8 version { get; default = 0; } - internal uint32 count { get; default = 0; } + internal uint32 count { get; set; default = 0; } internal uint32 size { get; default = 0; } internal FileManifestList.from_byte_stream(DataInputStream stream) @@ -687,11 +686,11 @@ namespace GameHub.Data.Sources.EpicGames private uint8 version { get; } private uint32 manifest_version { get; } private uint32 size { get; } - private uint32 count { get; } private HashMap guid_int_map { get; default = new HashMap(); } private HashMap guid_str_map { get; default = new HashMap(); } internal ArrayList elements { get; default = new ArrayList(); } + internal uint32 count { get; set; } internal ChunkDataList.from_byte_stream(DataInputStream stream, uint32 manifest_version = 18) { @@ -884,6 +883,12 @@ namespace GameHub.Data.Sources.EpicGames return result + ")>"; } + internal void clear_matching_maps() + { + _guid_int_map.clear(); + _guid_str_map.clear(); + } + /** * Contains information about one {@link Chunk}. * @@ -1058,6 +1063,42 @@ namespace GameHub.Data.Sources.EpicGames // FIXME: escape? return result; } + + internal void combine_manifest(Manifest delta_manifest) + { + var added = new ArrayList(); + + // overwrite file elements with the ones from the delta manifest + foreach(var base_file in file_manifest_list.elements) + { + var delta_file = delta_manifest.file_manifest_list.get_file_by_path(base_file.filename); + + if(delta_file == null) continue; + + var idx = file_manifest_list.elements.index_of(base_file); + file_manifest_list.elements.set(idx, delta_file); + added.add(delta_file.filename); + } + + // add other files that may be missing + foreach(var delta_file in delta_manifest.file_manifest_list.elements) + { + if(!(delta_file.filename in added)) + { + file_manifest_list.elements.add(delta_file); + } + } + + // update count and clear map + file_manifest_list.count = file_manifest_list.elements.size; + file_manifest_list.path_map = null; + + // add chunks from delta manifest to main manifest and again clear path caches + chunk_data_list.elements.add_all(delta_manifest.chunk_data_list.elements); + chunk_data_list.count = chunk_data_list.elements.size; + chunk_data_list.clear_matching_maps(); + // chunk_data_list._path_map = null; ?? + } } /** From 163d93066ba6542cef02d73d4cdae11591b8c6d7 Mon Sep 17 00:00:00 2001 From: Lucki Date: Tue, 11 May 2021 01:39:52 +0200 Subject: [PATCH 10/22] More DLC handling --- src/data/adapters/GamesAdapter.vala | 4 +- src/data/db/tables/Merges.vala | 2 +- .../runnables/tasks/install/Installer.vala | 2 +- src/data/sources/epicgames/EpicGame.vala | 27 +- src/data/sources/epicgames/EpicGames.vala | 21 +- .../sources/epicgames/EpicGamesServices.vala | 92 ++-- src/meson.build | 1 + .../GameDetailsView/GameDetailsPage.vala | 3 +- .../GameDetailsView/GameDetailsView.vala | 5 + .../GameDetailsView/blocks/EpicDetails.vala | 411 ++++++++++++++++++ src/ui/views/GamesView/GameContextMenu.vala | 8 +- 11 files changed, 529 insertions(+), 47 deletions(-) create mode 100644 src/ui/views/GameDetailsView/blocks/EpicDetails.vala diff --git a/src/data/adapters/GamesAdapter.vala b/src/data/adapters/GamesAdapter.vala index b6749b28..431d5f52 100644 --- a/src/data/adapters/GamesAdapter.vala +++ b/src/data/adapters/GamesAdapter.vala @@ -512,7 +512,7 @@ namespace GameHub.Data.Adapters private void merge_game(Game game) { - if(!filter_settings_merge || game is Sources.GOG.GOGGame.DLC) return; + if(!filter_settings_merge || game is Sources.GOG.GOGGame.DLC || game is Sources.EpicGames.EpicGame.DLC) return; foreach(var src in sources) { foreach(var game2 in src.games) @@ -524,7 +524,7 @@ namespace GameHub.Data.Adapters private void merge_game_with_game(GameSource src, Game game, Game game2) { - if(Game.is_equal(game, game2) || game2 is Sources.GOG.GOGGame.DLC) return; + if(Game.is_equal(game, game2) || game2 is Sources.GOG.GOGGame.DLC || game2 is Sources.EpicGames.EpicGame.DLC) return; if(Tables.Merges.is_game_merged(game) || Tables.Merges.is_game_merged(game2) || Tables.Merges.is_game_merged_as_primary(game2)) return; diff --git a/src/data/db/tables/Merges.vala b/src/data/db/tables/Merges.vala index ad28cdc0..40e04c22 100644 --- a/src/data/db/tables/Merges.vala +++ b/src/data/db/tables/Merges.vala @@ -55,7 +55,7 @@ namespace GameHub.Data.DB.Tables public static bool add(Game first, Game second) { - if(first is Sources.GOG.GOGGame.DLC || second is Sources.GOG.GOGGame.DLC) return false; + if(first is Sources.GOG.GOGGame.DLC || second is Sources.GOG.GOGGame.DLC || first is Sources.EpicGames.EpicGame.DLC || second is Sources.EpicGames.EpicGame.DLC) return false; unowned Sqlite.Database? db = Database.instance.db; if(db == null) return false; diff --git a/src/data/runnables/tasks/install/Installer.vala b/src/data/runnables/tasks/install/Installer.vala index dc3bc30c..1c9c1f76 100644 --- a/src/data/runnables/tasks/install/Installer.vala +++ b/src/data/runnables/tasks/install/Installer.vala @@ -227,7 +227,7 @@ namespace GameHub.Data.Runnables.Tasks.Install } } - if(dirname != null && !(task.runnable is GameHub.Data.Sources.GOG.GOGGame.DLC)) + if(dirname != null && !(task.runnable is GameHub.Data.Sources.GOG.GOGGame.DLC) && !(task.runnable is GameHub.Data.Sources.EpicGames.EpicGame.DLC)) { FS.mv_up(task.install_dir, dirname.replace(" ", "\\ ")); } diff --git a/src/data/sources/epicgames/EpicGame.vala b/src/data/sources/epicgames/EpicGame.vala index f50c06ff..8861afd7 100644 --- a/src/data/sources/epicgames/EpicGame.vala +++ b/src/data/sources/epicgames/EpicGame.vala @@ -77,7 +77,7 @@ namespace GameHub.Data.Sources.EpicGames if(_metadata.get_node_type() == Json.NodeType.NULL) { // FIXME: this will never update this way - var f = FS.file(FS.Paths.EpicGames.Metadata, get_metadata_filename()); + // var f = FS.file(FS.Paths.EpicGames.Metadata, get_metadata_filename()); _metadata = Parser.parse_json_file(FS.Paths.EpicGames.Metadata, get_metadata_filename()); if(_metadata.get_node_type() != Json.NodeType.NULL) return _metadata; @@ -340,11 +340,32 @@ namespace GameHub.Data.Sources.EpicGames return; } - Json.Node json; + var json = new Json.Node(Json.NodeType.NULL); + // This gets only saved for games which results into fetching for DLCs every time if(info_detailed == null || info_detailed.length == 0) { - json = EpicGamesServices.instance.get_store_details(asset_info.ns, asset_info.asset_id); + if(this is DLC) + { + // // FIXME: this will never update + // json = Parser.parse_json_file(FS.Paths.EpicGames.Metadata, id + ".dlc.json"); + + // if(json.get_node_type() == Json.NodeType.NULL) + // { + // var j = EpicGamesServices.instance.get_dlc_details(asset_info.ns); + // j.get_array().foreach_element((array, index, node) => { + // // FIXME: wrong id + // if(node.get_object().get_string_member("id") == asset_info.asset_id) + // { + // json = node; + // } + // }); + // } + } + else + { + json = EpicGamesServices.instance.get_store_details(asset_info.ns, asset_info.asset_id); + } if(json.get_node_type() != Json.NodeType.NULL) { diff --git a/src/data/sources/epicgames/EpicGames.vala b/src/data/sources/epicgames/EpicGames.vala index 7aebcfb4..22d636e1 100644 --- a/src/data/sources/epicgames/EpicGames.vala +++ b/src/data/sources/epicgames/EpicGames.vala @@ -648,23 +648,28 @@ namespace GameHub.Data.Sources.EpicGames if(asset.ns == "ue" && skip_unreal_engine) continue; - // FIXME: We're only loading games from the DB so we're never finding DLCs here - // This results into game == null so we're fetching metadata every time for DLCs var game = get_game(asset.app_name); + // We're only loading games from the DB so we're never finding DLCs here + // This results into game == null so we're fetching metadata every time for DLCs if(update_assets && (game == null || (game != null && game.version != asset.build_version && platform_override != null))) { - if(game != null - && game.version != asset.build_version - && platform_override != null) + // Try reading from disk cache first, this is solely for DLCs which we aren't getting information from the database + metadata = Parser.parse_json_file(FS.Paths.EpicGames.Metadata, asset.asset_id + ".json"); + + // Also make it null again if above wasn't successfull + if(metadata.get_node_type() == Json.NodeType.NULL) metadata = null; + + if((game != null + && game.version != asset.build_version) + || metadata == null) { - debug("[LegendaryCore] Updating meta information for %s due to build version mismatch", - asset.app_name); + debug("[Sources.EpicGames.get_game_and_dlc_list] Updating meta information for %s", asset.app_name); + metadata = EpicGamesServices.instance.get_game_info(asset.ns, asset.catalog_item_id); } - metadata = EpicGamesServices.instance.get_game_info(asset.ns, asset.catalog_item_id); assert(metadata.get_node_type() == Json.NodeType.OBJECT); // Don't add DLCs diff --git a/src/data/sources/epicgames/EpicGamesServices.vala b/src/data/sources/epicgames/EpicGamesServices.vala index 55c31a3e..1fd6cbeb 100644 --- a/src/data/sources/epicgames/EpicGamesServices.vala +++ b/src/data/sources/epicgames/EpicGamesServices.vala @@ -457,23 +457,40 @@ namespace GameHub.Data.Sources.EpicGames return true; } - // TODO: fetch descriptions etc - // https://github.com/SD4RK/epicstore_api/blob/master/epicstore_api/api.py#L72 - // https://store-content.ak.epicgames.com/api/de/content/products/rocket-league + // https://github.com/SD4RK/epicstore_api/blob/master/epicstore_api/api.py#L66 // https://store-content.ak.epicgames.com/api/content/productmapping + private void update_store_productmapping() + { + uint status; + var json = Parser.parse_remote_json_file( + @"https://$store_host/api/content/productmapping", + "GET", + null, + unauth_headers, + null, + out status); + assert(status < 400); + assert(json.get_node_type() == Json.NodeType.OBJECT); + + if(log_epic_games_services) debug("[Source.EpicGames.EpicGamesServices.update_store_productmapping] json dump: \n%s", Json.to_string(json, true)); + + _productmapping = json; + } /** - * Retrieve store information. - * - * Tries to match against https://store-content.ak.epicgames.com/api/content/productmapping - * which mostly has the namespace as identifier. However, some only have the appid to match - * against. - * - * Also it's possible the store page doesn't exist (anymore). - * - * @param ns Namespace of an asset - * @param appid Fallback in case the other ID is used - */ + * Retrieve store information. + * + * Tries to match against https://store-content.ak.epicgames.com/api/content/productmapping + * which mostly has the namespace as identifier. However, some only have the appid to match + * against. + * + * Also it's possible the store page doesn't exist (anymore). + * + * @param ns Namespace of an asset + * @param appid Fallback in case the other ID is used + */ + // https://github.com/SD4RK/epicstore_api/blob/master/epicstore_api/api.py#L72 + // https://store-content.ak.epicgames.com/api/de/content/products/darkest-dungeon internal Json.Node get_store_details(string ns, string appid) { var slug = appid; @@ -494,7 +511,8 @@ namespace GameHub.Data.Sources.EpicGames unauth_headers, null, out status); - return_val_if_fail(status < 400, new Json.Node(Json.NodeType.NULL)); // Removed games will fail + // Removed games will fail + return_val_if_fail(status < 400, new Json.Node(Json.NodeType.NULL)); assert(json.get_node_type() != Json.NodeType.NULL); if(log_epic_games_services) debug("[Source.EpicGames.EpicGamesServices.get_store_details] json dump: \n%s", Json.to_string(json, true)); @@ -502,22 +520,42 @@ namespace GameHub.Data.Sources.EpicGames return json; } - private void update_store_productmapping() + // https://github.com/SD4RK/epicstore_api/blob/master/epicstore_api/api.py#L160 + // https://github.com/SD4RK/epicstore_api/blob/master/epicstore_api/api.py#L403 + internal Json.Node get_dlc_details(string ns, string categories = "addons|digitalextras") { - uint status; - var json = Parser.parse_remote_json_file( - @"https://$store_host/api/content/productmapping", - "GET", - null, - unauth_headers, - null, - out status); + const string ADDONS_QUERY = "query getAddonsByNamespace($categories: String!, $count: Int!, $country: String!, $locale: String!, $namespace: String!, $sortBy: String!, $sortDir: String!) {\n Catalog {\n catalogOffers(namespace: $namespace, locale: $locale, params: {category: $categories, count: $count, country: $country, sortBy: $sortBy, sortDir: $sortDir}) {\n elements {\n countriesBlacklist\n customAttributes {\n key\n value\n }\n description\n developer\n effectiveDate\n id\n isFeatured\n keyImages {\n type\n url\n }\n lastModifiedDate\n longDescription\n namespace\n offerType\n productSlug\n releaseDate\n status\n technicalDetails\n title\n urlSlug\n }\n }\n }\n}\n"; + + var request_body_json = new Json.Node(Json.NodeType.OBJECT); + request_body_json.set_object(new Json.Object()); + request_body_json.get_object().set_string_member("query", ADDONS_QUERY); + request_body_json.get_object().set_object_member("variables", new Json.Object()); + request_body_json.get_object().get_object_member("variables").set_string_member("locale", language_code); + request_body_json.get_object().get_object_member("variables").set_string_member("country", country_code); + request_body_json.get_object().get_object_member("variables").set_string_member("namespace", ns); + request_body_json.get_object().get_object_member("variables").set_int_member("count", 250); + request_body_json.get_object().get_object_member("variables").set_string_member("categories", categories); + request_body_json.get_object().get_object_member("variables").set_string_member("sortBy", "releaseDate"); + request_body_json.get_object().get_object_member("variables").set_string_member("sortDir", "ASC"); + + var message = new Message("POST", "https://graphql.epicgames.com/graphql"); + message.request_body.append_take(Json.to_string(request_body_json, false).data); + + // unauth on purpose + var status = session.send_message(message); assert(status < 400); - assert(json.get_node_type() == Json.NodeType.OBJECT); - if(log_epic_games_services) debug("[Source.EpicGames.EpicGamesServices.update_store_productmapping] json dump: \n%s", Json.to_string(json, true)); + var json = Parser.parse_json((string) message.response_body.data); + assert(json.get_node_type() != Json.NodeType.NULL); - _productmapping = json; + if(log_epic_games_services) debug("[Source.EpicGames.EpicGamesServices.get_store_details] json dump: \n%s", Json.to_string(json, true)); + + assert(!json.get_object().has_member("errors")); + + var j = new Json.Node(Json.NodeType.ARRAY); + j.set_array(json.get_object().get_object_member("data").get_object_member("Catalog").get_object_member("catalogOffers").get_array_member("elements")); + + return j; } } } diff --git a/src/meson.build b/src/meson.build index b985e2d3..10d39bcf 100644 --- a/src/meson.build +++ b/src/meson.build @@ -177,6 +177,7 @@ gh_sources = files( 'ui/views/GameDetailsView/blocks/Playtime.vala', 'ui/views/GameDetailsView/blocks/Achievements.vala', 'ui/views/GameDetailsView/blocks/Description.vala', + 'ui/views/GameDetailsView/blocks/EpicDetails.vala', 'ui/views/GameDetailsView/blocks/GOGDetails.vala', 'ui/views/GameDetailsView/blocks/SteamDetails.vala', 'ui/views/GameDetailsView/blocks/IGDBInfo.vala', diff --git a/src/ui/views/GameDetailsView/GameDetailsPage.vala b/src/ui/views/GameDetailsView/GameDetailsPage.vala index ff31f5b9..3c071fd7 100644 --- a/src/ui/views/GameDetailsView/GameDetailsPage.vala +++ b/src/ui/views/GameDetailsView/GameDetailsPage.vala @@ -360,7 +360,8 @@ namespace GameHub.UI.Views.GameDetailsView new Blocks.Playtime(game), igdb, new Blocks.SteamDetails(game), - new Blocks.GOGDetails(game, this) + new Blocks.GOGDetails(game, this), + new Blocks.EpicDetails(game, this) }; foreach(var b in blk) diff --git a/src/ui/views/GameDetailsView/GameDetailsView.vala b/src/ui/views/GameDetailsView/GameDetailsView.vala index 5b775af0..bd44aa1b 100644 --- a/src/ui/views/GameDetailsView/GameDetailsView.vala +++ b/src/ui/views/GameDetailsView/GameDetailsView.vala @@ -240,6 +240,11 @@ namespace GameHub.UI.Views.GameDetailsView continue; } + if(Game.is_equal(g, m) || (g is Sources.EpicGames.EpicGame.DLC && Game.is_equal(((Sources.EpicGames.EpicGame.DLC)g).game, m))) + { + continue; + } + add_page(m); } } diff --git a/src/ui/views/GameDetailsView/blocks/EpicDetails.vala b/src/ui/views/GameDetailsView/blocks/EpicDetails.vala new file mode 100644 index 00000000..060ef3bc --- /dev/null +++ b/src/ui/views/GameDetailsView/blocks/EpicDetails.vala @@ -0,0 +1,411 @@ +/* +This file is part of GameHub. +Copyright (C) 2018-2019 Anatoliy Kashkin + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using Gtk; +using Gdk; +using Gee; + +using GameHub.Data; +using GameHub.Data.Runnables; +using GameHub.Data.Sources.EpicGames; + +using GameHub.UI.Widgets; +using GameHub.UI.Views.GamesView; + +using GameHub.Utils; + +namespace GameHub.UI.Views.GameDetailsView.Blocks +{ + public class EpicDetails: GameDetailsBlock + { + public GameDetailsPage details_page { get; construct; } + + public EpicDetails(Game game, GameDetailsPage page) + { + Object(game: game, orientation: Orientation.VERTICAL, details_page: page, text_max_width: 48); + } + + construct + { + if(!supports_game) return; + + var epic_game = game.cast(); + // var root = Parser.parse_json(game.info_detailed); + + // if(root == null || epic_game == null) return; + if(epic_game == null) return; + + get_style_context().add_class("gameinfo-sidebar-block"); + + var link = new ActionButton(game.source.icon, null, "EpicGames", true, true); + + if(game.store_page != null) + { + link.tooltip_text = game.store_page; + link.clicked.connect(() => { + Utils.open_uri(game.store_page); + }); + } + + add(link); + add(new Separator(Orientation.HORIZONTAL)); + + // var langs = Parser.json_object(root, { "languages" }); + + // if(langs != null) + // { + // var sys_langs = Intl.get_language_names(); + // var langs_string = ""; + // foreach(var l in langs.get_members()) + // { + // var lang = langs.get_string_member(l); + + // if(l in sys_langs) lang = @"$(lang)"; + + // langs_string += (langs_string.length > 0 ? ", " : "") + lang; + // } + + // var langs_label = _("Language"); + + // if(langs_string.contains(",")) + // { + // langs_label = _("Languages"); + // add_scrollable_label(langs_label, langs_string, true); + // } + // else + // { + // add_info_label(langs_label, langs_string, false, true); + // } + // } + + if(epic_game.dlc != null && epic_game.dlc.size > 0) + { + add(new Separator(Orientation.HORIZONTAL)); + + var installable = new ArrayList(); + var not_installable = new ArrayList(); + + foreach(var dlc in epic_game.dlc) + { + (dlc.is_installable ? installable : not_installable).add(dlc); + } + + var dlcbox = new Box(Orientation.VERTICAL, 0); + var header = Styled.H4Label(_("DLC")); + header.margin_start = header.margin_end = 8; + dlcbox.add(header); + + if(installable.size > 0 || not_installable.size <= 3) + { + var dlclist = new ListBox(); + dlclist.selection_mode = SelectionMode.NONE; + dlclist.get_style_context().add_class("gameinfo-content-list"); + + foreach(var dlc in installable) + { + dlclist.add(new DLCRow(dlc, details_page)); + } + + if(not_installable.size <= 3) + { + foreach(var dlc in not_installable) + { + dlclist.add(new DLCRow(dlc, details_page)); + } + } + + dlcbox.add(dlclist); + } + + if(not_installable.size > 3) + { + var dlclist_scrolled = new ScrolledWindow(null, null); + dlclist_scrolled.hscrollbar_policy = PolicyType.NEVER; + dlclist_scrolled.set_size_request(420, 64); + + #if GTK_3_22 + dlclist_scrolled.propagate_natural_width = true; + dlclist_scrolled.propagate_natural_height = true; + dlclist_scrolled.max_content_height = 720; + #endif + + var dlclist = new ListBox(); + dlclist.selection_mode = SelectionMode.NONE; + dlclist.get_style_context().add_class("gameinfo-content-list"); + + foreach(var dlc in not_installable) + { + dlclist.add(new DLCRow(dlc, details_page, false)); + } + + dlclist_scrolled.add(dlclist); + + var dlc_popover_button = new Button.with_label(_("%u DLCs cannot be installed").printf(not_installable.size)); + dlc_popover_button.get_style_context().add_class(Gtk.STYLE_CLASS_FLAT); + dlc_popover_button.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL); + + var dlc_popover = new Popover(dlc_popover_button); + dlc_popover.position = PositionType.LEFT; + + dlc_popover.add(dlclist_scrolled); + dlclist_scrolled.show_all(); + + dlc_popover_button.clicked.connect(() => { + #if GTK_3_22 + dlc_popover.popup(); + #else + dlc_popover.show(); + #endif + }); + + dlcbox.add(new Separator(Orientation.HORIZONTAL)); + dlcbox.add(dlc_popover_button); + } + + add(dlcbox); + } + + // if(epic_game.bonus_content != null && epic_game.bonus_content.size > 0) + // { + // add(new Separator(Orientation.HORIZONTAL)); + + // var bonuslist_scrolled = new ScrolledWindow(null, null); + // bonuslist_scrolled.hscrollbar_policy = PolicyType.NEVER; + // bonuslist_scrolled.set_size_request(420, 64); + + // #if GTK_3_22 + // bonuslist_scrolled.propagate_natural_width = true; + // bonuslist_scrolled.propagate_natural_height = true; + // bonuslist_scrolled.max_content_height = 720; + // #endif + + // var bonuslist = new ListBox(); + // bonuslist.selection_mode = SelectionMode.NONE; + // bonuslist.get_style_context().add_class("gameinfo-content-list"); + + // foreach(var bonus in epic_game.bonus_content) + // { + // bonuslist.add(new BonusContentRow(bonus)); + // } + + // bonuslist_scrolled.add(bonuslist); + + // var bonus_popover_button = new ActionButton("folder-download-symbolic", null, _("Bonus content"), true, true); + + // var bonus_popover = new Popover(bonus_popover_button); + // bonus_popover.position = PositionType.LEFT; + + // bonus_popover.add(bonuslist_scrolled); + // bonuslist_scrolled.show_all(); + + // bonus_popover_button.clicked.connect(() => { + // #if GTK_3_22 + // bonus_popover.popup(); + // #else + // bonus_popover.show(); + // #endif + // }); + + // add(bonus_popover_button); + // } + + show_all(); + + if(parent != null) parent.queue_draw(); + } + + // TODO: Do we need to check for info_detailed here? We don't use any information from it + public override bool supports_game { get { return (game is EpicGame) && game.info_detailed != null && game.info_detailed.length > 0; } } + + // public class BonusContentRow: ListBoxRow + // { + // public EpicGame.BonusContent bonus; + + // public BonusContentRow(EpicGame.BonusContent bonus) + // { + // this.bonus = bonus; + + // var content = new Overlay(); + + // var progress_bar = new Frame(null); + // progress_bar.halign = Align.START; + // progress_bar.vexpand = true; + // progress_bar.get_style_context().add_class("progress"); + + // var box = new Box(Orientation.HORIZONTAL, 8); + // box.margin_start = box.margin_end = 8; + // box.margin_top = box.margin_bottom = 8; + + // var icon = new Image.from_icon_name(bonus.icon, IconSize.BUTTON); + + // var name = new Label(bonus.text); + // name.ellipsize = Pango.EllipsizeMode.END; + // name.hexpand = true; + // name.halign = Align.START; + // name.xalign = 0; + + // var desc_label = new Label(format_size(bonus.size)); + // desc_label.halign = Align.END; + + // var status_icon = new Image.from_icon_name("folder-download-symbolic", IconSize.BUTTON); + // status_icon.halign = Align.END; + + // box.add(icon); + // box.add(name); + // box.add(desc_label); + // box.add(status_icon); + + // var event_box = new Box(Orientation.VERTICAL, 0); + // event_box.expand = true; + + // content.add(box); + // content.add_overlay(progress_bar); + // content.add_overlay(event_box); + + // bonus.status_change.connect(s => { + // if(s.state == EpicGame.BonusContent.State.DOWNLOADING) + // { + // Allocation alloc; + // content.get_allocation(out alloc); + + // if(s.download != null && s.download.status != null) + // { + // progress_bar.get_style_context().add_class("downloading"); + // progress_bar.set_size_request((int) (s.download.status.progress * alloc.width), alloc.height); + // desc_label.label = s.download.status.description; + // desc_label.get_style_context().remove_class(Gtk.STYLE_CLASS_DIM_LABEL); + // desc_label.ellipsize = Pango.EllipsizeMode.NONE; + // status_icon.icon_name = "folder-download-symbolic"; + // } + + // return; + // } + + // progress_bar.get_style_context().remove_class("downloading"); + // progress_bar.set_size_request(0, 0); + + // if(s.state == EpicGame.BonusContent.State.DOWNLOADED && (bonus.downloaded_file == null || !bonus.downloaded_file.query_exists())) + // { + // s.state = EpicGame.BonusContent.State.NOT_DOWNLOADED; + // } + + // if(s.state == EpicGame.BonusContent.State.DOWNLOADED) + // { + // desc_label.label = bonus.filename; + // desc_label.get_style_context().add_class(Gtk.STYLE_CLASS_DIM_LABEL); + // desc_label.ellipsize = Pango.EllipsizeMode.MIDDLE; + // status_icon.icon_name = "document-open-symbolic"; + // } + // else + // { + // desc_label.label = format_size(bonus.size); + // desc_label.get_style_context().remove_class(Gtk.STYLE_CLASS_DIM_LABEL); + // desc_label.ellipsize = Pango.EllipsizeMode.NONE; + // status_icon.icon_name = "folder-download-symbolic"; + // } + // }); + // bonus.status_change(bonus.status); + + // content.add_events(EventMask.ALL_EVENTS_MASK); + // content.button_release_event.connect(e => { + // if(e.button == 1) + // { + // if(bonus.status.state == EpicGame.BonusContent.State.NOT_DOWNLOADED || (bonus.status.state == GOGGame.BonusContent.State.DOWNLOADED && (bonus.downloaded_file == null || !bonus.downloaded_file.query_exists()))) + // { + // bonus.download.begin(); + // } + // else if(bonus.status.state == EpicGame.BonusContent.State.DOWNLOADED) + // { + // bonus.open(); + // } + // } + + // return true; + // }); + + // child = content; + // } + // } + + public class DLCRow: ListBoxRow + { + public EpicGame.DLC dlc; + + public DLCRow(EpicGame.DLC dlc, GameDetailsPage details_page, bool limit_name_width = true) + { + this.dlc = dlc; + + var ebox = new EventBox(); + ebox.margin_start = ebox.margin_end = 8; + ebox.margin_top = ebox.margin_bottom = 6; + + var box = new Box(Orientation.HORIZONTAL, 8); + + var name = new Label(dlc.name); + name.ellipsize = Pango.EllipsizeMode.END; + name.hexpand = true; + name.halign = Align.START; + name.xalign = 0; + + if(limit_name_width) + { + name.max_width_chars = 42; + name.tooltip_text = dlc.name; + } + + var status_icon = new Image.from_icon_name(dlc.status.state == Game.State.INSTALLED ? "process-completed-symbolic" : "folder-download-symbolic", IconSize.BUTTON); + status_icon.opacity = dlc.is_installable ? 1 : 0.6; + status_icon.halign = Align.END; + + ebox.add_events(EventMask.BUTTON_RELEASE_MASK); + ebox.button_release_event.connect(e => { + switch(e.button) + { + case 1: + details_page.details_view.navigate(dlc); + break; + + case 3: + new GameContextMenu(dlc, this).open(e, true); + break; + } + + return true; + }); + + dlc.notify["status"].connect(() => { + Idle.add(() => { + status_icon.icon_name = dlc.status.state == Game.State.INSTALLED ? "process-completed-symbolic" : "folder-download-symbolic"; + status_icon.opacity = dlc.is_installable ? 1 : 0.6; + + return Source.REMOVE; + }); + }); + + dlc.update_game_info.begin(); + + box.add(name); + box.add(status_icon); + + ebox.add(box); + + child = ebox; + } + } + } +} diff --git a/src/ui/views/GamesView/GameContextMenu.vala b/src/ui/views/GamesView/GameContextMenu.vala index e55321a8..ed91c13a 100644 --- a/src/ui/views/GamesView/GameContextMenu.vala +++ b/src/ui/views/GamesView/GameContextMenu.vala @@ -40,7 +40,7 @@ namespace GameHub.UI.Views.GamesView construct { - if(game.status.state == Game.State.INSTALLED && !(game is Sources.GOG.GOGGame.DLC)) + if(game.status.state == Game.State.INSTALLED && !(game is Sources.GOG.GOGGame.DLC) && !(game is Sources.EpicGames.EpicGame.DLC)) { var run = new Gtk.MenuItem.with_label(_("Run")); run.sensitive = game.can_be_launched(); @@ -80,7 +80,7 @@ namespace GameHub.UI.Views.GamesView details.activate.connect(() => new Dialogs.GameDetailsDialog(game).show_all()); add(details); - if(!(game is Sources.GOG.GOGGame.DLC)) + if(!(game is Sources.GOG.GOGGame.DLC) && !(game is Sources.EpicGames.EpicGame.DLC)) { if(Settings.UI.Behavior.instance.merge_games && !is_merge_submenu) { @@ -154,7 +154,7 @@ namespace GameHub.UI.Views.GamesView add(open_screenshots_dir); }*/ - if((game.status.state == Game.State.INSTALLED || game is Sources.User.UserGame) && !(game is Sources.GOG.GOGGame.DLC)) + if((game.status.state == Game.State.INSTALLED || game is Sources.User.UserGame) && !(game is Sources.GOG.GOGGame.DLC) && !(game is Sources.EpicGames.EpicGame.DLC)) { var uninstall = new Gtk.MenuItem.with_label((game is Sources.User.UserGame) ? _("Remove") : _("Uninstall")); uninstall.activate.connect(() => game.uninstall.begin()); @@ -162,7 +162,7 @@ namespace GameHub.UI.Views.GamesView add(uninstall); } - if(!(game is Sources.GOG.GOGGame.DLC)) + if(!(game is Sources.GOG.GOGGame.DLC) && !(game is Sources.EpicGames.EpicGame.DLC)) { add(new Gtk.SeparatorMenuItem()); var properties = new Gtk.MenuItem.with_label(_("Properties")); From 9b0cd05a184ab810caa3008178303d8a25c5cce5 Mon Sep 17 00:00:00 2001 From: Lucki Date: Tue, 11 May 2021 13:41:33 +0200 Subject: [PATCH 11/22] DLC version marker for independent updating is this even a thing? --- src/data/runnables/Game.vala | 4 +- src/data/sources/epicgames/EpicGame.vala | 67 +++++++++++++++++-- src/data/sources/epicgames/EpicGames.vala | 23 ++++++- src/data/sources/epicgames/EpicInstaller.vala | 2 +- 4 files changed, 87 insertions(+), 9 deletions(-) diff --git a/src/data/runnables/Game.vala b/src/data/runnables/Game.vala index 21439d24..cca33cf5 100644 --- a/src/data/runnables/Game.vala +++ b/src/data/runnables/Game.vala @@ -103,7 +103,7 @@ namespace GameHub.Data.Runnables // Version private string? _version = null; - public string? version + public virtual string? version { get { return _version; } set @@ -123,7 +123,7 @@ namespace GameHub.Data.Runnables } } - protected void load_version() + protected virtual void load_version() { if(install_dir == null || !install_dir.query_exists()) return; var file = get_file(@"$(FS.GAMEHUB_DIR)/version"); diff --git a/src/data/sources/epicgames/EpicGame.vala b/src/data/sources/epicgames/EpicGame.vala index 8861afd7..2815033c 100644 --- a/src/data/sources/epicgames/EpicGame.vala +++ b/src/data/sources/epicgames/EpicGame.vala @@ -173,6 +173,8 @@ namespace GameHub.Data.Sources.EpicGames { if(_manifest == null) { + // We need a version to load the proper manifest + // load_version() has already been called on game init if(version != null) { _manifest = EpicGames.load_manifest(load_manifest_from_disk()); @@ -420,7 +422,7 @@ namespace GameHub.Data.Sources.EpicGames // var gameinfo = get_file("gameinfo"); // var goggame = get_file(@"goggame-$(id).info"); - var gh_marker = get_file(@".gamehub_$(id)"); + var gh_marker = (this is DLC) ? get_file(@"$(FS.GAMEHUB_DIR)/$id.version") : get_file(@"$(FS.GAMEHUB_DIR)/version"); var files = new ArrayList(); @@ -1144,7 +1146,7 @@ namespace GameHub.Data.Sources.EpicGames Manifest? old_manifest = null; var tmp2_urls = base_urls; // copy list for manipulation - var old_bytes = version != null ? get_installed_manifest() : null; + var old_bytes = (version != null) ? get_installed_manifest() : null; if(old_bytes == null) { @@ -1197,8 +1199,6 @@ namespace GameHub.Data.Sources.EpicGames } } - // TODO: DLC - var force_update = true; // hardcoded for now // var install_path = task.install_dir; _resume_file = null; @@ -1262,6 +1262,7 @@ namespace GameHub.Data.Sources.EpicGames { if(status.state == Game.State.INSTALLED) { + // Update existing files ArrayList? dirs = new ArrayList(); dirs.add(install_dir); var task = new InstallTask(this, installers, dirs, InstallTask.Mode.AUTO_INSTALL, false); @@ -1411,12 +1412,70 @@ namespace GameHub.Data.Sources.EpicGames update_status(); } + // Allow saving installed DLC version seperate from main game + private string? _version = null; + public override string? version + { + get { return _version; } + set + { + _version = value; + + if(install_dir == null || !install_dir.query_exists()) return; + + var file = get_file(@"$(FS.GAMEHUB_DIR)/$id.version", false); + try + { + FS.mkdir(file.get_parent().get_path()); + FileUtils.set_contents(file.get_path(), _version); + } + catch (Error e) + { + warning("[Game.version.set] Error while writing game version: %s", e.message); + } + } + } + + protected override void load_version() + { + if(install_dir == null || !install_dir.query_exists()) return; + + var file = get_file(@"$(FS.GAMEHUB_DIR)/$id.version"); + + if(file != null) + { + try + { + string ver; + FileUtils.get_contents(file.get_path(), out ver); + version = ver; + } + catch (Error e) + { + warning("[Game.load_version] Error while reading game version: %s", e.message); + } + } + } + public override void update_status() { if(game == null) return; base.update_status(); } + + public override async void install(InstallTask.Mode install_mode = InstallTask.Mode.INTERACTIVE) + { + ArrayList? dirs = new ArrayList(); + dirs.add(install_dir); + var task = new InstallTask(this, installers, dirs, InstallTask.Mode.AUTO_INSTALL, false); + yield task.start(); + } + + public override async void uninstall() + { + // TODO: Only remove DLC files + } } public class Asset diff --git a/src/data/sources/epicgames/EpicGames.vala b/src/data/sources/epicgames/EpicGames.vala index 22d636e1..721bbf16 100644 --- a/src/data/sources/epicgames/EpicGames.vala +++ b/src/data/sources/epicgames/EpicGames.vala @@ -763,10 +763,29 @@ namespace GameHub.Data.Sources.EpicGames internal static Manifest? load_manifest(Bytes data) { - // FIXME: ugly json detection? + if(data == null) return null; + + // TODO: ugly json detection? if(data[0] == '{') { - return new Manifest.from_json(Parser.parse_json((string) data.get_data())); + // Try to fix that utf-8 failing below + uint8[] n = { '\0' }; + var json = (string) data.get_data() + (string) n; + + try + { + // Convert to UTF-8 if it's ASCII + // FIXME: This fails pretty often dunno why + if(!json.validate(-1)) json = convert(json, -1, "UTF-8", "ASCII"); + } + catch (Error e) + { + debug("ASCII to UTF-8 failed!"); + + return null; + } + + return new Manifest.from_json(Parser.parse_json(json)); } return new Manifest.from_bytes(data); diff --git a/src/data/sources/epicgames/EpicInstaller.vala b/src/data/sources/epicgames/EpicInstaller.vala index 4f0a9dde..a2163f7b 100644 --- a/src/data/sources/epicgames/EpicInstaller.vala +++ b/src/data/sources/epicgames/EpicInstaller.vala @@ -18,7 +18,7 @@ namespace GameHub.Data.Sources.EpicGames this.platform = platform; id = game.id; name = game.name; - full_size = game.get_installation_size(platform); // FIXME: This fetches and scans the manifest, try to get this from somewhere else e.g. the store page + full_size = game.get_installation_size(platform); can_import = true; if(platform != Platform.WINDOWS) From 3f60b50e68e37ade7e6844c13bf1e5a997ce7585 Mon Sep 17 00:00:00 2001 From: Lucki Date: Tue, 11 May 2021 14:01:37 +0200 Subject: [PATCH 12/22] Force installation of main game before DLCs --- src/data/sources/epicgames/EpicGame.vala | 7 +++++++ src/ui/views/GameDetailsView/GameDetailsPage.vala | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/data/sources/epicgames/EpicGame.vala b/src/data/sources/epicgames/EpicGame.vala index 2815033c..6dff974d 100644 --- a/src/data/sources/epicgames/EpicGame.vala +++ b/src/data/sources/epicgames/EpicGame.vala @@ -1466,6 +1466,13 @@ namespace GameHub.Data.Sources.EpicGames public override async void install(InstallTask.Mode install_mode = InstallTask.Mode.INTERACTIVE) { + if(game.status.state != Game.State.INSTALLED) + { + warning("Base game not installed, aborting"); + + return; + } + ArrayList? dirs = new ArrayList(); dirs.add(install_dir); var task = new InstallTask(this, installers, dirs, InstallTask.Mode.AUTO_INSTALL, false); diff --git a/src/ui/views/GameDetailsView/GameDetailsPage.vala b/src/ui/views/GameDetailsView/GameDetailsPage.vala index 3c071fd7..63f9eefc 100644 --- a/src/ui/views/GameDetailsView/GameDetailsPage.vala +++ b/src/ui/views/GameDetailsView/GameDetailsPage.vala @@ -291,7 +291,9 @@ namespace GameHub.UI.Views.GameDetailsView action_resume.visible = false; } action_install.visible = s.state != Game.State.INSTALLED; - action_install.sensitive = s.state == Game.State.UNINSTALLED && game.is_installable; + action_install.sensitive = s.state == Game.State.UNINSTALLED + && game.is_installable + && ((game is GameHub.Data.Sources.EpicGames.EpicGame.DLC) ? ((GameHub.Data.Sources.EpicGames.EpicGame.DLC)game).game.status.state == Game.State.INSTALLED : true); action_update.visible = s.state == Game.State.INSTALLED && game is GameHub.Data.Sources.EpicGames.EpicGame; action_update.sensitive = s.state == Game.State.INSTALLED && game is GameHub.Data.Sources.EpicGames.EpicGame && ((GameHub.Data.Sources.EpicGames.EpicGame) game).has_updates; action_run.visible = s.state == Game.State.INSTALLED; From b79d7aef402dc1e2fc2c3ee91abf65720994f84f Mon Sep 17 00:00:00 2001 From: Lucki Date: Mon, 14 Jun 2021 21:10:23 +0200 Subject: [PATCH 13/22] Make sure no dupe chunks are in the list derrod/legendary@7ae4eda5b8b74239f05cfd9395124fc054e0c5c5 --- src/data/sources/epicgames/EpicManifest.vala | 76 +++++++++++++++----- 1 file changed, 58 insertions(+), 18 deletions(-) diff --git a/src/data/sources/epicgames/EpicManifest.vala b/src/data/sources/epicgames/EpicManifest.vala index 8633e985..b1d1c988 100644 --- a/src/data/sources/epicgames/EpicManifest.vala +++ b/src/data/sources/epicgames/EpicManifest.vala @@ -85,7 +85,10 @@ namespace GameHub.Data.Sources.EpicGames { _version = number_string_to_byte_stream(json.get_object().get_string_member_with_default("ManifestFileVersion", "013000000000")).read_uint32(); } - catch (Error e) { debug("error: %s", e.message); } + catch (Error e) + { + debug("error: %s", e.message); + } _meta = new Meta.from_json(json); _chunk_data_list = new ChunkDataList.from_json(json, version); @@ -683,11 +686,11 @@ namespace GameHub.Data.Sources.EpicGames */ internal class ChunkDataList { - private uint8 version { get; } - private uint32 manifest_version { get; } - private uint32 size { get; } - private HashMap guid_int_map { get; default = new HashMap(); } - private HashMap guid_str_map { get; default = new HashMap(); } + private uint8 version { get; } + private uint32 manifest_version { get; } + private uint32 size { get; } + internal HashMap guid_int_map { get; default = new HashMap(); } + private HashMap guid_str_map { get; default = new HashMap(); } internal ArrayList elements { get; default = new ArrayList(); } internal uint32 count { get; set; } @@ -717,7 +720,10 @@ namespace GameHub.Data.Sources.EpicGames { chunk.guid[i] = stream.read_uint32(); } - catch (Error e) { debug("error: %s", e.message); } + catch (Error e) + { + debug("error: %s", e.message); + } } return true; @@ -729,7 +735,10 @@ namespace GameHub.Data.Sources.EpicGames { chunk.hash = stream.read_uint64(); } - catch (Error e) { debug("error: %s", e.message); } + catch (Error e) + { + debug("error: %s", e.message); + } return true; }); @@ -739,7 +748,10 @@ namespace GameHub.Data.Sources.EpicGames { chunk.sha_hash = stream.read_bytes(20); } - catch (Error e) { debug("error: %s", e.message); } + catch (Error e) + { + debug("error: %s", e.message); + } return true; }); @@ -750,7 +762,10 @@ namespace GameHub.Data.Sources.EpicGames { chunk.group_num = stream.read_byte(); } - catch (Error e) { debug("error: %s", e.message); } + catch (Error e) + { + debug("error: %s", e.message); + } return true; }); @@ -761,7 +776,10 @@ namespace GameHub.Data.Sources.EpicGames { chunk.window_size = stream.read_uint32(); } - catch (Error e) { debug("error: %s", e.message); } + catch (Error e) + { + debug("error: %s", e.message); + } return true; }); @@ -772,14 +790,18 @@ namespace GameHub.Data.Sources.EpicGames { chunk.file_size = stream.read_int64(); } - catch (Error e) { debug("error: %s", e.message); } + catch (Error e) + { + debug("error: %s", e.message); + } return true; }); assert(stream.tell() - start == size); } - catch (Error e) {} + catch (Error e) + {} if(log_chunk_data_list) debug(to_string()); } @@ -811,7 +833,10 @@ namespace GameHub.Data.Sources.EpicGames stream.set_byte_order(DataStreamByteOrder.BIG_ENDIAN); chunk_info.sha_hash = stream.read_bytes(20); } - catch (Error e) { debug("error: %s", e.message); } + catch (Error e) + { + debug("error: %s", e.message); + } elements.add(chunk_info); }); @@ -840,7 +865,9 @@ namespace GameHub.Data.Sources.EpicGames } debug("[Sources.EpicManifest.ChunkDataList.get_chunk_by_number] Invalid guid!"); - assert_not_reached(); + + // assert_not_reached(); + return null; } /** @@ -1058,7 +1085,8 @@ namespace GameHub.Data.Sources.EpicGames result = ""; // empty string } } - catch (Error e) {} + catch (Error e) + {} // FIXME: escape? return result; @@ -1093,8 +1121,20 @@ namespace GameHub.Data.Sources.EpicGames file_manifest_list.count = file_manifest_list.elements.size; file_manifest_list.path_map = null; - // add chunks from delta manifest to main manifest and again clear path caches - chunk_data_list.elements.add_all(delta_manifest.chunk_data_list.elements); + // ensure guid map exists + chunk_data_list.get_chunk_by_number(0); + + // add new chunks from delta manifest to main manifest and again clear maps and update count + var existing_chunks_guids = chunk_data_list.guid_int_map.keys; + + foreach(var chunk in delta_manifest.chunk_data_list.elements) + { + if(!(chunk.guid_num in existing_chunks_guids)) + { + chunk_data_list.elements.add(chunk); + } + } + chunk_data_list.count = chunk_data_list.elements.size; chunk_data_list.clear_matching_maps(); // chunk_data_list._path_map = null; ?? From c0bfc844beaf1bdd0041e2b5ee2878bd1da7c37c Mon Sep 17 00:00:00 2001 From: Lucki Date: Tue, 15 Jun 2021 11:54:28 +0200 Subject: [PATCH 14/22] Use system locale --- src/data/sources/epicgames/EpicGame.vala | 7 +-- src/data/sources/epicgames/EpicGames.vala | 50 +++++++++++++++++++ .../sources/epicgames/EpicGamesServices.vala | 19 +++---- 3 files changed, 59 insertions(+), 17 deletions(-) diff --git a/src/data/sources/epicgames/EpicGame.vala b/src/data/sources/epicgames/EpicGame.vala index 6dff974d..a91ad648 100644 --- a/src/data/sources/epicgames/EpicGame.vala +++ b/src/data/sources/epicgames/EpicGame.vala @@ -12,7 +12,7 @@ namespace GameHub.Data.Sources.EpicGames // These three contain sub information for a game. public class EpicGame: Game, Traits.HasExecutableFile, Traits.SupportsCompatTools, - Traits.Game.SupportsTweaks + Traits.Game.SupportsTweaks { // Traits.HasActions // public override ArrayList? actions { get; protected set; default = new ArrayList(); } @@ -935,13 +935,10 @@ namespace GameHub.Data.Sources.EpicGames parameters += "-epicovt=%s".printf(FS.file(FS.Paths.EpicGames.Cache, @"$(asset_info.ns)$(asset_info.catalog_item_id).ovt").get_path()); } - // TODO: language - // var language_code = language_code; - parameters += "-EpicPortal"; parameters += @"-epicusername=$(EpicGames.instance.user_name)"; parameters += @"-epicuserid=$(EpicGames.instance.user_id)"; - parameters += @"-epiclocale=en"; // FIXME: hardcoded for now + parameters += @"-epiclocale=$(EpicGames.instance.language_code)"; return parameters; } diff --git a/src/data/sources/epicgames/EpicGames.vala b/src/data/sources/epicgames/EpicGames.vala index 721bbf16..2201ac03 100644 --- a/src/data/sources/epicgames/EpicGames.vala +++ b/src/data/sources/epicgames/EpicGames.vala @@ -116,6 +116,56 @@ namespace GameHub.Data.Sources.EpicGames } } + /** + * The language code used for EGS API requests + * + * Lowercase two char string representing the language code. + * + * Defaults to system language code if available - otherwise to "en". + */ + private string? _language_code = null; + public string language_code + { + owned get + { + if(_language_code != null) + { + return _language_code; + } + + return Intl.setlocale(LocaleCategory.ALL, null).down().substring(0, 2) ?? "en"; + } + set + { + _language_code = value; + } + } + + /** + * The country code used for EGS API requests + * + * Uppercase two char string representing the country code. + * + * Defaults to system country code if available - otherwise to "US". + */ + private string? _country_code = null; + public string country_code + { + owned get + { + if(_country_code != null) + { + return _country_code; + } + + return Intl.setlocale(LocaleCategory.ALL, null).up().substring(3, 2) ?? "US"; + } + set + { + _country_code = value; + } + } + public EpicGames() { instance = this; diff --git a/src/data/sources/epicgames/EpicGamesServices.vala b/src/data/sources/epicgames/EpicGamesServices.vala index 1fd6cbeb..a556177c 100644 --- a/src/data/sources/epicgames/EpicGamesServices.vala +++ b/src/data/sources/epicgames/EpicGamesServices.vala @@ -26,11 +26,6 @@ namespace GameHub.Data.Sources.EpicGames private const string store_host = "store-content.ak.epicgames.com"; - // TODO: hardcoded for now - private string language_code = "en"; - private string country_code = "US"; - // var language_code = Intl.setlocale(LocaleCategory.ALL, null).down().substring(0, 2); - // used with session, does not include user-agent as that's already set for the session private HashMap auth_headers = new HashMap(); // does not include auth header so it can be used with access token for e.g. Utils.Parser @@ -288,8 +283,8 @@ namespace GameHub.Data.Sources.EpicGames data.set("id", catalog_item_id); data.set("includeDLCDetails", "True"); data.set("includeMainGameDetails", "True"); - data.set("country", country_code); - data.set("locale", language_code); + data.set("country", EpicGames.instance.country_code); + data.set("locale", EpicGames.instance.language_code); uint status; var json = Parser.parse_remote_json_file( @@ -297,8 +292,8 @@ namespace GameHub.Data.Sources.EpicGames ?id=$catalog_item_id &includeDLCDetails=True &includeMainGameDetails=True - &country=$country_code - &locale=$language_code", + &country=$(EpicGames.instance.country_code) + &locale=$(EpicGames.instance.language_code)", "GET", EpicGames.instance.access_token, unauth_headers, @@ -505,7 +500,7 @@ namespace GameHub.Data.Sources.EpicGames uint status; var json = Parser.parse_remote_json_file( - @"https://$store_host/api/$language_code/content/products/$slug", + @"https://$store_host/api/$(EpicGames.instance.language_code)/content/products/$slug", "GET", null, unauth_headers, @@ -530,8 +525,8 @@ namespace GameHub.Data.Sources.EpicGames request_body_json.set_object(new Json.Object()); request_body_json.get_object().set_string_member("query", ADDONS_QUERY); request_body_json.get_object().set_object_member("variables", new Json.Object()); - request_body_json.get_object().get_object_member("variables").set_string_member("locale", language_code); - request_body_json.get_object().get_object_member("variables").set_string_member("country", country_code); + request_body_json.get_object().get_object_member("variables").set_string_member("locale", EpicGames.instance.language_code); + request_body_json.get_object().get_object_member("variables").set_string_member("country", EpicGames.instance.country_code); request_body_json.get_object().get_object_member("variables").set_string_member("namespace", ns); request_body_json.get_object().get_object_member("variables").set_int_member("count", 250); request_body_json.get_object().get_object_member("variables").set_string_member("categories", categories); From 9110a258c1fa1d21418fde1d7228d8424321f34f Mon Sep 17 00:00:00 2001 From: Lucki Date: Tue, 15 Jun 2021 18:09:49 +0200 Subject: [PATCH 15/22] Only remove tracked files --- src/data/sources/epicgames/EpicGame.vala | 77 +++++++++++++++++-- .../GameDetailsView/GameDetailsPage.vala | 2 +- 2 files changed, 71 insertions(+), 8 deletions(-) diff --git a/src/data/sources/epicgames/EpicGame.vala b/src/data/sources/epicgames/EpicGame.vala index a91ad648..ad173321 100644 --- a/src/data/sources/epicgames/EpicGame.vala +++ b/src/data/sources/epicgames/EpicGame.vala @@ -567,11 +567,79 @@ namespace GameHub.Data.Sources.EpicGames public override async void uninstall() { - if(install_dir != null && install_dir.query_exists()) + if(install_dir != null && install_dir.query_exists() && status.state == Game.State.INSTALLED) { // yield umount_overlays(); - FS.rm(install_dir.get_path(), "", "-rf"); + // Remove DLC first so directory is empty when game uninstall runs + if(dlc != null) + { + foreach(var d in dlc) + { + yield d.uninstall(); + } + } + + // delete all files that were installed + ArrayList filelist = new ArrayList(); + foreach(var file_manifest in manifest.file_manifest_list.elements) + { + filelist.add(file_manifest.filename); + } + + ArrayList dirs = new ArrayList((a, b) => { + if(a.get_path() == b.get_path()) return true; + + return false; + }); + foreach(var file in filelist) + { + var folders = file.split("/"); + + // add intermediate directories that would have been missed otherwise + if(folders.length > 1) + { + for(int i = 1; i < folders.length; i++) + { + var folder = FS.file(install_dir.get_path(), string.joinv("/", folders[0 : i])); + + if(!dirs.contains(folder)) + { + dirs.add(folder); + } + } + } + + // FIXME: This takes forever + FS.rm(install_dir.get_path(), file); + } + + + // remove all directories + dirs.sort((a, b) => { + if(a.get_path().length > b.get_path().length) return -1; + + if(a.get_path().length < b.get_path().length) return 1; + + return 0; + }); + foreach(var dir in dirs) + { + FS.rm(dir.get_path(), null, "-d"); + } + + // delete root directory + // FS.rm(install_dir.get_path(), null, "-rf"); + + // Only deleting tracked files result in the gh_marker still present thinking gamehub the game is still installed + // we have to delete it manually + try + { + var gh_marker = (this is DLC) ? get_file(@"$(FS.GAMEHUB_DIR)/$id.version") : get_file(@"$(FS.GAMEHUB_DIR)/version"); + gh_marker.delete(); + } + catch (Error e) {} + update_status(); } @@ -1475,11 +1543,6 @@ namespace GameHub.Data.Sources.EpicGames var task = new InstallTask(this, installers, dirs, InstallTask.Mode.AUTO_INSTALL, false); yield task.start(); } - - public override async void uninstall() - { - // TODO: Only remove DLC files - } } public class Asset diff --git a/src/ui/views/GameDetailsView/GameDetailsPage.vala b/src/ui/views/GameDetailsView/GameDetailsPage.vala index 63f9eefc..b9f009bf 100644 --- a/src/ui/views/GameDetailsView/GameDetailsPage.vala +++ b/src/ui/views/GameDetailsView/GameDetailsPage.vala @@ -300,7 +300,7 @@ namespace GameHub.UI.Views.GameDetailsView action_run.sensitive = game.can_be_launched(); action_open_directory.visible = s.state == Game.State.INSTALLED && game.install_dir != null && game.install_dir.query_exists(); action_open_store_page.visible = game.store_page != null; - action_uninstall.visible = s.state == Game.State.INSTALLED && !(game is GameHub.Data.Sources.GOG.GOGGame.DLC) && !(game is GameHub.Data.Sources.EpicGames.EpicGame.DLC); + action_uninstall.visible = s.state == Game.State.INSTALLED && !(game is GameHub.Data.Sources.GOG.GOGGame.DLC); action_properties.visible = !(game is GameHub.Data.Sources.GOG.GOGGame.DLC) && !(game is GameHub.Data.Sources.EpicGames.EpicGame.DLC); } From 434852d068fb148143ce5417b4add34870f8cd8e Mon Sep 17 00:00:00 2001 From: Lucki Date: Tue, 15 Jun 2021 18:20:09 +0200 Subject: [PATCH 16/22] Merge locale --- src/data/sources/epicgames/EpicGame.vala | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/data/sources/epicgames/EpicGame.vala b/src/data/sources/epicgames/EpicGame.vala index ad173321..de116c1e 100644 --- a/src/data/sources/epicgames/EpicGame.vala +++ b/src/data/sources/epicgames/EpicGame.vala @@ -386,9 +386,7 @@ namespace GameHub.Data.Sources.EpicGames if(slug != "") { - // FIXME: Globify language and merge with …Services - var language_code = Intl.setlocale(LocaleCategory.ALL, null).down().substring(0, 2); - store_page = @"https://www.epicgames.com/store/$language_code/p/$slug"; + store_page = @"https://www.epicgames.com/store/$(EpicGames.instance.language_code)/p/$slug"; } if(about != null) From a644e505ef8566d8b4122cc7a2ff3525c17242c3 Mon Sep 17 00:00:00 2001 From: Lucki Date: Sat, 17 Jul 2021 03:19:17 +0200 Subject: [PATCH 17/22] Playtime tracking --- src/data/compat/tools/wine/Wine.vala | 2 ++ src/data/runnables/Game.vala | 7 +++++++ src/data/runnables/traits/HasExecutableFile.vala | 4 ++-- src/data/sources/epicgames/EpicGame.vala | 6 ++++++ 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/data/compat/tools/wine/Wine.vala b/src/data/compat/tools/wine/Wine.vala index b3175e30..4daf2bc6 100644 --- a/src/data/compat/tools/wine/Wine.vala +++ b/src/data/compat/tools/wine/Wine.vala @@ -179,7 +179,9 @@ namespace GameHub.Data.Compat.Tools.Wine var task = runnable.prepare_exec_task(prepare_exec_cmdline(runnable, file, wine_options), args); if(dir != null) task.dir(dir.get_path()); apply_env(runnable, task, wine_options_local); + if(runnable is Traits.HasExecutableFile) yield runnable.pre_run(); yield task.sync_thread(); + if(runnable is Traits.HasExecutableFile) yield runnable.post_run(); } public virtual File? get_prefix(Traits.SupportsCompatTools runnable, WineOptions? wine_options = null) diff --git a/src/data/runnables/Game.vala b/src/data/runnables/Game.vala index cca33cf5..a830db6a 100644 --- a/src/data/runnables/Game.vala +++ b/src/data/runnables/Game.vala @@ -44,8 +44,15 @@ namespace GameHub.Data.Runnables public string? store_page { get; protected set; default = null; } + /** + * Last launch date in unix time + */ public int64 last_launch { get; set; default = 0; } public int64 playtime_source { get; set; default = 0; } + + /** + * Tracked playtime in minutes + */ public int64 playtime_tracked { get; set; default = 0; } public int64 playtime { get { return playtime_source + playtime_tracked; } } diff --git a/src/data/runnables/traits/HasExecutableFile.vala b/src/data/runnables/traits/HasExecutableFile.vala index 5fb59922..4a51d97a 100644 --- a/src/data/runnables/traits/HasExecutableFile.vala +++ b/src/data/runnables/traits/HasExecutableFile.vala @@ -138,8 +138,8 @@ namespace GameHub.Data.Runnables.Traits }); } - protected virtual async void pre_run(){} - protected virtual async void post_run(){} + public virtual async void pre_run(){} + public virtual async void post_run(){} protected virtual string[] cmdline { diff --git a/src/data/sources/epicgames/EpicGame.vala b/src/data/sources/epicgames/EpicGame.vala index de116c1e..12662dea 100644 --- a/src/data/sources/epicgames/EpicGame.vala +++ b/src/data/sources/epicgames/EpicGame.vala @@ -490,6 +490,8 @@ namespace GameHub.Data.Sources.EpicGames // TODO: sync save files? E.g. Rocket League fails if no save was found // the prefix has to exist already for this + + last_launch = (new DateTime.now_utc()).to_unix(); } public override ExecTask prepare_exec_task(string[]? cmdline_override = null, @@ -556,6 +558,10 @@ namespace GameHub.Data.Sources.EpicGames public override async void post_run() { // TODO: sync save files? + + // minutes = TimeSpan / 6e7 + playtime_tracked += (new DateTime.now_utc()).difference(new DateTime.from_unix_utc(last_launch)) / 6000000; + save(); } // public void update_info(Json.Node json) From dbffbf4bd935e24533c6f73c1094d5ce97f0b05a Mon Sep 17 00:00:00 2001 From: Lucki Date: Thu, 29 Jul 2021 17:56:18 +0200 Subject: [PATCH 18/22] Moving stuff around --- src/data/runnables/Game.vala | 2 + src/data/sources/epicgames/EpicAnalysis.vala | 189 ++++++- .../sources/epicgames/EpicDownloader.vala | 530 +++++++----------- src/data/sources/epicgames/EpicGame.vala | 1 - src/data/sources/epicgames/EpicGames.vala | 21 +- src/data/sources/epicgames/EpicInstaller.vala | 207 ++----- 6 files changed, 447 insertions(+), 503 deletions(-) diff --git a/src/data/runnables/Game.vala b/src/data/runnables/Game.vala index a830db6a..417866b9 100644 --- a/src/data/runnables/Game.vala +++ b/src/data/runnables/Game.vala @@ -52,6 +52,8 @@ namespace GameHub.Data.Runnables /** * Tracked playtime in minutes + * + * minutes = {@link GLib.TimeSpan} / 6e7 */ public int64 playtime_tracked { get; set; default = 0; } public int64 playtime { get { return playtime_source + playtime_tracked; } } diff --git a/src/data/sources/epicgames/EpicAnalysis.vala b/src/data/sources/epicgames/EpicAnalysis.vala index 8acdf812..e4e5994b 100644 --- a/src/data/sources/epicgames/EpicAnalysis.vala +++ b/src/data/sources/epicgames/EpicAnalysis.vala @@ -151,7 +151,7 @@ namespace GameHub.Data.Sources.EpicGames if(mismatch > 0) { - warning(@"[Sources.EpicGames.AnalysisResult] $mismatch previously completed file(s) are missing, they will be redownloaded."); + warning(@"[Sources.EpicGames.AnalysisResult] $mismatch previously completed file(s) are corrupted, they will be redownloaded."); } // remove completed files from changed/added and move them to unchanged for the analysis. @@ -531,7 +531,10 @@ namespace GameHub.Data.Sources.EpicGames // This only exists so I can put both subclasses in one list // so that the tasks order stays in the correct position - internal abstract class Task {} + internal abstract class Task + { + internal async abstract bool process(FileOutputStream? iostream, File install_dir, EpicGame game); + } /** * Download manager task for a file @@ -543,7 +546,7 @@ namespace GameHub.Data.Sources.EpicGames */ internal class FileTask: Task { - internal string filename { get; } + internal string filename { get; } internal bool del { get; default = false; } internal bool empty { get; default = false; } internal bool fopen { get; default = false; } @@ -560,10 +563,7 @@ namespace GameHub.Data.Sources.EpicGames } } - internal FileTask(string filename) - { - _filename = filename; - } + internal FileTask(string filename) { _filename = filename; } internal FileTask.delete(string filename, bool silent = false) { @@ -597,17 +597,132 @@ namespace GameHub.Data.Sources.EpicGames _temporary_filename = old_filename; _del = dele; } + + internal async override bool process(FileOutputStream? iostream, File install_dir, EpicGame game) + { + // make directories + var full_path = File.new_build_filename(install_dir.get_path(), filename); + Utils.FS.mkdir(full_path.get_parent().get_path()); + + try + { + if(empty) + { + full_path.create_readwrite(FileCreateFlags.REPLACE_DESTINATION); + } + else if(fopen) + { + if(iostream != null) + { + warning("[Sources.EpicGames.Installer.install] Opening new file %s without closing previous!", + full_path.get_path()); + iostream.close(); + iostream = null; + } + + if(full_path.query_exists()) + { + iostream = yield full_path.replace_async(null, + false, + FileCreateFlags.REPLACE_DESTINATION); + } + else + { + iostream = yield full_path.create_async(FileCreateFlags.NONE); + } + } + else if(fclose) + { + if(iostream != null) + { + iostream.close(); + iostream = null; + } + else + { + warning("[Sources.EpicGames.Installer.install] Asking to close file that is not open: %s", + full_path.get_path()); + } + + // write last completed file to simple resume file + if(game.resume_file != null) + { + var path = full_path.get_path(); + + if(path[path.length - 4 : path.length] == ".tmp") + { + path = path[0 : path.length - 4]; + } + + var file_hash = yield Utils.compute_file_checksum(full_path, ChecksumType.SHA1); + // var tmp = ""; + + // if(((Analysis.FileTask)file_task).filename[((Analysis.FileTask)file_task).filename.length - 4 : ((Analysis.FileTask)file_task).filename.length] == ".tmp") + // { + // tmp = ((Analysis.FileTask)file_task).filename[0 : ((Analysis.FileTask)file_task).filename.length - 4]; + // } + // else + // { + // tmp = ((Analysis.FileTask)file_task).filename; + // } + + // debug(tmp); + // assert(file_hash == bytes_to_hex(analysis.result.manifest.file_manifest_list.get_file_by_path(tmp).sha_hash)); + + var output_stream = game.resume_file.append_to(FileCreateFlags.NONE); + output_stream.write((string.join(":", file_hash, path) + "\n").data); + + output_stream.close(); + } + } + else if(frename) + { + if(iostream != null) + { + warning("[Sources.EpicGames.Installer.install] Trying to rename file without closing first!"); + iostream.close(); + iostream = null; + } + + if(del) + { + Utils.FS.rm(full_path.get_path()); + } + + File.new_build_filename(install_dir.get_path(), temporary_filename).move(full_path, FileCopyFlags.NONE); + } + else if(del) + { + if(iostream != null) + { + warning("[Sources.EpicGames.Installer.install] Trying to delete file without closing first!"); + iostream.close(); + iostream = null; + } + + Utils.FS.rm(full_path.get_path()); + } + } + catch (Error e) + { + debug("file task failed: %s", e.message); + + return false; + } + + return true; + } } /** - * Download manager chunk task - * - * @param chunk_guid GUID of chunk - * @param cleanup whether or not this chunk can be removed from disk/memory after it has been written - * @param chunk_offset Offset into file or shared memory - * @param chunk_size Size to read from file or shared memory - * @param chunk_file Either cache or existing game file this chunk is read from if not using shared memory - */ + * Download manager chunk task + * + * @param chunk_guid GUID of chunk + * @param cleanup whether or not this chunk can be removed from disk/memory after it has been written + * @param chunk_offset Offset into file or shared memory + * @param chunk_size Size to read from file or shared memory + * @param chunk_file Either cache or existing game file this chunk is read from if not using shared memory + */ internal class ChunkTask: Task { internal uint32 chunk_guid { get; } @@ -622,6 +737,50 @@ namespace GameHub.Data.Sources.EpicGames _chunk_offset = chunk_offset; _chunk_size = chunk_size; } + + internal async override bool process(FileOutputStream? iostream, File install_dir, EpicGame game) + { + var downloaded_chunk = Utils.FS.file(Utils.FS.Paths.EpicGames.Cache + "/chunks/" + game.id + "/" + chunk_guid.to_string()); + + try + { + if(chunk_file != null) + { + // reuse chunk from existing file + FileInputStream? old_stream = null; + assert(File.new_build_filename(install_dir.get_path(), chunk_file).query_exists()); + old_stream = File.new_build_filename(install_dir.get_path(), chunk_file).read(); + old_stream.seek(chunk_offset, SeekType.SET); + var bytes = yield old_stream.read_bytes_async(chunk_size); + yield iostream.write_bytes_async(bytes); + old_stream.close(); + old_stream = null; + } + else if(downloaded_chunk.query_exists()) + { + var chunk = new Chunk.from_byte_stream(new DataInputStream(yield downloaded_chunk.read_async())); + // debug(@"chunk data length $(chunk.data.length)"); + // debug("chunk %s hash: %s", + // hunk_guid.to_string(), + // Checksum.compute_for_bytes(ChecksumType.SHA1, chunk.data)); + yield iostream.write_bytes_async(chunk.data[chunk_offset : chunk_offset + chunk_size]); + // debug(@"written $size bytes"); + + if(cleanup) + { + Utils.FS.rm(downloaded_chunk.get_path()); + } + } + } + catch (Error e) + { + debug("chunk task failed: %s", e.message); + + return false; + } + + return true; + } } } } diff --git a/src/data/sources/epicgames/EpicDownloader.vala b/src/data/sources/epicgames/EpicDownloader.vala index 9528900b..6509d439 100644 --- a/src/data/sources/epicgames/EpicDownloader.vala +++ b/src/data/sources/epicgames/EpicDownloader.vala @@ -2,23 +2,25 @@ using Gee; using Soup; using GameHub.Data.Runnables; -using GameHub.Utils; +// using GameHub.Utils; using GameHub.Utils.Downloader; -using GameHub.Utils.Downloader.SoupDownloader; +// using GameHub.Utils.Downloader.SoupDownloader; namespace GameHub.Data.Sources.EpicGames { // FIXME: This whole thing is a mess because I had to come up with my own stuff here // We need to download a number of x chunks per game and this should be properly represented in // the download manager - private class EpicDownloader: GameHub.Utils.Downloader.SoupDownloader.SoupDownloader + private class EpicDownloader: Downloader { private ArrayQueue dl_queue; private HashTable dl_info; private HashTable downloads; private Session session = new Session(); - private static string[] URL_SCHEMES = { "http", "https" }; + internal static EpicDownloader instance; + + // private static string[] URL_SCHEMES = { "http", "https" }; private static string[] FILENAME_BLACKLIST = { "download" }; internal EpicDownloader() @@ -30,121 +32,95 @@ namespace GameHub.Data.Sources.EpicGames session.max_conns_per_host = 16; session.user_agent = "EpicGamesLauncher/11.0.1-14907503+++Portal+Release-Live Windows/10.0.19041.1.256.64bit"; download_manager().add_downloader(this); + instance = this; } - private EpicDownload? get_game_download(EpicGame? game) + public override Download? get_download(string id) { - if(game == null) return null; - lock (downloads) { - return (EpicDownload?) downloads.get(game.full_id); + return downloads.get(id); } } - private async ArrayList fetch_parts(Installer installer) + private EpicDownload? get_game_download(EpicGame game) { - var parts = new ArrayList(); - debug("preparing download"); - installer.analysis = installer.game.prepare_download(installer.install_task); - - // game is either up to date or hasn't changed, so we have nothing to do - if(installer.analysis.result.dl_size < 1) - { - debug("[Sources.EpicGames.EpicGame.download] Download size is 0, the game is either already up to date or has not changed."); - - if(installer.game.needs_repair && installer.game.repair_file.query_exists()) - { - installer.game.needs_verification = false; - // remove repair file - FS.rm(installer.game.repair_file.get_path()); - - // check if install tags have changed, if they did; try deleting files that are no longer required. - // TODO: update install tags - } - } - - // debug("[Sources.EpicGames.EpicGame.download] Install size: %.02d MiB", installer.analysis.result.install_size / 1024 / 1024); - // debug("[Sources.EpicGames.EpicGame.download] Download size: %.02d MiB", installer.analysis.result.dl_size / 1024 / 1024); - // debug(@"[Sources.EpicGames.EpicGame.download] Reusable size: %.02d MiB (chunks) / $(installer.analysis.result.unchanged) (skipped)", installer.analysis.result.reuse_size / 1024 / 1024); - - foreach(var chunk_guid in installer.analysis.chunks_to_dl) + lock (downloads) { - var chunk = installer.analysis.chunk_data_list.get_chunk_by_number(chunk_guid); - var remote = File.new_for_uri(installer.analysis.base_url + "/" + chunk.path); - var local = FS.file(FS.Paths.EpicGames.Cache + "/chunks/" + installer.game.id + "/" + chunk.guid_num.to_string()); - // debug("local path: %s", local.get_path()); - FS.mkdir(local.get_parent().get_path()); - parts.add(new SoupDownload(remote, local, File.new_for_path(local.get_path() + "~"))); + return (EpicDownload?) downloads.get(game.id); } - - return parts; } // TODO: a lot of small files, we should probably handle this in parallel - internal new async ArrayList download(Installer installer) throws Error + internal async bool download(Installer installer) { var files = new ArrayList(); var game = installer.game; var download = get_game_download(game); - var parts = yield fetch_parts(installer); // installer.task.status = new InstallTask.Status(InstallTask.State.DOWNLOADING); - if(game == null || download != null) return yield await_download(download); + try + { + if(game == null || download != null) return yield await_download(download); + } + catch (Error e) + { + return false; + } - download = new EpicDownload(game.full_id, parts); + download = new EpicDownload(game.id, installer.analysis); + game.status = new Game.Status(Game.State.DOWNLOADING, game, download); - lock (downloads) downloads.set(game.full_id, download); + lock (downloads) downloads.set(game.id, download); download_started(download); var info = new DownloadInfo.for_runnable(game, "Downloading…"); info.download = download; - lock (dl_info) dl_info.set(game.full_id, info); + lock (dl_info) dl_info.set(game.id, info); dl_started(info); if(GameHub.Application.log_downloader) { - debug("[EpicDownloader] Installing '%s'...", game.full_id); + debug("[EpicDownloader] Installing '%s'...", game.id); } - game.status = new Game.Status(Game.State.DOWNLOADING, game, download); - - debug("[DownloadableInstaller.download] Starting (%d parts)", parts.size); + // var ds_id = download_manager().file_download_started.connect(dl => { + // if(dl.id != game.id) return; - var ds_id = download_manager().file_download_started.connect(dl => { - if(dl.id != game.full_id) return; - - installer.install_task.status = new Tasks.Install.InstallTask.Status( - Tasks.Install.InstallTask.State.DOWNLOADING, - dl); - // installer.download_state = new DownloadState(DownloadState.State.DOWNLOADING, dl); - dl.status_change.connect(s => { - installer.install_task.notify_property("status"); - }); - }); + // installer.install_task.status = new Tasks.Install.InstallTask.Status( + // Tasks.Install.InstallTask.State.DOWNLOADING, + // dl); + // // installer.download_state = new DownloadState(DownloadState.State.DOWNLOADING, dl); + // dl.status_change.connect(s => { + // installer.install_task.notify_property("status"); + // }); + // }); try { + yield await_queue(download); + download.status = new EpicDownload.Status(Download.State.STARTING); + debug("[DownloadableInstaller.download] Starting (%d parts)", download.parts.size); + uint32 current_part = 1; - foreach(var part in ((EpicDownload) download).parts) - { - debug("[DownloadableInstaller.download] Part %u: `%s`", current_part, part.remote.get_uri()); + var total_parts = download.parts.size; - FS.mkdir(part.local.get_parent().get_path()); + EpicPart part; + download.session = session; - var download_description = download.id; + while((part = download.parts.poll()) != null) + { + part.session = download.session; + debug("[DownloadableInstaller.download] Part %u of %u: `%s`", current_part, total_parts, part.remote.get_uri()); + lock (dl_info) dl_info.set(game.id, new Utils.Downloader.DownloadInfo.for_runnable(installer.game, _("Downloading part %1$u of %2$u.").printf(current_part, total_parts))); - if(parts.size > 1) - { - download_description = _("Part %1$u of %2$u: %3$s").printf(current_part, parts.size, part.id); - download.status = new EpicDownload.Status( - Download.State.DOWNLOADING, - (int64) installer.analysis.result.dl_size, - current_part / parts.size, - -1, - -1); - } + download.status = new EpicDownload.Status( + Download.State.DOWNLOADING, + (int64) installer.analysis.result.dl_size, + current_part / total_parts); + + Utils.FS.mkdir(part.local.get_parent().get_path()); debug("Downloading " + part.remote.get_uri()); @@ -154,27 +130,26 @@ namespace GameHub.Data.Sources.EpicGames continue; } - var uri = part.remote.get_uri(); - if(part.local.query_exists()) { // TODO: compare hash if(GameHub.Application.log_downloader) { - debug("[SoupDownloader] '%s' is already downloaded", uri); + debug("[SoupDownloader] '%s' is already downloaded", part.remote.get_uri()); } files.add(part.local); + download.downloaded_parts.offer(part); current_part++; continue; } - // var tmp = File.new_for_path(part.local.get_path() + "~"); + if(download.is_cancelled) + { + throw new IOError.CANCELLED("Download cancelled by user"); + } - if(part.remote.get_uri_scheme() in URL_SCHEMES) - yield download_from_http(part, false, false); - else - yield download_from_filesystem(part); + yield download_from_http(part, false, false); if(part.local_tmp.query_exists()) { @@ -185,6 +160,7 @@ namespace GameHub.Data.Sources.EpicGames if(part.local != null && part.local.query_exists()) { files.add(part.local); + download.downloaded_parts.offer(part); // TODO: uncompress, compare hash // https://github.com/derrod/legendary/blob/a2280edea8f7f8da9a080fd3fb2bafcabf9ee33d/legendary/downloader/workers.py#L99 // var chunk = new Chunk.from_file(new DataInputStream(file.read())); @@ -214,7 +190,7 @@ namespace GameHub.Data.Sources.EpicGames // n.set_icon(new ThemedIcon("dialog-warning")); // task.runnable.cast( // game => { - // runnable_id = game.full_id; + // runnable_id = game.id; // var icon = ImageCache.local_file(game.icon, @"games/$(game.source.id)/$(game.id)/icons/"); // if(icon != null && icon.query_exists()) // { @@ -249,7 +225,7 @@ namespace GameHub.Data.Sources.EpicGames if(info != null) dl_ended(info); - throw error; + return false; } catch (Error error) { @@ -258,145 +234,45 @@ namespace GameHub.Data.Sources.EpicGames if(info != null) dl_ended(info); - throw error; + return false; } finally { // download_state = new DownloadState(DownloadState.State.DOWNLOADED); download.status = new FileDownload.Status(Download.State.FINISHED); - lock (downloads) downloads.remove(game.full_id); - lock (dl_info) dl_info.remove(game.full_id); - // lock (dl_queue) dl_queue.remove(game.full_id); + lock (downloads) downloads.remove(game.id); + lock (dl_info) dl_info.remove(game.id); + // lock (dl_queue) dl_queue.remove(game.id); } - download_manager().disconnect(ds_id); + // download_manager().disconnect(ds_id); download_finished(download); dl_ended(info); // game.update_status(); - return files; + return true; } - // public async File? download_part(File remote, File local, DownloadInfo? info = null, bool preserve_filename = true, bool queue = true) throws Error - // { - // if(remote == null || remote.get_uri() == null || remote.get_uri().length == 0) return null; - - // var uri = remote.get_uri(); - // var download = get_file_download(remote); - - // if(download != null) return yield await_download(download); - - // if(local.query_exists()) - // { - // if(GameHub.Application.log_downloader) - // { - // debug("[SoupDownloader] '%s' is already downloaded", uri); - // } - // return local; - // } - - // var tmp = File.new_for_path(local.get_path() + "~"); - - // download = new SoupDownload(remote, local, tmp); - // download.session = session; - - // lock (downloads) - // { - // downloads.set(uri, download); - // } - - // download_started(download); - - // if(info != null) - // { - // info.download = download; - - // lock (dl_info) - // { - // dl_info.set(uri, info); - // } - - // dl_started(info); - // } - - // if(GameHub.Application.log_downloader) - // { - // debug("[SoupDownloader] Downloading '%s'...", uri); - // } - - // download.status = new FileDownload.Status(Download.State.STARTING); - - // try{ - // if(remote.get_uri_scheme() in URL_SCHEMES) - // yield download_from_http(download, preserve_filename, queue); - // else - // yield download_from_filesystem(download); - // } - // catch (IOError.CANCELLED error) - // { - // download.status = new FileDownload.Status(Download.State.CANCELLED); - // download_cancelled(download, error); - // if(info != null) dl_ended(info); - // throw error; - // } - // catch (Error error) - // { - // download.status = new FileDownload.Status(Download.State.FAILED); - // download_failed(download, error); - // if(info != null) dl_ended(info); - // throw error; - // } - // finally - // { - // lock (downloads) downloads.remove(uri); - // lock (dl_info) dl_info.remove(uri); - // lock (dl_queue) dl_queue.remove(uri); - // } - - // if(download.local_tmp.query_exists()) - // { - // download.local_tmp.move(download.local, FileCopyFlags.OVERWRITE); - // } - - // if(GameHub.Application.log_downloader) - // { - // debug("[SoupDownloader] Downloaded '%s'", uri); - // } - - // download_finished(download); - // if(info != null) dl_ended(info); - - // return download.local; - // } - - private async ArrayList? await_download(EpicDownload download) throws Error + private async bool await_download(EpicDownload download) throws Error { - ArrayList files = null; - Error download_error = null; + Error download_error = null; SourceFunc callback = await_download.callback; var download_finished_id = download_finished.connect((downloader, downloaded) => { - if(((SoupDownload) downloaded).id != download.id) return; - - files = new ArrayList(); - ((EpicDownload) downloaded).parts.foreach(part => { - files.add(part.local_tmp); // FIXME: local_tmp? - - return true; - }); + if(((EpicDownload) downloaded).id != download.id) return; callback (); }); var download_cancelled_id = download_cancelled.connect((downloader, cancelled_download, error) => { - if(((SoupDownload) cancelled_download).id != download.id) return; + if(((EpicDownload) cancelled_download).id != download.id) return; download_error = error; callback (); }); var download_failed_id = download_failed.connect((downloader, failed_download, error) => { - if(((SoupDownload) failed_download).id != download.id) return; + if(((EpicDownload) failed_download).id != download.id) return; download_error = error; callback (); @@ -410,60 +286,63 @@ namespace GameHub.Data.Sources.EpicGames if(download_error != null) throw download_error; - return files; + return true; } - // private async void await_queue(EpicDownload download) - // { - // lock (dl_queue) - // { - // if(download.remote.get_uri() in dl_queue) return; - // dl_queue.add(download.remote.get_uri()); - // } - - // var download_finished_id = download_finished.connect( - // (downloader, downloaded) => { - // lock (dl_queue) dl_queue.remove(((SoupDownload) downloaded).remote.get_uri()); - // }); - // var download_cancelled_id = download_cancelled.connect( - // (downloader, cancelled_download, error) => { - // lock (dl_queue) dl_queue.remove(((SoupDownload) cancelled_download).remote.get_uri()); - // }); - // var download_failed_id = download_failed.connect( - // (downloader, failed_download, error) => { - // lock (dl_queue) dl_queue.remove(((SoupDownload) failed_download).remote.get_uri()); - // }); - - // while(dl_queue.peek() != null && dl_queue.peek() != download.remote.get_uri() && !download.is_cancelled) { - // download.status = new FileDownload.Status(Download.State.QUEUED); - // yield Utils.sleep_async(2000); - // } - - // disconnect(download_finished_id); - // disconnect(download_cancelled_id); - // disconnect(download_failed_id); - // } - - private async void download_from_http(SoupDownload download, - bool preserve_filename = true, - bool queue = true) throws Error + private async void await_queue(EpicDownload download) { - var msg = new Message("GET", download.remote.get_uri()); + lock (dl_queue) + { + if(download.id in dl_queue) return; + + dl_queue.add(download.id); + } + + var download_finished_id = download_finished.connect( + (downloader, downloaded) => { + lock (dl_queue) dl_queue.remove(((EpicDownload) downloaded).id); + }); + var download_cancelled_id = download_cancelled.connect( + (downloader, cancelled_download, error) => { + lock (dl_queue) dl_queue.remove(((EpicDownload) cancelled_download).id); + }); + var download_failed_id = download_failed.connect( + (downloader, failed_download, error) => { + lock (dl_queue) dl_queue.remove(((EpicDownload) failed_download).id); + }); + + while(dl_queue.peek() != null && dl_queue.peek() != download.id && !download.is_cancelled) + { + download.status = new FileDownload.Status(Download.State.QUEUED); + yield Utils.sleep_async(2000); + } + + disconnect(download_finished_id); + disconnect(download_cancelled_id); + disconnect(download_failed_id); + } + + private async void download_from_http(EpicPart part, + bool preserve_filename = true, + bool queue = true) throws Error + { + var msg = new Message("GET", part.remote.get_uri()); msg.response_body.set_accumulate(false); - download.session = session; - download.message = msg; + // download.session = session; + // download.message = msg; + part.message = msg; // if(queue) // { // yield await_queue(download); - // // download.status = new FileDownload.Status(Download.State.STARTING); + // download.status = new EpicDownload.Status(Download.State.STARTING); // } - if(download.is_cancelled) - { - throw new IOError.CANCELLED("Download cancelled by user"); - } + // if(download.is_cancelled) + // { + // throw new IOError.CANCELLED("Download cancelled by user"); + // } #if !PKG_FLATPAK var address = msg.get_address(); @@ -481,25 +360,27 @@ namespace GameHub.Data.Sources.EpicGames int64 dl_bytes = 0; int64 dl_bytes_total = 0; - // #if SOUP_2_60 - // int64 resume_from = 0; - // var resume_dl = false; + #if SOUP_2_60 + int64 resume_from = 0; + var resume_dl = false; - // if(download.local_tmp.get_basename().has_suffix("~") && download.local_tmp.query_exists()) - // { - // var info = yield download.local_tmp.query_info_async(FileAttribute.STANDARD_SIZE, FileQueryInfoFlags.NONE); - // resume_from = info.get_size(); - // if(resume_from > 0) - // { - // resume_dl = true; - // msg.request_headers.set_range(resume_from, -1); - // if(GameHub.Application.log_downloader) - // { - // debug(@"[SoupDownloader] Download part found, size: $(resume_from)"); - // } - // } - // } - // #endif + if(part.local_tmp.get_basename().has_suffix("~") && part.local_tmp.query_exists()) + { + var info = yield part.local_tmp.query_info_async(FileAttribute.STANDARD_SIZE, FileQueryInfoFlags.NONE); + resume_from = info.get_size(); + + if(resume_from > 0) + { + resume_dl = true; + msg.request_headers.set_range(resume_from, -1); + + if(GameHub.Application.log_downloader) + { + debug(@"[SoupDownloader] Download part found, size: $(resume_from)"); + } + } + } + #endif msg.got_headers.connect(() => { dl_bytes_total = msg.response_headers.get_content_length(); @@ -532,24 +413,24 @@ namespace GameHub.Data.Sources.EpicGames if(filename == null) { - filename = download.remote.get_basename(); + filename = part.remote.get_basename(); } if(filename != null && !(filename in FILENAME_BLACKLIST)) { - download.local = download.local.get_parent().get_child(filename); + part.local = part.local.get_parent().get_child(filename); } } - if(download.local.query_exists()) + if(part.local.query_exists()) { if(GameHub.Application.log_downloader) { debug(@"[SoupDownloader] '%s' exists", - download.local.get_path()); + part.local.get_path()); } - var info = download.local.query_info(FileAttribute.STANDARD_SIZE, FileQueryInfoFlags.NONE); + var info = part.local.query_info(FileAttribute.STANDARD_SIZE, FileQueryInfoFlags.NONE); if(info.get_size() == dl_bytes_total) { @@ -561,25 +442,27 @@ namespace GameHub.Data.Sources.EpicGames if(GameHub.Application.log_downloader) { - debug(@"[SoupDownloader] Downloading to '%s'", download.local.get_path()); + debug(@"[SoupDownloader] Downloading to '%s'", part.local.get_path()); } - // #if SOUP_2_60 - // int64 rstart = -1, rend = -1; - // if(resume_dl && msg.response_headers.get_content_range(out rstart, out rend, out dl_bytes_total)) - // { - // if(GameHub.Application.log_downloader) - // { - // debug(@"[SoupDownloader] Content-Range is supported($(rstart)-$(rend)), resuming from $(resume_from)"); - // debug(@"[SoupDownloader] Content-Length: $(dl_bytes_total)"); - // } - // dl_bytes = resume_from; - // local_stream = download.local_tmp.append_to(FileCreateFlags.NONE); - // } - // else - // #endif + #if SOUP_2_60 + int64 rstart = -1, rend = -1; + + if(resume_dl && msg.response_headers.get_content_range(out rstart, out rend, out dl_bytes_total)) { - local_stream = download.local_tmp.replace(null, false, FileCreateFlags.REPLACE_DESTINATION); + if(GameHub.Application.log_downloader) + { + debug(@"[SoupDownloader] Content-Range is supported($(rstart)-$(rend)), resuming from $(resume_from)"); + debug(@"[SoupDownloader] Content-Length: $(dl_bytes_total)"); + } + + dl_bytes = resume_from; + local_stream = part.local_tmp.append_to(FileCreateFlags.NONE); + } + else + #endif + { + local_stream = part.local_tmp.replace(null, false, FileCreateFlags.REPLACE_DESTINATION); } } catch (Error e) @@ -588,7 +471,7 @@ namespace GameHub.Data.Sources.EpicGames } }); - int64 last_update = 0; + // int64 last_update = 0; int64 dl_bytes_from_last_update = 0; msg.got_chunk.connect((msg, chunk) => { @@ -601,19 +484,19 @@ namespace GameHub.Data.Sources.EpicGames local_stream.write(chunk.data); chunk.free(); - int64 now = get_real_time(); - int64 diff = now - last_update; + // int64 now = get_real_time(); + // int64 diff = now - last_update; - if(diff > 1000000) - { - int64 dl_speed = (int64) (((double) dl_bytes_from_last_update) / ((double) diff) * ((double) 1000000)); - download.status = new FileDownload.Status(Download.State.DOWNLOADING, - dl_bytes, - dl_bytes_total, - dl_speed); - last_update = now; - dl_bytes_from_last_update = 0; - } + // if(diff > 1000000) + // { + // int64 dl_speed = (int64) (((double) dl_bytes_from_last_update) / ((double) diff) * ((double) 1000000)); + // download.status = new FileDownload.Status(Download.State.DOWNLOADING, + // dl_bytes, + // dl_bytes_total, + // dl_speed); + // last_update = now; + // dl_bytes_from_last_update = 0; + // } } catch (Error e) { @@ -649,41 +532,50 @@ namespace GameHub.Data.Sources.EpicGames throw err; } } + } - private async void download_from_filesystem(SoupDownload download) throws GLib.Error + private class EpicPart + { + public weak Session? session; + public weak Message? message; + public File remote; + public File local; + public File local_tmp; + public Manifest.ChunkDataList.ChunkInfo chunk_info; + + public EpicPart(string id, Analysis analysis) { - if(download.remote == null || !download.remote.query_exists()) return; - - try - { - if(GameHub.Application.log_downloader) - { - debug("[SoupDownloader] Copying '%s' to '%s'", - download.remote.get_path(), - download.local_tmp.get_path()); - } + // base(id, analysis); + } - yield download.remote.copy_async(download.local_tmp, - FileCopyFlags.OVERWRITE, - Priority.DEFAULT, - null, - (current, total) => { download.status = new FileDownload.Status(Download.State.DOWNLOADING, current, total); }); - } - catch (IOError.EXISTS error) {} + public EpicPart.from_chunk_guid(string id, Analysis analysis, uint32 chunk_guid) + { + // base(id, analysis); + chunk_info = analysis.chunk_data_list.get_chunk_by_number(chunk_guid); + remote = File.new_for_uri(analysis.base_url + "/" + chunk_info.path); + local = Utils.FS.file(Utils.FS.Paths.EpicGames.Cache + "/chunks/" + id + "/" + chunk_info.guid_num.to_string()); + local_tmp = File.new_for_path(local.get_path() + "~"); + Utils.FS.mkdir(local.get_parent().get_path()); } } - public class EpicDownload: Download, PausableDownload + private class EpicDownload: Download, PausableDownload { - public weak Session? session; - public weak Message? message; - public bool is_cancelled = false; - public ArrayList parts { get; } + public weak Session? session; + public weak Message? message; + public bool is_cancelled = false; + public ArrayQueue parts { get; default = new ArrayQueue(); } + public ArrayQueue downloaded_parts { get; default = new ArrayQueue(); } - public EpicDownload(string id, ArrayList parts) + public EpicDownload(string id, Analysis analysis) { base(id); - _parts = parts; + + foreach(var chunk_guid in analysis.chunks_to_dl) + { + parts.offer(new EpicPart.from_chunk_guid(id, analysis, chunk_guid)); + // debug("local path: %s", local.get_path()); + } } public void pause() diff --git a/src/data/sources/epicgames/EpicGame.vala b/src/data/sources/epicgames/EpicGame.vala index 12662dea..05487bb3 100644 --- a/src/data/sources/epicgames/EpicGame.vala +++ b/src/data/sources/epicgames/EpicGame.vala @@ -559,7 +559,6 @@ namespace GameHub.Data.Sources.EpicGames { // TODO: sync save files? - // minutes = TimeSpan / 6e7 playtime_tracked += (new DateTime.now_utc()).difference(new DateTime.from_unix_utc(last_launch)) / 6000000; save(); } diff --git a/src/data/sources/epicgames/EpicGames.vala b/src/data/sources/epicgames/EpicGames.vala index 2201ac03..cb290c63 100644 --- a/src/data/sources/epicgames/EpicGames.vala +++ b/src/data/sources/epicgames/EpicGames.vala @@ -174,6 +174,7 @@ namespace GameHub.Data.Sources.EpicGames // Session we're using to access the api new EpicGamesServices(); + new EpicDownloader(); } public override bool is_installed(bool refresh = false) @@ -267,7 +268,8 @@ namespace GameHub.Data.Sources.EpicGames authenticate_with_exchange_code(authenticate_with_sid(cookies)); } - catch (Error e) {} + catch (Error e) + {} Idle.add(authenticate.callback); }); @@ -318,7 +320,8 @@ namespace GameHub.Data.Sources.EpicGames } } } - catch (Error e) {} + catch (Error e) + {} #endif return true; @@ -819,14 +822,22 @@ namespace GameHub.Data.Sources.EpicGames if(data[0] == '{') { // Try to fix that utf-8 failing below - uint8[] n = { '\0' }; - var json = (string) data.get_data() + (string) n; + // uint8[] n = { '\0' }; + // var json = (string) data.get_data() + (string) n; + + string json; try { // Convert to UTF-8 if it's ASCII // FIXME: This fails pretty often dunno why - if(!json.validate(-1)) json = convert(json, -1, "UTF-8", "ASCII"); + // if(!json.validate(-1)) + // { + // https://gist.github.com/hakre/4188459 + var converter = IConv.open("UTF-8//TRANSLIT", "US-ASCII"); + json = convert_with_iconv((string) data.get_data(), -1, converter); + converter.close(); + // } } catch (Error e) { diff --git a/src/data/sources/epicgames/EpicInstaller.vala b/src/data/sources/epicgames/EpicInstaller.vala index a2163f7b..fce10ce7 100644 --- a/src/data/sources/epicgames/EpicInstaller.vala +++ b/src/data/sources/epicgames/EpicInstaller.vala @@ -8,7 +8,7 @@ namespace GameHub.Data.Sources.EpicGames { internal class Installer: Runnables.Tasks.Install.Installer { - internal Analysis? analysis { get; set; default = null; } + internal Analysis? analysis { get; default = null; } internal EpicGame game { get; private set; } internal InstallTask? install_task { get; default = null; } @@ -45,181 +45,38 @@ namespace GameHub.Data.Sources.EpicGames if(game is EpicGame.DLC) { - if(((EpicGame.DLC)game).game.install_dir == null) return false; + if(((EpicGame.DLC) game).game.install_dir == null) return false; - _install_task.install_dir = ((EpicGame.DLC)game).game.install_dir; + install_task.install_dir = ((EpicGame.DLC) game).game.install_dir; } debug("starting installation"); - var downloader = new EpicDownloader(); - try - { - var downloaded_chunks = yield downloader.download(this); + debug("preparing download"); + _analysis = game.prepare_download(install_task); - // download_task should be available here with all required information - // tasks should be in the correct order open -> write chunk -> close - var full_path = install_task.install_dir; - FileOutputStream? iostream = null; + // game is either up to date or hasn't changed, so we have nothing to do + if(analysis.result.dl_size < 1) + { + debug("[Sources.EpicGames.EpicGame.download] Download size is 0, the game is either already up to date or has not changed."); - foreach(var file_task in analysis.tasks) + if(game.needs_repair && game.repair_file.query_exists()) { - if(file_task is Analysis.FileTask) - { - // make directories - full_path = File.new_build_filename(install_task.install_dir.get_path(), - ((Analysis.FileTask)file_task).filename); - FS.mkdir(full_path.get_parent().get_path()); - - if(((Analysis.FileTask)file_task).empty) - { - full_path.create_readwrite(FileCreateFlags.REPLACE_DESTINATION); - continue; - } - else if(((Analysis.FileTask)file_task).fopen) - { - if(iostream != null) - { - warning("[Sources.EpicGames.Installer.install] Opening new file %s without closing previous!", - full_path.get_path()); - iostream.close(); - iostream = null; - } - - if(full_path.query_exists()) - { - iostream = yield full_path.replace_async(null, - false, - FileCreateFlags.REPLACE_DESTINATION); - } - else - { - iostream = yield full_path.create_async(FileCreateFlags.NONE); - } - - continue; - } - else if(((Analysis.FileTask)file_task).fclose) - { - if(iostream != null) - { - iostream.close(); - iostream = null; - } - else - { - warning("[Sources.EpicGames.Installer.install] Asking to close file that is not open: %s", - full_path.get_path()); - } - - // write last completed file to simple resume file - if(game.resume_file != null) - { - var path = full_path.get_path(); - - if(path[path.length - 4:path.length] == ".tmp") - { - path = path[0 : path.length - 4]; - } - - var file_hash = yield Utils.compute_file_checksum(full_path, ChecksumType.SHA1); - // var tmp = ""; - - // if(((Analysis.FileTask)file_task).filename[((Analysis.FileTask)file_task).filename.length - 4 : ((Analysis.FileTask)file_task).filename.length] == ".tmp") - // { - // tmp = ((Analysis.FileTask)file_task).filename[0 : ((Analysis.FileTask)file_task).filename.length - 4]; - // } - // else - // { - // tmp = ((Analysis.FileTask)file_task).filename; - // } - - // debug(tmp); - // assert(file_hash == bytes_to_hex(analysis.result.manifest.file_manifest_list.get_file_by_path(tmp).sha_hash)); - - var output_stream = game.resume_file.append_to(FileCreateFlags.NONE); - output_stream.write((string.join(":", file_hash, path) + "\n").data); - - output_stream.close(); - } - - continue; - } - else if(((Analysis.FileTask)file_task).frename) - { - if(iostream != null) - { - warning("[Sources.EpicGames.Installer.install] Trying to rename file without closing first!"); - iostream.close(); - iostream = null; - } - - if(((Analysis.FileTask)file_task).del) - { - FS.rm(full_path.get_path()); - } - - File.new_build_filename(install_task.install_dir.get_path(), - ((Analysis.FileTask)file_task).temporary_filename).move(full_path, FileCopyFlags.NONE); - continue; - } - else if(((Analysis.FileTask)file_task).del) - { - if(iostream != null) - { - warning("[Sources.EpicGames.Installer.install] Trying to delete file without closing first!"); - iostream.close(); - iostream = null; - } - - FS.rm(full_path.get_path()); - continue; - } - } + if(game.needs_verification) game.needs_verification = false; - assert(file_task is Analysis.ChunkTask); - assert_nonnull(iostream); - - // FIXME: this blocks the UI, do in an own thread/async - var downloaded_chunk = FS.file(FS.Paths.EpicGames.Cache + "/chunks/" + game.id + "/" + ((Analysis.ChunkTask)file_task).chunk_guid.to_string()); - - if(((Analysis.ChunkTask)file_task).chunk_file != null) - { - // reuse chunk from existing file - FileInputStream? old_stream = null; - assert(File.new_build_filename(install_task.install_dir.get_path(), - ((Analysis.ChunkTask)file_task).chunk_file).query_exists()); - old_stream = File.new_build_filename(install_task.install_dir.get_path(), - ((Analysis.ChunkTask)file_task).chunk_file).read(); - old_stream.seek(((Analysis.ChunkTask)file_task).chunk_offset, SeekType.SET); - var bytes = yield old_stream.read_bytes_async(((Analysis.ChunkTask)file_task).chunk_size); - yield iostream.write_bytes_async(bytes); - old_stream.close(); - old_stream = null; - } - else if(downloaded_chunk.query_exists()) - { - var chunk = new Chunk.from_byte_stream(new DataInputStream(yield downloaded_chunk.read_async())); - // debug(@"chunk data length $(chunk.data.length)"); - // debug("chunk %s hash: %s", - // ((Analysis.ChunkTask)file_task).chunk_guid.to_string(), - // Checksum.compute_for_bytes(ChecksumType.SHA1, chunk.data)); - var size = yield iostream.write_bytes_async(chunk.data[((Analysis.ChunkTask)file_task).chunk_offset : ((Analysis.ChunkTask)file_task).chunk_offset + ((Analysis.ChunkTask)file_task).chunk_size]); - // debug(@"written $size bytes"); - } - else - { - assert_not_reached(); - } + // remove repair file + Utils.FS.rm(game.repair_file.get_path()); } + + // check if install tags have changed, if they did; try deleting files that are no longer required. + // TODO: update install tags } - catch (Error e) + else { - debug("chunk building failed: %s", e.message); - assert_not_reached(); - } + if(!yield EpicDownloader.instance.download(this)) assert_not_reached(); - // TODO: clean cache path + if(!yield write_files(game, install_task.install_dir, analysis.tasks)) assert_not_reached(); + } update_game_info(); @@ -276,5 +133,29 @@ namespace GameHub.Data.Sources.EpicGames game.save(); game.update_status(); } + + private async bool write_files(EpicGame game, File install_dir, ArrayList tasks) + { + // download_task should be available here with all required information + // tasks should be in the correct order: open -> write chunk -> close + FileOutputStream? iostream = null; + + foreach(var file_task in tasks) + { + if(file_task is Analysis.FileTask) + { + return_val_if_fail(yield file_task.process(iostream, install_dir, game), false); + continue; + } + + // We should only be here with a valid iostream + return_val_if_fail(file_task is Analysis.ChunkTask, false); + assert_nonnull(iostream); + + return_val_if_fail(yield file_task.process(iostream, install_dir, game), false); + } + + return true; + } } } From 285eb5da9d1892b5d9cecb1e218864223d31ecb5 Mon Sep 17 00:00:00 2001 From: Lucki Date: Fri, 30 Jul 2021 13:22:18 +0200 Subject: [PATCH 19/22] write files asap --- src/data/sources/epicgames/EpicAnalysis.vala | 103 ++++++++++------ .../sources/epicgames/EpicDownloader.vala | 24 ++-- src/data/sources/epicgames/EpicGame.vala | 4 +- src/data/sources/epicgames/EpicInstaller.vala | 114 ++++++++++++++++-- 4 files changed, 181 insertions(+), 64 deletions(-) diff --git a/src/data/sources/epicgames/EpicAnalysis.vala b/src/data/sources/epicgames/EpicAnalysis.vala index e4e5994b..790a3d12 100644 --- a/src/data/sources/epicgames/EpicAnalysis.vala +++ b/src/data/sources/epicgames/EpicAnalysis.vala @@ -13,11 +13,11 @@ namespace GameHub.Data.Sources.EpicGames // FIXME: There are a lot of things related to Legendarys memory management we probably don't even need private class Analysis { - internal AnalysisResult? result { get; default = null; } - internal ArrayList tasks { get; default = new ArrayList(); } - internal LinkedList chunks_to_dl { get; default = new LinkedList(); } - internal Manifest.ChunkDataList chunk_data_list { get; default = null; } - internal string? base_url { get; default = null; } + internal AnalysisResult? result { get; default = null; } + internal ArrayList> tasks { get; default = new ArrayList>(); } + internal LinkedList chunks_to_dl { get; default = new LinkedList(); } + internal Manifest.ChunkDataList chunk_data_list { get; default = null; } + internal string? base_url { get; default = null; } private File? resume_file { get; default = null; } private HashMap hash_map { get; default = new HashMap(); } @@ -70,15 +70,15 @@ namespace GameHub.Data.Sources.EpicGames private uint32 removed { get; default = 0; } private uint32 uncompressed_dl_size { get; default = 0; } - internal AnalysisResult(Manifest new_manifest, - string download_dir, - ref HashMap hash_map, - ref LinkedList chunks_to_dl, - ref ArrayList tasks, - out Manifest.ChunkDataList chunk_data_list, - Manifest? old_manifest = null, - File? resume_file = null, - string[]? file_install_tags = null) + internal AnalysisResult(Manifest new_manifest, + string download_dir, + ref HashMap hash_map, + ref LinkedList chunks_to_dl, + ref ArrayList> tasks, + out Manifest.ChunkDataList chunk_data_list, + Manifest? old_manifest = null, + File? resume_file = null, + string[]? file_install_tags = null) { foreach(var element in new_manifest.file_manifest_list.elements) { @@ -369,7 +369,9 @@ namespace GameHub.Data.Sources.EpicGames } else if(current_file.chunk_parts.size == 0) { - tasks.add(new FileTask.empty_file(current_file.filename)); + var task_list = new ArrayList(); + task_list.add(new FileTask.empty_file(current_file.filename)); + tasks.add(task_list); continue; } @@ -438,21 +440,26 @@ namespace GameHub.Data.Sources.EpicGames { if(log_analysis) debug(@"[Sources.EpicGames.AnalysisResult] Reusing $reused chunks from: $(current_file.filename)"); + var task_list = new ArrayList(); // open temporary file that will contain download + old file contents - tasks.add(new FileTask.open(current_file.filename + ".tmp")); - tasks.add_all(chunk_tasks); - tasks.add(new FileTask.close(current_file.filename + ".tmp")); + task_list.add(new FileTask.open(current_file.filename + ".tmp")); + task_list.add_all(chunk_tasks); + task_list.add(new FileTask.close(current_file.filename + ".tmp")); // delete old file and rename temporary - tasks.add(new FileTask.rename(current_file.filename, - current_file.filename + ".tmp", - true)); + task_list.add(new FileTask.rename(current_file.filename, + current_file.filename + ".tmp", + true)); + + tasks.add(task_list); } else { - tasks.add(new FileTask.open(current_file.filename)); - tasks.add_all(chunk_tasks); - tasks.add(new FileTask.close(current_file.filename)); + var task_list = new ArrayList(); + task_list.add(new FileTask.open(current_file.filename)); + task_list.add_all(chunk_tasks); + task_list.add(new FileTask.close(current_file.filename)); + tasks.add(task_list); } // check if runtime cache size has changed @@ -487,10 +494,12 @@ namespace GameHub.Data.Sources.EpicGames // add jobs to remove files foreach(var filename in manifest_comparison.removed) { - tasks.add(new FileTask.delete(filename)); + var task_list = new ArrayList(); + task_list.add(new FileTask.delete(filename)); + tasks.add(task_list); } - tasks.add_all(additional_deletion_tasks); + tasks.add(additional_deletion_tasks); _num_chunks_cache = dl_cache_guids.size; chunk_data_list = new_manifest.chunk_data_list; @@ -533,7 +542,7 @@ namespace GameHub.Data.Sources.EpicGames // so that the tasks order stays in the correct position internal abstract class Task { - internal async abstract bool process(FileOutputStream? iostream, File install_dir, EpicGame game); + internal abstract bool process(ref FileOutputStream? iostream, File install_dir, EpicGame game); } /** @@ -598,10 +607,11 @@ namespace GameHub.Data.Sources.EpicGames _del = dele; } - internal async override bool process(FileOutputStream? iostream, File install_dir, EpicGame game) + internal override bool process(ref FileOutputStream? iostream, File install_dir, EpicGame game) { // make directories var full_path = File.new_build_filename(install_dir.get_path(), filename); + debug("Path: " + full_path.get_path()); Utils.FS.mkdir(full_path.get_parent().get_path()); try @@ -622,13 +632,13 @@ namespace GameHub.Data.Sources.EpicGames if(full_path.query_exists()) { - iostream = yield full_path.replace_async(null, - false, - FileCreateFlags.REPLACE_DESTINATION); + iostream = full_path.replace(null, + false, + FileCreateFlags.REPLACE_DESTINATION); } else { - iostream = yield full_path.create_async(FileCreateFlags.NONE); + iostream = full_path.create(FileCreateFlags.NONE); } } else if(fclose) @@ -654,7 +664,20 @@ namespace GameHub.Data.Sources.EpicGames path = path[0 : path.length - 4]; } - var file_hash = yield Utils.compute_file_checksum(full_path, ChecksumType.SHA1); + // var file_hash = yield Utils.compute_file_checksum(full_path, ChecksumType.SHA1); + // This is basically Utils.compute_file_checksum() but I need this in sync to be able to use ref iostream + Checksum checksum = new Checksum(ChecksumType.SHA1); + FileStream stream = FileStream.open(full_path.get_path(), "rb"); + uint8 buf[4096]; + size_t size; + + while((size = stream.read(buf)) > 0) + { + checksum.update(buf, size); + } + + var file_hash = checksum.get_string(); + // var tmp = ""; // if(((Analysis.FileTask)file_task).filename[((Analysis.FileTask)file_task).filename.length - 4 : ((Analysis.FileTask)file_task).filename.length] == ".tmp") @@ -738,7 +761,7 @@ namespace GameHub.Data.Sources.EpicGames _chunk_size = chunk_size; } - internal async override bool process(FileOutputStream? iostream, File install_dir, EpicGame game) + internal override bool process(ref FileOutputStream? iostream, File install_dir, EpicGame game) { var downloaded_chunk = Utils.FS.file(Utils.FS.Paths.EpicGames.Cache + "/chunks/" + game.id + "/" + chunk_guid.to_string()); @@ -751,19 +774,19 @@ namespace GameHub.Data.Sources.EpicGames assert(File.new_build_filename(install_dir.get_path(), chunk_file).query_exists()); old_stream = File.new_build_filename(install_dir.get_path(), chunk_file).read(); old_stream.seek(chunk_offset, SeekType.SET); - var bytes = yield old_stream.read_bytes_async(chunk_size); - yield iostream.write_bytes_async(bytes); + var bytes = old_stream.read_bytes(chunk_size); + iostream.write_bytes(bytes); old_stream.close(); old_stream = null; } else if(downloaded_chunk.query_exists()) { - var chunk = new Chunk.from_byte_stream(new DataInputStream(yield downloaded_chunk.read_async())); + var chunk = new Chunk.from_byte_stream(new DataInputStream(downloaded_chunk.read())); // debug(@"chunk data length $(chunk.data.length)"); // debug("chunk %s hash: %s", // hunk_guid.to_string(), // Checksum.compute_for_bytes(ChecksumType.SHA1, chunk.data)); - yield iostream.write_bytes_async(chunk.data[chunk_offset : chunk_offset + chunk_size]); + iostream.write_bytes(chunk.data[chunk_offset: chunk_offset + chunk_size]); // debug(@"written $size bytes"); if(cleanup) @@ -771,6 +794,10 @@ namespace GameHub.Data.Sources.EpicGames Utils.FS.rm(downloaded_chunk.get_path()); } } + else + { + assert_not_reached(); + } } catch (Error e) { diff --git a/src/data/sources/epicgames/EpicDownloader.vala b/src/data/sources/epicgames/EpicDownloader.vala index 6509d439..13584356 100644 --- a/src/data/sources/epicgames/EpicDownloader.vala +++ b/src/data/sources/epicgames/EpicDownloader.vala @@ -54,7 +54,6 @@ namespace GameHub.Data.Sources.EpicGames // TODO: a lot of small files, we should probably handle this in parallel internal async bool download(Installer installer) { - var files = new ArrayList(); var game = installer.game; var download = get_game_download(game); @@ -138,8 +137,11 @@ namespace GameHub.Data.Sources.EpicGames debug("[SoupDownloader] '%s' is already downloaded", part.remote.get_uri()); } - files.add(part.local); - download.downloaded_parts.offer(part); + if(!yield installer.write_file(part.chunk_info.guid_num)) + { + throw new Error(0, 0, "Error"); + } + current_part++; continue; } @@ -159,8 +161,6 @@ namespace GameHub.Data.Sources.EpicGames // var file = yield download(part.remote, part.local, new Downloader.DownloadInfo.for_runnable(task.runnable, partDesc), false); if(part.local != null && part.local.query_exists()) { - files.add(part.local); - download.downloaded_parts.offer(part); // TODO: uncompress, compare hash // https://github.com/derrod/legendary/blob/a2280edea8f7f8da9a080fd3fb2bafcabf9ee33d/legendary/downloader/workers.py#L99 // var chunk = new Chunk.from_file(new DataInputStream(file.read())); @@ -210,6 +210,11 @@ namespace GameHub.Data.Sources.EpicGames // } } + if(!yield installer.write_file(part.chunk_info.guid_num)) + { + throw new Error(0, 0, "Error"); + } + current_part++; } @@ -242,7 +247,7 @@ namespace GameHub.Data.Sources.EpicGames download.status = new FileDownload.Status(Download.State.FINISHED); lock (downloads) downloads.remove(game.id); lock (dl_info) dl_info.remove(game.id); - // lock (dl_queue) dl_queue.remove(game.id); + lock (dl_queue) dl_queue.remove(game.id); } // download_manager().disconnect(ds_id); @@ -543,14 +548,10 @@ namespace GameHub.Data.Sources.EpicGames public File local_tmp; public Manifest.ChunkDataList.ChunkInfo chunk_info; - public EpicPart(string id, Analysis analysis) - { - // base(id, analysis); - } + // public EpicPart(string id, Analysis analysis) {} public EpicPart.from_chunk_guid(string id, Analysis analysis, uint32 chunk_guid) { - // base(id, analysis); chunk_info = analysis.chunk_data_list.get_chunk_by_number(chunk_guid); remote = File.new_for_uri(analysis.base_url + "/" + chunk_info.path); local = Utils.FS.file(Utils.FS.Paths.EpicGames.Cache + "/chunks/" + id + "/" + chunk_info.guid_num.to_string()); @@ -565,7 +566,6 @@ namespace GameHub.Data.Sources.EpicGames public weak Message? message; public bool is_cancelled = false; public ArrayQueue parts { get; default = new ArrayQueue(); } - public ArrayQueue downloaded_parts { get; default = new ArrayQueue(); } public EpicDownload(string id, Analysis analysis) { diff --git a/src/data/sources/epicgames/EpicGame.vala b/src/data/sources/epicgames/EpicGame.vala index 05487bb3..7e6224e1 100644 --- a/src/data/sources/epicgames/EpicGame.vala +++ b/src/data/sources/epicgames/EpicGame.vala @@ -641,8 +641,10 @@ namespace GameHub.Data.Sources.EpicGames var gh_marker = (this is DLC) ? get_file(@"$(FS.GAMEHUB_DIR)/$id.version") : get_file(@"$(FS.GAMEHUB_DIR)/version"); gh_marker.delete(); } - catch (Error e) {} + catch (Error e) + {} + _manifest = null; // Forget cached manifest update_status(); } diff --git a/src/data/sources/epicgames/EpicInstaller.vala b/src/data/sources/epicgames/EpicInstaller.vala index fce10ce7..a44b482d 100644 --- a/src/data/sources/epicgames/EpicInstaller.vala +++ b/src/data/sources/epicgames/EpicInstaller.vala @@ -12,6 +12,8 @@ namespace GameHub.Data.Sources.EpicGames internal EpicGame game { get; private set; } internal InstallTask? install_task { get; default = null; } + private ArrayList> file_tasks { get; } + internal Installer(EpicGame game, Platform platform) { _game = game; @@ -53,7 +55,8 @@ namespace GameHub.Data.Sources.EpicGames debug("starting installation"); debug("preparing download"); - _analysis = game.prepare_download(install_task); + _analysis = game.prepare_download(install_task); + _file_tasks = analysis.tasks; // game is either up to date or hasn't changed, so we have nothing to do if(analysis.result.dl_size < 1) @@ -73,9 +76,26 @@ namespace GameHub.Data.Sources.EpicGames } else { - if(!yield EpicDownloader.instance.download(this)) assert_not_reached(); + if(!yield EpicDownloader.instance.download(this)) + { + debug("downloading failed"); + task.status = new InstallTask.Status(InstallTask.State.NONE); + game.status = new Game.Status(Game.State.UNINSTALLED, this.game); - if(!yield write_files(game, install_task.install_dir, analysis.tasks)) assert_not_reached(); + return false; + } + + if(!file_tasks.is_empty) + { + if(!yield write_files(file_tasks)) + { + debug("downloading failed"); + task.status = new InstallTask.Status(InstallTask.State.NONE); + game.status = new Game.Status(Game.State.UNINSTALLED, this.game); + + return false; + } + } } update_game_info(); @@ -134,27 +154,95 @@ namespace GameHub.Data.Sources.EpicGames game.update_status(); } - private async bool write_files(EpicGame game, File install_dir, ArrayList tasks) + private async bool write_files(ArrayList> tasks) { // download_task should be available here with all required information // tasks should be in the correct order: open -> write chunk -> close FileOutputStream? iostream = null; - - foreach(var file_task in tasks) + foreach(var task_list in tasks) { - if(file_task is Analysis.FileTask) + foreach(var task in task_list) + { + if(task is Analysis.FileTask) + { + return_val_if_fail(task.process(ref iostream, install_task.install_dir, game), false); + continue; + } + + // We should only be here with a valid iostream + return_val_if_fail(task is Analysis.ChunkTask, false); + assert_nonnull(iostream); + + return_val_if_fail(task.process(ref iostream, install_task.install_dir, game), false); + } + } + + return true; + } + + /** Write file if we have all required chunks */ + internal async bool write_file(uint32 guid_num) + { + var current_file_tasks = new ArrayList>(); + + // Get all tasks with the current guid and process it if we also have all other chunks + lock (file_tasks) { + foreach(var task_list in file_tasks) { - return_val_if_fail(yield file_task.process(iostream, install_dir, game), false); - continue; + if(task_list.first_match(() => + { + foreach(var task in task_list) + { + if(task is Analysis.ChunkTask + && ((Analysis.ChunkTask) task).chunk_guid == guid_num) + { + return true; + } + } + }) == null) + { + // This task set does not include this guid + continue; + } + + var list_complete = true; + foreach(var task in task_list) + { + // Check if other downloaded chunks are available + if(task is Analysis.FileTask + || (task is Analysis.ChunkTask + && ((Analysis.ChunkTask) task).chunk_file != null)) + { + continue; + } + + if(!Utils.FS.file(Utils.FS.Paths.EpicGames.Cache + "/chunks/" + game.id + "/" + ((Analysis.ChunkTask) task).chunk_guid.to_string()).query_exists()) + { + list_complete = false; + break; + } + } + + if(list_complete) + { + // FIXME: We may have lists here already which includes cleanup of our chunk + // while others still depend on it being available + current_file_tasks.add(task_list); + } } - // We should only be here with a valid iostream - return_val_if_fail(file_task is Analysis.ChunkTask, false); - assert_nonnull(iostream); + file_tasks.remove_all(current_file_tasks); + } + + if(current_file_tasks.is_empty) + { + debug("Nothing to do yet…"); - return_val_if_fail(yield file_task.process(iostream, install_dir, game), false); + return true; } + return_val_if_fail(yield write_files(current_file_tasks), false); + return true; } } From b18d9655dccb9a08785ee4f83f209b5bfc23a298 Mon Sep 17 00:00:00 2001 From: Lucki Date: Sat, 30 Oct 2021 12:47:12 +0200 Subject: [PATCH 20/22] Fix for epic authentication API changes --- src/data/sources/epicgames/EpicGames.vala | 1 + 1 file changed, 1 insertion(+) diff --git a/src/data/sources/epicgames/EpicGames.vala b/src/data/sources/epicgames/EpicGames.vala index cb290c63..53c8bdd8 100644 --- a/src/data/sources/epicgames/EpicGames.vala +++ b/src/data/sources/epicgames/EpicGames.vala @@ -481,6 +481,7 @@ namespace GameHub.Data.Sources.EpicGames "EpicGamesLauncher/11.0.1-14907503+++Portal+Release-Live " + "UnrealEngine/4.23.0-14907503+++Portal+Release-Live " + "Chrome/84.0.4147.38 Safari/537.36"); + cookies.append(new Soup.Cookie("EPIC_COUNTRY", EpicGames.instance.country_code.up(), "epicgames.com", "/", 0)); cookies_to_request(cookies, message); var status = session.send_message(message); debug("[Sources.EpicGames.LegendaryCore.with_sid] Status: %s", status.to_string()); From 23e6f47e6df4094c2a5d5f27f1cd3af024b64c9e Mon Sep 17 00:00:00 2001 From: Lucki Date: Sat, 30 Oct 2021 12:55:27 +0200 Subject: [PATCH 21/22] Config changed --- src/settings/Auth.vala | 2 +- src/settings/Paths.vala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/settings/Auth.vala b/src/settings/Auth.vala index 1b4e68bc..f75adeca 100644 --- a/src/settings/Auth.vala +++ b/src/settings/Auth.vala @@ -64,7 +64,7 @@ namespace GameHub.Settings.Auth public EpicGames() { - base(ProjectConfig.PROJECT_NAME + ".auth.epicgames"); + base(Config.RDNN + ".auth.epicgames"); } private static EpicGames? _instance; diff --git a/src/settings/Paths.vala b/src/settings/Paths.vala index ad2ca573..cb5499ca 100644 --- a/src/settings/Paths.vala +++ b/src/settings/Paths.vala @@ -53,7 +53,7 @@ namespace GameHub.Settings.Paths public EpicGames() { - base(ProjectConfig.PROJECT_NAME + ".paths.epicgames"); + base(Config.RDNN + ".paths.epicgames"); } private static EpicGames _instance; From 51431d7ba21b9745fd9245fe2d75ab82472000a7 Mon Sep 17 00:00:00 2001 From: Lucki Date: Sat, 30 Oct 2021 17:23:53 +0200 Subject: [PATCH 22/22] Make importing work again --- src/data/sources/epicgames/EpicAnalysis.vala | 2 ++ src/data/sources/epicgames/EpicGame.vala | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/src/data/sources/epicgames/EpicAnalysis.vala b/src/data/sources/epicgames/EpicAnalysis.vala index 790a3d12..05112e65 100644 --- a/src/data/sources/epicgames/EpicAnalysis.vala +++ b/src/data/sources/epicgames/EpicAnalysis.vala @@ -249,6 +249,7 @@ namespace GameHub.Data.Sources.EpicGames // TODO: do we care about this? var references = new HashMultiSet(); // FIXME: correct type to count? var file_manifest_list = new_manifest.file_manifest_list.elements; + if (log_analysis) debug(@"[Sources.EpicGames.AnalysisResult] Total file count: $(file_manifest_list.size)"); file_manifest_list.sort((a, b) => { if(a.filename.down() < b.filename.down()) return -1; @@ -365,6 +366,7 @@ namespace GameHub.Data.Sources.EpicGames // skip unchanged and empty files if(current_file.filename in manifest_comparison.unchanged) { + if(log_analysis) debug(@"Skipping because it hasn't changed: $(current_file.filename)"); continue; } else if(current_file.chunk_parts.size == 0) diff --git a/src/data/sources/epicgames/EpicGame.vala b/src/data/sources/epicgames/EpicGame.vala index 7e6224e1..4323ca6c 100644 --- a/src/data/sources/epicgames/EpicGame.vala +++ b/src/data/sources/epicgames/EpicGame.vala @@ -1218,6 +1218,12 @@ namespace GameHub.Data.Sources.EpicGames var tmp2_urls = base_urls; // copy list for manipulation var old_bytes = (version != null) ? get_installed_manifest() : null; + // FIXME: Hack for importing existing files + // Somewhere in the import process gets the latest_version written to version which + // screws later checks and results into downloading nothing because all files are already + // present but they aren't. + if (version != null && version == latest_version) old_bytes = null; + if(old_bytes == null) { debug("[Sources.EpicGames.prepare_download] Could not load old manifest, patching will not work!");