diff --git a/language-change/Makefile b/language-change/Makefile new file mode 100644 index 0000000..cf67497 --- /dev/null +++ b/language-change/Makefile @@ -0,0 +1,12 @@ +APP_NAME = language-change.exe + +all: build + +build: + go build -o $(APP_NAME) ./ + +run: build + ./$(APP_NAME) + +clean: + rm -f $(APP_NAME) diff --git a/language-change/asset-meta/bytehash16.go b/language-change/asset-meta/bytehash16.go new file mode 100644 index 0000000..eb0823c --- /dev/null +++ b/language-change/asset-meta/bytehash16.go @@ -0,0 +1,30 @@ +package assetMeta + +import ( + "fmt" + "io" +) + +type ByteHash16 []byte + +func ByteHash16FromBytes(r io.ReadSeeker) (ByteHash16, error) { + fullHash := make([]byte, 16) + buf := make([]byte, 4) + for i := 0; i < 4; i++ { + if _, err := io.ReadFull(r, buf); err != nil { + return nil, err + } + for j := 0; j < 4; j++ { + fullHash[i*4+j] = buf[3-j] + } + } + return ByteHash16(fullHash), nil +} + +func (b ByteHash16) String() string { + s := "" + for _, v := range b { + s += fmt.Sprintf("%02x", v) + } + return s +} diff --git a/language-change/asset-meta/dataEntry.go b/language-change/asset-meta/dataEntry.go new file mode 100644 index 0000000..3943998 --- /dev/null +++ b/language-change/asset-meta/dataEntry.go @@ -0,0 +1,28 @@ +package assetMeta + +import ( + "encoding/binary" + "io" +) + +type DataEntry struct { + NameHash int32 + Size uint32 + Offset uint32 +} + +func DataEntryFromBytes(r io.Reader) (*DataEntry, error) { + var d DataEntry + + if err := binary.Read(r, binary.BigEndian, &d.NameHash); err != nil { + return nil, err + } + if err := binary.Read(r, binary.BigEndian, &d.Size); err != nil { + return nil, err + } + if err := binary.Read(r, binary.BigEndian, &d.Offset); err != nil { + return nil, err + } + + return &d, nil +} diff --git a/language-change/asset-meta/designIndex.go b/language-change/asset-meta/designIndex.go new file mode 100644 index 0000000..c1576f3 --- /dev/null +++ b/language-change/asset-meta/designIndex.go @@ -0,0 +1,98 @@ +package assetMeta + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "errors" + "fmt" + "os" + "path/filepath" +) + + +type DesignIndex struct { + UnkI64 int64 + FileCount int32 + DesignDataCount int32 + FileList []FileEntry +} + + + +func (d *DesignIndex) FindDataAndFileByTarget(target int32) (DataEntry, FileEntry, error) { + for _, file := range d.FileList { + for _, entry := range file.DataEntries { + if entry.NameHash == target { + return entry, file, nil + } + } + } + return DataEntry{}, FileEntry{}, errors.New("not found") +} + + +func DesignIndexFromBytes(assetFolder string, indexHash string) (*DesignIndex, error) { + path := filepath.Join(assetFolder, fmt.Sprintf("DesignV_%s.bytes", indexHash)) + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + r := bytes.NewReader(data) + + var d DesignIndex + + if err := binary.Read(r, binary.BigEndian, &d.UnkI64); err != nil { + return nil, err + } + if err := binary.Read(r, binary.BigEndian, &d.FileCount); err != nil { + return nil, err + } + if err := binary.Read(r, binary.BigEndian, &d.DesignDataCount); err != nil { + return nil, err + } + + d.FileList = make([]FileEntry, 0, d.FileCount) + for i := int32(0); i < d.FileCount; i++ { + entry, err := FileEntryFromBytes(r) + if err != nil { + return nil, err + } + d.FileList = append(d.FileList, *entry) + } + + return &d, nil +} + + +func GetIndexHash(assetFolder string) (string, error) { + path := filepath.Join(assetFolder, "M_DesignV.bytes") + + f, err := os.Open(path) + if err != nil { + return "", err + } + defer f.Close() + + _, err = f.Seek(0x1C, 0) + if err != nil { + return "", err + } + + hash := make([]byte, 0x10) + index := 0 + for i := 0; i < 4; i++ { + chunk := make([]byte, 4) + _, err := f.Read(chunk) + if err != nil { + return "", err + } + + for bytePos := 3; bytePos >= 0; bytePos-- { + hash[index] = chunk[bytePos] + index++ + } + } + + return hex.EncodeToString(hash), nil +} \ No newline at end of file diff --git a/language-change/asset-meta/fileEntry.go b/language-change/asset-meta/fileEntry.go new file mode 100644 index 0000000..a7c1cdc --- /dev/null +++ b/language-change/asset-meta/fileEntry.go @@ -0,0 +1,63 @@ +package assetMeta + +import ( + "encoding/binary" + "fmt" + "io" +) + +type FileEntry struct { + NameHash int32 + FileByteName string + Size int64 + DataCount int32 + DataEntries []DataEntry + Unk uint8 +} + +func FileEntryFromBytes(r io.Reader) (*FileEntry, error) { + var f FileEntry + + if err := binary.Read(r, binary.BigEndian, &f.NameHash); err != nil { + return nil, err + } + + buf := make([]byte, 16) + if _, err := io.ReadFull(r, buf); err != nil { + return nil, err + } + f.FileByteName = toHex(buf) + + if err := binary.Read(r, binary.BigEndian, &f.Size); err != nil { + return nil, err + } + if err := binary.Read(r, binary.BigEndian, &f.DataCount); err != nil { + return nil, err + } + + f.DataEntries = make([]DataEntry, 0, f.DataCount) + for i := int32(0); i < f.DataCount; i++ { + entry, err := DataEntryFromBytes(r) + if err != nil { + return nil, err + } + f.DataEntries = append(f.DataEntries, *entry) + } + + // read 1 byte + b := make([]byte, 1) + if _, err := r.Read(b); err != nil { + return nil, err + } + f.Unk = b[0] + + return &f, nil +} + +func toHex(buf []byte) string { + s := "" + for _, b := range buf { + s += fmt.Sprintf("%02x", b) + } + return s +} diff --git a/language-change/asset-meta/miniAsset.go b/language-change/asset-meta/miniAsset.go new file mode 100644 index 0000000..5b67910 --- /dev/null +++ b/language-change/asset-meta/miniAsset.go @@ -0,0 +1,32 @@ +package assetMeta + +import ( + "encoding/binary" + "io" +) + +type MiniAsset struct { + RevisionID uint32 + DesignIndexHash ByteHash16 +} + +func MiniAssetFromBytes(r io.ReadSeeker) (*MiniAsset, error) { + if _, err := r.Seek(6*4, io.SeekCurrent); err != nil { + return nil, err + } + + var revID uint32 + if err := binary.Read(r, binary.LittleEndian, &revID); err != nil { + return nil, err + } + + hash, err := ByteHash16FromBytes(r) + if err != nil { + return nil, err + } + + return &MiniAsset{ + RevisionID: revID, + DesignIndexHash: hash, + }, nil +} diff --git a/language-change/excel-language/patch.go b/language-change/excel-language/patch.go new file mode 100644 index 0000000..095df84 --- /dev/null +++ b/language-change/excel-language/patch.go @@ -0,0 +1,114 @@ +package excelLanguage + +import ( + "bytes" + "io" + assetMeta "language-change/asset-meta" + "os" + "path/filepath" +) + +type ExcelLanguage struct { + AssetFolder string + ExcelDataEntry *assetMeta.DataEntry + ExcelFileEntry *assetMeta.FileEntry +} + +func NewExcelLanguage(assetFolder string, dataEntry *assetMeta.DataEntry, fileEntry *assetMeta.FileEntry) *ExcelLanguage { + return &ExcelLanguage{ + AssetFolder: assetFolder, + ExcelDataEntry: dataEntry, + ExcelFileEntry: fileEntry, + } +} + +func (a *ExcelLanguage) Unmarshal(rows []LanguageRow) ([]byte, error) { + buf := new(bytes.Buffer) + + buf.WriteByte(0) + if err := writeI8Varint(buf, int8(len(rows))); err != nil { + return nil, err + } + + for _, row := range rows { + rowData, err := row.Unmarshal() + if err != nil { + return nil, err + } + if _, err := buf.Write(rowData); err != nil { + return nil, err + } + } + + return buf.Bytes(), nil +} + +func (a *ExcelLanguage) Parse() ([]LanguageRow, error) { + excelPath := filepath.Join(a.AssetFolder, a.ExcelFileEntry.FileByteName+".bytes") + + f, err := os.Open(excelPath) + if err != nil { + return nil, err + } + defer f.Close() + + if _, err := f.Seek(int64(a.ExcelDataEntry.Offset), io.SeekStart); err != nil { + return nil, err + } + + buffer := make([]byte, a.ExcelDataEntry.Size) + if _, err := io.ReadFull(f, buffer); err != nil { + return nil, err + } + + reader := bytes.NewReader(buffer) + + _, _ = reader.ReadByte() // skip first byte + count, err := readI8Varint(reader) + if err != nil { + return nil, err + } + + rows := make([]LanguageRow, 0, count) + for i := 0; i < count; i++ { + bitmask, err := reader.ReadByte() + if err != nil { + return nil, err + } + + row := LanguageRow{} + + if bitmask&(1<<0) != 0 { + s, err := readString(reader) + if err != nil { + return nil, err + } + row.Area = &s + } + if bitmask&(1<<1) != 0 { + t, err := reader.ReadByte() + if err != nil { + return nil, err + } + row.Type = &t + } + if bitmask&(1<<2) != 0 { + arr, err := readStringArray(reader) + if err != nil { + return nil, err + } + row.LanguageList = arr + } + if bitmask&(1<<3) != 0 { + s, err := readString(reader) + if err != nil { + return nil, err + } + row.DefaultLanguage = &s + } + + rows = append(rows, row) + } + + return rows, nil +} diff --git a/language-change/excel-language/row.go b/language-change/excel-language/row.go new file mode 100644 index 0000000..51d648f --- /dev/null +++ b/language-change/excel-language/row.go @@ -0,0 +1,81 @@ +package excelLanguage + +import ( + "bytes" + "errors" +) + +type LanguageRow struct { + Area *string + Type *uint8 + LanguageList []string + DefaultLanguage *string +} + +func (r *LanguageRow) Unmarshal() ([]byte, error) { + buf := new(bytes.Buffer) + + var bitmask uint8 + if r.Area != nil { + bitmask |= 1 << 0 + } + if r.Type != nil { + bitmask |= 1 << 1 + } + if len(r.LanguageList) > 0 { + bitmask |= 1 << 2 + } + if r.DefaultLanguage != nil { + bitmask |= 1 << 3 + } + + if err := buf.WriteByte(bitmask); err != nil { + return nil, err + } + + if r.Area != nil { + if err := writeString(buf, *r.Area); err != nil { + return nil, err + } + } + if r.Type != nil { + if err := buf.WriteByte(*r.Type); err != nil { + return nil, err + } + } + if len(r.LanguageList) > 0 { + if err := writeStringArray(buf, r.LanguageList); err != nil { + return nil, err + } + } + if r.DefaultLanguage != nil { + if err := writeString(buf, *r.DefaultLanguage); err != nil { + return nil, err + } + } + + return buf.Bytes(), nil +} + +func writeString(buf *bytes.Buffer, s string) error { + if len(s) > 255 { + return errors.New("string too long") + } + if err := buf.WriteByte(uint8(len(s))); err != nil { + return err + } + _, err := buf.Write([]byte(s)) + return err +} + +func writeStringArray(buf *bytes.Buffer, arr []string) error { + if err := writeI8Varint(buf, int8(len(arr))); err != nil { + return err + } + for _, s := range arr { + if err := writeString(buf, s); err != nil { + return err + } + } + return nil +} diff --git a/language-change/excel-language/utils.go b/language-change/excel-language/utils.go new file mode 100644 index 0000000..e1fd4c6 --- /dev/null +++ b/language-change/excel-language/utils.go @@ -0,0 +1,53 @@ +package excelLanguage + +import ( + "bytes" + "encoding/binary" + "io" +) + +func writeI8Varint(buf *bytes.Buffer, v int8) error { + uv := uint64((uint32(v) << 1) ^ uint32(v>>7)) // zigzag encode + b := make([]byte, binary.MaxVarintLen64) + n := binary.PutUvarint(b, uv) + _, err := buf.Write(b[:n]) + return err +} + +func readI8Varint(r *bytes.Reader) (int, error) { + uv, err := binary.ReadUvarint(r) + if err != nil { + return 0, err + } + // zigzag decode + v := int((uv >> 1) ^ uint64((int64(uv&1)<<63)>>63)) + return v, nil +} + +func readString(r *bytes.Reader) (string, error) { + l, err := r.ReadByte() + if err != nil { + return "", err + } + buf := make([]byte, l) + if _, err := io.ReadFull(r, buf); err != nil { + return "", err + } + return string(buf), nil +} + +func readStringArray(r *bytes.Reader) ([]string, error) { + length, err := readI8Varint(r) + if err != nil { + return nil, err + } + arr := make([]string, 0, length) + for i := 0; i < length; i++ { + s, err := readString(r) + if err != nil { + return nil, err + } + arr = append(arr, s) + } + return arr, nil +} \ No newline at end of file diff --git a/language-change/go.mod b/language-change/go.mod new file mode 100644 index 0000000..9e93a5f --- /dev/null +++ b/language-change/go.mod @@ -0,0 +1,3 @@ +module language-change + +go 1.25.0 diff --git a/language-change/main.go b/language-change/main.go new file mode 100644 index 0000000..6a47041 --- /dev/null +++ b/language-change/main.go @@ -0,0 +1,226 @@ +package main + +import ( + "bufio" + "bytes" + "errors" + "fmt" + assetMeta "language-change/asset-meta" + excelLanguage "language-change/excel-language" + "os" + "path/filepath" + "slices" + "strings" +) + +func isValidLang(lang string) bool { + valid := []string{"en", "jp", "cn", "kr"} + return slices.Contains(valid, lang) +} + +func GetLanguage(assetPath string) (string, string, error) { + typeVersionGame := "cn" + + indexHash, err := assetMeta.GetIndexHash(assetPath) + if err != nil { + return "", "", err + } + + DesignIndex, err := assetMeta.DesignIndexFromBytes(assetPath, indexHash) + if err != nil { + return "", "", err + } + dataEntry, fileEntry, err := DesignIndex.FindDataAndFileByTarget(-515329346) + if err != nil { + return "", "", err + } + allowedLanguage := excelLanguage.NewExcelLanguage(assetPath, &dataEntry, &fileEntry) + languageRows, err := allowedLanguage.Parse() + if err != nil { + return "", "", err + } + + 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 "", "", errors.New("not found language") + } + + return currentTextLang, currentVoiceLang, nil +} + +func SetLanguage(assetPath string, text, voice string) error { + indexHash, err := assetMeta.GetIndexHash(assetPath) + if err != nil { + return err + } + + DesignIndex, err := assetMeta.DesignIndexFromBytes(assetPath, indexHash) + if err != nil { + return err + } + + dataEntry, fileEntry, err := GetAssetData(DesignIndex, "AllowedLanguage") + if err != nil { + return err + } + allowedLanguage := excelLanguage.NewExcelLanguage(assetPath, dataEntry, fileEntry) + languageRows, err := allowedLanguage.Parse() + if err != nil { + return err + } + + 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 err + } + + filePath := filepath.Join(assetPath, fileEntry.FileByteName+".bytes") + + f, err := os.OpenFile(filePath, os.O_RDWR, 0644) + if err != nil { + return err + } + defer f.Close() + + if _, err := f.Seek(int64(dataEntry.Offset), 0); err != nil { + return err + } + if _, err := f.Write(data); err != nil { + return err + } + + 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 err + } + } + return nil +} + +func main() { + reader := bufio.NewReader(os.Stdin) + fmt.Print("Enter source DesignData folder path: ") + source, _ := reader.ReadString('\n') + source = strings.TrimSpace(source) + if source == "" { + fmt.Fprintln(os.Stderr, "no source folder provided") + os.Exit(1) + } + + info, err := os.Stat(source) + if os.IsNotExist(err) { + fmt.Fprintln(os.Stderr, "source folder does not exist") + os.Exit(1) + } + + if err != nil { + fmt.Fprintln(os.Stderr, "error accessing path:", err) + os.Exit(1) + } + + if !info.IsDir() { + fmt.Fprintln(os.Stderr, "path is not a directory") + os.Exit(1) + } + + + currentTextLang, currentVoiceLang, err := GetLanguage(source) + if err != nil { + fmt.Fprintln(os.Stderr, "error getting language:", err) + os.Exit(1) + } + + fmt.Println("Current language:", currentTextLang, currentVoiceLang) + + fmt.Println("Allow languages: en, jp, cn, kr") + + fmt.Print("Enter text language: ") + textLang, _ := reader.ReadString('\n') + textLang = strings.TrimSpace(textLang) + if textLang == "" { + fmt.Fprintln(os.Stderr, "no text language provided") + os.Exit(1) + } + + fmt.Print("Enter voice language: ") + voiceLang, _ := reader.ReadString('\n') + voiceLang = strings.TrimSpace(voiceLang) + if voiceLang == "" { + fmt.Fprintln(os.Stderr, "no voice language provided") + os.Exit(1) + } + + if err := SetLanguage(source, textLang, voiceLang); err != nil { + fmt.Fprintln(os.Stderr, "error setting language:", err) + os.Exit(1) + } + + fmt.Println("Language set successfully.") + +} diff --git a/language-change/ulils.go b/language-change/ulils.go new file mode 100644 index 0000000..67e958f --- /dev/null +++ b/language-change/ulils.go @@ -0,0 +1,39 @@ +package main + +import ( + "errors" + assetMeta "language-change/asset-meta" +) + +func Get32BitHashConst(s string) int32 { + var hash1 int32 = 5381 + var hash2 int32 = hash1 + + bytes := []byte(s) + length := len(bytes) + + for i := 0; i < length; i += 2 { + hash1 = ((hash1 << 5) + hash1) ^ int32(bytes[i]) + if i+1 < length { + hash2 = ((hash2 << 5) + hash2) ^ int32(bytes[i+1]) + } + } + + return int32(uint32(hash1) + uint32(hash2)*1566083941) +} + +func GetAssetData(assets *assetMeta.DesignIndex, name string) (*assetMeta.DataEntry, *assetMeta.FileEntry, error) { + dataEntry, fileEntry, err := assets.FindDataAndFileByTarget(Get32BitHashConst("BakedConfig/ExcelOutput/" + name + ".bytes")) + if err == nil { + return &dataEntry, &fileEntry, nil + } + dataEntry, fileEntry, err = assets.FindDataAndFileByTarget(Get32BitHashConst("BakedConfig/ExcelOutputGameCore/" + name + ".bytes")) + if err == nil { + return &dataEntry, &fileEntry, nil + } + dataEntry, fileEntry, err = assets.FindDataAndFileByTarget(Get32BitHashConst("BakedConfig/ExcelOutputAdventureGame/" + name + ".bytes")) + if err == nil { + return &dataEntry, &fileEntry, nil + } + return nil, nil, errors.New("not found") +}