251 lines
8.4 KiB
Go
251 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)
|
||
|
|
}
|