extends Node ## Game Update Manager - Unified update system for all platforms ## On desktop: Checks if launched via launcher, otherwise shows update prompt ## On mobile: Handles in-game patching with asset pack downloads signal update_check_started signal update_check_completed(has_update: bool, update_info: Dictionary) signal update_check_failed(error: String) signal download_started(total_size: int) signal download_progress(downloaded: int, total: int, percentage: float) signal download_completed signal download_failed(error: String) signal patch_applied signal store_update_required(store_url: String) # Configuration - Update these URLs for your game const VERSION_MANIFEST_URL := "https://raw.githubusercontent.com/adtpdn/tekton-updates/main/latest/version.json" const ANDROID_STORE_URL := "https://play.google.com/store/apps/details?id=com.yourcompany.tekton" const IOS_STORE_URL := "https://apps.apple.com/app/tekton/id123456789" # Platform detection enum Platform { WINDOWS, LINUX, MACOS, ANDROID, IOS, WEB } # State var current_version: String = "2.1.5" var latest_version: String = "2.1.5" var manifest_data: Dictionary = {} var http_request: HTTPRequest var download_request: HTTPRequest var is_downloading: bool = false var pending_pck_path: String = "" var _timeout_timer: Timer func _ready() -> void: # Load embedded version _load_embedded_version() # Create HTTP request nodes http_request = HTTPRequest.new() http_request.name = "VersionCheckRequest" add_child(http_request) http_request.request_completed.connect(_on_version_check_completed) download_request = HTTPRequest.new() download_request.name = "DownloadRequest" add_child(download_request) download_request.request_completed.connect(_on_download_completed) # Timeout fallback — if no response in 5s, use local manifest _timeout_timer = Timer.new() _timeout_timer.name = "VersionCheckTimeout" _timeout_timer.wait_time = 5.0 _timeout_timer.one_shot = true _timeout_timer.timeout.connect(_on_version_timeout) add_child(_timeout_timer) func _load_embedded_version() -> void: # Try to load version from embedded file first if FileAccess.file_exists("res://version.txt"): var file := FileAccess.open("res://version.txt", FileAccess.READ) if file: current_version = file.get_as_text().strip_edges() file.close() # Also check for locally saved version (from previous patch) var local_version_path := _get_local_version_path() if FileAccess.file_exists(local_version_path): var file := FileAccess.open(local_version_path, FileAccess.READ) if file: var local_ver := file.get_as_text().strip_edges() file.close() # Use local version if it's newer if _compare_versions(local_ver, current_version) > 0: current_version = local_ver print("[GameUpdateManager] Current version: ", current_version) # --- Platform Detection --- static func get_current_platform() -> Platform: var os_name := OS.get_name() match os_name: "Windows": return Platform.WINDOWS "Linux", "FreeBSD", "NetBSD", "OpenBSD", "BSD": return Platform.LINUX "macOS": return Platform.MACOS "Android": return Platform.ANDROID "iOS": return Platform.IOS "Web": return Platform.WEB _: return Platform.LINUX static func is_mobile() -> bool: var platform := get_current_platform() return platform == Platform.ANDROID or platform == Platform.IOS static func is_desktop() -> bool: var platform := get_current_platform() return platform in [Platform.WINDOWS, Platform.LINUX, Platform.MACOS] static func get_platform_name() -> String: match get_current_platform(): Platform.WINDOWS: return "windows" Platform.LINUX: return "linux" Platform.MACOS: return "macos" Platform.ANDROID: return "android" Platform.IOS: return "ios" Platform.WEB: return "web" return "unknown" # --- Update Checking --- func check_for_updates() -> void: emit_signal("update_check_started") # In the editor, skip network fetch — use local manifest directly if Engine.is_editor_hint() or OS.has_feature("editor"): print("[GameUpdateManager] Editor mode — using local manifest.") _load_local_manifest() return print("[GameUpdateManager] Checking for updates from: ", VERSION_MANIFEST_URL) _timeout_timer.start() var error := http_request.request(VERSION_MANIFEST_URL) if error != OK: _timeout_timer.stop() push_warning("[GameUpdateManager] Request failed instantly, using local manifest.") _load_local_manifest() func _on_version_timeout() -> void: push_warning("[GameUpdateManager] Version check timed out, using local manifest.") http_request.cancel_request() _load_local_manifest() func _on_version_check_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void: _timeout_timer.stop() if result != HTTPRequest.RESULT_SUCCESS: push_warning("[GameUpdateManager] Network error (result=%d), falling back to local manifest." % result) _load_local_manifest() return if response_code != 200: push_warning("[GameUpdateManager] Server returned %d, falling back to local manifest." % response_code) _load_local_manifest() return var json := JSON.new() if json.parse(body.get_string_from_utf8()) != OK: push_warning("[GameUpdateManager] JSON parse failed, falling back to local manifest.") _load_local_manifest() return manifest_data = json.data _process_manifest() func _load_local_manifest() -> void: const LOCAL_MANIFEST := "res://assets/data/version.json" if not FileAccess.file_exists(LOCAL_MANIFEST): emit_signal("update_check_failed", "No manifest available (offline)") return var f := FileAccess.open(LOCAL_MANIFEST, FileAccess.READ) if not f: emit_signal("update_check_failed", "Could not read local manifest") return var parsed = JSON.parse_string(f.get_as_text()) if not parsed is Dictionary: emit_signal("update_check_failed", "Invalid local manifest JSON") return manifest_data = parsed _process_manifest() func _process_manifest() -> void: latest_version = manifest_data.get("latest_version", current_version) print("[GameUpdateManager] current_version=%s latest_version=%s" % [current_version, latest_version]) var has_update := _compare_versions(current_version, latest_version) < 0 var min_version: String = manifest_data.get("minimum_app_version", "0.0.0") var needs_store_update := _compare_versions(current_version, min_version) < 0 var update_info := { "has_update": has_update, "latest_version": latest_version, "current_version": current_version, "needs_store_update": needs_store_update, "changelog": _get_changelog_since(current_version), "can_patch": _can_apply_patch() and not needs_store_update } print("[GameUpdateManager] has_update=%s can_patch=%s needs_store=%s" % [ has_update, update_info.can_patch, needs_store_update ]) emit_signal("update_check_completed", has_update, update_info) if needs_store_update and is_mobile(): var store_url := IOS_STORE_URL if get_current_platform() == Platform.IOS else ANDROID_STORE_URL emit_signal("store_update_required", store_url) func _can_apply_patch() -> bool: # On mobile, we can download asset packs # On desktop, we should use the launcher instead if is_mobile(): return true # On desktop, only patch if launcher isn't available return not _launcher_available() func _launcher_available() -> bool: # Check if we were launched from the launcher # The launcher could set an environment variable or command line arg return OS.has_environment("TEKTON_LAUNCHER") or "--from-launcher" in OS.get_cmdline_args() # --- Downloading Updates --- func download_update() -> void: if is_downloading: return var download_info := _get_download_info() if download_info.url == "": emit_signal("download_failed", "No download URL available for this platform") return is_downloading = true pending_pck_path = _get_patch_download_path() # Ensure download directory exists var dir := DirAccess.open("user://") if dir and not dir.dir_exists("patches"): dir.make_dir("patches") download_request.download_file = pending_pck_path print("[GameUpdateManager] Downloading update from: ", download_info.url) emit_signal("download_started", download_info.size) var error := download_request.request(download_info.url) if error != OK: is_downloading = false emit_signal("download_failed", "Failed to start download") func _process(_delta: float) -> void: if is_downloading and download_request: var downloaded := download_request.get_downloaded_bytes() var total := download_request.get_body_size() if total > 0: var percentage := (float(downloaded) / float(total)) * 100.0 emit_signal("download_progress", downloaded, total, percentage) func _on_download_completed(result: int, response_code: int, _headers: PackedStringArray, _body: PackedByteArray) -> void: is_downloading = false if result != HTTPRequest.RESULT_SUCCESS or response_code != 200: emit_signal("download_failed", "Download failed") return print("[GameUpdateManager] Download complete, applying patch...") _apply_patch() func _apply_patch() -> void: # Load the downloaded PCK as a resource pack var success := ProjectSettings.load_resource_pack(pending_pck_path, true) if success: # Save the new version _save_local_version(latest_version) current_version = latest_version print("[GameUpdateManager] Patch applied successfully!") emit_signal("patch_applied") emit_signal("download_completed") else: push_error("[GameUpdateManager] Failed to load resource pack") emit_signal("download_failed", "Failed to apply patch") # Clean up failed download var dir := DirAccess.open("user://patches/") if dir: dir.remove(pending_pck_path.get_file()) # --- Helper Functions --- func _get_download_info() -> Dictionary: var platform := get_platform_name() var releases: Array = manifest_data.get("releases", []) for release in releases: if release.get("version") == latest_version: # Check for platform-specific download var platform_key := "pck_" + platform if release.has(platform_key): var pdata: Dictionary = release.get(platform_key, {}) return { "url": pdata.get("url", ""), "size": pdata.get("size", 0), "checksum": pdata.get("checksum_md5", "") } # Fall back to generic return { "url": release.get("pck_url", ""), "size": release.get("pck_size", 0), "checksum": release.get("checksum_md5", "") } return {"url": "", "size": 0, "checksum": ""} func _get_patch_download_path() -> String: return "user://patches/patch_" + latest_version.replace(".", "_") + ".pck" func _get_local_version_path() -> String: return "user://current_version.txt" func _save_local_version(version: String) -> void: var file := FileAccess.open(_get_local_version_path(), FileAccess.WRITE) if file: file.store_string(version) file.close() func _compare_versions(v1: String, v2: String) -> int: var parts1 := v1.split(".") var parts2 := v2.split(".") for i in range(max(parts1.size(), parts2.size())): var p1 := int(parts1[i]) if i < parts1.size() else 0 var p2 := int(parts2[i]) if i < parts2.size() else 0 if p1 < p2: return -1 elif p1 > p2: return 1 return 0 func _get_changelog_since(since_version: String) -> Array: var changelog: Array = [] var releases: Array = manifest_data.get("releases", []) for release in releases: var ver: String = release.get("version", "") if _compare_versions(since_version, ver) < 0: changelog.append({ "version": ver, "date": release.get("date", ""), "changes": release.get("changelog", []) }) return changelog # --- Store Redirect --- func open_store_page() -> void: var url: String match get_current_platform(): Platform.ANDROID: url = ANDROID_STORE_URL Platform.IOS: url = IOS_STORE_URL _: url = "https://your-username.itch.io/tekton-local" OS.shell_open(url)