18 Commits
1.0 ... 2.0.0

Author SHA1 Message Date
dcdb3863ca FIX: Fix appicon resolution 2025-10-07 11:14:33 +07:00
d5710c9102 FIX: Fix appicon resolution 2025-10-07 11:12:53 +07:00
100c93e8da UPDATE: Update howto, fix bug bytes humman format 2025-10-04 20:30:51 +07:00
1e17f76340 FIX: Fix bug version parse 2025-09-26 10:06:49 +07:00
1f9c95d6ac FIX: Fix bug hdiff 2025-09-09 08:47:16 +07:00
d58e76d821 UPDATE: Add precheck version hdiff, support another hdiff 2025-08-29 21:13:10 +07:00
a4de02bc18 UPDATE: Add mutithread download 2025-08-27 08:27:02 +07:00
a021658fa9 FIX: Fix bug check diff type 2025-08-26 09:58:30 +07:00
6b222bfa70 UPDATE: Add ldiff 2025-08-25 18:12:13 +07:00
99b9df1ce5 UPDATE: update to go 1.25 2025-08-24 17:22:30 +07:00
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
95f8ed357d add skip check 2025-07-22 14:07:33 +07:00
111 changed files with 7130 additions and 4054 deletions

3
.gitignore vendored
View File

@@ -1,9 +1,10 @@
.task
bin
bin/
tests/
server/
proxy/
temp/
.history/
frontend/dist
frontend/node_modules
build/linux/appimage/build

34
Makefile Normal file
View File

@@ -0,0 +1,34 @@
PROTO_SRC=./proto
GEN_DEST=./pkg
proto:
@echo Compiling Protobuf files...
protoc --go_out=${GEN_DEST} --go-grpc_out=${GEN_DEST} ${PROTO_SRC}/*.proto
@echo Done!
icon:
@echo Updating icons...
magick build/appicon.png -define icon:auto-resize=16,24,32,48,64,128,256 build/windows/icon.ico
magick build/appicon.png -define icon:auto-resize=16,24,32,48,64,128,256,512 build/windows/icons.ico
@echo Done!
dev:
@echo Starting development server...
wails3 dev
@echo Done!
build:
@echo Building application...
wails3 build
@echo Done!
release:
@echo Building release application...
wails3 package
@echo Done!
update:
@echo Updating build assets...
wails3 update build-assets -name "firefly-launcher" -binaryname "firefly-launcher" -config build/config.yml
@echo Done!

Binary file not shown.

Binary file not shown.

BIN
assets/appicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
assets/hpatchz.exe Normal file

Binary file not shown.

View File

@@ -22,15 +22,19 @@ tasks:
- npm install
build:frontend:
label: build:frontend (PRODUCTION={{.PRODUCTION}})
summary: Build the frontend project
dir: frontend
sources:
- "**/*"
generates:
- dist/*
- dist/**/*
deps:
- task: install:frontend:deps
- task: generate:bindings
vars:
BUILD_FLAGS:
ref: .BUILD_FLAGS
cmds:
- npm run {{.BUILD_COMMAND}} -q
env:
@@ -40,15 +44,19 @@ tasks:
generate:bindings:
label: generate:bindings (BUILD_FLAGS={{.BUILD_FLAGS}})
summary: Generates bindings for the frontend
deps:
- task: go:mod:tidy
sources:
- "**/*.[jt]s"
- exclude: frontend/**/*
- frontend/bindings/**/* # Rerun when switching between dev/production mode causes changes in output
- "**/*.go"
- go.mod
- go.sum
generates:
- "frontend/bindings/**/*"
- frontend/bindings/**/*
cmds:
- wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true
@@ -58,10 +66,10 @@ tasks:
sources:
- "appicon.png"
generates:
- "icons.icns"
- "icons.ico"
- "darwin/icons.icns"
- "windows/icon.ico"
cmds:
- wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icons.ico
- wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico
dev:frontend:
summary: Runs the frontend in development mode

Binary file not shown.

Before

Width:  |  Height:  |  Size: 944 KiB

After

Width:  |  Height:  |  Size: 304 KiB

View File

@@ -11,7 +11,7 @@ info:
description: "Custom game launcher built for convenience and quick access" # The application description
copyright: "@ 2025, Firefly Shelter" # Copyright text
comments: "Custom game launcher built for convenience and quick access" # Comments
version: "0.1.0" # The application version
version: "1.0.0" # The application version
# Dev mode configuration
dev_mode:
@@ -44,9 +44,19 @@ dev_mode:
type: primary
# File Associations
# More information at: https://v3alpha.wails.io/noit/done/yet
# More information at: https://v3.wails.io/noit/done/yet
fileAssociations:
# - ext: wails
# name: Wails
# description: Wails Application File
# iconName: wailsFileIcon
# role: Editor
# - ext: jpg
# name: JPEG
# description: Image File
# iconName: jpegFileIcon
# role: Editor
# mimeType: image/jpeg # (optional)
# Other data
other:

View File

@@ -6,19 +6,19 @@
<key>CFBundleName</key>
<string>Firefly Launcher</string>
<key>CFBundleExecutable</key>
<string>Firefly Launcher</string>
<string>firefly-launcher</string>
<key>CFBundleIdentifier</key>
<string>com.fireflyshelter.fireflylauncher</string>
<key>CFBundleVersion</key>
<string>0.1.0</string>
<string>1.0.0</string>
<key>CFBundleGetInfoString</key>
<string>Custom game launcher built for convenience and quick access</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<string>1.0.0</string>
<key>CFBundleIconFile</key>
<string>icons</string>
<key>LSMinimumSystemVersion</key>
<string>10.13.0</string>
<string>10.15.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>

View File

@@ -6,19 +6,19 @@
<key>CFBundleName</key>
<string>Firefly Launcher</string>
<key>CFBundleExecutable</key>
<string>Firefly Launcher</string>
<string>firefly-launcher</string>
<key>CFBundleIdentifier</key>
<string>com.fireflyshelter.fireflylauncher</string>
<key>CFBundleVersion</key>
<string>0.1.0</string>
<string>1.0.0</string>
<key>CFBundleGetInfoString</key>
<string>Custom game launcher built for convenience and quick access</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<string>1.0.0</string>
<key>CFBundleIconFile</key>
<string>icons</string>
<key>LSMinimumSystemVersion</key>
<string>10.13.0</string>
<string>10.15.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>

View File

@@ -9,6 +9,11 @@ tasks:
deps:
- task: common:go:mod:tidy
- task: common:build:frontend
vars:
BUILD_FLAGS:
ref: .BUILD_FLAGS
PRODUCTION:
ref: .PRODUCTION
- task: common:generate:icons
cmds:
- go build {{.BUILD_FLAGS}} -o {{.OUTPUT}}
@@ -64,7 +69,13 @@ tasks:
- cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources
- cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS
- cp build/darwin/Info.plist {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents
- codesign --force --deep --sign - {{.BIN_DIR}}/{{.APP_NAME}}.app
run:
cmds:
- '{{.BIN_DIR}}/{{.APP_NAME}}'
- mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/{MacOS,Resources}
- cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources
- cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS
- cp build/darwin/Info.dev.plist {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Info.plist
- codesign --force --deep --sign - {{.BIN_DIR}}/{{.APP_NAME}}.dev.app
- '{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS/{{.APP_NAME}}'

Binary file not shown.

View File

@@ -9,6 +9,11 @@ tasks:
deps:
- task: common:go:mod:tidy
- task: common:build:frontend
vars:
BUILD_FLAGS:
ref: .BUILD_FLAGS
PRODUCTION:
ref: .PRODUCTION
- task: common:generate:icons
cmds:
- go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}
@@ -105,7 +110,7 @@ tasks:
vars:
APP_NAME: '{{.APP_NAME}}'
EXEC: '{{.APP_NAME}}'
ICON: 'appicon'
ICON: '{{.APP_NAME}}'
CATEGORIES: 'Development;'
OUTPUTFILE: '{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop'

View File

@@ -14,12 +14,21 @@ cp -r "${APP_BINARY}" "${APP_DIR}/usr/bin/"
cp "${ICON_PATH}" "${APP_DIR}/"
cp "${DESKTOP_FILE}" "${APP_DIR}/"
if [[ $(uname -m) == *x86_64* ]]; then
# Download linuxdeploy and make it executable
wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
chmod +x linuxdeploy-x86_64.AppImage
# Run linuxdeploy to bundle the application
./linuxdeploy-x86_64.AppImage --appdir "${APP_DIR}" --output appimage
else
# Download linuxdeploy and make it executable (arm64)
wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage
chmod +x linuxdeploy-aarch64.AppImage
# Run linuxdeploy to bundle the application (arm64)
./linuxdeploy-aarch64.AppImage --appdir "${APP_DIR}" --output appimage
fi
# Rename the generated AppImage
mv "${APP_NAME}*.AppImage" "${APP_NAME}.AppImage"

13
build/linux/desktop Normal file
View File

@@ -0,0 +1,13 @@
[Desktop Entry]
Version=1.0
Name=Firefly Launcher
Comment=Custom game launcher built for convenience and quick access
# The Exec line includes %u to pass the URL to the application
Exec=/usr/local/bin/firefly-launcher %u
Terminal=false
Type=Application
Icon=firefly-launcher
Categories=Utility;
StartupWMClass=firefly-launcher

View File

@@ -3,10 +3,10 @@
#
# The lines below are called `modelines`. See `:help modeline`
name: "Firefly Launcher"
name: "firefly-launcher"
arch: ${GOARCH}
platform: "linux"
version: "0.1.0"
version: "1.0.0"
section: "default"
priority: "extra"
maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
@@ -17,16 +17,38 @@ license: "MIT"
release: "1"
contents:
- src: "./bin/Firefly Launcher"
dst: "/usr/local/bin/Firefly Launcher"
- src: "./bin/firefly-launcher"
dst: "/usr/local/bin/firefly-launcher"
- src: "./build/appicon.png"
dst: "/usr/share/icons/hicolor/128x128/apps/Firefly Launcher.png"
- src: "./build/linux/Firefly Launcher.desktop"
dst: "/usr/share/applications/Firefly Launcher.desktop"
dst: "/usr/share/icons/hicolor/128x128/apps/firefly-launcher.png"
- src: "./build/linux/firefly-launcher.desktop"
dst: "/usr/share/applications/firefly-launcher.desktop"
# Default dependencies for Debian 12/Ubuntu 22.04+ with WebKit 4.1
depends:
- libgtk-3-0
- libwebkit2gtk-4.1-0
# Distribution-specific overrides for different package formats and WebKit versions
overrides:
# RPM packages for RHEL/CentOS/AlmaLinux/Rocky Linux (WebKit 4.0)
rpm:
depends:
- gtk3
- libwebkit2gtk
- webkit2gtk4.1
# Arch Linux packages (WebKit 4.1)
archlinux:
depends:
- gtk3
- webkit2gtk-4.1
# scripts section to ensure desktop database is updated after install
scripts:
postinstall: "./build/linux/nfpm/scripts/postinstall.sh"
# You can also add preremove, postremove if needed
# preremove: "./build/linux/nfpm/scripts/preremove.sh"
# postremove: "./build/linux/nfpm/scripts/postremove.sh"
# replaces:
# - foobar
@@ -43,8 +65,3 @@ depends:
# - not-foo
# - not-bar
# changelog: "changelog.yaml"
# scripts:
# preinstall: ./build/linux/nfpm/scripts/preinstall.sh
# postinstall: ./build/linux/nfpm/scripts/postinstall.sh
# preremove: ./build/linux/nfpm/scripts/preremove.sh
# postremove: ./build/linux/nfpm/scripts/postremove.sh

View File

@@ -1 +1,21 @@
#!/bin/bash
#!/bin/sh
# Update desktop database for .desktop file changes
# This makes the application appear in application menus and registers its capabilities.
if command -v update-desktop-database >/dev/null 2>&1; then
echo "Updating desktop database..."
update-desktop-database -q /usr/share/applications
else
echo "Warning: update-desktop-database command not found. Desktop file may not be immediately recognized." >&2
fi
# Update MIME database for custom URL schemes (x-scheme-handler)
# This ensures the system knows how to handle your custom protocols.
if command -v update-mime-database >/dev/null 2>&1; then
echo "Updating MIME database..."
update-mime-database -n /usr/share/mime
else
echo "Warning: update-mime-database command not found. Custom URL schemes may not be immediately recognized." >&2
fi
exit 0

View File

@@ -10,7 +10,10 @@ tasks:
- task: common:go:mod:tidy
- task: common:build:frontend
vars:
PRODUCTION: '{{.PRODUCTION}}'
BUILD_FLAGS:
ref: .BUILD_FLAGS
PRODUCTION:
ref: .PRODUCTION
- task: common:generate:icons
cmds:
- task: generate:syso
@@ -28,9 +31,11 @@ tasks:
PRODUCTION: '{{.PRODUCTION | default "false"}}'
package:
summary: Packages a production build of the application into a `.exe` bundle
summary: Packages a production build of the application
vars:
FORMAT: '{{.FORMAT | default "nsis"}}'
cmds:
- task: create:nsis:installer
- task: '{{if eq .FORMAT "msix"}}create:msix:package{{else}}create:nsis:installer{{end}}'
generate:syso:
summary: Generates Windows `.syso` file
@@ -49,12 +54,42 @@ tasks:
PRODUCTION: "true"
cmds:
# Create the Microsoft WebView2 bootstrapper if it doesn't exist
- wails3 generate webview2bootstrapper -dir "{{.ROOT_DIR}}\build\windows\nsis"
- makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}\{{.BIN_DIR}}\{{.APP_NAME}}.exe" project.nsi
- wails3 generate webview2bootstrapper -dir "{{.ROOT_DIR}}/build/windows/nsis"
- cmd: >
powershell -NoProfile -Command "& makensis ('-DARG_WAILS_{{.ARG_FLAG}}_BINARY=' + (\"{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe\" -replace '/', '\\')) 'project.nsi'"
platforms: [windows]
vars:
ARCH: '{{.ARCH | default ARCH}}'
ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}'
create:msix:package:
summary: Creates an MSIX package
deps:
- task: build
vars:
PRODUCTION: "true"
cmds:
- |-
wails3 tool msix \
--config "{{.ROOT_DIR}}/wails.json" \
--name "{{.APP_NAME}}" \
--executable "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" \
--arch "{{.ARCH}}" \
--out "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}-{{.ARCH}}.msix" \
{{if .CERT_PATH}}--cert "{{.CERT_PATH}}"{{end}} \
{{if .PUBLISHER}}--publisher "{{.PUBLISHER}}"{{end}} \
{{if .USE_MSIX_TOOL}}--use-msix-tool{{else}}--use-makeappx{{end}}
vars:
ARCH: '{{.ARCH | default ARCH}}'
CERT_PATH: '{{.CERT_PATH | default ""}}'
PUBLISHER: '{{.PUBLISHER | default ""}}'
USE_MSIX_TOOL: '{{.USE_MSIX_TOOL | default "false"}}'
install:msix:tools:
summary: Installs tools required for MSIX packaging
cmds:
- wails3 tool msix-install-tools
run:
cmds:
- '{{.BIN_DIR}}\\{{.APP_NAME}}.exe'
- '{{.BIN_DIR}}/{{.APP_NAME}}.exe'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 592 KiB

View File

@@ -1,10 +1,10 @@
{
"fixed": {
"file_version": "0.1.0"
"file_version": "1.0.0"
},
"info": {
"0000": {
"ProductVersion": "0.1.0",
"ProductVersion": "1.0.0",
"CompanyName": "Firefly Shelter",
"FileDescription": "Custom game launcher built for convenience and quick access",
"LegalCopyright": "@ 2025, Firefly Shelter",

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10">
<Identity
Name="com.wails.firefly-launcher"
Publisher="CN=My Company"
Version="0.1.0.0"
ProcessorArchitecture="x64" />
<Properties>
<DisplayName>My Product</DisplayName>
<PublisherDisplayName>My Company</PublisherDisplayName>
<Description>My Product Description</Description>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Resources>
<Resource Language="en-us" />
</Resources>
<Applications>
<Application Id="com.wails.firefly-launcher" Executable="firefly-launcher.exe" EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements
DisplayName="My Product"
Description="My Product Description"
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
<uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements>
<Extensions>
<desktop:Extension Category="windows.fullTrustProcess" Executable="firefly-launcher.exe" />
</Extensions>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<MsixPackagingToolTemplate
xmlns="http://schemas.microsoft.com/msix/packaging/msixpackagingtool/template/2022">
<Settings
AllowTelemetry="false"
ApplyACLsToPackageFiles="true"
GenerateCommandLineFile="true"
AllowPromptForPassword="false">
</Settings>
<Installer
Path="firefly-launcher.exe"
Arguments=""
InstallLocation="C:\Program Files\My Company\My Product">
</Installer>
<PackageInformation
PackageName="My Product"
PackageDisplayName="My Product"
PublisherName="CN=My Company"
PublisherDisplayName="My Company"
Version="0.1.0.0"
PackageDescription="My Product Description">
<Capabilities>
<Capability Name="runFullTrust" />
</Capabilities>
<Applications>
<Application
Id="com.wails.firefly-launcher"
Description="My Product Description"
DisplayName="My Product"
ExecutableName="firefly-launcher.exe"
EntryPoint="Windows.FullTrustApplication">
</Application>
</Applications>
<Resources>
<Resource Language="en-us" />
</Resources>
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Properties>
<Framework>false</Framework>
<DisplayName>My Product</DisplayName>
<PublisherDisplayName>My Company</PublisherDisplayName>
<Description>My Product Description</Description>
<Logo>Assets\AppIcon.png</Logo>
</Properties>
</PackageInformation>
<SaveLocation PackagePath="firefly-launcher.msix" />
<PackageIntegrity>
<CertificatePath></CertificatePath>
</PackageIntegrity>
</MsixPackagingToolTemplate>

View File

@@ -20,10 +20,10 @@ Unicode true
## The following information is taken from the wails_tools.nsh file, but they can be overwritten here.
####
## !define INFO_PROJECTNAME "my-project" # Default "firefly-launcher"
## !define INFO_COMPANYNAME "My Company" # Default "FireflyShelter"
## !define INFO_PRODUCTNAME "My Product Name" # Default "Firefly Launcher"
## !define INFO_COMPANYNAME "My Company" # Default "My Company"
## !define INFO_PRODUCTNAME "My Product Name" # Default "My Product"
## !define INFO_PRODUCTVERSION "1.0.0" # Default "0.1.0"
## !define INFO_COPYRIGHT "(c) Now, My Company" # Default "@ 2025, FireflyShelter"
## !define INFO_COPYRIGHT "(c) Now, My Company" # Default "© now, My Company"
###
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
@@ -92,6 +92,7 @@ Section
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
!insertmacro wails.associateFiles
!insertmacro wails.associateCustomProtocols
!insertmacro wails.writeUninstaller
SectionEnd
@@ -107,6 +108,7 @@ Section "uninstall"
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
!insertmacro wails.unassociateFiles
!insertmacro wails.unassociateCustomProtocols
!insertmacro wails.deleteUninstaller
SectionEnd

View File

@@ -14,7 +14,7 @@
!define INFO_PRODUCTNAME "Firefly Launcher"
!endif
!ifndef INFO_PRODUCTVERSION
!define INFO_PRODUCTVERSION "0.1.0"
!define INFO_PRODUCTVERSION "1.0.0"
!endif
!ifndef INFO_COPYRIGHT
!define INFO_COPYRIGHT "@ 2025, Firefly Shelter"
@@ -158,7 +158,7 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
${If} ${REQUEST_EXECUTION_LEVEL} == "user"
# If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
${If} $0 != ""
Goto ok
${EndIf}
@@ -210,3 +210,27 @@ RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
; Delete app associations
!macroend
!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
!macroend
!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
!macroend
!macro wails.associateCustomProtocols
; Create custom protocols associations
!macroend
!macro wails.unassociateCustomProtocols
; Delete app custom protocol associations
!macroend

View File

@@ -1,13 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity type="win32" name="com.fireflyshelter.fireflylauncher" version="0.1.0" processorArchitecture="*"/>
<assemblyIdentity type="win32" name="com.fireflyshelter.fireflylauncher" version="1.0.0" processorArchitecture="*"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
</asmv3:windowsSettings>
</asmv3:application>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
@@ -15,11 +19,4 @@
</requestedPrivileges>
</security>
</trustInfo>
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness>
</asmv3:windowsSettings>
</asmv3:application>
</assembly>

View File

@@ -0,0 +1,22 @@
// @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, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
/**
* @param {number} timeout
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function CloseAppAfterTimeout(timeout) {
return $Call.ByID(1705931481, timeout);
}
/**
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function GetCurrentLauncherVersion() {
return $Call.ByID(3575133982);
}

View File

@@ -0,0 +1,8 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import * as AppService from "./appservice.js";
export {
AppService
};

View File

@@ -0,0 +1,65 @@
// @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, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
/**
* @param {string} patchPath
* @returns {$CancellablePromise<[boolean, string, string]>}
*/
export function CheckTypeHDiff(patchPath) {
return $Call.ByID(3717449114, patchPath);
}
/**
* @param {string} gamePath
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function CutData(gamePath) {
return $Call.ByID(2019290107, gamePath);
}
/**
* @param {string} gamePath
* @param {string} patchPath
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function DataExtract(gamePath, patchPath) {
return $Call.ByID(2161622254, gamePath, patchPath);
}
/**
* @param {string} gamePath
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function DeleteFiles(gamePath) {
return $Call.ByID(1103091613, gamePath);
}
/**
* @param {string} gamePath
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function HDiffPatchData(gamePath) {
return $Call.ByID(3944051994, gamePath);
}
/**
* @param {string} gamePath
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function LDiffPatchData(gamePath) {
return $Call.ByID(360123238, gamePath);
}
/**
* @param {string} gamePath
* @param {string} patchPath
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function VersionValidate(gamePath, patchPath) {
return $Call.ByID(2105077257, gamePath, patchPath);
}

View File

@@ -0,0 +1,8 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import * as DiffService from "./diffservice.js";
export {
DiffService
};

View File

@@ -0,0 +1,71 @@
// @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, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
/**
* @param {string} path
* @returns {$CancellablePromise<boolean>}
*/
export function DirExists(path) {
return $Call.ByID(1772289644, path);
}
/**
* @param {string} path
* @returns {$CancellablePromise<boolean>}
*/
export function FileExists(path) {
return $Call.ByID(1782610747, path);
}
/**
* @param {string} archivePath
* @param {string} fileInside
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function FileExistsInZip(archivePath, fileInside) {
return $Call.ByID(2509699047, archivePath, fileInside);
}
/**
* @param {string} path
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function OpenFolder(path) {
return $Call.ByID(1635714001, path);
}
/**
* @param {string} filter
* @returns {$CancellablePromise<string>}
*/
export function PickFile(filter) {
return $Call.ByID(3756474934, filter);
}
/**
* @returns {$CancellablePromise<string>}
*/
export function PickFolder() {
return $Call.ByID(3654471460);
}
/**
* @param {string} path
* @returns {$CancellablePromise<boolean>}
*/
export function StartApp(path) {
return $Call.ByID(1267568402, path);
}
/**
* @param {string} path
* @returns {$CancellablePromise<boolean>}
*/
export function StartWithConsole(path) {
return $Call.ByID(3249271428, path);
}

View File

@@ -0,0 +1,8 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import * as FSService from "./fsservice.js";
export {
FSService
};

View File

@@ -1,78 +0,0 @@
// @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 {string} path
* @returns {Promise<boolean> & { cancel(): void }}
*/
export function DirExists(path) {
let $resultPromise = /** @type {any} */($Call.ByID(1291974398, path));
return $resultPromise;
}
/**
* @param {string} path
* @returns {Promise<boolean> & { cancel(): void }}
*/
export function FileExists(path) {
let $resultPromise = /** @type {any} */($Call.ByID(3373618865, path));
return $resultPromise;
}
/**
* @param {string} archivePath
* @param {string} fileInside
* @returns {Promise<[boolean, string]> & { cancel(): void }}
*/
export function FileExistsInZip(archivePath, fileInside) {
let $resultPromise = /** @type {any} */($Call.ByID(1737012553, archivePath, fileInside));
return $resultPromise;
}
/**
* @param {string} path
* @returns {Promise<[boolean, string]> & { cancel(): void }}
*/
export function OpenFolder(path) {
let $resultPromise = /** @type {any} */($Call.ByID(2733319263, path));
return $resultPromise;
}
/**
* @returns {Promise<string> & { cancel(): void }}
*/
export function PickFile() {
let $resultPromise = /** @type {any} */($Call.ByID(2181012076));
return $resultPromise;
}
/**
* @returns {Promise<string> & { cancel(): void }}
*/
export function PickFolder() {
let $resultPromise = /** @type {any} */($Call.ByID(3906046322));
return $resultPromise;
}
/**
* @param {string} path
* @returns {Promise<boolean> & { cancel(): void }}
*/
export function StartApp(path) {
let $resultPromise = /** @type {any} */($Call.ByID(3825990132, path));
return $resultPromise;
}
/**
* @param {string} path
* @returns {Promise<boolean> & { cancel(): void }}
*/
export function StartWithConsole(path) {
let $resultPromise = /** @type {any} */($Call.ByID(2364569062, path));
return $resultPromise;
}

View File

@@ -0,0 +1,66 @@
// @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, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
/**
* @param {string} version
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function DownloadProxyProgress(version) {
return $Call.ByID(3559275334, version);
}
/**
* @param {string} version
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function DownloadServerProgress(version) {
return $Call.ByID(1954767259, version);
}
/**
* @returns {$CancellablePromise<[boolean, string, string]>}
*/
export function GetLatestLauncherVersion() {
return $Call.ByID(262637602);
}
/**
* @returns {$CancellablePromise<[boolean, string, string]>}
*/
export function GetLatestProxyVersion() {
return $Call.ByID(289488362);
}
/**
* @returns {$CancellablePromise<[boolean, string, string]>}
*/
export function GetLatestServerVersion() {
return $Call.ByID(2918980975);
}
/**
* @returns {$CancellablePromise<void>}
*/
export function UnzipProxy() {
return $Call.ByID(2563246729);
}
/**
* @returns {$CancellablePromise<void>}
*/
export function UnzipServer() {
return $Call.ByID(1126363284);
}
/**
* @param {string} version
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function UpdateLauncherProgress(version) {
return $Call.ByID(97272861, version);
}

View File

@@ -0,0 +1,8 @@
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import * as GitService from "./gitservice.js";
export {
GitService
};

View File

@@ -1,59 +0,0 @@
// @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 {string} version
* @returns {Promise<[boolean, string]> & { cancel(): void }}
*/
export function DownloadProxyProgress(version) {
let $resultPromise = /** @type {any} */($Call.ByID(1951249093, version));
return $resultPromise;
}
/**
* @param {string} version
* @returns {Promise<[boolean, string]> & { cancel(): void }}
*/
export function DownloadServerProgress(version) {
let $resultPromise = /** @type {any} */($Call.ByID(314135954, version));
return $resultPromise;
}
/**
* @param {string} oldVersion
* @returns {Promise<[boolean, string, string]> & { cancel(): void }}
*/
export function GetLatestProxyVersion(oldVersion) {
let $resultPromise = /** @type {any} */($Call.ByID(1462362449, oldVersion));
return $resultPromise;
}
/**
* @param {string} oldVersion
* @returns {Promise<[boolean, string, string]> & { cancel(): void }}
*/
export function GetLatestServerVersion(oldVersion) {
let $resultPromise = /** @type {any} */($Call.ByID(1447982978, oldVersion));
return $resultPromise;
}
/**
* @returns {Promise<void> & { cancel(): void }}
*/
export function UnzipProxy() {
let $resultPromise = /** @type {any} */($Call.ByID(4071181044));
return $resultPromise;
}
/**
* @returns {Promise<void> & { cancel(): void }}
*/
export function UnzipServer() {
let $resultPromise = /** @type {any} */($Call.ByID(4110296071));
return $resultPromise;
}

View File

@@ -1,54 +0,0 @@
// @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 {string} gamePath
* @returns {Promise<[boolean, string]> & { cancel(): void }}
*/
export function CutData(gamePath) {
let $resultPromise = /** @type {any} */($Call.ByID(3671642725, gamePath));
return $resultPromise;
}
/**
* @param {string} gamePath
* @param {string} patchPath
* @returns {Promise<[boolean, string]> & { cancel(): void }}
*/
export function DataExtract(gamePath, patchPath) {
let $resultPromise = /** @type {any} */($Call.ByID(1843136452, gamePath, patchPath));
return $resultPromise;
}
/**
* @param {string} gamePath
* @returns {Promise<[boolean, string]> & { cancel(): void }}
*/
export function DeleteFiles(gamePath) {
let $resultPromise = /** @type {any} */($Call.ByID(989019003, gamePath));
return $resultPromise;
}
/**
* @param {string} gamePath
* @returns {Promise<[boolean, string]> & { cancel(): void }}
*/
export function PatchData(gamePath) {
let $resultPromise = /** @type {any} */($Call.ByID(3608591627, gamePath));
return $resultPromise;
}
/**
* @param {string} gamePath
* @param {string} patchPath
* @returns {Promise<[boolean, string]> & { cancel(): void }}
*/
export function VersionValidate(gamePath, patchPath) {
let $resultPromise = /** @type {any} */($Call.ByID(3916254383, gamePath, patchPath));
return $resultPromise;
}

View File

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

View File

@@ -4,24 +4,22 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @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
* @returns {Promise<[string, string]> & { cancel(): void }}
* @returns {$CancellablePromise<[boolean, string, string, string]>}
*/
export function GetLanguage(path) {
let $resultPromise = /** @type {any} */($Call.ByID(3450750492, path));
return $resultPromise;
return $Call.ByID(3574191687, path);
}
/**
* @param {string} path
* @param {string} text
* @param {string} voice
* @returns {Promise<boolean> & { cancel(): void }}
* @returns {$CancellablePromise<[boolean, string]>}
*/
export function SetLanguage(path, text, voice) {
let $resultPromise = /** @type {any} */($Call.ByID(2793672496, path, text, voice));
return $resultPromise;
return $Call.ByID(2768939795, path, text, voice);
}

View File

@@ -1,13 +1,16 @@
<!DOCTYPE html>
<html data-theme="cupcake" lang="en">
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/wails.png" />
<link rel="icon" type="image/svg+xml" href="/appicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Wails + React + TS</title>
<title>Firefly Launcher</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -11,33 +11,33 @@
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.11",
"@tanstack/react-router": "^1.124.0",
"@tanstack/react-router-devtools": "^1.124.0",
"lucide-react": "^0.525.0",
"motion": "^12.23.0",
"@tailwindcss/vite": "^4.1.14",
"@tanstack/react-router": "^1.131.27",
"@tanstack/react-router-devtools": "^1.131.27",
"lucide-react": "^0.541.0",
"motion": "^12.23.12",
"path-browserify": "^1.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-toastify": "^11.0.5",
"tailwindcss": "^4.1.11",
"zustand": "^5.0.6"
"tailwindcss": "^4.1.14",
"zustand": "^5.0.8"
},
"devDependencies": {
"@tanstack/router-plugin": "^1.124.0",
"@types/node": "^24.0.10",
"@tanstack/router-plugin": "^1.131.27",
"@types/node": "^24.3.0",
"@types/path-browserify": "^1.0.3",
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"@vitejs/plugin-react": "^4.2.1",
"@types/react": "^19.1.11",
"@types/react-dom": "^19.1.7",
"@typescript-eslint/eslint-plugin": "^8.40.0",
"@typescript-eslint/parser": "^8.40.0",
"@vitejs/plugin-react": "^5.0.1",
"@wailsio/runtime": "^3.0.0-alpha.66",
"daisyui": "^5.0.43",
"eslint": "^8.55.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"typescript": "^5.2.2",
"vite": "^5.0.8"
"daisyui": "^5.1.27",
"eslint": "^9.34.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"typescript": "^5.9.2",
"vite": "^7.1.3"
}
}

BIN
frontend/public/appicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -0,0 +1,38 @@
import useLauncherStore from "@/stores/launcherStore";
import { AppService, } from "@bindings/firefly-launcher/internal/app-service";
import { toast } from "react-toastify";
import { sleep } from "./sleep";
import { GitService } from "@bindings/firefly-launcher/internal/git-service";
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,35 @@
import useLauncherStore from "@/stores/launcherStore";
import useSettingStore from "@/stores/settingStore";
import { FSService } from "@bindings/firefly-launcher/internal/fs-service";
import { GitService } from "@bindings/firefly-launcher/internal/git-service";
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,39 @@
import useLauncherStore from '@/stores/launcherStore';
import useSettingStore from '@/stores/settingStore';
import { FSService } from '@bindings/firefly-launcher/internal/fs-service';
import { GitService } from '@bindings/firefly-launcher/internal/git-service';
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

@@ -1,6 +1,7 @@
// useGlobalEvents.ts
import { useEffect } from "react";
import { Events } from "@wailsio/runtime";
import { toast } from "react-toastify";
export function useGlobalEvents({
setGameRunning,
@@ -11,6 +12,7 @@ export function useGlobalEvents({
setProgressDownload,
setDownloadSpeed,
setMessageUpdate,
setStageType,
}: {
setGameRunning: (v: boolean) => void;
@@ -19,8 +21,9 @@ export function useGlobalEvents({
setProgressUpdate: (v: number) => void;
setMaxProgressUpdate: (v: number) => void;
setProgressDownload: (v: number) => void;
setDownloadSpeed: (v: number) => void;
setDownloadSpeed: (v: string) => void;
setMessageUpdate: (v: string) => void;
setStageType: (v: string) => void,
}) {
useEffect(() => {
const onGameExit = () => setGameRunning(false);
@@ -30,7 +33,7 @@ export function useGlobalEvents({
const onDownload = (event: any) => {
const { percent, speed } = event.data[0];
setProgressDownload(Number(percent));
setDownloadSpeed(Number(speed));
setDownloadSpeed(speed);
};
const onUpdateProgress = (event: any) => {
@@ -44,13 +47,23 @@ export function useGlobalEvents({
setMessageUpdate(message);
};
const onStageUpdate = (event: any) => {
const { stage } = event.data[0];
setStageType(stage);
};
Events.On("download:server", onDownload);
Events.On("download:proxy", onDownload);
Events.On("game:exit", onGameExit);
Events.On("server:exit", onServerExit);
Events.On("proxy:exit", onProxyExit);
Events.On("hdiffz:progress", onUpdateProgress);
Events.On("hdiffz:message", onMessageUpdate);
Events.On("diff:progress", onUpdateProgress);
Events.On("diff:message", onMessageUpdate);
Events.On("diff:stage", onStageUpdate);
Events.On("diff:error", (event: any) => {
const { message } = event.data[0];
toast.error(message);
});
return () => {
Events.Off("download:server");
@@ -58,8 +71,10 @@ export function useGlobalEvents({
Events.Off("game:exit");
Events.Off("server:exit");
Events.Off("proxy:exit");
Events.Off("hdiffz:progress");
Events.Off("hdiffz:message");
Events.Off("diff:progress")
Events.Off("diff:message");
Events.Off("diff:stage");
Events.Off("version:check");
};
}, []);
}

View File

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

View File

@@ -3,7 +3,7 @@ import { Link } from "@tanstack/react-router";
export default function AboutPage() {
return (
<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>
<div className="space-y-4">
@@ -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.
</p>
<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 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.

View File

@@ -3,7 +3,7 @@ import { Link } from '@tanstack/react-router';
export default function AnalysisPage() {
return (
<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">
Firefly Analysis & Veritas Plugin
</h1>
@@ -78,12 +78,12 @@ export default function AnalysisPage() {
<span className="font-semibold text-blue-800">Backup Website</span>
</div>
<a
href="https://sr-analysis.vercel.app/"
href="https://firefly-sranalysis.vercel.app/"
className="link link-warning font-mono text-sm break-all"
target="_blank"
rel="noopener noreferrer"
>
https://sr-analysis.vercel.app/
https://firefly-sranalysis.vercel.app/
</a>
</div>
</div>
@@ -144,7 +144,7 @@ export default function AnalysisPage() {
<div className="space-y-2 text-purple-700">
<div className="flex items-start gap-2">
<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 className="flex items-start gap-2">
<span className="font-medium min-w-[20px] text-purple-600">2.</span>

View File

@@ -2,11 +2,12 @@ import useSettingStore from "@/stores/settingStore"
import { Check, Folder, File, X, Settings } from "lucide-react"
import { useEffect } from "react"
import { toast } from "react-toastify"
import { FSService, HdiffzService} from "@bindings/firefly-launcher/internal"
import { DiffService} from "@bindings/firefly-launcher/internal/diff-service"
import { FSService } from "@bindings/firefly-launcher/internal/fs-service"
import { motion } from "motion/react"
import useHdiffzStore from "@/stores/hdiffzStore"
import useDiffStore from "@/stores/diffStore"
export default function HdiffzPage() {
export default function DiffPage() {
const { gameDir, setGameDir } = useSettingStore()
const {
isLoading,
@@ -27,7 +28,7 @@ export default function HdiffzPage() {
setStageType,
messageUpdate,
setMessageUpdate
} = useHdiffzStore()
} = useDiffStore()
useEffect(() => {
const getLanguage = async () => {
@@ -78,7 +79,7 @@ export default function HdiffzPage() {
const handlePickDiffFile = async () => {
try {
setIsLoading({game: false, diff: true})
const basePath = await FSService.PickFile()
const basePath = await FSService.PickFile("")
if (basePath) {
if (!basePath.endsWith(".7z") && !basePath.endsWith(".zip") && !basePath.endsWith(".rar")) {
toast.error('Not valid file type')
@@ -86,13 +87,6 @@ export default function HdiffzPage() {
setDiffDir('')
return
}
const [exists, error] = await FSService.FileExistsInZip(basePath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes")
if (!exists) {
toast.error(error)
setDiffCheckResult('error')
setDiffDir('')
return
}
setDiffDir(basePath)
setDiffCheckResult('success')
} else {
@@ -109,66 +103,79 @@ export default function HdiffzPage() {
}
const handleUpdateGame = async () => {
const handleResult = (ok: boolean, error: string) => {
if (!ok) {
toast.error(error)
return false
}
return true
}
try {
setIsDiffLoading(true)
if (!gameDir || !diffDir) {
toast.error('Please select game directory and diff file')
setIsDiffLoading(false)
return
}
setStageType('Check Type HDiff')
setProgressUpdate(0)
setMaxProgressUpdate(1)
const [isOk, validType, errorType] = await DiffService.CheckTypeHDiff(diffDir)
if (!handleResult(isOk, errorType)) return
setProgressUpdate(1)
if (['hdiffmap.json', 'hdifffiles.txt', 'hdifffiles.json'].includes(validType)) {
setStageType('Version Validate')
setProgressUpdate(0)
setMaxProgressUpdate(1)
const [validVersion, errorVersion] = await HdiffzService.VersionValidate(gameDir, diffDir)
if (!validVersion) {
toast.error(errorVersion)
setIsDiffLoading(false)
return
}
const [validVersion, errorVersion] = await DiffService.VersionValidate(gameDir, diffDir)
if (!handleResult(validVersion, errorVersion)) return
setProgressUpdate(1)
}
setStageType('Data Extract')
const [validData, errorData] = await HdiffzService.DataExtract(gameDir, diffDir)
if (!validData) {
toast.error(errorData)
setIsDiffLoading(false)
return
}
const [validData, errorData] = await DiffService.DataExtract(gameDir, diffDir)
if (!handleResult(validData, errorData)) return
setStageType('Cut Data')
setMessageUpdate('')
const [validCut, errorCut] = await HdiffzService.CutData(gameDir)
if (!validCut) {
toast.error(errorCut)
setIsDiffLoading(false)
return
}
const [validCut, errorCut] = await DiffService.CutData(gameDir)
if (!handleResult(validCut, errorCut)) return
switch (validType) {
case 'hdifffiles.txt':
case 'hdiffmap.json':
case 'hdifffiles.json': {
setStageType('Patch Data')
const [validPatch, errorPatch] = await HdiffzService.PatchData(gameDir)
if (!validPatch) {
toast.error(errorPatch)
setIsDiffLoading(false)
return
}
const [validPatch, errorPatch] = await DiffService.HDiffPatchData(gameDir)
if (!handleResult(validPatch, errorPatch)) return
setStageType('Delete old files')
const [validDelete, errorDelete] = await HdiffzService.DeleteFiles(gameDir)
if (!validDelete) {
toast.error(errorDelete)
setIsDiffLoading(false)
return
const [validDelete, errorDelete] = await DiffService.DeleteFiles(gameDir)
if (!handleResult(validDelete, errorDelete)) return
break
}
case 'manifest': {
setStageType('Patch Data')
const [validPatch, errorPatch] = await DiffService.LDiffPatchData(gameDir)
if (!handleResult(validPatch, errorPatch)) return
break
}
}
toast.success('Update game completed')
} catch (err: any) {
console.error(err)
toast.error('PickFile error:', err)
setIsDiffLoading(false)
toast.error(`PickFile error: ${err}`)
} finally {
setIsDiffLoading(false)
}
}
return (
<div className="p-2 mx-4">
<div className="max-w-4xl mx-auto">

View File

@@ -1,30 +1,57 @@
import { Link } from "@tanstack/react-router";
export default function SrToolsPage() {
export default function FireflyToolsPage() {
return (
<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">
<h1 className="text-4xl font-bold text-primary text-center">SR Tools</h1>
<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">Firefly Tools</h1>
{/* Section 1: About SR Tools */}
<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">
<span></span>
<span>About SR Tools</span>
<span>About Firefly Tools</span>
</h2>
<div className="space-y-3 text-blue-700">
<div className="flex items-start gap-3">
<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>
This site is a another version of {" "}
<span className="font-semibold text-success">Firefly Tools {" "}</span>
developed by {" "}
<span className="font-semibold text-warning">Me {"(Kain)"}</span>
</p>
</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-info"
className="link link-warning font-mono text-sm break-all"
target="_blank"
rel="noopener noreferrer"
>
srtools.kain.id.vn
https://srtools.kain.id.vn/
</a>
</p>
</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="text-blue-600 text-lg">👨💻</div>
@@ -68,9 +95,24 @@ export default function SrToolsPage() {
</div>
<div className="flex items-start gap-3">
<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 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>
<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>

View File

@@ -3,7 +3,7 @@ import { Link } from '@tanstack/react-router';
export default function HowToPage() {
return (
<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>
{/* Section 1: Launcher Features */}
@@ -28,13 +28,23 @@ export default function HowToPage() {
</p>
</div>
<div className="flex items-start gap-3">
<div className="text-green-600 text-lg">📦</div>
<p>Patch and update game files using{" "}
<a href="/hdiff" className="link link-info font-mono">Hdiffz Tool</a>
{" "}(HDiffPatch) fast & lightweight incremental updates.
<div className="text-green-600 text-2xl">📦</div>
<div>
<p className="text-green-800 font-semibold">
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>
</div>
</div>
</div>
</div>
{/* Section 2: FireflyGo Commands */}
@@ -65,8 +75,60 @@ export default function HowToPage() {
</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 */}
<div className="space-y-4">
<div className="space-y-4 mt-4">
<h3 className="text-lg font-semibold text-blue-800">Available Commands:</h3>
{/* Theorycraft Toggle */}
@@ -103,16 +165,28 @@ export default function HowToPage() {
<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">HP Override <span className="text-red-600 text-sm">(Theorycraft only)</span></h4>
<div className="space-y-1 text-blue-700">
<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>
<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>
<p className="text-sm"><code className="bg-blue-100 px-1 py-0.5 rounded text-sm">/hp 0</code> disables HP override</p>
<h4 className="font-semibold text-blue-800 mb-1">
HP Override <span className="text-red-600 text-sm">(Theorycraft only)</span>
</h4>
<div className="space-y-2 text-blue-700 text-sm">
<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>
{/* Log Command */}
<div className="bg-white border border-blue-200 rounded-lg p-4">
<div className="flex items-start gap-3">

View File

@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'
import { Folder, Settings, Check, X, Globe, Mic } from 'lucide-react'
import { FSService } from '@bindings/firefly-launcher/internal'
import { LanguageService } from '@bindings/firefly-launcher/internal'
import { FSService } from '@bindings/firefly-launcher/internal/fs-service'
import { LanguageService } from '@bindings/firefly-launcher/internal/language-service'
import { toast } from 'react-toastify'
import useSettingStore from '@/stores/settingStore'
@@ -27,28 +27,42 @@ export default function LanguagePage() {
useEffect(() => {
const getLanguage = async () => {
if (gameDir) {
const subPath = 'StarRail_Data/StreamingAssets/DesignData/Windows'
if (!gameDir) return
const subPath = "StarRail_Data/StreamingAssets"
const fullPath = `${gameDir}/${subPath}`
const exists = await FSService.DirExists(fullPath)
if (exists) {
const [textLang, voiceLang] = await LanguageService.GetLanguage(fullPath)
if (!exists) {
setTextLang("")
setVoiceLang("")
setSelectedTextLang("")
setSelectedVoiceLang("")
setFolderCheckResult("error")
setGameDir("")
return
}
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')
setFolderCheckResult("success")
setSelectedTextLang(textLang)
setSelectedVoiceLang(voiceLang)
} else {
setTextLang('')
setVoiceLang('')
setSelectedTextLang('')
setSelectedVoiceLang('')
setFolderCheckResult('error')
setGameDir('')
}
}
}
getLanguage()
}, [gameDir])
@@ -86,18 +100,18 @@ export default function LanguagePage() {
}
try {
setIsSettingLanguage(true)
const result = await LanguageService.SetLanguage(
const [ok, err] = await LanguageService.SetLanguage(
`${gameDir}/StarRail_Data/StreamingAssets/DesignData/Windows`,
selectedTextLang,
selectedVoiceLang
)
if (result) {
if (ok) {
toast.success('Language set successfully')
setTextLang(selectedTextLang)
setVoiceLang(selectedVoiceLang)
}
else {
toast.error('Language set failed')
toast.error(err)
}
} catch (err: any) {

View File

@@ -1,6 +1,7 @@
import { useEffect } from 'react';
import { Play, Menu, FolderOpen, MessageCircleQuestionMark } from 'lucide-react';
import { FSService, GitService } from '@bindings/firefly-launcher/internal';
import { AppService } from '@bindings/firefly-launcher/internal/app-service';
import { FSService } from '@bindings/firefly-launcher/internal/fs-service';
import { toast } from 'react-toastify';
import path from 'path-browserify'
import useSettingStore from '@/stores/settingStore';
@@ -8,6 +9,7 @@ import useModalStore from '@/stores/modalStore';
import useLauncherStore from '@/stores/launcherStore';
import { motion } from 'motion/react';
import { Link } from '@tanstack/react-router';
import { CheckUpdateLauncher, CheckUpdateProxy, CheckUpdateServer, sleep, UpdateLauncher, UpdateProxy, UpdateServer } from '@/helper';
export default function LauncherPage() {
const { gamePath,
@@ -18,15 +20,17 @@ export default function LauncherPage() {
gameDir,
serverVersion,
proxyVersion,
setServerVersion,
setProxyVersion,
setServerPath,
setProxyPath
} = useSettingStore()
const { isOpenNotification, setIsOpenNotification } = useModalStore()
const {
isOpenDownloadDataModal,
isOpenUpdateDataModal,
isOpenSelfUpdateModal,
setIsOpenDownloadDataModal,
setIsOpenUpdateDataModal,
setIsOpenSelfUpdateModal
} = useModalStore()
const {
isLoading,
modelName,
downloadType,
serverReady,
proxyReady,
@@ -36,8 +40,10 @@ export default function LauncherPage() {
gameRunning,
progressDownload,
downloadSpeed,
updateData,
launcherVersion,
setLauncherVersion,
setIsLoading,
setModelName,
setDownloadType,
setServerReady,
setProxyReady,
@@ -45,12 +51,12 @@ export default function LauncherPage() {
setServerRunning,
setProxyRunning,
setGameRunning,
setUpdateData,
} = useLauncherStore()
useEffect(() => {
const check = async () => {
if (!serverPath || !proxyPath || !serverVersion || !proxyVersion) {
if (!serverVersion || !proxyVersion) {
setServerReady(false)
setProxyReady(false)
return
@@ -67,30 +73,47 @@ export default function LauncherPage() {
useEffect(() => {
const checkStartUp = async (): Promise<void> => {
let isExists = false
if (serverPath && proxyPath && serverVersion && proxyVersion) {
isExists = true
setServerReady(true)
setProxyReady(true)
const [_, version] = await AppService.GetCurrentLauncherVersion()
setLauncherVersion(version)
const launcherData = await CheckUpdateLauncher()
if (launcherData.isUpdate) {
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)
if (!exitGame) {
setGameRunning(false)
setGamePath("")
setGameDir("")
}
if (!dataUpdate.server.isUpdate && !dataUpdate.proxy.isUpdate) {
setServerReady(true)
setProxyReady(true)
return
}
if (!isExists) {
if (!serverData.isExists || !proxyData.isExists) {
setServerReady(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()
}, []);
@@ -98,7 +121,7 @@ export default function LauncherPage() {
const handlePickFile = async () => {
try {
setIsLoading(true)
const basePath = await FSService.PickFile()
const basePath = await FSService.PickFile("exe")
if (basePath.endsWith("StarRail.exe") || basePath.endsWith("launcher.exe")) {
const normalized = basePath.replace(/\\/g, '/')
const folderPath = path.dirname(normalized)
@@ -121,9 +144,7 @@ export default function LauncherPage() {
}
}
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const handleStartGame = async () => {
if (!gamePath) {
@@ -174,84 +195,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)
for (const [key, value] of Object.entries(updateInfo)) {
if (value.isUpdate) {
if (key === "server") {
setDownloadType("Downloading server...")
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")
if (updateData.launcher.isUpdate) {
await UpdateLauncher(updateData.launcher.version)
setUpdateData({...updateData, launcher: { isUpdate: false, isExists: true, version: updateData.launcher.version }})
setIsOpenSelfUpdateModal(true)
}
} 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("")
setIsDownloading(false)
setServerReady(true)
setProxyReady(true)
}
// Handle ESC key to close modal
useEffect(() => {
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setIsOpenNotification(false);
setIsOpenDownloadDataModal(false);
setIsOpenUpdateDataModal(false);
setIsOpenSelfUpdateModal(false);
}
};
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenNotification]);
}, [isOpenDownloadDataModal, isOpenUpdateDataModal, isOpenSelfUpdateModal]);
return (
@@ -280,11 +259,10 @@ export default function LauncherPage() {
href="https://sranalysis.kain.id.vn/"
>
<img
src="https://icons.duckduckgo.com/ip3/sranalysis.kain.id.vn.ico"
src="https://sranalysis.kain.id.vn/ff-sranalysis.png"
alt="SRAnalysis Logo"
className="w-8 h-8 rounded-full"
/>
</a>
</div>
@@ -295,7 +273,7 @@ export default function LauncherPage() {
href="https://srtools.kain.id.vn/"
>
<img
src="https://icons.duckduckgo.com/ip3/srtools.kain.id.vn.ico"
src="https://srtools.kain.id.vn/ff-srtool.png"
alt="SRTools Logo"
className="w-8 h-8 rounded-full"
/>
@@ -369,13 +347,27 @@ export default function LauncherPage() {
<li><button onClick={handlePickFile}>Change Game Path</button></li>
<li><button
onClick={async () => {
const updateInfo = await handlerCheckUpdate()
if (updateInfo.server.isUpdate || updateInfo.proxy.isUpdate) {
setModelName("Update Data")
setIsOpenNotification(true)
} else {
toast.success("No updates available")
const serverData = await CheckUpdateServer(serverPath, serverVersion)
const proxyData = await CheckUpdateProxy(proxyPath, proxyVersion)
const launcherData = await CheckUpdateLauncher()
setUpdateData({
server: serverData,
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
</button></li>
@@ -403,12 +395,17 @@ export default function LauncherPage() {
{/* Downloading */}
{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="space-y-3">
<div className="flex justify-center items-center text-sm text-white/80">
<span>{downloadType}</span>
<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>
</div>
</div>
@@ -417,7 +414,7 @@ export default function LauncherPage() {
className="h-full bg-gradient-to-r from-cyan-400 to-blue-500 rounded-full"
initial={{ width: 0 }}
animate={{ width: `${progressDownload}%` }}
transition={{ duration: 0.3 }}
transition={{ type: "tween", ease: "linear", duration: 0.03 }}
/>
</div>
<div className="text-center text-xs text-white/60">
@@ -426,64 +423,185 @@ export default function LauncherPage() {
</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 */}
{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">
<p className="text-primary">Version server: {serverVersion}</p>
<p className="mt-2 text-secondary">Version proxy: {proxyVersion}</p>
<p className="mt-2 text-success">Version launcher: {launcherVersion}</p>
</div>
)}
{/* Modal */}
{isOpenNotification && (
{isOpenUpdateDataModal && (
<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">
{modelName !== "Download Data" && (
<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={() => setIsOpenNotification(false)}
onClick={() => setIsOpenUpdateDataModal(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">
{modelName}
Update Data
</h3>
</div>
<div className="px-6 pb-6">
<div className="mb-6">
<p className="text-warning text-lg">
{modelName === "Download Data"
? "Download required data!"
: "Do you want to update data?"}
Do you want to update data server and proxy?
</p>
</div>
<div className="flex justify-end gap-3">
{modelName !== "Download Data" && (
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="btn btn-outline btn-error"
onClick={() => setIsOpenNotification(false)}
onClick={() => setIsOpenUpdateDataModal(false)}
>
Cancel
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 () => {
setIsOpenNotification(false)
const dataCheck = await handlerCheckUpdate()
await handlerUpdateServer(dataCheck)
setIsOpenUpdateDataModal(false)
await handlerUpdateData()
}}
>
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

View File

@@ -12,7 +12,7 @@ import { Route as rootRouteImport } from './routes/__root'
import { Route as SrtoolsRouteImport } from './routes/srtools'
import { Route as LanguageRouteImport } from './routes/language'
import { Route as HowtoRouteImport } from './routes/howto'
import { Route as HdiffzRouteImport } from './routes/hdiffz'
import { Route as DiffRouteImport } from './routes/diff'
import { Route as AnalysisRouteImport } from './routes/analysis'
import { Route as AboutRouteImport } from './routes/about'
import { Route as IndexRouteImport } from './routes/index'
@@ -32,9 +32,9 @@ const HowtoRoute = HowtoRouteImport.update({
path: '/howto',
getParentRoute: () => rootRouteImport,
} as any)
const HdiffzRoute = HdiffzRouteImport.update({
id: '/hdiffz',
path: '/hdiffz',
const DiffRoute = DiffRouteImport.update({
id: '/diff',
path: '/diff',
getParentRoute: () => rootRouteImport,
} as any)
const AnalysisRoute = AnalysisRouteImport.update({
@@ -57,7 +57,7 @@ export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/about': typeof AboutRoute
'/analysis': typeof AnalysisRoute
'/hdiffz': typeof HdiffzRoute
'/diff': typeof DiffRoute
'/howto': typeof HowtoRoute
'/language': typeof LanguageRoute
'/srtools': typeof SrtoolsRoute
@@ -66,7 +66,7 @@ export interface FileRoutesByTo {
'/': typeof IndexRoute
'/about': typeof AboutRoute
'/analysis': typeof AnalysisRoute
'/hdiffz': typeof HdiffzRoute
'/diff': typeof DiffRoute
'/howto': typeof HowtoRoute
'/language': typeof LanguageRoute
'/srtools': typeof SrtoolsRoute
@@ -76,7 +76,7 @@ export interface FileRoutesById {
'/': typeof IndexRoute
'/about': typeof AboutRoute
'/analysis': typeof AnalysisRoute
'/hdiffz': typeof HdiffzRoute
'/diff': typeof DiffRoute
'/howto': typeof HowtoRoute
'/language': typeof LanguageRoute
'/srtools': typeof SrtoolsRoute
@@ -87,7 +87,7 @@ export interface FileRouteTypes {
| '/'
| '/about'
| '/analysis'
| '/hdiffz'
| '/diff'
| '/howto'
| '/language'
| '/srtools'
@@ -96,7 +96,7 @@ export interface FileRouteTypes {
| '/'
| '/about'
| '/analysis'
| '/hdiffz'
| '/diff'
| '/howto'
| '/language'
| '/srtools'
@@ -105,7 +105,7 @@ export interface FileRouteTypes {
| '/'
| '/about'
| '/analysis'
| '/hdiffz'
| '/diff'
| '/howto'
| '/language'
| '/srtools'
@@ -115,7 +115,7 @@ export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AboutRoute: typeof AboutRoute
AnalysisRoute: typeof AnalysisRoute
HdiffzRoute: typeof HdiffzRoute
DiffRoute: typeof DiffRoute
HowtoRoute: typeof HowtoRoute
LanguageRoute: typeof LanguageRoute
SrtoolsRoute: typeof SrtoolsRoute
@@ -144,11 +144,11 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof HowtoRouteImport
parentRoute: typeof rootRouteImport
}
'/hdiffz': {
id: '/hdiffz'
path: '/hdiffz'
fullPath: '/hdiffz'
preLoaderRoute: typeof HdiffzRouteImport
'/diff': {
id: '/diff'
path: '/diff'
fullPath: '/diff'
preLoaderRoute: typeof DiffRouteImport
parentRoute: typeof rootRouteImport
}
'/analysis': {
@@ -179,7 +179,7 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AboutRoute: AboutRoute,
AnalysisRoute: AnalysisRoute,
HdiffzRoute: HdiffzRoute,
DiffRoute: DiffRoute,
HowtoRoute: HowtoRoute,
LanguageRoute: LanguageRoute,
SrtoolsRoute: SrtoolsRoute,

View File

@@ -3,7 +3,7 @@ import ThemeController from '../components/themeController'
import { ToastContainer } from 'react-toastify'
import { useGlobalEvents } from '@/hooks';
import useLauncherStore from '@/stores/launcherStore';
import useHdiffzStore from '@/stores/hdiffzStore';
import useDiffStore from '@/stores/diffStore';
export const Route = createRootRoute({
component: RootLayout
@@ -11,7 +11,7 @@ export const Route = createRootRoute({
function RootLayout() {
const { setGameRunning, setServerRunning, setProxyRunning, setProgressDownload, setDownloadSpeed } = useLauncherStore()
const { setProgressUpdate, setMaxProgressUpdate, setMessageUpdate } = useHdiffzStore()
const { setProgressUpdate, setMaxProgressUpdate, setMessageUpdate, setStageType } = useDiffStore()
useGlobalEvents({
setGameRunning,
setServerRunning,
@@ -21,6 +21,7 @@ function RootLayout() {
setProgressDownload,
setDownloadSpeed,
setMessageUpdate,
setStageType
});
return (
@@ -40,7 +41,7 @@ function RootLayout() {
<a>Tools</a>
<ul className="p-2">
<li><Link to="/language">Language</Link></li>
<li><Link to="/hdiffz">Hdiffz</Link></li>
<li><Link to="/diff">Diff</Link></li>
</ul>
</li>
<li>
@@ -55,24 +56,29 @@ function RootLayout() {
</ul>
</div>
<Link to="/" className="grid grid-cols-1 items-start text-left gap-0 hover:scale-105 px-2">
<h1 className="text-lg font-bold">
<span className="text-emerald-500">Firefly Lauc</span>
<div className="flex items-center justify-center">
<img src="/appicon.png" alt="Logo" className='w-13 h-13 rounded-lg mx-2' />
<div className="flex flex-col justify-center items-start">
<h1 className="text-xl font-bold">
<span className="text-emerald-500">Firefly </span>
<span className="bg-clip-text text-transparent bg-gradient-to-r from-emerald-400 via-orange-500 to-red-500">
her
Launcher
</span>
</h1>
<p className="text-sm text-gray-500">By Kain</p>
</div>
</div>
</Link>
</div>
<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>
<details>
<summary>Tools</summary>
<ul className="p-2">
<li><Link to="/language">Language</Link></li>
<li><Link to="/hdiffz">Hdiffz</Link></li>
<li><Link to="/diff">Diff</Link></li>
</ul>
</details>
</li>
@@ -81,7 +87,7 @@ function RootLayout() {
<summary>Plugins</summary>
<ul className="p-2">
<li><Link to="/analysis">Analysis (Veritas)</Link></li>
<li><Link to="/srtools">SrTools</Link></li>
<li><Link to="/srtools">Firefly Tools</Link></li>
</ul>
</details>
</li>

View File

@@ -0,0 +1,7 @@
import DiffPage from '@/pages/diff'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/diff')({
component: DiffPage,
})

View File

@@ -1,7 +0,0 @@
import HdiffzPage from '@/pages/hdiffz'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/hdiffz')({
component: HdiffzPage,
})

View File

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

View File

@@ -1,7 +1,7 @@
import { create } from 'zustand'
interface LauncherState {
interface DiffState {
folderCheckResult: 'success' | 'error' | null,
isLoading: {game: boolean, diff: boolean},
diffDir: string,
@@ -22,7 +22,7 @@ interface LauncherState {
setStageType: (value: string) => void,
}
const useLauncherStore = create<LauncherState>((set, get) => ({
const useDiffStore = create<DiffState>((set, get) => ({
isLoading: {game: false, diff: false},
folderCheckResult: null,
diffDir: "",
@@ -43,4 +43,4 @@ const useLauncherStore = create<LauncherState>((set, get) => ({
setStageType: (value: string) => set({ stageType: value }),
}));
export default useLauncherStore;
export default useDiffStore;

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
@import "tailwindcss";
@plugin "daisyui";
@plugin "daisyui"{
themes: night --default, night --prefersdark, cupcake;
}

View File

@@ -8,6 +8,7 @@ export default defineConfig({
base: '/',
build: {
outDir: 'dist',
chunkSizeWarningLimit: 2000,
},
plugins: [
tanstackRouter({

59
go.mod
View File

@@ -1,54 +1,55 @@
module firefly-launcher
go 1.22.4
toolchain go1.24.4
go 1.25
require (
github.com/wailsapp/wails/v3 v3.0.0-alpha.9
golang.org/x/sys v0.28.0
github.com/klauspost/compress v1.18.0
github.com/minio/selfupdate v0.6.0
github.com/wailsapp/wails/v3 v3.0.0-alpha.34
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac
golang.org/x/sys v0.31.0
google.golang.org/protobuf v1.33.0
)
require (
aead.dev/minisign v0.2.0 // indirect
dario.cat/mergo v1.0.1 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v1.0.0 // indirect
github.com/adrg/xdg v0.5.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/adrg/xdg v0.5.3 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/cloudflare/circl v1.3.8 // indirect
github.com/cyphar/filepath-securejoin v0.2.5 // indirect
github.com/ebitengine/purego v0.4.0-alpha.4 // indirect
github.com/cloudflare/circl v1.6.0 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.0 // indirect
github.com/go-git/go-git/v5 v5.12.0 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-git/go-git/v5 v5.13.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/uuid v1.4.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
github.com/leaanthony/u v1.1.0 // indirect
github.com/lmittmann/tint v1.0.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/leaanthony/u v1.1.1 // indirect
github.com/lmittmann/tint v1.0.7 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/samber/lo v1.38.1 // indirect
github.com/samber/lo v1.49.1 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.2.2 // indirect
github.com/wailsapp/go-webview2 v1.0.19 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/wailsapp/go-webview2 v1.0.21 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/mod v0.19.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sync v0.9.0 // indirect
golang.org/x/tools v0.23.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/text v0.23.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)

180
go.sum
View File

@@ -1,59 +1,61 @@
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/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/adrg/xdg v0.5.0 h1:dDaZvhMXatArP1NPHhnfaQUqWBLBsmx1h1HXQdMoFCY=
github.com/adrg/xdg v0.5.0/go.mod h1:dDdY4M4DF9Rjy4kHPeNL+ilVF+p2lK8IdM9/rTSGcI4=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.3.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqwI=
github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo=
github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ebitengine/purego v0.4.0-alpha.4 h1:Y7yIV06Yo5M2BAdD7EVPhfp6LZ0tEcQo5770OhYUVes=
github.com/ebitengine/purego v0.4.0-alpha.4/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM=
github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8=
github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0=
github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -63,23 +65,25 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI=
github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/lmittmann/tint v1.0.4 h1:LeYihpJ9hyGvE0w+K2okPTGUdVLfng1+nDNVR4vWISc=
github.com/lmittmann/tint v1.0.4/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/lmittmann/tint v1.0.7 h1:D/0OqWZ0YOGZ6AyC+5Y2kD8PBEzBk6rFHVSfOqCkF9Y=
github.com/lmittmann/tint v1.0.7/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
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/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/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -87,99 +91,65 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU=
github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/wailsapp/go-webview2 v1.0.21 h1:k3dtoZU4KCoN/AEIbWiPln3P2661GtA2oEgA2Pb+maA=
github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v3 v3.0.0-alpha.9 h1:b8CfRrhPno8Fra0xFp4Ifyj+ogmXBc35rsQWvcrHtsI=
github.com/wailsapp/wails/v3 v3.0.0-alpha.9/go.mod h1:dSv6s722nSWaUyUiapAM1DHc5HKggNGY1a79shO85/g=
github.com/wailsapp/wails/v3 v3.0.0-alpha.34 h1:t6NwHOLJzXuESb3YSXarSd1C/U1h2CpXF+BLr0ekj2g=
github.com/wailsapp/wails/v3 v3.0.0-alpha.34/go.mod h1:UZpnhYuju4saspCJrIHAvC0H5XjtKnqd26FRxJLrQ0M=
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/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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
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.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.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
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/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
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/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/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-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-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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=

View File

@@ -0,0 +1,22 @@
package appService
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

@@ -0,0 +1,259 @@
package diffService
import (
"bufio"
"encoding/json"
"firefly-launcher/pkg/constant"
"firefly-launcher/pkg/hpatchz"
"firefly-launcher/pkg/models"
"firefly-launcher/pkg/sevenzip"
"firefly-launcher/pkg/verifier"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/wailsapp/wails/v3/pkg/application"
)
type DiffService struct{}
func (h *DiffService) CheckTypeHDiff(patchPath string) (bool, string, string) {
if ok, err := sevenzip.IsFileIn7z(patchPath, "hdifffiles.txt"); err == nil && ok {
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 {
return true, "hdiffmap.json", ""
}
if ok, err := sevenzip.IsFileIn7z(patchPath, "manifest"); err == nil && ok {
return true, "manifest", ""
}
return false, "", "not found hdifffiles.txt or hdiffmap.json"
}
func (h *DiffService) VersionValidate(gamePath, patchPath string) (bool, string) {
oldBinPath := filepath.Join(gamePath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes")
if _, err := os.Stat(oldBinPath); err != nil {
return false, err.Error()
}
if _, err := os.Stat(patchPath); err != nil {
return false, err.Error()
}
if _, err := os.Stat(constant.TempUrl); os.IsNotExist(err) {
if err := os.MkdirAll(constant.TempUrl, os.ModePerm); err != nil {
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 {
return false, err.Error()
}
tempBinFile = filepath.Join(constant.TempUrl, "BinaryVersion.bytes")
} else {
if err := sevenzip.ExtractAFileFromZip(patchPath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes.hdiff", constant.TempUrl); err != nil {
return false, err.Error()
}
patchBinFile := filepath.Join(constant.TempUrl, "BinaryVersion.bytes.hdiff")
sourceBinFile := oldBinPath
tempBinFile = filepath.Join(constant.TempUrl, "BinaryVersion.bytes")
if err := hpatchz.ApplyPatch(sourceBinFile, patchBinFile, tempBinFile); err != nil {
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"
}
func (h *DiffService) HDiffPatchData(gamePath string) (bool, string) {
hdiffMapPath := filepath.Join(gamePath, "hdiffmap.json")
hdiffFilesPath := filepath.Join(gamePath, "hdifffiles.txt")
hdifffilesJsonPath := filepath.Join(gamePath, "hdifffiles.json")
var jsonData struct {
DiffMap []*models.HDiffData `json:"diff_map"`
}
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(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 {
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"
}
application.Get().Event.Emit("diff:stage", map[string]string{"stage": "Patching HDiff"})
for i, entry := range jsonData.DiffMap {
application.Get().Event.Emit(
"diff:progress", map[string]int{
"progress": i,
"maxProgress": len(jsonData.DiffMap),
})
sourceFile := filepath.Join(gamePath, entry.SourceFileName)
patchFile := filepath.Join(gamePath, entry.PatchFileName)
targetFile := filepath.Join(gamePath, entry.TargetFileName)
if _, err := os.Stat(patchFile); os.IsNotExist(err) {
continue
}
if entry.SourceFileName == "" {
hpatchz.ApplyPatchEmpty(patchFile, targetFile)
os.Remove(patchFile)
continue
}
if _, err := os.Stat(sourceFile); os.IsNotExist(err) {
continue
}
hpatchz.ApplyPatch(sourceFile, patchFile, targetFile)
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"))
os.Remove(filepath.Join(gamePath, "hdifffiles.json"))
return true, "patching completed"
}
func (h *DiffService) DeleteFiles(gamePath string) (bool, string) {
var deleteFiles []string
file, err := os.Open(filepath.Join(gamePath, "deletefiles.txt"))
if err != nil {
return false, ""
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line != "" {
deleteFiles = append(deleteFiles, line)
}
}
if err := scanner.Err(); err != nil {
file.Close()
return false, "no delete files exist"
}
file.Close()
for i, file := range deleteFiles {
os.Remove(filepath.Join(gamePath, file))
application.Get().Event.Emit("diff:progress", map[string]int{"progress": i, "maxProgress": len(deleteFiles)})
}
os.Remove(filepath.Join(gamePath, "deletefiles.txt"))
return true, ""
}

View File

@@ -0,0 +1,172 @@
package diffService
import (
"firefly-launcher/pkg/firefly"
"firefly-launcher/pkg/firefly/pb"
"firefly-launcher/pkg/hpatchz"
"firefly-launcher/pkg/models"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"github.com/wailsapp/wails/v3/pkg/application"
)
func (h *DiffService) LDiffPatchData(gamePath string) (bool, string) {
entries, err := os.ReadDir(gamePath)
if err != nil {
return false, err.Error()
}
ldiffPath := filepath.Join(gamePath, "ldiff")
for _, entry := range entries {
if entry.IsDir() {
continue
}
if !entry.Type().IsRegular() {
continue
}
name := entry.Name()
if strings.HasPrefix(name, "manifest") {
manifestName := entry.Name()
manifestPath := filepath.Join(gamePath, manifestName)
manifest, err := firefly.LoadManifestProto(manifestPath)
if err != nil {
continue
}
ldiffEntries, err := os.ReadDir(ldiffPath)
if err != nil {
return false, err.Error()
}
application.Get().Event.Emit("diff:stage", map[string]string{"stage": "Processing LDiff"})
for i, ldiffEntry := range ldiffEntries {
assetName := ldiffEntry.Name()
var matchingAssets []struct {
AssetName string
AssetSize int64
Asset *pb.AssetManifest
}
application.Get().Event.Emit(
"diff:progress", map[string]int{
"progress": i,
"maxProgress": len(ldiffEntries),
})
var wg sync.WaitGroup
var mu sync.Mutex
for _, assetGroup := range manifest.Assets {
assetGroup := assetGroup
wg.Go(func() {
if data := assetGroup.AssetData; data != nil {
for _, asset := range data.Assets {
if asset.ChunkFileName == assetName {
mu.Lock()
matchingAssets = append(matchingAssets, struct {
AssetName string
AssetSize int64
Asset *pb.AssetManifest
}{assetGroup.AssetName, assetGroup.AssetSize, asset})
mu.Unlock()
}
}
}
})
}
wg.Wait()
for _, ma := range matchingAssets {
err := firefly.LDiffFile(ma.Asset, ma.AssetName, ma.AssetSize, ldiffPath, gamePath)
if err != nil {
continue
}
}
}
diffMapNames := make([]string, len(ldiffEntries))
for i, e := range ldiffEntries {
diffMapNames[i] = e.Name()
}
diffMapList, err := MakeDiffMap(manifest, diffMapNames)
if err != nil {
return false, err.Error()
}
application.Get().Event.Emit("diff:stage", map[string]string{"stage": "Patching HDiff"})
for i, entry := range diffMapList {
application.Get().Event.Emit(
"diff:progress", map[string]int{
"progress": i,
"maxProgress": len(diffMapList),
})
sourceFile := filepath.Join(gamePath, entry.SourceFileName)
patchFile := filepath.Join(gamePath, entry.PatchFileName)
targetFile := filepath.Join(gamePath, entry.TargetFileName)
if _, err := os.Stat(patchFile); os.IsNotExist(err) {
continue
}
if entry.SourceFileName == "" {
hpatchz.ApplyPatchEmpty(patchFile, targetFile)
os.Remove(patchFile)
continue
}
if _, err := os.Stat(sourceFile); os.IsNotExist(err) {
continue
}
hpatchz.ApplyPatch(sourceFile, patchFile, targetFile)
if entry.SourceFileName != entry.TargetFileName {
os.Remove(sourceFile)
}
os.Remove(patchFile)
}
}
}
os.RemoveAll(ldiffPath)
return true, "patching completed"
}
func MakeDiffMap(manifest *pb.ManifestProto, chunkNames []string) ([]*models.HDiffData, error) {
var hdiffFiles []*models.HDiffData
for _, asset := range manifest.Assets {
assetName := asset.AssetName
assetSize := asset.AssetSize
if asset.AssetData != nil {
for _, chunk := range asset.AssetData.Assets {
matched := false
for _, name := range chunkNames {
if name == chunk.ChunkFileName {
matched = true
break
}
}
if !matched {
continue
}
if chunk.OriginalFileSize != 0 || chunk.HdiffFileSize != assetSize {
hdiffFiles = append(hdiffFiles, &models.HDiffData{
SourceFileName: chunk.OriginalFilePath,
TargetFileName: assetName,
PatchFileName: fmt.Sprintf("%s.hdiff", assetName),
})
}
}
}
}
return hdiffFiles, nil
}

View File

@@ -0,0 +1,87 @@
package diffService
import (
"firefly-launcher/pkg/constant"
"firefly-launcher/pkg/sevenzip"
"io"
"os"
"path/filepath"
"github.com/wailsapp/wails/v3/pkg/application"
)
func (h *DiffService) DataExtract(gamePath, patchPath string) (bool, string) {
os.RemoveAll(constant.TempUrl)
if _, err := os.Stat(gamePath); err != nil {
return false, err.Error()
}
if _, err := os.Stat(patchPath); err != nil {
return false, err.Error()
}
if _, err := os.Stat(constant.TempUrl); os.IsNotExist(err) {
if err := os.MkdirAll(constant.TempUrl, os.ModePerm); err != nil {
return false, err.Error()
}
}
if err := sevenzip.ExtractAllFilesFromZip(patchPath, constant.TempUrl); err != nil {
os.RemoveAll(constant.TempUrl)
return false, err.Error()
}
return true, "extract completed"
}
func (h *DiffService) CutData(gamePath string) (bool, string) {
if _, err := os.Stat(constant.TempUrl); os.IsNotExist(err) {
return false, err.Error()
}
err := filepath.Walk(constant.TempUrl, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(constant.TempUrl, path)
if err != nil {
return err
}
destPath := filepath.Join(gamePath, relPath)
application.Get().Event.Emit("diff:message", map[string]string{"message": destPath})
if info.IsDir() {
return os.MkdirAll(destPath, os.ModePerm)
}
if err := os.MkdirAll(filepath.Dir(destPath), os.ModePerm); err != nil {
return err
}
if err := os.Rename(path, destPath); err != nil {
srcFile, err := os.Open(path)
if err != nil {
return err
}
defer srcFile.Close()
dstFile, err := os.Create(destPath)
if err != nil {
return err
}
defer dstFile.Close()
if _, err := io.Copy(dstFile, srcFile); err != nil {
return err
}
os.Remove(path)
}
return nil
})
if err != nil {
os.RemoveAll(constant.TempUrl)
return false, err.Error()
}
os.RemoveAll(constant.TempUrl)
return true, "cut completed"
}

View File

@@ -1,4 +1,4 @@
package internal
package fsService
import (
"firefly-launcher/pkg/sevenzip"
@@ -8,7 +8,6 @@ import (
"path/filepath"
"runtime"
"strings"
"github.com/wailsapp/wails/v3/pkg/application"
"golang.org/x/sys/windows"
)
@@ -31,7 +30,7 @@ func (f *FSService) PickFolder() (string, error) {
return "", nil
}
func (f *FSService) PickFile() (string, error) {
func (f *FSService) PickFile(filter string) (string, error) {
dialog := application.OpenFileDialog().
CanChooseFiles(true).
ResolvesAliases(true)
@@ -40,6 +39,9 @@ func (f *FSService) PickFile() (string, error) {
} else {
dialog.SetTitle("Select a file/directory")
}
if filter == "exe" {
dialog.AddFilter("Executable Files (*.exe)", "*.exe")
}
if path, err := dialog.PromptForSingleSelection(); err == nil {
return path, nil
}
@@ -70,7 +72,7 @@ func (f *FSService) StartApp(path string) (bool, error) {
if strings.HasSuffix(path, "StarRail.exe") {
go func() {
_ = cmd.Wait()
application.Get().EmitEvent("game:exit")
application.Get().Event.Emit("game:exit")
}()
}
@@ -107,11 +109,11 @@ func (f *FSService) StartWithConsole(path string) (bool, error) {
go func() {
_ = cmd.Wait()
if strings.HasSuffix(path, "launcher.exe") {
application.Get().EmitEvent("game:exit")
application.Get().Event.Emit("game:exit")
} else if strings.HasSuffix(path, "firefly-go_win.exe") {
application.Get().EmitEvent("server:exit")
application.Get().Event.Emit("server:exit")
} else if strings.HasSuffix(path, "FireflyProxy.exe") {
application.Get().EmitEvent("proxy:exit")
application.Get().Event.Emit("proxy:exit")
}
}()
return true, nil
@@ -128,7 +130,7 @@ func (f *FSService) OpenFolder(path string) (bool, string) {
}
url := "file:///" + filepath.ToSlash(absPath)
application.Get().BrowserOpenURL(url)
application.Get().Browser.OpenURL(url)
return true, ""
}
@@ -140,3 +142,4 @@ func (f *FSService) FileExistsInZip(archivePath, fileInside string) (bool, strin
}
return exists, ""
}

View File

@@ -1,203 +0,0 @@
package internal
import (
"encoding/json"
"firefly-launcher/pkg/constant"
"firefly-launcher/pkg/models"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"github.com/wailsapp/wails/v3/pkg/application"
)
type GitService struct{}
func (g *GitService) GetLatestServerVersion(oldVersion string) (bool, string, string) {
resp, err := http.Get(constant.ServerGitUrl)
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) DownloadServerProgress(version string) (bool, string) {
resp, err := http.Get(constant.ServerGitUrl)
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.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()
}
}
saveFile := filepath.Join(constant.ServerStorageUrl, assetWin.Name)
resp, err = http.Get(assetWin.BrowserDownloadURL)
if err != nil {
return false, err.Error()
}
defer resp.Body.Close()
DownloadFile(saveFile, assetWin.BrowserDownloadURL, func(percent float64, speed float64) {
application.Get().EmitEvent("download:server", map[string]interface{}{
"percent": fmt.Sprintf("%.2f", percent),
"speed": fmt.Sprintf("%.2f", speed),
})
})
return true, ""
}
func (g *GitService) UnzipServer() {
unzipParallel(filepath.Join(constant.ServerStorageUrl, constant.ServerZipFile), constant.ServerStorageUrl)
os.Remove(filepath.Join(constant.ServerStorageUrl, constant.ServerZipFile))
}
func (g *GitService) GetLatestProxyVersion(oldVersion string) (bool, string, string) {
resp, err := http.Get(constant.ProxyGitUrl)
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) DownloadProxyProgress(version string) (bool, string) {
resp, err := http.Get(constant.ProxyGitUrl)
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.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()
}
}
saveFile := filepath.Join(constant.ProxyStorageUrl, assetWin.Name)
resp, err = http.Get(assetWin.BrowserDownloadURL)
if err != nil {
return false, err.Error()
}
defer resp.Body.Close()
DownloadFile(saveFile, assetWin.BrowserDownloadURL, func(percent float64, speed float64) {
application.Get().EmitEvent("download:proxy", map[string]interface{}{
"percent": fmt.Sprintf("%.2f", percent),
"speed": fmt.Sprintf("%.2f", speed),
})
})
return true, ""
}
func (g *GitService) UnzipProxy() {
unzipParallel(filepath.Join(constant.ProxyStorageUrl, constant.ProxyZipFile), constant.ProxyStorageUrl)
os.Remove(filepath.Join(constant.ProxyStorageUrl, constant.ProxyZipFile))
}

View File

@@ -0,0 +1,57 @@
package gitService
import (
"encoding/json"
"firefly-launcher/pkg/constant"
"firefly-launcher/pkg/models"
"io"
"net/http"
"github.com/minio/selfupdate"
)
type GitService struct{}
func (g *GitService) GetLatestLauncherVersion() (bool, string, string) {
resp, err := http.Get(constant.LauncherGitUrl)
if err != nil {
return false, "", err.Error()
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var releases []models.ReleaseType
err = json.Unmarshal(body, &releases)
if err != nil {
return false, "", err.Error()
}
if len(releases) == 0 {
return false, "", "no releases found"
}
return true, releases[0].TagName, ""
}
func (g *GitService) UpdateLauncherProgress(version string) (bool, string) {
asset, ok := g.getReleaseAsset(version, constant.LauncherGitUrl, constant.LauncherFile)
if !ok {
return false, "no release found"
}
resp, err := http.Get(asset.BrowserDownloadURL)
if err != nil {
return false, err.Error()
}
defer resp.Body.Close()
err = selfupdate.Apply(resp.Body, selfupdate.Options{})
if err != nil {
return false, err.Error()
}
return true, ""
}

View File

@@ -0,0 +1,71 @@
package gitService
import (
"encoding/json"
"firefly-launcher/pkg/constant"
"firefly-launcher/pkg/models"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"time"
"github.com/wailsapp/wails/v3/pkg/application"
)
func (g *GitService) GetLatestProxyVersion() (bool, string, string) {
resp, err := http.Get(constant.ProxyGitUrl)
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) DownloadProxyProgress(version string) (bool, string) {
asset, ok := g.getReleaseAsset(version, constant.ProxyGitUrl, constant.ProxyZipFile)
if !ok {
return false, "no release found"
}
if err := os.MkdirAll(constant.ProxyStorageUrl, 0755); err != nil {
return false, err.Error()
}
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 {
return false, err.Error()
}
for i := 0; i < 3; i++ {
if err := os.Rename(tmpPath, saveFile); err == nil {
return true, ""
}
time.Sleep(300 * time.Millisecond)
}
return false, "failed to rename tmp file after retries"
}
func (g *GitService) UnzipProxy() {
g.unzipParallel(filepath.Join(constant.ProxyStorageUrl, constant.ProxyZipFile), constant.ProxyStorageUrl)
os.Remove(filepath.Join(constant.ProxyStorageUrl, constant.ProxyZipFile))
}

View File

@@ -0,0 +1,71 @@
package gitService
import (
"encoding/json"
"firefly-launcher/pkg/constant"
"firefly-launcher/pkg/models"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"time"
"github.com/wailsapp/wails/v3/pkg/application"
)
func (g *GitService) GetLatestServerVersion() (bool, string, string) {
resp, err := http.Get(constant.ServerGitUrl)
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) DownloadServerProgress(version string) (bool, string) {
asset, ok := g.getReleaseAsset(version, constant.ServerGitUrl, constant.ServerZipFile)
if !ok {
return false, "no release found"
}
if err := os.MkdirAll(constant.ServerStorageUrl, 0755); err != nil {
return false, err.Error()
}
saveFile := filepath.Join(constant.ServerStorageUrl, asset.Name)
tmpPath, err := g.downloadFileParallel(saveFile, asset.BrowserDownloadURL, 4, func(percent float64, speed string) {
application.Get().Event.Emit("download:server", map[string]interface{}{
"percent": fmt.Sprintf("%.2f", percent),
"speed": speed,
})
})
if err != nil {
return false, err.Error()
}
for i := 0; i < 3; i++ {
if err := os.Rename(tmpPath, saveFile); err == nil {
return true, ""
}
time.Sleep(300 * time.Millisecond)
}
return false, "failed to rename tmp file after retries"
}
func (g *GitService) UnzipServer() {
g.unzipParallel(filepath.Join(constant.ServerStorageUrl, constant.ServerZipFile), constant.ServerStorageUrl)
os.Remove(filepath.Join(constant.ServerStorageUrl, constant.ServerZipFile))
}

View File

@@ -0,0 +1,251 @@
package gitService
import (
"archive/zip"
"encoding/json"
"firefly-launcher/pkg/models"
"fmt"
"io"
"math"
"net/http"
"os"
"path/filepath"
"runtime"
"sync"
"time"
)
func HumanFormat(bytes float64) string {
if math.IsNaN(bytes) || math.IsInf(bytes, 0) {
return fmt.Sprintf("%v", bytes)
}
if bytes == 0 {
return "0B"
}
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 {
Total uint64
StartTime time.Time
OnEmit func(percent float64, speed string)
TotalSize int64
mu sync.Mutex
}
func NewWriteCounter(total int64, onEmit func(percent float64, speed string)) *WriteCounter {
return &WriteCounter{
StartTime: time.Now(),
TotalSize: total,
OnEmit: onEmit,
}
}
func (wc *WriteCounter) Add(n int) {
wc.mu.Lock()
defer wc.mu.Unlock()
wc.Total += uint64(n)
wc.PrintProgress()
}
func (wc *WriteCounter) PrintProgress() {
elapsed := time.Since(wc.StartTime).Seconds()
if elapsed < 0.001 {
elapsed = 0.001
}
speed := float64(wc.Total) / elapsed
percent := float64(wc.Total) / float64(wc.TotalSize) * 100
if wc.OnEmit != nil {
wc.OnEmit(percent, fmt.Sprintf("%s/s", HumanFormat(speed)))
}
}
func (g *GitService) downloadFileParallel(filePath, url string, numParts int, onEmit func(percent float64, speed string)) (tmpPath string, err error) {
resp, err := http.Head(url)
if err != nil {
return "", fmt.Errorf("failed to get head: %w", err)
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("bad status: %s", resp.Status)
}
size := resp.ContentLength
tmpPath = filePath + ".tmp"
out, err := os.Create(tmpPath)
if err != nil {
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
}
_ = start
_ = end
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 {
return
}
defer resp.Body.Close()
buf := make([]byte, 32*1024)
var written int64
for {
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 {
break
}
if err != nil {
break
}
}
})
}
wg.Wait()
return tmpPath, nil
}
// --- 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()
reserved := 1
if numCPU > 4 {
reserved = 2
}
maxWorkers := numCPU - reserved
if maxWorkers < 1 {
maxWorkers = 1
}
r, err := zip.OpenReader(src)
if err != nil {
return err
}
defer r.Close()
err = os.MkdirAll(dest, 0755)
if err != nil {
return err
}
type job struct {
f *zip.File
}
jobs := make(chan job)
var wg sync.WaitGroup
for i := 0; i < maxWorkers; i++ {
wg.Go(func() {
for j := range jobs {
err := g.extractFile(j.f, dest)
if err != nil {
fmt.Printf("Error extracting %s: %v\n", j.f.Name, err)
}
}
})
}
for _, f := range r.File {
jobs <- job{f}
}
close(jobs)
wg.Wait()
return nil
}
func (g *GitService) extractFile(f *zip.File, dest string) error {
fp := filepath.Join(dest, f.Name)
if f.FileInfo().IsDir() {
return os.MkdirAll(fp, f.Mode())
}
err := os.MkdirAll(filepath.Dir(fp), 0755)
if err != nil {
return err
}
rc, err := f.Open()
if err != nil {
return err
}
defer rc.Close()
out, err := os.OpenFile(fp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, rc)
return err
}

View File

@@ -1,212 +0,0 @@
package internal
import (
"bufio"
"encoding/json"
"firefly-launcher/pkg/constant"
"firefly-launcher/pkg/models"
"firefly-launcher/pkg/sevenzip"
"firefly-launcher/pkg/verifier"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"github.com/wailsapp/wails/v3/pkg/application"
)
type HdiffzService struct{}
func (h *HdiffzService) VersionValidate(gamePath, patchPath string) (bool, string) {
oldVersionData, err := models.ParseBinaryVersion(filepath.Join(gamePath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes"))
if err != nil {
return false, err.Error()
}
if _, err := os.Stat(patchPath); err != nil {
return false, err.Error()
}
if _, err := os.Stat(constant.TempUrl); os.IsNotExist(err) {
if err := os.MkdirAll(constant.TempUrl, os.ModePerm); err != nil {
return false, err.Error()
}
}
if err := sevenzip.ExtractAFileFromZip(patchPath, "StarRail_Data\\StreamingAssets\\BinaryVersion.bytes", constant.TempUrl); err != nil {
return false, err.Error()
}
binPath := filepath.Join(constant.TempUrl, "BinaryVersion.bytes")
newVersionData, err := models.ParseBinaryVersion(binPath)
if err != nil {
return false, err.Error()
}
defer os.Remove(binPath)
v := newVersionData.Subtract(*oldVersionData)
if v != 0 && v != 1 {
return false, fmt.Sprintf("the diff version %s not valid with game version %s", newVersionData, oldVersionData)
}
return true, "validated"
}
func (h *HdiffzService) DataExtract(gamePath, patchPath string) (bool, string) {
if _, err := os.Stat(gamePath); err != nil {
return false, err.Error()
}
if _, err := os.Stat(patchPath); err != nil {
return false, err.Error()
}
if _, err := os.Stat(constant.TempUrl); os.IsNotExist(err) {
if err := os.MkdirAll(constant.TempUrl, os.ModePerm); err != nil {
return false, err.Error()
}
}
if err := sevenzip.ExtractAllFilesFromZip(patchPath, constant.TempUrl); err != nil {
os.RemoveAll(constant.TempUrl)
return false, err.Error()
}
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 *HdiffzService) CutData(gamePath string) (bool, string) {
if _, err := os.Stat(constant.TempUrl); os.IsNotExist(err) {
return false, err.Error()
}
err := filepath.Walk(constant.TempUrl, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(constant.TempUrl, path)
if err != nil {
return err
}
destPath := filepath.Join(gamePath, relPath)
application.Get().EmitEvent("hdiffz:message", map[string]string{"message": destPath})
if info.IsDir() {
return os.MkdirAll(destPath, os.ModePerm)
}
if err := os.MkdirAll(filepath.Dir(destPath), os.ModePerm); err != nil {
return err
}
if err := os.Rename(path, destPath); err != nil {
srcFile, err := os.Open(path)
if err != nil {
return err
}
defer srcFile.Close()
dstFile, err := os.Create(destPath)
if err != nil {
return err
}
defer dstFile.Close()
if _, err := io.Copy(dstFile, srcFile); err != nil {
return err
}
_ = os.Remove(path)
}
return nil
})
if err != nil {
return false, err.Error()
}
_ = os.RemoveAll(constant.TempUrl)
return true, "cut completed"
}
func (h *HdiffzService) PatchData(gamePath string) (bool, string) {
data, err := os.ReadFile(filepath.Join(gamePath, "hdiffmap.json"))
if err != nil {
return false, err.Error()
}
var jsonData struct {
DiffMap []*models.DiffMapType `json:"diff_map"`
}
if err := json.Unmarshal(data, &jsonData); err != nil {
return false, err.Error()
}
for i, entry := range jsonData.DiffMap {
application.Get().EmitEvent(
"hdiffz:progress", map[string]int{
"progress": i,
"maxProgress": len(jsonData.DiffMap),
})
sourceFile := filepath.Join(gamePath, entry.SourceFileName)
patchFile := filepath.Join(gamePath, entry.PatchFileName)
targetFile := filepath.Join(gamePath, entry.TargetFileName)
if _, err := os.Stat(sourceFile); os.IsNotExist(err) {
continue
}
if _, err := os.Stat(patchFile); os.IsNotExist(err) {
continue
}
cmd := exec.Command(constant.ToolHPatchzExe.String(), sourceFile, patchFile, targetFile)
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
_, err := cmd.CombinedOutput()
if err != nil {
continue
}
}
return true, "patching completed"
}
func (h *HdiffzService) DeleteFiles(gamePath string) (bool, string) {
var deleteFiles []string
file, err := os.Open(filepath.Join(gamePath, "deletefiles.txt"))
if err != nil {
return false, ""
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line != "" {
deleteFiles = append(deleteFiles, line)
}
}
if err := scanner.Err(); err != nil {
return false, ""
}
for i, file := range deleteFiles {
os.Remove(filepath.Join(gamePath, file))
application.Get().EmitEvent("hdiffz:progress", map[string]int{"progress": i, "maxProgress": len(deleteFiles)})
}
return true, ""
}

View File

@@ -1,146 +0,0 @@
package internal
import (
"bytes"
"fmt"
"os"
"path/filepath"
)
type LanguageService struct{}
func isValidLang(lang string) bool {
valid := []string{"en", "jp", "cn", "kr"}
for _, v := range valid {
if lang == v {
return true
}
}
return false
}
func (l *LanguageService) GetLanguage(path string) (string, string, error) {
files, err := os.ReadDir(path)
if err != nil {
return "", "", err
}
for _, file := range files {
filePath := filepath.Join(path, file.Name())
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")
}
func replaceBytes(content []byte, idx int, choice string, param int) int {
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 {
return false, err
}
for _, file := range files {
filePath := filepath.Join(path, file.Name())
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
}
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")
}

View File

@@ -0,0 +1,175 @@
package languageService
import (
"bytes"
assetMeta "firefly-launcher/pkg/language-patch/asset-meta"
excelLanguage "firefly-launcher/pkg/language-patch/excel-language"
"firefly-launcher/pkg/models"
"os"
"path/filepath"
"slices"
"strings"
)
type LanguageService struct{}
func isValidLang(lang string) bool {
valid := []string{"en", "jp", "cn", "kr"}
return slices.Contains(valid, lang)
}
func (l *LanguageService) GetLanguage(path string) (bool, string, string, string) {
currentVersionGame, err := models.ParseBinaryVersion(filepath.Join(path, "BinaryVersion.bytes"))
if err != nil {
return false, "", "", err.Error()
}
typeVersionGame := "os"
if strings.Contains(currentVersionGame.Name, "CN") {
typeVersionGame = "cn"
}
assetPath := filepath.Join(path, "DesignData\\Windows")
indexHash, err := assetMeta.GetIndexHash(assetPath)
if err != nil {
return false, "", "", err.Error()
}
DesignIndex, err := assetMeta.DesignIndexFromBytes(assetPath, indexHash)
if err != nil {
return false, "", "", err.Error()
}
dataEntry, fileEntry, err := DesignIndex.FindDataAndFileByTarget(-515329346)
if err != nil {
return false, "", "", err.Error()
}
allowedLanguage := excelLanguage.NewExcelLanguage(assetPath, &dataEntry, &fileEntry)
languageRows, err := allowedLanguage.Parse()
if err != nil {
return false, "", "", err.Error()
}
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

@@ -1,192 +0,0 @@
package internal
import (
"archive/zip"
"fmt"
"io"
"math"
"net/http"
"os"
"path/filepath"
"runtime"
"sync"
"time"
)
func humanFormat(bytes int64) string {
n := float64(bytes)
for _, unit := range []string{"", "Ki", "Mi", "Gi"} {
if math.Abs(n) < 1024.0 {
return fmt.Sprintf("%3.1f%sB", n, unit)
}
n /= 1024.0
}
return fmt.Sprintf("%.1fTiB", n)
}
type WriteCounter struct {
Total uint64
StartTime time.Time
OnEmit func(percent float64, speedMBps float64)
TotalSize int64
lastLoggedPercent int
}
func NewWriteCounter(total int64, onEmit func(percent float64, speedMBps float64)) *WriteCounter {
return &WriteCounter{
StartTime: time.Now(),
TotalSize: total,
lastLoggedPercent: -1,
OnEmit: onEmit,
}
}
func (wc *WriteCounter) Write(p []byte) (int, error) {
n := len(p)
wc.Total += uint64(n)
wc.PrintProgress()
return n, nil
}
func (wc *WriteCounter) PrintProgress() {
elapsed := time.Since(wc.StartTime).Seconds()
if elapsed < 0.001 {
elapsed = 0.001
}
speed := float64(wc.Total) / 1024 / 1024 / elapsed // MB/s
percent := float64(wc.Total) / float64(wc.TotalSize) * 100
if wc.OnEmit != nil {
wc.OnEmit(percent, speed)
}
}
func DownloadFile(filepath string, url string, onEmit func(percent float64, speed float64)) error {
tmpPath := filepath + ".tmp"
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("failed to get file: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("bad status: %s", resp.Status)
}
out, err := os.Create(tmpPath)
if err != nil {
return fmt.Errorf("failed to create tmp file: %w", err)
}
counter := NewWriteCounter(resp.ContentLength, onEmit)
_, err = io.Copy(out, io.TeeReader(resp.Body, counter))
if closeErr := out.Close(); closeErr != nil {
return fmt.Errorf("failed to close tmp file: %w", closeErr)
}
if err != nil {
return fmt.Errorf("failed to download file: %w", err)
}
// Delete destination file if it exists
if _, err := os.Stat(filepath); err == nil {
if err := os.Remove(filepath); err != nil {
return fmt.Errorf("failed to remove existing file: %w", err)
}
}
for i := 0; i < 3; i++ {
err = os.Rename(tmpPath, filepath)
if err == nil {
break
}
time.Sleep(300 * time.Millisecond)
}
if err != nil {
return fmt.Errorf("failed to rename after retries: %w", err)
}
return nil
}
func unzipParallel(src string, dest string) error {
numCPU := runtime.NumCPU()
reserved := 1
if numCPU > 4 {
reserved = 2
}
maxWorkers := numCPU - reserved
if maxWorkers < 1 {
maxWorkers = 1
}
r, err := zip.OpenReader(src)
if err != nil {
return err
}
defer r.Close()
err = os.MkdirAll(dest, 0755)
if err != nil {
return err
}
type job struct {
f *zip.File
}
jobs := make(chan job)
var wg sync.WaitGroup
// Worker pool
for i := 0; i < maxWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := range jobs {
err := extractFile(j.f, dest)
if err != nil {
fmt.Printf("Error extracting %s: %v\n", j.f.Name, err)
}
}
}()
}
// Feed jobs
for _, f := range r.File {
jobs <- job{f}
}
close(jobs)
wg.Wait()
return nil
}
func extractFile(f *zip.File, dest string) error {
fp := filepath.Join(dest, f.Name)
if f.FileInfo().IsDir() {
return os.MkdirAll(fp, f.Mode())
}
err := os.MkdirAll(filepath.Dir(fp), 0755)
if err != nil {
return err
}
rc, err := f.Open()
if err != nil {
return err
}
defer rc.Close()
out, err := os.OpenFile(fp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, rc)
return err
}

73
main.go
View File

@@ -3,7 +3,13 @@ package main
import (
"embed"
_ "embed"
"firefly-launcher/internal"
appService "firefly-launcher/internal/app-service"
diffService "firefly-launcher/internal/diff-service"
fsService "firefly-launcher/internal/fs-service"
gitService "firefly-launcher/internal/git-service"
languageService "firefly-launcher/internal/language-service"
"firefly-launcher/pkg/constant"
"fmt"
"log"
@@ -13,11 +19,6 @@ import (
"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
var assets embed.FS
@@ -42,10 +43,8 @@ func extractFile(embedPath, outPath string) error {
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() {
// Extract required files
for outPath, embedPath := range constant.RequiredFiles {
if !fileExists(outPath.String()) {
err := extractFile(embedPath, outPath.String())
@@ -54,44 +53,44 @@ func main() {
}
}
}
// Create a new Wails application by providing the necessary options.
// Variables 'Name' and 'Description' are for application metadata.
// 'Assets' configures the asset server with the 'FS' variable pointing to the frontend files.
// 'Bind' is a list of Go struct instances. The frontend has access to the methods of these instances.
// 'Mac' options tailor the application when running an macOS.
// Remove old executable
exePath, err := os.Executable()
if err == nil {
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{
Name: "firefly-launcher",
Description: "Firefly Launcher - Kain",
Services: []application.Service{
application.NewService(&internal.FSService{}),
application.NewService(&internal.LanguageService{}),
application.NewService(&internal.GitService{}),
application.NewService(&internal.HdiffzService{}),
application.NewService(&fsService.FSService{}),
application.NewService(&languageService.LanguageService{}),
application.NewService(&gitService.GitService{}),
application.NewService(&diffService.DiffService{}),
application.NewService(&appService.AppService{}),
},
Assets: application.AssetOptions{
Handler: application.AssetFileServerFS(assets),
},
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: true,
},
})
// Create a new window with the necessary options.
// '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{
// Create window
app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "Firefly Launcher - Kain",
Mac: application.MacWindow{
InvisibleTitleBarHeight: 50,
Backdrop: application.MacBackdropTranslucent,
TitleBar: application.MacTitleBarHiddenInset,
},
BackgroundColour: application.NewRGB(27, 38, 54),
Width: 1200,
Height: 600,
@@ -99,7 +98,23 @@ func main() {
DevToolsEnabled: true,
})
err := app.Run()
iconBytes, _ := tools.ReadFile("assets/appicon.png")
systemTray := app.SystemTray.New()
systemTray.SetIcon(iconBytes)
systemTray.SetTooltip("Firefly Launcher")
// Attach the window to the system tray
menu := application.NewMenu()
menu.Add("Open").OnClick(func(ctx *application.Context) {
// Handle click
})
menu.Add("Quit").OnClick(func(ctx *application.Context) {
app.Quit()
})
systemTray.SetMenu(menu)
err = app.Run()
if err != nil {
log.Fatal(err)
}

View File

@@ -2,12 +2,16 @@ package constant
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 LauncherGitUrl = "https://git.kain.io.vn/api/v1/repos/Firefly-Shelter/Firefly_Launcher/releases"
const ServerStorageUrl = "./server"
const ProxyStorageUrl = "./proxy"
const ServerZipFile = "prebuild_win_x86.zip"
const ProxyZipFile = "64bit.zip"
const LauncherFile = "firefly-launcher.exe"
const TempUrl = "./temp"
const CurrentLauncherVersion = "2.0.0"
type ToolFile string
const (
@@ -15,15 +19,13 @@ const (
Tool7zaDLL ToolFile = "bin/7za.dll"
Tool7zxaDLL ToolFile = "bin/7zxa.dll"
ToolHPatchzExe ToolFile = "bin/hpatchz.exe"
ToolHDiffzExe ToolFile = "bin/hdiffz.exe"
)
var RequiredFiles = map[ToolFile]string{
Tool7zaExe: "assets/7zip/7za.exe",
Tool7zaDLL: "assets/7zip/7za.dll",
Tool7zxaDLL: "assets/7zip/7zxa.dll",
ToolHPatchzExe: "assets/HDiffPatch/hpatchz.exe",
ToolHDiffzExe: "assets/HDiffPatch/hdiffz.exe",
Tool7zaExe: "assets/7za.exe",
Tool7zaDLL: "assets/7za.dll",
Tool7zxaDLL: "assets/7zxa.dll",
ToolHPatchzExe: "assets/hpatchz.exe",
}
func (t ToolFile) GetEmbedPath() string {

73
pkg/firefly/chunk.go Normal file
View File

@@ -0,0 +1,73 @@
package firefly
import (
"fmt"
"io"
"os"
"path/filepath"
"golang.org/x/exp/mmap"
)
type ChunkInfo struct {
Name string
Offset int64
Size int64
}
func ProcessWithBufReader(path string, chunks []ChunkInfo) {
file, err := os.Open(path)
if err != nil {
fmt.Printf("Error opening file %s: %v\n", path, err)
return
}
defer file.Close()
for _, chunk := range chunks {
buffer, err := ReadChunkData(file, chunk.Offset, chunk.Size)
if err != nil {
fmt.Printf("Error reading chunk %s: %v\n", chunk.Name, err)
continue
}
assetPath := filepath.Join("chunk_tmp", chunk.Name)
parentDir := filepath.Dir(assetPath)
if _, err := os.Stat(parentDir); os.IsNotExist(err) {
if err := os.MkdirAll(parentDir, 0o755); err != nil {
fmt.Printf("Error creating directory %s: %v\n", parentDir, err)
continue
}
}
if err := os.WriteFile(assetPath, buffer, 0o644); err != nil {
fmt.Printf("Error writing chunk file %s: %v\n", assetPath, err)
}
}
}
func ReadChunkData(file *os.File, offset, size int64) ([]byte, error) {
info, err := file.Stat()
if err != nil {
return nil, fmt.Errorf("error getting file info: %w", err)
}
if info.Size() > 1024*1024 && size > 1024*1024 {
mmapReader, err := mmap.Open(file.Name())
if err == nil {
defer mmapReader.Close()
buffer := make([]byte, size)
_, err := mmapReader.ReadAt(buffer, offset)
if err != nil && err != io.EOF {
return nil, fmt.Errorf("error reading mmap: %w", err)
}
return buffer, nil
}
}
buffer := make([]byte, size)
_, err = file.ReadAt(buffer, offset)
if err != nil && err != io.EOF {
return nil, fmt.Errorf("error reading buffered: %w", err)
}
return buffer, nil
}

94
pkg/firefly/ldiff.go Normal file
View File

@@ -0,0 +1,94 @@
package firefly
import (
"firefly-launcher/pkg/firefly/pb"
"fmt"
"os"
"path/filepath"
"golang.org/x/exp/mmap"
)
func LDiffFile(data *pb.AssetManifest, assetName string, assetSize int64, ldiffsDir, outputDir string) error {
path := filepath.Join(ldiffsDir, data.ChunkFileName)
info, err := os.Stat(path)
if err != nil {
return fmt.Errorf("%s does not exist: %w", path, err)
}
fileSize := info.Size()
var buffer []byte
if fileSize > 10*1024*1024 && data.HdiffFileSize > 1*1024*1024 {
// mmap for large files using x/exp/mmap
reader, err := mmap.Open(path)
if err != nil {
// fallback to buffered read
file, err := os.Open(path)
if err != nil {
return fmt.Errorf("error opening file %s: %w", path, err)
}
defer file.Close()
buffer, err = ReadBuffer(file, data.HdiffFileInChunkOffset, data.HdiffFileSize)
if err != nil {
return err
}
} else {
defer reader.Close()
buffer = make([]byte, data.HdiffFileSize)
_, err := reader.ReadAt(buffer, data.HdiffFileInChunkOffset)
if err != nil {
return fmt.Errorf("error reading mmap data: %w", err)
}
}
} else {
// small files, buffered read
file, err := os.Open(path)
if err != nil {
return fmt.Errorf("error opening file %s: %w", path, err)
}
defer file.Close()
buffer, err = ReadBuffer(file, data.HdiffFileInChunkOffset, data.HdiffFileSize)
if err != nil {
return err
}
}
extension := ""
if data.OriginalFileSize != 0 || assetSize != data.HdiffFileSize {
extension = ".hdiff"
}
assetPath := filepath.Join(outputDir, assetName+extension)
parentDir := filepath.Dir(assetPath)
if _, err := os.Stat(parentDir); os.IsNotExist(err) {
if err := os.MkdirAll(parentDir, 0o755); err != nil {
return fmt.Errorf("error creating directory %s: %w", parentDir, err)
}
}
if err := os.WriteFile(assetPath, buffer, 0o644); err != nil {
return fmt.Errorf("error writing file %s: %w", assetPath, err)
}
return nil
}
func ReadBuffer(file *os.File, offset int64, size int64) ([]byte, error) {
buffer := make([]byte, size)
n, err := file.ReadAt(buffer, offset)
if err != nil {
return nil, fmt.Errorf("error reading data: %w", err)
}
if int64(n) != size {
return nil, fmt.Errorf("expected %d bytes, but read %d bytes", size, n)
}
return buffer, nil
}

66
pkg/firefly/loader.go Normal file
View File

@@ -0,0 +1,66 @@
package firefly
import (
"bufio"
"firefly-launcher/pkg/firefly/pb"
"io"
"os"
"github.com/klauspost/compress/zstd"
"google.golang.org/protobuf/proto"
)
func LoadChunkProto(path string) (*pb.ChunkProto, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
reader := bufio.NewReader(file)
decoder, err := zstd.NewReader(reader)
if err != nil {
return nil, err
}
defer decoder.Close()
data, err := io.ReadAll(decoder)
if err != nil {
return nil, err
}
var chunk pb.ChunkProto
if err := proto.Unmarshal(data, &chunk); err != nil {
return nil, err
}
return &chunk, nil
}
// Load ManifestProto từ file Zstd + Protobuf
func LoadManifestProto(path string) (*pb.ManifestProto, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
reader := bufio.NewReader(file)
decoder, err := zstd.NewReader(reader)
if err != nil {
return nil, err
}
defer decoder.Close()
data, err := io.ReadAll(decoder)
if err != nil {
return nil, err
}
var manifest pb.ManifestProto
if err := proto.Unmarshal(data, &manifest); err != nil {
return nil, err
}
return &manifest, nil
}

View File

@@ -0,0 +1,651 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.5
// protoc v4.25.6
// source: proto/firefly.proto
package pb
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type ChunkProto struct {
state protoimpl.MessageState `protogen:"open.v1"`
Assets []*AssetChunkProperty `protobuf:"bytes,1,rep,name=assets,proto3" json:"assets,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ChunkProto) Reset() {
*x = ChunkProto{}
mi := &file_proto_firefly_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ChunkProto) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ChunkProto) ProtoMessage() {}
func (x *ChunkProto) ProtoReflect() protoreflect.Message {
mi := &file_proto_firefly_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ChunkProto.ProtoReflect.Descriptor instead.
func (*ChunkProto) Descriptor() ([]byte, []int) {
return file_proto_firefly_proto_rawDescGZIP(), []int{0}
}
func (x *ChunkProto) GetAssets() []*AssetChunkProperty {
if x != nil {
return x.Assets
}
return nil
}
type AssetChunkProperty struct {
state protoimpl.MessageState `protogen:"open.v1"`
AssetName string `protobuf:"bytes,1,opt,name=asset_name,json=assetName,proto3" json:"asset_name,omitempty"`
AssetChunks []*AssetChunk `protobuf:"bytes,2,rep,name=asset_chunks,json=assetChunks,proto3" json:"asset_chunks,omitempty"`
AssetType int32 `protobuf:"varint,3,opt,name=asset_type,json=assetType,proto3" json:"asset_type,omitempty"`
AssetSize int64 `protobuf:"varint,4,opt,name=asset_size,json=assetSize,proto3" json:"asset_size,omitempty"`
AssetHashMd5 string `protobuf:"bytes,5,opt,name=asset_hash_md5,json=assetHashMd5,proto3" json:"asset_hash_md5,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AssetChunkProperty) Reset() {
*x = AssetChunkProperty{}
mi := &file_proto_firefly_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AssetChunkProperty) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AssetChunkProperty) ProtoMessage() {}
func (x *AssetChunkProperty) ProtoReflect() protoreflect.Message {
mi := &file_proto_firefly_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AssetChunkProperty.ProtoReflect.Descriptor instead.
func (*AssetChunkProperty) Descriptor() ([]byte, []int) {
return file_proto_firefly_proto_rawDescGZIP(), []int{1}
}
func (x *AssetChunkProperty) GetAssetName() string {
if x != nil {
return x.AssetName
}
return ""
}
func (x *AssetChunkProperty) GetAssetChunks() []*AssetChunk {
if x != nil {
return x.AssetChunks
}
return nil
}
func (x *AssetChunkProperty) GetAssetType() int32 {
if x != nil {
return x.AssetType
}
return 0
}
func (x *AssetChunkProperty) GetAssetSize() int64 {
if x != nil {
return x.AssetSize
}
return 0
}
func (x *AssetChunkProperty) GetAssetHashMd5() string {
if x != nil {
return x.AssetHashMd5
}
return ""
}
type AssetChunk struct {
state protoimpl.MessageState `protogen:"open.v1"`
ChunkName string `protobuf:"bytes,1,opt,name=chunk_name,json=chunkName,proto3" json:"chunk_name,omitempty"`
ChunkDecompressedHashMd5 string `protobuf:"bytes,2,opt,name=chunk_decompressed_hash_md5,json=chunkDecompressedHashMd5,proto3" json:"chunk_decompressed_hash_md5,omitempty"`
ChunkOnFileOffset int64 `protobuf:"varint,3,opt,name=chunk_on_file_offset,json=chunkOnFileOffset,proto3" json:"chunk_on_file_offset,omitempty"`
ChunkSize int64 `protobuf:"varint,4,opt,name=chunk_size,json=chunkSize,proto3" json:"chunk_size,omitempty"`
ChunkSizeDecompressed int64 `protobuf:"varint,5,opt,name=chunk_size_decompressed,json=chunkSizeDecompressed,proto3" json:"chunk_size_decompressed,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AssetChunk) Reset() {
*x = AssetChunk{}
mi := &file_proto_firefly_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AssetChunk) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AssetChunk) ProtoMessage() {}
func (x *AssetChunk) ProtoReflect() protoreflect.Message {
mi := &file_proto_firefly_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AssetChunk.ProtoReflect.Descriptor instead.
func (*AssetChunk) Descriptor() ([]byte, []int) {
return file_proto_firefly_proto_rawDescGZIP(), []int{2}
}
func (x *AssetChunk) GetChunkName() string {
if x != nil {
return x.ChunkName
}
return ""
}
func (x *AssetChunk) GetChunkDecompressedHashMd5() string {
if x != nil {
return x.ChunkDecompressedHashMd5
}
return ""
}
func (x *AssetChunk) GetChunkOnFileOffset() int64 {
if x != nil {
return x.ChunkOnFileOffset
}
return 0
}
func (x *AssetChunk) GetChunkSize() int64 {
if x != nil {
return x.ChunkSize
}
return 0
}
func (x *AssetChunk) GetChunkSizeDecompressed() int64 {
if x != nil {
return x.ChunkSizeDecompressed
}
return 0
}
type ManifestProto struct {
state protoimpl.MessageState `protogen:"open.v1"`
Assets []*AssetManifestProperty `protobuf:"bytes,1,rep,name=assets,proto3" json:"assets,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ManifestProto) Reset() {
*x = ManifestProto{}
mi := &file_proto_firefly_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ManifestProto) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ManifestProto) ProtoMessage() {}
func (x *ManifestProto) ProtoReflect() protoreflect.Message {
mi := &file_proto_firefly_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ManifestProto.ProtoReflect.Descriptor instead.
func (*ManifestProto) Descriptor() ([]byte, []int) {
return file_proto_firefly_proto_rawDescGZIP(), []int{3}
}
func (x *ManifestProto) GetAssets() []*AssetManifestProperty {
if x != nil {
return x.Assets
}
return nil
}
type AssetManifestProperty struct {
state protoimpl.MessageState `protogen:"open.v1"`
AssetName string `protobuf:"bytes,1,opt,name=asset_name,json=assetName,proto3" json:"asset_name,omitempty"`
AssetSize int64 `protobuf:"varint,2,opt,name=asset_size,json=assetSize,proto3" json:"asset_size,omitempty"`
AssetHashMd5 string `protobuf:"bytes,3,opt,name=asset_hash_md5,json=assetHashMd5,proto3" json:"asset_hash_md5,omitempty"`
AssetData *AssetManifestChunk `protobuf:"bytes,4,opt,name=asset_data,json=assetData,proto3" json:"asset_data,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AssetManifestProperty) Reset() {
*x = AssetManifestProperty{}
mi := &file_proto_firefly_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AssetManifestProperty) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AssetManifestProperty) ProtoMessage() {}
func (x *AssetManifestProperty) ProtoReflect() protoreflect.Message {
mi := &file_proto_firefly_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AssetManifestProperty.ProtoReflect.Descriptor instead.
func (*AssetManifestProperty) Descriptor() ([]byte, []int) {
return file_proto_firefly_proto_rawDescGZIP(), []int{4}
}
func (x *AssetManifestProperty) GetAssetName() string {
if x != nil {
return x.AssetName
}
return ""
}
func (x *AssetManifestProperty) GetAssetSize() int64 {
if x != nil {
return x.AssetSize
}
return 0
}
func (x *AssetManifestProperty) GetAssetHashMd5() string {
if x != nil {
return x.AssetHashMd5
}
return ""
}
func (x *AssetManifestProperty) GetAssetData() *AssetManifestChunk {
if x != nil {
return x.AssetData
}
return nil
}
type AssetManifestChunk struct {
state protoimpl.MessageState `protogen:"open.v1"`
LatestAssetVersion string `protobuf:"bytes,1,opt,name=latest_asset_version,json=latestAssetVersion,proto3" json:"latest_asset_version,omitempty"`
Assets []*AssetManifest `protobuf:"bytes,2,rep,name=assets,proto3" json:"assets,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AssetManifestChunk) Reset() {
*x = AssetManifestChunk{}
mi := &file_proto_firefly_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AssetManifestChunk) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AssetManifestChunk) ProtoMessage() {}
func (x *AssetManifestChunk) ProtoReflect() protoreflect.Message {
mi := &file_proto_firefly_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AssetManifestChunk.ProtoReflect.Descriptor instead.
func (*AssetManifestChunk) Descriptor() ([]byte, []int) {
return file_proto_firefly_proto_rawDescGZIP(), []int{5}
}
func (x *AssetManifestChunk) GetLatestAssetVersion() string {
if x != nil {
return x.LatestAssetVersion
}
return ""
}
func (x *AssetManifestChunk) GetAssets() []*AssetManifest {
if x != nil {
return x.Assets
}
return nil
}
type AssetManifest struct {
state protoimpl.MessageState `protogen:"open.v1"`
ChunkFileName string `protobuf:"bytes,1,opt,name=chunk_file_name,json=chunkFileName,proto3" json:"chunk_file_name,omitempty"`
ChunkFileVersion string `protobuf:"bytes,2,opt,name=chunk_file_version,json=chunkFileVersion,proto3" json:"chunk_file_version,omitempty"`
ChunkFileNode string `protobuf:"bytes,3,opt,name=chunk_file_node,json=chunkFileNode,proto3" json:"chunk_file_node,omitempty"`
ChunkFileSize int64 `protobuf:"varint,4,opt,name=chunk_file_size,json=chunkFileSize,proto3" json:"chunk_file_size,omitempty"`
ChunkFileMd5 string `protobuf:"bytes,5,opt,name=chunk_file_md5,json=chunkFileMd5,proto3" json:"chunk_file_md5,omitempty"`
HdiffFileInChunkOffset int64 `protobuf:"varint,6,opt,name=hdiff_file_in_chunk_offset,json=hdiffFileInChunkOffset,proto3" json:"hdiff_file_in_chunk_offset,omitempty"`
HdiffFileSize int64 `protobuf:"varint,7,opt,name=hdiff_file_size,json=hdiffFileSize,proto3" json:"hdiff_file_size,omitempty"`
OriginalFilePath string `protobuf:"bytes,8,opt,name=original_file_path,json=originalFilePath,proto3" json:"original_file_path,omitempty"`
OriginalFileSize int64 `protobuf:"varint,9,opt,name=original_file_size,json=originalFileSize,proto3" json:"original_file_size,omitempty"`
OriginalFileMd5 string `protobuf:"bytes,10,opt,name=original_file_md5,json=originalFileMd5,proto3" json:"original_file_md5,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AssetManifest) Reset() {
*x = AssetManifest{}
mi := &file_proto_firefly_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AssetManifest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AssetManifest) ProtoMessage() {}
func (x *AssetManifest) ProtoReflect() protoreflect.Message {
mi := &file_proto_firefly_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AssetManifest.ProtoReflect.Descriptor instead.
func (*AssetManifest) Descriptor() ([]byte, []int) {
return file_proto_firefly_proto_rawDescGZIP(), []int{6}
}
func (x *AssetManifest) GetChunkFileName() string {
if x != nil {
return x.ChunkFileName
}
return ""
}
func (x *AssetManifest) GetChunkFileVersion() string {
if x != nil {
return x.ChunkFileVersion
}
return ""
}
func (x *AssetManifest) GetChunkFileNode() string {
if x != nil {
return x.ChunkFileNode
}
return ""
}
func (x *AssetManifest) GetChunkFileSize() int64 {
if x != nil {
return x.ChunkFileSize
}
return 0
}
func (x *AssetManifest) GetChunkFileMd5() string {
if x != nil {
return x.ChunkFileMd5
}
return ""
}
func (x *AssetManifest) GetHdiffFileInChunkOffset() int64 {
if x != nil {
return x.HdiffFileInChunkOffset
}
return 0
}
func (x *AssetManifest) GetHdiffFileSize() int64 {
if x != nil {
return x.HdiffFileSize
}
return 0
}
func (x *AssetManifest) GetOriginalFilePath() string {
if x != nil {
return x.OriginalFilePath
}
return ""
}
func (x *AssetManifest) GetOriginalFileSize() int64 {
if x != nil {
return x.OriginalFileSize
}
return 0
}
func (x *AssetManifest) GetOriginalFileMd5() string {
if x != nil {
return x.OriginalFileMd5
}
return ""
}
var File_proto_firefly_proto protoreflect.FileDescriptor
var file_proto_firefly_proto_rawDesc = string([]byte{
0x0a, 0x13, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x66, 0x69, 0x72, 0x65, 0x66, 0x6c, 0x79, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x66, 0x69, 0x72, 0x65, 0x66, 0x6c, 0x79, 0x22, 0x41,
0x0a, 0x0a, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x33, 0x0a, 0x06,
0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x66,
0x69, 0x72, 0x65, 0x66, 0x6c, 0x79, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x43, 0x68, 0x75, 0x6e,
0x6b, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x52, 0x06, 0x61, 0x73, 0x73, 0x65, 0x74,
0x73, 0x22, 0xcf, 0x01, 0x0a, 0x12, 0x41, 0x73, 0x73, 0x65, 0x74, 0x43, 0x68, 0x75, 0x6e, 0x6b,
0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x73, 0x73, 0x65,
0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x73,
0x73, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x36, 0x0a, 0x0c, 0x61, 0x73, 0x73, 0x65, 0x74,
0x5f, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e,
0x66, 0x69, 0x72, 0x65, 0x66, 0x6c, 0x79, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x43, 0x68, 0x75,
0x6e, 0x6b, 0x52, 0x0b, 0x61, 0x73, 0x73, 0x65, 0x74, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x73, 0x12,
0x1d, 0x0a, 0x0a, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20,
0x01, 0x28, 0x05, 0x52, 0x09, 0x61, 0x73, 0x73, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1d,
0x0a, 0x0a, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01,
0x28, 0x03, 0x52, 0x09, 0x61, 0x73, 0x73, 0x65, 0x74, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x24, 0x0a,
0x0e, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x5f, 0x6d, 0x64, 0x35, 0x18,
0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x73, 0x73, 0x65, 0x74, 0x48, 0x61, 0x73, 0x68,
0x4d, 0x64, 0x35, 0x22, 0xf2, 0x01, 0x0a, 0x0a, 0x41, 0x73, 0x73, 0x65, 0x74, 0x43, 0x68, 0x75,
0x6e, 0x6b, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x5f, 0x6e, 0x61, 0x6d, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x4e, 0x61, 0x6d,
0x65, 0x12, 0x3d, 0x0a, 0x1b, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x5f, 0x64, 0x65, 0x63, 0x6f, 0x6d,
0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x5f, 0x6d, 0x64, 0x35,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x18, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x44, 0x65, 0x63,
0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x48, 0x61, 0x73, 0x68, 0x4d, 0x64, 0x35,
0x12, 0x2f, 0x0a, 0x14, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x5f, 0x6f, 0x6e, 0x5f, 0x66, 0x69, 0x6c,
0x65, 0x5f, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11,
0x63, 0x68, 0x75, 0x6e, 0x6b, 0x4f, 0x6e, 0x46, 0x69, 0x6c, 0x65, 0x4f, 0x66, 0x66, 0x73, 0x65,
0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18,
0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x53, 0x69, 0x7a, 0x65,
0x12, 0x36, 0x0a, 0x17, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x5f, 0x64,
0x65, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28,
0x03, 0x52, 0x15, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x53, 0x69, 0x7a, 0x65, 0x44, 0x65, 0x63, 0x6f,
0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x22, 0x47, 0x0a, 0x0d, 0x4d, 0x61, 0x6e, 0x69,
0x66, 0x65, 0x73, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x36, 0x0a, 0x06, 0x61, 0x73, 0x73,
0x65, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x66, 0x69, 0x72, 0x65,
0x66, 0x6c, 0x79, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73,
0x74, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x52, 0x06, 0x61, 0x73, 0x73, 0x65, 0x74,
0x73, 0x22, 0xb7, 0x01, 0x0a, 0x15, 0x41, 0x73, 0x73, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66,
0x65, 0x73, 0x74, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x61,
0x73, 0x73, 0x65, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x09, 0x61, 0x73, 0x73, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x73,
0x73, 0x65, 0x74, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09,
0x61, 0x73, 0x73, 0x65, 0x74, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x61, 0x73, 0x73,
0x65, 0x74, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x5f, 0x6d, 0x64, 0x35, 0x18, 0x03, 0x20, 0x01, 0x28,
0x09, 0x52, 0x0c, 0x61, 0x73, 0x73, 0x65, 0x74, 0x48, 0x61, 0x73, 0x68, 0x4d, 0x64, 0x35, 0x12,
0x3a, 0x0a, 0x0a, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x66, 0x69, 0x72, 0x65, 0x66, 0x6c, 0x79, 0x2e, 0x41, 0x73,
0x73, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x43, 0x68, 0x75, 0x6e, 0x6b,
0x52, 0x09, 0x61, 0x73, 0x73, 0x65, 0x74, 0x44, 0x61, 0x74, 0x61, 0x22, 0x76, 0x0a, 0x12, 0x41,
0x73, 0x73, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x43, 0x68, 0x75, 0x6e,
0x6b, 0x12, 0x30, 0x0a, 0x14, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x61, 0x73, 0x73, 0x65,
0x74, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x12, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x41, 0x73, 0x73, 0x65, 0x74, 0x56, 0x65, 0x72, 0x73,
0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x06, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x18, 0x02, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x66, 0x69, 0x72, 0x65, 0x66, 0x6c, 0x79, 0x2e, 0x41, 0x73,
0x73, 0x65, 0x74, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x52, 0x06, 0x61, 0x73, 0x73,
0x65, 0x74, 0x73, 0x22, 0xc7, 0x03, 0x0a, 0x0d, 0x41, 0x73, 0x73, 0x65, 0x74, 0x4d, 0x61, 0x6e,
0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x5f, 0x66,
0x69, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d,
0x63, 0x68, 0x75, 0x6e, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2c, 0x0a,
0x12, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73,
0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x63, 0x68, 0x75, 0x6e, 0x6b,
0x46, 0x69, 0x6c, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x26, 0x0a, 0x0f, 0x63,
0x68, 0x75, 0x6e, 0x6b, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x18, 0x03,
0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x4e,
0x6f, 0x64, 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x5f, 0x66, 0x69, 0x6c,
0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x63, 0x68,
0x75, 0x6e, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x63,
0x68, 0x75, 0x6e, 0x6b, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x6d, 0x64, 0x35, 0x18, 0x05, 0x20,
0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x4d, 0x64,
0x35, 0x12, 0x3a, 0x0a, 0x1a, 0x68, 0x64, 0x69, 0x66, 0x66, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x5f,
0x69, 0x6e, 0x5f, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x5f, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18,
0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x16, 0x68, 0x64, 0x69, 0x66, 0x66, 0x46, 0x69, 0x6c, 0x65,
0x49, 0x6e, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x26, 0x0a,
0x0f, 0x68, 0x64, 0x69, 0x66, 0x66, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65,
0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x68, 0x64, 0x69, 0x66, 0x66, 0x46, 0x69, 0x6c,
0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61,
0x6c, 0x5f, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x08, 0x20, 0x01, 0x28,
0x09, 0x52, 0x10, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x50,
0x61, 0x74, 0x68, 0x12, 0x2c, 0x0a, 0x12, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x5f,
0x66, 0x69, 0x6c, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52,
0x10, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x53, 0x69, 0x7a,
0x65, 0x12, 0x2a, 0x0a, 0x11, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x69,
0x6c, 0x65, 0x5f, 0x6d, 0x64, 0x35, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6f, 0x72,
0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x46, 0x69, 0x6c, 0x65, 0x4d, 0x64, 0x35, 0x42, 0x0e, 0x5a,
0x0c, 0x2e, 0x2f, 0x66, 0x69, 0x72, 0x65, 0x66, 0x6c, 0x79, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x33,
})
var (
file_proto_firefly_proto_rawDescOnce sync.Once
file_proto_firefly_proto_rawDescData []byte
)
func file_proto_firefly_proto_rawDescGZIP() []byte {
file_proto_firefly_proto_rawDescOnce.Do(func() {
file_proto_firefly_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_firefly_proto_rawDesc), len(file_proto_firefly_proto_rawDesc)))
})
return file_proto_firefly_proto_rawDescData
}
var file_proto_firefly_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
var file_proto_firefly_proto_goTypes = []any{
(*ChunkProto)(nil), // 0: firefly.ChunkProto
(*AssetChunkProperty)(nil), // 1: firefly.AssetChunkProperty
(*AssetChunk)(nil), // 2: firefly.AssetChunk
(*ManifestProto)(nil), // 3: firefly.ManifestProto
(*AssetManifestProperty)(nil), // 4: firefly.AssetManifestProperty
(*AssetManifestChunk)(nil), // 5: firefly.AssetManifestChunk
(*AssetManifest)(nil), // 6: firefly.AssetManifest
}
var file_proto_firefly_proto_depIdxs = []int32{
1, // 0: firefly.ChunkProto.assets:type_name -> firefly.AssetChunkProperty
2, // 1: firefly.AssetChunkProperty.asset_chunks:type_name -> firefly.AssetChunk
4, // 2: firefly.ManifestProto.assets:type_name -> firefly.AssetManifestProperty
5, // 3: firefly.AssetManifestProperty.asset_data:type_name -> firefly.AssetManifestChunk
6, // 4: firefly.AssetManifestChunk.assets:type_name -> firefly.AssetManifest
5, // [5:5] is the sub-list for method output_type
5, // [5:5] is the sub-list for method input_type
5, // [5:5] is the sub-list for extension type_name
5, // [5:5] is the sub-list for extension extendee
0, // [0:5] is the sub-list for field type_name
}
func init() { file_proto_firefly_proto_init() }
func file_proto_firefly_proto_init() {
if File_proto_firefly_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_firefly_proto_rawDesc), len(file_proto_firefly_proto_rawDesc)),
NumEnums: 0,
NumMessages: 7,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_proto_firefly_proto_goTypes,
DependencyIndexes: file_proto_firefly_proto_depIdxs,
MessageInfos: file_proto_firefly_proto_msgTypes,
}.Build()
File_proto_firefly_proto = out.File
file_proto_firefly_proto_goTypes = nil
file_proto_firefly_proto_depIdxs = nil
}

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
}

Some files were not shown because too many files have changed in this diff Show More