diff --git a/.github/workflows/build-launcher.yml b/.github/workflows/build-launcher.yml
new file mode 100644
index 0000000..8eb3a6f
--- /dev/null
+++ b/.github/workflows/build-launcher.yml
@@ -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
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..2b4b290
--- /dev/null
+++ b/AGENTS.md
@@ -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
diff --git a/Makefile b/Makefile
index a43b99e..a1f4775 100644
--- a/Makefile
+++ b/Makefile
@@ -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!
-
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..cdb9166
--- /dev/null
+++ b/README.md
@@ -0,0 +1,213 @@
+# 🚀 G-Launcher
+
+
+
+
+
+**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) | [ภาษาไทย](#-ภาษาไทย)
+
+
+
+---
+
+## 🇹🇭 ภาษาไทย
+
+### ✨ ฟีเจอร์เด่น (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**.
diff --git a/Taskfile.yml b/Taskfile.yml
index 68db0be..a05d590 100644
--- a/Taskfile.yml
+++ b/Taskfile.yml
@@ -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}}
-
diff --git a/build/windows/info.json b/build/windows/info.json
index 4ef303f..8012c74 100644
--- a/build/windows/info.json
+++ b/build/windows/info.json
@@ -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"
- }
- }
-}
\ No newline at end of file
+ "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"
+ }
+ }
+}
diff --git a/build/windows/nsis/wails_tools.nsh b/build/windows/nsis/wails_tools.nsh
index 95ee9fa..5fa65da 100644
--- a/build/windows/nsis/wails_tools.nsh
+++ b/build/windows/nsis/wails_tools.nsh
@@ -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"
diff --git a/build/windows/wails.exe.manifest b/build/windows/wails.exe.manifest
index f0d70ed..d2f300b 100644
--- a/build/windows/wails.exe.manifest
+++ b/build/windows/wails.exe.manifest
@@ -1,6 +1,6 @@
-
+
diff --git a/internal/git-service/launcher.go b/internal/git-service/launcher.go
index 930504c..3c2235b 100644
--- a/internal/git-service/launcher.go
+++ b/internal/git-service/launcher.go
@@ -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, ""
-}
\ No newline at end of file
+}
+
+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
+}
diff --git a/pkg/constant/constant.go b/pkg/constant/constant.go
index 1654d4c..e0496fc 100644
--- a/pkg/constant/constant.go
+++ b/pkg/constant/constant.go
@@ -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
diff --git a/pkg/constant/launcher_updates_development.go b/pkg/constant/launcher_updates_development.go
new file mode 100644
index 0000000..e3a4a86
--- /dev/null
+++ b/pkg/constant/launcher_updates_development.go
@@ -0,0 +1,5 @@
+//go:build !production
+
+package constant
+
+const LauncherUpdatesEnabled = false
diff --git a/pkg/constant/launcher_updates_production.go b/pkg/constant/launcher_updates_production.go
new file mode 100644
index 0000000..fa4bc73
--- /dev/null
+++ b/pkg/constant/launcher_updates_production.go
@@ -0,0 +1,5 @@
+//go:build production
+
+package constant
+
+const LauncherUpdatesEnabled = true
diff --git a/scripts/launcher-release.mjs b/scripts/launcher-release.mjs
new file mode 100644
index 0000000..fda1577
--- /dev/null
+++ b/scripts/launcher-release.mjs
@@ -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 | manifest