Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ repositories {
name = 'benwoo1110'
url = uri('https://repo.c0ding.party/multiverse-beta')
}

maven {
name = "viaversion"
url = uri("https://repo.viaversion.com")
}
}

configure(apiDependencies) {
Expand All @@ -41,6 +46,9 @@ dependencies {
shadowed('com.dumptruckman.minecraft:Logging:1.1.1') {
exclude group: 'junit', module: 'junit'
}
shadowed('com.viaversion:nbt:5.1.1') {
exclude group: 'it.unimi.dsi', module: 'fastutil'
}

// Caching
shadowed("com.github.ben-manes.caffeine:caffeine:3.2.2")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.mvplugins.multiverse.inventories.command;

import org.bukkit.Bukkit;
import org.bukkit.generator.WorldInfo;
import org.jetbrains.annotations.NotNull;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.MVCommandCompletions;
Expand All @@ -19,6 +21,7 @@
import org.mvplugins.multiverse.inventories.profile.key.ProfileTypes;
import org.mvplugins.multiverse.inventories.share.Sharables;

import java.io.File;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
Expand Down Expand Up @@ -60,6 +63,7 @@ private MVInvCommandCompletion(
commandCompletions.registerAsyncCompletion("shares", this::suggestShares);
commandCompletions.registerAsyncCompletion("worldGroups", this::suggestWorldGroups);
commandCompletions.registerAsyncCompletion("worldGroupWorlds", this::suggestWorldGroupWorlds);
commandCompletions.registerAsyncCompletion("worldwithplayerdata", this::suggestWorldWithPlayerData);
}

private Collection<String> suggestDataImporters(BukkitCommandCompletionContext context) {
Expand Down Expand Up @@ -176,4 +180,11 @@ private Collection<String> suggestWorldGroupWorlds(BukkitCommandCompletionContex

return addonToCommaSeperated(context.getInput(), worlds);
}

private Collection<String> suggestWorldWithPlayerData(BukkitCommandCompletionContext context) {
return Bukkit.getWorlds().stream()
.filter(world -> new File(world.getWorldFolder(), "playerdata").isDirectory())
.map(WorldInfo::getName)
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package org.mvplugins.multiverse.inventories.commands;

import com.google.common.io.Files;
import org.bukkit.World;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
import org.mvplugins.multiverse.external.acf.commands.annotation.CommandCompletion;
import org.mvplugins.multiverse.external.acf.commands.annotation.CommandPermission;
import org.mvplugins.multiverse.external.acf.commands.annotation.Description;
import org.mvplugins.multiverse.external.acf.commands.annotation.Subcommand;
import org.mvplugins.multiverse.external.acf.commands.annotation.Syntax;
import org.mvplugins.multiverse.external.jakarta.inject.Inject;
import org.mvplugins.multiverse.external.vavr.control.Try;
import org.mvplugins.multiverse.inventories.profile.ProfileDataSource;
import org.mvplugins.multiverse.inventories.profile.data.ProfileData;
import org.mvplugins.multiverse.inventories.profile.key.ContainerType;
import org.mvplugins.multiverse.inventories.profile.key.GlobalProfileKey;
import org.mvplugins.multiverse.inventories.profile.key.ProfileKey;
import org.mvplugins.multiverse.inventories.profile.key.ProfileTypes;
import org.mvplugins.multiverse.inventories.profile.nbt.PlayerDataExtractor;

import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;

@Service
final class PlayerDataImportCommand extends InventoriesCommand {

private final PlayerDataExtractor playerDataExtractor;
private final ProfileDataSource profileDataSource;

@Inject
PlayerDataImportCommand(PlayerDataExtractor playerDataExtractor, ProfileDataSource profileDataSource) {
this.playerDataExtractor = playerDataExtractor;
this.profileDataSource = profileDataSource;
}

@Subcommand("playerdata import")
@Syntax("<world>")
@CommandPermission("multiverse.inventories.importplayerdata")
@CommandCompletion("@worldwithplayerdata")
@Description("Import player data from the world's playerdata folder.")
void onCommand(MVCommandIssuer issuer, World world) {
Path worldPath = world.getWorldFolder().toPath();
File playerDataPath = worldPath.resolve("playerdata").toFile();
if (!playerDataPath.isDirectory()) {
issuer.sendMessage("World's playerdata folder does not exist: " + world.getName());
return;
}

List<CompletableFuture<Void>> playerDataFutures = new ArrayList<>();
File[] files = playerDataPath.listFiles();
if (files == null) {
issuer.sendMessage("No player data files found in the world's playerdata folder: " + world.getName());
return;
}

for (File playerDataFile : files) {
if (!Files.getFileExtension(playerDataFile.getName()).equals("dat")) {
continue;
}
UUID playerUUID = UUID.fromString(Files.getNameWithoutExtension(playerDataFile.getName()));
Try<ProfileData> profileData = playerDataExtractor.extract(playerDataFile.toPath());
playerDataFutures.add(profileDataSource
.getGlobalProfile(GlobalProfileKey.of(playerUUID))
.thenCompose(profileDataSource::updateGlobalProfile)
.thenCompose(ignore -> profileDataSource.getPlayerProfile(
ProfileKey.of(ContainerType.WORLD, world.getName(), ProfileTypes.getDefault(), playerUUID)))
.thenCompose(playerProfile -> {
playerProfile.update(profileData.get());
return profileDataSource.updatePlayerProfile(playerProfile);
}));
}
CompletableFuture.allOf(playerDataFutures.toArray(new CompletableFuture[0]))
.thenRun(() -> issuer.sendMessage("Successfully imported all player data from " + world.getName() + "."));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public sealed class GlobalProfileKey permits ProfileFileKey {
*/
public static GlobalProfileKey of(UUID playerUUID) {
return PlayerNamesMapper.getInstance().getKey(playerUUID)
.getOrElse(() -> new GlobalProfileKey(playerUUID, ""));
.getOrElse(() -> new GlobalProfileKey(playerUUID, playerUUID.toString()));
}

public static GlobalProfileKey of(OfflinePlayer offlinePlayer) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package org.mvplugins.multiverse.inventories.profile.nbt;

import com.dumptruckman.minecraft.util.Logging;
import com.viaversion.nbt.io.NBTIO;
import com.viaversion.nbt.tag.CompoundTag;
import com.viaversion.nbt.tag.ListTag;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.ApiStatus;
import org.jvnet.hk2.annotations.Service;
import org.mvplugins.multiverse.external.vavr.control.Try;
import org.mvplugins.multiverse.inventories.profile.data.ProfileData;
import org.mvplugins.multiverse.inventories.profile.data.ProfileDataSnapshot;
import org.mvplugins.multiverse.inventories.share.Sharables;
import org.mvplugins.multiverse.inventories.util.PlayerStats;

import javax.annotation.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.zip.GZIPOutputStream;

@ApiStatus.AvailableSince("5.2")
@Service
public final class PlayerDataExtractor {

@ApiStatus.AvailableSince("5.2")
public Try<ProfileData> extract(Path path) {
return Try.of(() -> {
if (!path.toFile().exists()) {
Logging.warning("File %s does not exist! %s", path);
throw new IOException();
}
Logging.finest("Extracting %s", path);

CompoundTag playerData = NBTIO
.reader(CompoundTag.class)
.named()
.read(path, true);

int dataVersion = playerData.getInt("DataVersion");
Logging.finest("Data version: %s", dataVersion);
CompoundTag equipment = playerData.getCompoundTag("equipment");
if (equipment == null) {
equipment = new CompoundTag();
}

ProfileData profileData = new ProfileDataSnapshot();

profileData.set(Sharables.ARMOR, new ItemStack[]{
extractItem(equipment.getCompoundTag("feet"), dataVersion),
extractItem(equipment.getCompoundTag("legs"), dataVersion),
extractItem(equipment.getCompoundTag("chest"), dataVersion),
extractItem(equipment.getCompoundTag("head"), dataVersion)
});
// ADVANCEMENTS
// BED_SPAWN
profileData.set(Sharables.ENDER_CHEST, extractItems(
playerData.getListTag("EnderItems", CompoundTag.class),
dataVersion,
PlayerStats.ENDER_CHEST_SIZE
));
profileData.set(Sharables.EXHAUSTION, playerData.getFloat("foodExhaustionLevel"));
profileData.set(Sharables.EXPERIENCE, playerData.getFloat("XpP"));
profileData.set(Sharables.FALL_DISTANCE, (float) playerData.getDouble("fall_distance"));
profileData.set(Sharables.FIRE_TICKS, (int) playerData.getShort("Fire"));
profileData.set(Sharables.FOOD_LEVEL, playerData.getInt("foodLevel"));
// GAME_STATISTICS
profileData.set(Sharables.HEALTH, (double) playerData.getFloat("Health"));
profileData.set(Sharables.INVENTORY, extractItems(
playerData.getListTag("Inventory", CompoundTag.class),
dataVersion,
PlayerStats.INVENTORY_SIZE
));
// LAST_LOCATION
profileData.set(Sharables.LEVEL, playerData.getInt("XpLevel"));
// MAXIMUM_AIR
// MAX_HEALTH
profileData.set(Sharables.OFF_HAND, extractItem(equipment.getCompoundTag("offhand"), dataVersion));
// POTIONS
// RECIPES
profileData.set(Sharables.REMAINING_AIR, (int) playerData.getShort("Air"));
profileData.set(Sharables.SATURATION, playerData.getFloat("foodSaturationLevel"));
profileData.set(Sharables.TOTAL_EXPERIENCE, playerData.getInt("XpTotal"));

return profileData;
}).onFailure(Throwable::printStackTrace);
}

private ItemStack[] extractItems(@Nullable ListTag<CompoundTag> inventoryList, int dataVersion, int inventorySize) throws IOException {
if (inventoryList == null) {
return new ItemStack[inventorySize];
}

ItemStack[] items = new ItemStack[inventorySize];
for (CompoundTag invData : inventoryList) {
int slot = invData.getInt("Slot");
invData.remove("Slot");
items[slot] = extractItem(invData, dataVersion);
}
return items;
}

private @Nullable ItemStack extractItem(@Nullable CompoundTag invData, int dataVersion) throws IOException {
if (invData == null) {
return null;
}

invData.putInt("DataVersion", dataVersion);

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream);
NBTIO.writer().named().write(gzipOutputStream, invData);
gzipOutputStream.close();
byteArrayOutputStream.close();

return Try.of(() -> ItemStack.deserializeBytes(byteArrayOutputStream.toByteArray()))
.onFailure(throwable -> Logging.warning("Failed to deserialize item: %s", throwable.getMessage()))
.getOrNull();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class InjectionTest : TestWithMockBukkit() {

@Test
fun `InventoriesCommand are available as a service`() {
assertEquals(30, serviceLocator.getAllActiveServices(InventoriesCommand::class.java).size)
assertEquals(31, serviceLocator.getAllActiveServices(InventoriesCommand::class.java).size)
}

@Test
Expand Down