Compare commits
4 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ee2691148d | |||
| 97c1519833 | |||
| 7ee08ac571 | |||
| 18f674c00b |
6 changed files with 104 additions and 78 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
<rect width="128" height="128" rx="16" fill="#fa5c5c"/>
|
<svg width="800px" height="800px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill="#ffffff" d="M64 100c-4-3-34-24-34-48a24 24 0 0 1 46-9 24 24 0 0 1 46 9c0 24-30 45-34 48a10 10 0 0 1-12 0 10 10 0 0 1-12 0z" transform="scale(0.72) translate(21,10)"/>
|
<circle cx="512" cy="512" r="512" style="fill:#fa5c5c"/>
|
||||||
</svg>
|
<path d="M322.7 284.6c-22.3 13.3-66.3 63.8-66.8 77.1v22c0 27.8 26 52.3 49.6 52.3 28.4 0 52-23.5 52-51.4 0 27.9 22.8 51.4 51.2 51.4 28.4 0 50.4-23.5 50.4-51.4 0 27.9 24.3 51.4 52.6 51.4h.5c28.4 0 52.6-23.5 52.6-51.4 0 27.9 22.1 51.4 50.4 51.4 28.4 0 51.2-23.5 51.2-51.4 0 27.9 23.6 51.4 52 51.4 23.6 0 49.6-24.4 49.6-52.3v-22c-.4-13.2-44.4-63.8-66.8-77.1-69.4-2.4-117.6-2.9-189.3-2.8s-169.4 1.1-189.2 2.8zm136.1 138.1c-2.8 4.9-6.1 9.1-10 12.8-10.7 10.5-25.4 16.9-41.5 16.9h-.1c-16.2 0-30.9-6.5-41.6-17-3.9-3.8-6.8-7.9-9.5-12.6-2.7 4.7-6.4 8.8-10.3 12.6-10.7 10.5-25.4 16.9-41.6 16.9h-.1c-1.9 0-4-.5-5.6-1.1-2.3 23.7-3.2 46.4-3.6 62.9v.1c0 8.4-.1 15.3-.1 24.9.4 49.8-4.9 161.4 22 188.8 41.7 9.7 118.3 14.1 195.2 14.2 76.9 0 153.6-4.5 195.2-14.2 26.9-27.4 21.5-139 22-188.8 0-9.6-.1-16.5-.1-24.9v-.1c-.3-16.5-1.3-39.2-3.6-62.9-1.6.6-3.7 1.1-5.6 1.1-16.2 0-30.9-6.5-41.6-17-3.9-3.8-7.6-7.9-10.3-12.6-2.7 4.7-5.7 8.8-9.5 12.6-10.7 10.5-25.4 17-41.6 17h-.1c-16.2 0-30.8-6.5-41.5-17-3.8-3.7-7.1-7.9-9.8-12.6l-.1-.3a57.5 57.5 0 0 1-9.9 12.8c-10.7 10.5-25.4 17-41.6 17h-3.6c-16.2 0-30.9-6.5-41.6-17-3.8-3.7-7.1-7.9-9.7-12.6l-.2.1zM416 478c17 .1 32 0 50.6 20.4 14.7-1.5 30-2.3 45.3-2.3 15.3 0 30.7.7 45.3 2.3C576 478 591 478.1 607.9 478c8 0 40 0 62.3 62.6l23.9 85.9c17.8 63.9-5.7 65.5-34.9 65.5-43.2-1.6-67.2-33-67.2-64.5-24 3.9-52 5.9-80 5.9s-56-2-80-5.9c0 31.5-24 62.9-67.3 64.6-29.2-.1-52.7-1.6-34.9-65.5l24-85.9C376 478 408 478 416 478zm96 49.2c0 .1-45.6 41.9-53.8 56.8l29.8-1.2v26c0 1.2 12 .7 24 .2 12 .6 24 1 24-.2v-26l29.8 1.2c-8.2-14.8-53.8-56.7-53.8-56.8z" style="fill:#fff"/>
|
||||||
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 309 B After Width: | Height: | Size: 1.8 KiB |
|
|
@ -15,11 +15,11 @@ extends NodeThread
|
||||||
## https://pkg.go.dev/github.com/itchio/butler/butlerd). If itch.io ships a
|
## https://pkg.go.dev/github.com/itchio/butler/butlerd). If itch.io ships a
|
||||||
## butler update that changes a field, diff against that spec.
|
## butler update that changes a field, diff against that spec.
|
||||||
|
|
||||||
const broth_base := "https://broth.itch.zone/butler"
|
const BROTH_BASE := "https://broth.itch.zone/butler"
|
||||||
## Persistent storage for the downloaded butler binary + butler.db. Kept OUT
|
## Persistent storage for the downloaded butler binary + butler.db. Kept OUT
|
||||||
## of user://plugins/<id>/ because OGPU wipes that directory (and everything
|
## of user://plugins/<id>/ because OGPU wipes that directory (and everything
|
||||||
## inside it) whenever the plugin is updated or re-extracted.
|
## inside it) whenever the plugin is updated or re-extracted.
|
||||||
const butler_dir := "user://butler"
|
const BUTLER_DIR := "user://butler"
|
||||||
## Where installed games live. This is intentionally NOT user://plugins/itch:
|
## Where installed games live. This is intentionally NOT user://plugins/itch:
|
||||||
## OGPU moves the entire extracted plugin directory (plugins/<id>/) to the
|
## OGPU moves the entire extracted plugin directory (plugins/<id>/) to the
|
||||||
## trash on every plugin update, so any install kept under it silently loses
|
## trash on every plugin update, so any install kept under it silently loses
|
||||||
|
|
@ -27,8 +27,11 @@ const butler_dir := "user://butler"
|
||||||
## used to break both fresh installs ("That upload is already installed!")
|
## used to break both fresh installs ("That upload is already installed!")
|
||||||
## and launches. Keeping games next to butler.db (user://butler) means they
|
## and launches. Keeping games next to butler.db (user://butler) means they
|
||||||
## survive plugin updates untouched.
|
## survive plugin updates untouched.
|
||||||
const games_dir := "user://butler/games"
|
const GAMES_DIR := "user://butler/games"
|
||||||
const CACHE_DIR := "itch"
|
const CACHE_NAMESPACE := "itch"
|
||||||
|
const CONNECTION_TIMEOUT_ITERATIONS := 50
|
||||||
|
const CONNECTION_POLL_DELAY_MS := 20
|
||||||
|
const PERCENTAGE_SCALE := 100
|
||||||
|
|
||||||
enum STATE {
|
enum STATE {
|
||||||
BOOT,
|
BOOT,
|
||||||
|
|
@ -57,9 +60,9 @@ signal app_updated(cave_id: String, success: bool)
|
||||||
signal app_uninstalled(cave_id: String, success: bool)
|
signal app_uninstalled(cave_id: String, success: bool)
|
||||||
signal launch_exited(cave_id: String)
|
signal launch_exited(cave_id: String)
|
||||||
|
|
||||||
var proc: InteractiveProcess
|
var butler_process: InteractiveProcess
|
||||||
var socket: StreamPeerTCP
|
var rpc_socket: StreamPeerTCP
|
||||||
var state: STATE = STATE.BOOT
|
var connection_state: STATE = STATE.BOOT
|
||||||
var client_started := false
|
var client_started := false
|
||||||
var is_logged_in := false
|
var is_logged_in := false
|
||||||
var profile: Dictionary = {}
|
var profile: Dictionary = {}
|
||||||
|
|
@ -67,7 +70,7 @@ var profile: Dictionary = {}
|
||||||
var _proc_buffer := ""
|
var _proc_buffer := ""
|
||||||
var _recv_buffer := ""
|
var _recv_buffer := ""
|
||||||
var _next_id := 1
|
var _next_id := 1
|
||||||
var _install_location_id := ""
|
var _cached_install_location_id := ""
|
||||||
|
|
||||||
var logger := Log.get_logger("ItchClient", Log.LEVEL.INFO)
|
var logger := Log.get_logger("ItchClient", Log.LEVEL.INFO)
|
||||||
|
|
||||||
|
|
@ -81,7 +84,7 @@ func _ready() -> void:
|
||||||
|
|
||||||
## Bootstraps the butler binary if it isn't present, then spawns butlerd.
|
## Bootstraps the butler binary if it isn't present, then spawns butlerd.
|
||||||
func bootstrap() -> void:
|
func bootstrap() -> void:
|
||||||
var butler_dir_global := ProjectSettings.globalize_path(butler_dir)
|
var butler_dir_global := ProjectSettings.globalize_path(BUTLER_DIR)
|
||||||
_migrate_butler(butler_dir_global)
|
_migrate_butler(butler_dir_global)
|
||||||
var butler_bin := "/".join([butler_dir_global, "butler"])
|
var butler_bin := "/".join([butler_dir_global, "butler"])
|
||||||
if not FileAccess.file_exists(butler_bin):
|
if not FileAccess.file_exists(butler_bin):
|
||||||
|
|
@ -101,16 +104,16 @@ func bootstrap() -> void:
|
||||||
"--keep-alive",
|
"--keep-alive",
|
||||||
"--dbpath", dbpath,
|
"--dbpath", dbpath,
|
||||||
"--address", "https://itch.io",
|
"--address", "https://itch.io",
|
||||||
"--user-agent", "OpenGamepadUI-itch/0.1.0",
|
"--user-agent", _get_user_agent(),
|
||||||
"--destiny-pid", str(OS.get_process_id()),
|
"--destiny-pid", str(OS.get_process_id()),
|
||||||
]
|
]
|
||||||
|
|
||||||
proc = InteractiveProcess.new(butler_bin, args)
|
butler_process = InteractiveProcess.new(butler_bin, args)
|
||||||
if proc.start() != OK:
|
if butler_process.start() != OK:
|
||||||
logger.error("Unable to spawn butlerd")
|
logger.error("Unable to spawn butlerd")
|
||||||
return
|
return
|
||||||
client_started = true
|
client_started = true
|
||||||
state = STATE.WAITING_HANDSHAKE
|
connection_state = STATE.WAITING_HANDSHAKE
|
||||||
bootstrap_finished.emit()
|
bootstrap_finished.emit()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -138,22 +141,22 @@ func _migrate_butler(dest_dir: String) -> void:
|
||||||
## Downloads a butler binary for this platform from itch's broth distribution
|
## Downloads a butler binary for this platform from itch's broth distribution
|
||||||
## channel (the same one the official itch.io app uses to self-update).
|
## channel (the same one the official itch.io app uses to self-update).
|
||||||
func _install_butler() -> bool:
|
func _install_butler() -> bool:
|
||||||
var goos := "linux"
|
var os_name := "linux"
|
||||||
var goarch := "amd64"
|
var arch_name := "amd64"
|
||||||
if OS.get_name() == "Windows":
|
if OS.get_name() == "Windows":
|
||||||
goos = "windows"
|
os_name = "windows"
|
||||||
if OS.get_name() == "macOS":
|
if OS.get_name() == "macOS":
|
||||||
goos = "darwin"
|
os_name = "darwin"
|
||||||
# Engine.get_architecture_name() is only available on newer Godot 4.x
|
# Engine.get_architecture_name() is only available on newer Godot 4.x
|
||||||
# builds; guard it so this still works if OpenGamepadUI is running on an
|
# builds; guard it so this still works if OpenGamepadUI is running on an
|
||||||
# older engine version.
|
# older engine version.
|
||||||
if Engine.has_method("get_architecture_name"):
|
if Engine.has_method("get_architecture_name"):
|
||||||
var arch: String = Engine.get_architecture_name()
|
var arch: String = Engine.get_architecture_name()
|
||||||
if "arm64" in arch or "aarch64" in arch:
|
if "arm64" in arch or "aarch64" in arch:
|
||||||
goarch = "arm64"
|
arch_name = "arm64"
|
||||||
|
|
||||||
var platform_slug := goos + "-" + goarch
|
var platform_slug := os_name + "-" + arch_name
|
||||||
var latest_url := "/".join([broth_base, platform_slug, "LATEST"])
|
var latest_url := "/".join([BROTH_BASE, platform_slug, "LATEST"])
|
||||||
|
|
||||||
var http := HTTPRequest.new()
|
var http := HTTPRequest.new()
|
||||||
add_child.call_deferred(http)
|
add_child.call_deferred(http)
|
||||||
|
|
@ -174,7 +177,7 @@ func _install_butler() -> bool:
|
||||||
return false
|
return false
|
||||||
var version := body.get_string_from_utf8().strip_edges()
|
var version := body.get_string_from_utf8().strip_edges()
|
||||||
|
|
||||||
var archive_url := "/".join([broth_base, platform_slug, version, "archive", "default"])
|
var archive_url := "/".join([BROTH_BASE, platform_slug, version, "archive", "default"])
|
||||||
if http.request(archive_url) != OK:
|
if http.request(archive_url) != OK:
|
||||||
logger.error("Error downloading butler: " + archive_url)
|
logger.error("Error downloading butler: " + archive_url)
|
||||||
remove_child(http)
|
remove_child(http)
|
||||||
|
|
@ -190,7 +193,7 @@ func _install_butler() -> bool:
|
||||||
logger.error("butler couldn't be downloaded: " + archive_url)
|
logger.error("butler couldn't be downloaded: " + archive_url)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
var globalized_dir := ProjectSettings.globalize_path(butler_dir)
|
var globalized_dir := ProjectSettings.globalize_path(BUTLER_DIR)
|
||||||
DirAccess.make_dir_recursive_absolute(globalized_dir)
|
DirAccess.make_dir_recursive_absolute(globalized_dir)
|
||||||
var zip_path := "/tmp/butler-" + version + ".zip"
|
var zip_path := "/tmp/butler-" + version + ".zip"
|
||||||
var file := FileAccess.open(zip_path, FileAccess.WRITE_READ)
|
var file := FileAccess.open(zip_path, FileAccess.WRITE_READ)
|
||||||
|
|
@ -204,13 +207,24 @@ func _install_butler() -> bool:
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _get_user_agent() -> String:
|
||||||
|
var version := "unknown"
|
||||||
|
var file := FileAccess.open("res://plugin.json", FileAccess.READ)
|
||||||
|
if file:
|
||||||
|
var json: Variant = JSON.parse_string(file.get_as_text())
|
||||||
|
file.close()
|
||||||
|
if json is Dictionary:
|
||||||
|
version = json.get("plugin.version", "unknown")
|
||||||
|
return "OpenGamepadUI-itch/" + version
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Wire protocol: spawn -> read handshake off stdout -> connect TCP -> auth
|
# Wire protocol: spawn -> read handshake off stdout -> connect TCP -> auth
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func _thread_process(_delta: float) -> void:
|
func _thread_process(_delta: float) -> void:
|
||||||
if state == STATE.WAITING_HANDSHAKE and proc:
|
if connection_state == STATE.WAITING_HANDSHAKE and butler_process:
|
||||||
_proc_buffer += proc.read()
|
_proc_buffer += butler_process.read()
|
||||||
if not _proc_buffer.contains("\n"):
|
if not _proc_buffer.contains("\n"):
|
||||||
return
|
return
|
||||||
var lines := _proc_buffer.split("\n")
|
var lines := _proc_buffer.split("\n")
|
||||||
|
|
@ -230,12 +244,12 @@ func _thread_process(_delta: float) -> void:
|
||||||
_connect_and_authenticate.call_deferred(address, secret)
|
_connect_and_authenticate.call_deferred(address, secret)
|
||||||
return
|
return
|
||||||
|
|
||||||
if state == STATE.CONNECTED and socket:
|
if connection_state == STATE.CONNECTED and rpc_socket:
|
||||||
socket.poll()
|
rpc_socket.poll()
|
||||||
var available := socket.get_available_bytes()
|
var available := rpc_socket.get_available_bytes()
|
||||||
if available <= 0:
|
if available <= 0:
|
||||||
return
|
return
|
||||||
var chunk := socket.get_partial_data(available)
|
var chunk := rpc_socket.get_partial_data(available)
|
||||||
if chunk[0] != OK:
|
if chunk[0] != OK:
|
||||||
return
|
return
|
||||||
_recv_buffer += (chunk[1] as PackedByteArray).get_string_from_utf8()
|
_recv_buffer += (chunk[1] as PackedByteArray).get_string_from_utf8()
|
||||||
|
|
@ -260,22 +274,22 @@ func _connect_and_authenticate(address: String, secret: String) -> void:
|
||||||
var host: String = parts[0]
|
var host: String = parts[0]
|
||||||
var port: int = int(parts[1])
|
var port: int = int(parts[1])
|
||||||
|
|
||||||
socket = StreamPeerTCP.new()
|
rpc_socket = StreamPeerTCP.new()
|
||||||
if socket.connect_to_host(host, port) != OK:
|
if rpc_socket.connect_to_host(host, port) != OK:
|
||||||
logger.error("Unable to connect to butlerd at " + address)
|
logger.error("Unable to connect to butlerd at " + address)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Wait for the connection to establish
|
# Wait for the connection to establish
|
||||||
var timeout := 50
|
var timeout := CONNECTION_TIMEOUT_ITERATIONS
|
||||||
while socket.get_status() == StreamPeerTCP.STATUS_CONNECTING and timeout > 0:
|
while rpc_socket.get_status() == StreamPeerTCP.STATUS_CONNECTING and timeout > 0:
|
||||||
socket.poll()
|
rpc_socket.poll()
|
||||||
OS.delay_msec(20)
|
OS.delay_msec(CONNECTION_POLL_DELAY_MS)
|
||||||
timeout -= 1
|
timeout -= 1
|
||||||
if socket.get_status() != StreamPeerTCP.STATUS_CONNECTED:
|
if rpc_socket.get_status() != StreamPeerTCP.STATUS_CONNECTED:
|
||||||
logger.error("Timed out connecting to butlerd")
|
logger.error("Timed out connecting to butlerd")
|
||||||
return
|
return
|
||||||
|
|
||||||
state = STATE.CONNECTED
|
connection_state = STATE.CONNECTED
|
||||||
|
|
||||||
var res := await _rpc_call("Meta.Authenticate", {"secret": secret})
|
var res := await _rpc_call("Meta.Authenticate", {"secret": secret})
|
||||||
if "error" in res:
|
if "error" in res:
|
||||||
|
|
@ -291,9 +305,9 @@ func _connect_and_authenticate(address: String, secret: String) -> void:
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func _send_line(text: String) -> void:
|
func _send_line(text: String) -> void:
|
||||||
if not socket:
|
if not rpc_socket:
|
||||||
return
|
return
|
||||||
socket.put_data((text + "\n").to_utf8_buffer())
|
rpc_socket.put_data((text + "\n").to_utf8_buffer())
|
||||||
|
|
||||||
|
|
||||||
## Godot's JSON.parse decodes every JSON number as a float (so 7670 becomes
|
## Godot's JSON.parse decodes every JSON number as a float (so 7670 becomes
|
||||||
|
|
@ -575,17 +589,17 @@ func _get_caves() -> Variant:
|
||||||
## every plugin update; existing locations pointing there are left alone (their
|
## every plugin update; existing locations pointing there are left alone (their
|
||||||
## orphaned caves are cleaned up by [method _resolve_cave]) but never reused.
|
## orphaned caves are cleaned up by [method _resolve_cave]) but never reused.
|
||||||
func _ensure_install_location() -> String:
|
func _ensure_install_location() -> String:
|
||||||
if _install_location_id != "":
|
if _cached_install_location_id != "":
|
||||||
return _install_location_id
|
return _cached_install_location_id
|
||||||
|
|
||||||
var target_path := ProjectSettings.globalize_path(games_dir)
|
var target_path := ProjectSettings.globalize_path(GAMES_DIR)
|
||||||
var res := await _rpc_call("Install.Locations.List", {})
|
var res := await _rpc_call("Install.Locations.List", {})
|
||||||
var locations: Array = res.get("installLocations", [])
|
var locations: Array = res.get("installLocations", [])
|
||||||
for loc in locations:
|
for loc in locations:
|
||||||
var location: Dictionary = loc
|
var location: Dictionary = loc
|
||||||
if location.get("path", "") == target_path:
|
if location.get("path", "") == target_path:
|
||||||
_install_location_id = location.get("id", "")
|
_cached_install_location_id = location.get("id", "")
|
||||||
return _install_location_id
|
return _cached_install_location_id
|
||||||
|
|
||||||
DirAccess.make_dir_recursive_absolute(target_path)
|
DirAccess.make_dir_recursive_absolute(target_path)
|
||||||
# Let butlerd generate the id so we never collide with a stale location
|
# Let butlerd generate the id so we never collide with a stale location
|
||||||
|
|
@ -594,8 +608,8 @@ func _ensure_install_location() -> String:
|
||||||
if "error" in add_res:
|
if "error" in add_res:
|
||||||
logger.error("Install.Locations.Add failed: " + str(add_res["error"]))
|
logger.error("Install.Locations.Add failed: " + str(add_res["error"]))
|
||||||
return ""
|
return ""
|
||||||
_install_location_id = add_res.get("installLocation", {}).get("id", "")
|
_cached_install_location_id = add_res.get("installLocation", {}).get("id", "")
|
||||||
return _install_location_id
|
return _cached_install_location_id
|
||||||
|
|
||||||
|
|
||||||
## Returns every configured install location, each as:
|
## Returns every configured install location, each as:
|
||||||
|
|
@ -615,7 +629,7 @@ func _get_install_locations() -> Array:
|
||||||
logger.warn("Install.Locations.List failed: " + str(res["error"]))
|
logger.warn("Install.Locations.List failed: " + str(res["error"]))
|
||||||
return []
|
return []
|
||||||
var locations: Array = res.get("installLocations", [])
|
var locations: Array = res.get("installLocations", [])
|
||||||
var preferred_path := ProjectSettings.globalize_path(games_dir)
|
var preferred_path := ProjectSettings.globalize_path(GAMES_DIR)
|
||||||
var plugin_dir := ProjectSettings.globalize_path("user://plugins/itch")
|
var plugin_dir := ProjectSettings.globalize_path("user://plugins/itch")
|
||||||
var preferred: Array = []
|
var preferred: Array = []
|
||||||
var others: Array = []
|
var others: Array = []
|
||||||
|
|
@ -790,8 +804,8 @@ func _install(game: Dictionary, cave_id: String, options: Dictionary) -> bool:
|
||||||
var progress: float = p.get("progress", 0.0)
|
var progress: float = p.get("progress", 0.0)
|
||||||
install_progressed.emit.call_deferred(
|
install_progressed.emit.call_deferred(
|
||||||
cave_id if cave_id != "" else str(game_id),
|
cave_id if cave_id != "" else str(game_id),
|
||||||
int(progress * 100),
|
int(progress * PERCENTAGE_SCALE),
|
||||||
100
|
PERCENTAGE_SCALE
|
||||||
)
|
)
|
||||||
rpc_notification.connect(on_notification)
|
rpc_notification.connect(on_notification)
|
||||||
|
|
||||||
|
|
@ -984,8 +998,8 @@ func _logout() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _exit_tree() -> void:
|
func _exit_tree() -> void:
|
||||||
if socket:
|
if rpc_socket:
|
||||||
socket.disconnect_from_host()
|
rpc_socket.disconnect_from_host()
|
||||||
if not proc:
|
if not butler_process:
|
||||||
return
|
return
|
||||||
proc.stop()
|
butler_process.stop()
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ var settings_manager := load("res://core/global/settings_manager.tres") as Setti
|
||||||
var notification_manager := load("res://core/global/notification_manager.tres") as NotificationManager
|
var notification_manager := load("res://core/global/notification_manager.tres") as NotificationManager
|
||||||
const icon := preload("res://plugins/itch/assets/itch.svg")
|
const icon := preload("res://plugins/itch/assets/itch.svg")
|
||||||
|
|
||||||
|
enum LOGIN_METHOD { API_KEY = 0, PASSWORD = 1 }
|
||||||
|
|
||||||
@onready var status := $%Status as StatusPanel
|
@onready var status := $%Status as StatusPanel
|
||||||
@onready var connected_status := $%ConnectedStatus as StatusPanel
|
@onready var connected_status := $%ConnectedStatus as StatusPanel
|
||||||
@onready var logged_in_status := $%LoggedInStatus as StatusPanel
|
@onready var logged_in_status := $%LoggedInStatus as StatusPanel
|
||||||
|
|
@ -44,7 +46,7 @@ func _ready() -> void:
|
||||||
|
|
||||||
# Configure connected status
|
# Configure connected status
|
||||||
connected_status.status = connected_status.STATUS.ACTIVE
|
connected_status.status = connected_status.STATUS.ACTIVE
|
||||||
if itch.state != itch.STATE.BOOT:
|
if itch.connection_state != itch.STATE.BOOT:
|
||||||
connected_status.color = "green"
|
connected_status.color = "green"
|
||||||
itch.client_ready.connect(func():
|
itch.client_ready.connect(func():
|
||||||
connected_status.color = "green"
|
connected_status.color = "green"
|
||||||
|
|
@ -82,8 +84,8 @@ func _ready() -> void:
|
||||||
show_purchases_check.button_pressed = settings_manager.get_value("plugin.itch", "show_purchases", true) as bool
|
show_purchases_check.button_pressed = settings_manager.get_value("plugin.itch", "show_purchases", true) as bool
|
||||||
|
|
||||||
# Connect signals
|
# Connect signals
|
||||||
save_button.pressed.connect(_on_save_button)
|
save_button.pressed.connect(_on_save_pressed)
|
||||||
logout_button.pressed.connect(_on_logout_button)
|
logout_button.pressed.connect(_on_logout_pressed)
|
||||||
login_method.item_selected.connect(_on_login_method_changed)
|
login_method.item_selected.connect(_on_login_method_changed)
|
||||||
filter_check.toggled.connect(_on_filter_toggled)
|
filter_check.toggled.connect(_on_filter_toggled)
|
||||||
show_purchases_check.toggled.connect(_on_show_purchases_toggled)
|
show_purchases_check.toggled.connect(_on_show_purchases_toggled)
|
||||||
|
|
@ -105,17 +107,17 @@ func _on_login(login_status: ItchClient.LOGIN_STATUS, _profile: Dictionary) -> v
|
||||||
func _on_login_method_changed(idx: int) -> void:
|
func _on_login_method_changed(idx: int) -> void:
|
||||||
# API key mode: show API key input, hide user/pass.
|
# API key mode: show API key input, hide user/pass.
|
||||||
# Password mode: show user/pass, hide API key.
|
# Password mode: show user/pass, hide API key.
|
||||||
var is_api := idx == 0
|
var is_api := idx == LOGIN_METHOD.API_KEY
|
||||||
api_key_box.visible = is_api
|
api_key_box.visible = is_api
|
||||||
username_box.visible = not is_api
|
username_box.visible = not is_api
|
||||||
password_box.visible = not is_api
|
password_box.visible = not is_api
|
||||||
|
|
||||||
|
|
||||||
func _on_save_button() -> void:
|
func _on_save_pressed() -> void:
|
||||||
var method: int = login_method.selected
|
var method: int = login_method.selected
|
||||||
settings_manager.set_value("plugin.itch", "login_method", method)
|
settings_manager.set_value("plugin.itch", "login_method", method)
|
||||||
|
|
||||||
if method == 0:
|
if method == LOGIN_METHOD.API_KEY:
|
||||||
# API key login
|
# API key login
|
||||||
settings_manager.set_value("plugin.itch", "username", "")
|
settings_manager.set_value("plugin.itch", "username", "")
|
||||||
var api_key: String = api_key_box.text.strip_edges()
|
var api_key: String = api_key_box.text.strip_edges()
|
||||||
|
|
@ -133,11 +135,11 @@ func _on_save_button() -> void:
|
||||||
itch.login_with_password(uname, password)
|
itch.login_with_password(uname, password)
|
||||||
|
|
||||||
|
|
||||||
func _on_logout_button() -> void:
|
func _on_logout_pressed() -> void:
|
||||||
var method: int = login_method.selected
|
var method: int = login_method.selected
|
||||||
|
|
||||||
# Clear saved credentials
|
# Clear saved credentials
|
||||||
if method == 0:
|
if method == LOGIN_METHOD.API_KEY:
|
||||||
settings_manager.set_value("plugin.itch", "api_key", "")
|
settings_manager.set_value("plugin.itch", "api_key", "")
|
||||||
api_key_box.text = ""
|
api_key_box.text = ""
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,13 @@ extends Library
|
||||||
|
|
||||||
const ItchClient := preload("res://plugins/itch/core/itch_client.gd")
|
const ItchClient := preload("res://plugins/itch/core/itch_client.gd")
|
||||||
const _apps_cache_file: String = "apps.json"
|
const _apps_cache_file: String = "apps.json"
|
||||||
|
const LAUNCH_WATCH_INTERVAL_SEC := 2.0
|
||||||
|
const UPDATE_CHECK_INTERVAL_SEC := 600.0
|
||||||
|
const BUTLERD_BOOT_MAX_ATTEMPTS := 10
|
||||||
|
const BUTLERD_BOOT_RETRY_DELAY_SEC := 0.5
|
||||||
|
const LAUNCH_WATCH_TIMEOUT_MS := 5000
|
||||||
|
## 0o111 — execute bit set for any of owner/group/others.
|
||||||
|
const EXECUTE_PERMISSION_BIT := 73
|
||||||
|
|
||||||
var settings_manager := load("res://core/global/settings_manager.tres") as SettingsManager
|
var settings_manager := load("res://core/global/settings_manager.tres") as SettingsManager
|
||||||
|
|
||||||
|
|
@ -35,14 +42,14 @@ func _ready() -> void:
|
||||||
# instead of leaving the user stuck on a black in-game screen.
|
# instead of leaving the user stuck on a black in-game screen.
|
||||||
if load("res://core/global/launch_manager.tres") != null:
|
if load("res://core/global/launch_manager.tres") != null:
|
||||||
var watch_timer := Timer.new()
|
var watch_timer := Timer.new()
|
||||||
watch_timer.wait_time = 2.0
|
watch_timer.wait_time = LAUNCH_WATCH_INTERVAL_SEC
|
||||||
watch_timer.autostart = true
|
watch_timer.autostart = true
|
||||||
watch_timer.timeout.connect(_check_failed_launches)
|
watch_timer.timeout.connect(_check_failed_launches)
|
||||||
add_child(watch_timer)
|
add_child(watch_timer)
|
||||||
# Keep the has_update() flags fresh in the background (CheckUpdate hits the
|
# Keep the has_update() flags fresh in the background (CheckUpdate hits the
|
||||||
# itch.io API, so it must never run on the UI thread or per-library-load).
|
# itch.io API, so it must never run on the UI thread or per-library-load).
|
||||||
var update_timer := Timer.new()
|
var update_timer := Timer.new()
|
||||||
update_timer.wait_time = 600.0
|
update_timer.wait_time = UPDATE_CHECK_INTERVAL_SEC
|
||||||
update_timer.autostart = true
|
update_timer.autostart = true
|
||||||
update_timer.timeout.connect(_refresh_update_flags)
|
update_timer.timeout.connect(_refresh_update_flags)
|
||||||
add_child(update_timer)
|
add_child(update_timer)
|
||||||
|
|
@ -502,8 +509,8 @@ func _load_library(caching_flags: int = Cache.FLAGS.LOAD | Cache.FLAGS.SAVE) ->
|
||||||
cached_installed = true
|
cached_installed = true
|
||||||
break
|
break
|
||||||
var attempts := 0
|
var attempts := 0
|
||||||
while cached_installed and attempts < 10 and (caves == null or (caves.is_empty() and itch.state != ItchClient.STATE.CONNECTED)):
|
while cached_installed and attempts < BUTLERD_BOOT_MAX_ATTEMPTS and (caves == null or (caves.is_empty() and itch.connection_state != ItchClient.STATE.CONNECTED)):
|
||||||
await get_tree().create_timer(0.5).timeout
|
await get_tree().create_timer(BUTLERD_BOOT_RETRY_DELAY_SEC).timeout
|
||||||
caves = await itch.get_caves()
|
caves = await itch.get_caves()
|
||||||
attempts += 1
|
attempts += 1
|
||||||
var caves_by_game_id := {}
|
var caves_by_game_id := {}
|
||||||
|
|
@ -792,8 +799,7 @@ func _candidate_is_game_binary(path: String) -> bool:
|
||||||
func _is_runnable_file(path: String) -> bool:
|
func _is_runnable_file(path: String) -> bool:
|
||||||
if path == "" or not FileAccess.file_exists(path):
|
if path == "" or not FileAccess.file_exists(path):
|
||||||
return false
|
return false
|
||||||
# 0o111 = execute bit set for any of owner/group/others.
|
return FileAccess.get_unix_permissions(path) & EXECUTE_PERMISSION_BIT != 0
|
||||||
return FileAccess.get_unix_permissions(path) & 73 != 0
|
|
||||||
|
|
||||||
|
|
||||||
## Looks up a runtime binary (love, java, ...). Checks the persistent runtime
|
## Looks up a runtime binary (love, java, ...). Checks the persistent runtime
|
||||||
|
|
@ -839,7 +845,7 @@ func _check_failed_launches() -> void:
|
||||||
continue
|
continue
|
||||||
if app.is_running():
|
if app.is_running():
|
||||||
continue
|
continue
|
||||||
if now - int(_launch_watch[app.pid]) < 5000:
|
if now - int(_launch_watch[app.pid]) < LAUNCH_WATCH_TIMEOUT_MS:
|
||||||
continue
|
continue
|
||||||
if app.pid in _diagnosed_pids:
|
if app.pid in _diagnosed_pids:
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
15
plugin.gd
15
plugin.gd
|
|
@ -14,10 +14,13 @@ var notification_manager := load("res://core/global/notification_manager.tres")
|
||||||
var settings_menu := load("res://plugins/itch/core/itch_settings.tscn") as PackedScene
|
var settings_menu := load("res://plugins/itch/core/itch_settings.tscn") as PackedScene
|
||||||
var icon := preload("res://plugins/itch/assets/itch.svg")
|
var icon := preload("res://plugins/itch/assets/itch.svg")
|
||||||
|
|
||||||
|
const SETTINGS_SECTION := "plugin.itch"
|
||||||
|
enum LOGIN_METHOD { API_KEY = 0, PASSWORD = 1 }
|
||||||
|
|
||||||
var itch: ItchClient
|
var itch: ItchClient
|
||||||
var api_key := settings_manager.get_value("plugin.itch", "api_key", "") as String
|
var api_key := settings_manager.get_value(SETTINGS_SECTION, "api_key", "") as String
|
||||||
var login_method := settings_manager.get_value("plugin.itch", "login_method", 0) as int
|
var login_method := settings_manager.get_value(SETTINGS_SECTION, "login_method", 0) as int
|
||||||
var username := settings_manager.get_value("plugin.itch", "username", "") as String
|
var username := settings_manager.get_value(SETTINGS_SECTION, "username", "") as String
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -26,7 +29,7 @@ func _ready() -> void:
|
||||||
# Load the itch client. This spawns butlerd in the background and
|
# Load the itch client. This spawns butlerd in the background and
|
||||||
# manages the JSON-RPC connection to it.
|
# manages the JSON-RPC connection to it.
|
||||||
itch = load("res://plugins/itch/core/itch_client.tscn").instantiate()
|
itch = load("res://plugins/itch/core/itch_client.tscn").instantiate()
|
||||||
itch.bootstrap_finished.connect(_on_client_start)
|
itch.bootstrap_finished.connect(_on_bootstrap_finished)
|
||||||
itch.client_ready.connect(_on_client_ready)
|
itch.client_ready.connect(_on_client_ready)
|
||||||
itch.logged_in.connect(_on_client_logged_in)
|
itch.logged_in.connect(_on_client_logged_in)
|
||||||
add_child(itch)
|
add_child(itch)
|
||||||
|
|
@ -37,7 +40,7 @@ func _ready() -> void:
|
||||||
|
|
||||||
|
|
||||||
# Triggers when butlerd has been spawned
|
# Triggers when butlerd has been spawned
|
||||||
func _on_client_start() -> void:
|
func _on_bootstrap_finished() -> void:
|
||||||
if not itch.client_started:
|
if not itch.client_started:
|
||||||
var notify := Notification.new("Unable to start the itch.io (butlerd) client")
|
var notify := Notification.new("Unable to start the itch.io (butlerd) client")
|
||||||
notify.icon = icon
|
notify.icon = icon
|
||||||
|
|
@ -52,7 +55,7 @@ func _on_client_ready() -> void:
|
||||||
# logins from previous runs without any stored tokens on our side.
|
# logins from previous runs without any stored tokens on our side.
|
||||||
if await itch.try_saved_login():
|
if await itch.try_saved_login():
|
||||||
return
|
return
|
||||||
if login_method == 0:
|
if login_method == LOGIN_METHOD.API_KEY:
|
||||||
# API key login
|
# API key login
|
||||||
if api_key == "":
|
if api_key == "":
|
||||||
var notify := Notification.new("itch.io API key required")
|
var notify := Notification.new("itch.io API key required")
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"plugin.id": "itch",
|
"plugin.id": "itch",
|
||||||
"plugin.name": "itch.io",
|
"plugin.name": "itch.io",
|
||||||
"plugin.version": "0.1.39",
|
"plugin.version": "0.1.40",
|
||||||
"plugin.min-api-version": "1.1.0",
|
"plugin.min-api-version": "1.1.0",
|
||||||
"plugin.link": "https://forge.thergic.ar/jose/itchio-opengamepadui-plugin",
|
"plugin.link": "https://forge.thergic.ar/jose/itchio-opengamepadui-plugin",
|
||||||
"plugin.source": "https://forge.thergic.ar/jose/itchio-opengamepadui-plugin",
|
"plugin.source": "https://forge.thergic.ar/jose/itchio-opengamepadui-plugin",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue