Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8491ad6444 | |||
| f04ffa4bbe | |||
| 4d9310bc35 | |||
| c26ecf70fe | |||
| cf9d499e91 |
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
*.zip
|
||||
*.rar
|
||||
hdiff-any-game/hdiff-any-game.exe
|
||||
ldiff-converter/ldiff-converter.exe
|
||||
language-change/test
|
||||
language-change/language-change.exe
|
||||
.history/
|
||||
@@ -1,4 +1,8 @@
|
||||
# Some-Tool-For-Diff
|
||||
# Some-Tool-Useful
|
||||
|
||||
## HDiff any game
|
||||
## LDiff converter
|
||||
## Language change
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -30,12 +30,10 @@ func safePartialMD5(path string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
size := fi.Size()
|
||||
modTime := fi.ModTime().UnixNano()
|
||||
|
||||
h := md5.New()
|
||||
|
||||
binary.Write(h, binary.LittleEndian, size)
|
||||
binary.Write(h, binary.LittleEndian, modTime)
|
||||
binary.Write(h, binary.LittleEndian, fi.Mode())
|
||||
|
||||
buf := make([]byte, 4096)
|
||||
@@ -99,9 +97,7 @@ func DiffFolders(oldPath, newPath string) (*DiffResult, error) {
|
||||
workers := runtime.NumCPU() * 2
|
||||
|
||||
for i := 0; i < workers; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
wg.Go(func() {
|
||||
for job := range jobs {
|
||||
rel, oldFile, newFile := job[0], job[1], job[2]
|
||||
oldHash, _ := safePartialMD5(oldFile)
|
||||
@@ -113,7 +109,7 @@ func DiffFolders(oldPath, newPath string) (*DiffResult, error) {
|
||||
}
|
||||
bar.Add(1)
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
for rel, oldFile := range oldFiles {
|
||||
@@ -133,6 +129,5 @@ func DiffFolders(oldPath, newPath string) (*DiffResult, error) {
|
||||
close(jobs)
|
||||
wg.Wait()
|
||||
bar.Finish()
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -36,6 +36,5 @@ func CopyNewFiles(newPath string, result *DiffResult) error {
|
||||
bar.Add(1)
|
||||
}
|
||||
bar.Finish()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ module hdiff-any-game
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/amenzhinsky/go-memexec v0.7.1 // indirect
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/schollz/progressbar/v3 v3.18.0 // indirect
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
github.com/amenzhinsky/go-memexec v0.7.1 h1:DVm4cXzklaNWZoTJgZUi/dlXtelhC7QBtX4luKjl1qk=
|
||||
github.com/amenzhinsky/go-memexec v0.7.1/go.mod h1:ApTO9/i2bcii7kvIXi74gum+/zYDzkiOXtuBZoYOKVE=
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
|
||||
Binary file not shown.
@@ -1,19 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func runHdiffz(oldPath, newPath, outDiff string) error {
|
||||
args := []string{"-s-64", "-SD", "-c-zstd-21-24", "-d", oldPath, newPath, outDiff}
|
||||
hdiffzPath, err := filepath.Abs(filepath.Join("bin", "hdiffz.exe"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.Command(hdiffzPath, args...)
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"embed"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -10,48 +10,12 @@ import (
|
||||
)
|
||||
|
||||
//go:embed bin/hdiffz.exe
|
||||
var hdiffz []byte
|
||||
|
||||
//go:embed bin/7za.exe
|
||||
var embeddedFiles embed.FS
|
||||
|
||||
func ensureBinaries() (map[string]string, error) {
|
||||
binDir := "bin"
|
||||
if _, err := os.Stat(binDir); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(binDir, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
files := []string{"hdiffz.exe", "7za.exe"}
|
||||
paths := make(map[string]string)
|
||||
|
||||
for _, f := range files {
|
||||
destPath := filepath.Join(binDir, f)
|
||||
paths[f] = destPath
|
||||
|
||||
if _, err := os.Stat(destPath); os.IsNotExist(err) {
|
||||
data, err := embeddedFiles.ReadFile("bin/" + f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.WriteFile(destPath, data, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return paths, nil
|
||||
}
|
||||
var sevenZip []byte
|
||||
|
||||
func main() {
|
||||
paths, err := ensureBinaries()
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
fmt.Println("Binary ready at:", path)
|
||||
}
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
fmt.Print("Enter OLD game path: ")
|
||||
@@ -82,31 +46,57 @@ func main() {
|
||||
hdiffName += ".zip"
|
||||
}
|
||||
|
||||
fmt.Println("Diffing folders...")
|
||||
result, err := DiffFolders(oldPath, newPath)
|
||||
if err != nil {
|
||||
fmt.Println("Error:", err)
|
||||
return
|
||||
fmt.Fprintln(os.Stderr, "Error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Println("Diffing folders done.")
|
||||
|
||||
hdiffFolderPath := filepath.Join(".", "hdiff")
|
||||
os.MkdirAll(hdiffFolderPath, 0755)
|
||||
if err := CopyNewFiles(newPath, result); err != nil {
|
||||
fmt.Println("Error writing diff:", err)
|
||||
return
|
||||
}
|
||||
if err := MakeHdiffFile(oldPath, newPath, result.Changed); err != nil {
|
||||
fmt.Println("Error writing diff:", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Copying new files...")
|
||||
if err := CopyNewFiles(newPath, result); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error writing diff:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Println("Copying new files done.")
|
||||
|
||||
fmt.Println("Making hdiff files...")
|
||||
if err := MakeHdiffFile(oldPath, newPath, result.Changed); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error writing diff:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Println("Making hdiff files done.")
|
||||
|
||||
fmt.Println("Zipping hdiff files...")
|
||||
if err := ZipWith7za(hdiffFolderPath, hdiffName); err != nil {
|
||||
fmt.Println("Error writing diff:", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := RemoveFolderWithProgress(hdiffFolderPath); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error removing temp dir:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(hdiffName); os.IsNotExist(err) {
|
||||
fmt.Println("File not found, retrying...")
|
||||
if err := ZipWith7za(hdiffFolderPath, hdiffName); err != nil {
|
||||
fmt.Println("Retry failed:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Zipping hdiff files done.")
|
||||
|
||||
fmt.Println("Removing hdiff temp files...")
|
||||
if err := RemoveFolderWithProgress(hdiffFolderPath, "🗑️ Removing hdiff temp files"); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error removing temp dir:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Println("Removing hdiff temp files done.")
|
||||
|
||||
fmt.Println("Done")
|
||||
}
|
||||
|
||||
@@ -16,31 +16,29 @@ type HdiffFile struct {
|
||||
}
|
||||
|
||||
func MakeHdiffFile(oldPath string, newPath string, changedFiles []string) error {
|
||||
delFile, err := os.Create("hdiff/hdifffiles.txt")
|
||||
diffFile, err := os.Create("hdiff/hdifffiles.txt")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer delFile.Close()
|
||||
defer diffFile.Close()
|
||||
|
||||
for _, f := range changedFiles {
|
||||
data, err := json.Marshal(HdiffFile{RemoteName: f})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(delFile, string(data))
|
||||
fmt.Fprintln(diffFile, string(data))
|
||||
}
|
||||
|
||||
bar := progressbar.NewOptions(len(changedFiles),
|
||||
progressbar.OptionSetDescription("Creating HDIFF files"),
|
||||
progressbar.OptionSetDescription("📦 Creating HDiff files"),
|
||||
progressbar.OptionShowCount(),
|
||||
progressbar.OptionSetWidth(30),
|
||||
progressbar.OptionSetPredictTime(true),
|
||||
)
|
||||
|
||||
workers := runtime.NumCPU() / 2
|
||||
if workers < 2 {
|
||||
workers = 2
|
||||
}
|
||||
workers := runtime.NumCPU()
|
||||
|
||||
jobs := make(chan string, len(changedFiles))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
@@ -54,7 +52,7 @@ func MakeHdiffFile(oldPath string, newPath string, changedFiles []string) error
|
||||
fmt.Fprintf(os.Stderr, "failed to create dir: %v\n", err)
|
||||
continue
|
||||
}
|
||||
runHdiffz(oldFile, newFile, hdiffPath)
|
||||
Hdiffz(oldFile, newFile, hdiffPath)
|
||||
bar.Add(1)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -4,11 +4,13 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/amenzhinsky/go-memexec"
|
||||
"github.com/schollz/progressbar/v3"
|
||||
)
|
||||
|
||||
@@ -97,65 +99,85 @@ func ZipWith7za(src, dest string) error {
|
||||
return fmt.Errorf("source folder is empty: %s", src)
|
||||
}
|
||||
|
||||
sevenZipPath, err := filepath.Abs(filepath.Join("bin", "7za.exe"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
destAbs, err := filepath.Abs(filepath.Join(".", dest))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
args := []string{"a", "-tzip", "-mx=1", "-mmt=on", destAbs}
|
||||
args := []string{"a", "-tzip", "-mx=3", "-mmt=on", destAbs}
|
||||
for _, f := range files {
|
||||
args = append(args, f.Name())
|
||||
}
|
||||
|
||||
cmd := exec.Command(sevenZipPath, args...)
|
||||
exe, err := memexec.New(sevenZip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer exe.Close()
|
||||
cmd := exe.Command(args...)
|
||||
cmd.Dir = src
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func RemoveFolderWithProgress(folder string) error {
|
||||
func RemoveFolderWithProgress(folder string, title string) error {
|
||||
var total int
|
||||
filepath.Walk(folder, func(path string, info os.FileInfo, err error) error {
|
||||
if err == nil {
|
||||
if err == nil && !info.IsDir() {
|
||||
total++
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
bar := progressbar.NewOptions(total,
|
||||
progressbar.OptionSetDescription("Removing temp files"),
|
||||
progressbar.OptionSetDescription(title),
|
||||
progressbar.OptionShowCount(),
|
||||
progressbar.OptionSetWidth(30),
|
||||
progressbar.OptionSetPredictTime(true),
|
||||
)
|
||||
|
||||
err := filepath.Walk(folder, func(path string, info os.FileInfo, err error) error {
|
||||
_ = filepath.Walk(folder, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
if err := os.Remove(path); err != nil {
|
||||
return err
|
||||
}
|
||||
bar.Add(1)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.RemoveAll(folder); err != nil {
|
||||
return err
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
var rmErr error
|
||||
for i := 0; i < 10; i++ {
|
||||
rmErr = os.Remove(path)
|
||||
if rmErr == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
}
|
||||
if rmErr != nil {
|
||||
fmt.Printf("⚠️ Could not remove %s: %v\n", path, rmErr)
|
||||
}
|
||||
|
||||
bar.Add(1)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err := os.RemoveAll(folder); err != nil {
|
||||
return fmt.Errorf("failed to remove folder %s: %w", folder, err)
|
||||
}
|
||||
bar.Finish()
|
||||
fmt.Println("\nTemp folder removed")
|
||||
return nil
|
||||
}
|
||||
|
||||
func Hdiffz(oldPath, newPath, outDiff string) error {
|
||||
args := []string{"-s-64", "-SD", "-c-zstd-21-24", "-d", oldPath, newPath, outDiff}
|
||||
exe, err := memexec.New(hdiffz)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer exe.Close()
|
||||
cmd := exe.Command(args...)
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
12
language-change/Makefile
Normal file
12
language-change/Makefile
Normal file
@@ -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)
|
||||
30
language-change/asset-meta/bytehash16.go
Normal file
30
language-change/asset-meta/bytehash16.go
Normal file
@@ -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
|
||||
}
|
||||
28
language-change/asset-meta/dataEntry.go
Normal file
28
language-change/asset-meta/dataEntry.go
Normal file
@@ -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
|
||||
}
|
||||
98
language-change/asset-meta/designIndex.go
Normal file
98
language-change/asset-meta/designIndex.go
Normal file
@@ -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
|
||||
}
|
||||
63
language-change/asset-meta/fileEntry.go
Normal file
63
language-change/asset-meta/fileEntry.go
Normal file
@@ -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
|
||||
}
|
||||
32
language-change/asset-meta/miniAsset.go
Normal file
32
language-change/asset-meta/miniAsset.go
Normal file
@@ -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
|
||||
}
|
||||
114
language-change/excel-language/patch.go
Normal file
114
language-change/excel-language/patch.go
Normal file
@@ -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
|
||||
}
|
||||
81
language-change/excel-language/row.go
Normal file
81
language-change/excel-language/row.go
Normal file
@@ -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
|
||||
}
|
||||
53
language-change/excel-language/utils.go
Normal file
53
language-change/excel-language/utils.go
Normal file
@@ -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
|
||||
}
|
||||
3
language-change/go.mod
Normal file
3
language-change/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module language-change
|
||||
|
||||
go 1.25.0
|
||||
226
language-change/main.go
Normal file
226
language-change/main.go
Normal file
@@ -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.")
|
||||
|
||||
}
|
||||
39
language-change/ulils.go
Normal file
39
language-change/ulils.go
Normal file
@@ -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")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
APP_NAME = ldff-converter.exe
|
||||
APP_NAME = ldiff-converter.exe
|
||||
|
||||
all: build
|
||||
|
||||
|
||||
BIN
ldiff-converter/bin/7za.exe
Normal file
BIN
ldiff-converter/bin/7za.exe
Normal file
Binary file not shown.
@@ -3,7 +3,16 @@ module ldiff-converter
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/amenzhinsky/go-memexec v0.7.1
|
||||
github.com/klauspost/compress v1.18.0
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b
|
||||
google.golang.org/protobuf v1.36.8
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/schollz/progressbar/v3 v3.18.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/term v0.28.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
github.com/amenzhinsky/go-memexec v0.7.1 h1:DVm4cXzklaNWZoTJgZUi/dlXtelhC7QBtX4luKjl1qk=
|
||||
github.com/amenzhinsky/go-memexec v0.7.1/go.mod h1:ApTO9/i2bcii7kvIXi74gum+/zYDzkiOXtuBZoYOKVE=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA=
|
||||
github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
||||
golang.org/x/sys v0.0.0-20210903071746-97244b99971b h1:3Dq0eVHn0uaQJmPO+/aYPI/fRMqdrVDbu7MQcku54gg=
|
||||
golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
|
||||
Binary file not shown.
@@ -1,15 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"ldiff-converter/pb"
|
||||
"fmt"
|
||||
"io"
|
||||
"ldiff-converter/pb"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"golang.org/x/exp/mmap"
|
||||
)
|
||||
|
||||
|
||||
func LDiffFile(data *pb.AssetManifest, assetName string, assetSize int64, ldiffsDir, outputDir string) error {
|
||||
path := filepath.Join(ldiffsDir, data.ChunkFileName)
|
||||
|
||||
@@ -17,15 +17,25 @@ func LDiffFile(data *pb.AssetManifest, assetName string, assetSize int64, ldiffs
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s does not exist: %w", path, err)
|
||||
}
|
||||
|
||||
fileSize := info.Size()
|
||||
|
||||
var buffer []byte
|
||||
|
||||
if fileSize > 10*1024*1024 && data.HdiffFileSize > 1*1024*1024 {
|
||||
// mmap for large files using x/exp/mmap
|
||||
// ưu tiên mmap cho file lớn
|
||||
reader, err := mmap.Open(path)
|
||||
if err != nil {
|
||||
// fallback to buffered read
|
||||
if err == nil {
|
||||
defer reader.Close()
|
||||
buffer = make([]byte, data.HdiffFileSize)
|
||||
n, err := reader.ReadAt(buffer, data.HdiffFileInChunkOffset)
|
||||
if err != nil && err != io.EOF {
|
||||
return fmt.Errorf("error reading mmap data: %w", err)
|
||||
}
|
||||
if int64(n) < data.HdiffFileSize {
|
||||
return fmt.Errorf("expected %d bytes, but read %d bytes", data.HdiffFileSize, n)
|
||||
}
|
||||
} else {
|
||||
// fallback sang buffered read
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error opening file %s: %w", path, err)
|
||||
@@ -35,22 +45,14 @@ func LDiffFile(data *pb.AssetManifest, assetName string, assetSize int64, ldiffs
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
defer reader.Close()
|
||||
buffer = make([]byte, data.HdiffFileSize)
|
||||
_, err := reader.ReadAt(buffer, data.HdiffFileInChunkOffset)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading mmap data: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// small files, buffered read
|
||||
// file nhỏ, dùng buffered read
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error opening file %s: %w", path, err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
buffer, err = ReadBuffer(file, data.HdiffFileInChunkOffset, data.HdiffFileSize)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -58,7 +60,7 @@ func LDiffFile(data *pb.AssetManifest, assetName string, assetSize int64, ldiffs
|
||||
}
|
||||
|
||||
extension := ""
|
||||
if data.OriginalFileSize != 0 || assetSize != data.HdiffFileSize {
|
||||
if data.OriginalFileSize > 0 && assetSize != data.HdiffFileSize {
|
||||
extension = ".hdiff"
|
||||
}
|
||||
assetPath := filepath.Join(outputDir, assetName+extension)
|
||||
@@ -76,19 +78,16 @@ func LDiffFile(data *pb.AssetManifest, assetName string, assetSize int64, ldiffs
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
func ReadBuffer(file *os.File, offset int64, size int64) ([]byte, error) {
|
||||
buffer := make([]byte, size)
|
||||
|
||||
n, err := file.ReadAt(buffer, offset)
|
||||
if err != nil {
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, fmt.Errorf("error reading data: %w", err)
|
||||
}
|
||||
if int64(n) != size {
|
||||
if int64(n) < size {
|
||||
return nil, fmt.Errorf("expected %d bytes, but read %d bytes", size, n)
|
||||
}
|
||||
|
||||
return buffer, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,18 +2,23 @@ package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"ldiff-converter/pb"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/schollz/progressbar/v3"
|
||||
)
|
||||
|
||||
func main() {
|
||||
//go:embed bin/7za.exe
|
||||
var sevenZip []byte
|
||||
|
||||
func main() {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fmt.Print("Enter zip ldiff path: ")
|
||||
fmt.Print("Enter ldiff path: ")
|
||||
ldiff, _ := reader.ReadString('\n')
|
||||
ldiff = strings.TrimSpace(ldiff)
|
||||
if ldiff == "" {
|
||||
@@ -28,6 +33,9 @@ func main() {
|
||||
fmt.Fprintln(os.Stderr, "no hdiff output provided")
|
||||
os.Exit(1)
|
||||
}
|
||||
if !strings.HasSuffix(strings.ToLower(hdiff), ".zip") {
|
||||
hdiff += ".zip"
|
||||
}
|
||||
|
||||
tmpFolderPath := filepath.Join(".", "temp")
|
||||
if err := os.MkdirAll(tmpFolderPath, 0755); err != nil {
|
||||
@@ -35,20 +43,24 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := Unzip(ldiff, tmpFolderPath); err != nil {
|
||||
fmt.Println("Unzipping ldiff...")
|
||||
if err := UnzipWith7za(ldiff, tmpFolderPath); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("Unzipping ldiff done.")
|
||||
|
||||
ldiffPath := filepath.Join(tmpFolderPath, "ldiff")
|
||||
manifestPath := filepath.Join(tmpFolderPath, "manifest")
|
||||
hdiffFolderPath := filepath.Join(".", "hdiff")
|
||||
|
||||
fmt.Println("Loading manifest proto...")
|
||||
manifestProto, err := LoadManifestProto(manifestPath)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error loading manifest proto:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("Loading manifest proto done.")
|
||||
|
||||
ldiffEntries, err := os.ReadDir(ldiffPath)
|
||||
if err != nil {
|
||||
@@ -56,6 +68,13 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
bar := progressbar.NewOptions(len(ldiffEntries),
|
||||
progressbar.OptionSetDescription("📦 Converting ldiff files"),
|
||||
progressbar.OptionShowCount(),
|
||||
progressbar.OptionSetWidth(30),
|
||||
progressbar.OptionSetPredictTime(true),
|
||||
)
|
||||
fmt.Println("Converting ldiff files...")
|
||||
for _, ldiffEntry := range ldiffEntries {
|
||||
assetName := ldiffEntry.Name()
|
||||
var matchingAssets []struct {
|
||||
@@ -77,7 +96,7 @@ func main() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bar.Add(1)
|
||||
for _, ma := range matchingAssets {
|
||||
err := LDiffFile(ma.Asset, ma.AssetName, ma.AssetSize, ldiffPath, hdiffFolderPath)
|
||||
if err != nil {
|
||||
@@ -85,12 +104,15 @@ func main() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bar.Finish()
|
||||
fmt.Println()
|
||||
fmt.Println("Converting ldiff files done.")
|
||||
diffMapNames := make([]string, len(ldiffEntries))
|
||||
for i, e := range ldiffEntries {
|
||||
diffMapNames[i] = e.Name()
|
||||
}
|
||||
|
||||
fmt.Println("Making diff map...")
|
||||
diffMapList, err := MakeDiffMap(manifestProto, diffMapNames)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error making diff map:", err)
|
||||
@@ -107,22 +129,39 @@ func main() {
|
||||
fmt.Fprintln(os.Stderr, "error writing diff map:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("Making diff map done.")
|
||||
|
||||
if err := os.RemoveAll(tmpFolderPath); err != nil {
|
||||
fmt.Println("Removing temp ldiff files...")
|
||||
if err := RemoveFolderWithProgress(tmpFolderPath, "🗑️ Removing temp ldiff files"); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error removing temp dir:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Println("Removing temp ldiff files done.")
|
||||
|
||||
if err := Zip(hdiffFolderPath, hdiff); err != nil {
|
||||
fmt.Println("Zipping hdiff files...")
|
||||
if err := ZipWith7za(hdiffFolderPath, hdiff); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error zip hdiff:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(hdiffFolderPath); err != nil {
|
||||
if _, err := os.Stat(hdiff); os.IsNotExist(err) {
|
||||
fmt.Println("File not found, retrying...")
|
||||
if err := ZipWith7za(hdiffFolderPath, hdiff); err != nil {
|
||||
fmt.Println("Retry failed:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
fmt.Println("Zipping hdiff files done.")
|
||||
|
||||
fmt.Println("Removing hdiff temp files...")
|
||||
if err := RemoveFolderWithProgress(hdiffFolderPath, "🗑️ Removing hdiff temp files"); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error removing temp dir:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Println("Removing hdiff temp files done.")
|
||||
|
||||
fmt.Println("done!")
|
||||
fmt.Println("Done!")
|
||||
|
||||
}
|
||||
|
||||
@@ -1,112 +1,90 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"io"
|
||||
"ldiff-converter/pb"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/amenzhinsky/go-memexec"
|
||||
"github.com/schollz/progressbar/v3"
|
||||
)
|
||||
|
||||
func Unzip(src, dest string) error {
|
||||
r, err := zip.OpenReader(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
for _, f := range r.File {
|
||||
fpath := filepath.Join(dest, f.Name)
|
||||
if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
|
||||
return fmt.Errorf("illegal file path: %s", fpath)
|
||||
func UnzipWith7za(src, dest string) error {
|
||||
if _, err := os.Stat(src); os.IsNotExist(err) {
|
||||
return fmt.Errorf("source file does not exist: %s", src)
|
||||
}
|
||||
|
||||
if f.FileInfo().IsDir() {
|
||||
if err := os.MkdirAll(fpath, f.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(fpath), 0755); err != nil {
|
||||
if err := os.MkdirAll(dest, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rc, err := f.Open()
|
||||
destAbs, err := filepath.Abs(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
exe, err := memexec.New(sevenZip)
|
||||
if err != nil {
|
||||
rc.Close()
|
||||
return err
|
||||
}
|
||||
defer exe.Close()
|
||||
|
||||
_, err = io.Copy(outFile, rc)
|
||||
rc.Close()
|
||||
outFile.Close()
|
||||
if err != nil {
|
||||
args := []string{"x", "-y", "-o" + destAbs, src}
|
||||
|
||||
cmd := exe.Command(args...)
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Zip(src, dest string) error {
|
||||
zipFile, err := os.Create(dest)
|
||||
if err != nil {
|
||||
func ZipWith7za(src, dest string) error {
|
||||
if _, err := os.Stat(src); os.IsNotExist(err) {
|
||||
return fmt.Errorf("source folder does not exist: %s", src)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
defer zipFile.Close()
|
||||
|
||||
zw := zip.NewWriter(zipFile)
|
||||
defer zw.Close()
|
||||
|
||||
err = filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
|
||||
files, err := os.ReadDir(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(src, path)
|
||||
if len(files) == 0 {
|
||||
return fmt.Errorf("source folder is empty: %s", src)
|
||||
}
|
||||
|
||||
destAbs, err := filepath.Abs(filepath.Join(".", dest))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
if relPath == "." {
|
||||
args := []string{"a", "-tzip", "-mx=1", "-mmt=on", destAbs}
|
||||
for _, f := range files {
|
||||
args = append(args, f.Name())
|
||||
}
|
||||
|
||||
exe, err := memexec.New(sevenZip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer exe.Close()
|
||||
cmd := exe.Command(args...)
|
||||
cmd.Dir = src
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
_, err := zw.Create(relPath + "/")
|
||||
return err
|
||||
}
|
||||
|
||||
fh, err := zip.FileInfoHeader(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fh.Name = relPath
|
||||
fh.Method = zip.Deflate
|
||||
|
||||
writer, err := zw.CreateHeader(fh)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(writer, file)
|
||||
return err
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func MakeDiffMap(manifest *pb.ManifestProto, chunkNames []string) ([]*HDiffData, error) {
|
||||
var hdiffFiles []*HDiffData
|
||||
|
||||
@@ -140,3 +118,51 @@ func MakeDiffMap(manifest *pb.ManifestProto, chunkNames []string) ([]*HDiffData,
|
||||
|
||||
return hdiffFiles, nil
|
||||
}
|
||||
|
||||
func RemoveFolderWithProgress(folder string, title string) error {
|
||||
var total int
|
||||
filepath.Walk(folder, func(path string, info os.FileInfo, err error) error {
|
||||
if err == nil && !info.IsDir() {
|
||||
total++
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
bar := progressbar.NewOptions(total,
|
||||
progressbar.OptionSetDescription(title),
|
||||
progressbar.OptionShowCount(),
|
||||
progressbar.OptionSetWidth(30),
|
||||
progressbar.OptionSetPredictTime(true),
|
||||
)
|
||||
|
||||
_ = filepath.Walk(folder, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Retry tối đa 10 lần khi xóa
|
||||
var rmErr error
|
||||
for i := 0; i < 10; i++ {
|
||||
rmErr = os.Remove(path)
|
||||
if rmErr == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
}
|
||||
if rmErr != nil {
|
||||
fmt.Printf("⚠️ Could not remove %s: %v\n", path, rmErr)
|
||||
}
|
||||
|
||||
bar.Add(1)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err := os.RemoveAll(folder); err != nil {
|
||||
return fmt.Errorf("failed to remove folder %s: %w", folder, err)
|
||||
}
|
||||
bar.Finish()
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user