diff --git a/examples/playerdataconvert/advancements.go b/examples/playerdataconvert/advancements.go new file mode 100644 index 00000000..6792a603 --- /dev/null +++ b/examples/playerdataconvert/advancements.go @@ -0,0 +1,58 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/google/uuid" +) + +func readAdvancements(dir string, m map[uuid.UUID]UserCache) { + entries, err := os.ReadDir(dir) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to open playerdata folder: %v", err) + return + } + for _, files := range entries { + filename := files.Name() + if ext := filepath.Ext(filename); ext != ".json" { + fmt.Fprintf(os.Stderr, "Unkown file type: %s\n", ext) + continue + } + + // Parse old UUID from filename + oldID, err := uuid.Parse(strings.TrimSuffix(filename, ".json")) + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to parse filename as uuid: %v\n", err) + continue + } + + if ver := oldID.Version(); ver != 3 { // v3 is for offline players + fmt.Printf("Ignoring UUID: %v version: %d\n", oldID, ver) + continue + } + + newUser, ok := m[oldID] + if !ok { + fmt.Printf("Skip user: %v\n", oldID) + continue + } + + content, err := os.ReadFile(filepath.Join(dir, filename)) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to read json file: %v\n", err) + continue + } + + newFile := newUser.UUID.String() + ".json" + err = os.WriteFile(filepath.Join(dir, newFile), content, 0o666) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to write json file: %v\n", err) + continue + } + + fmt.Printf("Converted advancement file: %s\n", newFile) + } +} diff --git a/examples/playerdataconvert/main.go b/examples/playerdataconvert/main.go index 1d7f12df..ce16440f 100644 --- a/examples/playerdataconvert/main.go +++ b/examples/playerdataconvert/main.go @@ -9,191 +9,82 @@ package main import ( - "compress/gzip" - "encoding/binary" "encoding/json" "flag" "fmt" - "io/fs" - "net/http" "os" "path/filepath" - "github.com/Tnze/go-mc/nbt" - "github.com/Tnze/go-mc/nbt/dynbt" "github.com/google/uuid" ) -var savePath = flag.String("save", "The save folder with \"usercache.json\" file inside", "") +var ( + savePath = flag.String("save", ".", "The save folder with \"usercache.json\" file inside") + convertPlayerData = flag.Bool("cplayerdata", true, "Whether convert files at /world/playerdata/*.dat") + convertEntities = flag.Bool("centities", true, "Whether convert pets' Owner at /world/entities/*") + convertAdvancements = flag.Bool("cadvancements", true, "Whether convert advancements at /world/advancements/*") +) func main() { flag.Parse() - save, err := os.ReadDir(*savePath) + + usercaches, err := readUsercache(filepath.Join(*savePath, "usercache.json")) if err != nil { - fmt.Fprintf(os.Stderr, "Failed to open dir: %v", err) + fmt.Fprintf(os.Stderr, "Failed to parse usercache file: %v\n", err) return } + fmt.Printf("Successfully reading usercache\n") + m := mappingUsers(usercaches) - var usercache fs.DirEntry - for i := range save { - name := save[i].Name() - if name == "usercache.json" && !save[i].IsDir() { - usercache = save[i] - } + if *convertPlayerData { + readPlayerdata(filepath.Join(*savePath, "world", "playerdata"), m) } - if usercache == nil { - fmt.Fprintf(os.Stderr, "usercache.json not found") - return + + if *convertEntities { + readEntities(filepath.Join(*savePath, "world", "entities"), m) + } + + if *convertAdvancements { + readAdvancements(filepath.Join(*savePath, "world", "advancements"), m) } - usercaches := readUsercache(filepath.Join(*savePath, usercache.Name())) - fmt.Printf("Successfully reading usercache\n") - readPlayerdata(filepath.Join(*savePath, "world", "playerdata"), usercaches) } type UserCache struct { - Name string `json:"name"` - UUID string `json:"uuid"` - ExpiresOn string `json:"expiresOn"` + Name string `json:"name"` + UUID uuid.UUID `json:"uuid"` } -func readUsercache(path string) []UserCache { +func readUsercache(path string) ([]UserCache, error) { data, err := os.ReadFile(path) if err != nil { - fmt.Fprintf(os.Stderr, "Failed to read usercache file: %v\n", err) - return nil + return nil, err } var usercache []UserCache err = json.Unmarshal(data, &usercache) if err != nil { - fmt.Fprintf(os.Stderr, "Failed to parse usercache file: %v\n", err) - return nil + return nil, err } - return usercache + return usercache, nil } -func readPlayerdata(dir string, users []UserCache) { +func mappingUsers(users []UserCache) map[uuid.UUID]UserCache { + m := make(map[uuid.UUID]UserCache) for _, user := range users { - nbtdata, err := readNbtData(dir, &user) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to read %s's nbt data\n", user.Name) - continue - } + name := user.Name + // // You can add your maps here + // if v, ok := offlineOnlineMaps[name]; ok { + // name = v + // } - // Get old UUID - uuidInts := nbtdata.Get("UUID").IntArray() - uuidBytes, err := intArrayToUUID(uuidInts) + name, id, err := usernameToUUID(name) if err != nil { - fmt.Fprintf(os.Stderr, "Failed to read %s's UUID\n", user.Name) - continue - } - - if ver := uuidBytes.Version(); ver != 3 { // v3 is for offline players - fmt.Printf("Ignoring UUID: %v version: %d\n", uuidBytes, ver) + fmt.Fprintf(os.Stderr, "Unable to fetch username for %s from Mojang server: %v\n", name, err) continue } - // Get new UUID - name, id, err := usernameToUUID(user.Name) - if err != nil { - fmt.Fprintf(os.Stderr, "Unable to fetch username for %s from Mojang server: %v\n", user.Name, err) - continue - } - - fmt.Printf("[%s] %v -> %v\n", name, uuidBytes, id) - - // Update UUID - ints := uuidToIntArray(id) - nbtdata.Set("UUID", dynbt.NewIntArray(ints[:])) - - // Create new .dat file - err = writeNbtData(dir, id.String(), &nbtdata) - if err != nil { - fmt.Fprintf(os.Stderr, "Unable to write %s's .dat file: %v\n", name, err) - continue - } - } -} - -func readNbtData(dir string, user *UserCache) (dynbt.Value, error) { - file, err := os.Open(filepath.Join(dir, user.UUID+".dat")) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to read %s's userdata: %v\n", user.Name, err) - } - defer file.Close() - - r, err := gzip.NewReader(file) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to decompress %s's userdata: %v\n", user.Name, err) - } - - var nbtdata dynbt.Value - _, err = nbt.NewDecoder(r).Decode(&nbtdata) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to parse %s's userdata: %v\n", user.Name, err) - } - return nbtdata, nil -} - -func writeNbtData(dir string, id string, nbtdata *dynbt.Value) error { - newDatFilePath := filepath.Join(dir, id+".dat") - file, err := os.Create(newDatFilePath) - if err != nil { - return err - } - - w := gzip.NewWriter(file) - err = nbt.NewEncoder(w).Encode(&nbtdata, "") - if err != nil { - return err - } - - err = w.Close() - if err != nil { - return err - } - - err = file.Close() - if err != nil { - return err - } - return nil -} - -func usernameToUUID(name string) (string, uuid.UUID, error) { - var id uuid.UUID - resp, err := http.Get("https://api.mojang.com/users/profiles/minecraft/" + name) - if err != nil { - return "", id, err - } - - var body struct { - Name string `json:"name"` - ID string `json:"id"` - } - err = json.NewDecoder(resp.Body).Decode(&body) - if err != nil { - return "", id, err - } - - id, err = uuid.Parse(body.ID) - return body.Name, id, err -} - -func intArrayToUUID(uuidInts []int32) (id uuid.UUID, err error) { - if uuidLen := len(uuidInts); uuidLen != 4 { - err = fmt.Errorf("invalid UUID len: %d * int32", uuidLen) - return - } - for i, v := range uuidInts { - binary.BigEndian.PutUint32(id[i*4:], uint32(v)) - } - return -} - -func uuidToIntArray(id uuid.UUID) (ints [4]int32) { - for i := range ints { - ints[i] = int32(binary.BigEndian.Uint32(id[i*4:])) + fmt.Printf("[%s] %v -> %v\n", name, user.UUID, id) + m[user.UUID] = UserCache{name, id} } - return + return m } diff --git a/examples/playerdataconvert/pets.go b/examples/playerdataconvert/pets.go new file mode 100644 index 00000000..c9c21085 --- /dev/null +++ b/examples/playerdataconvert/pets.go @@ -0,0 +1,129 @@ +package main + +import ( + "bytes" + "compress/gzip" + "compress/zlib" + "errors" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/Tnze/go-mc/nbt" + "github.com/Tnze/go-mc/nbt/dynbt" + "github.com/Tnze/go-mc/save/region" + "github.com/google/uuid" +) + +func readEntities(dir string, m map[uuid.UUID]UserCache) { + entries, err := os.ReadDir(dir) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to read entities dir: %v\n", err) + return + } + for i := range entries { + readEntityMcaFile(filepath.Join(dir, entries[i].Name()), m) + } +} + +func readEntityMcaFile(path string, m map[uuid.UUID]UserCache) { + r, err := region.Open(path) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to open entities region file %s: %v\n", path, err) + return + } + defer r.Close() + + for i := 0; i < 32; i++ { + for j := 0; j < 32; j++ { + if !r.ExistSector(i, j) { + continue + } + + data, err := r.ReadSector(i, j) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to read entities region sector: %v\n", err) + continue + } + + newdata, err := readEntityMcaSector(data, m) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to parse entities region sector: %v\n", err) + continue + } + + if newdata != nil { + err = r.WriteSector(i, j, newdata) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to update region sector data: %v\n", err) + continue + } + } + } + } +} + +func readEntityMcaSector(data []byte, m map[uuid.UUID]UserCache) ([]byte, error) { + var r io.Reader = bytes.NewReader(data[1:]) + var err error + switch data[0] { + default: + return nil, errors.New("unknown compression") + case 1: + r, err = gzip.NewReader(r) + case 2: + r, err = zlib.NewReader(r) + case 3: + } + if err != nil { + return nil, err + } + + var nbtdata dynbt.Value + _, err = nbt.NewDecoder(r).Decode(&nbtdata) + if err != nil { + return nil, err + } + + updated := false + + entities := nbtdata.Get("Entities") + if entities == nil { + return nil, fmt.Errorf("no Entities field in nbt, what happen?") + } + entities2 := entities.List() + for _, entity := range entities2 { + id := entity.Get("id").String() + if owner := entity.Get("Owner"); owner != nil { + owner, _ := intArrayToUUID(owner.IntArray()) + fmt.Printf("Found %s: owner=%s\n", id, owner) + + if owner.Version() != 3 { + continue + } + + if newOwner, ok := m[owner]; ok { + ownerInts := uuidToIntArray(newOwner.UUID) + entity.Set("Owner", dynbt.NewIntArray(ownerInts[:])) + updated = true + } + } + } + + if updated { + var w bytes.Buffer + w.WriteByte(1) + gw := gzip.NewWriter(&w) + + err := nbt.NewEncoder(gw).Encode(&nbtdata, "") + if err != nil { + gw.Close() + return nil, err + } + + err = gw.Close() + return w.Bytes(), err + } + return nil, nil +} diff --git a/examples/playerdataconvert/playerdata.go b/examples/playerdataconvert/playerdata.go new file mode 100644 index 00000000..b0ee4f19 --- /dev/null +++ b/examples/playerdataconvert/playerdata.go @@ -0,0 +1,120 @@ +package main + +import ( + "compress/gzip" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/Tnze/go-mc/nbt" + "github.com/Tnze/go-mc/nbt/dynbt" + "github.com/google/uuid" +) + +func readPlayerdata(dir string, m map[uuid.UUID]UserCache) { + entries, err := os.ReadDir(dir) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to open playerdata folder: %v", err) + return + } + for _, files := range entries { + filename := files.Name() + if ext := filepath.Ext(filename); ext != ".dat" { + fmt.Fprintf(os.Stderr, "Unkown file type: %s\n", ext) + continue + } + + // Parse old UUID from filename + oldID, err := uuid.Parse(strings.TrimSuffix(filename, ".dat")) + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to parse filename as uuid: %v\n", err) + continue + } + + nbtdata, err := readNbtData(filepath.Join(dir, filename)) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to read %s nbt data\n", filename) + continue + } + + // Read old UUID from nbt + uuidInts := nbtdata.Get("UUID").IntArray() + uuidBytes, err := intArrayToUUID(uuidInts) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to read %s UUID\n", filename) + continue + } + + // Does they matche? + if oldID != uuidBytes { + fmt.Fprintf(os.Stderr, "UUID in filename and nbt data don't match, what happend?\n") + } + + if ver := uuidBytes.Version(); ver != 3 { // v3 is for offline players + fmt.Printf("Ignoring UUID: %v version: %d\n", uuidBytes, ver) + continue + } + + newUser, ok := m[oldID] + if !ok { + fmt.Printf("Skip user: %v\n", oldID) + continue + } + + // Update UUID + ints := uuidToIntArray(newUser.UUID) + nbtdata.Set("UUID", dynbt.NewIntArray(ints[:])) + + // Create new .dat file + err = writeNbtData(dir, newUser.UUID.String(), &nbtdata) + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to write %s's .dat file: %v\n", newUser.Name, err) + continue + } + } +} + +func readNbtData(filepath string) (nbtdata dynbt.Value, err error) { + file, err := os.Open(filepath) + if err != nil { + return nbtdata, fmt.Errorf("failed to open userdata: %w", err) + } + defer file.Close() + + r, err := gzip.NewReader(file) + if err != nil { + return nbtdata, fmt.Errorf("failed to decompress userdata: %w", err) + } + + _, err = nbt.NewDecoder(r).Decode(&nbtdata) + if err != nil { + return nbtdata, fmt.Errorf("failed to parse userdata: %w", err) + } + return nbtdata, nil +} + +func writeNbtData(dir string, id string, nbtdata *dynbt.Value) error { + newDatFilePath := filepath.Join(dir, id+".dat") + file, err := os.Create(newDatFilePath) + if err != nil { + return err + } + + w := gzip.NewWriter(file) + err = nbt.NewEncoder(w).Encode(&nbtdata, "") + if err != nil { + return err + } + + err = w.Close() + if err != nil { + return err + } + + err = file.Close() + if err != nil { + return err + } + return nil +} diff --git a/examples/playerdataconvert/utils.go b/examples/playerdataconvert/utils.go new file mode 100644 index 00000000..1da0e454 --- /dev/null +++ b/examples/playerdataconvert/utils.go @@ -0,0 +1,48 @@ +package main + +import ( + "encoding/binary" + "encoding/json" + "fmt" + "net/http" + + "github.com/google/uuid" +) + +func usernameToUUID(name string) (string, uuid.UUID, error) { + var id uuid.UUID + resp, err := http.Get("https://api.mojang.com/users/profiles/minecraft/" + name) + if err != nil { + return "", id, err + } + + var body struct { + Name string `json:"name"` + ID string `json:"id"` + } + err = json.NewDecoder(resp.Body).Decode(&body) + if err != nil { + return "", id, err + } + + id, err = uuid.Parse(body.ID) + return body.Name, id, err +} + +func intArrayToUUID(uuidInts []int32) (id uuid.UUID, err error) { + if uuidLen := len(uuidInts); uuidLen != 4 { + err = fmt.Errorf("invalid UUID len: %d * int32", uuidLen) + return + } + for i, v := range uuidInts { + binary.BigEndian.PutUint32(id[i*4:], uint32(v)) + } + return +} + +func uuidToIntArray(id uuid.UUID) (ints [4]int32) { + for i := range ints { + ints[i] = int32(binary.BigEndian.Uint32(id[i*4:])) + } + return +}