Skip to content

Commit 99e6dd7

Browse files
committed
Add support for importing vanilla playerdata inventories
1 parent a114094 commit 99e6dd7

4 files changed

Lines changed: 201 additions & 1 deletion

File tree

build.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ repositories {
2121
name = 'benwoo1110'
2222
url = uri('https://repo.c0ding.party/multiverse-beta')
2323
}
24+
25+
maven {
26+
name = "viaversion"
27+
url = uri("https://repo.viaversion.com")
28+
}
2429
}
2530

2631
configure(apiDependencies) {
@@ -46,6 +51,7 @@ dependencies {
4651
shadowed('com.dumptruckman.minecraft:Logging:1.1.1') {
4752
exclude group: 'junit', module: 'junit'
4853
}
54+
shadowed 'com.viaversion:nbt:5.1.1'
4955

5056
// Caching
5157
shadowed("com.github.ben-manes.caffeine:caffeine:3.2.0")
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package org.mvplugins.multiverse.inventories.commands;
2+
3+
import com.google.common.io.Files;
4+
import org.bukkit.Bukkit;
5+
import org.jvnet.hk2.annotations.Service;
6+
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
7+
import org.mvplugins.multiverse.external.acf.commands.annotation.CommandCompletion;
8+
import org.mvplugins.multiverse.external.acf.commands.annotation.CommandPermission;
9+
import org.mvplugins.multiverse.external.acf.commands.annotation.Description;
10+
import org.mvplugins.multiverse.external.acf.commands.annotation.Subcommand;
11+
import org.mvplugins.multiverse.external.acf.commands.annotation.Syntax;
12+
import org.mvplugins.multiverse.external.jakarta.inject.Inject;
13+
import org.mvplugins.multiverse.external.vavr.control.Try;
14+
import org.mvplugins.multiverse.inventories.profile.ProfileDataSource;
15+
import org.mvplugins.multiverse.inventories.profile.data.ProfileData;
16+
import org.mvplugins.multiverse.inventories.profile.key.ContainerType;
17+
import org.mvplugins.multiverse.inventories.profile.key.GlobalProfileKey;
18+
import org.mvplugins.multiverse.inventories.profile.key.ProfileKey;
19+
import org.mvplugins.multiverse.inventories.profile.key.ProfileTypes;
20+
import org.mvplugins.multiverse.inventories.profile.nbt.PlayerDataExtractor;
21+
22+
import java.io.File;
23+
import java.nio.file.Path;
24+
import java.util.ArrayList;
25+
import java.util.List;
26+
import java.util.UUID;
27+
import java.util.concurrent.CompletableFuture;
28+
29+
@Service
30+
final class PlayerDataImportCommand extends InventoriesCommand {
31+
32+
private final PlayerDataExtractor playerDataExtractor;
33+
private final ProfileDataSource profileDataSource;
34+
35+
@Inject
36+
PlayerDataImportCommand(PlayerDataExtractor playerDataExtractor, ProfileDataSource profileDataSource) {
37+
this.playerDataExtractor = playerDataExtractor;
38+
this.profileDataSource = profileDataSource;
39+
}
40+
41+
@Subcommand("playerdata import")
42+
@Syntax("<world>")
43+
@CommandPermission("multiverse.inventories.importplayerdata")
44+
@CommandCompletion("")
45+
@Description("Import player data from the world's playerdata folder.")
46+
void onCommand(MVCommandIssuer issuer, String world) {
47+
Path worldPath = Bukkit.getWorldContainer().toPath().resolve(world);
48+
File playerDataPath = worldPath.resolve("playerdata").toFile();
49+
if (!playerDataPath.isDirectory()) {
50+
issuer.sendMessage("World's playerdata folder does not exist: " + world);
51+
return;
52+
}
53+
54+
List<CompletableFuture<Void>> playerDataFutures = new ArrayList<>();
55+
for (File playerDataFile : playerDataPath.listFiles()) {
56+
if (!Files.getFileExtension(playerDataFile.getName()).equals("dat")) {
57+
continue;
58+
}
59+
UUID playerUUID = UUID.fromString(Files.getNameWithoutExtension(playerDataFile.getName()));
60+
Try<ProfileData> profileData = playerDataExtractor.extract(playerDataFile.toPath());
61+
playerDataFutures.add(profileDataSource
62+
.getGlobalProfile(GlobalProfileKey.of(playerUUID))
63+
.thenCompose(profileDataSource::updateGlobalProfile)
64+
.thenCompose(ignore -> profileDataSource.getPlayerProfile(
65+
ProfileKey.of(ContainerType.WORLD, world, ProfileTypes.getDefault(), playerUUID)))
66+
.thenCompose(playerProfile -> {
67+
playerProfile.update(profileData.get());
68+
return profileDataSource.updatePlayerProfile(playerProfile);
69+
}));
70+
}
71+
CompletableFuture.allOf(playerDataFutures.toArray(new CompletableFuture[0]))
72+
.thenRun(() -> issuer.sendMessage("Successfully imported all player data from " + world + "."));
73+
}
74+
}

src/main/java/org/mvplugins/multiverse/inventories/profile/key/GlobalProfileKey.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public sealed class GlobalProfileKey permits ProfileFileKey {
2020
*/
2121
public static GlobalProfileKey of(UUID playerUUID) {
2222
return PlayerNamesMapper.getInstance().getKey(playerUUID)
23-
.getOrElse(() -> new GlobalProfileKey(playerUUID, ""));
23+
.getOrElse(() -> new GlobalProfileKey(playerUUID, playerUUID.toString()));
2424
}
2525

2626
public static GlobalProfileKey of(OfflinePlayer offlinePlayer) {
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package org.mvplugins.multiverse.inventories.profile.nbt;
2+
3+
import com.dumptruckman.minecraft.util.Logging;
4+
import com.viaversion.nbt.io.NBTIO;
5+
import com.viaversion.nbt.tag.CompoundTag;
6+
import com.viaversion.nbt.tag.ListTag;
7+
import org.bukkit.inventory.ItemStack;
8+
import org.jetbrains.annotations.ApiStatus;
9+
import org.jvnet.hk2.annotations.Service;
10+
import org.mvplugins.multiverse.external.vavr.control.Try;
11+
import org.mvplugins.multiverse.inventories.profile.data.ProfileData;
12+
import org.mvplugins.multiverse.inventories.profile.data.ProfileDataSnapshot;
13+
import org.mvplugins.multiverse.inventories.share.Sharables;
14+
import org.mvplugins.multiverse.inventories.util.PlayerStats;
15+
16+
import javax.annotation.Nullable;
17+
import java.io.ByteArrayOutputStream;
18+
import java.io.IOException;
19+
import java.nio.file.Path;
20+
import java.util.zip.GZIPOutputStream;
21+
22+
@ApiStatus.AvailableSince("5.2")
23+
@Service
24+
public final class PlayerDataExtractor {
25+
26+
@ApiStatus.AvailableSince("5.2")
27+
public Try<ProfileData> extract(Path path) {
28+
return Try.of(() -> {
29+
if (!path.toFile().exists()) {
30+
Logging.warning("File %s does not exist! %s", path);
31+
throw new IOException();
32+
}
33+
Logging.finest("Extracting %s", path);
34+
35+
CompoundTag playerData = NBTIO
36+
.reader(CompoundTag.class)
37+
.named()
38+
.read(path, true);
39+
40+
int dataVersion = playerData.getInt("DataVersion");
41+
Logging.finest("Data version: %s", dataVersion);
42+
CompoundTag equipment = playerData.getCompoundTag("equipment");
43+
if (equipment == null) {
44+
equipment = new CompoundTag();
45+
}
46+
47+
ProfileData profileData = new ProfileDataSnapshot();
48+
49+
profileData.set(Sharables.ARMOR, new ItemStack[]{
50+
extractItem(equipment.getCompoundTag("feet"), dataVersion),
51+
extractItem(equipment.getCompoundTag("legs"), dataVersion),
52+
extractItem(equipment.getCompoundTag("chest"), dataVersion),
53+
extractItem(equipment.getCompoundTag("head"), dataVersion)
54+
});
55+
// ADVANCEMENTS
56+
// BED_SPAWN
57+
profileData.set(Sharables.ENDER_CHEST, extractItems(
58+
playerData.getListTag("EnderItems", CompoundTag.class),
59+
dataVersion,
60+
PlayerStats.ENDER_CHEST_SIZE
61+
));
62+
profileData.set(Sharables.EXHAUSTION, playerData.getFloat("foodExhaustionLevel"));
63+
profileData.set(Sharables.EXPERIENCE, playerData.getFloat("XpP"));
64+
profileData.set(Sharables.FALL_DISTANCE, (float) playerData.getDouble("fall_distance"));
65+
profileData.set(Sharables.FIRE_TICKS, (int) playerData.getShort("Fire"));
66+
profileData.set(Sharables.FOOD_LEVEL, playerData.getInt("foodLevel"));
67+
// GAME_STATISTICS
68+
profileData.set(Sharables.HEALTH, (double) playerData.getFloat("Health"));
69+
profileData.set(Sharables.INVENTORY, extractItems(
70+
playerData.getListTag("Inventory", CompoundTag.class),
71+
dataVersion,
72+
PlayerStats.INVENTORY_SIZE
73+
));
74+
// LAST_LOCATION
75+
profileData.set(Sharables.LEVEL, playerData.getInt("XpLevel"));
76+
// MAXIMUM_AIR
77+
// MAX_HEALTH
78+
profileData.set(Sharables.OFF_HAND, extractItem(equipment.getCompoundTag("offhand"), dataVersion));
79+
// POTIONS
80+
// RECIPES
81+
profileData.set(Sharables.REMAINING_AIR, (int) playerData.getShort("Air"));
82+
profileData.set(Sharables.SATURATION, playerData.getFloat("foodSaturationLevel"));
83+
profileData.set(Sharables.TOTAL_EXPERIENCE, playerData.getInt("XpTotal"));
84+
85+
return profileData;
86+
}).onFailure(Throwable::printStackTrace);
87+
}
88+
89+
private ItemStack[] extractItems(@Nullable ListTag<CompoundTag> inventoryList, int dataVersion, int inventorySize) throws IOException {
90+
if (inventoryList == null) {
91+
return new ItemStack[inventorySize];
92+
}
93+
94+
ItemStack[] items = new ItemStack[inventorySize];
95+
for (CompoundTag invData : inventoryList) {
96+
int slot = invData.getInt("Slot");
97+
invData.remove("Slot");
98+
items[slot] = extractItem(invData, dataVersion);
99+
}
100+
return items;
101+
}
102+
103+
private @Nullable ItemStack extractItem(@Nullable CompoundTag invData, int dataVersion) throws IOException {
104+
if (invData == null) {
105+
return null;
106+
}
107+
108+
invData.putInt("DataVersion", dataVersion);
109+
110+
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
111+
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream);
112+
NBTIO.writer().named().write(gzipOutputStream, invData);
113+
gzipOutputStream.close();
114+
byteArrayOutputStream.close();
115+
116+
return Try.of(() -> ItemStack.deserializeBytes(byteArrayOutputStream.toByteArray()))
117+
.onFailure(throwable -> Logging.warning("Failed to deserialize item: %s", throwable.getMessage()))
118+
.getOrNull();
119+
}
120+
}

0 commit comments

Comments
 (0)