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.svgsymbolic/sources/steam.svg
+ symbolic/sources/epicgames.svgsymbolic/sources/gog.svgsymbolic/sources/humble.svgsymbolic/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 @@
+
+
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
+#