5 Commits
1.5.1 ... 1.9.0

33 changed files with 525 additions and 470 deletions

Binary file not shown.

Binary file not shown.

BIN
assets/hpatchz.exe Normal file

Binary file not shown.

View File

@@ -4,21 +4,19 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports // @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime"; import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
/** /**
* @param {number} timeout * @param {number} timeout
* @returns {Promise<[boolean, string]> & { cancel(): void }} * @returns {$CancellablePromise<[boolean, string]>}
*/ */
export function CloseAppAfterTimeout(timeout) { export function CloseAppAfterTimeout(timeout) {
let $resultPromise = /** @type {any} */($Call.ByID(1705931481, timeout)); return $Call.ByID(1705931481, timeout);
return $resultPromise;
} }
/** /**
* @returns {Promise<[boolean, string]> & { cancel(): void }} * @returns {$CancellablePromise<[boolean, string]>}
*/ */
export function GetCurrentLauncherVersion() { export function GetCurrentLauncherVersion() {
let $resultPromise = /** @type {any} */($Call.ByID(3575133982)); return $Call.ByID(3575133982);
return $resultPromise;
} }

View File

@@ -4,70 +4,62 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports // @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime"; import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
/** /**
* @param {string} patchPath * @param {string} patchPath
* @returns {Promise<[boolean, string, string]> & { cancel(): void }} * @returns {$CancellablePromise<[boolean, string, string]>}
*/ */
export function CheckTypeHDiff(patchPath) { export function CheckTypeHDiff(patchPath) {
let $resultPromise = /** @type {any} */($Call.ByID(3717449114, patchPath)); return $Call.ByID(3717449114, patchPath);
return $resultPromise;
} }
/** /**
* @param {string} gamePath * @param {string} gamePath
* @returns {Promise<[boolean, string]> & { cancel(): void }} * @returns {$CancellablePromise<[boolean, string]>}
*/ */
export function CutData(gamePath) { export function CutData(gamePath) {
let $resultPromise = /** @type {any} */($Call.ByID(2019290107, gamePath)); return $Call.ByID(2019290107, gamePath);
return $resultPromise;
} }
/** /**
* @param {string} gamePath * @param {string} gamePath
* @param {string} patchPath * @param {string} patchPath
* @param {boolean} isSkipVerify * @returns {$CancellablePromise<[boolean, string]>}
* @returns {Promise<[boolean, string]> & { cancel(): void }}
*/ */
export function DataExtract(gamePath, patchPath, isSkipVerify) { export function DataExtract(gamePath, patchPath) {
let $resultPromise = /** @type {any} */($Call.ByID(2161622254, gamePath, patchPath, isSkipVerify)); return $Call.ByID(2161622254, gamePath, patchPath);
return $resultPromise;
} }
/** /**
* @param {string} gamePath * @param {string} gamePath
* @returns {Promise<[boolean, string]> & { cancel(): void }} * @returns {$CancellablePromise<[boolean, string]>}
*/ */
export function DeleteFiles(gamePath) { export function DeleteFiles(gamePath) {
let $resultPromise = /** @type {any} */($Call.ByID(1103091613, gamePath)); return $Call.ByID(1103091613, gamePath);
return $resultPromise;
} }
/** /**
* @param {string} gamePath * @param {string} gamePath
* @returns {Promise<[boolean, string]> & { cancel(): void }} * @returns {$CancellablePromise<[boolean, string]>}
*/ */
export function HDiffPatchData(gamePath) { export function HDiffPatchData(gamePath) {
let $resultPromise = /** @type {any} */($Call.ByID(3944051994, gamePath)); return $Call.ByID(3944051994, gamePath);
return $resultPromise;
} }
/** /**
* @param {string} gamePath * @param {string} gamePath
* @returns {Promise<[boolean, string]> & { cancel(): void }} * @returns {$CancellablePromise<[boolean, string]>}
*/ */
export function LDiffPatchData(gamePath) { export function LDiffPatchData(gamePath) {
let $resultPromise = /** @type {any} */($Call.ByID(360123238, gamePath)); return $Call.ByID(360123238, gamePath);
return $resultPromise;
} }
/** /**
* @param {string} gamePath * @param {string} gamePath
* @param {string} patchPath * @param {string} patchPath
* @returns {Promise<[boolean, string]> & { cancel(): void }} * @returns {$CancellablePromise<[boolean, string]>}
*/ */
export function VersionValidate(gamePath, patchPath) { export function VersionValidate(gamePath, patchPath) {
let $resultPromise = /** @type {any} */($Call.ByID(2105077257, gamePath, patchPath)); return $Call.ByID(2105077257, gamePath, patchPath);
return $resultPromise;
} }

View File

@@ -4,76 +4,68 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports // @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime"; import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
/** /**
* @param {string} path * @param {string} path
* @returns {Promise<boolean> & { cancel(): void }} * @returns {$CancellablePromise<boolean>}
*/ */
export function DirExists(path) { export function DirExists(path) {
let $resultPromise = /** @type {any} */($Call.ByID(1772289644, path)); return $Call.ByID(1772289644, path);
return $resultPromise;
} }
/** /**
* @param {string} path * @param {string} path
* @returns {Promise<boolean> & { cancel(): void }} * @returns {$CancellablePromise<boolean>}
*/ */
export function FileExists(path) { export function FileExists(path) {
let $resultPromise = /** @type {any} */($Call.ByID(1782610747, path)); return $Call.ByID(1782610747, path);
return $resultPromise;
} }
/** /**
* @param {string} archivePath * @param {string} archivePath
* @param {string} fileInside * @param {string} fileInside
* @returns {Promise<[boolean, string]> & { cancel(): void }} * @returns {$CancellablePromise<[boolean, string]>}
*/ */
export function FileExistsInZip(archivePath, fileInside) { export function FileExistsInZip(archivePath, fileInside) {
let $resultPromise = /** @type {any} */($Call.ByID(2509699047, archivePath, fileInside)); return $Call.ByID(2509699047, archivePath, fileInside);
return $resultPromise;
} }
/** /**
* @param {string} path * @param {string} path
* @returns {Promise<[boolean, string]> & { cancel(): void }} * @returns {$CancellablePromise<[boolean, string]>}
*/ */
export function OpenFolder(path) { export function OpenFolder(path) {
let $resultPromise = /** @type {any} */($Call.ByID(1635714001, path)); return $Call.ByID(1635714001, path);
return $resultPromise;
} }
/** /**
* @param {string} filter * @param {string} filter
* @returns {Promise<string> & { cancel(): void }} * @returns {$CancellablePromise<string>}
*/ */
export function PickFile(filter) { export function PickFile(filter) {
let $resultPromise = /** @type {any} */($Call.ByID(3756474934, filter)); return $Call.ByID(3756474934, filter);
return $resultPromise;
} }
/** /**
* @returns {Promise<string> & { cancel(): void }} * @returns {$CancellablePromise<string>}
*/ */
export function PickFolder() { export function PickFolder() {
let $resultPromise = /** @type {any} */($Call.ByID(3654471460)); return $Call.ByID(3654471460);
return $resultPromise;
} }
/** /**
* @param {string} path * @param {string} path
* @returns {Promise<boolean> & { cancel(): void }} * @returns {$CancellablePromise<boolean>}
*/ */
export function StartApp(path) { export function StartApp(path) {
let $resultPromise = /** @type {any} */($Call.ByID(1267568402, path)); return $Call.ByID(1267568402, path);
return $resultPromise;
} }
/** /**
* @param {string} path * @param {string} path
* @returns {Promise<boolean> & { cancel(): void }} * @returns {$CancellablePromise<boolean>}
*/ */
export function StartWithConsole(path) { export function StartWithConsole(path) {
let $resultPromise = /** @type {any} */($Call.ByID(3249271428, path)); return $Call.ByID(3249271428, path);
return $resultPromise;
} }

View File

@@ -4,71 +4,63 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports // @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime"; import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
/** /**
* @param {string} version * @param {string} version
* @returns {Promise<[boolean, string]> & { cancel(): void }} * @returns {$CancellablePromise<[boolean, string]>}
*/ */
export function DownloadProxyProgress(version) { export function DownloadProxyProgress(version) {
let $resultPromise = /** @type {any} */($Call.ByID(3559275334, version)); return $Call.ByID(3559275334, version);
return $resultPromise;
} }
/** /**
* @param {string} version * @param {string} version
* @returns {Promise<[boolean, string]> & { cancel(): void }} * @returns {$CancellablePromise<[boolean, string]>}
*/ */
export function DownloadServerProgress(version) { export function DownloadServerProgress(version) {
let $resultPromise = /** @type {any} */($Call.ByID(1954767259, version)); return $Call.ByID(1954767259, version);
return $resultPromise;
} }
/** /**
* @returns {Promise<[boolean, string, string]> & { cancel(): void }} * @returns {$CancellablePromise<[boolean, string, string]>}
*/ */
export function GetLatestLauncherVersion() { export function GetLatestLauncherVersion() {
let $resultPromise = /** @type {any} */($Call.ByID(262637602)); return $Call.ByID(262637602);
return $resultPromise;
} }
/** /**
* @returns {Promise<[boolean, string, string]> & { cancel(): void }} * @returns {$CancellablePromise<[boolean, string, string]>}
*/ */
export function GetLatestProxyVersion() { export function GetLatestProxyVersion() {
let $resultPromise = /** @type {any} */($Call.ByID(289488362)); return $Call.ByID(289488362);
return $resultPromise;
} }
/** /**
* @returns {Promise<[boolean, string, string]> & { cancel(): void }} * @returns {$CancellablePromise<[boolean, string, string]>}
*/ */
export function GetLatestServerVersion() { export function GetLatestServerVersion() {
let $resultPromise = /** @type {any} */($Call.ByID(2918980975)); return $Call.ByID(2918980975);
return $resultPromise;
} }
/** /**
* @returns {Promise<void> & { cancel(): void }} * @returns {$CancellablePromise<void>}
*/ */
export function UnzipProxy() { export function UnzipProxy() {
let $resultPromise = /** @type {any} */($Call.ByID(2563246729)); return $Call.ByID(2563246729);
return $resultPromise;
} }
/** /**
* @returns {Promise<void> & { cancel(): void }} * @returns {$CancellablePromise<void>}
*/ */
export function UnzipServer() { export function UnzipServer() {
let $resultPromise = /** @type {any} */($Call.ByID(1126363284)); return $Call.ByID(1126363284);
return $resultPromise;
} }
/** /**
* @param {string} version * @param {string} version
* @returns {Promise<[boolean, string]> & { cancel(): void }} * @returns {$CancellablePromise<[boolean, string]>}
*/ */
export function UpdateLauncherProgress(version) { export function UpdateLauncherProgress(version) {
let $resultPromise = /** @type {any} */($Call.ByID(97272861, version)); return $Call.ByID(97272861, version);
return $resultPromise;
} }

View File

@@ -4,24 +4,22 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports // @ts-ignore: Unused imports
import {Call as $Call, Create as $Create} from "@wailsio/runtime"; import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
/** /**
* @param {string} path * @param {string} path
* @returns {Promise<[boolean, string, string, string]> & { cancel(): void }} * @returns {$CancellablePromise<[boolean, string, string, string]>}
*/ */
export function GetLanguage(path) { export function GetLanguage(path) {
let $resultPromise = /** @type {any} */($Call.ByID(3574191687, path)); return $Call.ByID(3574191687, path);
return $resultPromise;
} }
/** /**
* @param {string} path * @param {string} path
* @param {string} text * @param {string} text
* @param {string} voice * @param {string} voice
* @returns {Promise<[boolean, string]> & { cancel(): void }} * @returns {$CancellablePromise<[boolean, string]>}
*/ */
export function SetLanguage(path, text, voice) { export function SetLanguage(path, text, voice) {
let $resultPromise = /** @type {any} */($Call.ByID(2768939795, path, text, voice)); return $Call.ByID(2768939795, path, text, voice);
return $resultPromise;
} }

View File

@@ -21,7 +21,7 @@ export function useGlobalEvents({
setProgressUpdate: (v: number) => void; setProgressUpdate: (v: number) => void;
setMaxProgressUpdate: (v: number) => void; setMaxProgressUpdate: (v: number) => void;
setProgressDownload: (v: number) => void; setProgressDownload: (v: number) => void;
setDownloadSpeed: (v: number) => void; setDownloadSpeed: (v: string) => void;
setMessageUpdate: (v: string) => void; setMessageUpdate: (v: string) => void;
setStageType: (v: string) => void, setStageType: (v: string) => void,
}) { }) {
@@ -33,7 +33,7 @@ export function useGlobalEvents({
const onDownload = (event: any) => { const onDownload = (event: any) => {
const { percent, speed } = event.data[0]; const { percent, speed } = event.data[0];
setProgressDownload(Number(percent)); setProgressDownload(Number(percent));
setDownloadSpeed(Number(speed)); setDownloadSpeed(speed);
}; };
const onUpdateProgress = (event: any) => { const onUpdateProgress = (event: any) => {

View File

@@ -4,20 +4,15 @@ import { RouterProvider, createRouter } from '@tanstack/react-router'
import './styles/index.css' import './styles/index.css'
import "../runtime.js" import "../runtime.js"
import "@wailsio/runtime"; import "@wailsio/runtime";
// Import the generated route tree
import { routeTree } from './routeTree.gen.js' import { routeTree } from './routeTree.gen.js'
// Create a new router instance
const router = createRouter({ routeTree }) const router = createRouter({ routeTree })
// Register the router instance for type safety
declare module '@tanstack/react-router' { declare module '@tanstack/react-router' {
interface Register { interface Register {
router: typeof router router: typeof router
} }
} }
// Render the app
const rootElement = document.getElementById('root')! const rootElement = document.getElementById('root')!
if (!rootElement.innerHTML) { if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement) const root = ReactDOM.createRoot(rootElement)

View File

@@ -3,7 +3,7 @@ import { Link } from "@tanstack/react-router";
export default function AboutPage() { export default function AboutPage() {
return ( return (
<div className="min-h-screen bg-base-200 flex items-center justify-center p-6"> <div className="min-h-screen bg-base-200 flex items-center justify-center p-6">
<div className="max-w-3xl w-full bg-base-100 shadow-xl rounded-2xl p-8 space-y-6"> <div className="w-full bg-base-100 shadow-xl rounded-2xl p-8 space-y-6">
<h1 className="text-4xl font-bold text-primary text-center">About</h1> <h1 className="text-4xl font-bold text-primary text-center">About</h1>
<div className="space-y-4"> <div className="space-y-4">

View File

@@ -3,7 +3,7 @@ import { Link } from '@tanstack/react-router';
export default function AnalysisPage() { export default function AnalysisPage() {
return ( return (
<div className="min-h-screen bg-base-200 flex items-center justify-center p-6"> <div className="min-h-screen bg-base-200 flex items-center justify-center p-6">
<div className="max-w-4xl w-full bg-base-100 shadow-xl rounded-2xl p-8 space-y-8"> <div className="w-full bg-base-100 shadow-xl rounded-2xl p-8 space-y-8">
<h1 className="text-4xl font-bold text-primary text-center"> <h1 className="text-4xl font-bold text-primary text-center">
Firefly Analysis & Veritas Plugin Firefly Analysis & Veritas Plugin
</h1> </h1>
@@ -144,7 +144,7 @@ export default function AnalysisPage() {
<div className="space-y-2 text-purple-700"> <div className="space-y-2 text-purple-700">
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<span className="font-medium min-w-[20px] text-purple-600">1.</span> <span className="font-medium min-w-[20px] text-purple-600">1.</span>
<p>Launch the <span className="font-semibold">game</span> and your <span className="font-semibold">Firefly Private Server (PS)</span>.</p> <p>Launch the <span className="font-semibold">game</span> and your <span className="font-semibold">Firefly GO Server (PS)</span>.</p>
</div> </div>
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<span className="font-medium min-w-[20px] text-purple-600">2.</span> <span className="font-medium min-w-[20px] text-purple-600">2.</span>

View File

@@ -103,90 +103,79 @@ export default function DiffPage() {
} }
const handleUpdateGame = async () => { const handleUpdateGame = async () => {
const handleResult = (ok: boolean, error: string) => {
if (!ok) {
toast.error(error)
return false
}
return true
}
try { try {
setIsDiffLoading(true) setIsDiffLoading(true)
if (!gameDir || !diffDir) { if (!gameDir || !diffDir) {
toast.error('Please select game directory and diff file') toast.error('Please select game directory and diff file')
setIsDiffLoading(false)
return return
} }
setStageType('Check Type HDiff') setStageType('Check Type HDiff')
setProgressUpdate(0) setProgressUpdate(0)
setMaxProgressUpdate(1) setMaxProgressUpdate(1)
const [isOk, validType, errorType] = await DiffService.CheckTypeHDiff(diffDir) const [isOk, validType, errorType] = await DiffService.CheckTypeHDiff(diffDir)
if (!isOk) { if (!handleResult(isOk, errorType)) return
toast.error(errorType)
setIsDiffLoading(false)
return
}
setProgressUpdate(1) setProgressUpdate(1)
if (validType === 'hdiffmap.json') { if (['hdiffmap.json', 'hdifffiles.txt', 'hdifffiles.json'].includes(validType)) {
setStageType('Version Validate') setStageType('Version Validate')
setProgressUpdate(0) setProgressUpdate(0)
setMaxProgressUpdate(1) setMaxProgressUpdate(1)
const [validVersion, errorVersion] = await DiffService.VersionValidate(gameDir, diffDir) const [validVersion, errorVersion] = await DiffService.VersionValidate(gameDir, diffDir)
if (!validVersion) { if (!handleResult(validVersion, errorVersion)) return
toast.error(errorVersion)
setIsDiffLoading(false)
return
}
setProgressUpdate(1) setProgressUpdate(1)
} }
const isSkipVerify = validType === 'manifest' || validType === 'hdifffiles.txt'
setStageType('Data Extract') setStageType('Data Extract')
const [validData, errorData] = await DiffService.DataExtract(gameDir, diffDir, isSkipVerify) const [validData, errorData] = await DiffService.DataExtract(gameDir, diffDir)
if (!validData) { if (!handleResult(validData, errorData)) return
toast.error(errorData)
setIsDiffLoading(false)
return
}
setStageType('Cut Data') setStageType('Cut Data')
setMessageUpdate('') setMessageUpdate('')
const [validCut, errorCut] = await DiffService.CutData(gameDir) const [validCut, errorCut] = await DiffService.CutData(gameDir)
if (!validCut) { if (!handleResult(validCut, errorCut)) return
toast.error(errorCut)
setIsDiffLoading(false)
return
}
if ( validType === 'hdifffiles.txt' || validType === 'hdiffmap.json') { switch (validType) {
case 'hdifffiles.txt':
case 'hdiffmap.json':
case 'hdifffiles.json': {
setStageType('Patch Data') setStageType('Patch Data')
const [validPatch, errorPatch] = await DiffService.HDiffPatchData(gameDir) const [validPatch, errorPatch] = await DiffService.HDiffPatchData(gameDir)
if (!validPatch) { if (!handleResult(validPatch, errorPatch)) return
toast.error(errorPatch)
setIsDiffLoading(false)
return
}
setStageType('Delete old files') setStageType('Delete old files')
const [validDelete, errorDelete] = await DiffService.DeleteFiles(gameDir) const [validDelete, errorDelete] = await DiffService.DeleteFiles(gameDir)
if (!validDelete) { if (!handleResult(validDelete, errorDelete)) return
toast.error(errorDelete) break
setIsDiffLoading(false)
return
} }
} else if (validType === 'manifest') { case 'manifest': {
setStageType('Patch Data') setStageType('Patch Data')
const [validPatch, errorPatch] = await DiffService.LDiffPatchData(gameDir) const [validPatch, errorPatch] = await DiffService.LDiffPatchData(gameDir)
if (!validPatch) { if (!handleResult(validPatch, errorPatch)) return
toast.error(errorPatch) break
setIsDiffLoading(false)
return
} }
} }
toast.success('Update game completed') toast.success('Update game completed')
} catch (err: any) { } catch (err: any) {
console.error(err) console.error(err)
toast.error('PickFile error:', err) toast.error(`PickFile error: ${err}`)
setIsDiffLoading(false)
} finally { } finally {
setIsDiffLoading(false) setIsDiffLoading(false)
} }
} }
return ( return (
<div className="p-2 mx-4"> <div className="p-2 mx-4">
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto">

View File

@@ -1,23 +1,23 @@
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
export default function SrToolsPage() { export default function FireflyToolsPage() {
return ( return (
<div className="min-h-screen bg-base-200 flex items-center justify-center p-6"> <div className="min-h-screen bg-base-200 flex items-center justify-center p-6">
<div className="max-w-4xl w-full bg-base-100 shadow-xl rounded-2xl p-8 space-y-8"> <div className="w-full bg-base-100 shadow-xl rounded-2xl p-8 space-y-8">
<h1 className="text-4xl font-bold text-primary text-center">SR Tools</h1> <h1 className="text-4xl font-bold text-primary text-center">Firefly Tools</h1>
{/* Section 1: About SR Tools */} {/* Section 1: About SR Tools */}
<div className="bg-blue-50 border-l-4 border-blue-400 p-6 rounded-r-lg"> <div className="bg-blue-50 border-l-4 border-blue-400 p-6 rounded-r-lg">
<h2 className="text-2xl font-bold text-blue-800 flex items-center gap-2 mb-4"> <h2 className="text-2xl font-bold text-blue-800 flex items-center gap-2 mb-4">
<span></span> <span></span>
<span>About SR Tools</span> <span>About Firefly Tools</span>
</h2> </h2>
<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> <p>
This site is a another version of {" "} This site is a another version of {" "}
<span className="font-semibold text-success">SR Tools {" "}</span> <span className="font-semibold text-success">Firefly Tools {" "}</span>
developed by {" "} developed by {" "}
<span className="font-semibold text-warning">Me {"(Kain)"}</span> <span className="font-semibold text-warning">Me {"(Kain)"}</span>
</p> </p>
@@ -95,9 +95,24 @@ export default function SrToolsPage() {
</div> </div>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="text-green-600 text-lg">🔌</div> <div className="text-green-600 text-lg">🔌</div>
<p>Instantly apply setups to <span className="font-semibold text-warning">Firefly Private Server</span> using <span className="font-semibold">Connect PS</span> no manual file uploads required.</p> <p>Instantly apply setups to <span className="font-semibold text-warning">Firefly GO Server</span> using <span className="font-semibold">Connect PS</span> no manual file uploads required.</p>
</div> </div>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="text-green-600 text-2xl"></div>
<div>
<h4 className="font-semibold text-green-800 text-lg">Extra Settings</h4>
<p className="text-green-700 mt-1">
Enhance your <span className="font-semibold text-warning">Firefly GO Server</span> experience with extra features:
</p>
<ul className="list-disc list-inside mt-2 space-y-1 text-green-700">
<li>🎭 <span className="font-medium">Hidden Game UI</span> remove the entire game interface.</li>
<li>🚫 <span className="font-medium">Disable Censorship</span> get rid of Lens Flare censor 💀.</li>
<li>🧪 <span className="font-medium">Theorycraft Mode</span> configure HP, cycles, and more via the web.</li>
</ul>
</div>
</div>
<div className="flex items-start gap-3">
<div className="text-green-600 text-lg">📂</div> <div className="text-green-600 text-lg">📂</div>
<p>Export and import full builds using <code className="bg-gray-200 px-2 py-1 rounded text-sm">freesr-data.json</code>.</p> <p>Export and import full builds using <code className="bg-gray-200 px-2 py-1 rounded text-sm">freesr-data.json</code>.</p>
</div> </div>

View File

@@ -3,7 +3,7 @@ import { Link } from '@tanstack/react-router';
export default function HowToPage() { export default function HowToPage() {
return ( return (
<div className="min-h-screen bg-base-200 flex items-center justify-center p-6"> <div className="min-h-screen bg-base-200 flex items-center justify-center p-6">
<div className="max-w-4xl w-full bg-base-100 shadow-xl rounded-2xl p-8 space-y-8"> <div className=" w-full bg-base-100 shadow-xl rounded-2xl p-8 space-y-8">
<h1 className="text-4xl font-bold text-primary text-center">How to Use</h1> <h1 className="text-4xl font-bold text-primary text-center">How to Use</h1>
{/* Section 1: Launcher Features */} {/* Section 1: Launcher Features */}
@@ -28,13 +28,23 @@ export default function HowToPage() {
</p> </p>
</div> </div>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="text-green-600 text-lg">📦</div> <div className="text-green-600 text-2xl">📦</div>
<p>Patch and update game files using{" "} <div>
<a href="/hdiff" className="link link-info font-mono">Hdiffz Tool</a> <p className="text-green-800 font-semibold">
{" "}(HDiffPatch) fast & lightweight incremental updates. Patch & Update Game Files
</p>
<p className="text-green-700">
Use the{" "}
<a href="/diff" className="link link-info font-mono">Diff Tool</a>{" "}
(<span className="font-medium">DiffPatch</span>) for fast & lightweight incremental updates.
</p>
<p className="text-green-700 mt-1">
Supports <span className="font-semibold">Hdiff</span>, <span className="font-semibold">Ldiff</span>, and custom diff formats.
</p> </p>
</div> </div>
</div> </div>
</div>
</div> </div>
{/* Section 2: FireflyGo Commands */} {/* Section 2: FireflyGo Commands */}
@@ -65,8 +75,60 @@ export default function HowToPage() {
</div> </div>
</div> </div>
<div className="bg-white border border-blue-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<div className="text-blue-600 text-lg"></div>
<div className="flex-1">
<h4 className="font-semibold text-blue-800 mb-1">Extra Settings</h4>
<div className="space-y-4 text-blue-700 text-sm">
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<h5 className="font-semibold text-blue-800 flex items-center gap-2">
🎭 Hidden UI
</h5>
<p className="mt-1">
Instantly hides the entire game UI often used in DIM showcase videos.
</p>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<h5 className="font-semibold text-blue-800 flex items-center gap-2">
🚫 Disable Censorship
</h5>
<p className="mt-1">
Remove the Lens Flare censor effect 💀 for a cleaner experience.
</p>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<h5 className="font-semibold text-blue-800 flex items-center gap-2">
🧪 Theorycraft Mode
</h5>
<p className="mt-1">
No need to type chat commands anymore configure everything through the
web: adjust monster HP, set cycles, view logs, and more.
</p>
</div>
</div>
<div className="mt-4 aspect-w-16 aspect-h-9">
<iframe
src="https://www.youtube.com/embed/uiKdFrvn9NQ"
title="Extra Settings Tutorial"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
className="rounded-lg w-full h-[300px]"
></iframe>
</div>
</div>
</div>
</div>
{/* Commands List */} {/* Commands List */}
<div className="space-y-4"> <div className="space-y-4 mt-4">
<h3 className="text-lg font-semibold text-blue-800">Available Commands:</h3> <h3 className="text-lg font-semibold text-blue-800">Available Commands:</h3>
{/* Theorycraft Toggle */} {/* Theorycraft Toggle */}
@@ -103,16 +165,28 @@ export default function HowToPage() {
<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>
<div className="flex-1"> <div className="flex-1">
<h4 className="font-semibold text-blue-800 mb-1">HP Override <span className="text-red-600 text-sm">(Theorycraft only)</span></h4> <h4 className="font-semibold text-blue-800 mb-1">
<div className="space-y-1 text-blue-700"> HP Override <span className="text-red-600 text-sm">(Theorycraft only)</span>
<p><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp WAVE HP</code> Set monster HP for a specific wave</p> </h4>
<p className="text-sm">Example: <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp 1 2000000</code> sets Wave 1 monster HP to 2,000,000</p> <div className="space-y-2 text-blue-700 text-sm">
<p className="text-sm"><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp 0</code> disables HP override</p> <p>
<code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp N</code> Set monster HP (only available in Theorycraft mode)
</p>
<p>
<code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp 0</code> Disable the set HP feature
</p>
<p>
<code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp Wave V1 V2 ...</code> Set HP for each monster in a specific wave
</p>
<p className="ml-4">
Example: <code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp 1 2000000 3000000</code> sets wave 1 monster1 HP=2,000,000 and monster2 HP=3,000,000
</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/* Log Command */} {/* Log Command */}
<div className="bg-white border border-blue-200 rounded-lg p-4"> <div className="bg-white border border-blue-200 rounded-lg p-4">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">

View File

@@ -405,7 +405,7 @@ export default function LauncherPage() {
<div className="flex justify-center items-center text-sm text-white/80"> <div className="flex justify-center items-center text-sm text-white/80">
<span>{downloadType}</span> <span>{downloadType}</span>
<div className="flex items-center gap-4 ml-4"> <div className="flex items-center gap-4 ml-4">
<span className="text-cyan-400 font-semibold">{downloadSpeed.toFixed(1)} MB/s</span> <span className="text-cyan-400 font-semibold">{downloadSpeed}</span>
<span className="text-white font-bold">{progressDownload.toFixed(1)}%</span> <span className="text-white font-bold">{progressDownload.toFixed(1)}%</span>
</div> </div>
</div> </div>
@@ -414,7 +414,7 @@ export default function LauncherPage() {
className="h-full bg-gradient-to-r from-cyan-400 to-blue-500 rounded-full" className="h-full bg-gradient-to-r from-cyan-400 to-blue-500 rounded-full"
initial={{ width: 0 }} initial={{ width: 0 }}
animate={{ width: `${progressDownload}%` }} animate={{ width: `${progressDownload}%` }}
transition={{ duration: 0.3 }} transition={{ type: "tween", ease: "linear", duration: 0.03 }}
/> />
</div> </div>
<div className="text-center text-xs text-white/60"> <div className="text-center text-xs text-white/60">

View File

@@ -71,7 +71,7 @@ function RootLayout() {
</Link> </Link>
</div> </div>
<div className="navbar-center hidden md:flex"> <div className="navbar-center hidden md:flex">
<ul className="menu menu-horizontal px-1"> <ul className="menu menu-horizontal px-1 gap-4">
<li><Link to="/">Home</Link></li> <li><Link to="/">Home</Link></li>
<li> <li>
<details> <details>
@@ -87,7 +87,7 @@ function RootLayout() {
<summary>Plugins</summary> <summary>Plugins</summary>
<ul className="p-2"> <ul className="p-2">
<li><Link to="/analysis">Analysis (Veritas)</Link></li> <li><Link to="/analysis">Analysis (Veritas)</Link></li>
<li><Link to="/srtools">SrTools</Link></li> <li><Link to="/srtools">Firefly Tools</Link></li>
</ul> </ul>
</details> </details>
</li> </li>

View File

@@ -1,8 +1,8 @@
import SrToolsPage from '@/pages/srtools' import FireflyToolsPage from '@/pages/fireflytools'
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/srtools')({ export const Route = createFileRoute('/srtools')({
component: SrToolsPage, component: FireflyToolsPage,
}) })

View File

@@ -11,7 +11,7 @@ interface LauncherState {
isLoading: boolean; isLoading: boolean;
gameRunning: boolean; gameRunning: boolean;
progressDownload: number; progressDownload: number;
downloadSpeed: number; downloadSpeed: string;
launcherVersion: string; launcherVersion: string;
updateData: Record<'server' | 'proxy' | 'launcher', { isUpdate: boolean, isExists: boolean, version: string }>; updateData: Record<'server' | 'proxy' | 'launcher', { isUpdate: boolean, isExists: boolean, version: string }>;
setDownloadType: (value: string) => void; setDownloadType: (value: string) => void;
@@ -24,7 +24,7 @@ interface LauncherState {
setGameRunning: (value: boolean) => void; setGameRunning: (value: boolean) => void;
setProgressDownload: (value: number) => void; setProgressDownload: (value: number) => void;
setLauncherVersion: (value: string) => void; setLauncherVersion: (value: string) => void;
setDownloadSpeed: (value: number) => void; setDownloadSpeed: (value: string) => void;
setUpdateData: (value: Record<'server' | 'proxy' | 'launcher', { isUpdate: boolean, isExists: boolean, version: string }>) => void; setUpdateData: (value: Record<'server' | 'proxy' | 'launcher', { isUpdate: boolean, isExists: boolean, version: string }>) => void;
} }
@@ -38,7 +38,7 @@ const useLauncherStore = create<LauncherState>((set, get) => ({
proxyRunning: false, proxyRunning: false,
gameRunning: false, gameRunning: false,
progressDownload: 0, progressDownload: 0,
downloadSpeed: 0, downloadSpeed: "",
launcherVersion: "", launcherVersion: "",
updateData: { updateData: {
server: { isUpdate: false, isExists: false, version: "" }, server: { isUpdate: false, isExists: false, version: "" },
@@ -55,7 +55,7 @@ const useLauncherStore = create<LauncherState>((set, get) => ({
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 }), setLauncherVersion: (value: string) => set({ launcherVersion: value }),
setDownloadSpeed: (value: number) => set({ downloadSpeed: value }), setDownloadSpeed: (value: string) => set({ downloadSpeed: value }),
setUpdateData: (value: Record<'server' | 'proxy' | 'launcher', { isUpdate: boolean, isExists: boolean, version: string }>) => set({ updateData: value }), setUpdateData: (value: Record<'server' | 'proxy' | 'launcher', { isUpdate: boolean, isExists: boolean, version: string }>) => set({ updateData: value }),
})); }));

2
go.mod
View File

@@ -10,6 +10,7 @@ require (
require ( require (
aead.dev/minisign v0.3.0 // indirect aead.dev/minisign v0.3.0 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/text v0.28.0 // indirect golang.org/x/text v0.28.0 // indirect
) )
@@ -49,7 +50,6 @@ require (
github.com/wailsapp/go-webview2 v1.0.21 // indirect github.com/wailsapp/go-webview2 v1.0.21 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b
golang.org/x/net v0.43.0 // indirect golang.org/x/net v0.43.0 // indirect
google.golang.org/protobuf v1.36.8 google.golang.org/protobuf v1.36.8

View File

@@ -7,6 +7,7 @@ import (
"firefly-launcher/pkg/hpatchz" "firefly-launcher/pkg/hpatchz"
"firefly-launcher/pkg/models" "firefly-launcher/pkg/models"
"firefly-launcher/pkg/sevenzip" "firefly-launcher/pkg/sevenzip"
"firefly-launcher/pkg/verifier"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@@ -21,6 +22,9 @@ func (h *DiffService) CheckTypeHDiff(patchPath string) (bool, string, string) {
if ok, err := sevenzip.IsFileIn7z(patchPath, "hdifffiles.txt"); err == nil && ok { if ok, err := sevenzip.IsFileIn7z(patchPath, "hdifffiles.txt"); err == nil && ok {
return true, "hdifffiles.txt", "" return true, "hdifffiles.txt", ""
} }
if ok, err := sevenzip.IsFileIn7z(patchPath, "hdifffiles.txt"); err == nil && ok {
return true, "hdifffiles.json", ""
}
if ok, err := sevenzip.IsFileIn7z(patchPath, "hdiffmap.json"); err == nil && ok { if ok, err := sevenzip.IsFileIn7z(patchPath, "hdiffmap.json"); err == nil && ok {
return true, "hdiffmap.json", "" return true, "hdiffmap.json", ""
} }
@@ -32,11 +36,10 @@ func (h *DiffService) CheckTypeHDiff(patchPath string) (bool, string, string) {
} }
func (h *DiffService) VersionValidate(gamePath, patchPath string) (bool, string) { func (h *DiffService) VersionValidate(gamePath, patchPath string) (bool, string) {
oldVersionData, err := models.ParseBinaryVersion(filepath.Join(gamePath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes")) oldBinPath := filepath.Join(gamePath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes")
if err != nil { if _, err := os.Stat(oldBinPath); err != nil {
return false, err.Error() return false, err.Error()
} }
if _, err := os.Stat(patchPath); err != nil { if _, err := os.Stat(patchPath); err != nil {
return false, err.Error() return false, err.Error()
} }
@@ -46,28 +49,104 @@ func (h *DiffService) VersionValidate(gamePath, patchPath string) (bool, string)
return false, err.Error() return false, err.Error()
} }
} }
okFull, errFull := sevenzip.IsFileIn7z(patchPath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes")
okDiff, errDiff := sevenzip.IsFileIn7z(patchPath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes.hdiff")
if (errFull != nil && errDiff != nil) || (!okFull && !okDiff) {
return false, "BinaryVersion file not found in patch"
}
var tempBinFile string
if okFull {
if err := sevenzip.ExtractAFileFromZip(patchPath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes", constant.TempUrl); err != nil { if err := sevenzip.ExtractAFileFromZip(patchPath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes", constant.TempUrl); err != nil {
return false, err.Error() return false, err.Error()
} }
tempBinFile = filepath.Join(constant.TempUrl, "BinaryVersion.bytes")
binPath := filepath.Join(constant.TempUrl, "BinaryVersion.bytes") } else {
newVersionData, err := models.ParseBinaryVersion(binPath) if err := sevenzip.ExtractAFileFromZip(patchPath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes.hdiff", constant.TempUrl); err != nil {
if err != nil {
return false, err.Error() return false, err.Error()
} }
defer os.Remove(binPath) patchBinFile := filepath.Join(constant.TempUrl, "BinaryVersion.bytes.hdiff")
sourceBinFile := oldBinPath
v := newVersionData.Subtract(*oldVersionData) tempBinFile = filepath.Join(constant.TempUrl, "BinaryVersion.bytes")
if v != 0 && v != 1 { if err := hpatchz.ApplyPatch(sourceBinFile, patchBinFile, tempBinFile); err != nil {
return false, fmt.Sprintf("the diff version %s not valid with game version %s", newVersionData, oldVersionData) os.Remove(patchBinFile)
return false, err.Error()
}
os.Remove(patchBinFile)
} }
okFullPkg, err1 := sevenzip.IsFileIn7z(patchPath, "pkg_version")
okDiffPkg, err2 := sevenzip.IsFileIn7z(patchPath, "pkg_version.hdiff")
if err1 != nil && err2 != nil {
return false, err1.Error()
}
if okFullPkg {
if err := sevenzip.ExtractAFileFromZip(patchPath, "pkg_version", constant.TempUrl); err != nil {
return false, err.Error()
}
}
if okDiffPkg {
if err := sevenzip.ExtractAFileFromZip(patchPath, "pkg_version.hdiff", constant.TempUrl); err != nil {
return false, err.Error()
}
patchPkgFile := filepath.Join(constant.TempUrl, "pkg_version.hdiff")
sourcePkgFile := filepath.Join(gamePath, "pkg_version")
tempPkgFile := filepath.Join(constant.TempUrl, "pkg_version")
if err := hpatchz.ApplyPatch(sourcePkgFile, patchPkgFile, tempPkgFile); err != nil {
os.Remove(patchPkgFile)
os.Remove(tempPkgFile)
return false, err.Error()
}
os.Remove(patchPkgFile)
}
tempPkgFile := filepath.Join(constant.TempUrl, "pkg_version")
pkgDataList, err := models.LoadPkgVersion(tempPkgFile)
if err != nil {
os.Remove(tempPkgFile)
return false, err.Error()
}
os.Remove(tempPkgFile)
// MD5 check BinaryVersion
flags := false
for _, pkgData := range pkgDataList {
if filepath.ToSlash(pkgData.RemoteFile) == "StarRail_Data/StreamingAssets/BinaryVersion.bytes" {
flags = true
md5, err := verifier.FileMD5(tempBinFile)
if err != nil {
os.Remove(tempBinFile)
return false, err.Error()
}
if md5 != pkgData.MD5 {
os.Remove(tempBinFile)
return false, fmt.Sprintf("md5 mismatch for %s: expected %s, got %s",
tempBinFile, pkgData.MD5, md5)
}
break
}
}
if !flags {
os.Remove(tempBinFile)
return false, "BinaryVersion file not found in patch"
}
_, err = models.ParseBinaryVersion(tempBinFile)
if err != nil {
os.Remove(tempBinFile)
return false, err.Error()
}
os.Remove(tempBinFile)
return true, "validated" return true, "validated"
} }
func (h *DiffService) HDiffPatchData(gamePath string) (bool, string) { func (h *DiffService) HDiffPatchData(gamePath string) (bool, string) {
hdiffMapPath := filepath.Join(gamePath, "hdiffmap.json") hdiffMapPath := filepath.Join(gamePath, "hdiffmap.json")
hdiffFilesPath := filepath.Join(gamePath, "hdifffiles.txt") hdiffFilesPath := filepath.Join(gamePath, "hdifffiles.txt")
hdifffilesJsonPath := filepath.Join(gamePath, "hdifffiles.json")
var jsonData struct { var jsonData struct {
DiffMap []*models.HDiffData `json:"diff_map"` DiffMap []*models.HDiffData `json:"diff_map"`
@@ -88,6 +167,14 @@ func (h *DiffService) HDiffPatchData(gamePath string) (bool, string) {
for _, entry := range jsonDataDiffMap.DiffMap { for _, entry := range jsonDataDiffMap.DiffMap {
jsonData.DiffMap = append(jsonData.DiffMap, entry.ToHDiffData()) jsonData.DiffMap = append(jsonData.DiffMap, entry.ToHDiffData())
} }
} else if _, err := os.Stat(hdifffilesJsonPath); err == nil {
data, err := os.ReadFile(hdifffilesJsonPath)
if err != nil {
return false, err.Error()
}
if err := json.Unmarshal(data, &jsonData); err != nil {
return false, err.Error()
}
} else if _, err := os.Stat(hdiffFilesPath); err == nil { } else if _, err := os.Stat(hdiffFilesPath); err == nil {
files, err := models.LoadHDiffFiles(hdiffFilesPath) files, err := models.LoadHDiffFiles(hdiffFilesPath)
if err != nil { if err != nil {
@@ -99,6 +186,7 @@ func (h *DiffService) HDiffPatchData(gamePath string) (bool, string) {
} else { } else {
return false, "no hdiff entries map exist" return false, "no hdiff entries map exist"
} }
application.Get().Event.Emit("diff:stage", map[string]string{"stage": "Patching HDiff"}) application.Get().Event.Emit("diff:stage", map[string]string{"stage": "Patching HDiff"})
for i, entry := range jsonData.DiffMap { for i, entry := range jsonData.DiffMap {
application.Get().Event.Emit( application.Get().Event.Emit(
@@ -134,6 +222,7 @@ func (h *DiffService) HDiffPatchData(gamePath string) (bool, string) {
os.Remove(filepath.Join(gamePath, "hdiffmap.json")) os.Remove(filepath.Join(gamePath, "hdiffmap.json"))
os.Remove(filepath.Join(gamePath, "hdifffiles.txt")) os.Remove(filepath.Join(gamePath, "hdifffiles.txt"))
os.Remove(filepath.Join(gamePath, "hdifffiles.json"))
return true, "patching completed" return true, "patching completed"
} }

View File

@@ -2,15 +2,15 @@ package diffService
import ( import (
"firefly-launcher/pkg/constant" "firefly-launcher/pkg/constant"
"firefly-launcher/pkg/verifier"
"firefly-launcher/pkg/sevenzip" "firefly-launcher/pkg/sevenzip"
"github.com/wailsapp/wails/v3/pkg/application" "io"
"os" "os"
"path/filepath" "path/filepath"
"io"
"github.com/wailsapp/wails/v3/pkg/application"
) )
func (h *DiffService) DataExtract(gamePath, patchPath string, isSkipVerify bool) (bool, string) { func (h *DiffService) DataExtract(gamePath, patchPath string) (bool, string) {
os.RemoveAll(constant.TempUrl) os.RemoveAll(constant.TempUrl)
if _, err := os.Stat(gamePath); err != nil { if _, err := os.Stat(gamePath); err != nil {
return false, err.Error() return false, err.Error()
@@ -30,20 +30,7 @@ func (h *DiffService) DataExtract(gamePath, patchPath string, isSkipVerify bool)
os.RemoveAll(constant.TempUrl) os.RemoveAll(constant.TempUrl)
return false, err.Error() return false, err.Error()
} }
return true, "extract completed"
if !isSkipVerify {
validator, err := verifier.NewVerifier(gamePath, constant.TempUrl)
if err != nil {
os.RemoveAll(constant.TempUrl)
return false, err.Error()
}
if err := validator.VerifyAll(); err != nil {
os.RemoveAll(constant.TempUrl)
return false, err.Error()
}
}
return true, "validated"
} }
func (h *DiffService) CutData(gamePath string) (bool, string) { func (h *DiffService) CutData(gamePath string) (bool, string) {

View File

@@ -38,53 +38,17 @@ func (g *GitService) GetLatestLauncherVersion() (bool, string, string) {
func (g *GitService) UpdateLauncherProgress(version string) (bool, string) { func (g *GitService) UpdateLauncherProgress(version string) (bool, string) {
resp, err := http.Get(constant.LauncherGitUrl) asset, ok := g.getReleaseAsset(version, constant.LauncherGitUrl, constant.LauncherFile)
if err != nil { if !ok {
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" return false, "no release found"
} }
var assetWin models.AssetType resp, err := http.Get(asset.BrowserDownloadURL)
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 { if err != nil {
return false, err.Error() return false, err.Error()
} }
defer resp.Body.Close() defer resp.Body.Close()
err = selfupdate.Apply(resp.Body, selfupdate.Options{}) err = selfupdate.Apply(resp.Body, selfupdate.Options{})
if err != nil { if err != nil {
return false, err.Error() return false, err.Error()

View File

@@ -1,6 +1,7 @@
package gitService package gitService
import ( import (
"encoding/json"
"firefly-launcher/pkg/constant" "firefly-launcher/pkg/constant"
"firefly-launcher/pkg/models" "firefly-launcher/pkg/models"
"fmt" "fmt"
@@ -8,7 +9,8 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"encoding/json" "time"
"github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/application"
) )
@@ -35,72 +37,35 @@ func (g *GitService) GetLatestProxyVersion() (bool, string, string) {
} }
func (g *GitService) DownloadProxyProgress(version string) (bool, string) { func (g *GitService) DownloadProxyProgress(version string) (bool, string) {
resp, err := http.Get(constant.ProxyGitUrl) asset, ok := g.getReleaseAsset(version, constant.ProxyGitUrl, constant.ProxyZipFile)
if err != nil { if !ok {
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" return false, "no release found"
} }
var assetWin models.AssetType if err := os.MkdirAll(constant.ProxyStorageUrl, 0755); err != nil {
for _, asset := range releaseData.Assets {
if asset.Name == constant.ProxyZipFile {
assetWin = asset
break
}
}
if assetWin.Name == "" {
return false, "no assets found"
}
if err := os.Mkdir(constant.ProxyStorageUrl, 0755); err != nil {
if !os.IsExist(err) {
return false, err.Error() return false, err.Error()
} }
}
saveFile := filepath.Join(constant.ProxyStorageUrl, assetWin.Name)
resp, err = http.Get(assetWin.BrowserDownloadURL) saveFile := filepath.Join(constant.ProxyStorageUrl, asset.Name)
tmpPath, err := g.downloadFileParallel(saveFile, asset.BrowserDownloadURL, 4, func(percent float64, speed string) {
application.Get().Event.Emit("download:proxy", map[string]interface{}{
"percent": fmt.Sprintf("%.2f", percent),
"speed": speed,
})
})
if err != nil { if err != nil {
return false, err.Error() return false, err.Error()
} }
defer resp.Body.Close() for i := 0; i < 3; i++ {
if err := os.Rename(tmpPath, saveFile); err == nil {
DownloadFile(saveFile, assetWin.BrowserDownloadURL, func(percent float64, speed float64) {
application.Get().Event.Emit("download:proxy", map[string]interface{}{
"percent": fmt.Sprintf("%.2f", percent),
"speed": fmt.Sprintf("%.2f", speed),
})
})
return true, "" return true, ""
}
time.Sleep(300 * time.Millisecond)
}
return false, "failed to rename tmp file after retries"
} }
func (g *GitService) UnzipProxy() { func (g *GitService) UnzipProxy() {
unzipParallel(filepath.Join(constant.ProxyStorageUrl, constant.ProxyZipFile), constant.ProxyStorageUrl) g.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))
} }

View File

@@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"time"
"github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/application"
) )
@@ -36,72 +37,35 @@ func (g *GitService) GetLatestServerVersion() (bool, string, string) {
} }
func (g *GitService) DownloadServerProgress(version string) (bool, string) { func (g *GitService) DownloadServerProgress(version string) (bool, string) {
resp, err := http.Get(constant.ServerGitUrl) asset, ok := g.getReleaseAsset(version, constant.ServerGitUrl, constant.ServerZipFile)
if err != nil { if !ok {
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" return false, "no release found"
} }
var assetWin models.AssetType if err := os.MkdirAll(constant.ServerStorageUrl, 0755); err != nil {
for _, asset := range releaseData.Assets {
if asset.Name == constant.ServerZipFile {
assetWin = asset
break
}
}
if assetWin.Name == "" {
return false, "no assets found"
}
if err := os.Mkdir(constant.ServerStorageUrl, 0755); err != nil {
if !os.IsExist(err) {
return false, err.Error() return false, err.Error()
} }
}
saveFile := filepath.Join(constant.ServerStorageUrl, assetWin.Name) saveFile := filepath.Join(constant.ServerStorageUrl, asset.Name)
tmpPath, err := g.downloadFileParallel(saveFile, asset.BrowserDownloadURL, 4, func(percent float64, speed string) {
resp, err = http.Get(assetWin.BrowserDownloadURL) application.Get().Event.Emit("download:server", map[string]interface{}{
"percent": fmt.Sprintf("%.2f", percent),
"speed": speed,
})
})
if err != nil { if err != nil {
return false, err.Error() return false, err.Error()
} }
defer resp.Body.Close() for i := 0; i < 3; i++ {
if err := os.Rename(tmpPath, saveFile); err == nil {
DownloadFile(saveFile, assetWin.BrowserDownloadURL, func(percent float64, speed float64) {
application.Get().Event.Emit("download:server", map[string]interface{}{
"percent": fmt.Sprintf("%.2f", percent),
"speed": fmt.Sprintf("%.2f", speed),
})
})
return true, "" return true, ""
}
time.Sleep(300 * time.Millisecond)
}
return false, "failed to rename tmp file after retries"
} }
func (g *GitService) UnzipServer() { func (g *GitService) UnzipServer() {
unzipParallel(filepath.Join(constant.ServerStorageUrl, constant.ServerZipFile), constant.ServerStorageUrl) g.unzipParallel(filepath.Join(constant.ServerStorageUrl, constant.ServerZipFile), constant.ServerStorageUrl)
os.Remove(filepath.Join(constant.ServerStorageUrl, constant.ServerZipFile)) os.Remove(filepath.Join(constant.ServerStorageUrl, constant.ServerZipFile))
} }

View File

@@ -2,6 +2,8 @@ package gitService
import ( import (
"archive/zip" "archive/zip"
"encoding/json"
"firefly-launcher/pkg/models"
"fmt" "fmt"
"io" "io"
"math" "math"
@@ -11,41 +13,57 @@ import (
"runtime" "runtime"
"sync" "sync"
"time" "time"
) )
func humanFormat(bytes int64) string { func HumanFormat(bytes float64) string {
n := float64(bytes) if math.IsNaN(bytes) || math.IsInf(bytes, 0) {
for _, unit := range []string{"", "Ki", "Mi", "Gi"} { return fmt.Sprintf("%v", bytes)
if math.Abs(n) < 1024.0 {
return fmt.Sprintf("%3.1f%sB", n, unit)
} }
n /= 1024.0 if bytes == 0 {
return "0B"
} }
return fmt.Sprintf("%.1fTiB", n)
neg := bytes < 0
if neg {
bytes = -bytes
}
units := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
i := 0
for bytes >= 1024 && i < len(units)-1 {
bytes /= 1024
i++
}
res := fmt.Sprintf("%.1f%s", bytes, units[i])
if neg {
res = "-" + res
}
return res
} }
type WriteCounter struct { type WriteCounter struct {
Total uint64 Total uint64
StartTime time.Time StartTime time.Time
OnEmit func(percent float64, speedMBps float64) OnEmit func(percent float64, speed string)
TotalSize int64 TotalSize int64
lastLoggedPercent int mu sync.Mutex
} }
func NewWriteCounter(total int64, onEmit func(percent float64, speedMBps float64)) *WriteCounter { func NewWriteCounter(total int64, onEmit func(percent float64, speed string)) *WriteCounter {
return &WriteCounter{ return &WriteCounter{
StartTime: time.Now(), StartTime: time.Now(),
TotalSize: total, TotalSize: total,
lastLoggedPercent: -1,
OnEmit: onEmit, OnEmit: onEmit,
} }
} }
func (wc *WriteCounter) Write(p []byte) (int, error) { func (wc *WriteCounter) Add(n int) {
n := len(p) wc.mu.Lock()
defer wc.mu.Unlock()
wc.Total += uint64(n) wc.Total += uint64(n)
wc.PrintProgress() wc.PrintProgress()
return n, nil
} }
func (wc *WriteCounter) PrintProgress() { func (wc *WriteCounter) PrintProgress() {
@@ -53,63 +71,108 @@ func (wc *WriteCounter) PrintProgress() {
if elapsed < 0.001 { if elapsed < 0.001 {
elapsed = 0.001 elapsed = 0.001
} }
speed := float64(wc.Total) / elapsed
speed := float64(wc.Total) / 1024 / 1024 / elapsed // MB/s
percent := float64(wc.Total) / float64(wc.TotalSize) * 100 percent := float64(wc.Total) / float64(wc.TotalSize) * 100
if wc.OnEmit != nil { if wc.OnEmit != nil {
wc.OnEmit(percent, speed) wc.OnEmit(percent, fmt.Sprintf("%s/s", HumanFormat(speed)))
} }
} }
func DownloadFile(filepath string, url string, onEmit func(percent float64, speed float64)) error { func (g *GitService) downloadFileParallel(filePath, url string, numParts int, onEmit func(percent float64, speed string)) (tmpPath string, err error) {
tmpPath := filepath + ".tmp" resp, err := http.Head(url)
resp, err := http.Get(url)
if err != nil { if err != nil {
return fmt.Errorf("failed to get file: %w", err) return "", fmt.Errorf("failed to get head: %w", err)
} }
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return fmt.Errorf("bad status: %s", resp.Status) return "", fmt.Errorf("bad status: %s", resp.Status)
} }
size := resp.ContentLength
tmpPath = filePath + ".tmp"
out, err := os.Create(tmpPath) out, err := os.Create(tmpPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to create tmp file: %w", err) return "", fmt.Errorf("failed to create tmp file: %w", err)
}
defer out.Close()
counter := NewWriteCounter(size, onEmit)
partSize := size / int64(numParts)
var wg sync.WaitGroup
var mu sync.Mutex
for i := 0; i < numParts; i++ {
start := int64(i) * partSize
end := start + partSize - 1
if i == numParts-1 {
end = size - 1
} }
counter := NewWriteCounter(resp.ContentLength, onEmit) _ = start
_, err = io.Copy(out, io.TeeReader(resp.Body, counter)) _ = end
if closeErr := out.Close(); closeErr != nil {
return fmt.Errorf("failed to close tmp file: %w", closeErr) wg.Go(func() {
} req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", start, end))
resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return fmt.Errorf("failed to download file: %w", err) return
} }
defer resp.Body.Close()
// Delete destination file if it exists buf := make([]byte, 32*1024)
if _, err := os.Stat(filepath); err == nil { var written int64
if err := os.Remove(filepath); err != nil { for {
return fmt.Errorf("failed to remove existing file: %w", err) n, err := resp.Body.Read(buf)
if n > 0 {
mu.Lock()
out.Seek(start+written, 0)
out.Write(buf[:n])
mu.Unlock()
written += int64(n)
counter.Add(n)
} }
} if err == io.EOF {
for i := 0; i < 3; i++ {
err = os.Rename(tmpPath, filepath)
if err == nil {
break break
} }
time.Sleep(300 * time.Millisecond)
}
if err != nil { if err != nil {
return fmt.Errorf("failed to rename after retries: %w", err) break
}
}
})
} }
return nil wg.Wait()
return tmpPath, nil
} }
func unzipParallel(src string, dest string) error { // --- Helper getReleaseAsset ---
func (g *GitService) getReleaseAsset(version, url, fileName string) (models.AssetType, bool) {
resp, err := http.Get(url)
if err != nil {
return models.AssetType{}, false
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var releases []*models.ReleaseType
if err := json.Unmarshal(body, &releases); err != nil || len(releases) == 0 {
return models.AssetType{}, false
}
for _, release := range releases {
if release.TagName == version {
for _, asset := range release.Assets {
if asset.Name == fileName {
return asset, true
}
}
}
}
return models.AssetType{}, false
}
func (g *GitService) unzipParallel(src string, dest string) error {
numCPU := runtime.NumCPU() numCPU := runtime.NumCPU()
reserved := 1 reserved := 1
@@ -137,11 +200,11 @@ func unzipParallel(src string, dest string) error {
jobs := make(chan job) jobs := make(chan job)
var wg sync.WaitGroup var wg sync.WaitGroup
// Worker pool
for i := 0; i < maxWorkers; i++ { for i := 0; i < maxWorkers; i++ {
wg.Go(func() { wg.Go(func() {
for j := range jobs { for j := range jobs {
err := extractFile(j.f, dest) err := g.extractFile(j.f, dest)
if err != nil { if err != nil {
fmt.Printf("Error extracting %s: %v\n", j.f.Name, err) fmt.Printf("Error extracting %s: %v\n", j.f.Name, err)
} }
@@ -149,17 +212,17 @@ func unzipParallel(src string, dest string) error {
}) })
} }
// Feed jobs
for _, f := range r.File { for _, f := range r.File {
jobs <- job{f} jobs <- job{f}
} }
close(jobs) close(jobs)
wg.Wait() wg.Wait()
return nil return nil
} }
func extractFile(f *zip.File, dest string) error { func (g *GitService) extractFile(f *zip.File, dest string) error {
fp := filepath.Join(dest, f.Name) fp := filepath.Join(dest, f.Name)
if f.FileInfo().IsDir() { if f.FileInfo().IsDir() {

View File

@@ -98,7 +98,6 @@ 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

@@ -10,7 +10,7 @@ const ProxyZipFile = "64bit.zip"
const LauncherFile = "firefly-launcher.exe" const LauncherFile = "firefly-launcher.exe"
const TempUrl = "./temp" const TempUrl = "./temp"
const CurrentLauncherVersion = "1.5.1" const CurrentLauncherVersion = "1.9.0"
type ToolFile string type ToolFile string
@@ -19,15 +19,13 @@ const (
Tool7zaDLL ToolFile = "bin/7za.dll" Tool7zaDLL ToolFile = "bin/7za.dll"
Tool7zxaDLL ToolFile = "bin/7zxa.dll" Tool7zxaDLL ToolFile = "bin/7zxa.dll"
ToolHPatchzExe ToolFile = "bin/hpatchz.exe" ToolHPatchzExe ToolFile = "bin/hpatchz.exe"
ToolHDiffzExe ToolFile = "bin/hdiffz.exe"
) )
var RequiredFiles = map[ToolFile]string{ var RequiredFiles = map[ToolFile]string{
Tool7zaExe: "assets/7zip/7za.exe", Tool7zaExe: "assets/7za.exe",
Tool7zaDLL: "assets/7zip/7za.dll", Tool7zaDLL: "assets/7za.dll",
Tool7zxaDLL: "assets/7zip/7zxa.dll", Tool7zxaDLL: "assets/7zxa.dll",
ToolHPatchzExe: "assets/HDiffPatch/hpatchz.exe", ToolHPatchzExe: "assets/hpatchz.exe",
ToolHDiffzExe: "assets/HDiffPatch/hdiffz.exe",
} }
func (t ToolFile) GetEmbedPath() string { func (t ToolFile) GetEmbedPath() string {

View File

@@ -6,7 +6,6 @@ import (
"os" "os"
"regexp" "regexp"
"strconv" "strconv"
"strings"
) )
type BinaryVersion struct { type BinaryVersion struct {
@@ -14,6 +13,7 @@ type BinaryVersion struct {
Major int Major int
Minor int Minor int
Patch int Patch int
Data []byte
} }
func ParseBinaryVersion(path string) (*BinaryVersion, error) { func ParseBinaryVersion(path string) (*BinaryVersion, error) {
@@ -23,58 +23,39 @@ func ParseBinaryVersion(path string) (*BinaryVersion, error) {
} }
content := string(data) content := string(data)
fmt.Println(content)
lastDash := strings.LastIndex(content, "-") re := regexp.MustCompile(`([A-Za-z]+)(\d+)(?:\.(\d+))?(?:\.(\d+))?`)
if lastDash == -1 { matches := re.FindStringSubmatch(content)
return nil, errors.New("no dash found in version string") if len(matches) < 2 {
}
secondLastDash := strings.LastIndex(content[:lastDash], "-")
if secondLastDash == -1 {
return nil, errors.New("only one dash found in version string")
}
versionSlice := content[secondLastDash+1 : lastDash]
re := regexp.MustCompile(`^([A-Za-z]+)([\d\.]+)$`)
matches := re.FindStringSubmatch(versionSlice)
if len(matches) < 3 {
return nil, errors.New("invalid version format") return nil, errors.New("invalid version format")
} }
binaryVersion := BinaryVersion{ binaryVersion := BinaryVersion{
Name: matches[1], Name: matches[1],
} }
numbers := strings.Split(matches[2], ".") if matches[2] != "" {
binaryVersion.Major, _ = strconv.Atoi(matches[2])
if len(numbers) > 0 {
binaryVersion.Major, err = strconv.Atoi(numbers[0])
if err != nil {
return nil, err
} }
if len(matches) > 3 && matches[3] != "" {
binaryVersion.Minor, _ = strconv.Atoi(matches[3])
} }
if len(numbers) > 1 { if len(matches) > 4 && matches[4] != "" {
binaryVersion.Minor, err = strconv.Atoi(numbers[1]) binaryVersion.Patch, _ = strconv.Atoi(matches[4])
if err != nil {
return nil, err
}
}
if len(numbers) > 2 {
binaryVersion.Patch, err = strconv.Atoi(numbers[2])
if err != nil {
return nil, err
}
} }
binaryVersion.Data = data
return &binaryVersion, nil return &binaryVersion, nil
} }
func (v *BinaryVersion) String() string { func (v *BinaryVersion) String() string {
return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch) return fmt.Sprintf("%s-%d.%d.%d", v.Name, v.Major, v.Minor, v.Patch)
} }
func (v BinaryVersion) ToInt() int { func (v BinaryVersion) ToInt() int {
return v.Major*100 + v.Minor*10 + v.Patch return v.Major*100 + v.Minor*10 + v.Patch
} }
func (v BinaryVersion) Subtract(other BinaryVersion) int { func (v BinaryVersion) Subtract(other *BinaryVersion) int {
return v.ToInt() - other.ToInt() return v.ToInt() - other.ToInt()
} }

View File

@@ -67,7 +67,7 @@ func check(relPath string, expectedSize int64, expectedMD5, base string) error {
fullPath, expectedSize, info.Size()) fullPath, expectedSize, info.Size())
} }
md5Hash, err := fileMD5(fullPath) md5Hash, err := FileMD5(fullPath)
if err != nil { if err != nil {
return fmt.Errorf("error reading %s: %w", fullPath, err) return fmt.Errorf("error reading %s: %w", fullPath, err)
} }
@@ -80,7 +80,7 @@ func check(relPath string, expectedSize int64, expectedMD5, base string) error {
return nil return nil
} }
func fileMD5(path string) (string, error) { func FileMD5(path string) (string, error) {
f, err := os.Open(path) f, err := os.Open(path)
if err != nil { if err != nil {
return "", err return "", err