feat: initial commit for G-Launcher based on Firefly with Horoyoi enhancements
This commit is contained in:
commit
a99470a69f
155 files changed
+19061
No files matched your search
@@ -0,0 +1,63 @@
|
||||
package appService
|
||||
|
||||
import (
|
||||
"g-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, ""
|
||||
}
|
||||
|
||||
func (a *AppService) CloseApp() (bool, string) {
|
||||
application.Get().Quit()
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func (a *AppService) HideApp() (bool, string) {
|
||||
window := application.Get().Window.Current()
|
||||
if window == nil {
|
||||
return false, "not found window"
|
||||
}
|
||||
window.Hide()
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func (a *AppService) MinimizeApp() (bool, string) {
|
||||
window := application.Get().Window.Current()
|
||||
if window == nil {
|
||||
return false, "not found window"
|
||||
}
|
||||
window.Minimise()
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func (a *AppService) MaximizeApp() (bool, string) {
|
||||
window := application.Get().Window.Current()
|
||||
if window == nil {
|
||||
return false, "not found window"
|
||||
}
|
||||
window.Maximise()
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func (a *AppService) RestoreApp() (bool, string) {
|
||||
window := application.Get().Window.Current()
|
||||
if window == nil {
|
||||
return false, "not found window"
|
||||
}
|
||||
window.Restore()
|
||||
return true, ""
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
package diffService
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"g-launcher/pkg/constant"
|
||||
"g-launcher/pkg/hpatchz"
|
||||
"g-launcher/pkg/models"
|
||||
"g-launcher/pkg/sevenzip"
|
||||
"g-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.json"); 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()
|
||||
}
|
||||
var hdiffJson []*models.HDiffData
|
||||
if err := json.Unmarshal(data, &hdiffJson); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
jsonData.DiffMap = append(jsonData.DiffMap, hdiffJson...)
|
||||
} 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, ""
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package diffService
|
||||
|
||||
import (
|
||||
"g-launcher/pkg/firefly"
|
||||
"g-launcher/pkg/firefly/pb"
|
||||
"g-launcher/pkg/hpatchz"
|
||||
"g-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
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package diffService
|
||||
|
||||
import (
|
||||
"g-launcher/pkg/constant"
|
||||
"g-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"
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package diffService
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
const ziftExecutablePath = "bin/zift.exe"
|
||||
|
||||
var ziftPercentPattern = regexp.MustCompile(`(?i)(\d+(?:\.\d+)?)\s*%`)
|
||||
var ziftANSIPattern = regexp.MustCompile(`\x1b\[[0-?]*[ -/]*[@-~]`)
|
||||
|
||||
func (h *DiffService) ApplyDiffZiff(gamePath, patchPath string) (bool, string) {
|
||||
launcherPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return false, fmt.Sprintf("locate launcher directory: %v", err)
|
||||
}
|
||||
launcherDir := filepath.Dir(launcherPath)
|
||||
ziftPath := filepath.Join(launcherDir, filepath.FromSlash(ziftExecutablePath))
|
||||
if _, err := os.Stat(ziftPath); err != nil {
|
||||
if cwdPath, cwdErr := filepath.Abs(filepath.FromSlash(ziftExecutablePath)); cwdErr == nil {
|
||||
if _, statErr := os.Stat(cwdPath); statErr == nil {
|
||||
ziftPath = cwdPath
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(ziftPath); err != nil {
|
||||
return false, fmt.Sprintf("Zift executable not found at %s: %v", ziftPath, err)
|
||||
}
|
||||
if info, err := os.Stat(patchPath); err != nil || !info.Mode().IsRegular() {
|
||||
if err != nil {
|
||||
return false, fmt.Sprintf("patch file is unavailable: %v", err)
|
||||
}
|
||||
return false, "selected patch is not a file"
|
||||
}
|
||||
cmd := exec.Command(ziftPath, "-a", "-y", patchPath, gamePath)
|
||||
cmd.Dir = launcherDir
|
||||
outputReader, outputWriter := io.Pipe()
|
||||
cmd.Stdout = outputWriter
|
||||
cmd.Stderr = outputWriter
|
||||
application.Get().Event.Emit("diff:ziff-progress", map[string]interface{}{
|
||||
"message": "Starting Zift…",
|
||||
"percent": -1.0,
|
||||
})
|
||||
outputDone := make(chan string, 1)
|
||||
go func() { outputDone <- collectZiftOutput(outputReader) }()
|
||||
if err := cmd.Start(); err != nil {
|
||||
_ = outputWriter.Close()
|
||||
message := <-outputDone
|
||||
if message != "" {
|
||||
return false, fmt.Sprintf("run Zift: %v\n%s", err, message)
|
||||
}
|
||||
return false, fmt.Sprintf("run Zift: %v", err)
|
||||
}
|
||||
err = cmd.Wait()
|
||||
_ = outputWriter.Close()
|
||||
message := strings.TrimSpace(<-outputDone)
|
||||
if err != nil {
|
||||
if message == "" {
|
||||
return false, fmt.Sprintf("run Zift: %v", err)
|
||||
}
|
||||
return false, fmt.Sprintf("Zift failed: %v\n%s", err, message)
|
||||
}
|
||||
if message == "" {
|
||||
message = "Ziff patch applied successfully"
|
||||
}
|
||||
return true, message
|
||||
}
|
||||
|
||||
func collectZiftOutput(reader io.Reader) string {
|
||||
buffered := bufio.NewReader(reader)
|
||||
var output strings.Builder
|
||||
var line strings.Builder
|
||||
emitLine := func() {
|
||||
text := strings.TrimSpace(ziftANSIPattern.ReplaceAllString(line.String(), ""))
|
||||
line.Reset()
|
||||
if text == "" {
|
||||
return
|
||||
}
|
||||
output.WriteString(text)
|
||||
output.WriteByte('\n')
|
||||
progress := -1.0
|
||||
if match := ziftPercentPattern.FindStringSubmatch(text); len(match) > 1 {
|
||||
if value, err := strconv.ParseFloat(match[1], 64); err == nil && value >= 0 && value <= 100 {
|
||||
progress = value
|
||||
}
|
||||
}
|
||||
application.Get().Event.Emit("diff:ziff-progress", map[string]interface{}{
|
||||
"message": text,
|
||||
"percent": progress,
|
||||
})
|
||||
}
|
||||
|
||||
for {
|
||||
value, err := buffered.ReadByte()
|
||||
if err != nil {
|
||||
if line.Len() > 0 {
|
||||
emitLine()
|
||||
}
|
||||
break
|
||||
}
|
||||
if value == '\r' || value == '\n' {
|
||||
emitLine()
|
||||
continue
|
||||
}
|
||||
line.WriteByte(value)
|
||||
}
|
||||
return output.String()
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package fsService
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"g-launcher/pkg/sevenzip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
type FSService struct{}
|
||||
|
||||
func (f *FSService) PickFolder() (string, error) {
|
||||
dialog := application.Get().Dialog.OpenFile().
|
||||
CanChooseDirectories(true).
|
||||
CanChooseFiles(false).
|
||||
ResolvesAliases(true)
|
||||
if runtime.GOOS == "darwin" {
|
||||
dialog.SetMessage("Select a folder")
|
||||
} else {
|
||||
dialog.SetTitle("Select a folder")
|
||||
}
|
||||
return dialog.PromptForSingleSelection()
|
||||
}
|
||||
|
||||
func (f *FSService) PickFile(filter string) (string, error) {
|
||||
dialog := application.Get().Dialog.OpenFile().
|
||||
CanChooseFiles(true).
|
||||
ResolvesAliases(true)
|
||||
if runtime.GOOS == "darwin" {
|
||||
dialog.SetMessage("Select a file")
|
||||
} else {
|
||||
dialog.SetTitle("Select a file")
|
||||
}
|
||||
if filter == "exe" {
|
||||
dialog.AddFilter("Executable Files (*.exe)", "*.exe")
|
||||
} else if filter == "zip" {
|
||||
dialog.AddFilter("ZIP archives (*.zip)", "*.zip")
|
||||
} else if filter == "patch" {
|
||||
dialog.AddFilter("Patch files (*.zip;*.7z;*.ziff)", "*.zip;*.7z;*.ziff")
|
||||
}
|
||||
if path, err := dialog.PromptForSingleSelection(); err == nil {
|
||||
return path, nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (f *FSService) DirExists(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return info.IsDir()
|
||||
}
|
||||
|
||||
func (f *FSService) FileExists(path string) bool {
|
||||
if info, err := os.Stat(path); err == nil {
|
||||
return info.Mode().IsRegular()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (f *FSService) GetDir(path string) string {
|
||||
return filepath.Dir(path)
|
||||
}
|
||||
|
||||
func (f *FSService) Join(paths ...string) string {
|
||||
return filepath.Join(paths...)
|
||||
}
|
||||
|
||||
func (f *FSService) RemoveFile(path string) error {
|
||||
return os.Remove(path)
|
||||
}
|
||||
|
||||
func (f *FSService) StartApp(path string) (bool, string) {
|
||||
dir := filepath.Dir(path)
|
||||
cmd := exec.Command(path)
|
||||
cmd.Dir = dir
|
||||
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
if strings.HasSuffix(path, "StarRail.exe") || strings.HasSuffix(path, "ZenlessZoneZeroBeta.exe") {
|
||||
go func() {
|
||||
_ = cmd.Wait()
|
||||
application.Get().Event.Emit("game:exit")
|
||||
}()
|
||||
}
|
||||
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func (f *FSService) StartWithConsole(path string) (bool, string) {
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
if _, err := os.Stat(absPath); os.IsNotExist(err) {
|
||||
return false, "file not found: " + absPath
|
||||
}
|
||||
cmd := exec.Command(absPath)
|
||||
cmd.Dir = filepath.Dir(absPath)
|
||||
cmd.Stdin = nil
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
|
||||
cmd.SysProcAttr = &windows.SysProcAttr{
|
||||
CreationFlags: windows.CREATE_NEW_CONSOLE |
|
||||
windows.CREATE_BREAKAWAY_FROM_JOB,
|
||||
NoInheritHandles: true,
|
||||
}
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
go func() {
|
||||
_ = cmd.Wait()
|
||||
base := strings.ToLower(filepath.Base(path))
|
||||
if base == "launcher.exe" || base == "starrail.exe" {
|
||||
application.Get().Event.Emit("game:exit")
|
||||
} else if base == "firefly-go_win.exe" || base == "gameserver.exe" {
|
||||
application.Get().Event.Emit("server:exit")
|
||||
} else if base == "firefly-go-proxy.exe" || base == "proxy.exe" {
|
||||
application.Get().Event.Emit("proxy:exit")
|
||||
}
|
||||
}()
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func (f *FSService) OpenFolder(path string) (bool, string) {
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return false, "failed to resolve absolute path: " + err.Error()
|
||||
}
|
||||
|
||||
if !f.DirExists(absPath) {
|
||||
return false, "directory not found: " + absPath
|
||||
}
|
||||
|
||||
url := "file:///" + filepath.ToSlash(absPath)
|
||||
application.Get().Browser.OpenURL(url)
|
||||
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func (f *FSService) FileExistsInZip(archivePath, fileInside string) (bool, string) {
|
||||
exists, err := sevenzip.IsFileIn7z(archivePath, fileInside)
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
return exists, ""
|
||||
}
|
||||
|
||||
func (f *FSService) ReadConfigFile(gameDir, fileName string) (string, error) {
|
||||
if strings.TrimSpace(gameDir) == "" {
|
||||
return "", fmt.Errorf("select the game folder first")
|
||||
}
|
||||
target := filepath.Join(gameDir, fileName)
|
||||
content, err := os.ReadFile(target)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return "", nil
|
||||
}
|
||||
return "", fmt.Errorf("read file %s: %w", target, err)
|
||||
}
|
||||
return string(content), nil
|
||||
}
|
||||
|
||||
func (f *FSService) WriteConfigFile(gameDir, fileName, content string) error {
|
||||
if strings.TrimSpace(gameDir) == "" {
|
||||
return fmt.Errorf("select the game folder first")
|
||||
}
|
||||
target := filepath.Join(gameDir, fileName)
|
||||
return os.WriteFile(target, []byte(content), 0644)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package fsService
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
type gameSelection struct {
|
||||
GamePath string `json:"gamePath"`
|
||||
GameDir string `json:"gameDir"`
|
||||
}
|
||||
|
||||
func gameSelectionPath() (string, error) {
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("resolve user config directory: %w", err)
|
||||
}
|
||||
|
||||
return filepath.Join(configDir, "g-launcher", "game-selection.json"), nil
|
||||
}
|
||||
|
||||
func (f *FSService) GetSavedGameSelection() (string, string, error) {
|
||||
path, err := gameSelectionPath()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return "", "", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("read saved game selection: %w", err)
|
||||
}
|
||||
|
||||
var selection gameSelection
|
||||
if err := json.Unmarshal(data, &selection); err != nil {
|
||||
return "", "", fmt.Errorf("parse saved game selection: %w", err)
|
||||
}
|
||||
|
||||
return selection.GamePath, selection.GameDir, nil
|
||||
}
|
||||
|
||||
func (f *FSService) SaveGameSelection(gamePath, gameDir string) error {
|
||||
path, err := gameSelectionPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := json.Marshal(gameSelection{GamePath: gamePath, GameDir: gameDir})
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode game selection: %w", err)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
|
||||
return fmt.Errorf("create game selection directory: %w", err)
|
||||
}
|
||||
|
||||
tempPath := path + ".tmp"
|
||||
if err := os.WriteFile(tempPath, data, 0600); err != nil {
|
||||
return fmt.Errorf("write game selection: %w", err)
|
||||
}
|
||||
|
||||
tempPathPtr, err := windows.UTF16PtrFromString(tempPath)
|
||||
if err != nil {
|
||||
_ = os.Remove(tempPath)
|
||||
return fmt.Errorf("encode temporary game selection path: %w", err)
|
||||
}
|
||||
pathPtr, err := windows.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
_ = os.Remove(tempPath)
|
||||
return fmt.Errorf("encode game selection path: %w", err)
|
||||
}
|
||||
if err := windows.MoveFileEx(tempPathPtr, pathPtr, windows.MOVEFILE_REPLACE_EXISTING|windows.MOVEFILE_WRITE_THROUGH); err != nil {
|
||||
_ = os.Remove(tempPath)
|
||||
return fmt.Errorf("replace saved game selection: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
package gitService
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
const (
|
||||
minGitReleaseAPI = "https://api.github.com/repos/git-for-windows/git/releases/latest"
|
||||
minGitDirectory = "./server/tools/mingit"
|
||||
)
|
||||
|
||||
type gitRelease struct {
|
||||
Assets []struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"browser_download_url"`
|
||||
} `json:"assets"`
|
||||
}
|
||||
|
||||
// ensureGit provides a private, portable Git runtime when the user has not
|
||||
// installed Git system-wide. It requires no administrator privileges.
|
||||
func ensureGit() (string, error) {
|
||||
if git, err := exec.LookPath("git"); err == nil {
|
||||
return git, nil
|
||||
}
|
||||
|
||||
gitPath := filepath.Join(minGitDirectory, "cmd", "git.exe")
|
||||
if _, err := os.Stat(gitPath); err == nil {
|
||||
return gitPath, nil
|
||||
}
|
||||
if runtime.GOOS != "windows" || runtime.GOARCH != "amd64" {
|
||||
return "", fmt.Errorf("Git was not found on PATH, and automatic portable Git setup is currently supported on Windows x64 only")
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 2 * time.Minute}
|
||||
request, err := http.NewRequest(http.MethodGet, minGitReleaseAPI, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create Git download request: %w", err)
|
||||
}
|
||||
request.Header.Set("Accept", "application/vnd.github+json")
|
||||
request.Header.Set("User-Agent", "G-Launcher")
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("find a portable Git release: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("find a portable Git release: GitHub returned %s", response.Status)
|
||||
}
|
||||
var release gitRelease
|
||||
if err := json.NewDecoder(io.LimitReader(response.Body, 4<<20)).Decode(&release); err != nil {
|
||||
return "", fmt.Errorf("read portable Git release metadata: %w", err)
|
||||
}
|
||||
var archiveURL string
|
||||
for _, asset := range release.Assets {
|
||||
if strings.HasPrefix(asset.Name, "MinGit-") && strings.HasSuffix(asset.Name, "-64-bit.zip") {
|
||||
archiveURL = asset.URL
|
||||
break
|
||||
}
|
||||
}
|
||||
if archiveURL == "" {
|
||||
return "", fmt.Errorf("the latest Git for Windows release does not include a MinGit x64 package")
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(minGitDirectory), 0755); err != nil {
|
||||
return "", fmt.Errorf("create portable Git directory: %w", err)
|
||||
}
|
||||
archive, err := os.CreateTemp(filepath.Dir(minGitDirectory), "mingit-*.zip")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create portable Git archive: %w", err)
|
||||
}
|
||||
archivePath := archive.Name()
|
||||
defer os.Remove(archivePath)
|
||||
downloadRequest, err := http.NewRequest(http.MethodGet, archiveURL, nil)
|
||||
if err != nil {
|
||||
archive.Close()
|
||||
return "", fmt.Errorf("create portable Git download request: %w", err)
|
||||
}
|
||||
downloadRequest.Header.Set("User-Agent", "G-Launcher")
|
||||
downloadResponse, err := client.Do(downloadRequest)
|
||||
if err != nil {
|
||||
archive.Close()
|
||||
return "", fmt.Errorf("download portable Git: %w", err)
|
||||
}
|
||||
if downloadResponse.StatusCode != http.StatusOK {
|
||||
downloadResponse.Body.Close()
|
||||
archive.Close()
|
||||
return "", fmt.Errorf("download portable Git: GitHub returned %s", downloadResponse.Status)
|
||||
}
|
||||
_, copyErr := io.Copy(archive, io.LimitReader(downloadResponse.Body, 300<<20))
|
||||
responseErr := downloadResponse.Body.Close()
|
||||
closeErr := archive.Close()
|
||||
if copyErr != nil {
|
||||
return "", fmt.Errorf("save portable Git: %w", copyErr)
|
||||
}
|
||||
if responseErr != nil {
|
||||
return "", fmt.Errorf("finish portable Git download: %w", responseErr)
|
||||
}
|
||||
if closeErr != nil {
|
||||
return "", fmt.Errorf("save portable Git: %w", closeErr)
|
||||
}
|
||||
|
||||
stagingDir, err := os.MkdirTemp(filepath.Dir(minGitDirectory), "mingit-install-*")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create portable Git staging directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(stagingDir)
|
||||
if err := extractMinGit(archivePath, stagingDir); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(stagingDir, "cmd", "git.exe")); err != nil {
|
||||
return "", fmt.Errorf("portable Git archive is missing cmd/git.exe")
|
||||
}
|
||||
if err := os.RemoveAll(minGitDirectory); err != nil {
|
||||
return "", fmt.Errorf("replace incomplete portable Git setup: %w", err)
|
||||
}
|
||||
if err := os.Rename(stagingDir, minGitDirectory); err != nil {
|
||||
return "", fmt.Errorf("install portable Git: %w", err)
|
||||
}
|
||||
return gitPath, nil
|
||||
}
|
||||
|
||||
func gitCommand(gitPath string, args ...string) *exec.Cmd {
|
||||
command := exec.Command(gitPath, args...)
|
||||
if filepath.IsAbs(gitPath) && strings.EqualFold(filepath.Base(filepath.Dir(gitPath)), "cmd") {
|
||||
root := filepath.Dir(filepath.Dir(gitPath))
|
||||
command.Env = append(os.Environ(), "PATH="+filepath.Join(root, "mingw64", "bin")+";"+filepath.Join(root, "usr", "bin")+";"+os.Getenv("PATH"))
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
var gitProgressPattern = regexp.MustCompile(`(?i)(receiving objects|resolving deltas):\s*(\d+)%`)
|
||||
|
||||
func runGitWithProgress(command *exec.Cmd, action string) ([]byte, error) {
|
||||
stdout, err := command.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read Git output: %w", err)
|
||||
}
|
||||
stderr, err := command.StderrPipe()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read Git progress: %w", err)
|
||||
}
|
||||
if err := command.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var stdoutOutput bytes.Buffer
|
||||
var stderrOutput bytes.Buffer
|
||||
stdoutDone := make(chan error, 1)
|
||||
go func() {
|
||||
_, copyErr := io.Copy(&stdoutOutput, stdout)
|
||||
stdoutDone <- copyErr
|
||||
}()
|
||||
application.Get().Event.Emit("download:server", map[string]interface{}{
|
||||
"percent": "0",
|
||||
"speed": "",
|
||||
"message": action,
|
||||
})
|
||||
|
||||
scanner := bufio.NewScanner(stderr)
|
||||
scanner.Buffer(make([]byte, 4096), 1024*1024)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
stderrOutput.WriteString(line)
|
||||
stderrOutput.WriteByte('\n')
|
||||
match := gitProgressPattern.FindStringSubmatch(line)
|
||||
if len(match) != 3 {
|
||||
continue
|
||||
}
|
||||
phasePercent, parseErr := strconv.Atoi(match[2])
|
||||
if parseErr != nil {
|
||||
continue
|
||||
}
|
||||
percent := phasePercent * 90 / 100
|
||||
if strings.EqualFold(match[1], "resolving deltas") {
|
||||
percent = 90 + phasePercent/10
|
||||
}
|
||||
application.Get().Event.Emit("download:server", map[string]interface{}{
|
||||
"percent": fmt.Sprintf("%d", percent),
|
||||
"speed": "",
|
||||
"message": action + " · " + strings.ToLower(match[1]),
|
||||
})
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
command.Process.Kill()
|
||||
_ = command.Wait()
|
||||
<-stdoutDone
|
||||
return append(stdoutOutput.Bytes(), stderrOutput.Bytes()...), fmt.Errorf("read Git progress: %w", err)
|
||||
}
|
||||
stdoutErr := <-stdoutDone
|
||||
waitErr := command.Wait()
|
||||
if stdoutErr != nil {
|
||||
return append(stdoutOutput.Bytes(), stderrOutput.Bytes()...), fmt.Errorf("read Git output: %w", stdoutErr)
|
||||
}
|
||||
if waitErr != nil {
|
||||
return append(stdoutOutput.Bytes(), stderrOutput.Bytes()...), waitErr
|
||||
}
|
||||
application.Get().Event.Emit("download:server", map[string]interface{}{
|
||||
"percent": "100",
|
||||
"speed": "",
|
||||
"message": action + " · complete",
|
||||
})
|
||||
return append(stdoutOutput.Bytes(), stderrOutput.Bytes()...), nil
|
||||
}
|
||||
|
||||
func extractMinGit(archivePath, destination string) error {
|
||||
reader, err := zip.OpenReader(archivePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open portable Git archive: %w", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
root, err := filepath.Abs(destination)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve portable Git staging directory: %w", err)
|
||||
}
|
||||
for _, file := range reader.File {
|
||||
name := filepath.Clean(filepath.FromSlash(file.Name))
|
||||
if filepath.IsAbs(name) || name == "." || name == ".." || strings.HasPrefix(name, ".."+string(filepath.Separator)) {
|
||||
return fmt.Errorf("portable Git archive contains an unsafe path")
|
||||
}
|
||||
path := filepath.Join(root, name)
|
||||
if !strings.HasPrefix(path, root+string(filepath.Separator)) {
|
||||
return fmt.Errorf("portable Git archive contains an unsafe path")
|
||||
}
|
||||
if file.FileInfo().IsDir() {
|
||||
if err := os.MkdirAll(path, 0755); err != nil {
|
||||
return fmt.Errorf("extract portable Git directory: %w", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
return fmt.Errorf("create portable Git directory: %w", err)
|
||||
}
|
||||
source, err := file.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("read portable Git archive: %w", err)
|
||||
}
|
||||
output, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
source.Close()
|
||||
return fmt.Errorf("extract portable Git file: %w", err)
|
||||
}
|
||||
_, copyErr := io.Copy(output, source)
|
||||
sourceErr := source.Close()
|
||||
outputErr := output.Close()
|
||||
if copyErr != nil {
|
||||
return fmt.Errorf("extract portable Git file: %w", copyErr)
|
||||
}
|
||||
if sourceErr != nil {
|
||||
return fmt.Errorf("read portable Git archive: %w", sourceErr)
|
||||
}
|
||||
if outputErr != nil {
|
||||
return fmt.Errorf("write portable Git file: %w", outputErr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package gitService
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"g-launcher/pkg/constant"
|
||||
"g-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, ""
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package gitService
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"g-launcher/pkg/constant"
|
||||
"g-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.ProxyFile)
|
||||
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"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package gitService
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"g-launcher/pkg/constant"
|
||||
"g-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))
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
package gitService
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/json"
|
||||
"g-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
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package languageService
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
assetMeta "g-launcher/pkg/language-patch/asset-meta"
|
||||
excelLanguage "g-launcher/pkg/language-patch/excel-language"
|
||||
"g-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"
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
package languageService
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
betaPersistentContainer = "79568e2a754e3eab8afe4b5bf96c27ab.bytes"
|
||||
betaPersistentContainerCopy = "e303f4dc6c21f043021bb5b3b875b6c3.bytes"
|
||||
betaStreamingContainer = "4703a1942cfa452f451ba0a2f826b764.bytes"
|
||||
betaStreamingContainerCopy = "44b594f1fab938d6ed95d6a1154e03c0.bytes"
|
||||
betaPersistentDesignV = "878d1fefee5f49221b003050cb4a2d2f"
|
||||
betaPersistentDesignVCopy = "74b62fb914b36cccaa10d93ace9af2c0"
|
||||
betaStreamingDesignV = "b5eefe4a429b498a8f5003f28a4b21c6"
|
||||
betaStreamingDesignVCopy = "3ada95b84a1e0d96404ea5e7414ab353"
|
||||
betaPersistentContainerLimit = 26240102
|
||||
betaStreamingContainerLimit = 73631
|
||||
)
|
||||
|
||||
func (l *LanguageService) InstallThaiPatch(gameRoot, packageRoot string) (bool, string) {
|
||||
gameRoot, err := filepath.Abs(filepath.Clean(gameRoot))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("resolve game folder: %w", err).Error()
|
||||
}
|
||||
packageRoot, err = filepath.Abs(filepath.Clean(packageRoot))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("resolve patch package folder: %w", err).Error()
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filepath.Join(gameRoot, "StarRail.exe")); err != nil {
|
||||
return false, "selected folder does not contain StarRail.exe"
|
||||
}
|
||||
streamingAssets := filepath.Join(gameRoot, "StarRail_Data", "StreamingAssets")
|
||||
binaryVersion, err := os.ReadFile(filepath.Join(streamingAssets, "BinaryVersion.bytes"))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("read game version: %w", err).Error()
|
||||
}
|
||||
if !bytes.Contains(binaryVersion, []byte("BETAWin")) {
|
||||
return false, "this Thai patch package only supports Honkai: Star Rail Beta"
|
||||
}
|
||||
|
||||
assets := filepath.Join(packageRoot, "assets")
|
||||
readAsset := func(relative string) ([]byte, error) {
|
||||
data, err := os.ReadFile(filepath.Join(assets, filepath.FromSlash(relative)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read patch asset %s: %w", relative, err)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
requireAsset := func(relative string) ([]byte, error) {
|
||||
data, err := readAsset(relative)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil, fmt.Errorf("patch asset %s is empty", relative)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
textMap, err := requireAsset("1_TextMap_th/1fb7b1dce8d66efe8e111bb07a3eef44.bytes")
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
persistentUI, err := requireAsset("2_UI_Persistent/fd5227547d65b8bb1238feb3e5820fa3.bytes")
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
streamingUI, err := requireAsset("3_UI_StreamingAssets/2ea44f7fabf315d35de1d7f3b4c07d89.bytes")
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
localePack, err := requireAsset("4_Locale_Pak/th.pak")
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
allowedLanguages, err := requireAsset("5_AllowedLanguage/allowed_language_140bytes.bin")
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
persistentDesign, err := requireAsset("prebuilt_designv/DesignV_" + betaPersistentDesignV + ".bytes")
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
streamingDesign, err := requireAsset("prebuilt_designv/DesignV_" + betaStreamingDesignV + ".bytes")
|
||||
if err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
dataRoot := filepath.Join(gameRoot, "StarRail_Data")
|
||||
persistentDesignDir := filepath.Join(dataRoot, "Persistent", "DesignData", "Windows")
|
||||
streamingDesignDir := filepath.Join(dataRoot, "StreamingAssets", "DesignData", "Windows")
|
||||
for _, dir := range []string{
|
||||
filepath.Join(dataRoot, "Persistent", "Asb", "Windows"),
|
||||
filepath.Join(dataRoot, "Persistent", "Audio", "AudioPackage", "Windows"),
|
||||
persistentDesignDir,
|
||||
filepath.Join(dataRoot, "StreamingAssets", "Asb", "Windows"),
|
||||
streamingDesignDir,
|
||||
} {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return false, fmt.Errorf("create patch folder %s: %w", dir, err).Error()
|
||||
}
|
||||
}
|
||||
|
||||
type fileWrite struct {
|
||||
path string
|
||||
data []byte
|
||||
}
|
||||
writes := []fileWrite{
|
||||
{filepath.Join(dataRoot, "Persistent", "Asb", "Windows", "1fb7b1dce8d66efe8e111bb07a3eef44.bytes"), textMap},
|
||||
{filepath.Join(dataRoot, "Persistent", "Asb", "Windows", "fd5227547d65b8bb1238feb3e5820fa3.bytes"), persistentUI},
|
||||
{filepath.Join(dataRoot, "StreamingAssets", "Asb", "Windows", "2ea44f7fabf315d35de1d7f3b4c07d89.bytes"), streamingUI},
|
||||
{filepath.Join(dataRoot, "Persistent", "Audio", "AudioPackage", "Windows", "th.pak"), localePack},
|
||||
{filepath.Join(persistentDesignDir, "DesignV_"+betaPersistentDesignV+".bytes"), persistentDesign},
|
||||
{filepath.Join(persistentDesignDir, "DesignV_"+betaPersistentDesignVCopy+".bytes"), persistentDesign},
|
||||
{filepath.Join(streamingDesignDir, "DesignV_"+betaStreamingDesignV+".bytes"), streamingDesign},
|
||||
{filepath.Join(streamingDesignDir, "DesignV_"+betaStreamingDesignVCopy+".bytes"), streamingDesign},
|
||||
}
|
||||
|
||||
for _, write := range writes {
|
||||
if err := os.WriteFile(write.path, write.data, 0644); err != nil {
|
||||
return false, fmt.Errorf("save patch file %s: %w", write.path, err).Error()
|
||||
}
|
||||
}
|
||||
|
||||
if err := patchContainerFile(filepath.Join(persistentDesignDir, betaPersistentContainer), persistentDesign, betaPersistentContainerLimit); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
if err := patchContainerFile(filepath.Join(persistentDesignDir, betaPersistentContainerCopy), persistentDesign, betaPersistentContainerLimit); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
if err := patchContainerFile(filepath.Join(streamingDesignDir, betaStreamingContainer), streamingDesign, betaStreamingContainerLimit); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
if err := patchContainerFile(filepath.Join(streamingDesignDir, betaStreamingContainerCopy), streamingDesign, betaStreamingContainerLimit); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
if err := patchAllowedLanguages(filepath.Join(dataRoot, "Persistent", "AllowedLanguage.bytes"), allowedLanguages); err != nil {
|
||||
return false, err.Error()
|
||||
}
|
||||
|
||||
return true, ""
|
||||
}
|
||||
|
||||
func patchContainerFile(path string, targetPayload []byte, sizeLimit int64) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("read container %s: %w", path, err)
|
||||
}
|
||||
if int64(len(data)) > sizeLimit {
|
||||
return fmt.Errorf("container file is unexpectedly large: %s", path)
|
||||
}
|
||||
|
||||
entries, err := parseDesignVContainer(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse container %s: %w", path, err)
|
||||
}
|
||||
|
||||
for i := range entries {
|
||||
entries[i].payload = append([]byte(nil), targetPayload...)
|
||||
}
|
||||
|
||||
rebuilt, err := buildDesignVContainer(entries)
|
||||
if err != nil {
|
||||
return fmt.Errorf("rebuild container %s: %w", path, err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, rebuilt, 0644); err != nil {
|
||||
return fmt.Errorf("save container %s: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type containerEntry struct {
|
||||
hash string
|
||||
payload []byte
|
||||
}
|
||||
|
||||
func parseDesignVContainer(data []byte) ([]containerEntry, error) {
|
||||
if len(data) < 4 {
|
||||
return nil, fmt.Errorf("container is too short")
|
||||
}
|
||||
reader := bytes.NewReader(data)
|
||||
var count uint32
|
||||
if err := binary.Read(reader, binary.LittleEndian, &count); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries := make([]containerEntry, 0, count)
|
||||
for i := uint32(0); i < count; i++ {
|
||||
var hashBytes [16]byte
|
||||
if _, err := io.ReadFull(reader, hashBytes[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var length uint32
|
||||
if err := binary.Read(reader, binary.LittleEndian, &length); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload := make([]byte, length)
|
||||
if _, err := io.ReadFull(reader, payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries = append(entries, containerEntry{
|
||||
hash: hex.EncodeToString(hashBytes[:]),
|
||||
payload: payload,
|
||||
})
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func buildDesignVContainer(entries []containerEntry) ([]byte, error) {
|
||||
sort.Slice(entries, func(i, j int) bool {
|
||||
return strings.ToLower(entries[i].hash) < strings.ToLower(entries[j].hash)
|
||||
})
|
||||
var buffer bytes.Buffer
|
||||
if err := binary.Write(&buffer, binary.LittleEndian, uint32(len(entries))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
rawHash, err := hex.DecodeString(entry.hash)
|
||||
if err != nil || len(rawHash) != 16 {
|
||||
return nil, fmt.Errorf("invalid hash in container entry: %s", entry.hash)
|
||||
}
|
||||
if _, err := buffer.Write(rawHash); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := binary.Write(&buffer, binary.LittleEndian, uint32(len(entry.payload))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := buffer.Write(entry.payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func patchAllowedLanguages(path string, payload []byte) error {
|
||||
return os.WriteFile(path, payload, 0644)
|
||||
}
|
||||
Reference in new issue
Block a user