7 Commits
1.1 ... 1.4.0

Author SHA1 Message Date
b2adcd7981 UPDATE: update new language patch 2025-08-21 21:44:16 +07:00
ba58d24e06 FIX: fix bug constant 2025-08-19 22:31:20 +07:00
ec72b812de UPDATE: native with new hdiff type 2025-08-19 20:01:31 +07:00
448ced1260 FIX: fix admin require 2025-08-08 12:26:45 +07:00
d4c75b341f UPDATE: update icon 2025-07-26 22:19:47 +07:00
fb1cd82434 FIX: fix bug about checking version 2025-07-26 22:07:28 +07:00
461dfd93ba UPDATE: Add self update 2025-07-26 20:59:35 +07:00
53 changed files with 1638 additions and 410 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 944 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -6,7 +6,7 @@
<key>CFBundleName</key> <key>CFBundleName</key>
<string>Firefly Launcher</string> <string>Firefly Launcher</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>Firefly Launcher</string> <string>firefly-launcher</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>com.fireflyshelter.fireflylauncher</string> <string>com.fireflyshelter.fireflylauncher</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>

View File

@@ -6,7 +6,7 @@
<key>CFBundleName</key> <key>CFBundleName</key>
<string>Firefly Launcher</string> <string>Firefly Launcher</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>Firefly Launcher</string> <string>firefly-launcher</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>com.fireflyshelter.fireflylauncher</string> <string>com.fireflyshelter.fireflylauncher</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>

Binary file not shown.

View File

@@ -3,7 +3,7 @@
# #
# The lines below are called `modelines`. See `:help modeline` # The lines below are called `modelines`. See `:help modeline`
name: "Firefly Launcher" name: "firefly-launcher"
arch: ${GOARCH} arch: ${GOARCH}
platform: "linux" platform: "linux"
version: "0.1.0" version: "0.1.0"
@@ -17,12 +17,12 @@ license: "MIT"
release: "1" release: "1"
contents: contents:
- src: "./bin/Firefly Launcher" - src: "./bin/firefly-launcher"
dst: "/usr/local/bin/Firefly Launcher" dst: "/usr/local/bin/firefly-launcher"
- src: "./build/appicon.png" - src: "./build/appicon.png"
dst: "/usr/share/icons/hicolor/128x128/apps/Firefly Launcher.png" dst: "/usr/share/icons/hicolor/128x128/apps/firefly-launcher.png"
- src: "./build/linux/Firefly Launcher.desktop" - src: "./build/linux/firefly-launcher.desktop"
dst: "/usr/share/applications/Firefly Launcher.desktop" dst: "/usr/share/applications/firefly-launcher.desktop"
depends: depends:
- gtk3 - gtk3

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 124 KiB

View File

@@ -1,13 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?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"> <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="0.1.0" processorArchitecture="*"/> <assemblyIdentity type="win32" name="com.fireflyshelter.fireflylauncher" version="0.1.0" processorArchitecture="*"/>
<dependency> <dependency>
<dependentAssembly> <dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/> <assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly> </dependentAssembly>
</dependency> </dependency>
<!-- Yêu cầu quyền admin -->
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"> <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security> <security>
<requestedPrivileges> <requestedPrivileges>

View File

@@ -0,0 +1,24 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
/**
* @param {number} timeout
* @returns {Promise<[boolean, string]> & { cancel(): void }}
*/
export function CloseAppAfterTimeout(timeout) {
let $resultPromise = /** @type {any} */($Call.ByID(982751559, timeout));
return $resultPromise;
}
/**
* @returns {Promise<[boolean, string]> & { cancel(): void }}
*/
export function GetCurrentLauncherVersion() {
let $resultPromise = /** @type {any} */($Call.ByID(2542090372));
return $resultPromise;
}

View File

@@ -25,20 +25,26 @@ export function DownloadServerProgress(version) {
} }
/** /**
* @param {string} oldVersion
* @returns {Promise<[boolean, string, string]> & { cancel(): void }} * @returns {Promise<[boolean, string, string]> & { cancel(): void }}
*/ */
export function GetLatestProxyVersion(oldVersion) { export function GetLatestLauncherVersion() {
let $resultPromise = /** @type {any} */($Call.ByID(1462362449, oldVersion)); let $resultPromise = /** @type {any} */($Call.ByID(3191972855));
return $resultPromise; return $resultPromise;
} }
/** /**
* @param {string} oldVersion
* @returns {Promise<[boolean, string, string]> & { cancel(): void }} * @returns {Promise<[boolean, string, string]> & { cancel(): void }}
*/ */
export function GetLatestServerVersion(oldVersion) { export function GetLatestProxyVersion() {
let $resultPromise = /** @type {any} */($Call.ByID(1447982978, oldVersion)); let $resultPromise = /** @type {any} */($Call.ByID(1462362449));
return $resultPromise;
}
/**
* @returns {Promise<[boolean, string, string]> & { cancel(): void }}
*/
export function GetLatestServerVersion() {
let $resultPromise = /** @type {any} */($Call.ByID(1447982978));
return $resultPromise; return $resultPromise;
} }
@@ -57,3 +63,12 @@ export function UnzipServer() {
let $resultPromise = /** @type {any} */($Call.ByID(4110296071)); let $resultPromise = /** @type {any} */($Call.ByID(4110296071));
return $resultPromise; return $resultPromise;
} }
/**
* @param {string} version
* @returns {Promise<[boolean, string]> & { cancel(): void }}
*/
export function UpdateLauncherProgress(version) {
let $resultPromise = /** @type {any} */($Call.ByID(2648938152, version));
return $resultPromise;
}

View File

@@ -6,6 +6,15 @@
// @ts-ignore: Unused imports // @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime"; import {Call as $Call, Create as $Create} from "@wailsio/runtime";
/**
* @param {string} patchPath
* @returns {Promise<[boolean, string, string]> & { cancel(): void }}
*/
export function CheckTypeHDiff(patchPath) {
let $resultPromise = /** @type {any} */($Call.ByID(1068035136, patchPath));
return $resultPromise;
}
/** /**
* @param {string} gamePath * @param {string} gamePath
* @returns {Promise<[boolean, string]> & { cancel(): void }} * @returns {Promise<[boolean, string]> & { cancel(): void }}
@@ -18,10 +27,11 @@ export function CutData(gamePath) {
/** /**
* @param {string} gamePath * @param {string} gamePath
* @param {string} patchPath * @param {string} patchPath
* @param {boolean} isSkipVerify
* @returns {Promise<[boolean, string]> & { cancel(): void }} * @returns {Promise<[boolean, string]> & { cancel(): void }}
*/ */
export function DataExtract(gamePath, patchPath) { export function DataExtract(gamePath, patchPath, isSkipVerify) {
let $resultPromise = /** @type {any} */($Call.ByID(1843136452, gamePath, patchPath)); let $resultPromise = /** @type {any} */($Call.ByID(1843136452, gamePath, patchPath, isSkipVerify));
return $resultPromise; return $resultPromise;
} }

View File

@@ -2,11 +2,13 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT // This file is automatically generated. DO NOT EDIT
import * as AppService from "./appservice.js";
import * as FSService from "./fsservice.js"; import * as FSService from "./fsservice.js";
import * as GitService from "./gitservice.js"; import * as GitService from "./gitservice.js";
import * as HdiffzService from "./hdiffzservice.js"; import * as HdiffzService from "./hdiffzservice.js";
import * as LanguageService from "./languageservice.js"; import * as LanguageService from "./languageservice.js";
export { export {
AppService,
FSService, FSService,
GitService, GitService,
HdiffzService, HdiffzService,

View File

@@ -8,7 +8,7 @@ import {Call as $Call, Create as $Create} from "@wailsio/runtime";
/** /**
* @param {string} path * @param {string} path
* @returns {Promise<[string, string]> & { cancel(): void }} * @returns {Promise<[boolean, string, string, string]> & { cancel(): void }}
*/ */
export function GetLanguage(path) { export function GetLanguage(path) {
let $resultPromise = /** @type {any} */($Call.ByID(3450750492, path)); let $resultPromise = /** @type {any} */($Call.ByID(3450750492, path));
@@ -19,7 +19,7 @@ export function GetLanguage(path) {
* @param {string} path * @param {string} path
* @param {string} text * @param {string} text
* @param {string} voice * @param {string} voice
* @returns {Promise<boolean> & { cancel(): void }} * @returns {Promise<[boolean, string]> & { cancel(): void }}
*/ */
export function SetLanguage(path, text, voice) { export function SetLanguage(path, text, voice) {
let $resultPromise = /** @type {any} */($Call.ByID(2793672496, path, text, voice)); let $resultPromise = /** @type {any} */($Call.ByID(2793672496, path, text, voice));

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -0,0 +1,4 @@
export * from "./server"
export * from "./proxy"
export * from "./launcher"
export * from "./sleep"

View File

@@ -0,0 +1,37 @@
import useLauncherStore from "@/stores/launcherStore";
import { AppService, GitService } from "@bindings/firefly-launcher/internal";
import { toast } from "react-toastify";
import { sleep } from "./sleep";
export async function CheckUpdateLauncher(): Promise<{ isUpdate: boolean; isExists: boolean; version: string }> {
const [currentOk, currentVersion] = await AppService.GetCurrentLauncherVersion()
if (!currentOk) {
toast.error("Launcher error: cannot get current version")
return { isUpdate: false, isExists: true, version: "" }
}
const [latestOk, latestVersion, latestError] = await GitService.GetLatestLauncherVersion()
if (!latestOk) {
toast.error("Launcher error: " + latestError)
return { isUpdate: false, isExists: true, version: currentVersion }
}
const isUpdate = latestVersion !== currentVersion
return { isUpdate, isExists: true, version: latestVersion }
}
export async function UpdateLauncher(launcherVersion: string) : Promise<void> {
const {setDownloadType } = useLauncherStore.getState()
setDownloadType("update:launcher:downloading")
const [ok, error] = await GitService.UpdateLauncherProgress(launcherVersion)
if (ok) {
setDownloadType("update:launcher:success")
} else {
toast.error(error)
setDownloadType("update:launcher:failed")
}
AppService.CloseAppAfterTimeout(5)
await sleep(5000)
}

View File

@@ -0,0 +1,34 @@
import useLauncherStore from "@/stores/launcherStore";
import useSettingStore from "@/stores/settingStore";
import { FSService, GitService } from "@bindings/firefly-launcher/internal";
import { toast } from "react-toastify";
export async function CheckUpdateProxy(proxyPath: string, proxyVersion: string) : Promise<{isUpdate: boolean, isExists: boolean, version: string}> {
const [ok, latestVersion, error] = await GitService.GetLatestProxyVersion()
const isExists = await FSService.FileExists(proxyPath)
if (!ok) {
toast.error("Proxy error: " + error)
return { isUpdate: false, isExists, version: "" }
}
const isUpdate = latestVersion !== proxyVersion
return { isUpdate, isExists, version: latestVersion }
}
export async function UpdateProxy(proxyVersion: string) : Promise<void> {
const {setDownloadType } = useLauncherStore.getState()
const {setProxyPath, setProxyVersion} = useSettingStore.getState()
setDownloadType("Downloading proxy...")
const [ok, error] = await GitService.DownloadProxyProgress(proxyVersion)
if (ok) {
setDownloadType("Unzipping proxy...")
GitService.UnzipProxy()
setDownloadType("Download proxy successfully")
setProxyVersion(proxyVersion)
setProxyPath("./proxy/FireflyProxy.exe")
} else {
toast.error(error)
setDownloadType("Download proxy failed")
}
}

View File

@@ -0,0 +1,38 @@
import useLauncherStore from '@/stores/launcherStore';
import useSettingStore from '@/stores/settingStore';
import { FSService, GitService } from '@bindings/firefly-launcher/internal';
import { toast } from 'react-toastify';
export async function CheckUpdateServer(
serverPath: string,
serverVersion: string
): Promise<{ isUpdate: boolean; isExists: boolean; version: string }> {
const [ok, latestVersion, error] = await GitService.GetLatestServerVersion()
const isExists = await FSService.FileExists(serverPath)
if (!ok) {
toast.error("Server error: " + error)
return { isUpdate: false, isExists, version: "" }
}
const isUpdate = latestVersion !== serverVersion
return { isUpdate, isExists, version: latestVersion }
}
export async function UpdateServer(serverVersion: string) : Promise<void> {
const {setDownloadType } = useLauncherStore.getState()
const {setServerPath, setServerVersion} = useSettingStore.getState()
setDownloadType("Downloading server...")
const [ok, error] = await GitService.DownloadServerProgress(serverVersion)
if (ok) {
setDownloadType("Unzipping server...")
GitService.UnzipServer()
setDownloadType("Download server successfully")
setServerVersion(serverVersion)
setServerPath("./server/firefly-go_win.exe")
} else {
toast.error(error)
setDownloadType("Download server failed")
}
}

View File

@@ -0,0 +1,3 @@
export function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}

View File

@@ -65,6 +65,7 @@ export function useGlobalEvents({
Events.Off("proxy:exit"); Events.Off("proxy:exit");
Events.Off("hdiffz:progress"); Events.Off("hdiffz:progress");
Events.Off("hdiffz:message"); Events.Off("hdiffz:message");
Events.Off("version:check");
}; };
}, []); }, []);
} }

View File

@@ -14,7 +14,7 @@ export default function AboutPage() {
I created a lightweight and modern <span className="font-semibold text-success">Game Launcher</span> to help users easily launch and manage their games with better performance and simplicity. I created a lightweight and modern <span className="font-semibold text-success">Game Launcher</span> to help users easily launch and manage their games with better performance and simplicity.
</p> </p>
<p className="text-lg leading-relaxed"> <p className="text-lg leading-relaxed">
The launcher is built using <span className="font-mono text-info">Go + Wails</span>, with a clean and responsive interface styled with <span className="text-warning">Tailwind CSS</span> and <span className="text-warning">DaisyUI</span>. The launcher is built using <span className="font-mono text-info">Go + Wails3</span>, with a clean and responsive interface styled with <span className="text-warning">Tailwind CSS</span> and <span className="text-warning">DaisyUI</span>.
</p> </p>
<p className="text-lg leading-relaxed"> <p className="text-lg leading-relaxed">
My goal is to make tools that are fast, efficient, and enjoyable to use and this launcher is just the beginning. My goal is to make tools that are fast, efficient, and enjoyable to use and this launcher is just the beginning.

View File

@@ -78,12 +78,12 @@ export default function AnalysisPage() {
<span className="font-semibold text-blue-800">Backup Website</span> <span className="font-semibold text-blue-800">Backup Website</span>
</div> </div>
<a <a
href="https://sr-analysis.vercel.app/" href="https://firefly-sranalysis.vercel.app/"
className="link link-warning font-mono text-sm break-all" className="link link-warning font-mono text-sm break-all"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
https://sr-analysis.vercel.app/ https://firefly-sranalysis.vercel.app/
</a> </a>
</div> </div>
</div> </div>

View File

@@ -116,19 +116,32 @@ export default function HdiffzPage() {
setIsDiffLoading(false) setIsDiffLoading(false)
return return
} }
setStageType('Version Validate') setStageType('Check Type HDiff')
setProgressUpdate(0) setProgressUpdate(0)
setMaxProgressUpdate(1) setMaxProgressUpdate(1)
const [validVersion, errorVersion] = await HdiffzService.VersionValidate(gameDir, diffDir) const [isOk, validType, errorType] = await HdiffzService.CheckTypeHDiff(diffDir)
if (!validVersion) { if (!isOk) {
toast.error(errorVersion) toast.error(errorType)
setIsDiffLoading(false) setIsDiffLoading(false)
return return
} }
setProgressUpdate(1) setProgressUpdate(1)
if (validType === 'hdiffmap.json') {
setStageType('Version Validate')
setProgressUpdate(0)
setMaxProgressUpdate(1)
const [validVersion, errorVersion] = await HdiffzService.VersionValidate(gameDir, diffDir)
if (!validVersion) {
toast.error(errorVersion)
setIsDiffLoading(false)
return
}
setProgressUpdate(1)
}
setStageType('Data Extract') setStageType('Data Extract')
const [validData, errorData] = await HdiffzService.DataExtract(gameDir, diffDir) const [validData, errorData] = await HdiffzService.DataExtract(gameDir, diffDir, validType === 'hdifffiles.txt')
if (!validData) { if (!validData) {
toast.error(errorData) toast.error(errorData)
setIsDiffLoading(false) setIsDiffLoading(false)

View File

@@ -27,28 +27,42 @@ export default function LanguagePage() {
useEffect(() => { useEffect(() => {
const getLanguage = async () => { const getLanguage = async () => {
if (gameDir) { if (!gameDir) return
const subPath = 'StarRail_Data/StreamingAssets/DesignData/Windows'
const fullPath = `${gameDir}/${subPath}`
const exists = await FSService.DirExists(fullPath) const subPath = "StarRail_Data/StreamingAssets"
if (exists) { const fullPath = `${gameDir}/${subPath}`
const [textLang, voiceLang] = await LanguageService.GetLanguage(fullPath)
setTextLang(textLang) const exists = await FSService.DirExists(fullPath)
setVoiceLang(voiceLang) if (!exists) {
setFolderCheckResult('success') setTextLang("")
setSelectedTextLang(textLang) setVoiceLang("")
setSelectedVoiceLang(voiceLang) setSelectedTextLang("")
} else { setSelectedVoiceLang("")
setTextLang('') setFolderCheckResult("error")
setVoiceLang('') setGameDir("")
setSelectedTextLang('') return
setSelectedVoiceLang('')
setFolderCheckResult('error')
setGameDir('')
}
} }
const [ok, textLang, voiceLang, err] = await LanguageService.GetLanguage(fullPath)
if (!ok) {
setTextLang("")
setVoiceLang("")
setSelectedTextLang("")
setSelectedVoiceLang("")
setFolderCheckResult("error")
setGameDir("")
toast.error(err)
return
}
// success
setTextLang(textLang)
setVoiceLang(voiceLang)
setFolderCheckResult("success")
setSelectedTextLang(textLang)
setSelectedVoiceLang(voiceLang)
} }
getLanguage() getLanguage()
}, [gameDir]) }, [gameDir])
@@ -86,19 +100,19 @@ export default function LanguagePage() {
} }
try { try {
setIsSettingLanguage(true) setIsSettingLanguage(true)
const result = await LanguageService.SetLanguage( const [ok, err] = await LanguageService.SetLanguage(
`${gameDir}/StarRail_Data/StreamingAssets/DesignData/Windows`, `${gameDir}/StarRail_Data/StreamingAssets/DesignData/Windows`,
selectedTextLang, selectedTextLang,
selectedVoiceLang selectedVoiceLang
) )
if (result) { if (ok) {
toast.success('Language set successfully') toast.success('Language set successfully')
setTextLang(selectedTextLang) setTextLang(selectedTextLang)
setVoiceLang(selectedVoiceLang) setVoiceLang(selectedVoiceLang)
} }
else { else {
toast.error('Language set failed') toast.error(err)
} }
} catch (err: any) { } catch (err: any) {
toast.error('SetLanguage error:', err) toast.error('SetLanguage error:', err)
@@ -154,8 +168,8 @@ export default function LanguagePage() {
</div> </div>
{folderCheckResult && ( {folderCheckResult && (
<div className={`flex items-center gap-2 p-3 rounded-lg ${folderCheckResult === 'success' <div className={`flex items-center gap-2 p-3 rounded-lg ${folderCheckResult === 'success'
? 'bg-success/5 text-success border border-success' ? 'bg-success/5 text-success border border-success'
: 'bg-error/5 text-error border border-error' : 'bg-error/5 text-error border border-error'
}`}> }`}>
{folderCheckResult === 'success' ? ( {folderCheckResult === 'success' ? (
<> <>

View File

@@ -1,6 +1,6 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { Play, Menu, FolderOpen, MessageCircleQuestionMark } from 'lucide-react'; import { Play, Menu, FolderOpen, MessageCircleQuestionMark } from 'lucide-react';
import { FSService, GitService } from '@bindings/firefly-launcher/internal'; import { FSService, AppService } from '@bindings/firefly-launcher/internal';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import path from 'path-browserify' import path from 'path-browserify'
import useSettingStore from '@/stores/settingStore'; import useSettingStore from '@/stores/settingStore';
@@ -8,6 +8,7 @@ import useModalStore from '@/stores/modalStore';
import useLauncherStore from '@/stores/launcherStore'; import useLauncherStore from '@/stores/launcherStore';
import { motion } from 'motion/react'; import { motion } from 'motion/react';
import { Link } from '@tanstack/react-router'; import { Link } from '@tanstack/react-router';
import { CheckUpdateLauncher, CheckUpdateProxy, CheckUpdateServer, sleep, UpdateLauncher, UpdateProxy, UpdateServer } from '@/helper';
export default function LauncherPage() { export default function LauncherPage() {
const { gamePath, const { gamePath,
@@ -18,15 +19,17 @@ export default function LauncherPage() {
gameDir, gameDir,
serverVersion, serverVersion,
proxyVersion, proxyVersion,
setServerVersion,
setProxyVersion,
setServerPath,
setProxyPath
} = useSettingStore() } = useSettingStore()
const { isOpenNotification, setIsOpenNotification } = useModalStore() const {
isOpenDownloadDataModal,
isOpenUpdateDataModal,
isOpenSelfUpdateModal,
setIsOpenDownloadDataModal,
setIsOpenUpdateDataModal,
setIsOpenSelfUpdateModal
} = useModalStore()
const { const {
isLoading, isLoading,
modelName,
downloadType, downloadType,
serverReady, serverReady,
proxyReady, proxyReady,
@@ -36,8 +39,10 @@ export default function LauncherPage() {
gameRunning, gameRunning,
progressDownload, progressDownload,
downloadSpeed, downloadSpeed,
updateData,
launcherVersion,
setLauncherVersion,
setIsLoading, setIsLoading,
setModelName,
setDownloadType, setDownloadType,
setServerReady, setServerReady,
setProxyReady, setProxyReady,
@@ -45,12 +50,12 @@ export default function LauncherPage() {
setServerRunning, setServerRunning,
setProxyRunning, setProxyRunning,
setGameRunning, setGameRunning,
setUpdateData,
} = useLauncherStore() } = useLauncherStore()
useEffect(() => { useEffect(() => {
const check = async () => { const check = async () => {
if (!serverPath || !proxyPath || !serverVersion || !proxyVersion) { if (!serverVersion || !proxyVersion) {
setServerReady(false) setServerReady(false)
setProxyReady(false) setProxyReady(false)
return return
@@ -67,30 +72,47 @@ export default function LauncherPage() {
useEffect(() => { useEffect(() => {
const checkStartUp = async (): Promise<void> => { const checkStartUp = async (): Promise<void> => {
let isExists = false const [_, version] = await AppService.GetCurrentLauncherVersion()
if (serverPath && proxyPath && serverVersion && proxyVersion) { setLauncherVersion(version)
isExists = true const launcherData = await CheckUpdateLauncher()
setServerReady(true) if (launcherData.isUpdate) {
setProxyReady(true) setUpdateData({
server: { isUpdate: false, isExists: false, version: "" },
proxy: { isUpdate: false, isExists: false, version: "" },
launcher: launcherData
})
setIsOpenSelfUpdateModal(true)
return
} }
const dataUpdate = await handlerCheckUpdate() const serverData = await CheckUpdateServer(serverPath, serverVersion)
const proxyData = await CheckUpdateProxy(proxyPath, proxyVersion)
setUpdateData({
server: serverData,
proxy: proxyData,
launcher: launcherData
})
const exitGame = await FSService.FileExists(gamePath) const exitGame = await FSService.FileExists(gamePath)
if (!exitGame) { if (!exitGame) {
setGameRunning(false) setGameRunning(false)
setGamePath("") setGamePath("")
setGameDir("") setGameDir("")
} }
if (!dataUpdate.server.isUpdate && !dataUpdate.proxy.isUpdate) {
setServerReady(true) if (!serverData.isExists || !proxyData.isExists) {
setProxyReady(true)
return
}
if (!isExists) {
setServerReady(false) setServerReady(false)
setProxyReady(false) setProxyReady(false)
setIsOpenDownloadDataModal(true)
return
} }
setModelName(!isExists ? "Download Data" : "Update Data")
setIsOpenNotification(true) if (serverData.isUpdate || proxyData.isUpdate) {
setServerReady(true)
setProxyReady(true)
setIsOpenUpdateDataModal(true)
return
}
setServerReady(true)
setProxyReady(true)
} }
checkStartUp() checkStartUp()
}, []); }, []);
@@ -121,9 +143,7 @@ export default function LauncherPage() {
} }
} }
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const handleStartGame = async () => { const handleStartGame = async () => {
if (!gamePath) { if (!gamePath) {
@@ -174,84 +194,42 @@ export default function LauncherPage() {
} }
} }
const handlerCheckUpdate = async (): Promise<Record<string, { isUpdate: boolean, version: string }>> => {
let isUpdateServer = false
let isUpdateProxy = false
const [serverOk, serverNewVersion, serverError] = await GitService.GetLatestServerVersion(serverVersion)
const serverExists = await FSService.FileExists(serverPath)
if (serverOk) {
if (serverNewVersion !== serverVersion || !serverExists) {
isUpdateServer = true
}
} else {
toast.error("Server error: " + serverError)
}
const [proxyOk, proxyNewVersion, proxyError] = await GitService.GetLatestProxyVersion(proxyVersion)
const proxyExists = await FSService.FileExists(proxyPath)
if (proxyOk) {
if (proxyNewVersion !== proxyVersion || !proxyExists) {
isUpdateProxy = true
}
} else {
toast.error("Proxy error: " + proxyError)
}
return { server: { isUpdate: isUpdateServer, version: serverNewVersion }, proxy: { isUpdate: isUpdateProxy, version: proxyNewVersion } }
}
const handlerUpdateServer = async (updateInfo: Record<string, { isUpdate: boolean, version: string }>) => { const handlerUpdateData = async () => {
setIsDownloading(true) setIsDownloading(true)
for (const [key, value] of Object.entries(updateInfo)) { if (updateData.launcher.isUpdate) {
if (value.isUpdate) { await UpdateLauncher(updateData.launcher.version)
if (key === "server") { setUpdateData({...updateData, launcher: { isUpdate: false, isExists: true, version: updateData.launcher.version }})
setDownloadType("Downloading server...") setIsOpenSelfUpdateModal(true)
const [ok, error] = await GitService.DownloadServerProgress(value.version)
if (ok) {
setDownloadType("Unzipping server...")
GitService.UnzipServer()
setDownloadType("Download server successfully")
setServerVersion(value.version)
setServerPath("./server/firefly-go_win.exe")
} else {
toast.error(error)
setDownloadType("Download server failed")
}
} else if (key === "proxy") {
setDownloadType("Downloading proxy...")
const [ok, error] = await GitService.DownloadProxyProgress(value.version)
if (ok) {
setDownloadType("Unzipping proxy...")
GitService.UnzipProxy()
setDownloadType("Download proxy successfully")
setProxyVersion(value.version)
setProxyPath("./proxy/FireflyProxy.exe")
} else {
toast.error(error)
setDownloadType("Download proxy failed")
}
}
}
} }
if (updateData.server.isUpdate || !updateData.server.isExists) {
await UpdateServer(updateData.server.version)
setServerReady(true)
setUpdateData({...updateData, server: { isUpdate: false, isExists: true, version: updateData.server.version }})
}
if (updateData.proxy.isUpdate || !updateData.proxy.isExists) {
await UpdateProxy(updateData.proxy.version)
setProxyReady(true)
setUpdateData({...updateData, proxy: { isUpdate: false, isExists: true, version: updateData.proxy.version }})
}
setDownloadType("") setDownloadType("")
setIsDownloading(false) setIsDownloading(false)
setServerReady(true)
setProxyReady(true)
} }
// Handle ESC key to close modal // Handle ESC key to close modal
useEffect(() => { useEffect(() => {
const handleEscKey = (event: KeyboardEvent) => { const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') { if (event.key === 'Escape') {
setIsOpenNotification(false); setIsOpenDownloadDataModal(false);
setIsOpenUpdateDataModal(false);
setIsOpenSelfUpdateModal(false);
} }
}; };
window.addEventListener('keydown', handleEscKey); window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey); return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenNotification]); }, [isOpenDownloadDataModal, isOpenUpdateDataModal, isOpenSelfUpdateModal]);
return ( return (
@@ -280,11 +258,10 @@ export default function LauncherPage() {
href="https://sranalysis.kain.id.vn/" href="https://sranalysis.kain.id.vn/"
> >
<img <img
src="https://icons.duckduckgo.com/ip3/sranalysis.kain.id.vn.ico" src="https://sranalysis.kain.id.vn/ff-sranalysis.png"
alt="SRAnalysis Logo" alt="SRAnalysis Logo"
className="w-8 h-8 rounded-full" className="w-8 h-8 rounded-full"
/> />
</a> </a>
</div> </div>
@@ -295,7 +272,7 @@ export default function LauncherPage() {
href="https://srtools.kain.id.vn/" href="https://srtools.kain.id.vn/"
> >
<img <img
src="https://icons.duckduckgo.com/ip3/srtools.kain.id.vn.ico" src="https://srtools.kain.id.vn/ff-srtool.png"
alt="SRTools Logo" alt="SRTools Logo"
className="w-8 h-8 rounded-full" className="w-8 h-8 rounded-full"
/> />
@@ -369,13 +346,27 @@ export default function LauncherPage() {
<li><button onClick={handlePickFile}>Change Game Path</button></li> <li><button onClick={handlePickFile}>Change Game Path</button></li>
<li><button <li><button
onClick={async () => { onClick={async () => {
const updateInfo = await handlerCheckUpdate() const serverData = await CheckUpdateServer(serverPath, serverVersion)
if (updateInfo.server.isUpdate || updateInfo.proxy.isUpdate) { const proxyData = await CheckUpdateProxy(proxyPath, proxyVersion)
setModelName("Update Data") const launcherData = await CheckUpdateLauncher()
setIsOpenNotification(true) setUpdateData({
} else { server: serverData,
toast.success("No updates available") proxy: proxyData,
launcher: launcherData
})
if (launcherData.isUpdate) {
setIsOpenSelfUpdateModal(true)
return
} }
if (!serverData.isExists || !proxyData.isExists) {
setIsOpenDownloadDataModal(true)
return
}
if (serverData.isUpdate || proxyData.isUpdate) {
setIsOpenUpdateDataModal(true)
return
}
toast.success("No updates available")
}}> }}>
Check for Updates Check for Updates
</button></li> </button></li>
@@ -403,6 +394,11 @@ export default function LauncherPage() {
{/* Downloading */} {/* Downloading */}
{isDownloading && ( {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/20 backdrop-blur-sm rounded-lg p-4 shadow-lg">
<div className="space-y-3"> <div className="space-y-3">
<div className="flex justify-center items-center text-sm text-white/80"> <div className="flex justify-center items-center text-sm text-white/80">
@@ -426,64 +422,185 @@ export default function LauncherPage() {
</div> </div>
</div> </div>
)} )}
{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="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">
<span
className={`font-bold ${downloadType === "update:launcher:downloading"
? "text-yellow-200 text-2xl"
: downloadType === "update:launcher:success"
? "text-emerald-200 text-xl"
: "text-red-200 text-xl"
}`}
>
{downloadType === "update:launcher:downloading" && "Updating launcher"}
{downloadType === "update:launcher:success" && "Launcher updated successfully, auto closing after 5s"}
{downloadType === "update:launcher:failed" && "Launcher update failed, auto closing after 5s"}
<span className="dot-animation ml-1"></span>
</span>
</div>
)}
<div className="text-xs text-white/60">
{progressDownload < 100 ? "Please wait..." : "Complete!"}
</div>
</div>
<style>{`
.dot-animation::after {
content: '';
animation: dots 1.2s steps(4, end) infinite;
}
@keyframes dots {
0% { content: ''; }
25% { content: '.'; }
50% { content: '..'; }
75% { content: '...'; }
100% { content: ''; }
}
`}
</style>
</div>
)}
{/* Version Info */} {/* Version Info */}
{serverReady && proxyReady && !isDownloading && ( {serverReady && proxyReady && !isDownloading && (
<div className="hidden md:block fixed bottom-4 left-4 z-10 text-sm font-bold bg-black/20 backdrop-blur-sm rounded-lg p-2 shadow"> <div className="hidden md:block fixed bottom-4 left-4 z-10 text-sm font-bold bg-black/20 backdrop-blur-sm rounded-lg p-2 shadow">
<p className="text-primary">Version server: {serverVersion}</p> <p className="text-primary">Version server: {serverVersion}</p>
<p className="mt-2 text-secondary">Version proxy: {proxyVersion}</p> <p className="mt-2 text-secondary">Version proxy: {proxyVersion}</p>
<p className="mt-2 text-success">Version launcher: {launcherVersion}</p>
</div> </div>
)} )}
{/* Modal */} {/* Modal */}
{isOpenNotification && ( {isOpenUpdateDataModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="relative w-[90%] max-w-5xl bg-base-100 text-base-content rounded-xl border border-purple-500/50 shadow-lg shadow-purple-500/20"> <div className="relative w-[90%] max-w-5xl bg-base-100 text-base-content rounded-xl border border-purple-500/50 shadow-lg shadow-purple-500/20">
{modelName !== "Download Data" && ( <motion.button
<motion.button whileHover={{ scale: 1.1, rotate: 90 }}
whileHover={{ scale: 1.1, rotate: 90 }} transition={{ duration: 0.2 }}
transition={{ duration: 0.2 }} className="btn btn-circle btn-md btn-error absolute right-3 top-3"
className="btn btn-circle btn-md btn-error absolute right-3 top-3" onClick={() => setIsOpenUpdateDataModal(false)}
onClick={() => setIsOpenNotification(false)} >
>
</motion.button>
</motion.button>
)}
<div className="border-b border-purple-500/30 px-6 py-4 mb-4"> <div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-cyan-400"> <h3 className="font-bold text-2xl text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-cyan-400">
{modelName} Update Data
</h3> </h3>
</div> </div>
<div className="px-6 pb-6"> <div className="px-6 pb-6">
<div className="mb-6"> <div className="mb-6">
<p className="text-warning text-lg"> <p className="text-warning text-lg">
{modelName === "Download Data" Do you want to update data server and proxy?
? "Download required data!"
: "Do you want to update data?"}
</p> </p>
</div> </div>
<div className="flex justify-end gap-3"> <div className="flex justify-end gap-3">
{modelName !== "Download Data" && ( <motion.button
<motion.button whileHover={{ scale: 1.05 }}
whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}
whileTap={{ scale: 0.95 }} className="btn btn-outline btn-error"
className="btn btn-outline btn-error" onClick={() => setIsOpenUpdateDataModal(false)}
onClick={() => setIsOpenNotification(false)} >
> No
Cancel </motion.button>
</motion.button>
)}
<motion.button <motion.button
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.95 }}
className="btn btn-primary bg-gradient-to-r from-orange-200 to-red-400 border-none" className="btn btn-primary bg-gradient-to-r from-orange-200 to-red-400 border-none"
onClick={async () => { onClick={async () => {
setIsOpenNotification(false) setIsOpenUpdateDataModal(false)
const dataCheck = await handlerCheckUpdate() await handlerUpdateData()
await handlerUpdateServer(dataCheck) }}
>
Yes
</motion.button>
</div>
</div>
</div>
</div>
)}
{isOpenDownloadDataModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="relative w-[90%] max-w-5xl bg-base-100 text-base-content rounded-xl border border-purple-500/50 shadow-lg shadow-purple-500/20">
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-cyan-400">
Download Data
</h3>
</div>
<div className="px-6 pb-6">
<div className="mb-6">
<p className="text-warning text-lg">
Data server and proxy download required
</p>
</div>
<div className="flex justify-end gap-3">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="btn btn-primary bg-gradient-to-r from-orange-200 to-red-400 border-none"
onClick={async () => {
setIsOpenDownloadDataModal(false)
await handlerUpdateData()
}}
>
Download
</motion.button>
</div>
</div>
</div>
</div>
)}
{isOpenSelfUpdateModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="relative w-[90%] max-w-5xl bg-base-100 text-base-content rounded-xl border border-purple-500/50 shadow-lg shadow-purple-500/20">
<motion.button
whileHover={{ scale: 1.1, rotate: 90 }}
transition={{ duration: 0.2 }}
className="btn btn-circle btn-md btn-error absolute right-3 top-3"
onClick={() => setIsOpenSelfUpdateModal(false)}
>
</motion.button>
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-cyan-400">
Update Launcher
</h3>
</div>
<div className="px-6 pb-6">
<div className="mb-6">
<p className="text-warning text-lg">
Do you want to update launcher?
</p>
</div>
<div className="flex justify-end gap-3">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="btn btn-outline btn-error"
onClick={() => setIsOpenSelfUpdateModal(false)}
>
No
</motion.button>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="btn btn-primary bg-gradient-to-r from-orange-200 to-red-400 border-none"
onClick={async () => {
setIsOpenSelfUpdateModal(false)
await handlerUpdateData()
}} }}
> >
Yes Yes

View File

@@ -15,17 +15,44 @@ export default function SrToolsPage() {
<div className="space-y-3 text-blue-700"> <div className="space-y-3 text-blue-700">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="text-blue-600 text-lg">🏠</div> <div className="text-blue-600 text-lg">🏠</div>
<p>This site uses a self-hosted version of <span className="font-semibold text-success">SR Tools</span> available at{" "} <p>
<a This site is a another version of {" "}
href="https://srtools.kain.id.vn/" <span className="font-semibold text-success">SR Tools {" "}</span>
className="link link-info" developed by {" "}
target="_blank" <span className="font-semibold text-warning">Me {"(Kain)"}</span>
rel="noopener noreferrer"
>
srtools.kain.id.vn
</a>
</p> </p>
</div> </div>
<div className="grid md:grid-cols-2 gap-4">
<div className="bg-white border border-blue-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<span className="text-blue-600 text-lg">🏆</span>
<span className="font-semibold text-blue-800">Master Website</span>
</div>
<a
href="https://srtools.kain.id.vn/"
className="link link-warning font-mono text-sm break-all"
target="_blank"
rel="noopener noreferrer"
>
https://srtools.kain.id.vn/
</a>
</div>
<div className="bg-white border border-blue-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<span className="text-blue-600 text-lg">🔄</span>
<span className="font-semibold text-blue-800">Backup Website</span>
</div>
<a
href="https://firefly-srtools.vercel.app/"
className="link link-warning font-mono text-sm break-all"
target="_blank"
rel="noopener noreferrer"
>
https://firefly-srtools.vercel.app/
</a>
</div>
</div>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="text-blue-600 text-lg">👨💻</div> <div className="text-blue-600 text-lg">👨💻</div>
<p>The original tool was created by a third-party developer named <span className="font-semibold text-warning">Amazing</span>. This version is directly based on that work, without modification to core logic.</p> <p>The original tool was created by a third-party developer named <span className="font-semibold text-warning">Amazing</span>. This version is directly based on that work, without modification to core logic.</p>

View File

@@ -22,7 +22,7 @@ function RootLayout() {
setDownloadSpeed, setDownloadSpeed,
setMessageUpdate, setMessageUpdate,
}); });
return ( return (
<> <>
<div className="navbar bg-base-100 shadow-sm sticky top-0 z-50 px-3"> <div className="navbar bg-base-100 shadow-sm sticky top-0 z-50 px-3">
@@ -55,13 +55,18 @@ function RootLayout() {
</ul> </ul>
</div> </div>
<Link to="/" className="grid grid-cols-1 items-start text-left gap-0 hover:scale-105 px-2"> <Link to="/" className="grid grid-cols-1 items-start text-left gap-0 hover:scale-105 px-2">
<h1 className="text-lg font-bold"> <div className="flex items-center justify-center">
<span className="text-emerald-500">Firefly Lauc</span> <img src="/ff-launcher.png" alt="Logo" className='w-13 h-13' />
<span className="bg-clip-text text-transparent bg-gradient-to-r from-emerald-400 via-orange-500 to-red-500"> <div className="flex flex-col justify-center items-start">
her <h1 className="text-xl font-bold">
</span> <span className="text-emerald-500">Firefly </span>
</h1> <span className="bg-clip-text text-transparent bg-gradient-to-r from-emerald-400 via-orange-500 to-red-500">
<p className="text-sm text-gray-500">By Kain</p> Launcher
</span>
</h1>
<p className="text-sm text-gray-500">By Kain</p>
</div>
</div>
</Link> </Link>
</div> </div>
<div className="navbar-center hidden md:flex"> <div className="navbar-center hidden md:flex">

View File

@@ -2,7 +2,6 @@
import { create } from 'zustand' import { create } from 'zustand'
interface LauncherState { interface LauncherState {
modelName: string;
downloadType: string; downloadType: string;
serverReady: boolean; serverReady: boolean;
proxyReady: boolean; proxyReady: boolean;
@@ -13,7 +12,8 @@ interface LauncherState {
gameRunning: boolean; gameRunning: boolean;
progressDownload: number; progressDownload: number;
downloadSpeed: number; downloadSpeed: number;
setModelName: (value: string) => void; launcherVersion: string;
updateData: Record<'server' | 'proxy' | 'launcher', { isUpdate: boolean, isExists: boolean, version: string }>;
setDownloadType: (value: string) => void; setDownloadType: (value: string) => void;
setServerReady: (value: boolean) => void; setServerReady: (value: boolean) => void;
setProxyReady: (value: boolean) => void; setProxyReady: (value: boolean) => void;
@@ -23,12 +23,13 @@ interface LauncherState {
setIsLoading: (value: boolean) => void; setIsLoading: (value: boolean) => void;
setGameRunning: (value: boolean) => void; setGameRunning: (value: boolean) => void;
setProgressDownload: (value: number) => void; setProgressDownload: (value: number) => void;
setLauncherVersion: (value: string) => void;
setDownloadSpeed: (value: number) => void; setDownloadSpeed: (value: number) => void;
setUpdateData: (value: Record<'server' | 'proxy' | 'launcher', { isUpdate: boolean, isExists: boolean, version: string }>) => void;
} }
const useLauncherStore = create<LauncherState>((set, get) => ({ const useLauncherStore = create<LauncherState>((set, get) => ({
isLoading: false, isLoading: false,
modelName: "Download Data",
downloadType: "", downloadType: "",
serverReady: false, serverReady: false,
proxyReady: false, proxyReady: false,
@@ -38,8 +39,13 @@ const useLauncherStore = create<LauncherState>((set, get) => ({
gameRunning: false, gameRunning: false,
progressDownload: 0, progressDownload: 0,
downloadSpeed: 0, downloadSpeed: 0,
launcherVersion: "",
updateData: {
server: { isUpdate: false, isExists: false, version: "" },
proxy: { isUpdate: false, isExists: false, version: "" },
launcher: { isUpdate: false, isExists: true, version: "" },
},
setIsLoading: (value: boolean) => set({ isLoading: value }), setIsLoading: (value: boolean) => set({ isLoading: value }),
setModelName: (value: string) => set({ modelName: value }),
setDownloadType: (value: string) => set({ downloadType: value }), setDownloadType: (value: string) => set({ downloadType: value }),
setServerReady: (value: boolean) => set({ serverReady: value }), setServerReady: (value: boolean) => set({ serverReady: value }),
setProxyReady: (value: boolean) => set({ proxyReady: value }), setProxyReady: (value: boolean) => set({ proxyReady: value }),
@@ -48,7 +54,9 @@ const useLauncherStore = create<LauncherState>((set, get) => ({
setProxyRunning: (value: boolean) => set({ proxyRunning: value }), setProxyRunning: (value: boolean) => set({ proxyRunning: value }),
setGameRunning: (value: boolean) => set({ gameRunning: value }), setGameRunning: (value: boolean) => set({ gameRunning: value }),
setProgressDownload: (value: number) => set({ progressDownload: value }), setProgressDownload: (value: number) => set({ progressDownload: value }),
setLauncherVersion: (value: string) => set({ launcherVersion: value }),
setDownloadSpeed: (value: number) => set({ downloadSpeed: value }), setDownloadSpeed: (value: number) => set({ downloadSpeed: value }),
setUpdateData: (value: Record<'server' | 'proxy' | 'launcher', { isUpdate: boolean, isExists: boolean, version: string }>) => set({ updateData: value }),
})); }));
export default useLauncherStore; export default useLauncherStore;

View File

@@ -2,13 +2,21 @@
import { create } from 'zustand' import { create } from 'zustand'
interface ModalState { interface ModalState {
isOpenNotification: boolean; isOpenDownloadDataModal: boolean;
setIsOpenNotification: (modal: boolean) => void; isOpenUpdateDataModal: boolean;
isOpenSelfUpdateModal: boolean;
setIsOpenDownloadDataModal: (modal: boolean) => void;
setIsOpenUpdateDataModal: (modal: boolean) => void;
setIsOpenSelfUpdateModal: (modal: boolean) => void;
} }
const useModalStore = create<ModalState>((set, get) => ({ const useModalStore = create<ModalState>((set, get) => ({
isOpenNotification: false, isOpenDownloadDataModal: false,
setIsOpenNotification: (modal: boolean) => set({ isOpenNotification: modal }), isOpenUpdateDataModal: false,
isOpenSelfUpdateModal: false,
setIsOpenDownloadDataModal: (modal: boolean) => set({ isOpenDownloadDataModal: modal }),
setIsOpenUpdateDataModal: (modal: boolean) => set({ isOpenUpdateDataModal: modal }),
setIsOpenSelfUpdateModal: (modal: boolean) => set({ isOpenSelfUpdateModal: modal }),
})); }));
export default useModalStore; export default useModalStore;

3
go.mod
View File

@@ -9,6 +9,8 @@ require (
golang.org/x/sys v0.28.0 golang.org/x/sys v0.28.0
) )
require aead.dev/minisign v0.2.0 // indirect
require ( require (
dario.cat/mergo v1.0.1 // indirect dario.cat/mergo v1.0.1 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect
@@ -34,6 +36,7 @@ require (
github.com/lmittmann/tint v1.0.4 // indirect github.com/lmittmann/tint v1.0.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/minio/selfupdate v0.6.0
github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect

9
go.sum
View File

@@ -1,3 +1,5 @@
aead.dev/minisign v0.2.0 h1:kAWrq/hBRu4AARY6AlciO83xhNnW9UaC8YipS2uhLPk=
aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
@@ -74,6 +76,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDwU=
github.com/minio/selfupdate v0.6.0/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
@@ -111,7 +115,9 @@ github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
@@ -123,6 +129,7 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@@ -143,6 +150,7 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -157,6 +165,7 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=

22
internal/app-serrvice.go Normal file
View File

@@ -0,0 +1,22 @@
package internal
import (
"firefly-launcher/pkg/constant"
"time"
"github.com/wailsapp/wails/v3/pkg/application"
)
type AppService struct{}
func (a *AppService) GetCurrentLauncherVersion() (bool, string) {
return true, constant.CurrentLauncherVersion
}
func (a *AppService) CloseAppAfterTimeout(timeout int) (bool, string) {
go func() {
time.Sleep(time.Duration(timeout) * time.Second)
application.Get().Quit()
}()
return true, ""
}

View File

@@ -8,7 +8,6 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/application"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
) )
@@ -140,3 +139,4 @@ func (f *FSService) FileExistsInZip(archivePath, fileInside string) (bool, strin
} }
return exists, "" return exists, ""
} }

View File

@@ -10,12 +10,13 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/minio/selfupdate"
"github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/application"
) )
type GitService struct{} type GitService struct{}
func (g *GitService) GetLatestServerVersion(oldVersion string) (bool, string, string) { func (g *GitService) GetLatestServerVersion() (bool, string, string) {
resp, err := http.Get(constant.ServerGitUrl) resp, err := http.Get(constant.ServerGitUrl)
if err != nil { if err != nil {
return false, "", err.Error() return false, "", err.Error()
@@ -38,7 +39,6 @@ func (g *GitService) GetLatestServerVersion(oldVersion string) (bool, string, st
} }
func (g *GitService) DownloadServerProgress(version string) (bool, string) { func (g *GitService) DownloadServerProgress(version string) (bool, string) {
resp, err := http.Get(constant.ServerGitUrl) resp, err := http.Get(constant.ServerGitUrl)
if err != nil { if err != nil {
panic(err) panic(err)
@@ -109,7 +109,7 @@ func (g *GitService) UnzipServer() {
os.Remove(filepath.Join(constant.ServerStorageUrl, constant.ServerZipFile)) os.Remove(filepath.Join(constant.ServerStorageUrl, constant.ServerZipFile))
} }
func (g *GitService) GetLatestProxyVersion(oldVersion string) (bool, string, string) { func (g *GitService) GetLatestProxyVersion() (bool, string, string) {
resp, err := http.Get(constant.ProxyGitUrl) resp, err := http.Get(constant.ProxyGitUrl)
if err != nil { if err != nil {
return false, "", err.Error() return false, "", err.Error()
@@ -201,3 +201,82 @@ func (g *GitService) UnzipProxy() {
unzipParallel(filepath.Join(constant.ProxyStorageUrl, constant.ProxyZipFile), constant.ProxyStorageUrl) unzipParallel(filepath.Join(constant.ProxyStorageUrl, constant.ProxyZipFile), constant.ProxyStorageUrl)
os.Remove(filepath.Join(constant.ProxyStorageUrl, constant.ProxyZipFile)) os.Remove(filepath.Join(constant.ProxyStorageUrl, constant.ProxyZipFile))
} }
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, ""
}
func (g *GitService) UpdateLauncherProgress(version string) (bool, string) {
resp, err := http.Get(constant.LauncherGitUrl)
if err != nil {
panic(err)
}
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"
}
var releaseData *models.ReleaseType
for _, release := range releases {
if release.TagName == version {
releaseData = release
break
}
}
if releaseData == nil || releaseData.TagName == "" {
return false, "no release found"
}
var assetWin models.AssetType
for _, asset := range releaseData.Assets {
if asset.Name == constant.LauncherFile {
assetWin = asset
break
}
}
if assetWin.Name == "" {
return false, "no assets found"
}
resp, err = http.Get(assetWin.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, ""
}

View File

@@ -7,19 +7,30 @@ import (
"firefly-launcher/pkg/models" "firefly-launcher/pkg/models"
"firefly-launcher/pkg/sevenzip" "firefly-launcher/pkg/sevenzip"
"firefly-launcher/pkg/verifier" "firefly-launcher/pkg/verifier"
"firefly-launcher/pkg/hpatchz"
"fmt" "fmt"
"io" "io"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"syscall"
"github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/application"
) )
type HdiffzService struct{} type HdiffzService struct{}
func (h *HdiffzService) CheckTypeHDiff(patchPath string) (bool, string, string) {
isFileInTxt, _ := sevenzip.IsFileIn7z(patchPath, "hdifffiles.txt")
if isFileInTxt {
return true, "hdifffiles.txt", ""
}
isFileInJson, _ := sevenzip.IsFileIn7z(patchPath, "hdiffmap.json")
if isFileInJson {
return true, "hdiffmap.json", ""
}
return false, "", "not found hdifffiles.txt or hdiffmap.json"
}
func (h *HdiffzService) VersionValidate(gamePath, patchPath string) (bool, string) { func (h *HdiffzService) VersionValidate(gamePath, patchPath string) (bool, string) {
oldVersionData, err := models.ParseBinaryVersion(filepath.Join(gamePath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes")) oldVersionData, err := models.ParseBinaryVersion(filepath.Join(gamePath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes"))
if err != nil { if err != nil {
@@ -53,7 +64,8 @@ func (h *HdiffzService) VersionValidate(gamePath, patchPath string) (bool, strin
return true, "validated" return true, "validated"
} }
func (h *HdiffzService) DataExtract(gamePath, patchPath string) (bool, string) {
func (h *HdiffzService) DataExtract(gamePath, patchPath string, isSkipVerify bool) (bool, string) {
if _, err := os.Stat(gamePath); err != nil { if _, err := os.Stat(gamePath); err != nil {
return false, err.Error() return false, err.Error()
} }
@@ -73,15 +85,17 @@ func (h *HdiffzService) DataExtract(gamePath, patchPath string) (bool, string) {
return false, err.Error() return false, err.Error()
} }
validator, err := verifier.NewVerifier(gamePath, constant.TempUrl) if !isSkipVerify {
if err != nil { validator, err := verifier.NewVerifier(gamePath, constant.TempUrl)
os.RemoveAll(constant.TempUrl) if err != nil {
return false, err.Error() os.RemoveAll(constant.TempUrl)
} return false, err.Error()
}
if err := validator.VerifyAll(); err != nil { if err := validator.VerifyAll(); err != nil {
os.RemoveAll(constant.TempUrl) os.RemoveAll(constant.TempUrl)
return false, err.Error() return false, err.Error()
}
} }
return true, "validated" return true, "validated"
@@ -136,22 +150,42 @@ func (h *HdiffzService) CutData(gamePath string) (bool, string) {
return false, err.Error() return false, err.Error()
} }
_ = os.RemoveAll(constant.TempUrl)
return true, "cut completed" return true, "cut completed"
} }
func (h *HdiffzService) PatchData(gamePath string) (bool, string) { func (h *HdiffzService) PatchData(gamePath string) (bool, string) {
data, err := os.ReadFile(filepath.Join(gamePath, "hdiffmap.json")) hdiffMapPath := filepath.Join(gamePath, "hdiffmap.json")
if err != nil { hdiffFilesPath := filepath.Join(gamePath, "hdifffiles.txt")
return false, err.Error()
}
var jsonData struct { var jsonData struct {
DiffMap []*models.DiffMapType `json:"diff_map"` DiffMap []*models.HDiffData `json:"diff_map"`
} }
if err := json.Unmarshal(data, &jsonData); err != nil {
return false, err.Error() if _, err := os.Stat(hdiffMapPath); err == nil {
data, err := os.ReadFile(hdiffMapPath)
if err != nil {
return false, err.Error()
}
var jsonDataDiffMap struct {
DiffMap []*models.DiffMapType `json:"diff_map"`
}
if err := json.Unmarshal(data, &jsonDataDiffMap); err != nil {
return false, err.Error()
}
for _, entry := range jsonDataDiffMap.DiffMap {
jsonData.DiffMap = append(jsonData.DiffMap, entry.ToHDiffData())
}
} else if _, err := os.Stat(hdiffFilesPath); err == nil {
files, err := models.LoadHDiffFiles(hdiffFilesPath)
if err != nil {
return false, err.Error()
}
for _, entry := range files {
jsonData.DiffMap = append(jsonData.DiffMap, entry.ToHDiffData())
}
} else {
return false, "no hdiff entries map exist"
} }
for i, entry := range jsonData.DiffMap { for i, entry := range jsonData.DiffMap {
@@ -160,25 +194,48 @@ func (h *HdiffzService) PatchData(gamePath string) (bool, string) {
"progress": i, "progress": i,
"maxProgress": len(jsonData.DiffMap), "maxProgress": len(jsonData.DiffMap),
}) })
sourceFile := filepath.Join(gamePath, entry.SourceFileName) sourceFile := filepath.Join(gamePath, entry.SourceFileName)
patchFile := filepath.Join(gamePath, entry.PatchFileName) patchFile := filepath.Join(gamePath, entry.PatchFileName)
targetFile := filepath.Join(gamePath, entry.TargetFileName) targetFile := filepath.Join(gamePath, entry.TargetFileName)
if _, err := os.Stat(sourceFile); os.IsNotExist(err) { // Check patch file tồn tại chưa
continue
}
if _, err := os.Stat(patchFile); os.IsNotExist(err) { if _, err := os.Stat(patchFile); os.IsNotExist(err) {
continue continue
} }
cmd := exec.Command(constant.ToolHPatchzExe.String(), sourceFile, patchFile, targetFile) // Nếu không có source file hoặc SourceFileName rỗng → apply_patch_empty
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} if entry.SourceFileName == "" {
_, err := cmd.CombinedOutput() err := hpatchz.ApplyPatchEmpty(patchFile, targetFile)
if err != nil { if err != nil {
fmt.Printf("%s failed to patch! %v\n", entry.TargetFileName, err)
_ = os.Remove(patchFile)
return false, err.Error()
}
_ = os.Remove(patchFile)
continue continue
} }
if _, err := os.Stat(sourceFile); os.IsNotExist(err) {
continue
}
// Có source file → apply_patch
err := hpatchz.ApplyPatch(sourceFile, patchFile, targetFile)
if err != nil {
fmt.Printf("%s failed to patch! %v\n", entry.TargetFileName, err)
_ = os.Remove(patchFile)
return false, err.Error()
}
if entry.SourceFileName != entry.TargetFileName {
_ = os.Remove(sourceFile)
}
_ = os.Remove(patchFile)
} }
os.Remove(filepath.Join(gamePath, "hdiffmap.json"))
os.Remove(filepath.Join(gamePath, "hdifffiles.txt"))
return true, "patching completed" return true, "patching completed"
} }
@@ -200,13 +257,14 @@ func (h *HdiffzService) DeleteFiles(gamePath string) (bool, string) {
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
return false, "" return false, "no delete files exist"
} }
for i, file := range deleteFiles { for i, file := range deleteFiles {
os.Remove(filepath.Join(gamePath, file)) os.Remove(filepath.Join(gamePath, file))
application.Get().EmitEvent("hdiffz:progress", map[string]int{"progress": i, "maxProgress": len(deleteFiles)}) application.Get().EmitEvent("hdiffz:progress", map[string]int{"progress": i, "maxProgress": len(deleteFiles)})
} }
_ = os.Remove(filepath.Join(gamePath, "deletefiles.txt"))
return true, "" return true, ""
} }

View File

@@ -2,145 +2,174 @@ package internal
import ( import (
"bytes" "bytes"
"fmt" assetMeta "firefly-launcher/pkg/language-patch/asset-meta"
excelLanguage "firefly-launcher/pkg/language-patch/excel-language"
"firefly-launcher/pkg/models"
"os" "os"
"path/filepath" "path/filepath"
"slices"
"strings"
) )
type LanguageService struct{} type LanguageService struct{}
func isValidLang(lang string) bool { func isValidLang(lang string) bool {
valid := []string{"en", "jp", "cn", "kr"} valid := []string{"en", "jp", "cn", "kr"}
for _, v := range valid { return slices.Contains(valid, lang)
if lang == v {
return true
}
}
return false
} }
func (l *LanguageService) GetLanguage(path string) (string, string, error) { func (l *LanguageService) GetLanguage(path string) (bool, string, string, string) {
files, err := os.ReadDir(path) currentVersionGame, err := models.ParseBinaryVersion(filepath.Join(path, "BinaryVersion.bytes"))
if err != nil { if err != nil {
return "", "", err return false, "", "", err.Error()
} }
for _, file := range files { typeVersionGame := "os"
filePath := filepath.Join(path, file.Name()) if strings.Contains(currentVersionGame.Name, "CN") {
typeVersionGame = "cn"
content, err := os.ReadFile(filePath)
if err != nil {
continue
}
patternToFind := []byte("SpriteOutput/UI/Fonts/RPG_CN.ttf")
idx := bytes.Index(content, patternToFind)
if idx == -1 {
continue
}
pattern := []byte("Korean")
idx = bytes.Index(content, pattern)
if idx == -1 {
continue
}
// Move to os text language
idx += 10
idx += 4
osText := string(content[idx : idx+2])
idx += 3 * 4
// Move to cn voice language
idx += 1
idx += 5
cnVoice := string(content[idx : idx+2])
idx += 3 * 2 // skip 2 entries
// Move to os voice language
idx += 1
idx += 5
osVoice := string(content[idx : idx+2])
idx += 3 * 5 // skip 5 entries
// Move to cn text language
idx += 1
idx += 4
cnText := string(content[idx : idx+2])
textLang := osText
voiceLang := osVoice
if !isValidLang(textLang) {
textLang = cnText
}
if !isValidLang(voiceLang) {
voiceLang = cnVoice
}
return textLang, voiceLang, nil
} }
return "", "", fmt.Errorf("couldn't find file to read language from") assetPath := filepath.Join(path, "DesignData\\Windows")
}
func replaceBytes(content []byte, idx int, choice string, param int) int { indexHash, err := assetMeta.GetIndexHash(assetPath)
for i := 0; i < param; i++ {
copy(content[idx:idx+2], []byte(choice))
idx += 3
}
return idx
}
func (l *LanguageService) SetLanguage(path string, text, voice string) (bool, error) {
files, err := os.ReadDir(path)
if err != nil { if err != nil {
return false, err return false, "", "", err.Error()
} }
for _, file := range files { DesignIndex, err := assetMeta.DesignIndexFromBytes(assetPath, indexHash)
filePath := filepath.Join(path, file.Name()) if err != nil {
return false, "", "", err.Error()
content, err := os.ReadFile(filePath) }
if err != nil { dataEntry, fileEntry, err := DesignIndex.FindDataAndFileByTarget(-515329346)
continue if err != nil {
} return false, "", "", err.Error()
}
patternToFind := []byte("SpriteOutput/UI/Fonts/RPG_CN.ttf") allowedLanguage := excelLanguage.NewExcelLanguage(assetPath, &dataEntry, &fileEntry)
idx := bytes.Index(content, patternToFind) languageRows, err := allowedLanguage.Parse()
if idx == -1 { if err != nil {
continue return false, "", "", err.Error()
}
pattern := []byte("Korean")
idx = bytes.Index(content, pattern)
if idx == -1 {
continue
}
idx += 10
idx += 4
idx = replaceBytes(content, idx, text, 4)
idx += 1
idx += 5
idx = replaceBytes(content, idx, voice, 2)
idx += 1
idx += 5
idx = replaceBytes(content, idx, voice, 5)
idx += 1
idx += 4
_ = replaceBytes(content, idx, text, 2)
err = os.WriteFile(filePath, content, 0644)
if err != nil {
return false, err
}
return true, nil
} }
return false, fmt.Errorf("couldn't find file to patch. Make sure this file is placed in the correct folder") currentTextLang := ""
currentVoiceLang := ""
pairs := []struct {
area string
typ *uint8
}{
{"os", nil},
{"cn", func() *uint8 { v := uint8(1); return &v }()},
{"os", func() *uint8 { v := uint8(1); return &v }()},
{"cn", nil},
}
for _, p := range pairs {
var found *excelLanguage.LanguageRow
for i := range languageRows {
if languageRows[i].Area != nil && *languageRows[i].Area == p.area {
if (languageRows[i].Type == nil && p.typ == nil) ||
(languageRows[i].Type != nil && p.typ != nil && *languageRows[i].Type == *p.typ) {
found = &languageRows[i]
break
}
}
}
if found == nil {
continue
}
if found.DefaultLanguage != nil && found.Area != nil && *found.Area == typeVersionGame && found.Type == nil {
currentTextLang = *found.DefaultLanguage
}
if found.DefaultLanguage != nil && found.Area != nil && *found.Area == typeVersionGame && found.Type != nil {
currentVoiceLang = *found.DefaultLanguage
}
}
if currentTextLang == "" || currentVoiceLang == "" || !isValidLang(currentTextLang) || !isValidLang(currentVoiceLang) {
return false, "", "", "not found language"
}
return true, currentTextLang, currentVoiceLang, ""
}
func (l *LanguageService) SetLanguage(path string, text, voice string) (bool, string) {
indexHash, err := assetMeta.GetIndexHash(path)
if err != nil {
return false, err.Error()
}
DesignIndex, err := assetMeta.DesignIndexFromBytes(path, indexHash)
if err != nil {
return false, err.Error()
}
dataEntry, fileEntry, err := DesignIndex.FindDataAndFileByTarget(-515329346)
if err != nil {
return false, err.Error()
}
allowedLanguage := excelLanguage.NewExcelLanguage(path, &dataEntry, &fileEntry)
languageRows, err := allowedLanguage.Parse()
if err != nil {
return false, err.Error()
}
pairs := []struct {
area string
typ *uint8
lang string
}{
{"os", nil, text},
{"cn", func() *uint8 { v := uint8(1); return &v }(), voice},
{"os", func() *uint8 { v := uint8(1); return &v }(), voice},
{"cn", nil, text},
}
for _, p := range pairs {
var found *excelLanguage.LanguageRow
for i := range languageRows {
if languageRows[i].Area != nil && *languageRows[i].Area == p.area {
if (languageRows[i].Type == nil && p.typ == nil) ||
(languageRows[i].Type != nil && p.typ != nil && *languageRows[i].Type == *p.typ) {
found = &languageRows[i]
break
}
}
}
if found == nil {
continue
}
found.DefaultLanguage = &p.lang
found.LanguageList = []string{p.lang}
}
data, err := allowedLanguage.Unmarshal(languageRows)
if err != nil {
return false, err.Error()
}
filePath := filepath.Join(path, fileEntry.FileByteName+".bytes")
f, err := os.OpenFile(filePath, os.O_RDWR, 0644)
if err != nil {
return false, err.Error()
}
defer f.Close()
if _, err := f.Seek(int64(dataEntry.Offset), 0); err != nil {
return false, err.Error()
}
if _, err := f.Write(data); err != nil {
return false, err.Error()
}
if len(data) < int(dataEntry.Size) {
remaining := int(dataEntry.Size) - len(data)
zeros := bytes.Repeat([]byte{0}, remaining)
if _, err := f.Write(zeros); err != nil {
return false, err.Error()
}
}
return true, "success"
} }

View File

@@ -0,0 +1 @@
package internal

40
main.go
View File

@@ -13,11 +13,6 @@ import (
"github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/application"
) )
// Wails uses Go's `embed` package to embed the frontend files into the binary.
// Any files in the frontend/dist folder will be embedded into the binary and
// made available to the frontend.
// See https://pkg.go.dev/embed for more information.
//go:embed all:frontend/dist //go:embed all:frontend/dist
var assets embed.FS var assets embed.FS
@@ -42,10 +37,8 @@ func extractFile(embedPath, outPath string) error {
return os.WriteFile(outPath, data, 0755) return os.WriteFile(outPath, data, 0755)
} }
// main function serves as the application's entry point. It initializes the application, creates a window,
// and starts a goroutine that emits a time-based event every second. It subsequently runs the application and
// logs any error that might occur.
func main() { func main() {
// Extract required files
for outPath, embedPath := range constant.RequiredFiles { for outPath, embedPath := range constant.RequiredFiles {
if !fileExists(outPath.String()) { if !fileExists(outPath.String()) {
err := extractFile(embedPath, outPath.String()) err := extractFile(embedPath, outPath.String())
@@ -54,11 +47,18 @@ func main() {
} }
} }
} }
// Create a new Wails application by providing the necessary options.
// Variables 'Name' and 'Description' are for application metadata. // Remove old executable
// 'Assets' configures the asset server with the 'FS' variable pointing to the frontend files. exePath, err := os.Executable()
// 'Bind' is a list of Go struct instances. The frontend has access to the methods of these instances. if err == nil {
// 'Mac' options tailor the application when running an macOS. dir := filepath.Dir(exePath)
base := filepath.Base(exePath)
oldPath := filepath.Join(dir, "."+base+".old")
fmt.Println("Old executable path:", oldPath)
os.Remove(oldPath)
}
// Create application
app := application.New(application.Options{ app := application.New(application.Options{
Name: "firefly-launcher", Name: "firefly-launcher",
Description: "Firefly Launcher - Kain", Description: "Firefly Launcher - Kain",
@@ -67,31 +67,24 @@ func main() {
application.NewService(&internal.LanguageService{}), application.NewService(&internal.LanguageService{}),
application.NewService(&internal.GitService{}), application.NewService(&internal.GitService{}),
application.NewService(&internal.HdiffzService{}), application.NewService(&internal.HdiffzService{}),
application.NewService(&internal.AppService{}),
}, },
Assets: application.AssetOptions{ Assets: application.AssetOptions{
Handler: application.AssetFileServerFS(assets), Handler: application.AssetFileServerFS(assets),
}, },
Mac: application.MacOptions{ Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: true, ApplicationShouldTerminateAfterLastWindowClosed: true,
}, },
}) })
// Create a new window with the necessary options. // Create window
// 'Title' is the title of the window.
// 'Mac' options tailor the window when running on macOS.
// 'BackgroundColour' is the background colour of the window.
// 'URL' is the URL that will be loaded into the webview.
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Title: "Firefly Launcher - Kain", Title: "Firefly Launcher - Kain",
Mac: application.MacWindow{ Mac: application.MacWindow{
InvisibleTitleBarHeight: 50, InvisibleTitleBarHeight: 50,
Backdrop: application.MacBackdropTranslucent, Backdrop: application.MacBackdropTranslucent,
TitleBar: application.MacTitleBarHiddenInset, TitleBar: application.MacTitleBarHiddenInset,
}, },
BackgroundColour: application.NewRGB(27, 38, 54), BackgroundColour: application.NewRGB(27, 38, 54),
Width: 1200, Width: 1200,
Height: 600, Height: 600,
@@ -99,7 +92,8 @@ func main() {
DevToolsEnabled: true, DevToolsEnabled: true,
}) })
err := app.Run()
err = app.Run()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@@ -2,12 +2,17 @@ package constant
const ProxyGitUrl = "https://git.kain.io.vn/api/v1/repos/Firefly-Shelter/Firefly_Proxy/releases" const ProxyGitUrl = "https://git.kain.io.vn/api/v1/repos/Firefly-Shelter/Firefly_Proxy/releases"
const ServerGitUrl = "https://git.kain.io.vn/api/v1/repos/Firefly-Shelter/FireflyGo_Local_Archive/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 ServerStorageUrl = "./server" const ServerStorageUrl = "./server"
const ProxyStorageUrl = "./proxy" const ProxyStorageUrl = "./proxy"
const ServerZipFile = "prebuild_win_x86.zip" const ServerZipFile = "prebuild_win_x86.zip"
const ProxyZipFile = "64bit.zip" const ProxyZipFile = "64bit.zip"
const LauncherFile = "firefly-launcher.exe"
const TempUrl = "./temp" const TempUrl = "./temp"
const CurrentLauncherVersion = "1.4.0"
type ToolFile string type ToolFile string
const ( const (

34
pkg/hpatchz/hpatchz.go Normal file
View File

@@ -0,0 +1,34 @@
package hpatchz
import (
"firefly-launcher/pkg/constant"
"fmt"
"os/exec"
"syscall"
)
func ApplyPatch(oldFile, diffFile, newFile string) error {
cmd := exec.Command(constant.ToolHPatchzExe.String(), "-f", oldFile, diffFile, newFile)
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to execute hpatchz: %w", err)
}
if cmd.ProcessState.ExitCode() != 0 {
return fmt.Errorf("hpatchz failed: %s", string(output))
}
return nil
}
func ApplyPatchEmpty(diffFile, newFile string) error {
cmd := exec.Command(constant.ToolHPatchzExe.String(), "-f", "", diffFile, newFile)
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to execute hpatchz: %w", err)
}
if cmd.ProcessState.ExitCode() != 0 {
return fmt.Errorf("hpatchz failed: %s", string(output))
}
return nil
}

View File

@@ -0,0 +1,30 @@
package assetMeta
import (
"fmt"
"io"
)
type ByteHash16 []byte
func ByteHash16FromBytes(r io.ReadSeeker) (ByteHash16, error) {
fullHash := make([]byte, 16)
buf := make([]byte, 4)
for i := 0; i < 4; i++ {
if _, err := io.ReadFull(r, buf); err != nil {
return nil, err
}
for j := 0; j < 4; j++ {
fullHash[i*4+j] = buf[3-j]
}
}
return ByteHash16(fullHash), nil
}
func (b ByteHash16) String() string {
s := ""
for _, v := range b {
s += fmt.Sprintf("%02x", v)
}
return s
}

View File

@@ -0,0 +1,28 @@
package assetMeta
import (
"encoding/binary"
"io"
)
type DataEntry struct {
NameHash int32
Size uint32
Offset uint32
}
func DataEntryFromBytes(r io.Reader) (*DataEntry, error) {
var d DataEntry
if err := binary.Read(r, binary.BigEndian, &d.NameHash); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &d.Size); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &d.Offset); err != nil {
return nil, err
}
return &d, nil
}

View File

@@ -0,0 +1,98 @@
package assetMeta
import (
"bytes"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"os"
"path/filepath"
)
type DesignIndex struct {
UnkI64 int64
FileCount int32
DesignDataCount int32
FileList []FileEntry
}
func (d *DesignIndex) FindDataAndFileByTarget(target int32) (DataEntry, FileEntry, error) {
for _, file := range d.FileList {
for _, entry := range file.DataEntries {
if entry.NameHash == target {
return entry, file, nil
}
}
}
return DataEntry{}, FileEntry{}, errors.New("not found")
}
func DesignIndexFromBytes(assetFolder string, indexHash string) (*DesignIndex, error) {
path := filepath.Join(assetFolder, fmt.Sprintf("DesignV_%s.bytes", indexHash))
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
r := bytes.NewReader(data)
var d DesignIndex
if err := binary.Read(r, binary.BigEndian, &d.UnkI64); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &d.FileCount); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &d.DesignDataCount); err != nil {
return nil, err
}
d.FileList = make([]FileEntry, 0, d.FileCount)
for i := int32(0); i < d.FileCount; i++ {
entry, err := FileEntryFromBytes(r)
if err != nil {
return nil, err
}
d.FileList = append(d.FileList, *entry)
}
return &d, nil
}
func GetIndexHash(assetFolder string) (string, error) {
path := filepath.Join(assetFolder, "M_DesignV.bytes")
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
_, err = f.Seek(0x1C, 0)
if err != nil {
return "", err
}
hash := make([]byte, 0x10)
index := 0
for i := 0; i < 4; i++ {
chunk := make([]byte, 4)
_, err := f.Read(chunk)
if err != nil {
return "", err
}
for bytePos := 3; bytePos >= 0; bytePos-- {
hash[index] = chunk[bytePos]
index++
}
}
return hex.EncodeToString(hash), nil
}

View File

@@ -0,0 +1,63 @@
package assetMeta
import (
"encoding/binary"
"fmt"
"io"
)
type FileEntry struct {
NameHash int32
FileByteName string
Size int64
DataCount int32
DataEntries []DataEntry
Unk uint8
}
func FileEntryFromBytes(r io.Reader) (*FileEntry, error) {
var f FileEntry
if err := binary.Read(r, binary.BigEndian, &f.NameHash); err != nil {
return nil, err
}
buf := make([]byte, 16)
if _, err := io.ReadFull(r, buf); err != nil {
return nil, err
}
f.FileByteName = toHex(buf)
if err := binary.Read(r, binary.BigEndian, &f.Size); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &f.DataCount); err != nil {
return nil, err
}
f.DataEntries = make([]DataEntry, 0, f.DataCount)
for i := int32(0); i < f.DataCount; i++ {
entry, err := DataEntryFromBytes(r)
if err != nil {
return nil, err
}
f.DataEntries = append(f.DataEntries, *entry)
}
// read 1 byte
b := make([]byte, 1)
if _, err := r.Read(b); err != nil {
return nil, err
}
f.Unk = b[0]
return &f, nil
}
func toHex(buf []byte) string {
s := ""
for _, b := range buf {
s += fmt.Sprintf("%02x", b)
}
return s
}

View File

@@ -0,0 +1,32 @@
package assetMeta
import (
"encoding/binary"
"io"
)
type MiniAsset struct {
RevisionID uint32
DesignIndexHash ByteHash16
}
func MiniAssetFromBytes(r io.ReadSeeker) (*MiniAsset, error) {
if _, err := r.Seek(6*4, io.SeekCurrent); err != nil {
return nil, err
}
var revID uint32
if err := binary.Read(r, binary.LittleEndian, &revID); err != nil {
return nil, err
}
hash, err := ByteHash16FromBytes(r)
if err != nil {
return nil, err
}
return &MiniAsset{
RevisionID: revID,
DesignIndexHash: hash,
}, nil
}

View File

@@ -0,0 +1,114 @@
package excelLanguage
import (
"bytes"
assetMeta "firefly-launcher/pkg/language-patch/asset-meta"
"io"
"os"
"path/filepath"
)
type ExcelLanguage struct {
AssetFolder string
ExcelDataEntry *assetMeta.DataEntry
ExcelFileEntry *assetMeta.FileEntry
}
func NewExcelLanguage(assetFolder string, dataEntry *assetMeta.DataEntry, fileEntry *assetMeta.FileEntry) *ExcelLanguage {
return &ExcelLanguage{
AssetFolder: assetFolder,
ExcelDataEntry: dataEntry,
ExcelFileEntry: fileEntry,
}
}
func (a *ExcelLanguage) Unmarshal(rows []LanguageRow) ([]byte, error) {
buf := new(bytes.Buffer)
buf.WriteByte(0)
if err := writeI8Varint(buf, int8(len(rows))); err != nil {
return nil, err
}
for _, row := range rows {
rowData, err := row.Unmarshal()
if err != nil {
return nil, err
}
if _, err := buf.Write(rowData); err != nil {
return nil, err
}
}
return buf.Bytes(), nil
}
func (a *ExcelLanguage) Parse() ([]LanguageRow, error) {
excelPath := filepath.Join(a.AssetFolder, a.ExcelFileEntry.FileByteName+".bytes")
f, err := os.Open(excelPath)
if err != nil {
return nil, err
}
defer f.Close()
if _, err := f.Seek(int64(a.ExcelDataEntry.Offset), io.SeekStart); err != nil {
return nil, err
}
buffer := make([]byte, a.ExcelDataEntry.Size)
if _, err := io.ReadFull(f, buffer); err != nil {
return nil, err
}
reader := bytes.NewReader(buffer)
_, _ = reader.ReadByte() // skip first byte
count, err := readI8Varint(reader)
if err != nil {
return nil, err
}
rows := make([]LanguageRow, 0, count)
for i := 0; i < count; i++ {
bitmask, err := reader.ReadByte()
if err != nil {
return nil, err
}
row := LanguageRow{}
if bitmask&(1<<0) != 0 {
s, err := readString(reader)
if err != nil {
return nil, err
}
row.Area = &s
}
if bitmask&(1<<1) != 0 {
t, err := reader.ReadByte()
if err != nil {
return nil, err
}
row.Type = &t
}
if bitmask&(1<<2) != 0 {
arr, err := readStringArray(reader)
if err != nil {
return nil, err
}
row.LanguageList = arr
}
if bitmask&(1<<3) != 0 {
s, err := readString(reader)
if err != nil {
return nil, err
}
row.DefaultLanguage = &s
}
rows = append(rows, row)
}
return rows, nil
}

View File

@@ -0,0 +1,81 @@
package excelLanguage
import (
"bytes"
"errors"
)
type LanguageRow struct {
Area *string
Type *uint8
LanguageList []string
DefaultLanguage *string
}
func (r *LanguageRow) Unmarshal() ([]byte, error) {
buf := new(bytes.Buffer)
var bitmask uint8
if r.Area != nil {
bitmask |= 1 << 0
}
if r.Type != nil {
bitmask |= 1 << 1
}
if len(r.LanguageList) > 0 {
bitmask |= 1 << 2
}
if r.DefaultLanguage != nil {
bitmask |= 1 << 3
}
if err := buf.WriteByte(bitmask); err != nil {
return nil, err
}
if r.Area != nil {
if err := writeString(buf, *r.Area); err != nil {
return nil, err
}
}
if r.Type != nil {
if err := buf.WriteByte(*r.Type); err != nil {
return nil, err
}
}
if len(r.LanguageList) > 0 {
if err := writeStringArray(buf, r.LanguageList); err != nil {
return nil, err
}
}
if r.DefaultLanguage != nil {
if err := writeString(buf, *r.DefaultLanguage); err != nil {
return nil, err
}
}
return buf.Bytes(), nil
}
func writeString(buf *bytes.Buffer, s string) error {
if len(s) > 255 {
return errors.New("string too long")
}
if err := buf.WriteByte(uint8(len(s))); err != nil {
return err
}
_, err := buf.Write([]byte(s))
return err
}
func writeStringArray(buf *bytes.Buffer, arr []string) error {
if err := writeI8Varint(buf, int8(len(arr))); err != nil {
return err
}
for _, s := range arr {
if err := writeString(buf, s); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,53 @@
package excelLanguage
import (
"bytes"
"encoding/binary"
"io"
)
func writeI8Varint(buf *bytes.Buffer, v int8) error {
uv := uint64((uint32(v) << 1) ^ uint32(v>>7)) // zigzag encode
b := make([]byte, binary.MaxVarintLen64)
n := binary.PutUvarint(b, uv)
_, err := buf.Write(b[:n])
return err
}
func readI8Varint(r *bytes.Reader) (int, error) {
uv, err := binary.ReadUvarint(r)
if err != nil {
return 0, err
}
// zigzag decode
v := int((uv >> 1) ^ uint64((int64(uv&1)<<63)>>63))
return v, nil
}
func readString(r *bytes.Reader) (string, error) {
l, err := r.ReadByte()
if err != nil {
return "", err
}
buf := make([]byte, l)
if _, err := io.ReadFull(r, buf); err != nil {
return "", err
}
return string(buf), nil
}
func readStringArray(r *bytes.Reader) ([]string, error) {
length, err := readI8Varint(r)
if err != nil {
return nil, err
}
arr := make([]string, 0, length)
for i := 0; i < length; i++ {
s, err := readString(r)
if err != nil {
return nil, err
}
arr = append(arr, s)
}
return arr, nil
}

View File

@@ -4,11 +4,13 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"regexp"
"strconv" "strconv"
"strings" "strings"
) )
type BinaryVersion struct { type BinaryVersion struct {
Name string
Major int Major int
Minor int Minor int
Patch int Patch int
@@ -22,42 +24,47 @@ func ParseBinaryVersion(path string) (*BinaryVersion, error) {
content := string(data) content := string(data)
dashPos := strings.LastIndex(content, "-") lastDash := strings.LastIndex(content, "-")
if dashPos == -1 { if lastDash == -1 {
return nil, errors.New("no dash found in version string") return nil, errors.New("no dash found in version string")
} }
start := dashPos - 6 secondLastDash := strings.LastIndex(content[:lastDash], "-")
if start < 0 { if secondLastDash == -1 {
start = 0 return nil, errors.New("only one dash found in version string")
} }
versionSlice := content[start:] versionSlice := content[secondLastDash+1 : lastDash]
end := strings.Index(versionSlice, "-") re := regexp.MustCompile(`^([A-Za-z]+)([\d\.]+)$`)
if end == -1 { matches := re.FindStringSubmatch(versionSlice)
end = len(versionSlice) if len(matches) < 3 {
}
versionStr := versionSlice[:end]
parts := strings.SplitN(versionStr, ".", 3)
if len(parts) != 3 {
return nil, errors.New("invalid version format") return nil, errors.New("invalid version format")
} }
binaryVersion := BinaryVersion{
Name: matches[1],
}
numbers := strings.Split(matches[2], ".")
major, err := strconv.Atoi(parts[0]) if len(numbers) > 0 {
if err != nil { binaryVersion.Major, err = strconv.Atoi(numbers[0])
return nil, err if err != nil {
return nil, err
}
} }
minor, err := strconv.Atoi(parts[1]) if len(numbers) > 1 {
if err != nil { binaryVersion.Minor, err = strconv.Atoi(numbers[1])
return nil, err if err != nil {
return nil, err
}
} }
patch, err := strconv.Atoi(parts[2]) if len(numbers) > 2 {
if err != nil { binaryVersion.Patch, err = strconv.Atoi(numbers[2])
return nil, err if err != nil {
return nil, err
}
} }
return &BinaryVersion{major, minor, patch}, nil return &binaryVersion, nil
} }
func (v *BinaryVersion) String() string { func (v *BinaryVersion) String() string {

43
pkg/models/hdiffFiles.go Normal file
View File

@@ -0,0 +1,43 @@
package models
import (
"bufio"
"encoding/json"
"fmt"
"os"
)
type HDiffFiles struct {
RemoteFile string `json:"remoteName"`
}
func (h *HDiffFiles) ToHDiffData() *HDiffData {
return &HDiffData{
SourceFileName: h.RemoteFile,
TargetFileName: h.RemoteFile,
PatchFileName: fmt.Sprintf("%s.hdiff", h.RemoteFile),
}
}
func LoadHDiffFiles(path string) ([]*HDiffFiles, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
var results []*HDiffFiles
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
var item HDiffFiles
if err := json.Unmarshal([]byte(line), &item); err == nil {
results = append(results, &item)
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return results, nil
}

View File

@@ -12,4 +12,18 @@ type DiffMapType struct {
PatchFileName string `json:"patch_file_name"` PatchFileName string `json:"patch_file_name"`
PatchFileMD5 string `json:"patch_file_md5"` PatchFileMD5 string `json:"patch_file_md5"`
PatchFileSize int64 `json:"patch_file_size"` PatchFileSize int64 `json:"patch_file_size"`
}
type HDiffData struct {
SourceFileName string `json:"source_file_name"`
TargetFileName string `json:"target_file_name"`
PatchFileName string `json:"patch_file_name"`
}
func (d *DiffMapType) ToHDiffData() *HDiffData {
return &HDiffData{
SourceFileName: d.SourceFileName,
TargetFileName: d.TargetFileName,
PatchFileName: d.PatchFileName,
}
} }

36
pkg/models/pkgVersion.go Normal file
View File

@@ -0,0 +1,36 @@
package models
import (
"bufio"
"encoding/json"
"os"
)
type PkgVersion struct {
RemoteFile string `json:"remoteName"`
MD5 string `json:"md5"`
}
func LoadPkgVersion(path string) ([]*PkgVersion, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
var results []*PkgVersion
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
var item PkgVersion
if err := json.Unmarshal([]byte(line), &item); err == nil {
results = append(results, &item)
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return results, nil
}

View File

@@ -29,6 +29,41 @@ func IsFileIn7z(archivePath, fileInside string) (bool, error) {
return false, fmt.Errorf("%s not found in %s", fileInside, archivePath) return false, fmt.Errorf("%s not found in %s", fileInside, archivePath)
} }
func ListFilesInZip(archivePath string) ([]string, error) {
cmd := exec.Command(constant.Tool7zaExe.String(), "l", archivePath)
var out bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &out
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("7za list failed: %v\nOutput: %s", err, out.String())
}
lines := strings.Split(out.String(), "\n")
var files []string
foundTable := false
for _, line := range lines {
if strings.HasPrefix(line, "----------") {
if foundTable {
break
}
foundTable = true
continue
}
if foundTable {
fields := strings.Fields(line)
if len(fields) >= 6 {
fileName := strings.Join(fields[5:], " ")
files = append(files, fileName)
}
}
}
return files, nil
}
func ExtractAFileFromZip(archivePath, fileInside, outDir string) error { func ExtractAFileFromZip(archivePath, fileInside, outDir string) error {
cmd := exec.Command(constant.Tool7zaExe.String(), "e", archivePath, fileInside, "-o"+outDir, "-y") cmd := exec.Command(constant.Tool7zaExe.String(), "e", archivePath, fileInside, "-o"+outDir, "-y")
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout