feat: patch system
This commit is contained in:
@@ -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="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://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://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"]
|
[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"]
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_uxput"]
|
||||||
|
|||||||
@@ -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_merge_angle": 60.0,
|
||||||
"lods/normal_split_angle": 25.0,
|
"lods/normal_split_angle": 25.0,
|
||||||
"save_to_file/enabled": true,
|
"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"
|
"save_to_file/path": "uid://dc8xogqvnqor7"
|
||||||
},
|
},
|
||||||
"tiles_armagedon_Cube_012": {
|
"tiles_armagedon_Cube_012": {
|
||||||
|
|||||||
Binary file not shown.
@@ -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
@@ -8,7 +8,7 @@ custom_features=""
|
|||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter=""
|
include_filter=""
|
||||||
exclude_filter=""
|
exclude_filter=""
|
||||||
export_path="build/tekton_armageddon_v2.1.4.exe"
|
export_path="build/tekton_armageddon_v2.1.5.exe"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
patch_delta_encoding=false
|
patch_delta_encoding=false
|
||||||
patch_delta_compression_level_zstd=19
|
patch_delta_compression_level_zstd=19
|
||||||
@@ -80,7 +80,7 @@ custom_features=""
|
|||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter=""
|
include_filter=""
|
||||||
exclude_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()
|
patches=PackedStringArray()
|
||||||
patch_delta_encoding=false
|
patch_delta_encoding=false
|
||||||
patch_delta_compression_level_zstd=19
|
patch_delta_compression_level_zstd=19
|
||||||
@@ -111,7 +111,7 @@ architectures/arm64-v8a=true
|
|||||||
architectures/x86=false
|
architectures/x86=false
|
||||||
architectures/x86_64=false
|
architectures/x86_64=false
|
||||||
version/code=2
|
version/code=2
|
||||||
version/name="2.1.2"
|
version/name="2.1.5"
|
||||||
package/unique_name="com.danchiego.$genname"
|
package/unique_name="com.danchiego.$genname"
|
||||||
package/name="Tekton Dash Armageddon"
|
package/name="Tekton Dash Armageddon"
|
||||||
package/signed=true
|
package/signed=true
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -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 |
@@ -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
|
|
||||||
@@ -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"
|
|
||||||
@@ -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"
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://huq4exk8psgu
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://cer12vb230kfw
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://cvstgaigwt0sj
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://coer7l074xm6x
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://n2ptlbblexo6
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
uid://ptdurqksg1sv
|
|
||||||
@@ -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
@@ -15,8 +15,8 @@ compatibility/default_parent_skeleton_in_mesh_instance_3d=true
|
|||||||
[application]
|
[application]
|
||||||
|
|
||||||
config/name="Tekton Dash Armageddon"
|
config/name="Tekton Dash Armageddon"
|
||||||
config/version="2.1.2"
|
config/version="2.1.5"
|
||||||
run/main_scene="res://scenes/ui/login_screen.tscn"
|
run/main_scene="res://scenes/ui/boot_screen.tscn"
|
||||||
config/features=PackedStringArray("4.6", "Forward Plus")
|
config/features=PackedStringArray("4.6", "Forward Plus")
|
||||||
config/icon="res://icon.svg"
|
config/icon="res://icon.svg"
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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"
|
||||||
@@ -14,7 +14,7 @@ signal patch_applied
|
|||||||
signal store_update_required(store_url: String)
|
signal store_update_required(store_url: String)
|
||||||
|
|
||||||
# Configuration - Update these URLs for your game
|
# 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 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"
|
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 }
|
enum Platform { WINDOWS, LINUX, MACOS, ANDROID, IOS, WEB }
|
||||||
|
|
||||||
# State
|
# State
|
||||||
var current_version: String = "0.9.0"
|
var current_version: String = "2.1.5"
|
||||||
var latest_version: String = "0.9.0"
|
var latest_version: String = "2.1.5"
|
||||||
var manifest_data: Dictionary = {}
|
var manifest_data: Dictionary = {}
|
||||||
var http_request: HTTPRequest
|
var http_request: HTTPRequest
|
||||||
var download_request: HTTPRequest
|
var download_request: HTTPRequest
|
||||||
|
|||||||
+104
-114
@@ -1,123 +1,96 @@
|
|||||||
extends Control
|
extends Control
|
||||||
## Boot screen that handles update checking before launching the game
|
## Boot screen that handles update checking before launching the game
|
||||||
## On mobile: Shows update UI and handles in-game patching
|
## Shows changelog with pagination, checking patch, and resource loading
|
||||||
## On desktop: Quick check, then proceeds (assumes launcher handles updates)
|
|
||||||
|
|
||||||
@onready var status_label := %StatusLabel as Label
|
@onready var status_label := %StatusLabel as Label
|
||||||
@onready var progress_container := %ProgressContainer as VBoxContainer
|
|
||||||
@onready var progress_bar := %ProgressBar as ProgressBar
|
@onready var progress_bar := %ProgressBar as ProgressBar
|
||||||
@onready var progress_label := %ProgressLabel as Label
|
@onready var progress_label := %ProgressLabel as Label
|
||||||
@onready var button_container := %ButtonContainer as HBoxContainer
|
@onready var button_container := %ButtonContainer as HBoxContainer
|
||||||
@onready var update_button := %UpdateButton as Button
|
@onready var update_button := %UpdateButton as Button
|
||||||
@onready var skip_button := %SkipButton 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_manager: Node
|
||||||
var update_info: Dictionary = {}
|
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:
|
func _ready() -> void:
|
||||||
# Get or create the update manager
|
|
||||||
update_manager = _get_update_manager()
|
update_manager = _get_update_manager()
|
||||||
|
|
||||||
# Connect signals
|
|
||||||
update_manager.update_check_completed.connect(_on_update_check_completed)
|
update_manager.update_check_completed.connect(_on_update_check_completed)
|
||||||
update_manager.update_check_failed.connect(_on_update_check_failed)
|
update_manager.update_check_failed.connect(_on_update_check_failed)
|
||||||
update_manager.download_started.connect(_on_download_started)
|
update_manager.download_started.connect(_on_download_started)
|
||||||
update_manager.download_progress.connect(_on_download_progress)
|
update_manager.download_progress.connect(_on_download_progress)
|
||||||
update_manager.download_completed.connect(_on_download_completed)
|
update_manager.download_completed.connect(_on_download_completed)
|
||||||
update_manager.download_failed.connect(_on_download_failed)
|
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)
|
update_button.pressed.connect(_on_update_pressed)
|
||||||
skip_button.pressed.connect(_on_skip_pressed)
|
skip_button.pressed.connect(_begin_resource_load)
|
||||||
store_button.pressed.connect(_on_store_pressed)
|
prev_button.pressed.connect(_on_prev_pressed)
|
||||||
|
next_button.pressed.connect(_on_next_pressed)
|
||||||
|
|
||||||
# Show current version
|
changelog_panel.visible = false
|
||||||
version_label.text = "v" + update_manager.current_version
|
button_container.visible = false
|
||||||
|
progress_bar.visible = false
|
||||||
|
progress_label.visible = false
|
||||||
|
|
||||||
# Start update check after a brief delay
|
status_label.text = "Checking versions..."
|
||||||
await get_tree().create_timer(0.5).timeout
|
update_manager.check_for_updates()
|
||||||
_check_for_updates()
|
|
||||||
|
|
||||||
func _get_update_manager() -> Node:
|
func _get_update_manager() -> Node:
|
||||||
# Try to get from autoload first
|
|
||||||
if has_node("/root/GameUpdateManager"):
|
if has_node("/root/GameUpdateManager"):
|
||||||
return get_node("/root/GameUpdateManager")
|
return get_node("/root/GameUpdateManager")
|
||||||
|
|
||||||
# Otherwise, create instance
|
|
||||||
var manager_script := load("res://scripts/managers/game_update_manager.gd")
|
var manager_script := load("res://scripts/managers/game_update_manager.gd")
|
||||||
var manager: Node = manager_script.new()
|
var manager: Node = manager_script.new()
|
||||||
manager.name = "GameUpdateManager"
|
manager.name = "GameUpdateManager"
|
||||||
get_tree().root.add_child(manager)
|
get_tree().root.add_child(manager)
|
||||||
return 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:
|
func _on_update_check_completed(has_update: bool, info: Dictionary) -> void:
|
||||||
update_info = info
|
update_info = info
|
||||||
version_label.text = "v" + info.current_version
|
|
||||||
|
|
||||||
if has_update:
|
# Load changelog array specifically
|
||||||
if info.needs_store_update:
|
changelog_data = update_manager.manifest_data.get("releases", [])
|
||||||
status_label.text = "A required update is available.\nPlease update from the store."
|
if changelog_data.size() > 0:
|
||||||
button_container.visible = true
|
changelog_panel.visible = true
|
||||||
update_button.visible = false
|
current_page = 0
|
||||||
skip_button.visible = false
|
_update_pagination_ui()
|
||||||
store_button.visible = true
|
|
||||||
elif info.can_patch:
|
if has_update and info.can_patch:
|
||||||
status_label.text = "Update available: v" + info.latest_version
|
status_label.text = "Update Available: v" + info.latest_version
|
||||||
button_container.visible = true
|
button_container.visible = true
|
||||||
update_button.visible = true
|
update_button.visible = true
|
||||||
skip_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:
|
else:
|
||||||
status_label.text = "Game is up to date!"
|
status_label.text = "Game up to date."
|
||||||
await get_tree().create_timer(1.0).timeout
|
_begin_resource_load()
|
||||||
_proceed_to_game()
|
|
||||||
|
|
||||||
func _on_update_check_failed(error: String) -> void:
|
func _on_update_check_failed(_error: String) -> void:
|
||||||
status_label.text = "Could not check for updates"
|
status_label.text = "Could not check for updates"
|
||||||
print("[BootScreen] Update check failed: ", error)
|
button_container.visible = true
|
||||||
|
update_button.visible = false
|
||||||
# Allow playing anyway
|
skip_button.visible = true
|
||||||
await get_tree().create_timer(1.5).timeout
|
skip_button.text = "Play Offline"
|
||||||
_proceed_to_game()
|
|
||||||
|
|
||||||
func _on_update_pressed() -> void:
|
func _on_update_pressed() -> void:
|
||||||
button_container.visible = false
|
button_container.visible = false
|
||||||
status_label.text = "Downloading update..."
|
status_label.text = "Downloading update..."
|
||||||
update_manager.download_update()
|
update_manager.download_update()
|
||||||
|
|
||||||
func _on_skip_pressed() -> void:
|
func _on_download_started(total_size: int) -> void:
|
||||||
_proceed_to_game()
|
progress_bar.visible = true
|
||||||
|
progress_label.visible = true
|
||||||
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_bar.value = 0
|
||||||
progress_label.text = "0%"
|
progress_label.text = "0%"
|
||||||
|
|
||||||
@@ -126,56 +99,73 @@ func _on_download_progress(_downloaded: int, _total: int, percentage: float) ->
|
|||||||
progress_label.text = "%.0f%%" % percentage
|
progress_label.text = "%.0f%%" % percentage
|
||||||
|
|
||||||
func _on_download_completed() -> void:
|
func _on_download_completed() -> void:
|
||||||
status_label.text = "Update installed!"
|
status_label.text = "Update installed successfully!"
|
||||||
progress_container.visible = false
|
progress_bar.visible = false
|
||||||
|
progress_label.visible = false
|
||||||
# Reload the game to apply changes
|
_begin_resource_load()
|
||||||
await get_tree().create_timer(1.0).timeout
|
|
||||||
get_tree().reload_current_scene()
|
|
||||||
|
|
||||||
func _on_download_failed(error: String) -> void:
|
func _on_download_failed(error: String) -> void:
|
||||||
status_label.text = "Update failed: " + error
|
status_label.text = "Update failed: " + error
|
||||||
progress_container.visible = false
|
progress_bar.visible = false
|
||||||
|
progress_label.visible = false
|
||||||
button_container.visible = true
|
button_container.visible = true
|
||||||
update_button.text = "Retry"
|
update_button.text = "Retry"
|
||||||
|
|
||||||
func _on_store_update_required(store_url: String) -> void:
|
func _begin_resource_load() -> void:
|
||||||
status_label.text = "Please update from the store"
|
button_container.visible = false
|
||||||
button_container.visible = true
|
status_label.text = "Loading resources..."
|
||||||
update_button.visible = false
|
progress_bar.visible = true
|
||||||
skip_button.visible = false
|
progress_label.visible = true
|
||||||
store_button.visible = true
|
progress_bar.value = 0
|
||||||
|
progress_label.text = "0%"
|
||||||
func _proceed_to_game() -> void:
|
|
||||||
# Load any previously downloaded patches
|
|
||||||
_load_existing_patches()
|
|
||||||
|
|
||||||
# Change to main game scene
|
is_loading_game = true
|
||||||
get_tree().change_scene_to_file(main_scene_path)
|
ResourceLoader.load_threaded_request(main_scene_path)
|
||||||
|
|
||||||
func _load_existing_patches() -> void:
|
func _process(_delta: float) -> void:
|
||||||
# Load any PCK files from the patches directory
|
if is_loading_game:
|
||||||
var patches_dir := "user://patches/"
|
var progress := []
|
||||||
var dir := DirAccess.open(patches_dir)
|
var status := ResourceLoader.load_threaded_get_status(main_scene_path, progress)
|
||||||
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
|
return
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
var patches: Array[String] = []
|
var changes: Array = entry.get("changelog", [])
|
||||||
dir.list_dir_begin()
|
for change in changes:
|
||||||
var patch_file := dir.get_next()
|
txt += "• " + change + "\n"
|
||||||
while patch_file != "":
|
|
||||||
if patch_file.ends_with(".pck"):
|
changelog_richtext.text = txt
|
||||||
patches.append(patch_file)
|
|
||||||
patch_file = dir.get_next()
|
|
||||||
dir.list_dir_end()
|
|
||||||
|
|
||||||
# Sort patches by version (filename includes version)
|
page_label.text = str(current_page + 1) + " / " + str(changelog_data.size())
|
||||||
patches.sort()
|
prev_button.disabled = (current_page == 0)
|
||||||
|
next_button.disabled = (current_page >= changelog_data.size() - 1)
|
||||||
# Load each patch in order
|
|
||||||
for patch in patches:
|
func _on_prev_pressed():
|
||||||
var patch_path := patches_dir + patch
|
if current_page > 0:
|
||||||
if ProjectSettings.load_resource_pack(patch_path, true):
|
current_page -= 1
|
||||||
print("[BootScreen] Loaded patch: ", patch)
|
_update_pagination_ui()
|
||||||
else:
|
|
||||||
push_warning("[BootScreen] Failed to load patch: ", patch)
|
func _on_next_pressed():
|
||||||
|
if current_page < changelog_data.size() - 1:
|
||||||
|
current_page += 1
|
||||||
|
_update_pagination_ui()
|
||||||
|
|||||||
Binary file not shown.
@@ -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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://baco51hmps6s1
|
||||||
@@ -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 +0,0 @@
|
|||||||
0.9.0
|
|
||||||
Reference in New Issue
Block a user