feat(ui): rebrand to G Launcher, add side server selector, update wallpaper and app icon, integrate GaCy-GO PS

This commit is contained in:
CAtlas0784 committed 2026-10-30 22:33:21 +07:00
1 parent 023ae32b58
commit 25843372ad
21 files changed
+397 -123

No files matched your search

+1
View File
@@ -34,6 +34,7 @@
| **Thai Mod Engine (ExcelData)** | ✅ | ✅ | ✅ | ⚠️ รอเกมจริง | `internal/language-service/thai-patch.go` มีระบบ `.backup` |
| **Multi-Language (i18n)** | ✅ | ✅ | ✅ | ✅ | 6 ภาษา (`th`, `en`, `ja`, `ko`, `vi`, `zh`) ใน `frontend/public/locales/` |
| **Standalone Tool Extraction** | ✅ | ✅ | ✅ | ✅ | แตกไฟล์ `assets/` (`7za.exe`, `hpatchz.exe`, `zift.exe`) ลง `bin/` |
| **Server Selection & GaCy PS** | ✅ | ✅ | ✅ | ✅ | ปุ่มเลือกเซิร์ฟเวอร์ด้านข้าง + ระบบดาวน์โหลด GaCy-GO (ZIP หรือ standalone sdk/server) |
---
Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 592 KiB

After

Width:  |  Height:  |  Size: 361 KiB

@@ -30,6 +30,13 @@ export function DownloadServerProgress(version) {
return $Call.ByID(1818966979, version);
}
/**
* @returns {$CancellablePromise<[string, string]>}
*/
export function FindServerExecutables() {
return $Call.ByID(1413748964);
}
/**
* @returns {$CancellablePromise<[boolean, string, string]>}
*/
Binary file not shown.

Before

Width:  |  Height:  |  Size: 305 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

+12 -12
View File
@@ -1,4 +1,4 @@
{
{
"header": {
"home": "Home",
"tools": "Tools",
@@ -13,7 +13,7 @@
"settings": "Settings",
"minimize": "Minimize",
"close": "Close",
"by": "Firefly Shelter"
"by": "GaCy"
},
"background": {
"select_bg": "Select Background",
@@ -50,14 +50,14 @@
"launcher_label": "Launcher"
},
"home": {
"header_title": "Firefly GO",
"tooltip_how_to": "How to use all tools & commands",
"header_title": "G Launcher",
"tooltip_how_to": "How to use all tools \u0026 commands",
"btn_select_game": "Select Game file",
"btn_selecting": "Selecting...",
"btn_start_game": "Start Game",
"btn_game_running": "Game is running",
"menu_change_path": "Change Game Path",
"menu_check_update": "Check for Updates Server & Proxy",
"menu_check_update": "Check for Updates Server \u0026 Proxy",
"menu_open_server": "Open server folder",
"menu_open_proxy": "Open proxy folder",
"menu_open_voice": "Open voice folder",
@@ -102,9 +102,9 @@
"modal_update_title": "Update Game",
"status_wait": "Please wait...",
"inst_title": "📋 Instructions:",
"inst_step_1": "1. Click \"Select Game Folder\" and choose your game's root directory",
"inst_step_1": "1. Click \"Select Game Folder\" and choose your game\u0027s root directory",
"inst_step_2": "2. Wait for the system to validate the game directory",
"inst_step_3": "3. Click \"Select Diff file Folder\" and choose your diff file's root directory",
"inst_step_3": "3. Click \"Select Diff file Folder\" and choose your diff file\u0027s root directory",
"inst_step_4": "4. Wait for the system to validate the diff file directory",
"inst_step_5": "5. Click \"Update Game\" to save your changes",
"toast_game_dir_not_found": "Game directory not found. Please select the correct folder.",
@@ -139,7 +139,7 @@
"btn_apply": "Apply Language Settings",
"btn_applying": "Applying...",
"inst_title": "📋 Instructions:",
"inst_step_1": "1. Click \"Select Game Folder\" and choose your game's root directory",
"inst_step_1": "1. Click \"Select Game Folder\" and choose your game\u0027s root directory",
"inst_step_2": "2. Wait for the system to validate the game directory",
"inst_step_3": "3. Select your preferred text and voice languages",
"inst_step_4": "4. Click \"Apply Language Settings\" to save your changes",
@@ -156,10 +156,10 @@
"sect1_launch_game": "Launch the game directly through the launcher with correct parameters and runtime environment.",
"sect1_lang_pre": "Support switching in-game language (e.g., EN, JP, ZH, KR) via",
"sect1_lang_link": "Language Tools",
"sect1_patch_title": "Patch & Update Game Files",
"sect1_patch_title": "Patch \u0026 Update Game Files",
"sect1_patch_desc_1_pre": "Use the",
"sect1_patch_desc_1_link": "Diff Tool",
"sect1_patch_desc_1_post": "(DiffPatch) for fast & lightweight incremental updates.",
"sect1_patch_desc_1_post": "(DiffPatch) for fast \u0026 lightweight incremental updates.",
"sect1_patch_desc_2_pre": "Supports",
"sect1_patch_desc_2_post": ", and custom diff formats.",
"sect2_title": "FireflyGo Chat Commands",
@@ -281,13 +281,13 @@
"btn_back": "Back to Home"
},
"analysis": {
"title": "Firefly Analysis & Veritas Plugin",
"title": "Firefly Analysis \u0026 Veritas Plugin",
"sect1_title": "About Veritas",
"sect1_p1_pre": "Veritas",
"sect1_p1_mid": " is a powerful ",
"sect1_p1_highlight": "Damage Logger",
"sect1_p1_post": " designed for analyzing damage in real-time during gameplay.",
"sect1_p2": "It's lightweight, fast, and easy to use for comprehensive damage analysis.",
"sect1_p2": "It\u0027s lightweight, fast, and easy to use for comprehensive damage analysis.",
"sect1_github": "GitHub Repository",
"sect2_title": "Web Analysis Tools",
"sect2_desc": "Use these web applications for real-time damage analysis with Veritas:",
+4 -4
View File
@@ -1,4 +1,4 @@
{
{
"header": {
"home": "ホーム",
"tools": "ツール",
@@ -13,7 +13,7 @@
"settings": "設定",
"minimize": "最小化",
"close": "閉じる",
"by": "Firefly Shelter"
"by": "GaCy"
},
"background": {
"select_bg": "背景を選択",
@@ -50,7 +50,7 @@
"launcher_label": "Launcher"
},
"home": {
"header_title": "Firefly GO",
"header_title": "G Launcher",
"tooltip_how_to": "ツールとコマンドの使い方",
"btn_select_game": "ゲームファイルを選択",
"btn_selecting": "選択中...",
@@ -281,7 +281,7 @@
"btn_back": "ホームに戻る"
},
"analysis": {
"title": "Firefly 分析 & Veritas プラグイン",
"title": "Firefly 分析 \u0026 Veritas プラグイン",
"sect1_title": "Veritas について",
"sect1_p1_pre": "Veritas",
"sect1_p1_mid": " は、ゲームプレイ中のダメージをリアルタイムで分析するために設計された強力な ",
+4 -4
View File
@@ -1,4 +1,4 @@
{
{
"header": {
"home": "홈",
"tools": "도구",
@@ -13,7 +13,7 @@
"settings": "설정",
"minimize": "최소화",
"close": "닫기",
"by": "Firefly Shelter"
"by": "GaCy"
},
"background": {
"select_bg": "배경 선택",
@@ -50,7 +50,7 @@
"launcher_label": "Launcher"
},
"home": {
"header_title": "Firefly GO",
"header_title": "G Launcher",
"tooltip_how_to": "도구 및 명령 사용법",
"btn_select_game": "게임 파일 선택",
"btn_selecting": "선택 중...",
@@ -281,7 +281,7 @@
"btn_back": "홈으로 돌아가기"
},
"analysis": {
"title": "Firefly 분석 & Veritas 플러그인",
"title": "Firefly 분석 \u0026 Veritas 플러그인",
"sect1_title": "Veritas 소개",
"sect1_p1_pre": "Veritas",
"sect1_p1_mid": "는 게임 플레이 중 실시간으로 피해량을 분석하도록 설계된 강력한 ",
+2 -2
View File
@@ -1,4 +1,4 @@
{
{
"header": {
"home": "หน้าแรก",
"tools": "เครื่องมือ",
@@ -57,7 +57,7 @@
"btn_start_game": "เริ่มเกม",
"btn_game_running": "เกมกำลังทำงานอยู่",
"menu_change_path": "เปลี่ยนโฟลเดอร์เกม",
"menu_check_update": "ตรวจสอบอัปเดตเซิร์ฟเวอร์ & พร็อกซี",
"menu_check_update": "ตรวจสอบอัปเดตเซิร์ฟเวอร์ \u0026 พร็อกซี",
"menu_open_server": "เปิดโฟลเดอร์เซิร์ฟเวอร์",
"menu_open_proxy": "เปิดโฟลเดอร์พร็อกซี",
"menu_open_voice": "เปิดโฟลเดอร์เสียงพากย์",
+8 -8
View File
@@ -1,4 +1,4 @@
{
{
"header": {
"home": "Trang chủ",
"tools": "Công cụ",
@@ -13,7 +13,7 @@
"settings": "Cài đặt",
"minimize": "Thu nhỏ",
"close": "Đóng",
"by": "Firefly Shelter"
"by": "GaCy"
},
"background": {
"select_bg": "Chọn nền",
@@ -50,14 +50,14 @@
"launcher_label": "Launcher"
},
"home": {
"header_title": "Firefly GO",
"tooltip_how_to": "Cách sử dụng công cụ & lệnh",
"header_title": "G Launcher",
"tooltip_how_to": "Cách sử dụng công cụ \u0026 lệnh",
"btn_select_game": "Chọn tệp Game",
"btn_selecting": "Đang chọn...",
"btn_start_game": "Bắt đầu Game",
"btn_game_running": "Game đang chạy",
"menu_change_path": "Thay đổi đường dẫn Game",
"menu_check_update": "Kiểm tra cập nhật Server & Proxy",
"menu_check_update": "Kiểm tra cập nhật Server \u0026 Proxy",
"menu_open_server": "Mở thư mục Server",
"menu_open_proxy": "Mở thư mục Proxy",
"menu_open_voice": "Mở thư mục Voice",
@@ -156,10 +156,10 @@
"sect1_launch_game": "Khởi chạy game trực tiếp thông qua launcher với các thông số và môi trường chạy chính xác.",
"sect1_lang_pre": "Hỗ trợ chuyển đổi ngôn ngữ trong game (ví dụ: EN, JP, ZH, KR) qua",
"sect1_lang_link": "Công cụ Ngôn ngữ",
"sect1_patch_title": "Vá & Cập nhật Tệp Game",
"sect1_patch_title": "Vá \u0026 Cập nhật Tệp Game",
"sect1_patch_desc_1_pre": "Sử dụng",
"sect1_patch_desc_1_link": "Công cụ Diff",
"sect1_patch_desc_1_post": "(DiffPatch) để cập nhật gia tăng nhanh chóng & nhẹ nhàng.",
"sect1_patch_desc_1_post": "(DiffPatch) để cập nhật gia tăng nhanh chóng \u0026 nhẹ nhàng.",
"sect1_patch_desc_2_pre": "Hỗ trợ",
"sect1_patch_desc_2_post": "và các định dạng diff tùy chỉnh.",
"sect2_title": "Lệnh Chat FireflyGo",
@@ -281,7 +281,7 @@
"btn_back": "Trở về Trang chủ"
},
"analysis": {
"title": "Phân tích Firefly & Plugin Veritas",
"title": "Phân tích Firefly \u0026 Plugin Veritas",
"sect1_title": "Về Veritas",
"sect1_p1_pre": "",
"sect1_p1_mid": " là một ",
+4 -4
View File
@@ -1,4 +1,4 @@
{
{
"header": {
"home": "首页",
"tools": "工具",
@@ -13,7 +13,7 @@
"settings": "设置",
"minimize": "最小化",
"close": "关闭",
"by": "Firefly Shelter"
"by": "GaCy"
},
"background": {
"select_bg": "选择背景",
@@ -50,7 +50,7 @@
"launcher_label": "Launcher"
},
"home": {
"header_title": "Firefly GO",
"header_title": "G Launcher",
"tooltip_how_to": "如何使用工具与命令",
"btn_select_game": "选择游戏文件",
"btn_selecting": "正在选择...",
@@ -281,7 +281,7 @@
"btn_back": "返回主页"
},
"analysis": {
"title": "Firefly 分析 & Veritas 插件",
"title": "Firefly 分析 \u0026 Veritas 插件",
"sect1_title": "关于 Veritas",
"sect1_p1_pre": "Veritas",
"sect1_p1_mid": " 是一款强大的",
+1 -1
View File
@@ -1,4 +1,4 @@
import { Link } from "@tanstack/react-router";
import { Link } from "@tanstack/react-router";
import useModalStore from "@/stores/modalStore";
import { Blend, BookOpen, Diff, Home, Info, Languages, Minus, Puzzle, Settings, TrendingUpDown, Wrench, X } from "lucide-react";
import { AppService } from "@bindings/g-launcher/internal/app-service";
+17 -6
View File
@@ -9,7 +9,15 @@ export async function CheckUpdateServer(
serverVersion: string
): Promise<{ isUpdate: boolean; isExists: boolean; version: string }> {
const [ok, latestVersion, error] = await GitService.GetLatestServerVersion()
const isExists = await FSService.FileExists(serverPath)
let isExists = await FSService.FileExists(serverPath)
if (!isExists) {
const [gameServer] = await GitService.FindServerExecutables()
if (gameServer) {
useSettingStore.getState().setServerPath(gameServer)
isExists = true
}
}
if (!ok) {
toast.error("Server error: " + error)
@@ -23,15 +31,18 @@ export async function CheckUpdateServer(
export async function UpdateServer(serverVersion: string) : Promise<void> {
const {setDownloadType } = useLauncherStore.getState()
const {setServerPath, setServerVersion} = useSettingStore.getState()
setDownloadType("Downloading server...")
const {setServerPath, setSdkServerPath, setServerVersion} = useSettingStore.getState()
setDownloadType("Downloading GaCy PS server...")
const [ok, error] = await GitService.DownloadServerProgress(serverVersion)
if (ok) {
setDownloadType("Unzipping server...")
GitService.UnzipServer()
setDownloadType("Configuring server files...")
const [gameServer, sdkServer] = await GitService.FindServerExecutables()
setDownloadType("Download server successfully")
setServerVersion(serverVersion)
setServerPath("./server/firefly-go_win.exe")
setServerPath(gameServer || "./server/gameserver-go.exe")
if (sdkServer) {
setSdkServerPath(sdkServer)
}
} else {
toast.error(error)
setDownloadType("Download server failed")
+154 -61
View File
@@ -1,7 +1,8 @@
import { useEffect } from 'react';
import { Play, Menu, FolderOpen, MessageCircleQuestionMark } from 'lucide-react';
import { Play, Menu, FolderOpen, MessageCircleQuestionMark, Server, Globe, CheckCircle2 } from 'lucide-react';
import { AppService } from '@bindings/g-launcher/internal/app-service';
import { FSService } from '@bindings/g-launcher/internal/fs-service';
import { GitService } from '@bindings/g-launcher/internal/git-service';
import { toast } from 'react-toastify';
import path from 'path-browserify'
import useSettingStore from '@/stores/settingStore';
@@ -21,11 +22,16 @@ export default function LauncherPage() {
setGamePath,
setGameDir,
serverPath,
setServerPath,
sdkServerPath,
setSdkServerPath,
proxyPath,
gameDir,
serverVersion,
proxyVersion,
background
background,
selectedServer,
setSelectedServer
} = useSettingStore()
const { t } = useTranslation()
const {
@@ -40,8 +46,6 @@ export default function LauncherPage() {
const {
isLoading,
downloadType,
serverReady,
proxyReady,
isDownloading,
serverRunning,
proxyRunning,
@@ -49,7 +53,6 @@ export default function LauncherPage() {
progressDownload,
downloadSpeed,
updateData,
// launcherVersion,
setLauncherVersion,
setIsLoading,
setDownloadType,
@@ -64,13 +67,13 @@ export default function LauncherPage() {
const widgetLinks = [
{
tooltip: "Firefly SRAnalysis",
tooltip: "SRAnalysis",
href: "https://sranalysis.punklorde.org",
img: "https://sranalysis.punklorde.org/ff-sranalysis.png",
btnClass: "btn-primary"
},
{
tooltip: "Firefly SRTools",
tooltip: "SRTools",
href: "https://srtools.punklorde.org",
img: "https://srtools.punklorde.org/ff-srtool.png",
btnClass: "btn-secondary"
@@ -91,7 +94,18 @@ export default function LauncherPage() {
return
}
const serverExists = await FSService.FileExists(serverPath)
let serverExists = await FSService.FileExists(serverPath)
if (!serverExists) {
const [gameServer, sdkServer] = await GitService.FindServerExecutables()
if (gameServer) {
setServerPath(gameServer)
serverExists = true
}
if (sdkServer) {
setSdkServerPath(sdkServer)
}
}
const proxyExists = await FSService.FileExists(proxyPath)
setServerReady(serverExists)
setProxyReady(proxyExists)
@@ -121,17 +135,10 @@ export default function LauncherPage() {
proxy: proxyData,
launcher: launcherData
})
const exitGame = await FSService.FileExists(gamePath)
if (!exitGame) {
setGameRunning(false)
setGamePath("")
setGameDir("")
}
if (!serverData.isExists || !proxyData.isExists) {
setServerReady(false)
setProxyReady(false)
setIsOpenDownloadDataModal(true)
return
}
@@ -175,6 +182,7 @@ export default function LauncherPage() {
const handleStartGame = async () => {
if (!gamePath) {
toast.info("Please select the game executable first")
return
}
if (gameRunning) {
@@ -182,36 +190,59 @@ export default function LauncherPage() {
}
try {
setIsLoading(true)
if (selectedServer === "gacy-ps") {
const [gameServer, sdkServer] = await GitService.FindServerExecutables()
const activeServer = gameServer || serverPath
if (!activeServer) {
toast.error("Please download GaCy PS server files first")
setIsOpenDownloadDataModal(true)
return
}
// 1. Start Proxy
const activeProxy = proxyPath || "./proxy/firefly-go-proxy.exe"
if (!proxyRunning && !gamePath.endsWith("launcher.exe")) {
const [resultProxy, error] = await FSService.StartWithConsole(proxyPath)
const [resultProxy, error] = await FSService.StartWithConsole(activeProxy)
if (!resultProxy) {
toast.error(t("home.toast_start_proxy_failed") + error)
toast.error(t("home.toast_start_proxy_failed") + ": " + error)
return
}
setProxyRunning(true)
}
await sleep(500)
// 2. Start SDK Server if exists
const activeSdk = sdkServer || sdkServerPath
if (activeSdk) {
await FSService.StartWithConsole(activeSdk)
await sleep(400)
}
// 3. Start Game Server
if (!serverRunning) {
const [resultServer, error] = await FSService.StartWithConsole(serverPath)
const [resultServer, error] = await FSService.StartWithConsole(activeServer)
if (!resultServer) {
toast.error(t("home.toast_start_server_failed") + error)
toast.error(t("home.toast_start_server_failed") + ": " + error)
return
}
setServerRunning(true)
}
await sleep(1000)
}
// 4. Start Game
if (gamePath.endsWith("launcher.exe")) {
const [resultGame, error] = await FSService.StartWithConsole(gamePath)
if (!resultGame) {
toast.error(t("home.toast_start_game_failed") + error)
toast.error(t("home.toast_start_game_failed") + ": " + error)
return
}
} else {
const [resultGame, error] = await FSService.StartApp(gamePath)
if (!resultGame) {
toast.error(t("home.toast_start_game_failed") + error)
toast.error(t("home.toast_start_game_failed") + ": " + error)
return
}
}
@@ -266,58 +297,116 @@ export default function LauncherPage() {
return (
<div className="relative min-h-fit overflow-hidden">
<img
src={background}
src={background || "bg-1.jpeg"}
alt="background"
className="fixed inset-0 w-full h-full object-cover z-0"
onError={(e) => {
const target = e.currentTarget as HTMLImageElement
target.src = "bg-12.jpg"
target.src = "bg-1.jpeg"
}}
/>
{/* Header */}
{/* Header Title */}
<header className="hidden sm:flex fixed z-10 items-center justify-between py-6 px-4">
<div className="text-2xl font-bold text-white bg-gray-500/5 rounded-full p-1">{t("home.header_title")}</div>
<div className="text-2xl font-black tracking-wide text-white bg-black/40 backdrop-blur-md rounded-2xl px-4 py-1.5 border border-white/10 shadow-lg">
<span className="text-cyan-400">G</span> Launcher
</div>
</header>
<div className="hidden sm:flex fixed top-1/4 right-4 z-10 flex-col space-y-3 bg-white/5 rounded-xl p-3 shadow-lg">
{/* Side Server Selector */}
<div className="hidden sm:flex fixed top-1/4 right-5 z-20 flex-col items-end gap-3">
<div className="text-[11px] uppercase tracking-wider font-bold text-white/80 bg-black/50 backdrop-blur-md px-3.5 py-1 rounded-full border border-white/15 shadow-sm">
เลือกเซิร์ฟเวอร์ (Select Server)
</div>
{/* Server Option 1: GaCy PS */}
<button
onClick={() => setSelectedServer("gacy-ps")}
className={`flex items-center gap-3 w-64 p-3 rounded-2xl backdrop-blur-md transition-all duration-200 border cursor-pointer ${
selectedServer === "gacy-ps"
? "bg-cyan-500/30 border-cyan-400 text-white shadow-xl shadow-cyan-500/20 ring-2 ring-cyan-400"
: "bg-black/55 border-white/15 text-white/80 hover:bg-black/75 hover:border-cyan-400/50"
}`}
>
<div className={`p-2.5 rounded-xl ${selectedServer === "gacy-ps" ? "bg-cyan-500 text-black font-bold" : "bg-cyan-500/20 text-cyan-400"}`}>
<Server className="w-5 h-5" />
</div>
<div className="flex flex-col text-left flex-1 min-w-0">
<div className="flex items-center justify-between">
<span className="font-bold text-sm tracking-tight truncate">GaCy PS</span>
<span className="badge badge-xs badge-info font-mono text-[10px]">4.6.54</span>
</div>
<span className="text-xs text-white/60 truncate">Private Server (Local)</span>
</div>
{selectedServer === "gacy-ps" && (
<CheckCircle2 className="w-5 h-5 text-cyan-400 shrink-0" />
)}
</button>
{/* Server Option 2: Official Server */}
<button
onClick={() => setSelectedServer("official")}
className={`flex items-center gap-3 w-64 p-3 rounded-2xl backdrop-blur-md transition-all duration-200 border cursor-pointer ${
selectedServer === "official"
? "bg-emerald-500/30 border-emerald-400 text-white shadow-xl shadow-emerald-500/20 ring-2 ring-emerald-400"
: "bg-black/55 border-white/15 text-white/80 hover:bg-black/75 hover:border-emerald-400/50"
}`}
>
<div className={`p-2.5 rounded-xl ${selectedServer === "official" ? "bg-emerald-500 text-black font-bold" : "bg-emerald-500/20 text-emerald-400"}`}>
<Globe className="w-5 h-5" />
</div>
<div className="flex flex-col text-left flex-1 min-w-0">
<div className="flex items-center justify-between">
<span className="font-bold text-sm tracking-tight truncate">Official Server</span>
<span className="badge badge-xs badge-success font-mono text-[10px]">Live</span>
</div>
<span className="text-xs text-white/60 truncate">Hoyoverse Official</span>
</div>
{selectedServer === "official" && (
<CheckCircle2 className="w-5 h-5 text-emerald-400 shrink-0" />
)}
</button>
{/* External Tools Mini Bar */}
<div className="flex items-center gap-2 pt-2">
{widgetLinks.map((link, idx) => (
<div key={idx} className="tooltip tooltip-left" data-tip={link.tooltip}>
<div key={idx} className="tooltip tooltip-bottom" data-tip={link.tooltip}>
<a
className={`btn btn-circle ${link.btnClass}`}
className="btn btn-circle btn-sm bg-black/50 border-white/20 hover:border-cyan-400"
target="_blank"
href={link.href}
>
<img
src={link.img}
alt={link.tooltip}
className="w-8 h-8 rounded-full"
className="w-5 h-5 rounded-full"
/>
</a>
</div>
))}
<div className="tooltip tooltip-left" data-tip={t("home.tooltip_how_to")}>
<div className="tooltip tooltip-bottom" data-tip={t("home.tooltip_how_to")}>
<Link
to="/howto"
className="btn btn-warning btn-circle"
className="btn btn-circle btn-sm btn-warning"
>
<MessageCircleQuestionMark className="w-6 h-6 font-bold rounded-full" />
<MessageCircleQuestionMark className="w-4 h-4 font-bold rounded-full" />
</Link>
</div>
</div>
</div>
{/* Left Background Selector */}
<div className="hidden sm:flex fixed top-1/2 left-5 z-10">
<BackgroundSelector />
</div>
{/* Bottom Panel */}
{serverReady && proxyReady && !isDownloading && (
<div className="fixed bottom-0 right-0 p-8 z-2">
{/* Bottom Play Panel: Appears once a server is selected! */}
{selectedServer !== "" && !isDownloading && (
<div className="fixed bottom-0 right-0 p-8 z-2 animate-in fade-in slide-in-from-bottom-4 duration-300">
<div className="flex flex-wrap items-center justify-center gap-2">
{gamePath === "" ? (
<button
className="btn btn-accent btn-xl font-bold"
className="btn btn-accent btn-xl font-bold shadow-xl"
onClick={handlePickFile}
>
<FolderOpen className="w-6 h-6" />
@@ -325,19 +414,27 @@ export default function LauncherPage() {
</button>
) : (
<button
className="btn btn-warning btn-xl font-bold"
className={`btn btn-xl font-bold shadow-2xl transition-all ${
selectedServer === "gacy-ps"
? "btn-warning text-black hover:brightness-110"
: "btn-success text-white hover:brightness-110"
}`}
onClick={handleStartGame}
>
<Play className="w-6 h-6" />
{isLoading ? t("home.btn_selecting") : gameRunning ? t("home.btn_game_running") : t("home.btn_start_game")}
<Play className="w-6 h-6 fill-current" />
{isLoading
? t("home.btn_selecting")
: gameRunning
? t("home.btn_game_running")
: `${t("home.btn_start_game")} [${selectedServer === "gacy-ps" ? "GaCy PS" : "Official"}]`}
</button>
)}
<div className="dropdown dropdown-top dropdown-end">
<div tabIndex={0} role="button" className="btn btn-black btn-circle btn-xl m-1">
<div tabIndex={0} role="button" className="btn btn-black btn-circle btn-xl m-1 border border-white/20 shadow-xl">
<Menu className="w-6 h-6" />
</div>
<ul tabIndex={0} className="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm">
<ul tabIndex={0} className="dropdown-content menu bg-black/80 backdrop-blur-md rounded-box z-1 w-56 p-2 shadow-2xl border border-white/10 text-white">
<li><button onClick={handlePickFile}>{t("home.menu_change_path")}</button></li>
<li>
<button
@@ -364,23 +461,16 @@ export default function LauncherPage() {
</button>
</li>
<li>
</li>
<li>
<button disabled={!serverPath} onClick={() => {
if (serverPath) {
<button onClick={() => {
FSService.OpenFolder("./server")
}
}}
>
{t("home.menu_open_server")}
</button>
</li>
<li>
<button disabled={!proxyPath} onClick={() => {
if (proxyPath) {
<button onClick={() => {
FSService.OpenFolder("./proxy")
}
}}
>
{t("home.menu_open_proxy")}
@@ -397,20 +487,27 @@ export default function LauncherPage() {
</button>
</li>
</ul>
</div>
</div>
</div>
)}
{/* Downloading */}
{/* Notification when no server is selected yet */}
{selectedServer === "" && !isDownloading && (
<div className="fixed bottom-8 right-8 z-2 flex items-center gap-2 bg-black/60 backdrop-blur-md px-5 py-3 rounded-2xl border border-white/20 text-white shadow-xl animate-pulse">
<span className="text-cyan-400 text-lg">👉</span>
<span className="text-sm font-medium">กรุณาเลือกเซิร์ฟเวอร์ด้านบนเพื่อเริ่มเล่น (Select a server to play)</span>
</div>
)}
{/* Downloading Progress Bar */}
{isDownloading && (
updateData.proxy.isUpdate
|| updateData.server.isUpdate
|| !updateData.proxy.isExists
|| !updateData.server.isExists
) && (
<div className="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-10 w-[60vw] bg-black/20 backdrop-blur-sm rounded-lg p-4 shadow-lg">
<div className="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-10 w-[60vw] bg-black/60 backdrop-blur-md rounded-2xl p-4 shadow-2xl border border-white/10">
<div className="space-y-3">
<div className="flex justify-center items-center text-sm text-white/80">
<span>{downloadType}</span>
@@ -434,8 +531,9 @@ export default function LauncherPage() {
</div>
)}
{/* Launcher Self Update Bar */}
{isDownloading && updateData.launcher.isUpdate && (
<div className="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-10 w-[60vw] bg-black/20 backdrop-blur-sm rounded-lg p-4 shadow-lg">
<div className="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-10 w-[60vw] bg-black/60 backdrop-blur-md rounded-2xl p-4 shadow-2xl border border-white/10">
<div className="space-y-3 text-sm text-white/80 text-center">
{["update:launcher:downloading", "update:launcher:success", "update:launcher:failed"].includes(downloadType) && (
<div className="flex justify-center items-center gap-4 ml-4">
@@ -477,12 +575,7 @@ export default function LauncherPage() {
</div>
)}
<div className="hidden md:block fixed bottom-4 left-5 z-10">
<img src="/heart-hsr.gif" alt="firefly animation" className="rounded-lg w-24 h-24" />
</div>
{/* Modal */}
{/* Modals */}
<UpdateModal
isOpen={isOpenUpdateDataModal}
onClose={() => setIsOpenUpdateDataModal(false)}
+9 -1
View File
@@ -7,9 +7,11 @@ interface SettingState {
gamePath: string;
gameDir: string;
serverPath: string;
sdkServerPath: string;
proxyPath: string;
serverVersion: string;
proxyVersion: string;
selectedServer: string;
closingOption: {
isMinimize: boolean;
isAsk: boolean;
@@ -18,11 +20,13 @@ interface SettingState {
extraBackgrounds: string[];
setExtraBackgrounds: (newExtraBackgrounds: string[]) => void;
setBackground: (newBackground: string) => void;
setSelectedServer: (newServer: string) => void;
setClosingOption: (newClosingOption: { isMinimize: boolean; isAsk: boolean }) => void;
setLocale: (newLocale: string) => void;
setGamePath: (newGamePath: string) => void;
setGameDir: (newGameDir: string) => void;
setServerPath: (newServerPath: string) => void;
setSdkServerPath: (newSdkServerPath: string) => void;
setProxyPath: (newProxyPath: string) => void;
setServerVersion: (newServerVersion: string) => void;
setProxyVersion: (newProxyVersion: string) => void;
@@ -35,22 +39,26 @@ const useSettingStore = create<SettingState>()(
gamePath: "",
gameDir: "",
serverPath: "",
sdkServerPath: "",
proxyPath: "",
serverVersion: "",
proxyVersion: "",
selectedServer: "",
closingOption: {
isMinimize: false,
isAsk: true,
},
background: "bg-12.jpg",
background: "bg-1.jpeg",
extraBackgrounds: [],
setExtraBackgrounds: (newExtraBackgrounds: string[]) => set({ extraBackgrounds: newExtraBackgrounds }),
setBackground: (newBackground: string) => set({ background: newBackground }),
setSelectedServer: (newServer: string) => set({ selectedServer: newServer }),
setClosingOption: (newClosingOption: { isMinimize: boolean; isAsk: boolean }) => set({ closingOption: newClosingOption }),
setLocale: (newLocale: string) => set({ locale: newLocale }),
setGamePath: (newGamePath: string) => set({ gamePath: newGamePath }),
setGameDir: (newGameDir: string) => set({ gameDir: newGameDir }),
setServerPath: (newServerPath: string) => set({ serverPath: newServerPath }),
setSdkServerPath: (newSdkServerPath: string) => set({ sdkServerPath: newSdkServerPath }),
setProxyPath: (newProxyPath: string) => set({ proxyPath: newProxyPath }),
setServerVersion: (newServerVersion: string) => set({ serverVersion: newServerVersion }),
setProxyVersion: (newProxyVersion: string) => set({ proxyVersion: newProxyVersion }),
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

+1 -1
View File
@@ -128,7 +128,7 @@ func (f *FSService) StartWithConsole(path string) (bool, string) {
base := strings.ToLower(filepath.Base(path))
if base == "launcher.exe" || base == "starrail.exe" {
application.Get().Event.Emit("game:exit")
} else if base == "firefly-go_win.exe" || base == "gameserver.exe" {
} else if base == "firefly-go_win.exe" || base == "gameserver.exe" || base == "gameserver-go.exe" || base == "server.exe" || base == "sdkserver-go.exe" || base == "sdkserver.exe" {
application.Get().Event.Emit("server:exit")
} else if base == "firefly-go-proxy.exe" || base == "proxy.exe" {
application.Get().Event.Emit("proxy:exit")
+21 -3
View File
@@ -40,9 +40,11 @@ func (g *GitService) GetLatestLauncherVersion() (bool, string, string) {
}
}
// Fallback to Gitea/Forgejo/GitHub Releases API
client := &http.Client{Timeout: 10 * time.Second}
// Fallback to Gitea Releases API
if constant.LauncherGitUrl != "" {
resp, err := http.Get(constant.LauncherGitUrl)
resp, err := client.Get(constant.LauncherGitUrl)
if err == nil {
defer resp.Body.Close()
body, readErr := io.ReadAll(resp.Body)
@@ -55,7 +57,23 @@ func (g *GitService) GetLatestLauncherVersion() (bool, string, string) {
}
}
return false, "", "no launcher updates found"
// Fallback to Gitea Tags API
tagsURL := "https://gaytea.cod-algieba.ts.net/api/v1/repos/GaCy/G-Launcher/tags"
resp, err := client.Get(tagsURL)
if err == nil {
defer resp.Body.Close()
body, readErr := io.ReadAll(resp.Body)
if readErr == nil {
var tags []struct {
Name string `json:"name"`
}
if json.Unmarshal(body, &tags) == nil && len(tags) > 0 {
return true, tags[0].Name, ""
}
}
}
return true, constant.CurrentLauncherVersion, ""
}
func (g *GitService) UpdateLauncherProgress(version string) (bool, string) {
+150 -14
View File
@@ -2,26 +2,31 @@ package gitService
import (
"encoding/json"
"fmt"
"g-launcher/pkg/constant"
"g-launcher/pkg/models"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/wailsapp/wails/v3/pkg/application"
)
func (g *GitService) GetLatestServerVersion() (bool, string, string) {
resp, err := http.Get(constant.ServerGitUrl)
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Get(constant.ServerGitUrl)
if err != nil {
return false, "", err.Error()
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
return false, "", err.Error()
}
var releases []models.ReleaseType
err = json.Unmarshal(body, &releases)
@@ -37,35 +42,166 @@ func (g *GitService) GetLatestServerVersion() (bool, string, string) {
}
func (g *GitService) DownloadServerProgress(version string) (bool, string) {
asset, ok := g.getReleaseAsset(version, constant.ServerGitUrl, constant.ServerZipFile)
if !ok {
return false, "no release found"
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Get(constant.ServerGitUrl)
if err != nil {
return false, fmt.Sprintf("fetch server releases: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return false, fmt.Sprintf("read server releases: %v", err)
}
var releases []*models.ReleaseType
if err := json.Unmarshal(body, &releases); err != nil || len(releases) == 0 {
return false, "no releases found on server repository"
}
var targetRelease *models.ReleaseType
for _, rel := range releases {
if rel.TagName == version {
targetRelease = rel
break
}
}
if targetRelease == nil && len(releases) > 0 {
targetRelease = releases[0]
}
if targetRelease == nil {
return false, "could not find server release matching version " + version
}
if err := os.MkdirAll(constant.ServerStorageUrl, 0755); err != nil {
return false, err.Error()
}
saveFile := filepath.Join(constant.ServerStorageUrl, asset.Name)
tmpPath, err := g.downloadFileParallel(saveFile, asset.BrowserDownloadURL, 4, func(percent float64, speed string) {
// 1. Search for .zip asset
var zipAsset *models.AssetType
for _, asset := range targetRelease.Assets {
if strings.HasSuffix(strings.ToLower(asset.Name), ".zip") {
aCopy := asset
zipAsset = &aCopy
break
}
}
if zipAsset != nil {
// Case A: ZIP archive found
saveZipPath := filepath.Join(constant.ServerStorageUrl, zipAsset.Name)
tmpPath, err := g.downloadFileParallel(saveZipPath, zipAsset.BrowserDownloadURL, 4, func(percent float64, speed string) {
application.Get().Event.Emit("download:server", map[string]interface{}{
"percent": fmt.Sprintf("%.2f", percent),
"speed": speed,
})
})
if err != nil {
return false, err.Error()
return false, fmt.Sprintf("download server zip: %v", err)
}
renamed := false
for i := 0; i < 3; i++ {
if err := os.Rename(tmpPath, saveFile); err == nil {
return true, ""
if err := os.Rename(tmpPath, saveZipPath); err == nil {
renamed = true
break
}
time.Sleep(300 * time.Millisecond)
}
return false, "failed to rename tmp file after retries"
if !renamed {
os.Remove(tmpPath)
return false, "failed to rename downloaded server zip"
}
// Unzip immediately
if err := g.unzipParallel(saveZipPath, constant.ServerStorageUrl); err != nil {
return false, fmt.Sprintf("unzip server: %v", err)
}
os.Remove(saveZipPath)
return true, ""
}
// Case B: No ZIP archive, download sdkserver and server executables
var sdkAsset, serverAsset *models.AssetType
for _, asset := range targetRelease.Assets {
nameLower := strings.ToLower(asset.Name)
if strings.Contains(nameLower, "sdkserver") {
aCopy := asset
sdkAsset = &aCopy
} else if strings.Contains(nameLower, "server") {
aCopy := asset
serverAsset = &aCopy
}
}
if sdkAsset == nil && serverAsset == nil {
return false, "no zip or server executable assets found in release"
}
if sdkAsset != nil {
saveSdk := filepath.Join(constant.ServerStorageUrl, sdkAsset.Name)
tmpPath, err := g.downloadFileParallel(saveSdk, sdkAsset.BrowserDownloadURL, 4, func(percent float64, speed string) {
application.Get().Event.Emit("download:server", map[string]interface{}{
"percent": fmt.Sprintf("%.2f", percent*0.5),
"speed": speed,
})
})
if err == nil {
_ = os.Rename(tmpPath, saveSdk)
}
}
if serverAsset != nil {
saveServer := filepath.Join(constant.ServerStorageUrl, serverAsset.Name)
tmpPath, err := g.downloadFileParallel(saveServer, serverAsset.BrowserDownloadURL, 4, func(percent float64, speed string) {
application.Get().Event.Emit("download:server", map[string]interface{}{
"percent": fmt.Sprintf("%.2f", 50.0+percent*0.5),
"speed": speed,
})
})
if err == nil {
_ = os.Rename(tmpPath, saveServer)
}
}
return true, ""
}
func (g *GitService) UnzipServer() {
g.unzipParallel(filepath.Join(constant.ServerStorageUrl, constant.ServerZipFile), constant.ServerStorageUrl)
os.Remove(filepath.Join(constant.ServerStorageUrl, constant.ServerZipFile))
entries, err := os.ReadDir(constant.ServerStorageUrl)
if err != nil {
return
}
for _, entry := range entries {
if !entry.IsDir() && strings.HasSuffix(strings.ToLower(entry.Name()), ".zip") {
zipPath := filepath.Join(constant.ServerStorageUrl, entry.Name())
_ = g.unzipParallel(zipPath, constant.ServerStorageUrl)
_ = os.Remove(zipPath)
}
}
}
func (g *GitService) FindServerExecutables() (string, string) {
serverDir := constant.ServerStorageUrl
var gameServer, sdkServer string
// Candidate filenames for game server
for _, name := range []string{"gameserver-go.exe", "server.exe", "gameserver.exe", "firefly-go_win.exe"} {
p := filepath.Join(serverDir, name)
if info, err := os.Stat(p); err == nil && !info.IsDir() {
gameServer = p
break
}
}
// Candidate filenames for sdk server
for _, name := range []string{"sdkserver-go.exe", "sdkserver.exe"} {
p := filepath.Join(serverDir, name)
if info, err := os.Stat(p); err == nil && !info.IsDir() {
sdkServer = p
break
}
}
return gameServer, sdkServer
}
+2 -2
View File
@@ -1,7 +1,7 @@
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 ServerGitUrl = "https://gaytea.cod-algieba.ts.net/api/v1/repos/GaCySR/GaCy-GO/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"
@@ -11,7 +11,7 @@ const LauncherBuildTempDir = "temp/updater_build"
const ServerStorageUrl = "./server"
const ProxyStorageUrl = "./proxy"
const ServerZipFile = "prebuild_win_x86.zip"
const ServerZipFile = "gacy 4.6.54.zip"
const ProxyFile = "firefly-go-proxy.exe"
const LauncherFile = "G-Launcher.exe"
const TempUrl = "./temp"