Install games to persistent user://butler/games and reconcile orphaned caves
OGPU moves the whole extracted plugin directory (plugins/itch/) to trash on every plugin update, so keeping installed games under user://plugins/itch/games meant game files silently vanished on update while butler.db kept the (now orphaned) cave. That made fresh installs fail with butlerd's "That upload is already installed!" and broke launches for games whose files were gone. - New installs go to user://butler/games (persistent, next to butler.db) via a reworked _ensure_install_location; prereqs move there too - _install/_update reconcile the cave first: a cave whose install folder no longer exists is uninstalled before a fresh (re)install, and a valid existing cave turns the request into an update instead - _make_item treats missing-folder caves as not installed, and the cache path re-checks cwd so stale caches don't render a broken Play button - orphaned caves are cleaned out of butler.db in the background on library load - drop the plugin's global InstallLocation class stub: it collided with OGPU core's nested Library.InstallLocation and broke compilation on a cold .godot cache (the unused install_to arg is now untyped) - Makefile: compute preset number with shell arithmetic instead of bc
This commit is contained in:
parent
7d5dedc0b9
commit
3473073582
4 changed files with 119 additions and 25 deletions
|
|
@ -27,7 +27,7 @@ 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:
|
||||
func install_to(item: LibraryLaunchItem, _location = null, _options: Dictionary = {}) -> void:
|
||||
var game := (item.metadata.get("game", {}) as Dictionary)
|
||||
_active_item = item
|
||||
var success: bool = await itch.install(game)
|
||||
|
|
@ -113,6 +113,21 @@ func _filter_unsupported() -> bool:
|
|||
return settings_manager.get_value("plugin.itch", "filter_unsupported", true) as bool
|
||||
|
||||
|
||||
## A cave is only usable when its install folder actually exists on disk.
|
||||
## OGPU moves the whole extracted plugin directory (where older builds kept
|
||||
## installed games) to the trash on every plugin update, so butler.db can hold
|
||||
## "orphan" caves whose files are gone. Those must not show as installed: the
|
||||
## user can't launch them, and Install.Queue would reject a fresh install with
|
||||
## "That upload is already installed!".
|
||||
func _cave_has_files(cave: Dictionary) -> bool:
|
||||
if cave.is_empty():
|
||||
return false
|
||||
var install_folder: String = cave.get("installInfo", {}).get("installFolder", "")
|
||||
if install_folder == "":
|
||||
return false
|
||||
return DirAccess.dir_exists_absolute(install_folder)
|
||||
|
||||
|
||||
## Builds a LibraryLaunchItem for a game, or null when the entry isn't an
|
||||
## actual game (itch.io also hosts tools, assets, soundtracks, comics, ...)
|
||||
## or is filtered out by the "available on this platform" setting.
|
||||
|
|
@ -123,18 +138,19 @@ func _make_item(game: Dictionary, caves_by_game_id: Dictionary) -> Variant:
|
|||
return null
|
||||
var game_id: int = game.get("id", 0)
|
||||
var cave: Dictionary = caves_by_game_id.get(game_id, {})
|
||||
if not _game_available_on_current_platform(game, not cave.is_empty()):
|
||||
var cave_valid: bool = _cave_has_files(cave)
|
||||
if not _game_available_on_current_platform(game, cave_valid):
|
||||
return null
|
||||
|
||||
var item := LibraryLaunchItem.new()
|
||||
item.provider_app_id = cave.get("id", "")
|
||||
item.provider_app_id = cave.get("id", "") if cave_valid else ""
|
||||
item.name = game.get("title", "Unknown itch.io game")
|
||||
item.tags = ["itch"]
|
||||
item.categories = ["Game"]
|
||||
item.installed = not cave.is_empty()
|
||||
item.installed = cave_valid
|
||||
item.metadata = {"game": game}
|
||||
|
||||
if not cave.is_empty():
|
||||
if cave_valid:
|
||||
var install_folder: String = cave.get("installInfo", {}).get("installFolder", "")
|
||||
var exe := _find_executable(install_folder)
|
||||
item.command = exe
|
||||
|
|
@ -154,6 +170,14 @@ func _load_library(caching_flags: int = Cache.FLAGS.LOAD | Cache.FLAGS.SAVE) ->
|
|||
var items := [] as Array[LibraryLaunchItem]
|
||||
for i in json_items:
|
||||
var item := LibraryLaunchItem.from_dict(i)
|
||||
# A cached item can claim to be installed while its folder was
|
||||
# wiped by a plugin update (see _cave_has_files). Re-check the
|
||||
# folder so stale caches don't render a broken Play button.
|
||||
if item.installed and item.cwd != "" and not DirAccess.dir_exists_absolute(item.cwd):
|
||||
logger.info("Cached install '" + item.name + "' has no files on disk. Marking as not installed.")
|
||||
item.installed = false
|
||||
item.command = ""
|
||||
item.provider_app_id = ""
|
||||
var game: Dictionary = item.metadata.get("game", {})
|
||||
if not _game_available_on_current_platform(game, item.installed):
|
||||
continue
|
||||
|
|
@ -170,6 +194,18 @@ func _load_library(caching_flags: int = Cache.FLAGS.LOAD | Cache.FLAGS.SAVE) ->
|
|||
var caves: Array = await itch.get_caves()
|
||||
var collection_games: Array = await itch.get_collection_games()
|
||||
|
||||
# Clean up "orphan" caves (butler.db entries whose install folder was wiped
|
||||
# by a plugin update) in the background so a later Install.Queue doesn't
|
||||
# trip over them. The folder is already gone, so nothing is lost.
|
||||
var orphan_cave_ids := []
|
||||
for c in caves:
|
||||
var cave: Dictionary = c
|
||||
if not _cave_has_files(cave):
|
||||
orphan_cave_ids.append(cave.get("id", ""))
|
||||
if not orphan_cave_ids.is_empty():
|
||||
logger.info("Found " + str(orphan_cave_ids.size()) + " orphaned cave(s) with missing files. Cleaning up.")
|
||||
_cleanup_orphan_caves(orphan_cave_ids)
|
||||
|
||||
var caves_by_game_id := {}
|
||||
for c in caves:
|
||||
var cave: Dictionary = c
|
||||
|
|
@ -181,7 +217,7 @@ func _load_library(caching_flags: int = Cache.FLAGS.LOAD | Cache.FLAGS.SAVE) ->
|
|||
for o in owned:
|
||||
var owned_entry: Dictionary = o
|
||||
var game: Dictionary = owned_entry.get("game", {})
|
||||
var item := _make_item(game, caves_by_game_id)
|
||||
var item: Variant = _make_item(game, caves_by_game_id)
|
||||
if item == null:
|
||||
continue
|
||||
seen_game_ids[game.get("id", 0)] = true
|
||||
|
|
@ -194,7 +230,7 @@ func _load_library(caching_flags: int = Cache.FLAGS.LOAD | Cache.FLAGS.SAVE) ->
|
|||
var game_id: int = game.get("id", 0)
|
||||
if game_id in seen_game_ids:
|
||||
continue
|
||||
var item := _make_item(game, caves_by_game_id)
|
||||
var item: Variant = _make_item(game, caves_by_game_id)
|
||||
if item == null:
|
||||
continue
|
||||
seen_game_ids[game_id] = true
|
||||
|
|
@ -214,6 +250,14 @@ func _load_library(caching_flags: int = Cache.FLAGS.LOAD | Cache.FLAGS.SAVE) ->
|
|||
return items
|
||||
|
||||
|
||||
## Fire-and-forget removal of butler.db caves whose install folders no longer
|
||||
## exist. Runs sequentially (one butlerd uninstall at a time) so the shared
|
||||
## thread's one-shot queue never holds concurrent RPCs.
|
||||
func _cleanup_orphan_caves(cave_ids: Array) -> void:
|
||||
for cave_id in cave_ids:
|
||||
await itch.uninstall(cave_id)
|
||||
|
||||
|
||||
## Queues background downloads of each game's cover art into OGPU's local
|
||||
## boxart directory (user://boxart/local/). The built-in "local" BoxArtProvider
|
||||
## picks those files up by <game name>-<layout>.png, so games show their real
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue