Merge pull request 'Dev' (#2) from Dev into main

Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
GaCy committed 2026-10-30 21:13:37 +07:00
commit d34dbe16ab
14 files changed
+708 -61

No files matched your search

+133
View File
@@ -0,0 +1,133 @@
name: Build G-Launcher Release
on:
workflow_dispatch:
inputs:
tag_name:
description: 'Release Tag Name (e.g. latest, release, v1.0.0)'
required: false
default: 'release'
custom_version:
description: 'Override Version (leave blank for auto-increment)'
required: false
default: ''
push:
tags:
- 'v*'
permissions:
contents: write
concurrency:
group: build-g-launcher-release
cancel-in-progress: false
jobs:
build:
runs-on: windows-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache-dependency-path: go.sum
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Install Wails CLI
shell: pwsh
run: |
go install github.com/wailsapp/wails/v3/cmd/wails3@v3.0.0-alpha.74
"$env:USERPROFILE\go\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Calculate launcher version
shell: pwsh
run: |
$customVersion = "${{ github.event.inputs.custom_version }}"
$releaseTag = if ("${{ github.event.inputs.tag_name }}") { "${{ github.event.inputs.tag_name }}" } else { "release" }
if ($customVersion -and $customVersion.Trim() -ne "") {
$version = $customVersion.Trim()
Write-Host "Using user-specified version: $version"
} else {
$manifestPath = Join-Path $env:RUNNER_TEMP "latest.json"
# Attempt to fetch latest.json from current repository release
$repo = "${{ github.repository }}"
$manifestUrl = "https://github.com/$repo/releases/download/$releaseTag/latest.json"
$cacheBust = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
$requestUrl = "$($manifestUrl)?cachebust=$cacheBust"
Write-Host "Checking current manifest from: $requestUrl"
$status = & curl.exe --location --silent --show-error --output $manifestPath --write-out "%{http_code}" $requestUrl
if ($status -eq "200") {
$currentVersion = (Get-Content -Raw $manifestPath | ConvertFrom-Json).version
Write-Host "Found previous version: $currentVersion"
$version = node scripts/launcher-release.mjs next $currentVersion
} else {
Write-Host "No previous manifest found (HTTP $status). Starting at 1.0.0"
$version = "1.0.0"
}
}
[System.IO.File]::AppendAllText($env:GITHUB_ENV, "LAUNCHER_VERSION=$version" + [Environment]::NewLine)
[System.IO.File]::AppendAllText($env:GITHUB_ENV, "RELEASE_TAG=$releaseTag" + [Environment]::NewLine)
node scripts/launcher-release.mjs stamp-info $version build/windows/info.json
Write-Host "Configured G-Launcher version $version"
- name: Install Frontend Dependencies
working-directory: frontend
run: npm ci
- name: Build Frontend
working-directory: frontend
run: npm run build
- name: Build Windows Executable
shell: pwsh
run: |
wails3 task build BUILD_VERSION=$env:LAUNCHER_VERSION PRODUCTION=true
env:
PRODUCTION: "true"
- name: Prepare Release Assets
shell: pwsh
run: |
$builtLauncher = "bin/G-Launcher.exe"
if (!(Test-Path $builtLauncher)) {
$fallback = Get-ChildItem -Path "bin" -Filter "*.exe" | Select-Object -First 1
if ($fallback) {
$builtLauncher = $fallback.FullName
} else {
throw "Build output was not found in bin directory."
}
}
$releaseLauncher = Join-Path $env:RUNNER_TEMP "G-Launcher.exe"
Copy-Item -LiteralPath $builtLauncher -Destination $releaseLauncher -Force
$manifestPath = Join-Path $env:RUNNER_TEMP "latest.json"
node scripts/launcher-release.mjs manifest $env:LAUNCHER_VERSION $releaseLauncher $manifestPath
Write-Host "Release assets prepared:"
Get-Item $releaseLauncher, $manifestPath | Select-Object Name, Length
- name: Publish Release Assets
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.RELEASE_TAG }}
files: |
${{ runner.temp }}/G-Launcher.exe
${{ runner.temp }}/latest.json
overwrite_files: true
fail_on_unmatched_files: true
+60
View File
@@ -0,0 +1,60 @@
# 📜 G-Launcher Development Rules & Engineering Guidelines
> กฎและแนวทางการพัฒนา G-Launcher จากบทเรียนโปรเจกต์เดิมและคำแนะนำของ Horoyoi-san
---
### 1. 🏗️ แก้ที่ระบบหลังบ้านก่อน UI เสมอ (System-First Architecture)
* **คำแนะนำจาก Horoyoi:** *"ระบบมันรองรับ build zig แนะนำตอนแก้ ให้แก้ที่ระบบก่อน แล้วค่อยไปแก้ UI หน้าต่างต่างๆ ของ launcher ไม่งั้นเดี๋ยวพัง"*
* **แนวปฏิบัติ:**
1. ทุกครั้งที่เพิ่มฟีเจอร์หรือแก้ไข logic ให้เริ่มที่ **Go Backend** (`internal/`, `pkg/`, `main.go`) ให้เสร็จสิ้น
2. รัน `go vet ./...` เพื่อให้แน่ใจว่าไม่มี type error หรือ compilation error
3. Generate หรืออัปเดต TypeScript Bindings (`@bindings/...`)
4. จากนั้นจึงค่อยไปแก้ที่ **Frontend UI** (`frontend/src/`) เพื่อเชื่อมต่อกับ API / Event ของ Backend
5. **เหตุผล:** Wails v3 มี Bindings และ Event IPC ที่ผูกกันแน่น หากแก้ UI ก่อนโดยที่ Backend ยังไม่พร้อม หน้าต่าง Launcher จะ crash หรือเกิด runtime exception ทันที
---
### 2. 🚫 ห้าม Hardcode เด็ดขาด (Strict No-Hardcoding Rule)
* **ห้ามฝังค่าต่อไปนี้ในโค้ด logic โดยตรง:**
* URL, Domain, IP Address, หรือ Port (เช่น API Endpoint, Git Release URLs, Proxy URLs)
* File Path สัมบูรณ์ (ห้ามใส่ `C:\...` หรือ Drive Letter ใดๆ)
* ชื่อไฟล์หรือโฟลเดอร์เฉพาะทางที่อาจเปลี่ยนแปลงได้
* **แนวปฏิบัติ:**
* รวมศูนย์ค่า Configuration ไว้ที่ [`pkg/constant/constant.go`](file:///c:/Users/Phitchayut/Desktop/G%20Launcher/pkg/constant/constant.go) หรือไฟล์ Configuration กลาง
* สำหรับ Path ให้ใช้ `filepath.Join(...)` เสมอ และเก็บข้อมูลผู้ใช้ไว้ที่ `os.UserConfigDir()` (`%APPDATA%`)
* เปิดช่องให้ผู้ใช้สามารถปรับแต่ง URL เซิร์ฟเวอร์, พร็อกซี หรือพารามิเตอร์ของเกมได้ผ่าน UI Settings หรือ Config File
---
### 3. 🏷️ การเปลี่ยนชื่อและ References (Refactoring & Naming Rule)
* **คำแนะนำจาก Horoyoi:** *"ส่วนชื่อที่เป็น sunbringer หรือชื่อผมค่อยไปเปลี่ยนทีหลังนะ"*
* **แนวปฏิบัติ:**
* ห้ามใช้วิธี Find-and-Replace ชื่อโปรเจกต์เดิม (เช่น `sunbringer`, `firefly`) แบบสุ่มสี่สุ่มห้าทั่วทั้งโปรเจกต์ เพราะอาจกระทบ internal protocol, protobuf identifier, build manifest หรือ package bindings
* แยก User-Facing Branding (เช่น ชื่อหน้าต่าง, Title, Tooltip, Config metadata) ออกจาก System/Package naming ให้ชัดเจน
* เปลี่ยนชื่อส่วนที่จำเป็นเมื่อระบบ core นิ่งแล้วเท่านั้น
---
### 4. 🛡️ ความปลอดภัยของข้อมูลและการ Rollback (Data Safety & Rollback)
* **บทเรียนจาก GaC-hkrpg-go / Ananta:**
* ไฟล์ตัวเกม Hoyoverse / Unity มีขนาดใหญ่มากและเสียหายได้ง่าย
* **Safe Backup:** ก่อนจะ Patch, แก้ไข หรือเขียนทับไฟล์ใดๆ ของเกม (เช่น `.dll`, `.exe`, asset binaries) จะต้องสำรองไฟล์เดิมไว้เป็นนามสกุล `.backup` เสมอ
* **Atomic Rollback:** หากการ Copy, Verify หรือ Download เกิดความล้มเหลว ระบบจะต้อง Rollback นำไฟล์สำรองกลับคืนมาทันที ห้ามทิ้งไฟล์เกมให้อยู่ในสภาพเสียหาย
* **Atomic Write สำหรับ Config:** การบันทึกไฟล์คอนฟิก (เช่น `game-selection.json`) ให้เขียนเป็น `.tmp` ก่อน แล้วใช้ Windows API `MoveFileEx` เพื่อป้องกันไฟล์เสียหายจากไฟดับหรือ crash
---
### 5. 🧰 Zero-Dependency & Portability First
* ไม่สมมติว่าเครื่องผู้ใช้ปลายทางมี Git, Zig หรือ 7-Zip อยู่ใน PATH
* ต้องมี Toolchain Helper และ Portable Runtimes ในตัว:
* **MinGit:** ใช้ระบบ Auto-download MinGit portable หากตรวจไม่พบ Git ในระบบ
* **Tools Extraction:** ดึง binary tools (`7za.exe`, `hpatchz.exe`, `zift.exe`) ออกจาก embed assets อัตโนมัติเมื่อเริ่มโปรแกรม
---
### 6. 🌿 Git Workflow & Code Hygiene
* **Active Branch:** ทำงานและ Commit บน branch **`Dev`** เท่านั้น
* **Pre-commit Verification:**
* ฝั่ง Go: `go vet ./...` ต้องผ่าน 100%
* ฝั่ง Frontend: `npm run build` ต้องผ่าน 100%
* **Clean Repository:** รักษาความสะอาดของ `.gitignore` ห้าม commit ไฟล์ `_references/`, `dist/`, `node_modules/`, `bin/`, `temp/`, หรือ `server/` เข้าสู่ Git
+1 -2
View File
@@ -34,6 +34,5 @@ release:
update:
@echo Updating build assets...
wails3 update build-assets -name "firefly-launcher" -binaryname "firefly-launcher" -config build/config.yml
wails3 update build-assets -name "g-launcher" -binaryname "g-launcher" -config build/config.yml
@echo Done!
+213
View File
@@ -0,0 +1,213 @@
# 🚀 G-Launcher
<div align="center">
![G-Launcher Banner](frontend/public/bg-1.jpeg)
**A lightweight, modern, and high-performance game launcher for Anime & Hoyoverse games.**
Built with **Go**, **Wails v3**, **React 19**, and **Tailwind CSS v4**.
[![Go Version](https://img.shields.io/badge/Go-1.24%2B-00ADD8?style=flat&logo=go)](https://go.dev/)
[![Wails Version](https://img.shields.io/badge/Wails-v3--alpha-DF0000?style=flat&logo=wails)](https://v3.wails.io/)
[![React Version](https://img.shields.io/badge/React-19-61DAFB?style=flat&logo=react)](https://react.dev/)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.9%2B-3178C6?style=flat&logo=typescript)](https://www.typescriptlang.org/)
[![Tailwind CSS](https://img.shields.io/badge/Tailwind_CSS-v4-38B2AC?style=flat&logo=tailwind-css)](https://tailwindcss.com/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[English](#-english) | [ภาษาไทย](#-ภาษาไทย)
</div>
---
## 🇹🇭 ภาษาไทย
### ✨ ฟีเจอร์เด่น (Key Features)
* **🎮 Game Process & Environment Management:**
* ตรวจสอบและเปิดเกมพร้อมพารามิเตอร์ที่ถูกต้องผ่านระบบจัดการ Console ของ Windows (`CREATE_NEW_CONSOLE | CREATE_BREAKAWAY_FROM_JOB`)
* ตรวจจับสถานะการทำงานของตัวเกมและเซิร์ฟเวอร์แบบ Real-time พร้อมแจ้งเตือนเมื่อเกมปิดตัวลง
* **🌐 ระบบหลายภาษา (Multi-Language UI & Game Patcher):**
* หน้า UI รองรับ **ภาษาไทย (TH)**, **English (EN)**, **日本語 (JA)**, **한국어 (KO)**, **Tiếng Việt (VI)**, และ **中文 (ZH)**
* เครื่องมือสลับภาษาภายในเกมผ่าน Binary `ExcelData` Patcher (EN, JP, CN, KR) และรองรับ Thai Mod Pack สำหรับตัวเกม Beta
* **📦 ระบบอัปเดตไฟล์เกมแบบลดขนาด (Incremental Diff Patching):**
* รองรับไฟล์อัปเดตแบบ **HDiffPatch** โดยตรงจาก Chunk Protobuf Manifest ของเกม (`proto/firefly.proto`)
* รองรับการลงแพตช์ไฟล์นามสกุล `.ziff` ด้วย **Zift Tool** (`zift.exe`)
* ตรวจสอบความถูกต้องของไฟล์ผ่าน Checksum (MD5 / SHA-256)
* **🔄 ระบบอัปเดต Launcher อัตโนมัติ (Smart Self-Update):**
* อัปเดตผ่าน Manifest `latest.json` พร้อมตรวจสอบความสมบูรณ์ด้วย SHA-256
* มีระบบ Fallback ไปยัง Gitea/GitHub Releases API
* ป้องกันการทับไฟล์ในโหมด Development อัตโนมัติ
* **🛡️ ความปลอดภัยของข้อมูล (Safe Data & Atomic Operations):**
* จดจำโฟลเดอร์เกมใน AppData (`%APPDATA%/g-launcher/game-selection.json`) ด้วย Windows API `MoveFileEx` แบบ Atomic ป้องกันไฟล์เสียหาย
* สำรองไฟล์ตัวจริงเป็น `.backup` ก่อนลงแพตช์ และมีระบบ Rollback คืนสภาพไฟล์อัตโนมัติหากการลงแพตช์ล้มเหลว
* **🧰 พร้อมใช้งานแบบไม่ต้องติดตั้งเครื่องมือเสริม (Zero-Dependency):**
* **Portable MinGit:** ดาวน์โหลด Git แบบพกพามาติดตั้งในตัวอัตโนมัติหากตรวจไม่พบ Git ในเครื่องผู้ใช้
* บรรจุ Binary Tools สำคัญ (`7za.exe`, `hpatchz.exe`, `zift.exe`) ไว้ในตัวโปรแกรมและแตกไฟล์ใช้งานอัตโนมัติ
* **🎨 ปรับแต่งหน้าตาได้ตามใจ (Customization & Themes):**
* หน้าต่างไร้ขอบ (Frameless Glassmorphism) พร้อมถาดระบบ (System Tray Menu)
* ระบบเปลี่ยนภาพพื้นหลัง (Background Selector) รองรับทั้งคลังภาพในตัว, อัปโหลดจากเครื่อง หรือใส่ Image URL
---
### 🏗️ สถาปัตยกรรมระบบ (Architecture)
```mermaid
flowchart TD
subgraph Frontend["Frontend (React 19 + TypeScript + Vite)"]
UI["UI Pages & Modals"]
Router["TanStack Router"]
Store["Zustand Stores"]
I18n["i18next Locales"]
Bindings["TypeScript Bindings (@bindings/*)"]
UI --> Router
Router --> Store
Store --> Bindings
UI --> I18n
end
subgraph IPC["Wails v3 IPC Gateway"]
IPCBridge["Bidirectional Events & RPC Bindings"]
end
subgraph Backend["Backend (Go 1.24+)"]
AppSvc["AppService (Window Controls)"]
FSSvc["FSService (Process & Storage)"]
GitSvc["GitService (Update & MinGit)"]
DiffSvc["DiffService (HDiff & Zift)"]
LangSvc["LanguageService (Excel & Mod)"]
end
subgraph System["Windows OS & Game Files"]
GameExe["Game Process (StarRail.exe)"]
GameFiles["Game Assets & Backup Files"]
Config["User Config (%APPDATA%/g-launcher)"]
Tools["Tools (MinGit, 7za, Zift)"]
end
Bindings <--> IPCBridge
IPCBridge <--> AppSvc
IPCBridge <--> FSSvc
IPCBridge <--> GitSvc
IPCBridge <--> DiffSvc
IPCBridge <--> LangSvc
FSSvc --> GameExe
FSSvc --> Config
GitSvc --> Tools
DiffSvc --> GameFiles
LangSvc --> GameFiles
```
---
### 💻 ข้อกำหนดของระบบและเครื่องมือพัฒนา (Prerequisites)
* [Go 1.24+](https://go.dev/dl/)
* [Node.js 22+](https://nodejs.org/)
* [Wails v3 CLI](https://v3.wails.io/):
```bash
go install github.com/wailsapp/wails/v3/cmd/wails3@v3.0.0-alpha.74
```
---
### 🛠️ วิธีการรันและบิลด์ (Development & Build)
#### 1. ติดตั้ง Dependencies
```bash
# ติดตั้งแพ็กเกจ Frontend
cd frontend
npm install
cd ..
# ติดตั้งโมดูล Go
go mod tidy
```
#### 2. รันในโหมดพัฒนา (Development Mode)
```bash
# รัน Wails dev (มี Hot Reload ทั้ง Frontend และ Backend)
wails3 dev
# หรือรันสคริปต์
.\run.bat
```
#### 3. บิลด์สำหรับใช้งานจริง (Production Build)
```bash
# บิลด์แบบปกติผ่าน Wails Task
wails3 task build PRODUCTION=true
# หรือใช้สคริปต์ Build พร้อมบีบอัดขนาดไฟล์ด้วย UPX
.\build.bat
```
#### 4. สร้างตัวติดตั้ง (Installer Packaging)
```bash
wails3 task package
```
---
## 🇬🇧 English
### ✨ Features
* **Game Process Orchestration:** Launch games with decoupled console execution flags (`CREATE_NEW_CONSOLE | CREATE_BREAKAWAY_FROM_JOB`) with active process lifecycle hooks.
* **Full Multi-Language Support:** Fully localized in English, Thai, Japanese, Korean, Vietnamese, and Chinese with live switching in the header. Includes game asset translation utilities.
* **Smart Incremental Patching:** Parse Hoyoverse manifest protobuf chunks, apply `.hdiff` delta patches with `hpatchz`, and apply `.ziff` archives via `zift`.
* **Reliable CI/CD Self-Updating:** Integrated release workflows via `.github/workflows/build-launcher.yml` and `scripts/launcher-release.mjs` verifying SHA-256 hashes against `latest.json`.
* **Zero-Dependency Tooling:** Auto-configures MinGit portable and extracts required binaries (`7za.exe`, `hpatchz.exe`, `zift.exe`) automatically.
* **Data Safety First:** Atomic config updates with Windows `MoveFileEx` and automatic rollback on patch failure.
---
### 📁 Project Structure
```
G-Launcher/
├── .github/workflows/ # GitHub Actions release & build workflows
├── assets/ # Embedded binary tools (7za, hpatchz, zift, appicon)
├── build/ # Wails build configurations & OS templates
│ ├── config.yml # App metadata (CompanyName, ProductName, AppID)
│ └── windows/ # Windows manifests, icons, info.json
├── frontend/ # React 19 Frontend application
│ ├── bindings/ # Auto-generated Wails TypeScript bindings
│ ├── public/ # Static assets, backgrounds, locales (en, th, ja, ko, vi, zh)
│ └── src/ # React components, pages, stores, hooks
├── internal/ # Go Backend Services
│ ├── app-service/ # Window lifecycle & version management
│ ├── diff-service/ # HDiffz, Ziff, and LDiff chunk patching
│ ├── fs-service/ # File dialogs, process runner, persistent storage
│ ├── git-service/ # Self-update, server/proxy downloader, portable MinGit
│ └── language-service/ # ExcelData binary patcher & Thai mod engine
├── pkg/ # Shared Go packages & constants
│ ├── constant/ # Centralized URLs & configuration
│ ├── firefly/ # Protobuf parser & chunk loader
│ ├── hpatchz/ # HPatchz wrapper
│ └── sevenzip/ # 7-Zip archive utilities
├── proto/ # Protobuf definitions
├── scripts/ # Release & version calculation scripts (launcher-release.mjs)
├── AGENTS.md # Engineering guidelines & development rules
├── build.bat # Windows build & UPX compression script
├── main.go # Application entry point & service registration
└── Taskfile.yml # Wails build tasks definition
```
---
### 📜 Engineering Guidelines
1. **System-First Rule:** Always implement and verify changes in the **Go Backend** (`internal/`, `pkg/`) first. Run `go vet ./...` and generate bindings before touching Frontend UI.
2. **Strict No-Hardcoding:** Do not hardcode URLs, IP addresses, ports, or absolute drive paths. Centralize constants in [`pkg/constant/constant.go`](pkg/constant/constant.go).
3. **Branch Workflow:** All active development takes place on branch **`Dev`**.
---
### 🤝 Credits & Acknowledgments
* **[Firefly Shelter](https://git.kain.io.vn/Firefly-Shelter/Firefly_Launcher)** — The original upstream foundation, proxy architecture, and UI design.
* **[Horoyoi-san](https://github.com/horoyoi-san/game-Launcher)** — Architectural enhancements (portable MinGit, safe rollback, Zig integration patterns, and update workflows).
* **[Wails](https://v3.wails.io/)** — Next-generation desktop application framework for Go.
* Developed with ❤️ by **GaCy**.
+1 -2
View File
@@ -7,7 +7,7 @@ includes:
linux: ./build/linux/Taskfile.yml
vars:
APP_NAME: "firefly-launcher"
APP_NAME: "G-Launcher"
BIN_DIR: "bin"
VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}'
@@ -31,4 +31,3 @@ tasks:
summary: Runs the application in development mode
cmds:
- wails3 dev -config ./build/config.yml -port {{.VITE_PORT}}
+14 -14
View File
@@ -1,15 +1,15 @@
{
"fixed": {
"file_version": "1.0.0"
},
"info": {
"0000": {
"ProductVersion": "1.0.0",
"CompanyName": "Firefly Shelter",
"FileDescription": "Firefly Launcher",
"LegalCopyright": "@ 2025, Firefly Shelter",
"ProductName": "Firefly Launcher",
"Comments": "Firefly Launcher"
}
}
}
"fixed": {
"file_version": "1.0.0"
},
"info": {
"0000": {
"ProductVersion": "1.0.0",
"CompanyName": "GaCy",
"FileDescription": "G-Launcher",
"LegalCopyright": "@ 2026, GaCy",
"ProductName": "G-Launcher",
"Comments": "G-Launcher"
}
}
}
+4 -4
View File
@@ -5,19 +5,19 @@
!include "FileFunc.nsh"
!ifndef INFO_PROJECTNAME
!define INFO_PROJECTNAME "firefly-launcher"
!define INFO_PROJECTNAME "g-launcher"
!endif
!ifndef INFO_COMPANYNAME
!define INFO_COMPANYNAME "Firefly Shelter"
!define INFO_COMPANYNAME "GaCy"
!endif
!ifndef INFO_PRODUCTNAME
!define INFO_PRODUCTNAME "Firefly Launcher"
!define INFO_PRODUCTNAME "G-Launcher"
!endif
!ifndef INFO_PRODUCTVERSION
!define INFO_PRODUCTVERSION "1.0.0"
!endif
!ifndef INFO_COPYRIGHT
!define INFO_COPYRIGHT "@ 2025, Firefly Shelter"
!define INFO_COPYRIGHT "@ 2025, GaCy"
!endif
!ifndef PRODUCT_EXECUTABLE
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity type="win32" name="com.fireflyshelter.fireflylauncher" version="1.0.0" processorArchitecture="*"/>
<assemblyIdentity type="win32" name="com.gacy.glauncher" version="1.0.0" processorArchitecture="*"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
+157 -35
View File
@@ -1,57 +1,179 @@
package gitService
import (
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"encoding/json"
"g-launcher/pkg/constant"
"g-launcher/pkg/models"
"fmt"
"io"
"net/http"
"github.com/minio/selfupdate"
"net/url"
"os"
"regexp"
"strings"
"time"
"g-launcher/pkg/constant"
"g-launcher/pkg/models"
"github.com/minio/selfupdate"
)
type GitService struct{}
func (g *GitService) GetLatestLauncherVersion() (bool, string, string) {
resp, err := http.Get(constant.LauncherGitUrl)
if err != nil {
return false, "", err.Error()
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var releases []models.ReleaseType
err = json.Unmarshal(body, &releases)
if err != nil {
return false, "", err.Error()
}
if len(releases) == 0 {
return false, "", "no releases found"
}
return true, releases[0].TagName, ""
type launcherManifest struct {
Version string `json:"version"`
SHA256 string `json:"sha256"`
}
var launcherVersionPattern = regexp.MustCompile(`^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$`)
func (g *GitService) GetLatestLauncherVersion() (bool, string, string) {
// First, attempt to check via latest.json manifest
if constant.LauncherManifestURL != "" {
manifest, err := fetchLauncherManifest(constant.LauncherManifestURL)
if err == nil && manifest.Version != "" {
return true, manifest.Version, ""
}
}
// Fallback to Gitea/Forgejo/GitHub Releases API
if constant.LauncherGitUrl != "" {
resp, err := http.Get(constant.LauncherGitUrl)
if err == nil {
defer resp.Body.Close()
body, readErr := io.ReadAll(resp.Body)
if readErr == nil {
var releases []models.ReleaseType
if json.Unmarshal(body, &releases) == nil && len(releases) > 0 {
return true, releases[0].TagName, ""
}
}
}
}
return false, "", "no launcher updates found"
}
func (g *GitService) UpdateLauncherProgress(version string) (bool, string) {
asset, ok := g.getReleaseAsset(version, constant.LauncherGitUrl, constant.LauncherFile)
if !ok {
return false, "no release found"
if !constant.LauncherUpdatesEnabled {
return false, "launcher updates are disabled in development builds"
}
resp, err := http.Get(asset.BrowserDownloadURL)
if err != nil {
return false, err.Error()
// Mode 1: Manifest-based update with SHA-256 integrity verification
if constant.LauncherManifestURL != "" && constant.LauncherDownloadURL != "" {
manifest, err := fetchLauncherManifest(constant.LauncherManifestURL)
if err == nil && manifest.Version == version && manifest.SHA256 != "" {
return g.applyManifestUpdate(manifest)
}
}
defer resp.Body.Close()
err = selfupdate.Apply(resp.Body, selfupdate.Options{})
if err != nil {
return false, err.Error()
// Mode 2: Release API fallback
if constant.LauncherGitUrl != "" {
asset, ok := g.getReleaseAsset(version, constant.LauncherGitUrl, constant.LauncherFile)
if ok && asset.BrowserDownloadURL != "" {
resp, err := http.Get(asset.BrowserDownloadURL)
if err != nil {
return false, err.Error()
}
defer resp.Body.Close()
err = selfupdate.Apply(resp.Body, selfupdate.Options{})
if err != nil {
return false, err.Error()
}
return true, ""
}
}
return false, "could not locate valid launcher update package"
}
func (g *GitService) applyManifestUpdate(manifest launcherManifest) (bool, string) {
client := &http.Client{Timeout: 10 * time.Minute}
response, err := client.Get(constant.LauncherDownloadURL)
if err != nil {
return false, fmt.Errorf("download launcher update: %w", err).Error()
}
defer response.Body.Close()
if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices {
return false, fmt.Sprintf("download launcher update: server returned %s", response.Status)
}
temporaryFile, err := os.CreateTemp("", "g-launcher-update-*.exe")
if err != nil {
return false, fmt.Errorf("create temporary launcher update: %w", err).Error()
}
temporaryPath := temporaryFile.Name()
defer os.Remove(temporaryPath)
hash := sha256.New()
if _, err := io.Copy(io.MultiWriter(temporaryFile, hash), response.Body); err != nil {
temporaryFile.Close()
return false, fmt.Errorf("save launcher update: %w", err).Error()
}
if err := temporaryFile.Close(); err != nil {
return false, fmt.Errorf("close launcher update: %w", err).Error()
}
expectedHash, err := hex.DecodeString(manifest.SHA256)
if err != nil || subtle.ConstantTimeCompare(hash.Sum(nil), expectedHash) != 1 {
return false, "launcher update checksum mismatch"
}
updateFile, err := os.Open(temporaryPath)
if err != nil {
return false, fmt.Errorf("open launcher update: %w", err).Error()
}
defer updateFile.Close()
if err := selfupdate.Apply(updateFile, selfupdate.Options{}); err != nil {
return false, fmt.Errorf("apply launcher update: %w", err).Error()
}
return true, ""
}
}
func fetchLauncherManifest(manifestURL string) (launcherManifest, error) {
parsedURL, err := url.Parse(manifestURL)
if err != nil || (parsedURL.Scheme != "https" && parsedURL.Scheme != "http") {
return launcherManifest{}, fmt.Errorf("invalid launcher manifest URL")
}
query := parsedURL.Query()
query.Set("_", fmt.Sprintf("%d", time.Now().UnixNano()))
parsedURL.RawQuery = query.Encode()
request, err := http.NewRequest(http.MethodGet, parsedURL.String(), nil)
if err != nil {
return launcherManifest{}, fmt.Errorf("create launcher manifest request: %w", err)
}
request.Header.Set("Cache-Control", "no-cache")
request.Header.Set("User-Agent", "G-Launcher")
client := &http.Client{Timeout: 20 * time.Second}
response, err := client.Do(request)
if err != nil {
return launcherManifest{}, fmt.Errorf("check launcher updates: %w", err)
}
defer response.Body.Close()
if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices {
return launcherManifest{}, fmt.Errorf("check launcher updates: server returned %s", response.Status)
}
var manifest launcherManifest
if err := json.NewDecoder(io.LimitReader(response.Body, 1<<20)).Decode(&manifest); err != nil {
return launcherManifest{}, fmt.Errorf("read launcher update manifest: %w", err)
}
if !launcherVersionPattern.MatchString(manifest.Version) {
return launcherManifest{}, fmt.Errorf("launcher update manifest has invalid version %q", manifest.Version)
}
manifest.SHA256 = strings.TrimSpace(manifest.SHA256)
if len(manifest.SHA256) != sha256.Size*2 {
return launcherManifest{}, fmt.Errorf("launcher update manifest has invalid SHA-256")
}
if _, err := hex.DecodeString(manifest.SHA256); err != nil {
return launcherManifest{}, fmt.Errorf("launcher update manifest has invalid SHA-256: %w", err)
}
manifest.SHA256 = strings.ToLower(manifest.SHA256)
return manifest, nil
}
+6 -3
View File
@@ -2,15 +2,18 @@ package constant
const ProxyGitUrl = "https://git.kain.io.vn/api/v1/repos/Firefly-Shelter/FireflyGo_Proxy/releases"
const ServerGitUrl = "https://git.kain.io.vn/api/v1/repos/Firefly-Shelter/FireflyGo_Local_Archive/releases"
const LauncherGitUrl = "https://git.kain.io.vn/api/v1/repos/Firefly-Shelter/Firefly_Launcher/releases"
const LauncherGitUrl = "https://gaytea.cod-algieba.ts.net/api/v1/repos/GaCy/G-Launcher/releases"
const LauncherManifestURL = "https://gaytea.cod-algieba.ts.net/GaCy/G-Launcher/releases/download/latest/latest.json"
const LauncherDownloadURL = "https://gaytea.cod-algieba.ts.net/GaCy/G-Launcher/releases/download/latest/G-Launcher.exe"
const ServerStorageUrl = "./server"
const ProxyStorageUrl = "./proxy"
const ServerZipFile = "prebuild_win_x86.zip"
const ProxyFile = "firefly-go-proxy.exe"
const LauncherFile = "g-launcher.exe"
const LauncherFile = "G-Launcher.exe"
const TempUrl = "./temp"
const CurrentLauncherVersion = "1.0.0"
var CurrentLauncherVersion = "1.0.0"
type ToolFile string
@@ -0,0 +1,5 @@
//go:build !production
package constant
const LauncherUpdatesEnabled = false
@@ -0,0 +1,5 @@
//go:build production
package constant
const LauncherUpdatesEnabled = true
+74
View File
@@ -0,0 +1,74 @@
import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { pathToFileURL } from "node:url";
const versionPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
export function nextLauncherVersion(currentVersion) {
const match = currentVersion.match(versionPattern);
if (!match) {
throw new Error(`Invalid launcher version: ${currentVersion}`);
}
let [major, minor, patch] = match.slice(1).map(Number);
patch += 1;
if (patch === 10) {
patch = 0;
minor += 1;
if (minor === 10) {
minor = 0;
major += 1;
}
}
return `${major}.${minor}.${patch}`;
}
export async function createLauncherManifest(version, executablePath) {
if (!versionPattern.test(version)) {
throw new Error(`Invalid launcher version: ${version}`);
}
const executable = await readFile(executablePath);
return {
version,
sha256: createHash("sha256").update(executable).digest("hex"),
};
}
async function main(args) {
const [command, ...values] = args;
if (command === "next" && values.length === 1) {
console.log(nextLauncherVersion(values[0]));
return;
}
if (command === "manifest" && values.length === 3) {
const [version, executablePath, manifestPath] = values;
const manifest = await createLauncherManifest(version, executablePath);
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
return;
}
if (command === "stamp-info" && values.length === 2) {
const [version, infoPath] = values;
if (!versionPattern.test(version)) {
throw new Error(`Invalid launcher version: ${version}`);
}
const info = JSON.parse(await readFile(infoPath, "utf8"));
info.fixed.file_version = version;
info.info["0000"].ProductVersion = version;
await writeFile(infoPath, `${JSON.stringify(info, null, 2)}\n`);
return;
}
throw new Error(
"Usage: launcher-release.mjs next <version> | manifest <version> <exe> <output> | stamp-info <version> <info.json>",
);
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main(process.argv.slice(2)).catch((error) => {
console.error(error.message);
process.exitCode = 1;
});
}
+34
View File
@@ -0,0 +1,34 @@
import assert from "node:assert/strict";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "node:test";
import { createLauncherManifest, nextLauncherVersion } from "./launcher-release.mjs";
test("increments patch digits and rolls over minor and major digits", () => {
assert.equal(nextLauncherVersion("2.1.0"), "2.1.1");
assert.equal(nextLauncherVersion("2.1.9"), "2.2.0");
assert.equal(nextLauncherVersion("2.9.9"), "3.0.0");
});
test("rejects versions outside the three-number sequence", () => {
assert.throws(() => nextLauncherVersion("2.1"), /Invalid launcher version/);
assert.throws(() => nextLauncherVersion("02.1.0"), /Invalid launcher version/);
});
test("creates a manifest with the executable SHA-256", async () => {
const directory = await mkdtemp(join(tmpdir(), "sunbringer-release-test-"));
const executablePath = join(directory, "launcher.exe");
try {
await writeFile(executablePath, "test executable");
const manifest = await createLauncherManifest("2.1.1", executablePath);
assert.equal(manifest.version, "2.1.1");
assert.equal(
manifest.sha256,
"ff8715f0270731bbdb0bb3586a77d032f5e883b8909dcafbf3e8909cb8c71201",
);
} finally {
await rm(directory, { recursive: true, force: true });
}
});