diff --git a/assets/itch.svg b/assets/itch.svg
index 492b393..691bbdb 100644
--- a/assets/itch.svg
+++ b/assets/itch.svg
@@ -1,5 +1,4 @@
-
-
\ No newline at end of file
+
diff --git a/core/itch_client.gd b/core/itch_client.gd
index 8a30fa5..5af9df4 100644
--- a/core/itch_client.gd
+++ b/core/itch_client.gd
@@ -15,11 +15,11 @@ extends NodeThread
## https://pkg.go.dev/github.com/itchio/butler/butlerd). If itch.io ships a
## 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
## of user://plugins// because OGPU wipes that directory (and everything
## 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:
## OGPU moves the entire extracted plugin directory (plugins//) to the
## trash on every plugin update, so any install kept under it silently loses
@@ -27,11 +27,8 @@ const BUTLER_DIR := "user://butler"
## used to break both fresh installs ("That upload is already installed!")
## and launches. Keeping games next to butler.db (user://butler) means they
## survive plugin updates untouched.
-const GAMES_DIR := "user://butler/games"
-const CACHE_NAMESPACE := "itch"
-const CONNECTION_TIMEOUT_ITERATIONS := 50
-const CONNECTION_POLL_DELAY_MS := 20
-const PERCENTAGE_SCALE := 100
+const games_dir := "user://butler/games"
+const CACHE_DIR := "itch"
enum STATE {
BOOT,
@@ -60,9 +57,9 @@ signal app_updated(cave_id: String, success: bool)
signal app_uninstalled(cave_id: String, success: bool)
signal launch_exited(cave_id: String)
-var butler_process: InteractiveProcess
-var rpc_socket: StreamPeerTCP
-var connection_state: STATE = STATE.BOOT
+var proc: InteractiveProcess
+var socket: StreamPeerTCP
+var state: STATE = STATE.BOOT
var client_started := false
var is_logged_in := false
var profile: Dictionary = {}
@@ -70,7 +67,7 @@ var profile: Dictionary = {}
var _proc_buffer := ""
var _recv_buffer := ""
var _next_id := 1
-var _cached_install_location_id := ""
+var _install_location_id := ""
var logger := Log.get_logger("ItchClient", Log.LEVEL.INFO)
@@ -84,7 +81,7 @@ func _ready() -> void:
## Bootstraps the butler binary if it isn't present, then spawns butlerd.
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)
var butler_bin := "/".join([butler_dir_global, "butler"])
if not FileAccess.file_exists(butler_bin):
@@ -104,16 +101,16 @@ func bootstrap() -> void:
"--keep-alive",
"--dbpath", dbpath,
"--address", "https://itch.io",
- "--user-agent", _get_user_agent(),
+ "--user-agent", "OpenGamepadUI-itch/0.1.0",
"--destiny-pid", str(OS.get_process_id()),
]
- butler_process = InteractiveProcess.new(butler_bin, args)
- if butler_process.start() != OK:
+ proc = InteractiveProcess.new(butler_bin, args)
+ if proc.start() != OK:
logger.error("Unable to spawn butlerd")
return
client_started = true
- connection_state = STATE.WAITING_HANDSHAKE
+ state = STATE.WAITING_HANDSHAKE
bootstrap_finished.emit()
@@ -141,22 +138,22 @@ func _migrate_butler(dest_dir: String) -> void:
## 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).
func _install_butler() -> bool:
- var os_name := "linux"
- var arch_name := "amd64"
+ var goos := "linux"
+ var goarch := "amd64"
if OS.get_name() == "Windows":
- os_name = "windows"
+ goos = "windows"
if OS.get_name() == "macOS":
- os_name = "darwin"
+ goos = "darwin"
# 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
# older engine version.
if Engine.has_method("get_architecture_name"):
var arch: String = Engine.get_architecture_name()
if "arm64" in arch or "aarch64" in arch:
- arch_name = "arm64"
+ goarch = "arm64"
- var platform_slug := os_name + "-" + arch_name
- var latest_url := "/".join([BROTH_BASE, platform_slug, "LATEST"])
+ var platform_slug := goos + "-" + goarch
+ var latest_url := "/".join([broth_base, platform_slug, "LATEST"])
var http := HTTPRequest.new()
add_child.call_deferred(http)
@@ -177,7 +174,7 @@ func _install_butler() -> bool:
return false
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:
logger.error("Error downloading butler: " + archive_url)
remove_child(http)
@@ -193,7 +190,7 @@ func _install_butler() -> bool:
logger.error("butler couldn't be downloaded: " + archive_url)
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)
var zip_path := "/tmp/butler-" + version + ".zip"
var file := FileAccess.open(zip_path, FileAccess.WRITE_READ)
@@ -207,24 +204,13 @@ func _install_butler() -> bool:
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
# ---------------------------------------------------------------------------
func _thread_process(_delta: float) -> void:
- if connection_state == STATE.WAITING_HANDSHAKE and butler_process:
- _proc_buffer += butler_process.read()
+ if state == STATE.WAITING_HANDSHAKE and proc:
+ _proc_buffer += proc.read()
if not _proc_buffer.contains("\n"):
return
var lines := _proc_buffer.split("\n")
@@ -244,12 +230,12 @@ func _thread_process(_delta: float) -> void:
_connect_and_authenticate.call_deferred(address, secret)
return
- if connection_state == STATE.CONNECTED and rpc_socket:
- rpc_socket.poll()
- var available := rpc_socket.get_available_bytes()
+ if state == STATE.CONNECTED and socket:
+ socket.poll()
+ var available := socket.get_available_bytes()
if available <= 0:
return
- var chunk := rpc_socket.get_partial_data(available)
+ var chunk := socket.get_partial_data(available)
if chunk[0] != OK:
return
_recv_buffer += (chunk[1] as PackedByteArray).get_string_from_utf8()
@@ -274,22 +260,22 @@ func _connect_and_authenticate(address: String, secret: String) -> void:
var host: String = parts[0]
var port: int = int(parts[1])
- rpc_socket = StreamPeerTCP.new()
- if rpc_socket.connect_to_host(host, port) != OK:
+ socket = StreamPeerTCP.new()
+ if socket.connect_to_host(host, port) != OK:
logger.error("Unable to connect to butlerd at " + address)
return
# Wait for the connection to establish
- var timeout := CONNECTION_TIMEOUT_ITERATIONS
- while rpc_socket.get_status() == StreamPeerTCP.STATUS_CONNECTING and timeout > 0:
- rpc_socket.poll()
- OS.delay_msec(CONNECTION_POLL_DELAY_MS)
+ var timeout := 50
+ while socket.get_status() == StreamPeerTCP.STATUS_CONNECTING and timeout > 0:
+ socket.poll()
+ OS.delay_msec(20)
timeout -= 1
- if rpc_socket.get_status() != StreamPeerTCP.STATUS_CONNECTED:
+ if socket.get_status() != StreamPeerTCP.STATUS_CONNECTED:
logger.error("Timed out connecting to butlerd")
return
- connection_state = STATE.CONNECTED
+ state = STATE.CONNECTED
var res := await _rpc_call("Meta.Authenticate", {"secret": secret})
if "error" in res:
@@ -305,9 +291,9 @@ func _connect_and_authenticate(address: String, secret: String) -> void:
# ---------------------------------------------------------------------------
func _send_line(text: String) -> void:
- if not rpc_socket:
+ if not socket:
return
- rpc_socket.put_data((text + "\n").to_utf8_buffer())
+ socket.put_data((text + "\n").to_utf8_buffer())
## Godot's JSON.parse decodes every JSON number as a float (so 7670 becomes
@@ -589,17 +575,17 @@ func _get_caves() -> Variant:
## every plugin update; existing locations pointing there are left alone (their
## orphaned caves are cleaned up by [method _resolve_cave]) but never reused.
func _ensure_install_location() -> String:
- if _cached_install_location_id != "":
- return _cached_install_location_id
+ if _install_location_id != "":
+ return _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 locations: Array = res.get("installLocations", [])
for loc in locations:
var location: Dictionary = loc
if location.get("path", "") == target_path:
- _cached_install_location_id = location.get("id", "")
- return _cached_install_location_id
+ _install_location_id = location.get("id", "")
+ return _install_location_id
DirAccess.make_dir_recursive_absolute(target_path)
# Let butlerd generate the id so we never collide with a stale location
@@ -608,8 +594,8 @@ func _ensure_install_location() -> String:
if "error" in add_res:
logger.error("Install.Locations.Add failed: " + str(add_res["error"]))
return ""
- _cached_install_location_id = add_res.get("installLocation", {}).get("id", "")
- return _cached_install_location_id
+ _install_location_id = add_res.get("installLocation", {}).get("id", "")
+ return _install_location_id
## Returns every configured install location, each as:
@@ -629,7 +615,7 @@ func _get_install_locations() -> Array:
logger.warn("Install.Locations.List failed: " + str(res["error"]))
return []
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 preferred: Array = []
var others: Array = []
@@ -804,8 +790,8 @@ func _install(game: Dictionary, cave_id: String, options: Dictionary) -> bool:
var progress: float = p.get("progress", 0.0)
install_progressed.emit.call_deferred(
cave_id if cave_id != "" else str(game_id),
- int(progress * PERCENTAGE_SCALE),
- PERCENTAGE_SCALE
+ int(progress * 100),
+ 100
)
rpc_notification.connect(on_notification)
@@ -998,8 +984,8 @@ func _logout() -> void:
func _exit_tree() -> void:
- if rpc_socket:
- rpc_socket.disconnect_from_host()
- if not butler_process:
+ if socket:
+ socket.disconnect_from_host()
+ if not proc:
return
- butler_process.stop()
+ proc.stop()
diff --git a/core/itch_settings.gd b/core/itch_settings.gd
index e1d5536..a2bd26c 100644
--- a/core/itch_settings.gd
+++ b/core/itch_settings.gd
@@ -11,8 +11,6 @@ var settings_manager := load("res://core/global/settings_manager.tres") as Setti
var notification_manager := load("res://core/global/notification_manager.tres") as NotificationManager
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 connected_status := $%ConnectedStatus as StatusPanel
@onready var logged_in_status := $%LoggedInStatus as StatusPanel
@@ -46,7 +44,7 @@ func _ready() -> void:
# Configure connected status
connected_status.status = connected_status.STATUS.ACTIVE
- if itch.connection_state != itch.STATE.BOOT:
+ if itch.state != itch.STATE.BOOT:
connected_status.color = "green"
itch.client_ready.connect(func():
connected_status.color = "green"
@@ -84,8 +82,8 @@ func _ready() -> void:
show_purchases_check.button_pressed = settings_manager.get_value("plugin.itch", "show_purchases", true) as bool
# Connect signals
- save_button.pressed.connect(_on_save_pressed)
- logout_button.pressed.connect(_on_logout_pressed)
+ save_button.pressed.connect(_on_save_button)
+ logout_button.pressed.connect(_on_logout_button)
login_method.item_selected.connect(_on_login_method_changed)
filter_check.toggled.connect(_on_filter_toggled)
show_purchases_check.toggled.connect(_on_show_purchases_toggled)
@@ -107,17 +105,17 @@ func _on_login(login_status: ItchClient.LOGIN_STATUS, _profile: Dictionary) -> v
func _on_login_method_changed(idx: int) -> void:
# API key mode: show API key input, hide user/pass.
# Password mode: show user/pass, hide API key.
- var is_api := idx == LOGIN_METHOD.API_KEY
+ var is_api := idx == 0
api_key_box.visible = is_api
username_box.visible = not is_api
password_box.visible = not is_api
-func _on_save_pressed() -> void:
+func _on_save_button() -> void:
var method: int = login_method.selected
settings_manager.set_value("plugin.itch", "login_method", method)
- if method == LOGIN_METHOD.API_KEY:
+ if method == 0:
# API key login
settings_manager.set_value("plugin.itch", "username", "")
var api_key: String = api_key_box.text.strip_edges()
@@ -135,11 +133,11 @@ func _on_save_pressed() -> void:
itch.login_with_password(uname, password)
-func _on_logout_pressed() -> void:
+func _on_logout_button() -> void:
var method: int = login_method.selected
# Clear saved credentials
- if method == LOGIN_METHOD.API_KEY:
+ if method == 0:
settings_manager.set_value("plugin.itch", "api_key", "")
api_key_box.text = ""
else:
diff --git a/core/library_itch.gd b/core/library_itch.gd
index 5d02b33..6813c92 100644
--- a/core/library_itch.gd
+++ b/core/library_itch.gd
@@ -2,13 +2,6 @@ extends Library
const ItchClient := preload("res://plugins/itch/core/itch_client.gd")
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
@@ -42,14 +35,14 @@ func _ready() -> void:
# instead of leaving the user stuck on a black in-game screen.
if load("res://core/global/launch_manager.tres") != null:
var watch_timer := Timer.new()
- watch_timer.wait_time = LAUNCH_WATCH_INTERVAL_SEC
+ watch_timer.wait_time = 2.0
watch_timer.autostart = true
watch_timer.timeout.connect(_check_failed_launches)
add_child(watch_timer)
# 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).
var update_timer := Timer.new()
- update_timer.wait_time = UPDATE_CHECK_INTERVAL_SEC
+ update_timer.wait_time = 600.0
update_timer.autostart = true
update_timer.timeout.connect(_refresh_update_flags)
add_child(update_timer)
@@ -509,8 +502,8 @@ func _load_library(caching_flags: int = Cache.FLAGS.LOAD | Cache.FLAGS.SAVE) ->
cached_installed = true
break
var attempts := 0
- 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(BUTLERD_BOOT_RETRY_DELAY_SEC).timeout
+ while cached_installed and attempts < 10 and (caves == null or (caves.is_empty() and itch.state != ItchClient.STATE.CONNECTED)):
+ await get_tree().create_timer(0.5).timeout
caves = await itch.get_caves()
attempts += 1
var caves_by_game_id := {}
@@ -799,7 +792,8 @@ func _candidate_is_game_binary(path: String) -> bool:
func _is_runnable_file(path: String) -> bool:
if path == "" or not FileAccess.file_exists(path):
return false
- return FileAccess.get_unix_permissions(path) & EXECUTE_PERMISSION_BIT != 0
+ # 0o111 = execute bit set for any of owner/group/others.
+ return FileAccess.get_unix_permissions(path) & 73 != 0
## Looks up a runtime binary (love, java, ...). Checks the persistent runtime
@@ -845,7 +839,7 @@ func _check_failed_launches() -> void:
continue
if app.is_running():
continue
- if now - int(_launch_watch[app.pid]) < LAUNCH_WATCH_TIMEOUT_MS:
+ if now - int(_launch_watch[app.pid]) < 5000:
continue
if app.pid in _diagnosed_pids:
continue
diff --git a/plugin.gd b/plugin.gd
index fe7fc84..ba649b4 100644
--- a/plugin.gd
+++ b/plugin.gd
@@ -14,13 +14,10 @@ var notification_manager := load("res://core/global/notification_manager.tres")
var settings_menu := load("res://plugins/itch/core/itch_settings.tscn") as PackedScene
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 api_key := settings_manager.get_value(SETTINGS_SECTION, "api_key", "") as String
-var login_method := settings_manager.get_value(SETTINGS_SECTION, "login_method", 0) as int
-var username := settings_manager.get_value(SETTINGS_SECTION, "username", "") as String
+var api_key := settings_manager.get_value("plugin.itch", "api_key", "") as String
+var login_method := settings_manager.get_value("plugin.itch", "login_method", 0) as int
+var username := settings_manager.get_value("plugin.itch", "username", "") as String
func _ready() -> void:
@@ -29,7 +26,7 @@ func _ready() -> void:
# Load the itch client. This spawns butlerd in the background and
# manages the JSON-RPC connection to it.
itch = load("res://plugins/itch/core/itch_client.tscn").instantiate()
- itch.bootstrap_finished.connect(_on_bootstrap_finished)
+ itch.bootstrap_finished.connect(_on_client_start)
itch.client_ready.connect(_on_client_ready)
itch.logged_in.connect(_on_client_logged_in)
add_child(itch)
@@ -40,7 +37,7 @@ func _ready() -> void:
# Triggers when butlerd has been spawned
-func _on_bootstrap_finished() -> void:
+func _on_client_start() -> void:
if not itch.client_started:
var notify := Notification.new("Unable to start the itch.io (butlerd) client")
notify.icon = icon
@@ -55,7 +52,7 @@ func _on_client_ready() -> void:
# logins from previous runs without any stored tokens on our side.
if await itch.try_saved_login():
return
- if login_method == LOGIN_METHOD.API_KEY:
+ if login_method == 0:
# API key login
if api_key == "":
var notify := Notification.new("itch.io API key required")
diff --git a/plugin.json b/plugin.json
index 7f942c0..e6d4d11 100644
--- a/plugin.json
+++ b/plugin.json
@@ -1,7 +1,7 @@
{
"plugin.id": "itch",
"plugin.name": "itch.io",
- "plugin.version": "0.1.40",
+ "plugin.version": "0.1.39",
"plugin.min-api-version": "1.1.0",
"plugin.link": "https://forge.thergic.ar/jose/itchio-opengamepadui-plugin",
"plugin.source": "https://forge.thergic.ar/jose/itchio-opengamepadui-plugin",