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,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