From 58d28366bb925157695f0077a51afbf39bd0b0cf Mon Sep 17 00:00:00 2001 From: adtpdn Date: Wed, 10 Dec 2025 02:35:17 +0800 Subject: [PATCH] init --- docs/MOBILE_UPDATES.md | 93 +++++++ launcher/README.md | 124 +++++++++ launcher/icon.svg | 27 ++ launcher/project.godot | 27 ++ launcher/scenes/launcher.tscn | 206 +++++++++++++++ launcher/scripts/config.gd | 72 +++++ launcher/scripts/download_manager.gd | 133 ++++++++++ launcher/scripts/launcher_main.gd | 332 ++++++++++++++++++++++++ launcher/scripts/news_fetcher.gd | 68 +++++ launcher/scripts/update_applier.gd | 162 ++++++++++++ launcher/scripts/version_checker.gd | 140 ++++++++++ launcher/server/version.json | 63 +++++ project.godot | 1 + scenes/boot_screen.tscn | 101 +++++++ scripts/managers/game_update_manager.gd | 309 ++++++++++++++++++++++ scripts/ui/boot_screen.gd | 181 +++++++++++++ tiles_armagedon_a1.res | Bin 11257 -> 0 bytes tools/create_patch.ps1 | 77 ++++++ version.txt | 1 + 19 files changed, 2117 insertions(+) create mode 100644 docs/MOBILE_UPDATES.md create mode 100644 launcher/README.md create mode 100644 launcher/icon.svg create mode 100644 launcher/project.godot create mode 100644 launcher/scenes/launcher.tscn create mode 100644 launcher/scripts/config.gd create mode 100644 launcher/scripts/download_manager.gd create mode 100644 launcher/scripts/launcher_main.gd create mode 100644 launcher/scripts/news_fetcher.gd create mode 100644 launcher/scripts/update_applier.gd create mode 100644 launcher/scripts/version_checker.gd create mode 100644 launcher/server/version.json create mode 100644 scenes/boot_screen.tscn create mode 100644 scripts/managers/game_update_manager.gd create mode 100644 scripts/ui/boot_screen.gd delete mode 100644 tiles_armagedon_a1.res create mode 100644 tools/create_patch.ps1 create mode 100644 version.txt diff --git a/docs/MOBILE_UPDATES.md b/docs/MOBILE_UPDATES.md new file mode 100644 index 0000000..2f6d4a0 --- /dev/null +++ b/docs/MOBILE_UPDATES.md @@ -0,0 +1,93 @@ +# Mobile Update System + +This document explains how the in-game update system works for mobile platforms (Android/iOS). + +## How It Works + +``` +┌─────────────────────────────────────────────────────────────┐ +│ BOOT SCREEN │ +│ ┌─────────────────┐ │ +│ │ Check for │──→ No update? ──→ Proceed to game │ +│ │ updates │ │ +│ └────────┬────────┘ │ +│ │ │ +│ ▼ Update available │ +│ ┌─────────────────────────────────────┐ │ +│ │ Can patch in-app? │ │ +│ │ YES → Show "Update Now" button │ │ +│ │ NO → Show "Open Store" button │ │ +│ └─────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Platform Behavior + +| Platform | Update Method | Notes | +|----------|---------------|-------| +| Windows | External launcher | Use TektonLauncher.exe | +| Linux | External launcher | Use TektonLauncher | +| macOS | External launcher | Use TektonLauncher.app | +| Android | In-game patching | Downloads PCK files | +| iOS | In-game patching | Downloads PCK files | +| Web | N/A | Always uses latest deployed version | + +## Key Files + +- `scripts/managers/game_update_manager.gd` - Core update logic +- `scripts/ui/boot_screen.gd` - Update UI controller +- `scenes/boot_screen.tscn` - Boot screen scene + +## Setting Up + +### 1. Set Boot Screen as Main Scene (for mobile builds) + +For mobile exports, you may want to use `boot_screen.tscn` as the main scene: +- Go to Project > Project Settings > Application > Run +- Set `Main Scene` to `res://scenes/boot_screen.tscn` + +Alternatively, keep your current main scene and call the update manager manually. + +### 2. Configure URLs + +Edit `scripts/managers/game_update_manager.gd`: + +```gdscript +const VERSION_MANIFEST_URL := "https://your-server.com/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" +``` + +### 3. Using minimum_app_version + +In `version.json`, set `minimum_app_version` to force store updates: + +```json +{ + "latest_version": "1.2.0", + "minimum_app_version": "1.1.0" +} +``` + +If a user has version 1.0.0, they'll be directed to the store since in-app patching can't upgrade from below the minimum. + +## Creating Mobile Patches + +1. Export your game changes as a PCK file from Godot +2. Upload to your hosting (same as desktop patches) +3. Update `version.json` with `pck_android` and `pck_ios` entries + +```json +"pck_android": { + "url": "https://your-server.com/patches/tekton-1.2.0-android.pck", + "size": 5242880, + "checksum_md5": "abc123..." +} +``` + +## Limitations + +- **Cannot update native code** - Engine/plugin changes require store update +- **Cannot update main executable** - Only assets and GDScript +- **Storage space** - Patches accumulate in user://patches/ +- **iOS App Store rules** - Be careful about downloading executable content diff --git a/launcher/README.md b/launcher/README.md new file mode 100644 index 0000000..7b9b29b --- /dev/null +++ b/launcher/README.md @@ -0,0 +1,124 @@ +# Tekton Launcher + +A custom game launcher for **tekton-local** with auto-update functionality via itch.io. + +## Features + +- **Version Checking** - Automatically checks for game updates on launch +- **Progress Tracking** - Visual download progress with percentage +- **Checksum Verification** - MD5/SHA256 validation for downloaded files +- **Backup System** - Automatic backup before updates with rollback support +- **News Feed** - Displays devlog and announcements from itch.io +- **Changelog Display** - Shows what's new in each version + +## Project Structure + +``` +launcher/ +├── project.godot # Godot 4.4 project file +├── icon.svg # Launcher icon +├── scenes/ +│ └── launcher.tscn # Main launcher scene +├── scripts/ +│ ├── config.gd # Global configuration (URLs, colors) +│ ├── launcher_main.gd # Main orchestration script +│ ├── version_checker.gd # Version comparison and manifest fetching +│ ├── download_manager.gd # HTTP downloads with progress +│ ├── update_applier.gd # Backup, install, and rollback +│ └── news_fetcher.gd # News/devlog fetching +└── server/ + └── version.json # Example version manifest +``` + +## Setup + +### 1. Configure itch.io URLs + +Edit `scripts/config.gd` and update: + +```gdscript +const ITCH_GAME_URL := "https://your-username.itch.io/tekton-local" +const VERSION_MANIFEST_URL := "https://your-username.itch.io/tekton-local/data/version.json" +``` + +### 2. Host version.json on itch.io + +Upload the `server/version.json` file to itch.io alongside your game files. Update it whenever you release a new version. + +### 3. Export the Launcher + +1. Open `launcher/project.godot` in Godot 4.4 +2. Go to **Project > Export...** +3. Add a Windows Desktop preset +4. Export as `TektonLauncher.exe` + +### 4. Creating Game Patches + +Use the PowerShell script in the main project: + +```powershell +.\tools\create_patch.ps1 -Version "1.0.0" +``` + +This will: +- Update `version.txt` in your game +- Generate checksums for the PCK file +- Create a JSON snippet to add to `version.json` + +## Workflow for Releasing Updates + +1. Make changes to the main game +2. Update `version.txt` with the new version number +3. Export the game as PCK only from Godot +4. Run `create_patch.ps1` to generate checksums +5. Upload the new PCK to itch.io +6. Update `version.json` with the new release entry +7. Upload updated `version.json` to itch.io + +Players will see the update next time they launch the game! + +## UI Theme + +The launcher features a cyberpunk-inspired dark theme: +- Primary: Cyan (#00d4ff) +- Secondary: Magenta (#ff00aa) +- Background: Dark blue-black (#0a0a1a) + +Customize colors in `scripts/config.gd`. + +## Cross-Platform Support + +The launcher automatically detects the operating system and: + +| Platform | Executable | PCK File | Launch Method | +|----------|------------|----------|---------------| +| Windows | `tekton-local.exe` | `tekton-local-windows.pck` | Direct process | +| Linux | `tekton-local.x86_64` | `tekton-local-linux.pck` | chmod +x, then launch | +| macOS | `tekton-local.app` | `tekton-local-macos.pck` | `open -a` command | + +### Multi-Platform Releases + +When releasing for multiple platforms, use platform-specific keys in `version.json`: + +```json +{ + "version": "1.0.0", + "pck_windows": { + "url": "https://..../tekton-local-1.0.0-windows.pck", + "size": 52428800, + "checksum_md5": "..." + }, + "pck_linux": { + "url": "https://..../tekton-local-1.0.0-linux.pck", + "size": 52428800, + "checksum_md5": "..." + }, + "pck_macos": { + "url": "https://..../tekton-local-1.0.0-macos.pck", + "size": 52428800, + "checksum_md5": "..." + } +} +``` + +The launcher will automatically download the correct PCK for the current platform. diff --git a/launcher/icon.svg b/launcher/icon.svg new file mode 100644 index 0000000..d10f9ee --- /dev/null +++ b/launcher/icon.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/launcher/project.godot b/launcher/project.godot new file mode 100644 index 0000000..0a24675 --- /dev/null +++ b/launcher/project.godot @@ -0,0 +1,27 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Tekton Launcher" +run/main_scene="res://scenes/launcher.tscn" +config/features=PackedStringArray("4.4", "Forward Plus") +config/icon="res://icon.svg" + +[autoload] + +Config="*res://scripts/config.gd" + +[display] + +window/size/viewport_width=900 +window/size/viewport_height=600 +window/size/resizable=false +window/stretch/mode="viewport" diff --git a/launcher/scenes/launcher.tscn b/launcher/scenes/launcher.tscn new file mode 100644 index 0000000..0d92918 --- /dev/null +++ b/launcher/scenes/launcher.tscn @@ -0,0 +1,206 @@ +[gd_scene load_steps=7 format=3 uid="uid://launcher_main"] + +[ext_resource type="Script" path="res://scripts/launcher_main.gd" id="1"] +[ext_resource type="Script" path="res://scripts/version_checker.gd" id="2"] +[ext_resource type="Script" path="res://scripts/download_manager.gd" id="3"] +[ext_resource type="Script" path="res://scripts/update_applier.gd" id="4"] +[ext_resource type="Script" path="res://scripts/news_fetcher.gd" id="5"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bg"] +bg_color = Color(0.039, 0.039, 0.102, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_panel"] +bg_color = Color(0.078, 0.078, 0.157, 1) +corner_radius_top_left = 12 +corner_radius_top_right = 12 +corner_radius_bottom_right = 12 +corner_radius_bottom_left = 12 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_titlebar"] +bg_color = Color(0.059, 0.059, 0.118, 1) +corner_radius_top_left = 12 +corner_radius_top_right = 12 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_button"] +bg_color = Color(0, 0.831, 1, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_button_hover"] +bg_color = Color(0.2, 0.9, 1, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_progress_bg"] +bg_color = Color(0.1, 0.1, 0.2, 1) +corner_radius_top_left = 4 +corner_radius_top_right = 4 +corner_radius_bottom_right = 4 +corner_radius_bottom_left = 4 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_progress_fill"] +bg_color = Color(0, 0.831, 1, 1) +corner_radius_top_left = 4 +corner_radius_top_right = 4 +corner_radius_bottom_right = 4 +corner_radius_bottom_left = 4 + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_tab_panel"] +bg_color = Color(0.059, 0.059, 0.118, 1) +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 +content_margin_left = 8.0 +content_margin_top = 8.0 +content_margin_right = 8.0 +content_margin_bottom = 8.0 + +[node name="Launcher" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_styles/panel = SubResource("StyleBoxFlat_bg") +script = ExtResource("1") + +[node name="Background" type="ColorRect" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0.039, 0.039, 0.102, 1) + +[node name="VersionChecker" type="Node" parent="."] +script = ExtResource("2") + +[node name="DownloadManager" type="Node" parent="."] +script = ExtResource("3") + +[node name="UpdateApplier" type="Node" parent="."] +script = ExtResource("4") + +[node name="NewsFetcher" type="Node" parent="."] +script = ExtResource("5") + +[node name="MainPanel" type="PanelContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 20.0 +offset_top = 20.0 +offset_right = -20.0 +offset_bottom = -20.0 +theme_override_styles/panel = SubResource("StyleBoxFlat_panel") + +[node name="TitleBar" type="PanelContainer" parent="MainPanel"] +layout_mode = 2 +size_flags_vertical = 0 +custom_minimum_size = Vector2(0, 60) +theme_override_styles/panel = SubResource("StyleBoxFlat_titlebar") + +[node name="HBoxContainer" type="HBoxContainer" parent="MainPanel/TitleBar"] +layout_mode = 2 +offset_left = 16.0 +offset_right = -16.0 + +[node name="TitleLabel" type="Label" parent="MainPanel/TitleBar/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "TEKTON LAUNCHER" +theme_override_colors/font_color = Color(0, 0.831, 1, 1) +theme_override_font_sizes/font_size = 24 + +[node name="VersionLabel" type="Label" parent="MainPanel/TitleBar"] +layout_mode = 2 +offset_left = 750.0 +offset_top = 20.0 +offset_right = 840.0 +offset_bottom = 40.0 +text = "v0.0.0" +theme_override_colors/font_color = Color(0.533, 0.533, 0.6, 1) +horizontal_alignment = 2 + +[node name="ContentContainer" type="VBoxContainer" parent="MainPanel"] +layout_mode = 2 +offset_top = 60.0 + +[node name="TabContainer" type="TabContainer" parent="MainPanel/ContentContainer"] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_styles/panel = SubResource("StyleBoxFlat_tab_panel") +current_tab = 0 + +[node name="News" type="Control" parent="MainPanel/ContentContainer/TabContainer"] +layout_mode = 2 +metadata/_tab_index = 0 + +[node name="ScrollContainer" type="ScrollContainer" parent="MainPanel/ContentContainer/TabContainer/News"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 + +[node name="NewsVBox" type="VBoxContainer" parent="MainPanel/ContentContainer/TabContainer/News/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Changelog" type="Control" parent="MainPanel/ContentContainer/TabContainer"] +visible = false +layout_mode = 2 +metadata/_tab_index = 1 + +[node name="ScrollContainer" type="ScrollContainer" parent="MainPanel/ContentContainer/TabContainer/Changelog"] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 + +[node name="ChangelogVBox" type="VBoxContainer" parent="MainPanel/ContentContainer/TabContainer/Changelog/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="BottomBar" type="HBoxContainer" parent="MainPanel/ContentContainer"] +layout_mode = 2 +custom_minimum_size = Vector2(0, 80) +alignment = 2 + +[node name="StatusLabel" type="Label" parent="MainPanel/ContentContainer/BottomBar"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Checking for updates..." +theme_override_colors/font_color = Color(0.533, 0.533, 0.6, 1) + +[node name="ProgressContainer" type="VBoxContainer" parent="MainPanel/ContentContainer/BottomBar"] +visible = false +layout_mode = 2 +custom_minimum_size = Vector2(200, 0) + +[node name="ProgressBar" type="ProgressBar" parent="MainPanel/ContentContainer/BottomBar/ProgressContainer"] +layout_mode = 2 +custom_minimum_size = Vector2(200, 20) +theme_override_styles/background = SubResource("StyleBoxFlat_progress_bg") +theme_override_styles/fill = SubResource("StyleBoxFlat_progress_fill") +show_percentage = false + +[node name="ProgressLabel" type="Label" parent="MainPanel/ContentContainer/BottomBar/ProgressContainer"] +layout_mode = 2 +text = "0%" +theme_override_colors/font_color = Color(0.533, 0.533, 0.6, 1) +horizontal_alignment = 1 + +[node name="PlayButton" type="Button" parent="MainPanel/ContentContainer/BottomBar"] +layout_mode = 2 +custom_minimum_size = Vector2(160, 50) +theme_override_colors/font_color = Color(0.039, 0.039, 0.102, 1) +theme_override_font_sizes/font_size = 18 +theme_override_styles/normal = SubResource("StyleBoxFlat_button") +theme_override_styles/hover = SubResource("StyleBoxFlat_button_hover") +text = "► PLAY" diff --git a/launcher/scripts/config.gd b/launcher/scripts/config.gd new file mode 100644 index 0000000..4cfcf3a --- /dev/null +++ b/launcher/scripts/config.gd @@ -0,0 +1,72 @@ +extends Node +## Global configuration for the Tekton Launcher + +# itch.io Configuration +const ITCH_GAME_URL := "https://your-username.itch.io/tekton-local" +const ITCH_DEVLOG_URL := "https://itch.io/api/1/your-key/game/your-game-id/devlog" # Replace with actual + +# Version manifest URL (hosted on itch.io or alongside game files) +const VERSION_MANIFEST_URL := "https://your-username.itch.io/tekton-local/data/version.json" + +# Local paths (shared across platforms) +const GAME_DIRECTORY := "user://game/" +const LOCAL_VERSION_FILE := "user://version.txt" +const BACKUP_DIRECTORY := "user://backup/" + +# Platform detection +enum Platform { WINDOWS, LINUX, MACOS } + +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 + _: + push_warning("Unknown platform: " + os_name + ", defaulting to Linux") + return Platform.LINUX + +static func get_game_executable() -> String: + match get_current_platform(): + Platform.WINDOWS: + return "tekton-local.exe" + Platform.LINUX: + return "tekton-local.x86_64" + Platform.MACOS: + return "tekton-local.app" + return "tekton-local" + +static func get_game_pck() -> String: + # PCK is the same across platforms, but we include platform in name for clarity + match get_current_platform(): + Platform.WINDOWS: + return "tekton-local-windows.pck" + Platform.LINUX: + return "tekton-local-linux.pck" + Platform.MACOS: + return "tekton-local-macos.pck" + return "tekton-local.pck" + +static func get_platform_name() -> String: + match get_current_platform(): + Platform.WINDOWS: + return "windows" + Platform.LINUX: + return "linux" + Platform.MACOS: + return "macos" + return "unknown" + +# UI Colors (Cyberpunk/Tech theme matching game aesthetic) +const COLOR_PRIMARY := Color("#00d4ff") # Cyan +const COLOR_SECONDARY := Color("#ff00aa") # Magenta +const COLOR_BACKGROUND := Color("#0a0a1a") # Dark blue-black +const COLOR_SURFACE := Color("#141428") # Slightly lighter +const COLOR_SUCCESS := Color("#00ff88") # Green +const COLOR_WARNING := Color("#ffaa00") # Orange +const COLOR_ERROR := Color("#ff4444") # Red +const COLOR_TEXT := Color("#ffffff") # White +const COLOR_TEXT_DIM := Color("#888899") # Dim text diff --git a/launcher/scripts/download_manager.gd b/launcher/scripts/download_manager.gd new file mode 100644 index 0000000..c0080b9 --- /dev/null +++ b/launcher/scripts/download_manager.gd @@ -0,0 +1,133 @@ +extends Node +class_name DownloadManager +## Handles downloading game files with progress tracking and checksum verification + +signal download_started(file_name: String, total_size: int) +signal download_progress(downloaded: int, total: int, percentage: float) +signal download_completed(file_path: String) +signal download_failed(error: String) + +var http_request: HTTPRequest +var download_path: String = "" +var expected_checksum_md5: String = "" +var expected_checksum_sha256: String = "" +var total_size: int = 0 +var is_downloading: bool = false + +func _ready() -> void: + http_request = HTTPRequest.new() + add_child(http_request) + http_request.request_completed.connect(_on_download_completed) + http_request.download_file = "" # We'll set this per download + + # Ensure directories exist + _ensure_directories() + +func _ensure_directories() -> void: + var dir := DirAccess.open("user://") + if dir: + if not dir.dir_exists("game"): + dir.make_dir("game") + if not dir.dir_exists("backup"): + dir.make_dir("backup") + if not dir.dir_exists("temp"): + dir.make_dir("temp") + +func _process(_delta: float) -> void: + if is_downloading and http_request: + var downloaded := http_request.get_downloaded_bytes() + var body_size := http_request.get_body_size() + + if body_size > 0: + var percentage := (float(downloaded) / float(body_size)) * 100.0 + emit_signal("download_progress", downloaded, body_size, percentage) + +func download_file(url: String, file_name: String, size: int = 0, md5: String = "", sha256: String = "") -> void: + if is_downloading: + emit_signal("download_failed", "Download already in progress") + return + + download_path = "user://temp/" + file_name + expected_checksum_md5 = md5 + expected_checksum_sha256 = sha256 + total_size = size + is_downloading = true + + # Set download file path + http_request.download_file = download_path + + print("[DownloadManager] Starting download: ", url) + print("[DownloadManager] Saving to: ", download_path) + + emit_signal("download_started", file_name, size) + + var error := http_request.request(url) + if error != OK: + is_downloading = false + emit_signal("download_failed", "Failed to initiate download: " + str(error)) + +func _on_download_completed(result: int, response_code: int, _headers: PackedStringArray, _body: PackedByteArray) -> void: + is_downloading = false + + if result != HTTPRequest.RESULT_SUCCESS: + emit_signal("download_failed", "Download failed with result: " + str(result)) + return + + if response_code != 200: + emit_signal("download_failed", "Server returned error: " + str(response_code)) + return + + # Verify checksum if provided + if expected_checksum_md5 != "" or expected_checksum_sha256 != "": + if not _verify_checksum(): + emit_signal("download_failed", "Checksum verification failed - file may be corrupted") + # Delete corrupted file + var dir := DirAccess.open("user://temp/") + if dir: + dir.remove(download_path.get_file()) + return + + print("[DownloadManager] Download completed: ", download_path) + emit_signal("download_completed", download_path) + +func _verify_checksum() -> bool: + ## Verify the downloaded file's checksum + var file := FileAccess.open(download_path, FileAccess.READ) + if not file: + print("[DownloadManager] Could not open file for checksum verification") + return false + + var content := file.get_buffer(file.get_length()) + file.close() + + # Verify MD5 if provided + if expected_checksum_md5 != "": + var ctx := HashingContext.new() + ctx.start(HashingContext.HASH_MD5) + ctx.update(content) + var calculated_md5 := ctx.finish().hex_encode() + + if calculated_md5.to_lower() != expected_checksum_md5.to_lower(): + print("[DownloadManager] MD5 mismatch! Expected: ", expected_checksum_md5, " Got: ", calculated_md5) + return false + print("[DownloadManager] MD5 checksum verified") + + # Verify SHA256 if provided + if expected_checksum_sha256 != "": + var ctx := HashingContext.new() + ctx.start(HashingContext.HASH_SHA256) + ctx.update(content) + var calculated_sha256 := ctx.finish().hex_encode() + + if calculated_sha256.to_lower() != expected_checksum_sha256.to_lower(): + print("[DownloadManager] SHA256 mismatch! Expected: ", expected_checksum_sha256, " Got: ", calculated_sha256) + return false + print("[DownloadManager] SHA256 checksum verified") + + return true + +func cancel_download() -> void: + if is_downloading: + http_request.cancel_request() + is_downloading = false + print("[DownloadManager] Download cancelled") diff --git a/launcher/scripts/launcher_main.gd b/launcher/scripts/launcher_main.gd new file mode 100644 index 0000000..150c417 --- /dev/null +++ b/launcher/scripts/launcher_main.gd @@ -0,0 +1,332 @@ +extends Control +## Main launcher scene script - orchestrates all launcher functionality + +# 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) diff --git a/launcher/scripts/news_fetcher.gd b/launcher/scripts/news_fetcher.gd new file mode 100644 index 0000000..2f7b60e --- /dev/null +++ b/launcher/scripts/news_fetcher.gd @@ -0,0 +1,68 @@ +extends Node +class_name NewsFetcher +## Fetches news/devlog entries from itch.io + +signal news_fetched(news_items: Array) +signal news_fetch_failed(error: String) + +var http_request: HTTPRequest + +# itch.io doesn't have a public devlog API, so we'll fetch from a custom endpoint +# You can host this JSON alongside your game files on itch.io + +func _ready() -> void: + http_request = HTTPRequest.new() + add_child(http_request) + http_request.request_completed.connect(_on_request_completed) + +func fetch_news() -> void: + # Fetch from the version manifest which includes news + print("[NewsFetcher] Fetching news from manifest...") + var error := http_request.request(Config.VERSION_MANIFEST_URL) + if error != OK: + emit_signal("news_fetch_failed", "Failed to initiate news request") + +func _on_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void: + if result != HTTPRequest.RESULT_SUCCESS: + emit_signal("news_fetch_failed", "Network error fetching news") + return + + if response_code != 200: + emit_signal("news_fetch_failed", "Server returned error: " + str(response_code)) + return + + var json := JSON.new() + var parse_result := json.parse(body.get_string_from_utf8()) + if parse_result != OK: + emit_signal("news_fetch_failed", "Failed to parse news data") + return + + var data: Dictionary = json.data + var news_items: Array = data.get("news", []) + + # Also include recent releases as news items + var releases: Array = data.get("releases", []) + for release in releases: + news_items.append({ + "title": "Version " + release.get("version", "?") + " Released!", + "date": release.get("date", ""), + "content": _format_changelog(release.get("changelog", [])), + "type": "release" + }) + + # Sort by date descending + news_items.sort_custom(_sort_by_date) + + print("[NewsFetcher] Fetched ", news_items.size(), " news items") + emit_signal("news_fetched", news_items) + +func _format_changelog(changelog: Array) -> String: + var text := "" + for item in changelog: + text += "• " + str(item) + "\n" + return text.strip_edges() + +func _sort_by_date(a: Dictionary, b: Dictionary) -> bool: + var date_a: String = a.get("date", "") + var date_b: String = b.get("date", "") + return date_a > date_b # Descending order diff --git a/launcher/scripts/update_applier.gd b/launcher/scripts/update_applier.gd new file mode 100644 index 0000000..53a4ab2 --- /dev/null +++ b/launcher/scripts/update_applier.gd @@ -0,0 +1,162 @@ +extends Node +class_name UpdateApplier +## Handles applying downloaded updates: backup, replace, rollback + +signal update_started +signal update_progress(step: String, percentage: float) +signal update_completed +signal update_failed(error: String) +signal rollback_completed + +const BACKUP_COUNT := 2 # Keep this many backup versions + +func apply_update(downloaded_pck_path: String, version: String) -> void: + emit_signal("update_started") + + var game_dir := Config.GAME_DIRECTORY + var pck_name := Config.get_game_pck() + var target_pck := game_dir + pck_name + var backup_dir := Config.BACKUP_DIRECTORY + + # Step 1: Ensure game directory exists + emit_signal("update_progress", "Preparing directories...", 10.0) + if not _ensure_directory(game_dir): + emit_signal("update_failed", "Failed to create game directory") + return + + if not _ensure_directory(backup_dir): + emit_signal("update_failed", "Failed to create backup directory") + return + + # Step 2: Backup existing PCK if it exists + emit_signal("update_progress", "Backing up current version...", 30.0) + if FileAccess.file_exists(target_pck): + var backup_name := pck_name.get_basename() + "_backup_" + Time.get_datetime_string_from_system().replace(":", "-") + ".pck" + var backup_path := backup_dir + backup_name + + if not _copy_file(target_pck, backup_path): + emit_signal("update_failed", "Failed to backup current version") + return + + # Clean up old backups + _cleanup_old_backups(backup_dir, BACKUP_COUNT) + + # Step 3: Move downloaded file to game directory + emit_signal("update_progress", "Installing update...", 60.0) + + # Delete old PCK first + if FileAccess.file_exists(target_pck): + var dir := DirAccess.open(game_dir) + if dir: + var err := dir.remove(pck_name) + if err != OK: + emit_signal("update_failed", "Failed to remove old game file") + return + + # Copy new PCK + if not _copy_file(downloaded_pck_path, target_pck): + emit_signal("update_failed", "Failed to install update") + return + + # Step 4: Copy executable if needed (first install) + emit_signal("update_progress", "Finalizing...", 80.0) + + # Clean up temp file + var temp_dir := DirAccess.open("user://temp/") + if temp_dir: + temp_dir.remove(downloaded_pck_path.get_file()) + + emit_signal("update_progress", "Update complete!", 100.0) + emit_signal("update_completed") + +func _ensure_directory(path: String) -> bool: + var dir := DirAccess.open("user://") + if dir: + var relative_path := path.replace("user://", "") + if not dir.dir_exists(relative_path): + return dir.make_dir_recursive(relative_path) == OK + return true + return false + +func _copy_file(from: String, to: String) -> bool: + var source := FileAccess.open(from, FileAccess.READ) + if not source: + push_error("[UpdateApplier] Cannot open source file: " + from) + return false + + var dest := FileAccess.open(to, FileAccess.WRITE) + if not dest: + source.close() + push_error("[UpdateApplier] Cannot open destination file: " + to) + return false + + # Copy in chunks for large files + const CHUNK_SIZE := 1024 * 1024 # 1MB chunks + while source.get_position() < source.get_length(): + var chunk := source.get_buffer(CHUNK_SIZE) + dest.store_buffer(chunk) + + source.close() + dest.close() + + print("[UpdateApplier] Copied: ", from, " -> ", to) + return true + +func _cleanup_old_backups(backup_dir: String, keep_count: int) -> void: + var dir := DirAccess.open(backup_dir) + if not dir: + return + + var backups: Array[String] = [] + dir.list_dir_begin() + var file := dir.get_next() + while file != "": + if file.ends_with(".pck") and "backup" in file: + backups.append(file) + file = dir.get_next() + dir.list_dir_end() + + # Sort by name (which includes timestamp) + backups.sort() + + # Remove oldest backups if we have too many + while backups.size() > keep_count: + var old_backup := backups.pop_front() + dir.remove(old_backup) + print("[UpdateApplier] Removed old backup: ", old_backup) + +func rollback_to_backup() -> bool: + ## Rollback to the most recent backup + var backup_dir := Config.BACKUP_DIRECTORY + var game_dir := Config.GAME_DIRECTORY + var pck_name := Config.get_game_pck() + + var dir := DirAccess.open(backup_dir) + if not dir: + push_error("[UpdateApplier] Cannot access backup directory") + return false + + # Find the most recent backup + var backups: Array[String] = [] + dir.list_dir_begin() + var file := dir.get_next() + while file != "": + if file.ends_with(".pck") and "backup" in file: + backups.append(file) + file = dir.get_next() + dir.list_dir_end() + + if backups.is_empty(): + push_error("[UpdateApplier] No backups found") + return false + + backups.sort() + var latest_backup := backups.back() + + # Restore the backup + if _copy_file(backup_dir + latest_backup, game_dir + pck_name): + emit_signal("rollback_completed") + print("[UpdateApplier] Rolled back to: ", latest_backup) + return true + + return false diff --git a/launcher/scripts/version_checker.gd b/launcher/scripts/version_checker.gd new file mode 100644 index 0000000..8d0c05d --- /dev/null +++ b/launcher/scripts/version_checker.gd @@ -0,0 +1,140 @@ +extends Node +class_name VersionChecker +## Handles checking for game updates from the version manifest + +signal version_check_started +signal version_check_completed(has_update: bool, latest_version: String, changelog: Array) +signal version_check_failed(error: String) + +var http_request: HTTPRequest +var current_version: String = "0.0.0" +var latest_version: String = "0.0.0" +var manifest_data: Dictionary = {} + +func _ready() -> void: + http_request = HTTPRequest.new() + add_child(http_request) + http_request.request_completed.connect(_on_request_completed) + + # Load current local version + _load_local_version() + +func _load_local_version() -> void: + if FileAccess.file_exists(Config.LOCAL_VERSION_FILE): + var file := FileAccess.open(Config.LOCAL_VERSION_FILE, FileAccess.READ) + if file: + current_version = file.get_as_text().strip_edges() + file.close() + print("[VersionChecker] Local version: ", current_version) + else: + current_version = "0.0.0" + print("[VersionChecker] No local version found, assuming fresh install") + +func check_for_updates() -> void: + emit_signal("version_check_started") + print("[VersionChecker] Checking for updates at: ", Config.VERSION_MANIFEST_URL) + + var error := http_request.request(Config.VERSION_MANIFEST_URL) + if error != OK: + emit_signal("version_check_failed", "Failed to initiate version check request") + +func _on_request_completed(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void: + if result != HTTPRequest.RESULT_SUCCESS: + emit_signal("version_check_failed", "Network error: " + str(result)) + return + + if response_code != 200: + emit_signal("version_check_failed", "Server returned error: " + str(response_code)) + return + + var json := JSON.new() + var parse_result := json.parse(body.get_string_from_utf8()) + if parse_result != OK: + emit_signal("version_check_failed", "Failed to parse version manifest") + return + + manifest_data = json.data + latest_version = manifest_data.get("latest_version", "0.0.0") + + var has_update := _compare_versions(current_version, latest_version) < 0 + var changelog: Array = _get_changelog_since(current_version) + + print("[VersionChecker] Latest version: ", latest_version, " | Has update: ", has_update) + emit_signal("version_check_completed", has_update, latest_version, changelog) + +func _compare_versions(v1: String, v2: String) -> int: + ## Returns -1 if v1 < v2, 0 if equal, 1 if v1 > v2 + 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: + ## Get all changelog entries since the given version + var changelog: Array = [] + var releases: Array = manifest_data.get("releases", []) + + for release in releases: + var release_version: String = release.get("version", "") + if _compare_versions(since_version, release_version) < 0: + changelog.append({ + "version": release_version, + "date": release.get("date", ""), + "changes": release.get("changelog", []) + }) + + return changelog + +func get_download_info() -> Dictionary: + ## Returns info needed to download the latest version (platform-specific) + var platform := Config.get_platform_name() # "windows", "linux", or "macos" + var releases: Array = manifest_data.get("releases", []) + + for release in releases: + if release.get("version") == latest_version: + # Try platform-specific URL first, fall back to generic pck_url + var pck_url: String = "" + var pck_size: int = 0 + var checksum_md5: String = "" + var checksum_sha256: String = "" + + # Check for platform-specific fields + var platform_key := "pck_" + platform # e.g., "pck_windows", "pck_linux", "pck_macos" + if release.has(platform_key): + var platform_data: Dictionary = release.get(platform_key, {}) + pck_url = platform_data.get("url", "") + pck_size = platform_data.get("size", 0) + checksum_md5 = platform_data.get("checksum_md5", "") + checksum_sha256 = platform_data.get("checksum_sha256", "") + else: + # Fall back to generic fields (single PCK for all platforms) + pck_url = release.get("pck_url", "") + pck_size = release.get("pck_size", 0) + checksum_md5 = release.get("checksum_md5", "") + checksum_sha256 = release.get("checksum_sha256", "") + + return { + "url": pck_url, + "size": pck_size, + "checksum_md5": checksum_md5, + "checksum_sha256": checksum_sha256 + } + return {} + +func save_version(version: String) -> void: + ## Save the current version to the local version file + var file := FileAccess.open(Config.LOCAL_VERSION_FILE, FileAccess.WRITE) + if file: + file.store_string(version) + file.close() + current_version = version + print("[VersionChecker] Saved version: ", version) diff --git a/launcher/server/version.json b/launcher/server/version.json new file mode 100644 index 0000000..6957499 --- /dev/null +++ b/launcher/server/version.json @@ -0,0 +1,63 @@ +{ + "latest_version": "1.0.0", + "minimum_launcher_version": "1.0.0", + "minimum_app_version": "1.0.0", + "releases": [ + { + "version": "1.0.0", + "date": "2025-12-10", + "pck_windows": { + "url": "https://your-host.com/releases/tekton-local-1.0.0-windows.pck", + "size": 52428800, + "checksum_md5": "", + "checksum_sha256": "" + }, + "pck_linux": { + "url": "https://your-host.com/releases/tekton-local-1.0.0-linux.pck", + "size": 52428800, + "checksum_md5": "", + "checksum_sha256": "" + }, + "pck_macos": { + "url": "https://your-host.com/releases/tekton-local-1.0.0-macos.pck", + "size": 52428800, + "checksum_md5": "", + "checksum_sha256": "" + }, + "pck_android": { + "url": "https://your-host.com/releases/tekton-local-1.0.0-android.pck", + "size": 52428800, + "checksum_md5": "", + "checksum_sha256": "" + }, + "pck_ios": { + "url": "https://your-host.com/releases/tekton-local-1.0.0-ios.pck", + "size": 52428800, + "checksum_md5": "", + "checksum_sha256": "" + }, + "changelog": [ + "Initial release", + "Multiplayer support via Nakama", + "Turn-based gameplay system", + "Lobby and matchmaking", + "Cross-platform support (Windows, Linux, macOS, Android, iOS)" + ], + "required": true + } + ], + "news": [ + { + "title": "Welcome to Tekton!", + "date": "2025-12-10", + "content": "The game is now available on all platforms! Join us for multiplayer tactical battles.", + "type": "announcement" + }, + { + "title": "Mobile Launch", + "date": "2025-12-10", + "content": "Tekton is now available on Android and iOS! Download from your app store.", + "type": "devlog" + } + ] +} \ No newline at end of file diff --git a/project.godot b/project.godot index 285557b..bd2fadd 100644 --- a/project.godot +++ b/project.godot @@ -27,6 +27,7 @@ TurnManager="*res://scripts/managers/turn_manager.gd" GoalManager="*res://scripts/managers/goal_manager.gd" GameStateManager="*res://scripts/managers/game_state_manager.gd" LobbyManager="*res://scripts/managers/lobby_manager.gd" +GameUpdateManager="*res://scripts/managers/game_update_manager.gd" [display] diff --git a/scenes/boot_screen.tscn b/scenes/boot_screen.tscn new file mode 100644 index 0000000..3657868 --- /dev/null +++ b/scenes/boot_screen.tscn @@ -0,0 +1,101 @@ +[gd_scene load_steps=2 format=3 uid="uid://boot_screen"] + +[ext_resource type="Script" path="res://scripts/ui/boot_screen.gd" id="1"] + +[node name="BootScreen" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource("1") + +[node name="Background" type="ColorRect" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0.039, 0.039, 0.102, 1) + +[node name="CenterContainer" type="CenterContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 + +[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer"] +layout_mode = 2 +theme_override_constants/separation = 20 + +[node name="Logo" type="Label" parent="CenterContainer/VBoxContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(0, 0.831, 1, 1) +theme_override_font_sizes/font_size = 48 +text = "TEKTON" +horizontal_alignment = 1 + +[node name="StatusLabel" type="Label" parent="CenterContainer/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_colors/font_color = Color(0.533, 0.533, 0.6, 1) +theme_override_font_sizes/font_size = 16 +text = "Checking for updates..." +horizontal_alignment = 1 + +[node name="ProgressContainer" type="VBoxContainer" parent="CenterContainer/VBoxContainer"] +unique_name_in_owner = true +visible = false +layout_mode = 2 + +[node name="ProgressBar" type="ProgressBar" parent="CenterContainer/VBoxContainer/ProgressContainer"] +unique_name_in_owner = true +layout_mode = 2 +custom_minimum_size = Vector2(300, 20) +show_percentage = false + +[node name="ProgressLabel" type="Label" parent="CenterContainer/VBoxContainer/ProgressContainer"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_colors/font_color = Color(0.533, 0.533, 0.6, 1) +text = "0%" +horizontal_alignment = 1 + +[node name="ButtonContainer" type="HBoxContainer" parent="CenterContainer/VBoxContainer"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +alignment = 1 + +[node name="UpdateButton" type="Button" parent="CenterContainer/VBoxContainer/ButtonContainer"] +unique_name_in_owner = true +layout_mode = 2 +custom_minimum_size = Vector2(140, 40) +text = "Update Now" + +[node name="SkipButton" type="Button" parent="CenterContainer/VBoxContainer/ButtonContainer"] +unique_name_in_owner = true +layout_mode = 2 +custom_minimum_size = Vector2(140, 40) +text = "Play Anyway" + +[node name="StoreButton" type="Button" parent="CenterContainer/VBoxContainer/ButtonContainer"] +unique_name_in_owner = true +visible = false +layout_mode = 2 +custom_minimum_size = Vector2(140, 40) +text = "Open Store" + +[node name="VersionLabel" type="Label" parent="."] +layout_mode = 1 +anchors_preset = 3 +anchor_left = 1.0 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = -100.0 +offset_top = -30.0 +offset_right = -10.0 +offset_bottom = -10.0 +theme_override_colors/font_color = Color(0.4, 0.4, 0.5, 1) +theme_override_font_sizes/font_size = 12 +text = "v1.0.0" +horizontal_alignment = 2 diff --git a/scripts/managers/game_update_manager.gd b/scripts/managers/game_update_manager.gd new file mode 100644 index 0000000..cf002a1 --- /dev/null +++ b/scripts/managers/game_update_manager.gd @@ -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) diff --git a/scripts/ui/boot_screen.gd b/scripts/ui/boot_screen.gd new file mode 100644 index 0000000..c22b830 --- /dev/null +++ b/scripts/ui/boot_screen.gd @@ -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) diff --git a/tiles_armagedon_a1.res b/tiles_armagedon_a1.res deleted file mode 100644 index e1cc909e35d1715f8e24c8b86169ef5d1bf8ff40..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11257 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 diff --git a/tools/create_patch.ps1 b/tools/create_patch.ps1 new file mode 100644 index 0000000..06c8c39 --- /dev/null +++ b/tools/create_patch.ps1 @@ -0,0 +1,77 @@ +# PowerShell script to create game patches for itch.io distribution +# Run this from the tekton-enet project root + +param( + [Parameter(Mandatory=$true)] + [string]$Version, + + [string]$GodotPath = "godot", + [string]$OutputDir = ".\dist" +) + +Write-Host "=== Tekton Patch Creator ===" -ForegroundColor Cyan +Write-Host "Creating patch for version: $Version" -ForegroundColor Yellow + +# Create output directory +if (-not (Test-Path $OutputDir)) { + New-Item -ItemType Directory -Path $OutputDir | Out-Null +} + +$PckName = "tekton-local-$Version.pck" +$PckPath = Join-Path $OutputDir $PckName + +# Update version file +Write-Host "Updating version.txt..." -ForegroundColor Green +Set-Content -Path ".\version.txt" -Value $Version + +# Export PCK only (no executable) +Write-Host "Exporting PCK file..." -ForegroundColor Green +Write-Host "Note: Run this export from the Godot editor or configure export preset first" + +# Generate checksums +Write-Host "Generating checksums..." -ForegroundColor Green + +if (Test-Path $PckPath) { + $md5 = (Get-FileHash -Path $PckPath -Algorithm MD5).Hash.ToLower() + $sha256 = (Get-FileHash -Path $PckPath -Algorithm SHA256).Hash.ToLower() + $size = (Get-Item $PckPath).Length + + Write-Host "MD5: $md5" -ForegroundColor White + Write-Host "SHA256: $sha256" -ForegroundColor White + Write-Host "Size: $size bytes" -ForegroundColor White + + # Generate JSON snippet for version.json + $jsonSnippet = @" +{ + "version": "$Version", + "date": "$(Get-Date -Format 'yyyy-MM-dd')", + "pck_url": "https://your-username.itch.io/tekton-local/files/$PckName", + "pck_size": $size, + "checksum_md5": "$md5", + "checksum_sha256": "$sha256", + "changelog": [ + "Add your changelog items here" + ], + "required": false +} +"@ + + $snippetPath = Join-Path $OutputDir "release-$Version.json" + Set-Content -Path $snippetPath -Value $jsonSnippet + + Write-Host "`nRelease JSON snippet saved to: $snippetPath" -ForegroundColor Green + Write-Host "Copy this into your version.json releases array" -ForegroundColor Yellow +} else { + Write-Host "PCK file not found at: $PckPath" -ForegroundColor Red + Write-Host "Please export the PCK manually from Godot:" -ForegroundColor Yellow + Write-Host " 1. Open project in Godot Editor" -ForegroundColor White + Write-Host " 2. Go to Project > Export..." -ForegroundColor White + Write-Host " 3. Configure Windows Desktop preset" -ForegroundColor White + Write-Host " 4. Click 'Export PCK/ZIP' and save as $PckName" -ForegroundColor White +} + +Write-Host "`n=== Patch Creation Complete ===" -ForegroundColor Cyan +Write-Host "Next steps:" -ForegroundColor Yellow +Write-Host " 1. Upload $PckName to itch.io" -ForegroundColor White +Write-Host " 2. Update version.json with the new release entry" -ForegroundColor White +Write-Host " 3. Upload updated version.json to itch.io" -ForegroundColor White diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..3eefcb9 --- /dev/null +++ b/version.txt @@ -0,0 +1 @@ +1.0.0