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/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/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/db/tables/Games.vala b/src/data/db/tables/Games.vala index a2a1cbc1..b7d64d43 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; @@ -154,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; @@ -339,6 +341,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 +430,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/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/Game.vala b/src/data/runnables/Game.vala index 21439d24..417866b9 100644 --- a/src/data/runnables/Game.vala +++ b/src/data/runnables/Game.vala @@ -44,8 +44,17 @@ 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 + * + * minutes = {@link GLib.TimeSpan} / 6e7 + */ public int64 playtime_tracked { get; set; default = 0; } public int64 playtime { get { return playtime_source + playtime_tracked; } } @@ -103,7 +112,7 @@ namespace GameHub.Data.Runnables // Version private string? _version = null; - public string? version + public virtual string? version { get { return _version; } set @@ -123,7 +132,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/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..1c9c1f76 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 @@ -223,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/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/EpicAnalysis.vala b/src/data/sources/epicgames/EpicAnalysis.vala new file mode 100644 index 00000000..05112e65 --- /dev/null +++ b/src/data/sources/epicgames/EpicAnalysis.vala @@ -0,0 +1,815 @@ +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); + + 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); + + 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 corrupted, 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; + 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; + + 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) + { + 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>(); + 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); + } + + 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"); + 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(var thing in existing_chunks.get(chunk_part.guid_num)) + { + // check if new chunk part is wholly contained in the old chunk part + 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( + key => { return key.hash(); }, + (a, b) => { return a.equal_to(b); })); + } + + 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; + + // 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) + { + if(log_analysis) debug(@"Skipping because it hasn't changed: $(current_file.filename)"); + continue; + } + else if(current_file.chunk_parts.size == 0) + { + var task_list = new ArrayList(); + task_list.add(new FileTask.empty_file(current_file.filename)); + tasks.add(task_list); + continue; + } + + var 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 + var key = new ChunkKey(chunk_part.guid_num, chunk_part.offset, chunk_part.size); + + if(existing_chunks != null && existing_chunks.has_key(key)) + { + 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); + } + 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) + { + 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 + 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 + task_list.add(new FileTask.rename(current_file.filename, + current_file.filename + ".tmp", + true)); + + tasks.add(task_list); + } + else + { + 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 + if(current_cache_size > last_cache_size) + { + if(log_analysis) debug(@"[Sources.EpicGames.AnalysisResult] New maximum cache size: $(current_cache_size / 1024 / 1024) MiB"); + + last_cache_size = current_cache_size; + } + } + + 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 + // 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) + { + var task_list = new ArrayList(); + task_list.add(new FileTask.delete(filename)); + tasks.add(task_list); + } + + tasks.add(additional_deletion_tasks); + + _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 + // so that the tasks order stays in the correct position + internal abstract class Task + { + internal abstract bool process(ref FileOutputStream? iostream, File install_dir, EpicGame game); + } + + /** + * 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 dele = false) + { + this(new_filename); + _frename = true; + _temporary_filename = old_filename; + _del = dele; + } + + 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 + { + 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 = full_path.replace(null, + false, + FileCreateFlags.REPLACE_DESTINATION); + } + else + { + iostream = full_path.create(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); + // 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") + // { + // 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 + */ + 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; + } + + 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()); + + 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 = 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(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)); + iostream.write_bytes(chunk.data[chunk_offset: chunk_offset + chunk_size]); + // debug(@"written $size bytes"); + + if(cleanup) + { + Utils.FS.rm(downloaded_chunk.get_path()); + } + } + else + { + assert_not_reached(); + } + } + catch (Error e) + { + debug("chunk task failed: %s", e.message); + + return false; + } + + return true; + } + } + } +} diff --git a/src/data/sources/epicgames/EpicChunk.vala b/src/data/sources/epicgames/EpicChunk.vala new file mode 100644 index 00000000..7e4707db --- /dev/null +++ b/src/data/sources/epicgames/EpicChunk.vala @@ -0,0 +1,186 @@ +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); + // } + + // // TODO: 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..13584356 --- /dev/null +++ b/src/data/sources/epicgames/EpicDownloader.vala @@ -0,0 +1,658 @@ +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: Downloader + { + private ArrayQueue dl_queue; + private HashTable dl_info; + private HashTable downloads; + private Session session = new Session(); + + internal static EpicDownloader instance; + + // 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.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); + instance = this; + } + + public override Download? get_download(string id) + { + lock (downloads) + { + return downloads.get(id); + } + } + + private EpicDownload? get_game_download(EpicGame game) + { + lock (downloads) + { + return (EpicDownload?) downloads.get(game.id); + } + } + + // TODO: a lot of small files, we should probably handle this in parallel + internal async bool download(Installer installer) + { + var game = installer.game; + var download = get_game_download(game); + + // installer.task.status = new InstallTask.Status(InstallTask.State.DOWNLOADING); + try + { + if(game == null || download != null) return yield await_download(download); + } + catch (Error e) + { + return false; + } + + download = new EpicDownload(game.id, installer.analysis); + game.status = new Game.Status(Game.State.DOWNLOADING, game, 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.id, info); + dl_started(info); + + if(GameHub.Application.log_downloader) + { + debug("[EpicDownloader] Installing '%s'...", game.id); + } + + // var ds_id = download_manager().file_download_started.connect(dl => { + // if(dl.id != game.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"); + // }); + // }); + + 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; + var total_parts = download.parts.size; + + EpicPart part; + download.session = session; + + 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))); + + 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()); + + if(part.remote == null || part.remote.get_uri() == null || part.remote.get_uri().length == 0) + { + current_part++; + continue; + } + + if(part.local.query_exists()) + { + // TODO: compare hash + if(GameHub.Application.log_downloader) + { + debug("[SoupDownloader] '%s' is already downloaded", part.remote.get_uri()); + } + + if(!yield installer.write_file(part.chunk_info.guid_num)) + { + throw new Error(0, 0, "Error"); + } + + current_part++; + continue; + } + + if(download.is_cancelled) + { + throw new IOError.CANCELLED("Download cancelled by user"); + } + + yield download_from_http(part, false, false); + + 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()) + { + // 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.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); + // } + } + + if(!yield installer.write_file(part.chunk_info.guid_num)) + { + throw new Error(0, 0, "Error"); + } + + 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); + + return false; + } + catch (Error error) + { + download.status = new FileDownload.Status(Download.State.FAILED); + download_failed(download, error); + + if(info != null) dl_ended(info); + + return false; + } + finally + { + // download_state = new DownloadState(DownloadState.State.DOWNLOADED); + 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); + } + + // download_manager().disconnect(ds_id); + + download_finished(download); + dl_ended(info); + + // game.update_status(); + + return true; + } + + private async bool await_download(EpicDownload download) throws Error + { + Error download_error = null; + + SourceFunc callback = await_download.callback; + var download_finished_id = download_finished.connect((downloader, downloaded) => { + if(((EpicDownload) downloaded).id != download.id) return; + + callback (); + }); + var download_cancelled_id = download_cancelled.connect((downloader, cancelled_download, error) => { + if(((EpicDownload) cancelled_download).id != download.id) return; + + download_error = error; + callback (); + }); + var download_failed_id = download_failed.connect((downloader, failed_download, error) => { + if(((EpicDownload) 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 true; + } + + private async void await_queue(EpicDownload download) + { + 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; + part.message = msg; + + // if(queue) + // { + // yield await_queue(download); + // download.status = new EpicDownload.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(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(); + + 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 = part.remote.get_basename(); + } + + if(filename != null && !(filename in FILENAME_BLACKLIST)) + { + part.local = part.local.get_parent().get_child(filename); + } + } + + if(part.local.query_exists()) + { + if(GameHub.Application.log_downloader) + { + debug(@"[SoupDownloader] '%s' exists", + part.local.get_path()); + } + + var info = part.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'", 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 = part.local_tmp.append_to(FileCreateFlags.NONE); + } + else + #endif + { + local_stream = part.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 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) {} + + public EpicPart.from_chunk_guid(string id, Analysis analysis, uint32 chunk_guid) + { + 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()); + } + } + + private class EpicDownload: Download, PausableDownload + { + public weak Session? session; + public weak Message? message; + public bool is_cancelled = false; + public ArrayQueue parts { get; default = new ArrayQueue(); } + + public EpicDownload(string id, Analysis analysis) + { + base(id); + + 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() + { + 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..4323ca6c --- /dev/null +++ b/src/data/sources/epicgames/EpicGame.vala @@ -0,0 +1,1662 @@ +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_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) => { + urls.add(node.get_string()); + }); + + return urls; + } + set + { + var urls = new Json.Node(Json.NodeType.ARRAY); + urls.set_array(new Json.Array()); + value.foreach(url => { + urls.get_array().add_string_element(url); + + return true; + }); + + 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; } + + 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 + // 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; + + 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; + } + 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? resume_file { get; default = null; } + 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) + { + // 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()); + } + 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 + { + return_val_if_fail(metadata.get_node_type() == Json.NodeType.OBJECT, false); + + return metadata.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? 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(metadata != null) this.metadata = metadata; + + _asset_info = asset; + load_version(); + name = this.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; + } + + if(game_info_updated) + { + game_info_updating = false; + + return; + } + + 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) + { + 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) + { + 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 != "") + { + store_page = @"https://www.epicgames.com/store/$(EpicGames.instance.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(); + + 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 = (this is DLC) ? get_file(@"$(FS.GAMEHUB_DIR)/$id.version") : get_file(@"$(FS.GAMEHUB_DIR)/version"); + + 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(); + // var action = new RunnableAction(this); + + // // if(!action.is_hidden) + // // { + // actions.add(action); + // // } + } + + 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: 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 + + last_launch = (new DateTime.now_utc()).to_unix(); + } + + 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? + + playtime_tracked += (new DateTime.now_utc()).difference(new DateTime.from_unix_utc(last_launch)) / 6000000; + save(); + } + + // 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() && status.state == Game.State.INSTALLED) + { + // yield umount_overlays(); + + // 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) + {} + + _manifest = null; // Forget cached manifest + 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.size == 0) + { + 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; + } + + 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()); + // 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()); + } + + parameters += "-EpicPortal"; + parameters += @"-epicusername=$(EpicGames.instance.user_name)"; + parameters += @"-epicuserid=$(EpicGames.instance.user_id)"; + parameters += @"-epiclocale=$(EpicGames.instance.language_code)"; + + 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; + + // 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!"); + } + 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) + { + 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, + out delta_manifest_data); + + 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 from build " + + @"$(old_manifest.meta.build_id) to $(new_manifest.meta.build_id)"); + new_manifest.combine_manifest(delta_manifest); + } + else + { + debug("[Sources.EpicGames.prepare_download] No Delta manifest received from CDN"); + } + } + + var force_update = true; // hardcoded for now + // var install_path = task.install_dir; + _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_update = false; + } + else if(force_update) + { + _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); + + // 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); + } + + public override async void install(InstallTask.Mode install_mode = InstallTask.Mode.INTERACTIVE) + { + 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); + 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() + // { + // 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 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 + // 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(); + } + + // 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) + { + 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); + yield task.start(); + } + } + + 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"; + // is_hidden = !game.has_updates; + // } + + // 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); } + // } + } +} diff --git a/src/data/sources/epicgames/EpicGames.vala b/src/data/sources/epicgames/EpicGames.vala new file mode 100644 index 00000000..53c8bdd8 --- /dev/null +++ b/src/data/sources/epicgames/EpicGames.vala @@ -0,0 +1,907 @@ +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_analysis = 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 = true; + 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 Settings.Auth.EpicGames settings; + + 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 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 + { + 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"); + } + } + + 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); + } + } + + /** + * 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; + settings = Settings.Auth.EpicGames.instance; + _userdata = Parser.parse_json(settings.userdata); + + // Session we're using to access the api + new EpicGamesServices(); + new EpicDownloader(); + } + + 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) + { + if(Application.log_auth) debug("[Sources.EpicGames.is_authenticated] Access token is less than 10 minutes valid."); + + return false; + } + + return access_token != null && access_token.length > 0; + } + + 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); + + if(game_loaded != null) + { + game_loaded(g, true); + } + } + + games_count++; + } + } + + if(cache_loaded != null) + { + cache_loaded(); + } + + var owned_games = get_game_and_dlc_list(true); + + 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); + + 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 + 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.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()); + 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 && access_token != null && access_token.length > 0) + { + var list = new ArrayList(); + 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); + var asset = new EpicGame.Asset.from_egs_json(node); + list.add(asset); + }); + + return list; + } + + if((update_assets || assets.is_empty) && access_token != null && access_token.length > 0) + { + 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); + var asset = new EpicGame.Asset.from_egs_json(node); + + if(!assets.contains(asset)) + { + assets.add(asset); + } + else + { + assets.set(assets.index_of(asset), asset); + } + + // Also update asset info in EpicGame because we rely on this being up-to-date + var game = get_game(asset.asset_id); + + if(game != null) game.asset_info = asset; + }); + + // trigger disk save + assets = assets; + } + + 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(string id, bool update_meta = false) + { + if(update_meta) + { + var owned_games = get_game_and_dlc_list(true); + + _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 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 >(); + + 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; + + 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))) + { + // 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("[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); + } + + assert(metadata.get_node_type() == Json.NodeType.OBJECT); + + // 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(); + } + + // 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; + // } + + // temporay save DLCs to list and assign later to main games + // so were surely have all main games loaded + if(game == null) + { + 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); + + var main_id = metadata.get_object().get_object_member("mainGameItem").get_string_member("id"); + 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(game.id, game); + } + + // TODO: mods? + } + + // we got all games, add the DLCs to it + foreach(var game_name in dlcs) + { + if(game_name.value == null) continue; + + 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); + } + } + + return owned_games; + } + + 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) + { + if(data == null) return null; + + // TODO: ugly json detection? + if(data[0] == '{') + { + // Try to fix that utf-8 failing below + // 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)) + // { + // 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) + { + debug("ASCII to UTF-8 failed!"); + + return null; + } + + return new Manifest.from_json(Parser.parse_json(json)); + } + + 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..a556177c --- /dev/null +++ b/src/data/sources/epicgames/EpicGamesServices.vala @@ -0,0 +1,556 @@ +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"; + + private const string store_host = "store-content.ak.epicgames.com"; + + // 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"; + + Json.Node? _productmapping = null; + Json.Node productmapping + { + get + { + if(_productmapping == null) update_store_productmapping(); + + return _productmapping; + } + } + + 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" + // } + } + + // 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) + 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", + "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() + requires(EpicGames.instance.access_token != null && EpicGames.instance.access_token.length > 0) + { + 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") + requires(EpicGames.instance.access_token != null && EpicGames.instance.access_token.length > 0) + { + 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") + requires(EpicGames.instance.access_token != null && EpicGames.instance.access_token.length > 0) + { + 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) + requires(EpicGames.instance.access_token != null && EpicGames.instance.access_token.length > 0) + { + Gee.HashMap data = new Gee.HashMap(); + + data.set("id", catalog_item_id); + data.set("includeDLCDetails", "True"); + data.set("includeMainGameDetails", "True"); + data.set("country", EpicGames.instance.country_code); + data.set("locale", EpicGames.instance.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 + &includeMainGameDetails=True + &country=$(EpicGames.instance.country_code) + &locale=$(EpicGames.instance.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 ArrayList get_library_items(bool include_metadata = true) + requires(EpicGames.instance.access_token != null && EpicGames.instance.access_token.length > 0) + { + 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)); + + 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) + requires(EpicGames.instance.access_token != null && EpicGames.instance.access_token.length > 0) + { + var app_name = game_id; + + if(app_name.length > 0 && manifests) + { + app_name += "/manifests/"; + } + else if(app_name.length > 0) + { + app_name += "/"; + } + + string method = "GET"; + HashMap data = null; + + if(filenames != null && filenames.length > 0) + { + method = "POST"; + data = new HashMap(); + data.set("files", filenames); + } + + uint status; + var 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 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 false; + + 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); + + data = new Bytes(message.response_body.data); + + return true; + } + + // 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 + */ + // 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; + + 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/$(EpicGames.instance.language_code)/content/products/$slug", + "GET", + null, + unauth_headers, + null, + out status); + // 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)); + + return json; + } + + // 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") + { + 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", 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); + 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); + + var json = Parser.parse_json((string) message.response_body.data); + 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)); + + 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/data/sources/epicgames/EpicInstaller.vala b/src/data/sources/epicgames/EpicInstaller.vala new file mode 100644 index 00000000..a44b482d --- /dev/null +++ b/src/data/sources/epicgames/EpicInstaller.vala @@ -0,0 +1,249 @@ +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 { get; default = null; } + 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; + this.platform = platform; + id = game.id; + name = game.name; + full_size = game.get_installation_size(platform); + 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) + { + _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"); + + debug("preparing download"); + _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) + { + debug("[Sources.EpicGames.EpicGame.download] Download size is 0, the game is either already up to date or has not changed."); + + if(game.needs_repair && game.repair_file.query_exists()) + { + if(game.needs_verification) game.needs_verification = false; + + // 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 + } + else + { + 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); + + 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(); + + 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) + { + _install_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 = 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(); + } + + 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 task_list in tasks) + { + 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) + { + 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); + } + } + + file_tasks.remove_all(current_file_tasks); + } + + if(current_file_tasks.is_empty) + { + debug("Nothing to do yet…"); + + return true; + } + + return_val_if_fail(yield write_files(current_file_tasks), false); + + return true; + } + } +} diff --git a/src/data/sources/epicgames/EpicManifest.vala b/src/data/sources/epicgames/EpicManifest.vala new file mode 100644 index 00000000..b1d1c988 --- /dev/null +++ b/src/data/sources/epicgames/EpicManifest.vala @@ -0,0 +1,1207 @@ +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 + { + _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); } + + // 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 + { + 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; set; 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; } + 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; } + + 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(); + 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 => + { + 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(); + return null; + } + + /** + * 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 = ""; + } + + internal void clear_matching_maps() + { + _guid_int_map.clear(); + _guid_str_map.clear(); + } + + /** + * 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(); + 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); + } + + stream.close(); + memory.close(); + } + catch (Error e) + { + debug("error: %s", e.message); + assert_not_reached(); + } + + _group_num = (ZLib.Utility.crc32(0, memory.steal_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: verify this 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 + length *= -2; + result = convert((string) stream.read_bytes(length), -1, "UTF-8", "UTF-16"); // convert to utf8 + } + else if(length > 0) + { + result = (string) stream.read_bytes(length).get_data(); + } + else + { + result = ""; // empty string + } + } + catch (Error e) + {} + + // 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; + + // 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; ?? + } + } + + /** + * 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) + { + // 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); + } + 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..c63502c2 --- /dev/null +++ b/src/data/sources/epicgames/EpicUtils.vala @@ -0,0 +1,154 @@ +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; + } + + 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..10d39bcf 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', @@ -166,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/settings/Auth.vala b/src/settings/Auth.vala index 9d31bcce..f75adeca 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(Config.RDNN + ".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..cb5499ca 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(Config.RDNN + ".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..ebf4bef8 --- /dev/null +++ b/src/ui/dialogs/SettingsDialog/pages/sources/EpicGames.vala @@ -0,0 +1,142 @@ +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 a 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/GameDetailsView/GameDetailsPage.vala b/src/ui/views/GameDetailsView/GameDetailsPage.vala index 7e9325d7..b9f009bf 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); @@ -289,13 +291,17 @@ 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; 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_properties.visible = !(game is GameHub.Data.Sources.GOG.GOGGame.DLC) && !(game is GameHub.Data.Sources.EpicGames.EpicGame.DLC); } public void update() @@ -356,7 +362,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) @@ -399,6 +406,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) 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")); 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"; 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 +#