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:
commit
d34dbe16ab
14 files changed
+708
-61
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -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!
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
# 🚀 G-Launcher
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
**A lightweight, modern, and high-performance game launcher for Anime & Hoyoverse games.**
|
||||
Built with **Go**, **Wails v3**, **React 19**, and **Tailwind CSS v4**.
|
||||
|
||||
[](https://go.dev/)
|
||||
[](https://v3.wails.io/)
|
||||
[](https://react.dev/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](https://tailwindcss.com/)
|
||||
[](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
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,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="*"/>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user