Initial commit
This commit is contained in:
commit
3132a5015a
14 changed files with 1730 additions and 0 deletions
154
core/library_itch.gd
Normal file
154
core/library_itch.gd
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
extends Library
|
||||
|
||||
const ItchClient := preload("res://plugins/itch/core/itch_client.gd")
|
||||
const _apps_cache_file: String = "apps.json"
|
||||
|
||||
@onready var itch: ItchClient = get_tree().get_first_node_in_group("itch_client")
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
super()
|
||||
library_id = "itch"
|
||||
logger_name = "itch"
|
||||
logger = Log.get_logger(logger_name, log_level)
|
||||
logger.info("itch.io library loaded")
|
||||
itch.logged_in.connect(_on_logged_in)
|
||||
itch.install_progressed.connect(_on_install_progressed)
|
||||
|
||||
|
||||
func get_library_launch_items() -> Array[LibraryLaunchItem]:
|
||||
return await _load_library(Cache.FLAGS.LOAD | Cache.FLAGS.SAVE)
|
||||
|
||||
|
||||
func install_to(item: LibraryLaunchItem, _location: InstallLocation = null, _options: Dictionary = {}) -> void:
|
||||
var game := (item.metadata.get("game", {}) as Dictionary)
|
||||
itch.install(game)
|
||||
|
||||
|
||||
func update(item: LibraryLaunchItem) -> void:
|
||||
var game := (item.metadata.get("game", {}) as Dictionary)
|
||||
itch.install(game, item.provider_app_id)
|
||||
|
||||
|
||||
func uninstall(item: LibraryLaunchItem) -> void:
|
||||
itch.uninstall(item.provider_app_id)
|
||||
|
||||
|
||||
## itch.io's CheckUpdate call is async (and rate-limited), so we don't poll it
|
||||
## synchronously here. LibraryManager is expected to periodically call
|
||||
## get_library_launch_items() again, which re-fetches from butlerd's cache.
|
||||
func has_update(_item: LibraryLaunchItem) -> bool:
|
||||
return false
|
||||
|
||||
|
||||
func _on_logged_in(status: ItchClient.LOGIN_STATUS, _profile: Dictionary) -> void:
|
||||
if status != ItchClient.LOGIN_STATUS.OK:
|
||||
return
|
||||
logger.info("Logged in. Refreshing itch.io library.")
|
||||
var items: Array = await _load_library(Cache.FLAGS.SAVE)
|
||||
for i in items:
|
||||
var item: LibraryLaunchItem = i
|
||||
if not library_manager.has_app(item.name):
|
||||
logger.debug("App '" + item.name + "' was not loaded. Reloading library.")
|
||||
library_manager.reload_library()
|
||||
return
|
||||
|
||||
|
||||
## Re-emits itch.io's raw (id, current, total) install progress under the
|
||||
## base [Library] signal contract (item, percent_completed) so the UI's
|
||||
## generic install-progress widgets pick it up regardless of provider.
|
||||
## TODO: verify the exact LibraryManager lookup method/key for resolving a
|
||||
## provider_app_id back to its LibraryLaunchItem (this plugin was written
|
||||
## against the public Library/LibraryLaunchItem API, but LibraryManager's
|
||||
## internals weren't available while writing this) and wire the emit below
|
||||
## through it instead of dropping the notification.
|
||||
func _on_install_progressed(_id: String, _current: int, _total: int) -> void:
|
||||
pass
|
||||
|
||||
|
||||
## Builds the full itch.io library: owned games merged with install state
|
||||
## from Fetch.Caves. Uses the standard Cache system so we don't hammer
|
||||
## butlerd (and, transitively, the itch.io API) on every library refresh.
|
||||
func _load_library(caching_flags: int = Cache.FLAGS.LOAD | Cache.FLAGS.SAVE) -> Array[LibraryLaunchItem]:
|
||||
if caching_flags & Cache.FLAGS.LOAD and Cache.is_cached(_cache_dir, _apps_cache_file):
|
||||
var json_items = Cache.get_json(_cache_dir, _apps_cache_file)
|
||||
if json_items != null:
|
||||
logger.info("itch.io apps found in cache. Using cache.")
|
||||
var items := [] as Array[LibraryLaunchItem]
|
||||
for i in json_items:
|
||||
items.append(LibraryLaunchItem.from_dict(i))
|
||||
return items
|
||||
|
||||
if not itch.is_logged_in:
|
||||
logger.info("itch.io client is not logged in yet.")
|
||||
return []
|
||||
|
||||
logger.info("Fetching itch.io library...")
|
||||
var owned: Array = await itch.get_owned_games(false)
|
||||
var caves: Array = await itch.get_caves()
|
||||
|
||||
var caves_by_game_id := {}
|
||||
for c in caves:
|
||||
var cave: Dictionary = c
|
||||
var game_id: int = cave.get("game", {}).get("id", 0)
|
||||
caves_by_game_id[game_id] = cave
|
||||
|
||||
var items := [] as Array[LibraryLaunchItem]
|
||||
for o in owned:
|
||||
var owned_entry: Dictionary = o
|
||||
var game: Dictionary = owned_entry.get("game", {})
|
||||
if game.is_empty():
|
||||
continue
|
||||
# Only surface actual games (itch.io also hosts tools, assets,
|
||||
# soundtracks, comics, etc. under the same ownership API).
|
||||
if game.get("classification", "game") != "game":
|
||||
continue
|
||||
|
||||
var game_id: int = game.get("id", 0)
|
||||
var cave: Dictionary = caves_by_game_id.get(game_id, {})
|
||||
|
||||
var item := LibraryLaunchItem.new()
|
||||
item.provider_app_id = cave.get("id", "")
|
||||
item.name = game.get("title", "Unknown itch.io game")
|
||||
item.tags = ["itch"]
|
||||
item.categories = ["Game"]
|
||||
item.installed = not cave.is_empty()
|
||||
item.metadata = {"game": game}
|
||||
|
||||
if not cave.is_empty():
|
||||
var install_folder: String = cave.get("installInfo", {}).get("installFolder", "")
|
||||
var exe := _find_executable(install_folder)
|
||||
item.command = exe
|
||||
item.args = []
|
||||
item.cwd = install_folder
|
||||
|
||||
items.append(item)
|
||||
|
||||
if caching_flags & Cache.FLAGS.SAVE:
|
||||
logger.debug("Saving itch.io apps to cache.")
|
||||
var json_items := []
|
||||
for i in items:
|
||||
var item: LibraryLaunchItem = i
|
||||
json_items.append(item.to_dict())
|
||||
if Cache.save_json(_cache_dir, _apps_cache_file, json_items) != OK:
|
||||
logger.warn("Unable to save itch.io apps cache")
|
||||
|
||||
return items
|
||||
|
||||
|
||||
## Best-effort discovery of the game's launch executable inside its install
|
||||
## folder. butlerd doesn't hand back a ready-to-exec command the way a
|
||||
## "gog://" or "steam://" URI does, so we look at what's actually on disk.
|
||||
## TODO: parse .itch/receipt.json.gz for the authoritative launch target
|
||||
## and any declared manifest Actions instead of guessing.
|
||||
func _find_executable(install_folder: String) -> String:
|
||||
if install_folder == "":
|
||||
return ""
|
||||
var out := []
|
||||
OS.execute("bash", [
|
||||
"-c",
|
||||
"find " + install_folder.c_escape() + " -maxdepth 2 -type f -executable | head -n 1"
|
||||
], out)
|
||||
if out.is_empty():
|
||||
return ""
|
||||
return (out[0] as String).strip_edges()
|
||||
Loading…
Add table
Add a link
Reference in a new issue