178 lines
6.4 KiB
GDScript
178 lines
6.4 KiB
GDScript
extends Node
|
|
## The update check (docs/ROADMAP.md, M4 "Updates", first step): is there a
|
|
## newer Cinderwild than this one? Asked once at start-up, of the feed in
|
|
## Project Settings (`cinderwild/updates/feed`). The feed is either
|
|
## - a release API answer (Gitea, Forgejo or GitHub `releases/latest`): its
|
|
## `version.json` asset is fetched next, and its other assets are the
|
|
## downloads, by file name; or
|
|
## - a `version.json` itself:
|
|
## {"version": "0.2.0", "protocol": 13, "notes": "...", "page": "https://...",
|
|
## "downloads": {"macos": {"file": "Cinderwild-0.2.0-macos.zip", "sha256": "...",
|
|
## "size": 61164958, "url": "(optional; else the asset)"},
|
|
## "windows": {...}}}
|
|
## tools/export.sh writes that file next to the zips, for the release.
|
|
##
|
|
## This only tells you (title screen, join refusals, the server's log);
|
|
## downloading and swapping in the new version is the next step. Off in
|
|
## headless runs (tests, bots) unless forced; a dedicated server forces it.
|
|
## Your setting (on or off) is in user://updates.json.
|
|
|
|
signal checked(state: String)
|
|
|
|
const TIMEOUT := 10.0
|
|
|
|
var version: String = String(ProjectSettings.get_setting("application/config/version", "0.0.0"))
|
|
var feed: String = String(ProjectSettings.get_setting("cinderwild/updates/feed", ""))
|
|
var prefs_path := "user://updates.json"
|
|
var enabled := true
|
|
## idle | off | checking | current | available | failed
|
|
var state := "idle"
|
|
var latest: Dictionary = {} ## the version.json of the newest release
|
|
var assets: Dictionary = {} ## file name -> download URL, from the release
|
|
var page := "" ## a web page for the release (or the feed's "page")
|
|
var error := ""
|
|
## Tests: url -> response text, instead of the network.
|
|
var fetch_override: Callable
|
|
|
|
func _ready() -> void:
|
|
load_prefs()
|
|
|
|
func load_prefs() -> void:
|
|
enabled = true
|
|
if FileAccess.file_exists(prefs_path):
|
|
var d: Variant = JSON.parse_string(FileAccess.get_file_as_string(prefs_path))
|
|
if d is Dictionary:
|
|
enabled = bool(d.get("check", true))
|
|
|
|
func set_enabled(on: bool) -> void:
|
|
enabled = on
|
|
var f := FileAccess.open(prefs_path, FileAccess.WRITE)
|
|
if f != null:
|
|
f.store_string(JSON.stringify({"check": enabled}, "\t"))
|
|
|
|
## Asks the feed once. `force`: even headless, even with the setting off (a
|
|
## dedicated server, which has its own setting).
|
|
func check(force: bool = false) -> void:
|
|
if state == "checking":
|
|
return
|
|
if not force and (not enabled or (DisplayServer.get_name() == "headless" and not fetch_override.is_valid())):
|
|
_finish("off")
|
|
return
|
|
if feed.is_empty():
|
|
error = "no update feed set"
|
|
_finish("failed")
|
|
return
|
|
state = "checking"
|
|
latest = {}
|
|
assets = {}
|
|
page = ""
|
|
error = ""
|
|
var first: Variant = await _get_json(feed)
|
|
if not (first is Dictionary):
|
|
_finish("failed")
|
|
return
|
|
var d: Dictionary = first
|
|
if not d.has("version") and d.has("assets"):
|
|
# A release: its assets by name, then its version.json.
|
|
page = String(d.get("html_url", ""))
|
|
for a in d.get("assets", []):
|
|
if a is Dictionary and a.has("name") and a.has("browser_download_url"):
|
|
assets[String(a.name)] = String(a.browser_download_url)
|
|
if not assets.has("version.json"):
|
|
error = "the latest release has no version.json"
|
|
_finish("failed")
|
|
return
|
|
var v: Variant = await _get_json(assets["version.json"])
|
|
if not (v is Dictionary):
|
|
_finish("failed")
|
|
return
|
|
d = v
|
|
if not d.has("version"):
|
|
error = "the feed has no version"
|
|
_finish("failed")
|
|
return
|
|
latest = d
|
|
if page.is_empty():
|
|
page = String(d.get("page", ""))
|
|
_finish("available" if compare_versions(String(d.version), version) > 0 else "current")
|
|
|
|
func _finish(s: String) -> void:
|
|
state = s
|
|
if s == "failed":
|
|
print("[updates] couldn't check for updates: ", error)
|
|
elif s == "available":
|
|
print("[updates] Cinderwild %s is out (this is %s)" % [latest_version(), version])
|
|
checked.emit(s)
|
|
|
|
func _get_json(url: String) -> Variant:
|
|
var text := ""
|
|
if fetch_override.is_valid():
|
|
text = String(fetch_override.call(url))
|
|
else:
|
|
var http := HTTPRequest.new()
|
|
http.timeout = TIMEOUT
|
|
add_child(http)
|
|
var err := http.request(url, ["Accept: application/json", "User-Agent: Cinderwild/" + version])
|
|
if err != OK:
|
|
http.queue_free()
|
|
error = "couldn't ask %s (%d)" % [url, err]
|
|
return null
|
|
var r: Array = await http.request_completed
|
|
http.queue_free()
|
|
if int(r[0]) != HTTPRequest.RESULT_SUCCESS or int(r[1]) != 200:
|
|
error = "%s answered %s" % [url, "HTTP %d" % int(r[1]) if int(r[0]) == HTTPRequest.RESULT_SUCCESS else "nothing (result %d)" % int(r[0])]
|
|
return null
|
|
text = (r[3] as PackedByteArray).get_string_from_utf8()
|
|
var j := JSON.new() # (quietly: a broken feed isn't an engine error)
|
|
if j.parse(text) != OK or not (j.data is Dictionary):
|
|
error = "%s didn't answer with JSON" % url
|
|
return null
|
|
return j.data
|
|
|
|
# --- what we found --------------------------------------------------------------------
|
|
|
|
func latest_version() -> String:
|
|
return String(latest.get("version", ""))
|
|
|
|
func latest_protocol() -> int:
|
|
return int(latest.get("protocol", 0))
|
|
|
|
## "macos", "windows" or "linux": the key in version.json's downloads.
|
|
static func platform_key() -> String:
|
|
match OS.get_name():
|
|
"macOS":
|
|
return "macos"
|
|
"Windows":
|
|
return "windows"
|
|
return "linux"
|
|
|
|
## This platform's download in the newest release: {file, sha256, size, url}.
|
|
func download() -> Dictionary:
|
|
var dl: Variant = latest.get("downloads", {}).get(platform_key(), {})
|
|
if not (dl is Dictionary) or dl.is_empty():
|
|
return {}
|
|
var out: Dictionary = (dl as Dictionary).duplicate()
|
|
if String(out.get("url", "")).is_empty():
|
|
out["url"] = String(assets.get(String(out.get("file", "")), ""))
|
|
return out
|
|
|
|
## Where the Download button goes: this platform's file, else the release page.
|
|
func download_url() -> String:
|
|
var u := String(download().get("url", ""))
|
|
return u if not u.is_empty() else page
|
|
|
|
## A packaged game (an exported build), as opposed to running from the source.
|
|
static func packaged() -> bool:
|
|
return OS.has_feature("template")
|
|
|
|
## -1, 0 or 1. "v0.10.2" > "0.9", "1.0.0-beta" == "1.0.0" (suffixes ignored).
|
|
static func compare_versions(a: String, b: String) -> int:
|
|
var pa := a.strip_edges().trim_prefix("v").get_slice("-", 0).split(".")
|
|
var pb := b.strip_edges().trim_prefix("v").get_slice("-", 0).split(".")
|
|
for i in range(maxi(pa.size(), pb.size())):
|
|
var x := int(pa[i]) if i < pa.size() else 0
|
|
var y := int(pb[i]) if i < pb.size() else 0
|
|
if x != y:
|
|
return 1 if x > y else -1
|
|
return 0
|