339 lines
11 KiB
GDScript
339 lines
11 KiB
GDScript
extends Control
|
|
## Main launcher scene script - orchestrates all launcher functionality
|
|
|
|
const Config = preload("config.gd")
|
|
const VersionChecker = preload("version_checker.gd")
|
|
const DownloadManager = preload("download_manager.gd")
|
|
const UpdateApplier = preload("update_applier.gd")
|
|
const NewsFetcher = preload("news_fetcher.gd")
|
|
|
|
# Child nodes (will be set up in _ready)
|
|
@onready var version_checker := $VersionChecker as VersionChecker
|
|
@onready var download_manager := $DownloadManager as DownloadManager
|
|
@onready var update_applier := $UpdateApplier as UpdateApplier
|
|
@onready var news_fetcher := $NewsFetcher as NewsFetcher
|
|
|
|
# UI Elements
|
|
@onready var play_button := $MainPanel/ContentContainer/BottomBar/PlayButton as Button
|
|
@onready var progress_bar := $MainPanel/ContentContainer/BottomBar/ProgressContainer/ProgressBar as ProgressBar
|
|
@onready var progress_label := $MainPanel/ContentContainer/BottomBar/ProgressContainer/ProgressLabel as Label
|
|
@onready var progress_container := $MainPanel/ContentContainer/BottomBar/ProgressContainer as Control
|
|
@onready var status_label := $MainPanel/ContentContainer/BottomBar/StatusLabel as Label
|
|
@onready var version_label := $MainPanel/TitleBar/VersionLabel as Label
|
|
@onready var news_container := $MainPanel/ContentContainer/TabContainer/News/ScrollContainer/NewsVBox as VBoxContainer
|
|
@onready var changelog_container := $MainPanel/ContentContainer/TabContainer/Changelog/ScrollContainer/ChangelogVBox as VBoxContainer
|
|
|
|
enum LauncherState {
|
|
CHECKING,
|
|
UP_TO_DATE,
|
|
UPDATE_AVAILABLE,
|
|
DOWNLOADING,
|
|
INSTALLING,
|
|
READY,
|
|
ERROR
|
|
}
|
|
|
|
var current_state: LauncherState = LauncherState.CHECKING
|
|
var pending_changelog: Array = []
|
|
|
|
func _ready() -> void:
|
|
_setup_signals()
|
|
_apply_theme()
|
|
_set_state(LauncherState.CHECKING)
|
|
|
|
# Start checking for updates
|
|
await get_tree().create_timer(0.5).timeout # Small delay for UI to initialize
|
|
version_checker.check_for_updates()
|
|
news_fetcher.fetch_news()
|
|
|
|
func _setup_signals() -> void:
|
|
# Version checker signals
|
|
version_checker.version_check_completed.connect(_on_version_check_completed)
|
|
version_checker.version_check_failed.connect(_on_version_check_failed)
|
|
|
|
# Download manager signals
|
|
download_manager.download_started.connect(_on_download_started)
|
|
download_manager.download_progress.connect(_on_download_progress)
|
|
download_manager.download_completed.connect(_on_download_completed)
|
|
download_manager.download_failed.connect(_on_download_failed)
|
|
|
|
# Update applier signals
|
|
update_applier.update_started.connect(_on_update_started)
|
|
update_applier.update_progress.connect(_on_update_progress)
|
|
update_applier.update_completed.connect(_on_update_completed)
|
|
update_applier.update_failed.connect(_on_update_failed)
|
|
|
|
# News fetcher signals
|
|
news_fetcher.news_fetched.connect(_on_news_fetched)
|
|
|
|
# Play button
|
|
play_button.pressed.connect(_on_play_pressed)
|
|
|
|
func _apply_theme() -> void:
|
|
# Apply theme colors from config
|
|
var bg_style := StyleBoxFlat.new()
|
|
bg_style.bg_color = Config.COLOR_BACKGROUND
|
|
add_theme_stylebox_override("panel", bg_style)
|
|
|
|
func _set_state(new_state: LauncherState) -> void:
|
|
current_state = new_state
|
|
|
|
match new_state:
|
|
LauncherState.CHECKING:
|
|
status_label.text = "Checking for updates..."
|
|
play_button.disabled = true
|
|
play_button.text = "CHECKING..."
|
|
progress_container.visible = false
|
|
|
|
LauncherState.UP_TO_DATE:
|
|
status_label.text = "Game is up to date!"
|
|
play_button.disabled = false
|
|
play_button.text = "► PLAY"
|
|
progress_container.visible = false
|
|
version_label.text = "v" + version_checker.current_version
|
|
|
|
LauncherState.UPDATE_AVAILABLE:
|
|
status_label.text = "Update available: v" + version_checker.latest_version
|
|
play_button.disabled = false
|
|
play_button.text = "⬇ UPDATE"
|
|
progress_container.visible = false
|
|
|
|
LauncherState.DOWNLOADING:
|
|
status_label.text = "Downloading update..."
|
|
play_button.disabled = true
|
|
play_button.text = "DOWNLOADING..."
|
|
progress_container.visible = true
|
|
progress_bar.value = 0
|
|
|
|
LauncherState.INSTALLING:
|
|
status_label.text = "Installing update..."
|
|
play_button.disabled = true
|
|
play_button.text = "INSTALLING..."
|
|
progress_container.visible = true
|
|
|
|
LauncherState.READY:
|
|
status_label.text = "Ready to play!"
|
|
play_button.disabled = false
|
|
play_button.text = "► PLAY"
|
|
progress_container.visible = false
|
|
version_label.text = "v" + version_checker.current_version
|
|
|
|
LauncherState.ERROR:
|
|
play_button.disabled = true
|
|
play_button.text = "ERROR"
|
|
progress_container.visible = false
|
|
|
|
# --- Version Check Callbacks ---
|
|
|
|
func _on_version_check_completed(has_update: bool, latest_version: String, changelog: Array) -> void:
|
|
pending_changelog = changelog
|
|
_populate_changelog(changelog)
|
|
|
|
if has_update:
|
|
_set_state(LauncherState.UPDATE_AVAILABLE)
|
|
else:
|
|
# Check if game is installed
|
|
if _is_game_installed():
|
|
_set_state(LauncherState.UP_TO_DATE)
|
|
else:
|
|
# Fresh install needed
|
|
status_label.text = "Game not installed - click UPDATE to download"
|
|
_set_state(LauncherState.UPDATE_AVAILABLE)
|
|
|
|
func _on_version_check_failed(error: String) -> void:
|
|
status_label.text = "Failed to check for updates: " + error
|
|
|
|
# Still allow playing if game is installed
|
|
if _is_game_installed():
|
|
_set_state(LauncherState.READY)
|
|
status_label.text = "Offline mode - " + error
|
|
else:
|
|
_set_state(LauncherState.ERROR)
|
|
|
|
# --- Download Callbacks ---
|
|
|
|
func _on_download_started(_file_name: String, _total_size: int) -> void:
|
|
_set_state(LauncherState.DOWNLOADING)
|
|
|
|
func _on_download_progress(_downloaded: int, _total: int, percentage: float) -> void:
|
|
progress_bar.value = percentage
|
|
progress_label.text = "%.1f%%" % percentage
|
|
|
|
func _on_download_completed(file_path: String) -> void:
|
|
_set_state(LauncherState.INSTALLING)
|
|
update_applier.apply_update(file_path, version_checker.latest_version)
|
|
|
|
func _on_download_failed(error: String) -> void:
|
|
status_label.text = "Download failed: " + error
|
|
_set_state(LauncherState.ERROR)
|
|
|
|
# --- Update Callbacks ---
|
|
|
|
func _on_update_started() -> void:
|
|
progress_bar.value = 0
|
|
|
|
func _on_update_progress(step: String, percentage: float) -> void:
|
|
progress_bar.value = percentage
|
|
progress_label.text = step
|
|
|
|
func _on_update_completed() -> void:
|
|
version_checker.save_version(version_checker.latest_version)
|
|
_set_state(LauncherState.READY)
|
|
status_label.text = "Update complete! Ready to play."
|
|
|
|
func _on_update_failed(error: String) -> void:
|
|
status_label.text = "Update failed: " + error
|
|
_set_state(LauncherState.ERROR)
|
|
|
|
# --- News Callbacks ---
|
|
|
|
func _on_news_fetched(news_items: Array) -> void:
|
|
_populate_news(news_items)
|
|
|
|
# --- UI Helpers ---
|
|
|
|
func _populate_changelog(changelog: Array) -> void:
|
|
# Clear existing
|
|
for child in changelog_container.get_children():
|
|
child.queue_free()
|
|
|
|
if changelog.is_empty():
|
|
var label := Label.new()
|
|
label.text = "No changelog available"
|
|
label.add_theme_color_override("font_color", Config.COLOR_TEXT_DIM)
|
|
changelog_container.add_child(label)
|
|
return
|
|
|
|
for entry in changelog:
|
|
var version_label_item := Label.new()
|
|
version_label_item.text = "Version " + entry.get("version", "?") + " - " + entry.get("date", "")
|
|
version_label_item.add_theme_color_override("font_color", Config.COLOR_PRIMARY)
|
|
version_label_item.add_theme_font_size_override("font_size", 16)
|
|
changelog_container.add_child(version_label_item)
|
|
|
|
var changes: Array = entry.get("changes", [])
|
|
for change in changes:
|
|
var change_label := Label.new()
|
|
change_label.text = " • " + str(change)
|
|
change_label.add_theme_color_override("font_color", Config.COLOR_TEXT)
|
|
changelog_container.add_child(change_label)
|
|
|
|
# Spacer
|
|
var spacer := Control.new()
|
|
spacer.custom_minimum_size = Vector2(0, 10)
|
|
changelog_container.add_child(spacer)
|
|
|
|
func _populate_news(news_items: Array) -> void:
|
|
# Clear existing
|
|
for child in news_container.get_children():
|
|
child.queue_free()
|
|
|
|
if news_items.is_empty():
|
|
var label := Label.new()
|
|
label.text = "No news available"
|
|
label.add_theme_color_override("font_color", Config.COLOR_TEXT_DIM)
|
|
news_container.add_child(label)
|
|
return
|
|
|
|
for item in news_items:
|
|
# News card panel
|
|
var card := PanelContainer.new()
|
|
var card_style := StyleBoxFlat.new()
|
|
card_style.bg_color = Config.COLOR_SURFACE
|
|
card_style.set_corner_radius_all(8)
|
|
card_style.set_content_margin_all(12)
|
|
card.add_theme_stylebox_override("panel", card_style)
|
|
|
|
var vbox := VBoxContainer.new()
|
|
card.add_child(vbox)
|
|
|
|
# Title
|
|
var title := Label.new()
|
|
title.text = item.get("title", "Untitled")
|
|
title.add_theme_color_override("font_color", Config.COLOR_PRIMARY)
|
|
title.add_theme_font_size_override("font_size", 16)
|
|
vbox.add_child(title)
|
|
|
|
# Date
|
|
var date := Label.new()
|
|
date.text = item.get("date", "")
|
|
date.add_theme_color_override("font_color", Config.COLOR_TEXT_DIM)
|
|
date.add_theme_font_size_override("font_size", 12)
|
|
vbox.add_child(date)
|
|
|
|
# Content
|
|
var content := Label.new()
|
|
content.text = item.get("content", "")
|
|
content.add_theme_color_override("font_color", Config.COLOR_TEXT)
|
|
content.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
vbox.add_child(content)
|
|
|
|
news_container.add_child(card)
|
|
|
|
# Spacer
|
|
var spacer := Control.new()
|
|
spacer.custom_minimum_size = Vector2(0, 8)
|
|
news_container.add_child(spacer)
|
|
|
|
func _is_game_installed() -> bool:
|
|
var pck_path := Config.GAME_DIRECTORY + Config.get_game_pck()
|
|
return FileAccess.file_exists(pck_path)
|
|
|
|
func _on_play_pressed() -> void:
|
|
match current_state:
|
|
LauncherState.UPDATE_AVAILABLE:
|
|
# Start download
|
|
var download_info := version_checker.get_download_info()
|
|
if download_info.get("url", "") != "":
|
|
download_manager.download_file(
|
|
download_info.url,
|
|
Config.get_game_pck(),
|
|
download_info.get("size", 0),
|
|
download_info.get("checksum_md5", ""),
|
|
download_info.get("checksum_sha256", "")
|
|
)
|
|
else:
|
|
status_label.text = "Error: No download URL available"
|
|
_set_state(LauncherState.ERROR)
|
|
|
|
LauncherState.UP_TO_DATE, LauncherState.READY:
|
|
_launch_game()
|
|
|
|
func _launch_game() -> void:
|
|
var game_executable := Config.get_game_executable()
|
|
var game_path := ProjectSettings.globalize_path(Config.GAME_DIRECTORY + game_executable)
|
|
|
|
if not FileAccess.file_exists(Config.GAME_DIRECTORY + game_executable):
|
|
# Try to find the executable in the same directory as launcher
|
|
var launcher_dir := OS.get_executable_path().get_base_dir()
|
|
game_path = launcher_dir + "/" + game_executable
|
|
|
|
print("[Launcher] Platform: ", Config.get_platform_name())
|
|
print("[Launcher] Launching game: ", game_path)
|
|
|
|
var args: PackedStringArray = []
|
|
var pid: int = -1
|
|
|
|
# Platform-specific launch handling
|
|
match Config.get_current_platform():
|
|
Config.Platform.MACOS:
|
|
# macOS: Use 'open' command for .app bundles
|
|
if game_executable.ends_with(".app"):
|
|
pid = OS.create_process("open", PackedStringArray(["-a", game_path]))
|
|
else:
|
|
pid = OS.create_process(game_path, args)
|
|
Config.Platform.LINUX:
|
|
# Linux: May need to set executable permission
|
|
OS.execute("chmod", PackedStringArray(["+x", game_path]))
|
|
pid = OS.create_process(game_path, args)
|
|
_:
|
|
# Windows and others
|
|
pid = OS.create_process(game_path, args)
|
|
|
|
if pid > 0:
|
|
# Successfully launched, close launcher
|
|
get_tree().quit()
|
|
else:
|
|
status_label.text = "Failed to launch game"
|
|
_set_state(LauncherState.ERROR)
|