Files
tekton/scripts/managers/game_update_manager.gd
T
2026-04-22 05:05:12 +08:00

310 lines
9.9 KiB
GDScript

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 = ""
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)
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")
print("[GameUpdateManager] Checking for updates...")
var error := http_request.request(VERSION_MANIFEST_URL)
if error != OK:
emit_signal("update_check_failed", "Failed to connect to update server")
func _on_version_check_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
if result != HTTPRequest.RESULT_SUCCESS:
emit_signal("update_check_failed", "Network error")
return
if response_code != 200:
emit_signal("update_check_failed", "Server error: " + str(response_code))
return
var json := JSON.new()
if json.parse(body.get_string_from_utf8()) != OK:
emit_signal("update_check_failed", "Invalid update data")
return
manifest_data = json.data
latest_version = manifest_data.get("latest_version", current_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] Update check complete - Has update: ", has_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)