diff --git a/src/data/sources/steam/Steam.vala b/src/data/sources/steam/Steam.vala
index ce3b43c7..51cfb433 100644
--- a/src/data/sources/steam/Steam.vala
+++ b/src/data/sources/steam/Steam.vala
@@ -19,6 +19,7 @@ along with GameHub. If not, see .
using Gee;
using GameHub.Data.DB;
using GameHub.Utils;
+using LevelDB;
using ZLib.Utility;
namespace GameHub.Data.Sources.Steam
@@ -476,14 +477,25 @@ namespace GameHub.Data.Sources.Steam
}
public static void add_game_shortcut(Game game)
+ {
+ if(is_running())
+ {
+ notify("[Sources.Steam.add_game_shortcut] No shortcut created because Steam is running, make sure Steam is closed properly.");
+ return;
+ }
+ set_shortcut(game);
+ set_shortcut_collection(game);
+ set_shortcut_assets(game);
+ }
+
+ // add or update shortcut
+ private static void set_shortcut(Game game)
{
var config_dir = FSUtils.find_case_insensitive(get_userdata_dir(), "config");
if(config_dir == null || !config_dir.query_exists()) return;
var shortcuts = FSUtils.find_case_insensitive(config_dir, "shortcuts.vdf") ?? FSUtils.file(config_dir.get_path(), "shortcuts.vdf");
-
var vdf = new BinaryVDF(shortcuts);
-
var root_node = vdf.read() as BinaryVDF.ListNode;
if(root_node.get("shortcuts") == null)
@@ -495,56 +507,266 @@ namespace GameHub.Data.Sources.Steam
root_node = root_node.get("shortcuts") as BinaryVDF.ListNode;
}
- var game_node = new BinaryVDF.ListNode.node(root_node.nodes.size.to_string());
+ BinaryVDF.ListNode? game_node = null;
+
+ foreach(BinaryVDF.Node node in root_node.nodes.values)
+ {
+ var tmp = node as BinaryVDF.ListNode;
+ var existing_node = tmp.get("LaunchOptions") as BinaryVDF.StringNode;
+ if(existing_node.value == "--run " + game.full_id)
+ {
+ game_node = tmp;
+ break;
+ }
+ }
+
+ if(game_node == null)
+ {
+ game_node = new BinaryVDF.ListNode.node(root_node.nodes.size.to_string());
+ }
- game_node.add_node(new BinaryVDF.StringNode.node("AppName", game.name));
+ game_node.add_node(new BinaryVDF.StringNode.node("appname", game.name));
game_node.add_node(new BinaryVDF.StringNode.node("exe", ProjectConfig.PROJECT_NAME));
game_node.add_node(new BinaryVDF.StringNode.node("LaunchOptions", "--run " + game.full_id));
game_node.add_node(new BinaryVDF.StringNode.node("ShortcutPath", ProjectConfig.DATADIR + "/applications/" + ProjectConfig.PROJECT_NAME + ".desktop"));
game_node.add_node(new BinaryVDF.StringNode.node("StartDir", "."));
- game_node.add_node(new BinaryVDF.IntNode.node("IsHidden", 0));
- game_node.add_node(new BinaryVDF.IntNode.node("OpenVR", 0));
- game_node.add_node(new BinaryVDF.IntNode.node("AllowOverlay", 1));
- game_node.add_node(new BinaryVDF.IntNode.node("AllowDesktopConfig", 1));
- game_node.add_node(new BinaryVDF.IntNode.node("LastPlayTime", 1));
- if(game.image != null)
+ if(game.icon != null)
+ {
+ var cached = ImageCache.local_file(game.icon, @"games/$(game.source.id)/$(game.id)/icons/");
+ game_node.add_node(new BinaryVDF.StringNode.node("icon", cached.get_path()));
+ }
+ else if(game.image != null)
{
var cached = ImageCache.local_file(game.image, @"games/$(game.source.id)/$(game.id)/images/");
game_node.add_node(new BinaryVDF.StringNode.node("icon", cached.get_path()));
}
+ // add missing tags
+ var tags_node = game_node.get("tags") as BinaryVDF.ListNode;
+ if(tags_node == null) tags_node = new BinaryVDF.ListNode.node("tags");
+ tags_node.add_node(new BinaryVDF.StringNode.node("0", "GameHub"));
+
+ foreach(var tag in game.tags)
+ {
+ var tag_exists = false;
+ foreach(var value in tags_node.nodes.values)
+ {
+ var tmp = value as BinaryVDF.StringNode;
+ if(tag.name == tmp.value)
+ {
+ tag_exists = true;
+ break;
+ }
+ }
+ if(!tag_exists)
+ {
+ tags_node.add_node(new BinaryVDF.StringNode.node(tags_node.nodes.size.to_string(), tag.name));
+ }
+ }
+
+ game_node.add_node(tags_node);
+ root_node.add_node(game_node);
+
+ root_node.show();
+
+ BinaryVDF.write(shortcuts, root_node);
+ }
+
+ // add or update artwork
+ private static void set_shortcut_assets(Game game)
+ {
+ var custom_appid = generate_new_appid(ProjectConfig.PROJECT_NAME, game.name);
+
+ if(game.image != null)
+ {
+ try
+ {
+ var cached = ImageCache.local_file(game.image, @"games/$(game.source.id)/$(game.id)/images/");
+ var dest = FSUtils.file(get_userdata_dir().get_child("config").get_child("grid").get_path(), custom_appid + ".png");
+ cached.copy(dest, FileCopyFlags.OVERWRITE);
+ }
+ catch (Error e) {}
+ }
+
if(game.image_vertical != null)
{
try
{
var cached = ImageCache.local_file(game.image_vertical, @"games/$(game.source.id)/$(game.id)/images/");
- // https://github.com/boppreh/steamgrid/blob/master/games.go#L120
- uint64 id = crc32(0, (ProjectConfig.PROJECT_NAME + game.name).data) | 0x80000000;
- var dest = FSUtils.file(get_userdata_dir().get_child("config").get_child("grid").get_path(), id.to_string() + "p.png");
- cached.copy(dest, NONE);
+ var dest = FSUtils.file(get_userdata_dir().get_child("config").get_child("grid").get_path(), custom_appid + "p.png");
+ cached.copy(dest, FileCopyFlags.OVERWRITE);
}
catch (Error e) {}
}
+ }
- var tags_node = new BinaryVDF.ListNode.node("tags");
- tags_node.add_node(new BinaryVDF.StringNode.node("0", "GameHub"));
+ // add or update collections
+ private static void set_shortcut_collection(Game game)
+ {
+ string? error;
+ var config_dir = FSUtils.find_case_insensitive(get_userdata_dir(), "config");
+ if(config_dir == null || !config_dir.query_exists()) return;
+
+ var collections_db = new SteamCollectionDatabase(instance.user_id, out error);
+ if(error != null) return;
+
+ collections_db.read(out error);
+ if(error != null) return;
+
+ var localconfig = Parser.parse_vdf_file(config_dir.get_path(), "localconfig.vdf");
+ if(localconfig == null || localconfig.get_node_type() != Json.NodeType.OBJECT) return;
+ var user_collections = Parser.parse_json(localconfig.get_object().get_object_member("UserLocalConfigStore").get_object_member("WebStorage").get_string_member("user-collections"));
+ if(user_collections == null || user_collections.get_node_type() != Json.NodeType.OBJECT) return;
+
+ int64? custom_appid_int;
+ int64.try_parse(generate_new_appid(ProjectConfig.PROJECT_NAME, game.name), out custom_appid_int);
+ if(custom_appid_int == null) return;
+
+ // Remove from collections where the game doesn't have the tag anymore
+ user_collections.get_object().foreach_member((object, name, node) =>
+ {
+ if(name.has_prefix("gh-") || name == "favorite" || name == "hidden")
+ {
+ foreach(var tag in game.tags)
+ {
+ string key;
+ if(tag.id == "builtin:favorites")
+ {
+ key = "favorite";
+ }
+ else if(tag.id == "builtin:hidden")
+ {
+ key = "hidden";
+ }
+ else
+ {
+ key = @"gh-$(tag.id)";
+ }
+
+ if(node.get_object().get_string_member("id") == key) return;
+ }
+
+ if(node.get_object().has_member("added") && node.get_object().get_member("added").get_node_type() == Json.NodeType.ARRAY)
+ {
+ uint? index = null;
+
+ node.get_object().get_array_member("added").foreach_element((a, i, n) =>
+ {
+ if(n.get_int() == custom_appid_int) index = i;
+ });
+
+ if(index != null)
+ {
+ node.get_object().get_array_member("added").remove_element(index);
+ return;
+ }
+ }
+ }
+ });
+
+ // add new tags
+ var created_collection = false;
foreach(var tag in game.tags)
{
- if(tag.removable)
+ string key;
+ if(tag.id == "builtin:favorites")
+ {
+ key = "favorite";
+ }
+ else if(tag.id == "builtin:hidden")
+ {
+ key = "hidden";
+ }
+ else
+ {
+ key = @"gh-$(tag.id)";
+ }
+
+ var collection = collections_db.get_collection(key);
+
+ // create categorie if it doesn't exist already
+ if(collection == null && key.has_prefix("gh-"))
+ {
+ collection = new Json.Object();
+ collection.set_string_member("id", key);
+ collection.set_string_member("name", tag.name);
+ collection.set_array_member("added", new Json.Array());
+ collection.set_array_member("removed", new Json.Array());
+ collections_db.set_collection(key, collection);
+ created_collection = true;
+ }
+
+ if(!user_collections.get_object().has_member(key))
+ {
+ var object = new Json.Object();
+ object.set_string_member("id", key);
+ object.set_array_member("added", new Json.Array());
+ object.set_array_member("removed", new Json.Array());
+ user_collections.get_object().set_object_member(key, object);
+ }
+
+ var already_present = false;
+ user_collections.get_object().get_object_member(key).get_array_member("added").foreach_element((array, index, node) =>
+ {
+ if(node.get_int() == custom_appid_int) already_present = true;
+ });
+ if(!already_present)
{
- tags_node.add_node(new BinaryVDF.StringNode.node((game.tags.index_of(tag) + 1).to_string(), tag.name));
+ user_collections.get_object().get_object_member(key).get_array_member("added").add_int_element(custom_appid_int);
}
}
- game_node.add_node(tags_node);
+ debug(@"[Sources.Steam.set_shortcut_collection]\n$(Json.to_string(user_collections, true))");
- root_node.add_node(game_node);
+ var generator = new Json.Generator();
+ generator.set_root(user_collections);
+ localconfig.get_object().get_object_member("UserLocalConfigStore").get_object_member("WebStorage").set_string_member("user-collections", generator.to_data(null));
+ FSUtils.write_string_to_file(FSUtils.file(config_dir.get_path(), "localconfig.vdf"), generate_vdf_from_json(localconfig.get_object()));
+ if(created_collection) collections_db.save(out error);
+ if(error != null) warning(@"[Sources.Steam.set_shortcut_collection] Error saving database: `%s`", error);
+ }
- root_node.show();
+ // https://github.com/node-steam/vdf/blob/master/src/index.ts#L78-L117
+ private static string generate_vdf_from_json(Json.Object object, int level = 0)
+ {
+ var seperator = " ";
+ var result = "";
+ var indent = "";
- BinaryVDF.write(shortcuts, root_node);
+ for(var i = 0; i < level; i++)
+ {
+ indent += seperator;
+ }
+
+ object.foreach_member((object, name, node) =>
+ {
+ if(node.get_node_type() == Json.NodeType.OBJECT && node.get_object() != null)
+ {
+ result += indent + "\"" + name + "\"\n" + indent + "{\n" + generate_vdf_from_json(node.get_object(), level + 1) + indent + "}\n";
+ }
+ else
+ {
+ var generator = new Json.Generator();
+ generator.set_root(node);
+ result += indent + "\"" + name + "\"" + seperator + seperator + generator.to_data(null) + "\n";
+ }
+ });
+
+ return result;
+ }
+
+ public static bool is_running()
+ {
+ if(Utils.run({"pidof", "steam"}).run_sync(true).exit_code == 0) return true;
+ return false;
+ }
+
+ public static string generate_new_appid(string exe, string name)
+ {
+ // https://github.com/boppreh/steamgrid/blob/master/games.go#L120
+ return (crc32(0, (exe + name).data) | 0x80000000).to_string();
}
public static bool IsAnyAppRunning = false;
diff --git a/src/data/sources/steam/SteamCollectionDatabase.vala b/src/data/sources/steam/SteamCollectionDatabase.vala
new file mode 100644
index 00000000..6b81877f
--- /dev/null
+++ b/src/data/sources/steam/SteamCollectionDatabase.vala
@@ -0,0 +1,340 @@
+using Gee;
+using GameHub.Utils;
+using LevelDB;
+
+namespace GameHub.Data.Sources.Steam
+{
+ private class SteamCollectionDatabase
+ {
+ private string db_path = FSUtils.Paths.Steam.Home + "/" + FSUtils.Paths.Steam.LevelDB;
+ private string steamid3;
+ private LevelDB.Database db;
+ private LevelDB.Options db_options = new LevelDB.Options();
+ private LevelDB.ReadOptions db_read_options = new LevelDB.ReadOptions();
+ private LevelDB.WriteOptions db_write_options = new LevelDB.WriteOptions();
+ private ByteArray key_prefix = new ByteArray();
+ private ByteArray namespaces_prefix = new ByteArray();
+ private HashMap namespace_collections = new HashMap();
+ private LinkedList namespaces = new LinkedList();
+
+ public SteamCollectionDatabase(string communityid, out string? error)
+ {
+ steamid3 = Steam.communityid_to_steamid3(uint64.parse(communityid)).to_string();
+
+ // "_https://steamloopback.host\x0\x1U$(steamid3)-cloud-storage-namespace"
+ key_prefix.append("_https://steamloopback.host".data);
+ key_prefix.append({ 0, 1 });
+ key_prefix.append(@"U$(steamid3)-cloud-storage-namespace".data);
+
+ // "_https://steamloopback.host\x0\x1U$(steamid3)-cloud-storage-namespaces"
+ namespaces_prefix = new ByteArray.take(key_prefix.data);
+ namespaces_prefix.append("s".data);
+
+ db_options.set_create_if_missing(false);
+
+ db = new LevelDB.Database(db_options, db_path, out error);
+ if(error != null) return;
+ }
+
+ // Initially read the database and convert values we care about into json
+ public void read(out string? error)
+ {
+ // 0x01[[1,"413"], ...]
+ var namespaces_raw_json = db.get(db_read_options, namespaces_prefix.data, out error);
+ if(error != null) return;
+
+ var namespaces_json = Parser.parse_json(((string) prepare_bytes(namespaces_raw_json).data));
+ if(namespaces_json == null || namespaces_json.get_node_type() != Json.NodeType.ARRAY) return;
+
+ namespaces_json.get_array().foreach_element((array, index, node) =>
+ {
+ // [1,"413"]
+ if(node == null || node.get_node_type() != Json.NodeType.ARRAY) return;
+ if(node.get_array().get_length() < 1) return;
+
+ // "_https://steamloopback.host\x0\x1U$(steamid3)-cloud-storage-namespace-1"
+ var namespace_key = new ByteArray.take(key_prefix.data);
+ namespace_key.append(@"-$(node.get_array().get_int_element(0))".data);
+ if(!namespaces.add(namespace_key)) return;
+ });
+
+ bool abort = false;
+ namespaces.foreach((namespace_key) =>
+ {
+ string? e;
+
+ var namespace_value = db.get(db_read_options, namespace_key.data, out e);
+ if(namespace_value == null || e != null)
+ {
+ warning(@"[Sources.Steam.SteamCollectionsDatabase.Read] Error reading namespace: `%s`: `%s`", (string) prepare_bytes(namespace_key.data).data, e);
+ return false;
+ }
+
+ // debug_bytes(namespace_value);
+ var namespace_json = unserialize_collections((string) prepare_bytes(namespace_value).data);
+ if(namespace_json == null)
+ {
+ abort = true;
+ return false;
+ }
+
+ namespace_collections.set(namespace_key, namespace_json);
+ return true;
+ });
+
+ if(abort)
+ {
+ error = "Error parsing json";
+ return;
+ }
+ }
+
+ // Example:
+ // [
+ // "user-collections.gh-gamehub",
+ // {
+ // "key": "user-collections.gh-gamehub",
+ // "timestamp": 1587322550,
+ // "value": {
+ // "id": "gh-gamehub",
+ // "name": "gamehub",
+ // "added": [
+ // ],
+ // "removed":[
+ // ]
+ // },
+ // "conflictResolutionMethod": "custom",
+ // "strMethodId": "union-collections"
+ // }
+ // ]
+ private Json.Array? @get(string id)
+ {
+ foreach(var collection_set in namespace_collections.values)
+ {
+ var collections = collection_set.get_array().get_elements();
+
+ foreach(var collection in collections)
+ {
+ if(collection.get_array().get_string_element(0) == id)
+ {
+ return collection.get_array();
+ }
+ }
+ }
+ return null;
+ }
+
+ // This returns a json object associated to an id which is included in the "value" member.
+ // Example:
+ // {
+ // "id" : "gh-gamehub",
+ // "name" : "gamehub",
+ // "added" : [
+ // ],
+ // "removed" : [
+ // ]
+ // }
+ public Json.Object? get_collection(string id)
+ {
+ var collection = @get(@"user-collections.$(id)");
+
+ if(collection != null && !collection.get_object_element(1).has_member("is_deleted") && collection.get_object_element(1).has_member("value") && collection.get_object_element(1).get_member("value").get_node_type() == Json.NodeType.OBJECT)
+ {
+ return collection.get_object_element(1).get_object_member("value");
+ }
+ return null;
+ }
+
+ public GLib.List get_collections_with_game(int64 appid)
+ {
+ var filtered_collections = new GLib.List();
+
+ foreach(var collection_set in namespace_collections.values)
+ {
+ var collections = collection_set.get_array().get_elements();
+ foreach(var collection in collections)
+ {
+ if(collection.get_array().get_element(1).get_node_type() == Json.NodeType.OBJECT)
+ {
+ if(collection.get_array().get_object_element(1).has_member("value") && collection.get_array().get_object_element(1).get_member("value").get_node_type() == Json.NodeType.OBJECT)
+ {
+ if(collection.get_array().get_object_element(1).get_object_member("value").has_member("added") && collection.get_array().get_object_element(1).get_object_member("value").get_member("added").get_node_type() == Json.NodeType.ARRAY)
+ {
+ collection.get_array().get_object_element(1).get_object_member("value").get_array_member("added").foreach_element((array, index, node) =>
+ {
+ if(node.get_int() == appid) filtered_collections.append(collection.get_array().get_object_element(1).get_object_member("value"));
+ });
+ }
+ }
+ }
+ }
+ }
+ return filtered_collections.copy();
+ }
+
+ // add or update a collection
+ public void set_collection(string id, Json.Object value)
+ {
+ Json.Object? object = null;
+ var array = @get(@"user-collections.$(id)");
+ if(array == null)
+ {
+ array = new Json.Array();
+ array.add_string_element(@"user-collections.$(id)");
+ }
+
+ if(array.get_length() > 1)
+ {
+ object = array.get_object_element(1);
+ }
+
+ if(object == null || object.has_member("is_deleted"))
+ {
+ object = new Json.Object();
+ object.set_string_member("key", @"user-collections.$(id)");
+ object.set_string_member("conflictResolutionMethod", "custom");
+ object.set_string_member("strMethodId", "union-collections");
+ array.add_object_element(object);
+ }
+
+ object.set_int_member("timestamp", new DateTime.now_utc().to_unix());
+ object.set_object_member("value", value);
+
+ // Update collection if already present
+ foreach(var collection_set in namespace_collections.values)
+ {
+ var collections = collection_set.get_array().get_elements();
+ foreach(var collection in collections)
+ {
+ if(collection.get_array().get_string_element(0) == @"user-collections.$(id)")
+ {
+ collection.get_array().remove_element(1);
+ collection.get_array().add_object_element(object);
+ return;
+ }
+ }
+ }
+
+ // collection is new, add it to the last namespace
+ var collections = namespace_collections.get(namespaces.last());
+ collections.get_array().add_array_element(array);
+ namespace_collections.set(namespaces.last(), collections);
+ }
+
+ // First byte is 0x01 which causes trouble converting into a string, strip it
+ // Last byte isn't always zero so make sure we can get a string by terminating with zero by ourself
+ private ByteArray prepare_bytes(uint8[] raw_bytes)
+ {
+ var bytes = new ByteArray.take(raw_bytes[1:raw_bytes.length]);
+ bytes.append({ 0 });
+ return bytes;
+ }
+
+ private Json.Node? unserialize_collections(string raw_iso)
+ {
+ string? raw_json;
+ try
+ {
+ // string has 'e4' for 'ä' so the character set can be Cp1252, ISO 8859-1 or ISO 8859-15
+ // according to some random website ISO 8859-1 is often used for html ¯\_(ツ)_/¯
+ raw_json = convert(raw_iso, -1, "UTF-8", "ISO 8859-1");
+ } catch (Error e) {return null;}
+
+ var root = Parser.parse_json(raw_json);
+ if(root == null || root.get_node_type() != Json.NodeType.ARRAY) return null;
+
+ root.get_array().foreach_element((array, index, node) =>
+ {
+ if(node == null || node.get_node_type() != Json.NodeType.ARRAY) return;
+
+ var object = node.get_array().get_object_element(1);
+ if(object == null) return;
+
+ if(object.has_member("value") && object.get_member("value").get_value_type() == Type.STRING)
+ {
+ if(Parser.parse_json((string) object.get_string_member("value")).get_node_type() == Json.NodeType.OBJECT)
+ {
+ object.set_member("value", Parser.parse_json((string) object.get_string_member("value")));
+ }
+ }
+ });
+
+ return root;
+ }
+
+ private string? serialize_collections(Json.Node root)
+ {
+ if(root.get_node_type() != Json.NodeType.ARRAY) return null;
+ string? raw_json;
+ var generator = new Json.Generator();
+
+ root.get_array().foreach_element((array, index, node) =>
+ {
+ if(node.get_node_type() != Json.NodeType.ARRAY) return;
+ if(node.get_array().get_object_element(1).has_member("value") && node.get_array().get_object_element(1).get_member("value").get_node_type() == Json.NodeType.OBJECT)
+ {
+ var object = node.get_array().get_object_element(1).get_object_member("value");
+ if(object == null) return;
+ var new_object = new Json.Node(Json.NodeType.OBJECT);
+ new_object.init_object(object);
+ generator.set_root(new_object);
+ node.get_array().get_object_element(1).remove_member("value");
+ node.get_array().get_object_element(1).set_string_member("value", generator.to_data(null));
+ }
+ });
+ generator.set_root(root);
+ raw_json = generator.to_data(null);
+
+ try
+ {
+ var raw_iso = convert(raw_json, -1, "ISO 8859-1", "UTF-8");
+
+ // We've stripped the first byte, add it back
+ return @"\x1$(raw_iso)";
+ } catch (Error e) {return null;}
+ }
+
+ public void save(out string? error)
+ {
+ var write_batch = new LevelDB.WriteBatch();
+
+ namespaces.foreach((k) =>
+ {
+ var raw_string = serialize_collections(namespace_collections.get(k)).data;
+ if(raw_string == null) return false;
+
+ debug(@"[Sources.Steam.SteamCollectionsDatabase.Save]\n$(serialize_collections(namespace_collections.get(k)), false)");
+ write_batch.put(k.data, raw_string);
+ return true;
+ });
+
+ try
+ {
+ if(!FSUtils.copy(FSUtils.file(db_path), FSUtils.file(db_path + "~"), FileCopyFlags.OVERWRITE))
+ {
+ error = "Failed creating backup";
+ return;
+ }
+ }
+ catch (Error e)
+ {
+ error = e.message;
+ return;
+ }
+
+ write_batch.write(db, db_write_options, out error);
+ if(error != null) return;
+ }
+
+ private void debug_bytes(int8[] bytes)
+ {
+ // View raw bytes for debugging purposes
+ string tmp = "";
+ for(int i = 0; i < bytes.length; i++)
+ {
+ tmp = tmp + " %2x".printf(bytes[i]);
+ }
+ debug(tmp);
+ }
+ }
+}
diff --git a/src/meson.build b/src/meson.build
index 2acdeac9..9f194025 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -22,6 +22,10 @@ project_config = vcs_tag(
fallback: ''
)
+# vala compiler flags
+vapi_dir = meson.current_source_dir() / 'vapi'
+add_project_arguments('--vapidir=' + vapi_dir, language: 'vala')
+
deps = [
dependency('gdk-3.0'),
dependency('json-glib-1.0'),
@@ -30,8 +34,10 @@ deps = [
dependency('libxml-2.0'),
dependency('gio-unix-2.0'),
dependency('zlib'),
+ dependency('leveldb'),
meson.get_compiler('vala').find_library('posix'),
- meson.get_compiler('vala').find_library('linux')
+ meson.get_compiler('vala').find_library('linux'),
+ meson.get_compiler('vala').find_library('leveldb', dirs: vapi_dir)
]
sources = [
@@ -44,6 +50,7 @@ sources = [
'data/sources/steam/Steam.vala',
'data/sources/steam/SteamGame.vala',
+ 'data/sources/steam/SteamCollectionDatabase.vala',
'data/sources/gog/GOG.vala',
'data/sources/gog/GOGGame.vala',
diff --git a/src/utils/FSUtils.vala b/src/utils/FSUtils.vala
index c90b72da..0d9d9a23 100644
--- a/src/utils/FSUtils.vala
+++ b/src/utils/FSUtils.vala
@@ -107,6 +107,8 @@ namespace GameHub.Utils
public const string AppInfoVDF = "steam/appcache/appinfo.vdf";
public const string PackageInfoVDF = "steam/appcache/packageinfo.vdf";
+
+ public const string LevelDB = "steam/config/htmlcache/Local Storage/leveldb";
}
public class GOG
@@ -525,5 +527,53 @@ namespace GameHub.Utils
}
#endif
}
+
+ public static void write_string_to_file(File? file, string? data)
+ {
+ if(file == null || data == null) return;
+
+ try
+ {
+ var stream = new DataOutputStream(file.replace(null, true, FileCreateFlags.NONE));
+ stream.set_byte_order(DataStreamByteOrder.LITTLE_ENDIAN);
+ stream.put_string(data);
+ stream.flush();
+ stream.close();
+ }
+ catch(Error e)
+ {
+ warning("[FSUtils.write_string_to_file] Error writing `%s`: %s", file.get_path(), e.message);
+ }
+ }
+
+ // https://stackoverflow.com/a/16454105
+ public static bool copy(File src, File dest, FileCopyFlags flags=FileCopyFlags.NONE, Cancellable? cancellable=null) throws GLib.Error
+ {
+ FileType src_type = src.query_file_type(FileQueryInfoFlags.NONE, cancellable);
+ if(src_type == FileType.DIRECTORY)
+ {
+ DirUtils.create_with_parents(dest.get_path(), 777);
+ src.copy_attributes(dest, flags, cancellable);
+
+ string src_path = src.get_path();
+ string dest_path = dest.get_path();
+ FileEnumerator enumerator = src.enumerate_children(FileAttribute.STANDARD_NAME, FileQueryInfoFlags.NONE, cancellable);
+ for(FileInfo? info = enumerator.next_file(cancellable); info != null; info = enumerator.next_file(cancellable))
+ {
+ // copy recursive
+ copy(
+ File.new_for_path(Path.build_filename (src_path, info.get_name ())),
+ File.new_for_path(Path.build_filename (dest_path, info.get_name ())),
+ flags,
+ cancellable);
+ }
+ }
+ else if(src_type == FileType.REGULAR)
+ {
+ src.copy(dest, flags, cancellable);
+ }
+
+ return true;
+ }
}
}
diff --git a/src/utils/Parser.vala b/src/utils/Parser.vala
index d40d692a..372be658 100644
--- a/src/utils/Parser.vala
+++ b/src/utils/Parser.vala
@@ -293,8 +293,8 @@ namespace GameHub.Utils
return buf.content();
}
- public delegate void JsonBulderDelegate(Json.Builder builder);
- public static Json.Node json(JsonBulderDelegate? d=null)
+ public delegate void JsonBuilderDelegate(Json.Builder builder);
+ public static Json.Node json(JsonBuilderDelegate? d=null)
{
var builder = new Json.Builder();
builder.begin_object();
diff --git a/src/vapi/leveldb.vapi b/src/vapi/leveldb.vapi
new file mode 100644
index 00000000..3bc5be7e
--- /dev/null
+++ b/src/vapi/leveldb.vapi
@@ -0,0 +1,163 @@
+/* LevelDB Vala Bindings
+ * Copyright 2012 Evan Nemerson
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use, copy,
+ * modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+[CCode (cheader_filename = "leveldb/c.h")]
+namespace LevelDB {
+ [Compact, CCode (cname = "leveldb_t", lower_case_cprefix = "leveldb_", free_function = "leveldb_close")]
+ public class Database {
+ [CCode (cname = "leveldb_open")]
+ public Database (LevelDB.Options options, string name, out string? err);
+
+ public LevelDB.Iterator create_iterator (LevelDB.ReadOptions options);
+ public LevelDB.Snapshot create_snapshot ();
+ public void delete (LevelDB.WriteOptions options, [CCode (array_length_type = "size_t")] uint8[] key, out string? err);
+ [CCode (cname = "leveldb_get", array_length_pos = 2.9, array_length_type = "size_t")]
+ public uint8[]? get (LevelDB.ReadOptions options, [CCode (array_length_type = "size_t")] uint8[] key, out string? err);
+ [CCode (cname = "leveldb_property_value")]
+ public string get_property_value (string propname);
+ public void put (LevelDB.WriteOptions options, [CCode (array_length_type = "size_t")] uint8[] key, [CCode (array_length_type = "size_t")] uint8[] val, out string? err);
+ public void write (LevelDB.WriteOptions options, LevelDB.WriteBatch batch, out string? err);
+
+ [CCode (cname = "leveldb_destroy_db")]
+ public static void destroy (LevelDB.Options options, string name, out string? err);
+ [CCode (cname = "leveldb_repair_db")]
+ public static void repair (LevelDB.Options options, string name, out string? err);
+ }
+
+ [Compact, CCode (cname = "leveldb_cache_t", free_function = "leveldb_cache_destroy")]
+ public class Cache {
+ private Cache ();
+ [CCode (cname = "leveldb_cache_create_lru")]
+ public Cache.lru (size_t capacity);
+ }
+
+ [CCode (instance_pos = 0.1)]
+ public delegate int CompareFunc ([CCode (array_length_type = "size_t")] uint8[] a, [CCode (array_length_type = "size_t")] uint8[] b);
+
+ [CCode (has_target = false)]
+ public delegate string NameFunc ();
+
+ [Compact, CCode (cname = "leveldb_comparator_t", free_function = "leveldb_comparator_destroy")]
+ public class Comparator {
+ [CCode (cname = "leveldb_comparator_create")]
+ public Comparator ([CCode (delegate_target_pos = 0.1, type = "int (*)(void*,const char*,size_t,const char*,size_t)")] owned LevelDB.CompareFunc compare, [CCode (type = "const char* (*)(void*)")] LevelDB.NameFunc name);
+ }
+
+ [CCode (cname = "int", has_type_id = false)]
+ public enum Compression {
+ [CCode (cname = "leveldb_no_compression")]
+ NONE,
+ [CCode (cname = "leveldb_snappy_compression")]
+ SNAPPY
+ }
+
+ [Compact, CCode (cname = "leveldb_env_t", free_function = "leveldb_env_destroy")]
+ public class Environment {
+ private Environment ();
+ [CCode (cname = "leveldb_create_default_env")]
+ public Environment.default ();
+ }
+
+ [Compact, CCode (cname = "leveldb_iterator_t", lower_case_cprefix = "leveldb_iter_", free_function = "leveldb_iter_destroy")]
+ public class Iterator {
+ public bool valid ();
+ public void seek_to_first ();
+ public void seek_to_last ();
+ public void seek ([CCode (type = "const char*", array_length_type = "size_t")] uint8[] k);
+ public void next ();
+ public void prev ();
+ [CCode (array_length_type = "size_t")]
+ public uint8[] key ();
+ [CCode (array_length_type = "size_t")]
+ public uint8[] value ();
+ public void get_error (out string? err);
+ }
+
+ [Compact, CCode (cname = "leveldb_logger_t")]
+ public class Logger {
+ private Logger ();
+ }
+
+ [Compact, CCode (cname = "leveldb_options_t", lower_case_cprefix = "leveldb_options_", free_function = "leveldb_options_destroy")]
+ public class Options {
+ [CCode (cname = "leveldb_options_create")]
+ public Options ();
+
+ public void set_comparator (LevelDB.Comparator comparator);
+ public void set_create_if_missing (bool compare_if_missing);
+ public void set_error_if_exists (bool error_if_exists);
+ public void set_paranoid_checks (bool paranoid_checks);
+ public void set_env (LevelDB.Environment env);
+ public void set_info_log (LevelDB.Logger info_log);
+ public void set_write_buffer (size_t write_buffer);
+ public void set_max_open_files (int max_open_files);
+ public void set_cache (LevelDB.Cache cache);
+ public void set_block_size (size_t block_size);
+ public void set_block_restart_interval (int block_restart_interval);
+ public void set_compression (LevelDB.Compression compression);
+ }
+
+ [Compact, CCode (cname = "leveldb_readoptions_t", lower_case_cprefix = "leveldb_readoptions_", free_function = "leveldb_readoptions_destroy")]
+ public class ReadOptions {
+ [CCode (cname = "leveldb_readoptions_create")]
+ public ReadOptions ();
+
+ public void set_fill_cache (bool fill_cache);
+ public void set_snapshot (LevelDB.Snapshot snapshot);
+ public void set_verify_checksums (bool verify_checksums);
+ }
+
+ [Compact, CCode (cname = "leveldb_snapshot_t", free_function = "leveldb_release_snapshot")]
+ public class Snapshot {
+ [CCode (cname = "leveldb_create_snapshot")]
+ public Snapshot (LevelDB.Database db);
+ }
+
+ [Compact, CCode (cname = "leveldb_writeoptions_t", lower_case_cprefix = "leveldb_writeoptions_", free_function = "leveldb_writeoptions_destroy")]
+ public class WriteOptions {
+ [CCode (cname = "leveldb_writeoptions_create")]
+ public WriteOptions ();
+
+ public void set_sync (bool sync);
+ }
+
+ [Compact, CCode (cname = "leveldb_writebatch_t", free_function = "leveldb_writebatch_destroy", lower_case_cprefix = "leveldb_writebatch_")]
+ public class WriteBatch {
+ [CCode (cname = "leveldb_writebatch_create")]
+ public WriteBatch ();
+
+ [CCode (has_target = false, simple_generics = true)]
+ public delegate void PutFunc (T state, [CCode (array_length_type = "size_t")] uint8[] key, [CCode (array_length_type = "size_t")] uint8[] val);
+ [CCode (has_target = false, simple_generics = true)]
+ public delegate void DeleteFunc (T state, [CCode (array_length_type = "size_t")] uint8[] key);
+
+ public void clear ();
+ public void delete ([CCode (array_length_type = "size_t")] uint8[] key);
+ [CCode (simple_generics = true)]
+ public void iterate (T state, LevelDB.WriteBatch.PutFunc put, LevelDB.WriteBatch.DeleteFunc delete);
+ public void put ([CCode (array_length_type = "size_t")] uint8[] key, [CCode (array_length_type = "size_t")] uint8[] val);
+ [CCode (cname = "leveldb_write", instance_pos = 2.5)]
+ public void write (LevelDB.Database db, LevelDB.WriteOptions options, out string? err);
+ }
+}