Files
G-Launcher/internal/language-service/thai-patch.go
T

252 lines
8.4 KiB
Go

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)
}