feat: patch system

This commit is contained in:
2026-04-22 04:21:00 +08:00
parent 083735aec1
commit c8e5a45529
36 changed files with 364 additions and 1847 deletions
+60
View File
@@ -0,0 +1,60 @@
name: Build and Release Patch PCK
on:
push:
branches:
- 'patch-release'
paths:
- 'scripts/**'
- 'scenes/**'
- 'assets/**'
- 'assets/data/version.json'
jobs:
build-and-deploy-patch:
runs-on: ubuntu-latest
steps:
- name: Checkout Source Code
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Generate Changed Files List
run: |
git diff --name-only HEAD^ HEAD > changed_files.txt
echo "Files to patch:"
cat changed_files.txt
- name: Setup Godot
uses: chickensoft-games/setup-godot@v1
with:
version: '4.2.1'
use-dotnet: false
- name: Run Build Patch Script
run: godot --headless -s tools/build_patch.gd
# Push the patch files directly to the public repository structure!
- name: Push to Public Repository
uses: dmnemec/copy_file_to_another_repo_action@main
env:
API_TOKEN_GITHUB: ${{ secrets.PUBLIC_REPO_PAT }}
with:
source_file: 'patch.pck'
destination_repo: '${{ github.actor }}/tekton-updates'
destination_folder: 'latest'
user_email: 'action@github.com'
user_name: 'PatchBot'
commit_message: '[AUTO] Pushed new patch.pck via CI'
- name: Push Version Manifest to Public Repository
uses: dmnemec/copy_file_to_another_repo_action@main
env:
API_TOKEN_GITHUB: ${{ secrets.PUBLIC_REPO_PAT }}
with:
source_file: 'assets/data/version.json'
destination_repo: '${{ github.actor }}/tekton-updates'
destination_folder: 'latest'
user_email: 'action@github.com'
user_name: 'PatchBot'
commit_message: '[AUTO] Pushed new version.json via CI'
@@ -14,7 +14,7 @@
[ext_resource type="Texture2D" uid="uid://dpkx1a780pvwv" path="res://assets/textures/tile_diamond.png" id="10_sx8rm"]
[ext_resource type="BoxMesh" uid="uid://fy4bhoeii40c" path="res://addons/enhanced_gridmap/meshlibrary/tile_safe_zone.tres" id="10_uwjsj"]
[ext_resource type="BoxMesh" uid="uid://b5cc3prem52r6" path="res://addons/enhanced_gridmap/meshlibrary/tile_freeze.tres" id="11_pgnbl"]
[ext_resource type="BoxMesh" uid="uid://dcjdwbffgtutt" path="res://addons/enhanced_gridmap/meshlibrary/tile_non_walkable.tres" id="11_uwjsj"]
[ext_resource type="BoxMesh" path="res://addons/enhanced_gridmap/meshlibrary/tile_non_walkable.tres" id="11_uwjsj"]
[ext_resource type="Texture2D" uid="uid://cdnxwlysxnujd" path="res://assets/textures/tile_heart.png" id="12_heart_tex"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_uxput"]
+15
View File
@@ -0,0 +1,15 @@
{
"latest_version": "2.1.5",
"minimum_app_version": "2.1.0",
"releases": [
{
"version": "2.1.5",
"date": "2026-04-20",
"pck_url": "https://raw.githubusercontent.com/tekton-studios/tekton-updates/main/patch.pck",
"changelog": [
"Initial release of the new patching system.",
"Added boot screen paginator."
]
}
]
}
@@ -76,7 +76,7 @@ _subresources={
"lods/normal_merge_angle": 60.0,
"lods/normal_split_angle": 25.0,
"save_to_file/enabled": true,
"save_to_file/fallback_path": "res://tiles_armagedon_a1.res",
"save_to_file/fallback_path": "res://assets/models/meshes/tiles_armagedon_a1.res",
"save_to_file/path": "uid://dc8xogqvnqor7"
},
"tiles_armagedon_Cube_012": {
Binary file not shown.
-93
View File
@@ -1,93 +0,0 @@
# 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
+3 -3
View File
@@ -8,7 +8,7 @@ custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="build/tekton_armageddon_v2.1.4.exe"
export_path="build/tekton_armageddon_v2.1.5.exe"
patches=PackedStringArray()
patch_delta_encoding=false
patch_delta_compression_level_zstd=19
@@ -80,7 +80,7 @@ custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="build/tekton-dash-armageddon-v.2.1.4.apk"
export_path="build/tekton-dash-armageddon-v.2.1.5.apk"
patches=PackedStringArray()
patch_delta_encoding=false
patch_delta_compression_level_zstd=19
@@ -111,7 +111,7 @@ architectures/arm64-v8a=true
architectures/x86=false
architectures/x86_64=false
version/code=2
version/name="2.1.2"
version/name="2.1.5"
package/unique_name="com.danchiego.$genname"
package/name="Tekton Dash Armageddon"
package/signed=true
View File
-124
View File
@@ -1,124 +0,0 @@
# 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.
-27
View File
@@ -1,27 +0,0 @@
<svg width="256" height="256" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#0a0a1a"/>
<stop offset="100%" style="stop-color:#141428"/>
</linearGradient>
<linearGradient id="accent" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#00d4ff"/>
<stop offset="100%" style="stop-color:#ff00aa"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="256" height="256" rx="32" fill="url(#bg)"/>
<!-- Stylized T logo -->
<path d="M60 70 L196 70 L196 90 L138 90 L138 186 L118 186 L118 90 L60 90 Z"
fill="url(#accent)" opacity="0.9"/>
<!-- Decorative circuit lines -->
<path d="M40 130 L80 130 L80 150 L60 150" stroke="#00d4ff" stroke-width="2" fill="none" opacity="0.5"/>
<path d="M216 130 L176 130 L176 150 L196 150" stroke="#ff00aa" stroke-width="2" fill="none" opacity="0.5"/>
<!-- Glow dots -->
<circle cx="40" cy="130" r="4" fill="#00d4ff"/>
<circle cx="216" cy="130" r="4" fill="#ff00aa"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

-37
View File
@@ -1,37 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b048nsl527nah"
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://icon.svg"
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
-27
View File
@@ -1,27 +0,0 @@
; 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"
-197
View File
@@ -1,197 +0,0 @@
[gd_scene load_steps=12 format=3 uid="uid://lwtn5sbr5jr2"]
[ext_resource type="Script" uid="uid://cvstgaigwt0sj" path="res://scripts/launcher_main.gd" id="1"]
[ext_resource type="Script" uid="uid://ptdurqksg1sv" path="res://scripts/version_checker.gd" id="2"]
[ext_resource type="Script" uid="uid://cer12vb230kfw" path="res://scripts/download_manager.gd" id="3"]
[ext_resource type="Script" uid="uid://n2ptlbblexo6" path="res://scripts/update_applier.gd" id="4"]
[ext_resource type="Script" uid="uid://coer7l074xm6x" path="res://scripts/news_fetcher.gd" id="5"]
[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_tab_panel"]
content_margin_left = 8.0
content_margin_top = 8.0
content_margin_right = 8.0
content_margin_bottom = 8.0
bg_color = Color(0.059, 0.059, 0.118, 1)
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_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_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
[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
script = ExtResource("1")
[node name="Background" type="ColorRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
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="VBoxContainer" 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
grow_horizontal = 2
grow_vertical = 2
[node name="TitleBar" type="PanelContainer" parent="MainPanel"]
custom_minimum_size = Vector2(0, 60)
layout_mode = 2
size_flags_vertical = 0
theme_override_styles/panel = SubResource("StyleBoxFlat_titlebar")
[node name="HBoxContainer" type="HBoxContainer" parent="MainPanel/TitleBar"]
layout_mode = 2
[node name="TitleLabel" type="Label" parent="MainPanel/TitleBar/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_colors/font_color = Color(0, 0.831, 1, 1)
theme_override_font_sizes/font_size = 24
text = "TEKTON LAUNCHER"
[node name="VersionLabel" type="Label" parent="MainPanel/TitleBar"]
layout_mode = 2
theme_override_colors/font_color = Color(0.533, 0.533, 0.6, 1)
text = "v0.0.0"
horizontal_alignment = 2
[node name="ContentContainer" type="VBoxContainer" parent="MainPanel"]
layout_mode = 2
size_flags_vertical = 3
alignment = 1
[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
grow_horizontal = 2
grow_vertical = 2
[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
grow_horizontal = 2
grow_vertical = 2
[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"]
custom_minimum_size = Vector2(0, 80)
layout_mode = 2
alignment = 2
[node name="StatusLabel" type="Label" parent="MainPanel/ContentContainer/BottomBar"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_colors/font_color = Color(0.533, 0.533, 0.6, 1)
text = "Checking for updates..."
[node name="ProgressContainer" type="VBoxContainer" parent="MainPanel/ContentContainer/BottomBar"]
visible = false
custom_minimum_size = Vector2(200, 0)
layout_mode = 2
[node name="ProgressBar" type="ProgressBar" parent="MainPanel/ContentContainer/BottomBar/ProgressContainer"]
custom_minimum_size = Vector2(200, 20)
layout_mode = 2
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
theme_override_colors/font_color = Color(0.533, 0.533, 0.6, 1)
text = "0%"
horizontal_alignment = 1
[node name="PlayButton" type="Button" parent="MainPanel/ContentContainer/BottomBar"]
custom_minimum_size = Vector2(160, 50)
layout_mode = 2
theme_override_colors/font_color = Color(0.039, 0.039, 0.102, 1)
theme_override_font_sizes/font_size = 18
theme_override_styles/hover = SubResource("StyleBoxFlat_button_hover")
theme_override_styles/normal = SubResource("StyleBoxFlat_button")
text = "► PLAY"
-73
View File
@@ -1,73 +0,0 @@
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
-1
View File
@@ -1 +0,0 @@
uid://huq4exk8psgu
-133
View File
@@ -1,133 +0,0 @@
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")
-1
View File
@@ -1 +0,0 @@
uid://cer12vb230kfw
-338
View File
@@ -1,338 +0,0 @@
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)
-1
View File
@@ -1 +0,0 @@
uid://cvstgaigwt0sj
-70
View File
@@ -1,70 +0,0 @@
extends Node
class_name NewsFetcher
const Config = preload("config.gd")
## 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
-1
View File
@@ -1 +0,0 @@
uid://coer7l074xm6x
-164
View File
@@ -1,164 +0,0 @@
extends Node
class_name UpdateApplier
const Config = preload("config.gd")
## 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
-1
View File
@@ -1 +0,0 @@
uid://n2ptlbblexo6
-142
View File
@@ -1,142 +0,0 @@
extends Node
class_name VersionChecker
const Config = preload("config.gd")
## 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)
-1
View File
@@ -1 +0,0 @@
uid://ptdurqksg1sv
-83
View File
@@ -1,83 +0,0 @@
{
"latest_version": "0.9.0",
"minimum_launcher_version": "0.9.0",
"minimum_app_version": "0.9.0",
"releases": [
{
"version": "0.9.0",
"date": "2026-03-09",
"pck_windows": {
"url": "https://your-host.com/releases/tekton-local-0.9.0-windows.pck",
"size": 52428800,
"checksum_md5": "",
"checksum_sha256": ""
},
"changelog": [
"Power-up spawning before countdown in Stop n Go",
"Fixed safe zone visuals persistence",
"Unified Game Over freeze for bots and players",
"Added wall overlap protection for spawned tiles",
"Restricted bot tile collection to current standing tile",
"Improved door visibility in Tekton Doors mode",
"Fixed knock logic when carrying a Tekton"
],
"required": true
},
{
"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"
}
]
}
+2 -2
View File
@@ -15,8 +15,8 @@ compatibility/default_parent_skeleton_in_mesh_instance_3d=true
[application]
config/name="Tekton Dash Armageddon"
config/version="2.1.2"
run/main_scene="res://scenes/ui/login_screen.tscn"
config/version="2.1.5"
run/main_scene="res://scenes/ui/boot_screen.tscn"
config/features=PackedStringArray("4.6", "Forward Plus")
config/icon="res://icon.svg"
-131
View File
@@ -1,131 +0,0 @@
[gd_scene load_steps=5 format=3 uid="uid://cyfjwldknv8m6"]
[ext_resource type="Script" uid="uid://vgyrq5y5p7jw" path="res://scripts/ui/boot_screen.gd" id="1"]
[ext_resource type="Theme" uid="uid://da337sh5qxi0s" path="res://assets/themes/ui_theme.tres" id="2"]
[ext_resource type="Texture2D" uid="uid://40tlo0mda3wr" path="res://assets/graphics/main_menu/result_bg.png" id="3_v46t4"]
[ext_resource type="Texture2D" uid="uid://dvp0as6yyudco" path="res://assets/graphics/main_menu/bg_illust.png" id="4_okh44"]
[node name="BootScreen" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme = ExtResource("2")
script = ExtResource("1")
[node name="Background" type="TextureRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
texture = ExtResource("3_v46t4")
expand_mode = 2
[node name="Background2" type="TextureRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
texture = ExtResource("4_okh44")
expand_mode = 3
[node name="CenterContainer" type="CenterContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer"]
layout_mode = 2
theme_override_constants/separation = 24
[node name="Logo" type="Label" parent="CenterContainer/VBoxContainer"]
layout_mode = 2
theme_override_colors/font_color = Color(0.647, 0.996, 0.224, 1)
theme_override_font_sizes/font_size = 52
text = "TEKTON"
horizontal_alignment = 1
[node name="Subtitle" type="Label" parent="CenterContainer/VBoxContainer"]
layout_mode = 2
theme_override_colors/font_color = Color(0.992, 0.796, 0.047, 1)
theme_override_font_sizes/font_size = 14
text = "ARMAGEDDON"
horizontal_alignment = 1
[node name="StatusLabel" type="Label" parent="CenterContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
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
theme_override_constants/separation = 8
[node name="ProgressBar" type="ProgressBar" parent="CenterContainer/VBoxContainer/ProgressContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(320, 24)
layout_mode = 2
show_percentage = false
[node name="ProgressLabel" type="Label" parent="CenterContainer/VBoxContainer/ProgressContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "0%"
horizontal_alignment = 1
[node name="ButtonContainer" type="HBoxContainer" parent="CenterContainer/VBoxContainer"]
unique_name_in_owner = true
visible = false
layout_mode = 2
theme_override_constants/separation = 16
alignment = 1
[node name="UpdateButton" type="Button" parent="CenterContainer/VBoxContainer/ButtonContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(160, 48)
layout_mode = 2
text = "Update Now"
[node name="SkipButton" type="Button" parent="CenterContainer/VBoxContainer/ButtonContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(160, 48)
layout_mode = 2
text = "Play Anyway"
[node name="StoreButton" type="Button" parent="CenterContainer/VBoxContainer/ButtonContainer"]
unique_name_in_owner = true
visible = false
custom_minimum_size = Vector2(160, 48)
layout_mode = 2
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 = -120.0
offset_top = -40.0
offset_right = -16.0
offset_bottom = -16.0
grow_horizontal = 0
grow_vertical = 0
theme_override_colors/font_color = Color(0.6, 0.6, 0.6, 0.6)
theme_override_font_sizes/font_size = 12
text = "v0.9.0"
horizontal_alignment = 2
+120
View File
@@ -0,0 +1,120 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://scripts/ui/boot_screen.gd" id="1_boot"]
[node name="BootScreen" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_boot")
[node name="Background" type="ColorRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0.0823529, 0.0823529, 0.109804, 1)
[node name="CenterContainer" type="CenterContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="MainVBox" type="VBoxContainer" parent="CenterContainer"]
custom_minimum_size = Vector2(600, 0)
layout_mode = 2
theme_override_constants/separation = 20
[node name="Title" type="Label" parent="CenterContainer/MainVBox"]
layout_mode = 2
theme_override_font_sizes/font_size = 32
text = "Tekton Dash Booting..."
horizontal_alignment = 1
[node name="ChangelogPanel" type="PanelContainer" parent="CenterContainer/MainVBox"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 300)
layout_mode = 2
[node name="Margin" type="MarginContainer" parent="CenterContainer/MainVBox/ChangelogPanel"]
layout_mode = 2
theme_override_constants/margin_left = 15
theme_override_constants/margin_top = 15
theme_override_constants/margin_right = 15
theme_override_constants/margin_bottom = 15
[node name="VBox" type="VBoxContainer" parent="CenterContainer/MainVBox/ChangelogPanel/Margin"]
layout_mode = 2
[node name="ChangelogRichText" type="RichTextLabel" parent="CenterContainer/MainVBox/ChangelogPanel/Margin/VBox"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
bbcode_enabled = true
text = "Loading changelog..."
[node name="HBox" type="HBoxContainer" parent="CenterContainer/MainVBox/ChangelogPanel/Margin/VBox"]
layout_mode = 2
alignment = 1
[node name="PrevButton" type="Button" parent="CenterContainer/MainVBox/ChangelogPanel/Margin/VBox/HBox"]
unique_name_in_owner = true
layout_mode = 2
text = " < Previous "
[node name="PageLabel" type="Label" parent="CenterContainer/MainVBox/ChangelogPanel/Margin/VBox/HBox"]
unique_name_in_owner = true
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
text = "1 / 1"
horizontal_alignment = 1
[node name="NextButton" type="Button" parent="CenterContainer/MainVBox/ChangelogPanel/Margin/VBox/HBox"]
unique_name_in_owner = true
layout_mode = 2
text = " Next > "
[node name="StatusLabel" type="Label" parent="CenterContainer/MainVBox"]
unique_name_in_owner = true
layout_mode = 2
text = "Checking for updates..."
horizontal_alignment = 1
[node name="ProgressBar" type="ProgressBar" parent="CenterContainer/MainVBox"]
unique_name_in_owner = true
layout_mode = 2
show_percentage = false
[node name="ProgressLabel" type="Label" parent="CenterContainer/MainVBox/ProgressBar"]
unique_name_in_owner = true
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
horizontal_alignment = 1
[node name="ButtonContainer" type="HBoxContainer" parent="CenterContainer/MainVBox"]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/separation = 20
alignment = 1
[node name="SkipButton" type="Button" parent="CenterContainer/MainVBox/ButtonContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Skip"
[node name="UpdateButton" type="Button" parent="CenterContainer/MainVBox/ButtonContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Download Update"
+3 -3
View File
@@ -14,7 +14,7 @@ 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 VERSION_MANIFEST_URL := "https://raw.githubusercontent.com/tekton-studios/tekton-updates/main/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"
@@ -22,8 +22,8 @@ const IOS_STORE_URL := "https://apps.apple.com/app/tekton/id123456789"
enum Platform { WINDOWS, LINUX, MACOS, ANDROID, IOS, WEB }
# State
var current_version: String = "0.9.0"
var latest_version: String = "0.9.0"
var current_version: String = "2.1.5"
var latest_version: String = "2.1.5"
var manifest_data: Dictionary = {}
var http_request: HTTPRequest
var download_request: HTTPRequest
+99 -109
View File
@@ -1,123 +1,96 @@
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)
## Shows changelog with pagination, checking patch, and resource loading
@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
@onready var changelog_panel := %ChangelogPanel
@onready var changelog_richtext := %ChangelogRichText as RichTextLabel
@onready var prev_button := %PrevButton as Button
@onready var next_button := %NextButton as Button
@onready var page_label := %PageLabel as Label
var update_manager: Node
var update_info: Dictionary = {}
var main_scene_path := "res://scenes/main.tscn" # Your main game scene
var main_scene_path := "res://scenes/ui/login_screen.tscn"
# Changelog Pagination
var changelog_data: Array = []
var current_page: int = 0
var is_loading_game: bool = false
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)
skip_button.pressed.connect(_begin_resource_load)
prev_button.pressed.connect(_on_prev_pressed)
next_button.pressed.connect(_on_next_pressed)
# Show current version
version_label.text = "v" + update_manager.current_version
changelog_panel.visible = false
button_container.visible = false
progress_bar.visible = false
progress_label.visible = false
# Start update check after a brief delay
await get_tree().create_timer(0.5).timeout
_check_for_updates()
status_label.text = "Checking versions..."
update_manager.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: Node = 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
# Load changelog array specifically
changelog_data = update_manager.manifest_data.get("releases", [])
if changelog_data.size() > 0:
changelog_panel.visible = true
current_page = 0
_update_pagination_ui()
if has_update and 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
skip_button.text = "Play without updating"
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()
status_label.text = "Game up to date."
_begin_resource_load()
func _on_update_check_failed(error: String) -> void:
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()
button_container.visible = true
update_button.visible = false
skip_button.visible = true
skip_button.text = "Play Offline"
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
func _on_download_started(total_size: int) -> void:
progress_bar.visible = true
progress_label.visible = true
progress_bar.value = 0
progress_label.text = "0%"
@@ -126,56 +99,73 @@ func _on_download_progress(_downloaded: int, _total: int, percentage: float) ->
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()
status_label.text = "Update installed successfully!"
progress_bar.visible = false
progress_label.visible = false
_begin_resource_load()
func _on_download_failed(error: String) -> void:
status_label.text = "Update failed: " + error
progress_container.visible = false
progress_bar.visible = false
progress_label.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 _begin_resource_load() -> void:
button_container.visible = false
status_label.text = "Loading resources..."
progress_bar.visible = true
progress_label.visible = true
progress_bar.value = 0
progress_label.text = "0%"
func _proceed_to_game() -> void:
# Load any previously downloaded patches
_load_existing_patches()
is_loading_game = true
ResourceLoader.load_threaded_request(main_scene_path)
# Change to main game scene
get_tree().change_scene_to_file(main_scene_path)
func _process(_delta: float) -> void:
if is_loading_game:
var progress := []
var status := ResourceLoader.load_threaded_get_status(main_scene_path, progress)
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:
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS:
var perc = progress[0] * 100.0
progress_bar.value = perc
progress_label.text = "%.0f%%" % perc
elif status == ResourceLoader.THREAD_LOAD_LOADED:
is_loading_game = false
get_tree().change_scene_to_packed(ResourceLoader.load_threaded_get(main_scene_path))
elif status == ResourceLoader.THREAD_LOAD_FAILED:
is_loading_game = false
status_label.text = "Error Loading Game"
# --- Pagination UI Functions ---
func _update_pagination_ui():
if changelog_data.size() == 0:
changelog_richtext.text = "No changelog data available."
prev_button.disabled = true
next_button.disabled = true
page_label.text = "0/0"
return
var patches: Array[String] = []
dir.list_dir_begin()
var patch_file := dir.get_next()
while patch_file != "":
if patch_file.ends_with(".pck"):
patches.append(patch_file)
patch_file = dir.get_next()
dir.list_dir_end()
var entry: Dictionary = changelog_data[current_page]
var txt := "[font_size=20][b]Version " + entry.get("version", "Unknown") + "[/b][/font_size]\n[color=gray]" + entry.get("date", "") + "[/color]\n\n"
# Sort patches by version (filename includes version)
patches.sort()
var changes: Array = entry.get("changelog", [])
for change in changes:
txt += "" + change + "\n"
# 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)
changelog_richtext.text = txt
page_label.text = str(current_page + 1) + " / " + str(changelog_data.size())
prev_button.disabled = (current_page == 0)
next_button.disabled = (current_page >= changelog_data.size() - 1)
func _on_prev_pressed():
if current_page > 0:
current_page -= 1
_update_pagination_ui()
func _on_next_pressed():
if current_page < changelog_data.size() - 1:
current_page += 1
_update_pagination_ui()
Binary file not shown.
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env -S godot --headless -s
extends SceneTree
func _init():
print("--- Starting Automated Patch Build ---")
var output_file = "patch.pck"
var changed_files_txt = "changed_files.txt"
if not FileAccess.file_exists(changed_files_txt):
print("ERROR: missing changed_files.txt. Cannot build patch.")
quit(1)
return
var pck_packer = PCKPacker.new()
var err = pck_packer.pck_start(output_file)
if err != OK:
print("ERROR: Could not start PCK file: ", output_file)
quit(1)
return
var file = FileAccess.open(changed_files_txt, FileAccess.READ)
var count = 0
while not file.eof_reached():
var line = file.get_line().strip_edges()
if line.is_empty(): continue
var res_path = "res://" + line
# Include automatically compiled scripts for GDScript
if line.ends_with(".gd"):
var remap_path = res_path.replace(".gd", ".gdc")
if FileAccess.file_exists(remap_path):
pck_packer.add_file(res_path, remap_path)
else:
pck_packer.add_file(res_path, res_path)
count += 1
print("Adding (Script): ", res_path)
elif FileAccess.file_exists(res_path):
print("Adding to patch: ", res_path)
pck_packer.add_file(res_path, res_path)
count += 1
else:
print("Warning: Changed file not found or Is Directory, skipping: ", res_path)
# Always package our version/changelog list so clients see the new changelog
var version_manifest = "res://assets/data/version.json"
pck_packer.add_file(version_manifest, version_manifest)
print("Adding Version Manifest: ", version_manifest)
pck_packer.flush(true)
print("--- Patch Build Complete! Packed %d files into %s ---" % [count + 1, output_file])
quit(0)
+1
View File
@@ -0,0 +1 @@
uid://baco51hmps6s1
-77
View File
@@ -1,77 +0,0 @@
# 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
-1
View File
@@ -1 +0,0 @@
0.9.0