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