init
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
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://your-username.itch.io/tekton-local/data/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 = "1.0.0"
|
||||
var latest_version: String = "1.0.0"
|
||||
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)
|
||||
@@ -0,0 +1,181 @@
|
||||
extends Control
|
||||
## Boot screen that handles update checking before launching the game
|
||||
## On mobile: Shows update UI and handles in-game patching
|
||||
## On desktop: Quick check, then proceeds (assumes launcher handles updates)
|
||||
|
||||
@onready var status_label := %StatusLabel as Label
|
||||
@onready var progress_container := %ProgressContainer as VBoxContainer
|
||||
@onready var progress_bar := %ProgressBar as ProgressBar
|
||||
@onready var progress_label := %ProgressLabel as Label
|
||||
@onready var button_container := %ButtonContainer as HBoxContainer
|
||||
@onready var update_button := %UpdateButton as Button
|
||||
@onready var skip_button := %SkipButton as Button
|
||||
@onready var store_button := %StoreButton as Button
|
||||
@onready var version_label := $VersionLabel as Label
|
||||
|
||||
var update_manager: Node
|
||||
var update_info: Dictionary = {}
|
||||
var main_scene_path := "res://scenes/main.tscn" # Your main game scene
|
||||
|
||||
func _ready() -> void:
|
||||
# Get or create the update manager
|
||||
update_manager = _get_update_manager()
|
||||
|
||||
# Connect signals
|
||||
update_manager.update_check_completed.connect(_on_update_check_completed)
|
||||
update_manager.update_check_failed.connect(_on_update_check_failed)
|
||||
update_manager.download_started.connect(_on_download_started)
|
||||
update_manager.download_progress.connect(_on_download_progress)
|
||||
update_manager.download_completed.connect(_on_download_completed)
|
||||
update_manager.download_failed.connect(_on_download_failed)
|
||||
update_manager.store_update_required.connect(_on_store_update_required)
|
||||
|
||||
# Connect buttons
|
||||
update_button.pressed.connect(_on_update_pressed)
|
||||
skip_button.pressed.connect(_on_skip_pressed)
|
||||
store_button.pressed.connect(_on_store_pressed)
|
||||
|
||||
# Show current version
|
||||
version_label.text = "v" + update_manager.current_version
|
||||
|
||||
# Start update check after a brief delay
|
||||
await get_tree().create_timer(0.5).timeout
|
||||
_check_for_updates()
|
||||
|
||||
func _get_update_manager() -> Node:
|
||||
# Try to get from autoload first
|
||||
if has_node("/root/GameUpdateManager"):
|
||||
return get_node("/root/GameUpdateManager")
|
||||
|
||||
# Otherwise, create instance
|
||||
var manager_script := load("res://scripts/managers/game_update_manager.gd")
|
||||
var manager := manager_script.new()
|
||||
manager.name = "GameUpdateManager"
|
||||
get_tree().root.add_child(manager)
|
||||
return manager
|
||||
|
||||
func _check_for_updates() -> void:
|
||||
status_label.text = "Checking for updates..."
|
||||
progress_container.visible = false
|
||||
button_container.visible = false
|
||||
|
||||
# On desktop without launcher, skip update check and go straight to game
|
||||
if update_manager.is_desktop() and not update_manager._launcher_available():
|
||||
# Quick check but don't block
|
||||
update_manager.check_for_updates()
|
||||
# Auto-proceed after short delay if on desktop
|
||||
await get_tree().create_timer(2.0).timeout
|
||||
if not update_info.get("has_update", false):
|
||||
_proceed_to_game()
|
||||
else:
|
||||
update_manager.check_for_updates()
|
||||
|
||||
func _on_update_check_completed(has_update: bool, info: Dictionary) -> void:
|
||||
update_info = info
|
||||
version_label.text = "v" + info.current_version
|
||||
|
||||
if has_update:
|
||||
if info.needs_store_update:
|
||||
status_label.text = "A required update is available.\nPlease update from the store."
|
||||
button_container.visible = true
|
||||
update_button.visible = false
|
||||
skip_button.visible = false
|
||||
store_button.visible = true
|
||||
elif info.can_patch:
|
||||
status_label.text = "Update available: v" + info.latest_version
|
||||
button_container.visible = true
|
||||
update_button.visible = true
|
||||
skip_button.visible = true
|
||||
store_button.visible = false
|
||||
else:
|
||||
# Desktop with launcher - just proceed
|
||||
status_label.text = "Update available via launcher"
|
||||
await get_tree().create_timer(1.5).timeout
|
||||
_proceed_to_game()
|
||||
else:
|
||||
status_label.text = "Game is up to date!"
|
||||
await get_tree().create_timer(1.0).timeout
|
||||
_proceed_to_game()
|
||||
|
||||
func _on_update_check_failed(error: String) -> void:
|
||||
status_label.text = "Could not check for updates"
|
||||
print("[BootScreen] Update check failed: ", error)
|
||||
|
||||
# Allow playing anyway
|
||||
await get_tree().create_timer(1.5).timeout
|
||||
_proceed_to_game()
|
||||
|
||||
func _on_update_pressed() -> void:
|
||||
button_container.visible = false
|
||||
status_label.text = "Downloading update..."
|
||||
update_manager.download_update()
|
||||
|
||||
func _on_skip_pressed() -> void:
|
||||
_proceed_to_game()
|
||||
|
||||
func _on_store_pressed() -> void:
|
||||
update_manager.open_store_page()
|
||||
|
||||
func _on_download_started(_total_size: int) -> void:
|
||||
progress_container.visible = true
|
||||
progress_bar.value = 0
|
||||
progress_label.text = "0%"
|
||||
|
||||
func _on_download_progress(_downloaded: int, _total: int, percentage: float) -> void:
|
||||
progress_bar.value = percentage
|
||||
progress_label.text = "%.0f%%" % percentage
|
||||
|
||||
func _on_download_completed() -> void:
|
||||
status_label.text = "Update installed!"
|
||||
progress_container.visible = false
|
||||
|
||||
# Reload the game to apply changes
|
||||
await get_tree().create_timer(1.0).timeout
|
||||
get_tree().reload_current_scene()
|
||||
|
||||
func _on_download_failed(error: String) -> void:
|
||||
status_label.text = "Update failed: " + error
|
||||
progress_container.visible = false
|
||||
button_container.visible = true
|
||||
update_button.text = "Retry"
|
||||
|
||||
func _on_store_update_required(store_url: String) -> void:
|
||||
status_label.text = "Please update from the store"
|
||||
button_container.visible = true
|
||||
update_button.visible = false
|
||||
skip_button.visible = false
|
||||
store_button.visible = true
|
||||
|
||||
func _proceed_to_game() -> void:
|
||||
# Load any previously downloaded patches
|
||||
_load_existing_patches()
|
||||
|
||||
# Change to main game scene
|
||||
get_tree().change_scene_to_file(main_scene_path)
|
||||
|
||||
func _load_existing_patches() -> void:
|
||||
# Load any PCK files from the patches directory
|
||||
var patches_dir := "user://patches/"
|
||||
var dir := DirAccess.open(patches_dir)
|
||||
if not dir:
|
||||
return
|
||||
|
||||
var patches: Array[String] = []
|
||||
dir.list_dir_begin()
|
||||
var file := dir.get_next()
|
||||
while file != "":
|
||||
if file.ends_with(".pck"):
|
||||
patches.append(file)
|
||||
file = dir.get_next()
|
||||
dir.list_dir_end()
|
||||
|
||||
# Sort patches by version (filename includes version)
|
||||
patches.sort()
|
||||
|
||||
# Load each patch in order
|
||||
for patch in patches:
|
||||
var patch_path := patches_dir + patch
|
||||
if ProjectSettings.load_resource_pack(patch_path, true):
|
||||
print("[BootScreen] Loaded patch: ", patch)
|
||||
else:
|
||||
push_warning("[BootScreen] Failed to load patch: ", patch)
|
||||
Reference in New Issue
Block a user