Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Further improve steam shortcut creation #368

Closed
wants to merge 10 commits into from
266 changes: 244 additions & 22 deletions src/data/sources/steam/Steam.vala
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ along with GameHub. If not, see <https://www.gnu.org/licenses/>.
using Gee;
using GameHub.Data.DB;
using GameHub.Utils;
using LevelDB;
using ZLib.Utility;

namespace GameHub.Data.Sources.Steam
Expand Down Expand Up @@ -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)
Expand All @@ -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));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to test if intentionally not setting "appid" here makes steam use the old format as "appid". (target+name…)

If it has that effect: good, we can predict the artwork name.
If not: new appid generation is currently unknown so we can't create shortcuts and set the artwork in one go.

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();
}
Comment on lines +766 to 770
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now replaced by a 4 byte little endian "appid" in the shortcut.vdf.

BigPicture is still using this tho. ((crc32(taget+name)|0x80000000)<<32|0x02000000)


public static bool IsAnyAppRunning = false;
Expand Down
Loading