Update the game from inside it (#13)
"Version X is available" in the top bar is now a button that opens an Update popup: - Update and restart gets the download link from the server (the redirect is not followed, as the Gitea link needs no token), downloads the new game next to the running one with a progress bar, checks it (complete, and an MZ or ELF file), swaps it in and restarts. The quiz is saved first and continues. On Windows the running .exe is renamed to .old, put back if the swap fails, and deleted at the next start. - Download in browser opens the .exe, the Linux package, or the release page on other platforms. - From the editor, an unwritable folder or another platform, only the browser download is offered, with the reason. The message now fits between the tabs and the buttons in every theme; in Nord and Christmas it ran into Sync. The macOS export preset is removed, and the unused download_client_* functions are replaced by fetch_client_download_url. Closes #13 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c03c0e4efe
commit
f39865fffc
30 files changed
+2363
-354
No files matched your search
@@ -19,16 +19,20 @@ func _init(owner: Node) -> void:
|
||||
## [code](response_code: int, body: PackedByteArray, headers: PackedStringArray)[/code],
|
||||
## the headers as [code]"Name: value"[/code] lines. Connection errors and timeouts are
|
||||
## reported with response code 0 and no headers. [param headers], which name the
|
||||
## [param body]'s type, are sent as-is.
|
||||
## [param body]'s type, are sent as-is. Without [param follow_redirects] a redirect is
|
||||
## answered as it is, with its [code]Location[/code] header.
|
||||
func request(
|
||||
url: String,
|
||||
method: int,
|
||||
on_complete: Callable,
|
||||
headers: PackedStringArray = PackedStringArray(),
|
||||
body: PackedByteArray = PackedByteArray(),
|
||||
follow_redirects: bool = true,
|
||||
) -> void:
|
||||
var http_request: HTTPRequest = HTTPRequest.new()
|
||||
http_request.timeout = timeout_seconds
|
||||
if !follow_redirects:
|
||||
http_request.max_redirects = 0
|
||||
_owner.add_child(http_request)
|
||||
http_request.request_completed.connect(
|
||||
func(
|
||||
|
||||
@@ -30,6 +30,9 @@ const CLIENT_TYPE: String = "godot"
|
||||
const QUEUE_HEADER: String = "X-Queue-Id"
|
||||
## The type of the JSON bodies sent.
|
||||
const JSON_TYPE: String = "application/json"
|
||||
## The files of a MusicPlayer release that can be downloaded: the Windows game, the Linux
|
||||
## game, and the Linux package with an installer.
|
||||
const DOWNLOAD_ASSETS: Array[String] = ["windows", "linux", "linux-package"]
|
||||
## Where the cover of a soundtrack can be picked from, by the game's or anime's ID there.
|
||||
const COVER_SOURCES: Array[String] = ["steamgriddb", "anilist"]
|
||||
|
||||
@@ -346,14 +349,14 @@ func fetch_client_assets(on_done: Callable, on_failed: Callable = Callable()) ->
|
||||
get_json("/download/list", on_done, on_failed)
|
||||
|
||||
|
||||
## Downloads the newest MusicPlayer for Windows.
|
||||
func download_client_windows(on_done: Callable, on_failed: Callable = Callable()) -> void:
|
||||
get_bytes("/download/windows", on_done, on_failed)
|
||||
|
||||
|
||||
## Downloads the newest MusicPlayer for Linux.
|
||||
func download_client_linux(on_done: Callable, on_failed: Callable = Callable()) -> void:
|
||||
get_bytes("/download/linux", on_done, on_failed)
|
||||
## Calls [param on_done] with the link to download the newest MusicPlayer file
|
||||
## [param asset] from, one of [constant DOWNLOAD_ASSETS]. The server answers with a
|
||||
## redirect to the release on Gitea, which is not followed: the link needs no token, so
|
||||
## the browser can open it.
|
||||
func fetch_client_download_url(
|
||||
asset: String, on_done: Callable, on_failed: Callable = Callable()) -> void:
|
||||
_request("/download/" + asset, HTTPClient.METHOD_GET, on_done, on_failed, true,
|
||||
PackedByteArray(), JSON_TYPE, false)
|
||||
|
||||
|
||||
# Tokens
|
||||
@@ -391,14 +394,9 @@ func queue_id() -> String:
|
||||
|
||||
# Keeps the queue ID in [param headers] from [param server] when it is new
|
||||
func _remember_queue_id(server: String, headers: PackedStringArray) -> void:
|
||||
var prefix: String = QUEUE_HEADER.to_lower() + ":"
|
||||
for header: String in headers:
|
||||
if !header.to_lower().begins_with(prefix):
|
||||
continue
|
||||
var id: String = header.substr(prefix.length()).strip_edges()
|
||||
if !id.is_empty() && Settings.queue_ids.get(server, "") != id:
|
||||
Settings.set_queue_id(server, id)
|
||||
return
|
||||
var id: String = header_value(headers, QUEUE_HEADER)
|
||||
if !id.is_empty() && Settings.queue_ids.get(server, "") != id:
|
||||
Settings.set_queue_id(server, id)
|
||||
|
||||
|
||||
# Generic requests
|
||||
@@ -482,7 +480,9 @@ func _parse_json(
|
||||
|
||||
|
||||
# Sends the request with the token, and once more with a new token if the server
|
||||
# answers 401. A [param body] of the type [param content_type] is sent with it.
|
||||
# answers 401. A [param body] of the type [param content_type] is sent with it. Without
|
||||
# [param follow_redirects], a redirect is a success and [param on_success] gets where it
|
||||
# points to.
|
||||
func _request(
|
||||
path: String,
|
||||
method: int,
|
||||
@@ -491,17 +491,25 @@ func _request(
|
||||
may_retry: bool = true,
|
||||
body: PackedByteArray = PackedByteArray(),
|
||||
content_type: String = JSON_TYPE,
|
||||
follow_redirects: bool = true,
|
||||
) -> void:
|
||||
_with_token(func(token: String) -> void:
|
||||
_send(path, method, token, func(response_code: int, response: PackedByteArray) -> void:
|
||||
_send(path, method, token, func(response_code: int, response: PackedByteArray,
|
||||
headers: PackedStringArray) -> void:
|
||||
if response_code == 401 && may_retry:
|
||||
GameLog.debug("Token refused by the server, getting a new one")
|
||||
if _token == token:
|
||||
clear_token()
|
||||
_request(path, method, on_success, on_failed, false, body, content_type)
|
||||
_request(path, method, on_success, on_failed, false, body, content_type,
|
||||
follow_redirects)
|
||||
return
|
||||
var location: String = header_value(headers, "Location")
|
||||
if !follow_redirects && response_code >= 300 && response_code < 400 \
|
||||
&& !location.is_empty():
|
||||
on_success.call(location)
|
||||
return
|
||||
_handle_response(path, response_code, response, on_success, on_failed)
|
||||
, body, content_type)
|
||||
, body, content_type, follow_redirects)
|
||||
, on_failed)
|
||||
|
||||
|
||||
@@ -509,7 +517,8 @@ func _request(
|
||||
func _send_once(
|
||||
path: String, method: int, token: String, on_success: Callable, on_failed: Callable,
|
||||
) -> void:
|
||||
_send(path, method, token, func(response_code: int, body: PackedByteArray) -> void:
|
||||
_send(path, method, token, func(response_code: int, body: PackedByteArray,
|
||||
_headers: PackedStringArray) -> void:
|
||||
_handle_response(path, response_code, body, on_success, on_failed)
|
||||
)
|
||||
|
||||
@@ -521,6 +530,7 @@ func _send(
|
||||
on_complete: Callable,
|
||||
body: PackedByteArray = PackedByteArray(),
|
||||
content_type: String = JSON_TYPE,
|
||||
follow_redirects: bool = true,
|
||||
) -> void:
|
||||
var server: String = Settings.default_path
|
||||
var headers: PackedStringArray = []
|
||||
@@ -535,8 +545,8 @@ func _send(
|
||||
func(response_code: int, response_body: PackedByteArray,
|
||||
response_headers: PackedStringArray) -> void:
|
||||
_remember_queue_id(server, response_headers)
|
||||
on_complete.call(response_code, response_body)
|
||||
, headers, body)
|
||||
on_complete.call(response_code, response_body, response_headers)
|
||||
, headers, body, follow_redirects)
|
||||
|
||||
|
||||
func _handle_response(
|
||||
@@ -575,7 +585,7 @@ func _request_token() -> void:
|
||||
token_request.client_type = CLIENT_TYPE
|
||||
var server: String = Settings.default_path
|
||||
_send("/token", HTTPClient.METHOD_POST, "",
|
||||
func(response_code: int, body: PackedByteArray) -> void:
|
||||
func(response_code: int, body: PackedByteArray, _headers: PackedStringArray) -> void:
|
||||
var waiters: Array[Array] = _token_waiters.duplicate()
|
||||
_token_waiters.clear()
|
||||
var token: String = _read_token(response_code, body)
|
||||
@@ -616,6 +626,17 @@ static func _parse_or_null(body: PackedByteArray) -> Variant:
|
||||
return json.data if json.parse(body.get_string_from_utf8()) == OK else null
|
||||
|
||||
|
||||
## The value of the header [param header_name] in [param headers], given as
|
||||
## [code]"Name: value"[/code] lines, or empty when it isn't there. Names are matched
|
||||
## without regard to case.
|
||||
static func header_value(headers: PackedStringArray, header_name: String) -> String:
|
||||
for header: String in headers:
|
||||
var colon: int = header.find(":")
|
||||
if colon > 0 && header.left(colon).strip_edges().to_lower() == header_name.to_lower():
|
||||
return header.substr(colon + 1).strip_edges()
|
||||
return ""
|
||||
|
||||
|
||||
static func _cover_path(uuid: String) -> String:
|
||||
return "/soundtracks/%s/cover" % uuid.uri_encode()
|
||||
|
||||
|
||||
Reference in new issue
Block a user