Compare commits

..

1 Commits

Author SHA1 Message Date
InkerBot 29d8f2ecf4 add legacy support 3 months ago

@ -9,7 +9,7 @@ plugins {
id 'java'
}
group 'net.william278'
group 'org.inksnow.husk'
version "$ext.plugin_version${versionMetadata()}"
description "$ext.plugin_description"
defaultTasks 'licenseFormat', 'build'
@ -28,30 +28,12 @@ ext {
publishing {
repositories {
if (System.getenv("RELEASES_MAVEN_USERNAME") != null) {
maven {
name = "william278-releases"
url = "https://repo.william278.net/releases"
name = 'husk-release'
url = findProperty("repository.huskrelease.url")
credentials {
username = System.getenv("RELEASES_MAVEN_USERNAME")
password = System.getenv("RELEASES_MAVEN_PASSWORD")
}
authentication {
basic(BasicAuthentication)
}
}
}
if (System.getenv("SNAPSHOTS_MAVEN_USERNAME") != null) {
maven {
name = "william278-snapshots"
url = "https://repo.william278.net/snapshots"
credentials {
username = System.getenv("SNAPSHOTS_MAVEN_USERNAME")
password = System.getenv("SNAPSHOTS_MAVEN_PASSWORD")
}
authentication {
basic(BasicAuthentication)
}
username = findProperty("repository.huskrelease.username")
password = findProperty("repository.huskrelease.password")
}
}
}
@ -63,23 +45,26 @@ allprojects {
apply plugin: 'java'
compileJava.options.encoding = 'UTF-8'
compileJava.options.release.set 17
compileJava.options.release.set 8
javadoc.options.encoding = 'UTF-8'
javadoc.options.addStringOption('Xdoclint:none', '-quiet')
repositories {
mavenLocal()
mavenCentral()
maven { url 'https://repo.william278.net/releases/' }
maven {
url findProperty('repository.huskpublic.url')
credentials {
username = findProperty('repository.huskpublic.username')
password = findProperty('repository.huskpublic.password')
}
}
maven { url 'https://r.irepo.space/maven/' }
maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' }
maven { url "https://repo.dmulloy2.net/repository/public/" }
maven { url 'https://repo.codemc.io/repository/maven-public/' }
maven { url 'https://repo.minebench.de/' }
maven { url 'https://repo.alessiodp.com/releases/' }
maven { url 'https://jitpack.io' }
maven { url 'https://mvn-repo.arim.space/lesser-gpl3/' }
maven { url 'https://libraries.minecraft.net/' }
}
dependencies {
@ -148,7 +133,7 @@ subprojects {
if (['common'].contains(project.name)) {
publications {
mavenJavaCommon(MavenPublication) {
groupId = 'net.william278.husksync'
groupId = 'org.inksnow.husk.husksync'
artifactId = 'husksync-common'
version = "$rootProject.version"
artifact shadowJar
@ -161,7 +146,7 @@ subprojects {
if (['bukkit'].contains(project.name)) {
publications {
mavenJavaBukkit(MavenPublication) {
groupId = 'net.william278.husksync'
groupId = 'org.inksnow.husk.husksync'
artifactId = 'husksync-bukkit'
version = "$rootProject.version"
artifact shadowJar
@ -174,7 +159,7 @@ subprojects {
if (['fabric'].contains(project.name)) {
publications {
mavenJavaFabric(MavenPublication) {
groupId = 'net.william278.husksync'
groupId = 'org.inksnow.husk.husksync'
artifactId = 'husksync-fabric'
version = "$rootProject.version+${fabric_minecraft_version}"
artifact remapJar

@ -1,15 +1,22 @@
dependencies {
implementation project(path: ':common')
implementation 'net.william278.uniform:uniform-bukkit:1.2.1'
implementation 'org.slf4j:slf4j-api:1.7.36'
implementation 'org.inksnow.husk.uniform:uniform-bukkit:1.2.1-ad25f16'
implementation 'net.william278:mpdbdataconverter:1.0.1'
implementation 'net.william278:hsldataconverter:1.0'
implementation 'org.inksnow.husk:hsldataconverter:1.0'
implementation 'net.william278:mapdataapi:1.0.3'
implementation 'org.bstats:bstats-bukkit:3.0.2'
implementation 'net.kyori:adventure-platform-bukkit:4.3.4'
implementation 'dev.triumphteam:triumph-gui:3.1.10'
implementation 'space.arim.morepaperlib:morepaperlib:0.4.4'
implementation 'de.tr7zw:item-nbt-api:2.13.2'
implementation 'com.google.guava:guava:33.2.1-jre'
implementation ("redis.clients:jedis:$jedis_version") {
exclude group: 'org.slf4j', module: 'slf4j-api'
}
implementation "org.xerial.snappy:snappy-java:$snappy_version"
implementation "org.mongodb:mongodb-driver-sync:$mongodb_driver_version"
compileOnly 'org.spigotmc:spigot-api:1.17.1-R0.1-SNAPSHOT'
compileOnly 'com.github.retrooper.packetevents:spigot:2.3.0'
@ -17,24 +24,27 @@ dependencies {
compileOnly 'org.projectlombok:lombok:1.18.34'
compileOnly 'commons-io:commons-io:2.16.1'
compileOnly 'org.json:json:20240303'
compileOnly 'net.william278:minedown:1.8.2'
compileOnly 'de.exlll:configlib-yaml:4.5.0'
compileOnly 'com.zaxxer:HikariCP:5.1.0'
compileOnly 'net.william278:DesertWell:2.0.4'
compileOnly 'de.themoep:minedown-adventure:1.7.3-SNAPSHOT'
compileOnly 'org.inksnow.husk:configlib-yaml:4.5.4'
compileOnly 'com.zaxxer:HikariCP:4.0.3'
compileOnly 'org.inksnow.husk:desertwell:2.0.5-9962d59:all'
compileOnly 'net.william278:AdvancementAPI:97a9583413'
compileOnly "redis.clients:jedis:$jedis_version"
annotationProcessor 'org.projectlombok:lombok:1.18.34'
}
shadowJar {
dependencies {
exclude(dependency('com.mojang:brigadier'))
}
mergeServiceFiles()
relocate 'org.inksnow.cputil', 'net.william278.husksync.libraries.cputil'
relocate 'org.slf4j', 'net.william278.husksync.libraries.slf4j'
relocate 'org.objectweb.asm', 'net.william278.husksync.libraries.asm'
relocate 'org.apache.commons.io', 'net.william278.husksync.libraries.commons.io'
relocate 'org.apache.commons.text', 'net.william278.husksync.libraries.commons.text'
relocate 'org.apache.commons.lang3', 'net.william278.husksync.libraries.commons.lang3'
relocate 'com.google.gson', 'net.william278.husksync.libraries.gson'
relocate 'com.google.common', 'net.william278.husksync.libraries.guava'
relocate 'com.fatboyindustrial', 'net.william278.husksync.libraries'
relocate 'de.themoep', 'net.william278.husksync.libraries'
relocate 'org.jetbrains', 'net.william278.husksync.libraries'
@ -54,6 +64,4 @@ shadowJar {
relocate 'dev.triumphteam.gui', 'net.william278.husksync.libraries.triumphgui'
relocate 'space.arim.morepaperlib', 'net.william278.husksync.libraries.paperlib'
relocate 'de.tr7zw.changeme.nbtapi', 'net.william278.husksync.libraries.nbtapi'
minimize()
}

@ -57,6 +57,8 @@ import net.william278.husksync.util.BukkitLegacyConverter;
import net.william278.husksync.util.BukkitMapPersister;
import net.william278.husksync.util.BukkitTask;
import net.william278.husksync.util.LegacyConverter;
import net.william278.husksync.util.StringUtil;
import net.william278.husksync.util.ref.RefUtil;
import net.william278.uniform.Uniform;
import net.william278.uniform.bukkit.BukkitUniform;
import org.bstats.bukkit.Metrics;
@ -64,6 +66,9 @@ import org.bukkit.entity.Player;
import org.bukkit.map.MapView;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import org.inksnow.cputil.AuroraCputil;
import org.inksnow.cputil.AuroraUrl;
import org.inksnow.cputil.logger.AuroraLoggerFactory;
import org.jetbrains.annotations.NotNull;
import space.arim.morepaperlib.MorePaperLib;
import space.arim.morepaperlib.scheduling.AsynchronousScheduler;
@ -81,10 +86,21 @@ import java.util.stream.Collectors;
public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.Supplier,
BukkitEventDispatcher, BukkitMapPersister {
static {
AuroraLoggerFactory.instance().nameMapping(it -> {
int split = it.lastIndexOf('.');
if (split != -1) {
return "HuskSync-Aurora " + it.substring(split + 1);
} else {
return "HuskSync-Aurora " + it;
}
});
}
/**
* Metrics ID for <a href="https://bstats.org/plugin/bukkit/HuskSync%20-%20Bukkit/13140">HuskSync on Bukkit</a>.
* Metrics ID for <a href="https://bstats.org/plugin/bukkit/HuskSync-Aurora/23157">HuskSync-Aurora</a>.
*/
private static final int METRICS_ID = 13140;
private static final int METRICS_ID = 23157;
private static final String PLATFORM_TYPE_ID = "bukkit";
private final TreeMap<Identifier, Serializer<? extends Data>> serializers = Maps.newTreeMap(
@ -151,15 +167,21 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
// Prepare serializers
initialize("data serializers", (plugin) -> {
if (RefUtil.bootstrapVirtualMethod("Lorg/bukkit/persistence/PersistentDataHolder;getPersistentDataContainer()Lorg/bukkit/persistence/PersistentDataContainer;") != null) {
registerSerializer(Identifier.PERSISTENT_DATA, new BukkitSerializer.PersistentData(this));
}
registerSerializer(Identifier.INVENTORY, new BukkitSerializer.Inventory(this));
registerSerializer(Identifier.ENDER_CHEST, new BukkitSerializer.EnderChest(this));
registerSerializer(Identifier.ADVANCEMENTS, new BukkitSerializer.Advancements(this));
if (RefUtil.bootstrapStaticFieldGet("Lorg/bukkit/Registry;STATISTIC:Lorg/bukkit/Registry;") != null) {
registerSerializer(Identifier.STATISTICS, new Serializer.Json<>(this, BukkitData.Statistics.class));
}
registerSerializer(Identifier.POTION_EFFECTS, new BukkitSerializer.PotionEffects(this));
registerSerializer(Identifier.GAME_MODE, new Serializer.Json<>(this, BukkitData.GameMode.class));
registerSerializer(Identifier.FLIGHT_STATUS, new Serializer.Json<>(this, BukkitData.FlightStatus.class));
if (RefUtil.bootstrapStaticFieldGet("Lorg/bukkit/Registry;ATTRIBUTE:Lorg/bukkit/Registry;") != null) {
registerSerializer(Identifier.ATTRIBUTES, new Serializer.Json<>(this, BukkitData.Attributes.class));
}
registerSerializer(Identifier.HEALTH, new Serializer.Json<>(this, BukkitData.Health.class));
registerSerializer(Identifier.HUNGER, new Serializer.Json<>(this, BukkitData.Hunger.class));
registerSerializer(Identifier.EXPERIENCE, new Serializer.Json<>(this, BukkitData.Experience.class));
@ -178,11 +200,24 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
// Initialize the database
initialize(getSettings().getDatabase().getType().getDisplayName() + " database connection", (plugin) -> {
this.database = switch (settings.getDatabase().getType()) {
case MYSQL, MARIADB -> new MySqlDatabase(this);
case POSTGRES -> new PostgresDatabase(this);
case MONGO -> new MongoDbDatabase(this);
};
switch (settings.getDatabase().getType()) {
case MYSQL:
case MARIADB: {
this.database = new MySqlDatabase(this);
break;
}
case POSTGRES: {
this.database = new PostgresDatabase(this);
break;
}
case MONGO: {
this.database = new MongoDbDatabase(this);
break;
}
default: {
// do nothing
}
}
this.database.initialize();
});
@ -295,14 +330,14 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
// Register bStats metrics
public void registerMetrics(int metricsId) {
if (!getPluginVersion().getMetadata().isBlank()) {
if (!StringUtil.isBlank(getPluginVersion().getMetadata())) {
return;
}
try {
new Metrics(this, metricsId);
} catch (Throwable e) {
log(Level.WARNING, "Failed to register bStats metrics (%s)".formatted(e.getMessage()));
log(Level.WARNING, "Failed to register bStats metrics (" + e.getMessage() + ")");
}
}

@ -33,6 +33,7 @@ import net.william278.husksync.HuskSync;
import net.william278.husksync.adapter.Adaptable;
import net.william278.husksync.config.Settings.SynchronizationSettings.AttributeSettings;
import net.william278.husksync.user.BukkitUser;
import net.william278.husksync.util.ref.RefUtil;
import org.bukkit.*;
import org.bukkit.advancement.AdvancementProgress;
import org.bukkit.attribute.AttributeInstance;
@ -92,8 +93,8 @@ public abstract class BukkitData implements Data {
stack.hasItemMeta() && Objects.requireNonNull(stack.getItemMeta()).hasEnchants() ?
stack.getItemMeta().getEnchants().keySet().stream()
.map(enchantment -> enchantment.getKey().getKey())
.toList()
: List.of()
.collect(Collectors.toList())
: Collections.emptyList()
) : null)
.toArray(Stack[]::new);
}
@ -118,7 +119,8 @@ public abstract class BukkitData implements Data {
@Override
public boolean equals(Object obj) {
if (obj instanceof BukkitData.Items items) {
if (obj instanceof BukkitData.Items) {
BukkitData.Items items = (BukkitData.Items) obj;
return Arrays.equals(contents, items.getContents());
}
return false;
@ -189,7 +191,7 @@ public abstract class BukkitData implements Data {
@NotNull
public static BukkitData.Items.EnderChest adapt(@NotNull Collection<ItemStack> items) {
return adapt(items.toArray(ItemStack[]::new));
return adapt(items.toArray(new ItemStack[0]));
}
@NotNull
@ -212,7 +214,7 @@ public abstract class BukkitData implements Data {
@NotNull
public static ItemArray adapt(@NotNull Collection<ItemStack> drops) {
return new ItemArray(drops.toArray(ItemStack[]::new));
return new ItemArray(drops.toArray(new ItemStack[0]));
}
@NotNull
@ -238,26 +240,28 @@ public abstract class BukkitData implements Data {
@NotNull
public static BukkitData.PotionEffects from(@NotNull Collection<PotionEffect> sei) {
return new BukkitData.PotionEffects(Lists.newArrayList(sei.stream().filter(e -> !e.isAmbient()).toList()));
return new BukkitData.PotionEffects(Lists.newArrayList(
sei.stream().filter(e -> !e.isAmbient())
.collect(Collectors.toList())
));
}
@NotNull
public static BukkitData.PotionEffects adapt(@NotNull Collection<Effect> effects) {
return from(effects.stream()
.map(effect -> {
final PotionEffectType type = matchEffectType(effect.type());
final PotionEffectType type = matchEffectType(effect.getType());
return type != null ? new PotionEffect(
type,
effect.duration(),
effect.amplifier(),
effect.getDuration(),
effect.getAmplifier(),
effect.isAmbient(),
effect.showParticles(),
effect.hasIcon()
effect.isShowParticles(),
effect.isHasIcon()
) : null;
})
.filter(Objects::nonNull)
.toList());
.collect(Collectors.toList()));
}
@NotNull
@ -290,7 +294,7 @@ public abstract class BukkitData implements Data {
potionEffect.hasParticles(),
potionEffect.hasIcon()
))
.toList();
.collect(Collectors.toList());
}
}
@ -335,16 +339,19 @@ public abstract class BukkitData implements Data {
final Optional<Advancement> record = completed.stream()
.filter(r -> r.getKey().equals(advancement.getKey().toString()))
.findFirst();
if (record.isEmpty()) {
this.setAdvancement(plugin, advancement, player, user, List.of(), progress.getAwardedCriteria());
if (!record.isPresent()) {
this.setAdvancement(plugin, advancement, player, user, Collections.emptyList(), progress.getAwardedCriteria());
return;
}
final Map<String, Date> criteria = record.get().getCompletedCriteria();
this.setAdvancement(
plugin, advancement, player, user,
criteria.keySet().stream().filter(key -> !progress.getAwardedCriteria().contains(key)).toList(),
progress.getAwardedCriteria().stream().filter(key -> !criteria.containsKey(key)).toList()
criteria.keySet().stream()
.filter(key -> !progress.getAwardedCriteria().contains(key))
.collect(Collectors.toList()),
progress.getAwardedCriteria().stream().filter(key -> !criteria.containsKey(key))
.collect(Collectors.toList())
);
}));
}
@ -426,7 +433,7 @@ public abstract class BukkitData implements Data {
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
try {
final org.bukkit.Location location = new org.bukkit.Location(
Bukkit.getWorld(world.name()), x, y, z, yaw, pitch
Bukkit.getWorld(world.getName()), x, y, z, yaw, pitch
);
user.getPlayer().teleport(location);
} catch (Throwable e) {
@ -457,10 +464,25 @@ public abstract class BukkitData implements Data {
items = Maps.newHashMap(), entities = Maps.newHashMap();
Registry.STATISTIC.forEach(id -> {
switch (id.getType()) {
case UNTYPED -> addStatistic(player, id, generic);
case BLOCK -> addMaterialStatistic(player, id, blocks, true);
case ITEM -> addMaterialStatistic(player, id, items, false);
case ENTITY -> addEntityStatistic(player, id, entities);
case UNTYPED: {
addStatistic(player, id, generic);
break;
}
case BLOCK: {
addMaterialStatistic(player, id, blocks, true);
break;
}
case ITEM: {
addMaterialStatistic(player, id, items, false);
break;
}
case ENTITY: {
addEntityStatistic(player, id, entities);
break;
}
default: {
// do nothing
}
}
});
return new BukkitData.Statistics(generic, blocks, items, entities);
@ -527,9 +549,22 @@ public abstract class BukkitData implements Data {
try {
switch (type) {
case UNTYPED -> player.setStatistic(stat, value);
case BLOCK, ITEM -> player.setStatistic(stat, Objects.requireNonNull(matchMaterial(key[0])), value);
case ENTITY -> player.setStatistic(stat, Objects.requireNonNull(matchEntityType(key[0])), value);
case UNTYPED: {
player.setStatistic(stat, value);
break;
}
case BLOCK:
case ITEM: {
player.setStatistic(stat, Objects.requireNonNull(matchMaterial(key[0])), value);
break;
}
case ENTITY: {
player.setStatistic(stat, Objects.requireNonNull(matchEntityType(key[0])), value);
break;
}
default: {
// do nothing
}
}
} catch (Throwable ignored) {
}
@ -591,7 +626,7 @@ public abstract class BukkitData implements Data {
}
public Optional<Attribute> getAttribute(@NotNull org.bukkit.attribute.Attribute id) {
return attributes.stream().filter(attribute -> attribute.name().equals(id.getKey().toString())).findFirst();
return attributes.stream().filter(attribute -> attribute.getName().equals(id.getKey().toString())).findFirst();
}
@SuppressWarnings("unused")
@ -648,10 +683,10 @@ public abstract class BukkitData implements Data {
if (instance == null) {
return;
}
instance.setBaseValue(attribute == null ? instance.getDefaultValue() : attribute.baseValue());
instance.setBaseValue(attribute == null ? instance.getDefaultValue() : attribute.getBaseValue());
instance.getModifiers().forEach(instance::removeModifier);
if (attribute != null) {
attribute.modifiers().stream()
attribute.getModifiers().stream()
.filter(mod -> instance.getModifiers().stream().map(AttributeModifier::getName)
.noneMatch(n -> n.equals(mod.name())))
.distinct()
@ -722,19 +757,19 @@ public abstract class BukkitData implements Data {
}
/**
* @deprecated Use {@link #from(double, double, boolean)} instead
* @deprecated Use {@link #from(double, double, boolean)} instead, since 3.5.4
*/
@NotNull
@Deprecated(since = "3.5.4")
@Deprecated
public static BukkitData.Health from(double health, double scale) {
return from(health, scale, false);
}
/**
* @deprecated Use {@link #from(double, double, boolean)} instead
* @deprecated Use {@link #from(double, double, boolean)} instead, since 3.5
*/
@NotNull
@Deprecated(forRemoval = true, since = "3.5")
@Deprecated
public static BukkitData.Health from(double health, @SuppressWarnings("unused") double max, double scale) {
return from(health, scale, false);
}
@ -757,7 +792,7 @@ public abstract class BukkitData implements Data {
try {
player.setHealth(Math.min(health, player.getMaxHealth()));
} catch (Throwable e) {
plugin.log(Level.WARNING, "Error setting %s's health to %s".formatted(player.getName(), health), e);
plugin.log(Level.WARNING, "Error setting " + player.getName() + "'s health to " + health, e);
}
// Set health scale
@ -766,7 +801,7 @@ public abstract class BukkitData implements Data {
player.setHealthScale(scale);
player.setHealthScaled(isHealthScaled);
} catch (Throwable e) {
plugin.log(Level.WARNING, "Error setting %s's health scale to %s".formatted(player.getName(), scale), e);
plugin.log(Level.WARNING, "Error setting " + player.getName() + "'s health scale to " + scale, e);
}
}
@ -855,7 +890,7 @@ public abstract class BukkitData implements Data {
}
@NotNull
@Deprecated(forRemoval = true, since = "3.5")
@Deprecated
@SuppressWarnings("unused")
public static BukkitData.GameMode from(@NotNull String gameMode, boolean allowFlight, boolean isFlying) {
return new BukkitData.GameMode(gameMode);

@ -33,6 +33,7 @@ import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.HuskSync;
import net.william278.husksync.adapter.Adaptable;
import net.william278.husksync.api.HuskSyncAPI;
import net.william278.husksync.util.ref.RefUtil;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.ApiStatus;
@ -127,13 +128,13 @@ public class BukkitSerializer {
@Nullable
default ItemStack[] getItems(@NotNull ReadWriteNBT tag, @NotNull Version mcVersion) {
if (mcVersion.compareTo(getPlugin().getMinecraftVersion()) < 0) {
return upgradeItemStacks((NBTCompound) tag, mcVersion);
return private$upgradeItemStacks((NBTCompound) tag, mcVersion);
}
return NBT.itemStackArrayFromNBT(tag);
}
@NotNull
private ItemStack @NotNull [] upgradeItemStacks(@NotNull NBTCompound itemsNbt, @NotNull Version mcVersion) {
default ItemStack @NotNull [] private$upgradeItemStacks(@NotNull NBTCompound itemsNbt, @NotNull Version mcVersion) {
final ReadWriteNBTCompoundList items = itemsNbt.getCompoundList("items");
final ItemStack[] itemStacks = new ItemStack[itemsNbt.getInteger("size")];
for (int i = 0; i < items.size(); i++) {
@ -142,7 +143,7 @@ public class BukkitSerializer {
continue;
}
try {
itemStacks[i] = NBT.itemStackFromNBT(upgradeItemData(items.get(i), mcVersion));
itemStacks[i] = NBT.itemStackFromNBT(private$upgradeItemData(items.get(i), mcVersion));
} catch (Throwable e) {
itemStacks[i] = new ItemStack(Material.AIR);
}
@ -151,23 +152,47 @@ public class BukkitSerializer {
}
@NotNull
private ReadWriteNBT upgradeItemData(@NotNull ReadWriteNBT tag, @NotNull Version mcVersion)
default ReadWriteNBT private$upgradeItemData(@NotNull ReadWriteNBT tag, @NotNull Version mcVersion)
throws NoSuchFieldException, IllegalAccessException {
return DataFixerUtil.fixUpItemData(tag, getDataVersion(mcVersion), DataFixerUtil.getCurrentVersion());
}
private int getDataVersion(@NotNull Version mcVersion) {
return switch (mcVersion.toStringWithoutMetadata()) {
case "1.16", "1.16.1", "1.16.2", "1.16.3", "1.16.4", "1.16.5" -> DataFixerUtil.VERSION1_16_5;
case "1.17", "1.17.1" -> DataFixerUtil.VERSION1_17_1;
case "1.18", "1.18.1", "1.18.2" -> DataFixerUtil.VERSION1_18_2;
case "1.19", "1.19.1", "1.19.2" -> DataFixerUtil.VERSION1_19_2;
case "1.20", "1.20.1", "1.20.2" -> DataFixerUtil.VERSION1_20_2;
case "1.20.3", "1.20.4" -> DataFixerUtil.VERSION1_20_4;
case "1.20.5", "1.20.6" -> DataFixerUtil.VERSION1_20_5;
case "1.21" -> DataFixerUtil.VERSION1_21;
default -> DataFixerUtil.getCurrentVersion();
};
return DataFixerUtil.fixUpItemData(tag, private$getDataVersion(mcVersion), DataFixerUtil.getCurrentVersion());
}
default int private$getDataVersion(@NotNull Version mcVersion) {
switch (mcVersion.toStringWithoutMetadata()) {
case "1.12": case "1.12.1": case "1.12.2":
case "1.13": case "1.13.1": case "1.13.2":
case "1.14": case "1.14.1": case "1.14.2": case "1.14.3": case "1.14.4":
case "1.15": case "1.15.1": case "1.15.2": {
return DataFixerUtil.VERSION1_12_2;
}
case "1.16": case "1.16.1": case "1.16.2": case "1.16.3": case "1.16.4": case "1.16.5": {
return DataFixerUtil.VERSION1_16_5;
}
case "1.17": case "1.17.1": {
return DataFixerUtil.VERSION1_17_1;
}
case "1.18": case "1.18.1": case "1.18.2": {
return DataFixerUtil.VERSION1_18_2;
}
case "1.19": case "1.19.1": case "1.19.2": {
return DataFixerUtil.VERSION1_19_2;
}
case "1.20": case "1.20.1": case "1.20.2": {
return DataFixerUtil.VERSION1_20_2;
}
case "1.20.3": case "1.20.4": {
return DataFixerUtil.VERSION1_20_4;
}
case "1.20.5": case "1.20.6": {
return DataFixerUtil.VERSION1_20_5;
}
case "1.21": {
return DataFixerUtil.VERSION1_21;
}
default: {
return DataFixerUtil.getCurrentVersion();
}
}
}
@NotNull
@ -176,7 +201,7 @@ public class BukkitSerializer {
public static class PotionEffects extends BukkitSerializer implements Serializer<BukkitData.PotionEffects> {
private static final TypeToken<List<Data.PotionEffects.Effect>> TYPE = new TypeToken<>() {
private static final TypeToken<List<Data.PotionEffects.Effect>> TYPE = new TypeToken<List<Data.PotionEffects.Effect>>() {
};
public PotionEffects(@NotNull HuskSync plugin) {
@ -200,7 +225,7 @@ public class BukkitSerializer {
public static class Advancements extends BukkitSerializer implements Serializer<BukkitData.Advancements> {
private static final TypeToken<List<Data.Advancements.Advancement>> TYPE = new TypeToken<>() {
private static final TypeToken<List<Data.Advancements.Advancement>> TYPE = new TypeToken<List<Data.Advancements.Advancement>>() {
};
public Advancements(@NotNull HuskSync plugin) {
@ -241,9 +266,9 @@ public class BukkitSerializer {
}
/**
* @deprecated Use {@link Serializer.Json} in the common module instead
* @deprecated Use {@link Serializer.Json} in the common module instead, since 2.6
*/
@Deprecated(since = "2.6")
@Deprecated
public class Json<T extends Data & Adaptable> extends Serializer.Json<T> {
public Json(@NotNull HuskSync plugin, @NotNull Class<T> type) {

@ -32,22 +32,22 @@ public interface BukkitUserDataHolder extends UserDataHolder {
@Override
default Optional<? extends Data> getData(@NotNull Identifier id) {
if (!id.isCustom()) {
return switch (id.getKeyValue()) {
case "inventory" -> getInventory();
case "ender_chest" -> getEnderChest();
case "potion_effects" -> getPotionEffects();
case "advancements" -> getAdvancements();
case "location" -> getLocation();
case "statistics" -> getStatistics();
case "health" -> getHealth();
case "hunger" -> getHunger();
case "attributes" -> getAttributes();
case "experience" -> getExperience();
case "game_mode" -> getGameMode();
case "flight_status" -> getFlightStatus();
case "persistent_data" -> getPersistentData();
default -> throw new IllegalStateException(String.format("Unexpected data type: %s", id));
};
switch (id.getKeyValue()) {
case "inventory": return getInventory();
case "ender_chest": return getEnderChest();
case "potion_effects": return getPotionEffects();
case "advancements": return getAdvancements();
case "location": return getLocation();
case "statistics": return getStatistics();
case "health": return getHealth();
case "hunger": return getHunger();
case "attributes": return getAttributes();
case "experience": return getExperience();
case "game_mode": return getGameMode();
case "flight_status": return getFlightStatus();
case "persistent_data": return getPersistentData();
default: throw new IllegalStateException(String.format("Unexpected data type: %s", id));
}
}
return Optional.ofNullable(getCustomDataStore().get(id));
}
@ -154,9 +154,9 @@ public interface BukkitUserDataHolder extends UserDataHolder {
Player getPlayer();
/**
* @deprecated Use {@link #getPlayer()} instead
* @deprecated Use {@link #getPlayer()} instead, since 3.6
*/
@Deprecated(since = "3.6")
@Deprecated
@NotNull
default Player getBukkitPlayer() {
return getPlayer();

@ -30,7 +30,7 @@ public interface BukkitEventDispatcher extends EventDispatcher {
@Override
default <T extends Event> boolean fireIsCancelled(@NotNull T event) {
Bukkit.getPluginManager().callEvent((org.bukkit.event.Event) event);
return event instanceof Cancellable cancellable && cancellable.isCancelled();
return event instanceof Cancellable && ((Cancellable) event).isCancelled();
}
@NotNull

@ -23,6 +23,7 @@ import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.data.BukkitData;
import net.william278.husksync.user.BukkitUser;
import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.util.MaterialUtil;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
@ -82,7 +83,7 @@ public class BukkitEventListener extends EventListener implements BukkitJoinEven
public void handlePlayerQuit(@NotNull BukkitUser bukkitUser) {
final Player player = bukkitUser.getPlayer();
final ItemStack itemOnCursor = player.getItemOnCursor();
if (!bukkitUser.isLocked() && !itemOnCursor.getType().isAir()) {
if (!bukkitUser.isLocked() && !MaterialUtil.isAir(itemOnCursor.getType())) {
player.setItemOnCursor(null);
player.getWorld().dropItem(player.getLocation(), itemOnCursor);
plugin.debug("Dropped " + itemOnCursor + " for " + player.getName() + " on quit");

@ -60,8 +60,8 @@ public class BukkitLockedEventListener implements LockedHandler, Listener {
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onProjectileLaunch(@NotNull ProjectileLaunchEvent event) {
final Projectile projectile = event.getEntity();
if (projectile.getShooter() instanceof Player player) {
cancelPlayerEvent(player.getUniqueId(), event);
if (projectile.getShooter() instanceof Player) {
cancelPlayerEvent(((Player) projectile.getShooter()).getUniqueId(), event);
}
}
@ -72,8 +72,8 @@ public class BukkitLockedEventListener implements LockedHandler, Listener {
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPickupItem(@NotNull EntityPickupItemEvent event) {
if (event.getEntity() instanceof Player player) {
cancelPlayerEvent(player.getUniqueId(), event);
if (event.getEntity() instanceof Player) {
cancelPlayerEvent(event.getEntity().getUniqueId(), event);
}
}
@ -104,8 +104,8 @@ public class BukkitLockedEventListener implements LockedHandler, Listener {
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onInventoryOpen(@NotNull InventoryOpenEvent event) {
if (event.getPlayer() instanceof Player player) {
cancelPlayerEvent(player.getUniqueId(), event);
if (event.getPlayer() instanceof Player) {
cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
}
}
@ -116,8 +116,8 @@ public class BukkitLockedEventListener implements LockedHandler, Listener {
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerTakeDamage(@NotNull EntityDamageEvent event) {
if (event.getEntity() instanceof Player player) {
cancelPlayerEvent(player.getUniqueId(), event);
if (event.getEntity() instanceof Player) {
cancelPlayerEvent(event.getEntity().getUniqueId(), event);
}
}

@ -25,6 +25,7 @@ import com.github.retrooper.packetevents.event.PacketListenerPriority;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder;
import net.william278.husksync.BukkitHuskSync;
@ -59,7 +60,7 @@ public class BukkitPacketEventsLockedPacketListener extends BukkitLockedEventLis
private static class PlayerPacketAdapter extends PacketListenerAbstract {
private static final Set<PacketType.Play.Client> ALLOWED_PACKETS = Set.of(
private static final Set<PacketType.Play.Client> ALLOWED_PACKETS = ImmutableSet.of(
PacketType.Play.Client.KEEP_ALIVE, PacketType.Play.Client.PONG, PacketType.Play.Client.PLUGIN_MESSAGE, // Connection packets
PacketType.Play.Client.CHAT_MESSAGE, PacketType.Play.Client.CHAT_COMMAND, PacketType.Play.Client.CHAT_SESSION_UPDATE, // Chat / command packets
PacketType.Play.Client.PLAYER_POSITION, PacketType.Play.Client.PLAYER_POSITION_AND_ROTATION, PacketType.Play.Client.PLAYER_ROTATION, // Movement packets
@ -79,10 +80,10 @@ public class BukkitPacketEventsLockedPacketListener extends BukkitLockedEventLis
@Override
public void onPacketReceive(PacketReceiveEvent event) {
if (!(event.getPacketType() instanceof PacketType.Play.Client client)) {
if (!(event.getPacketType() instanceof PacketType.Play.Client)) {
return;
}
if (!CANCEL_PACKETS.contains(client)) {
if (!CANCEL_PACKETS.contains((PacketType.Play.Client) event.getPacketType())) {
return;
}
if (listener.cancelPlayerEvent(event.getUser().getUUID())) {
@ -92,10 +93,10 @@ public class BukkitPacketEventsLockedPacketListener extends BukkitLockedEventLis
@Override
public void onPacketSend(PacketSendEvent event) {
if (!(event.getPacketType() instanceof PacketType.Play.Client client)) {
if (!(event.getPacketType() instanceof PacketType.Play.Client)) {
return;
}
if (!CANCEL_PACKETS.contains(client)) {
if (!CANCEL_PACKETS.contains((PacketType.Play.Client) event.getPacketType())) {
return;
}
if (listener.cancelPlayerEvent(event.getUser().getUUID())) {

@ -24,6 +24,7 @@ import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.events.ListenerPriority;
import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.events.PacketEvent;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import net.william278.husksync.BukkitHuskSync;
import org.jetbrains.annotations.NotNull;
@ -50,7 +51,7 @@ public class BukkitProtocolLibLockedPacketListener extends BukkitLockedEventList
private static class PlayerPacketAdapter extends PacketAdapter {
// Packets we want the player to still be able to send/receiver to/from the server
private static final Set<PacketType> ALLOWED_PACKETS = Set.of(
private static final Set<PacketType> ALLOWED_PACKETS = ImmutableSet.of(
Client.KEEP_ALIVE, Client.PONG, Client.CUSTOM_PAYLOAD, // Connection packets
Client.CHAT_COMMAND, Client.CLIENT_COMMAND, Client.CHAT, Client.CHAT_SESSION_UPDATE, // Chat / command packets
Client.POSITION, Client.POSITION_LOOK, Client.LOOK, // Movement packets

@ -22,6 +22,7 @@ package net.william278.husksync.migrator;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.zaxxer.hikari.HikariDataSource;
import lombok.Value;
import me.william278.husksync.bukkit.data.DataSerializer;
import net.william278.hslmigrator.HSLConverter;
import net.william278.husksync.HuskSync;
@ -43,6 +44,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static net.william278.husksync.config.Settings.DatabaseSettings;
@ -95,14 +97,12 @@ public class LegacyMigrator extends Migrator {
plugin.log(Level.INFO, "Downloading raw data from the legacy database (this might take a while)...");
final List<LegacyData> dataToMigrate = Lists.newArrayList();
try (final Connection connection = connectionPool.getConnection()) {
try (final PreparedStatement statement = connection.prepareStatement("""
SELECT `uuid`, `username`, `inventory`, `ender_chest`, `health`, `max_health`, `health_scale`, `hunger`, `saturation`, `saturation_exhaustion`, `selected_slot`, `status_effects`, `total_experience`, `exp_level`, `exp_progress`, `game_mode`, `statistics`, `is_flying`, `advancements`, `location`
FROM `%source_players_table%`
INNER JOIN `%source_data_table%`
ON `%source_players_table%`.`id` = `%source_data_table%`.`player_id`
WHERE `username` IS NOT NULL;
""".replaceAll(Pattern.quote("%source_players_table%"), sourcePlayersTable)
.replaceAll(Pattern.quote("%source_data_table%"), sourceDataTable))) {
try (final PreparedStatement statement = connection.prepareStatement(
"SELECT `uuid`, `username`, `inventory`, `ender_chest`, `health`, `max_health`, `health_scale`, `hunger`, `saturation`, `saturation_exhaustion`, `selected_slot`, `status_effects`, `total_experience`, `exp_level`, `exp_progress`, `game_mode`, `statistics`, `is_flying`, `advancements`, `location` " +
"FROM `" + sourcePlayersTable + "` " +
"INNER JOIN `" + sourceDataTable + "` " +
"ON `" + sourcePlayersTable + "`.`id` = `" + sourceDataTable + "`.`player_id` " +
"WHERE `username` IS NOT NULL;")) {
try (final ResultSet resultSet = statement.executeQuery()) {
int playersMigrated = 0;
while (resultSet.next()) {
@ -142,11 +142,11 @@ public class LegacyMigrator extends Migrator {
final AtomicInteger playersConverted = new AtomicInteger();
dataToMigrate.forEach(data -> {
final DataSnapshot.Packed convertedData = data.toUserData(hslConverter, plugin);
plugin.getDatabase().ensureUser(data.user());
plugin.getDatabase().ensureUser(data.getUser());
try {
plugin.getDatabase().addSnapshot(data.user(), convertedData);
plugin.getDatabase().addSnapshot(data.getUser(), convertedData);
} catch (Throwable e) {
plugin.log(Level.SEVERE, "Failed to migrate legacy data for " + data.user().getUsername() + ": " + e.getMessage());
plugin.log(Level.SEVERE, "Failed to migrate legacy data for " + data.getUser().getUsername() + ": " + e.getMessage());
return;
}
@ -167,41 +167,53 @@ public class LegacyMigrator extends Migrator {
@Override
public void handleConfigurationCommand(@NotNull String[] args) {
if (args.length == 2) {
if (switch (args[0].toLowerCase(Locale.ENGLISH)) {
case "host" -> {
boolean $yield;
switch (args[0].toLowerCase(Locale.ENGLISH)) {
case "host": {
this.sourceHost = args[1];
yield true;
$yield = true;
break;
}
case "port" -> {
case "port": {
try {
this.sourcePort = Integer.parseInt(args[1]);
yield true;
$yield = true;
} catch (NumberFormatException e) {
yield false;
$yield = false;
}
break;
}
case "username" -> {
case "username": {
this.sourceUsername = args[1];
yield true;
$yield = true;
break;
}
case "password" -> {
case "password": {
this.sourcePassword = args[1];
yield true;
$yield = true;
break;
}
case "database" -> {
case "database": {
this.sourceDatabase = args[1];
yield true;
$yield = true;
break;
}
case "players_table" -> {
case "players_table": {
this.sourcePlayersTable = args[1];
yield true;
$yield = true;
break;
}
case "data_table" -> {
case "data_table": {
this.sourceDataTable = args[1];
yield true;
$yield = true;
break;
}
default -> false;
}) {
default: {
$yield = false;
break;
}
}
if ($yield) {
plugin.log(Level.INFO, getHelpMenu());
plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
obfuscateDataString(args[1]));
@ -229,59 +241,65 @@ public class LegacyMigrator extends Migrator {
@NotNull
@Override
public String getHelpMenu() {
return """
=== HuskSync v1.x --> v3.x Migration Wizard =========
This will migrate all user data from HuskSync v1.x to
HuskSync v3.x's new format. To perform the migration,
please follow the steps below carefully.
[!] Existing data in the database will be wiped. [!]
STEP 1] Please ensure no players are on any servers.
STEP 2] HuskSync will need to connect to the database
used to hold the existing, legacy HuskSync data.
If this is the same database as the one you are
currently using, you probably don't need to change
anything.
Please check that the credentials below are the
correct credentials of the source legacy HuskSync
database.
- host: %source_host%
- port: %source_port%
- username: %source_username%
- password: %source_password%
- database: %source_database%
- players_table: %source_players_table%
- data_table: %source_data_table%
If any of these are not correct, please correct them
using the command:
"husksync migrate legacy set <parameter> <value>"
(e.g.: "husksync migrate legacy set host 1.2.3.4")
STEP 3] HuskSync will migrate data into the database
tables configures in the config.yml file of this
server. Please make sure you're happy with this
before proceeding.
STEP 4] To start the migration, please run:
"husksync migrate legacy start"
""".replaceAll(Pattern.quote("%source_host%"), obfuscateDataString(sourceHost))
.replaceAll(Pattern.quote("%source_port%"), Integer.toString(sourcePort))
.replaceAll(Pattern.quote("%source_username%"), obfuscateDataString(sourceUsername))
.replaceAll(Pattern.quote("%source_password%"), obfuscateDataString(sourcePassword))
.replaceAll(Pattern.quote("%source_database%"), sourceDatabase)
.replaceAll(Pattern.quote("%source_players_table%"), sourcePlayersTable)
.replaceAll(Pattern.quote("%source_data_table%"), sourceDataTable);
}
private record LegacyData(@NotNull User user,
@NotNull String serializedInventory, @NotNull String serializedEnderChest,
double health, double maxHealth, double healthScale, int hunger, float saturation,
float saturationExhaustion, int selectedSlot, @NotNull String serializedPotionEffects,
int totalExp, int expLevel, float expProgress,
@NotNull String gameMode, @NotNull String serializedStatistics, boolean isFlying,
@NotNull String serializedAdvancements, @NotNull String serializedLocation) {
return "=== HuskSync v1.x --> v3.x Migration Wizard =========\n" +
"This will migrate all user data from HuskSync v1.x to\n" +
"HuskSync v3.x's new format. To perform the migration,\n" +
"please follow the steps below carefully.\n" +
"\n" +
"[!] Existing data in the database will be wiped. [!]\n" +
"\n" +
"STEP 1] Please ensure no players are on any servers.\n" +
"\n" +
"STEP 2] HuskSync will need to connect to the database\n" +
"used to hold the existing, legacy HuskSync data.\n" +
"If this is the same database as the one you are\n" +
"currently using, you probably don't need to change\n" +
"anything.\n" +
"Please check that the credentials below are the\n" +
"correct credentials of the source legacy HuskSync\n" +
"database.\n" +
"- host: " + obfuscateDataString(sourceHost) + "\n" +
"- port: " + sourcePort + "\n" +
"- username: " + obfuscateDataString(sourceUsername) + "\n" +
"- password: " + obfuscateDataString(sourcePassword) + "\n" +
"- database: " + sourceDatabase + "\n" +
"- players_table: " + sourcePlayersTable + "\n" +
"- data_table: " + sourceDataTable + "\n" +
"If any of these are not correct, please correct them\n" +
"using the command:\n" +
"\"husksync migrate legacy set <parameter> <value>\"\n" +
"(e.g.: \"husksync migrate legacy set host 1.2.3.4\")\n" +
"\n" +
"STEP 3] HuskSync will migrate data into the database\n" +
"tables configures in the config.yml file of this\n" +
"server. Please make sure you're happy with this\n" +
"before proceeding.\n" +
"\n" +
"STEP 4] To start the migration, please run:\n" +
"\"husksync migrate legacy start\"\n";
}
@Value
private class LegacyData {
@NotNull User user;
@NotNull String serializedInventory;
@NotNull String serializedEnderChest;
double health;
double maxHealth;
double healthScale;
int hunger;
float saturation;
float saturationExhaustion;
int selectedSlot;
@NotNull String serializedPotionEffects;
int totalExp;
int expLevel;
float expProgress;
@NotNull String gameMode;
@NotNull String serializedStatistics;
boolean isFlying;
@NotNull String serializedAdvancements;
@NotNull String serializedLocation;
@NotNull
public DataSnapshot.Packed toUserData(@NotNull HSLConverter converter, @NotNull HuskSync plugin) {
@ -319,7 +337,7 @@ public class LegacyMigrator extends Migrator {
.advancements(BukkitData.Advancements.from(converter
.deserializeAdvancementData(serializedAdvancements).stream()
.map(data -> Data.Advancements.Advancement.adapt(data.key(), data.criteriaMap()))
.toList()))
.collect(Collectors.toList())))
// Stats
.statistics(BukkitData.Statistics.from(

@ -21,6 +21,7 @@ package net.william278.husksync.migrator;
import com.google.common.collect.Lists;
import com.zaxxer.hikari.HikariDataSource;
import lombok.Value;
import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.BukkitData;
@ -104,16 +105,14 @@ public class MpdbMigrator extends Migrator {
plugin.log(Level.INFO, "Downloading raw data from the MySQLPlayerDataBridge database (this might take a while)...");
final List<MpdbData> dataToMigrate = Lists.newArrayList();
try (final Connection connection = connectionPool.getConnection()) {
try (final PreparedStatement statement = connection.prepareStatement("""
SELECT `%source_inventory_table%`.`player_uuid`, `%source_inventory_table%`.`player_name`, `inventory`, `armor`, `enderchest`, `exp_lvl`, `exp`, `total_exp`
FROM `%source_inventory_table%`
INNER JOIN `%source_ender_chest_table%`
ON `%source_inventory_table%`.`player_uuid` = `%source_ender_chest_table%`.`player_uuid`
INNER JOIN `%source_xp_table%`
ON `%source_inventory_table%`.`player_uuid` = `%source_xp_table%`.`player_uuid`;
""".replaceAll(Pattern.quote("%source_inventory_table%"), sourceInventoryTable)
.replaceAll(Pattern.quote("%source_ender_chest_table%"), sourceEnderChestTable)
.replaceAll(Pattern.quote("%source_xp_table%"), sourceExperienceTable))) {
try (final PreparedStatement statement = connection.prepareStatement(
"SELECT `" + sourceInventoryTable + "`.`player_uuid`, `" + sourceInventoryTable + "`.`player_name`, `inventory`, `armor`, `enderchest`, `exp_lvl`, `exp`, `total_exp` " +
"FROM `" + sourceInventoryTable + "` " +
" INNER JOIN `" + sourceEnderChestTable + "` " +
" ON `" + sourceInventoryTable + "`.`player_uuid` = `" + sourceEnderChestTable + "`.`player_uuid` " +
" INNER JOIN `" + sourceExperienceTable + "` " +
" ON `" + sourceInventoryTable + "`.`player_uuid` = `" + sourceExperienceTable + "`.`player_uuid`;"
)) {
try (final ResultSet resultSet = statement.executeQuery()) {
int playersMigrated = 0;
while (resultSet.next()) {
@ -141,8 +140,8 @@ public class MpdbMigrator extends Migrator {
final AtomicInteger playersConverted = new AtomicInteger();
dataToMigrate.forEach(data -> {
final DataSnapshot.Packed convertedData = data.toUserData(mpdbConverter, plugin);
plugin.getDatabase().ensureUser(data.user());
plugin.getDatabase().addSnapshot(data.user(), convertedData);
plugin.getDatabase().ensureUser(data.getUser());
plugin.getDatabase().addSnapshot(data.getUser(), convertedData);
playersConverted.getAndIncrement();
if (playersConverted.get() % 50 == 0) {
plugin.log(Level.INFO, "Converted MySQLPlayerDataBridge data for " + playersConverted + " players...");
@ -160,45 +159,57 @@ public class MpdbMigrator extends Migrator {
@Override
public void handleConfigurationCommand(@NotNull String[] args) {
if (args.length == 2) {
if (switch (args[0].toLowerCase(Locale.ENGLISH)) {
case "host" -> {
boolean $yield;
switch (args[0].toLowerCase(Locale.ENGLISH)) {
case "host": {
this.sourceHost = args[1];
yield true;
$yield = true;
break;
}
case "port" -> {
case "port": {
try {
this.sourcePort = Integer.parseInt(args[1]);
yield true;
$yield = true;
} catch (NumberFormatException e) {
yield false;
$yield = false;
}
break;
}
case "username" -> {
case "username": {
this.sourceUsername = args[1];
yield true;
$yield = true;
break;
}
case "password" -> {
case "password": {
this.sourcePassword = args[1];
yield true;
$yield = true;
break;
}
case "database" -> {
case "database": {
this.sourceDatabase = args[1];
yield true;
$yield = true;
break;
}
case "inventory_table" -> {
case "inventory_table": {
this.sourceInventoryTable = args[1];
yield true;
$yield = true;
break;
}
case "ender_chest_table" -> {
case "ender_chest_table": {
this.sourceEnderChestTable = args[1];
yield true;
$yield = true;
break;
}
case "experience_table" -> {
case "experience_table": {
this.sourceExperienceTable = args[1];
yield true;
$yield = true;
break;
}
default -> false;
}) {
default: {
$yield = false;
}
}
if ($yield) {
plugin.log(Level.INFO, getHelpMenu());
plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
obfuscateDataString(args[1]));
@ -226,77 +237,81 @@ public class MpdbMigrator extends Migrator {
@NotNull
@Override
public String getHelpMenu() {
return """
=== MySQLPlayerDataBridge Migration Wizard ==========
NOTE: This migrator currently WORKS WITH MPDB version
v4.9.2 and below!
This will migrate inventories, ender chests and XP
from the MySQLPlayerDataBridge plugin to HuskSync.
To prevent excessive migration times, other non-vital
data will not be transferred.
[!] Existing data in the database will be wiped. [!]
STEP 1] Please ensure no players are on any servers.
STEP 2] HuskSync will need to connect to the database
used to hold the source MySQLPlayerDataBridge data.
Please check these database parameters are OK:
- host: %source_host%
- port: %source_port%
- username: %source_username%
- password: %source_password%
- database: %source_database%
- inventory_table: %source_inventory_table%
- ender_chest_table: %source_ender_chest_table%
- experience_table: %source_xp_table%
If any of these are not correct, please correct them
using the command:
"husksync migrate mpdb set <parameter> <value>"
(e.g.: "husksync migrate set mpdb host 1.2.3.4")
STEP 3] HuskSync will migrate data into the database
tables configures in the config.yml file of this
server. Please make sure you're happy with this
before proceeding.
STEP 4] To start the migration, please run:
"husksync migrate start mpdb"
NOTE: This migrator currently WORKS WITH MPDB version
v4.9.2 and below!
""".replaceAll(Pattern.quote("%source_host%"), obfuscateDataString(sourceHost))
.replaceAll(Pattern.quote("%source_port%"), Integer.toString(sourcePort))
.replaceAll(Pattern.quote("%source_username%"), obfuscateDataString(sourceUsername))
.replaceAll(Pattern.quote("%source_password%"), obfuscateDataString(sourcePassword))
.replaceAll(Pattern.quote("%source_database%"), sourceDatabase)
.replaceAll(Pattern.quote("%source_inventory_table%"), sourceInventoryTable)
.replaceAll(Pattern.quote("%source_ender_chest_table%"), sourceEnderChestTable)
.replaceAll(Pattern.quote("%source_xp_table%"), sourceExperienceTable);
return "=== MySQLPlayerDataBridge Migration Wizard ==========\n" +
"NOTE: This migrator currently WORKS WITH MPDB version\n" +
"v4.9.2 and below!\n" +
"\n" +
"This will migrate inventories, ender chests and XP\n" +
"from the MySQLPlayerDataBridge plugin to HuskSync.\n" +
"\n" +
"To prevent excessive migration times, other non-vital\n" +
"data will not be transferred.\n" +
"\n" +
"[!] Existing data in the database will be wiped. [!]\n" +
"\n" +
"STEP 1] Please ensure no players are on any servers.\n" +
"\n" +
"STEP 2] HuskSync will need to connect to the database\n" +
"used to hold the source MySQLPlayerDataBridge data.\n" +
"Please check these database parameters are OK:\n" +
"- host: " + obfuscateDataString(sourceHost) + "\n" +
"- port: " + sourcePort + "\n" +
"- username: " + obfuscateDataString(sourceUsername) + "\n" +
"- password: " + obfuscateDataString(sourcePassword) + "\n" +
"- database: " + sourceDatabase + "\n" +
"- inventory_table: " + sourceInventoryTable + "\n" +
"- ender_chest_table: " + sourceEnderChestTable + "\n" +
"- experience_table: " + sourceExperienceTable + "\n" +
"If any of these are not correct, please correct them\n" +
"using the command:\n" +
"\"husksync migrate mpdb set <parameter> <value>\"\n" +
"(e.g.: \"husksync migrate set mpdb host 1.2.3.4\")\n" +
"\n" +
"STEP 3] HuskSync will migrate data into the database\n" +
"tables configures in the config.yml file of this\n" +
"server. Please make sure you're happy with this\n" +
"before proceeding.\n" +
"\n" +
"STEP 4] To start the migration, please run:\n" +
"\"husksync migrate start mpdb\"\n" +
"\n" +
"NOTE: This migrator currently WORKS WITH MPDB version\n" +
"v4.9.2 and below!\n";
}
/**
* Represents data exported from the MySQLPlayerDataBridge source database
*
* @param user The user whose data is being migrated
* @param serializedInventory The serialized inventory data
* @param serializedArmor The serialized armor data
* @param serializedEnderChest The serialized ender chest data
* @param expLevel The player's current XP level
* @param expProgress The player's current XP progress
* @param totalExp The player's total XP score
*/
private record MpdbData(
@NotNull User user,
@NotNull String serializedInventory,
@NotNull String serializedArmor,
@NotNull String serializedEnderChest,
int expLevel,
float expProgress,
int totalExp
) {
@Value
private static class MpdbData {
/**
* The user whose data is being migrated
*/
@NotNull User user;
/**
* The serialized inventory data
*/
@NotNull String serializedInventory;
/**
* The serialized armor data
*/
@NotNull String serializedArmor;
/**
* The serialized ender chest data
*/
@NotNull String serializedEnderChest;
/**
* The player's current XP level
*/
int expLevel;
/**
* The player's current XP progress
*/
float expProgress;
/**
* The player's total XP score
*/
int totalExp;
/**
* Converts exported MySQLPlayerDataBridge data into HuskSync's {@link DataSnapshot} object format

@ -62,7 +62,7 @@ public class BukkitUser extends OnlineUser implements BukkitUserDataHolder {
}
@Override
@Deprecated(since = "3.6.7")
@Deprecated
public void sendToast(@NotNull MineDown title, @NotNull MineDown description,
@NotNull String iconMaterial, @NotNull String backgroundType) {
plugin.log(Level.WARNING, "Toast notifications are deprecated. " +

@ -77,7 +77,7 @@ public class BukkitLegacyConverter extends LegacyConverter {
@NotNull
private Map<Identifier, Data> readStatusData(@NotNull JSONObject object) {
if (!object.has("status_data")) {
return Map.of();
return Collections.emptyMap();
}
final JSONObject status = object.getJSONObject("status_data");

@ -69,7 +69,7 @@ public interface BukkitMapPersister {
if (!getPlugin().getSettings().getSynchronization().isPersistLockedMaps()) {
return items;
}
return forEachMap(items, map -> this.persistMapView(map, delegateRenderer));
return private$forEachMap(items, map -> this.persistMapView(map, delegateRenderer));
}
/**
@ -83,21 +83,26 @@ public interface BukkitMapPersister {
if (!getPlugin().getSettings().getSynchronization().isPersistLockedMaps()) {
return items;
}
return forEachMap(items, this::applyMapView);
return private$forEachMap(items, this::private$applyMapView);
}
// Perform an operation on each map in an array of ItemStacks
@NotNull
private ItemStack[] forEachMap(ItemStack[] items, @NotNull Function<ItemStack, ItemStack> function) {
default ItemStack[] private$forEachMap(ItemStack[] items, @NotNull Function<ItemStack, ItemStack> function) {
for (int i = 0; i < items.length; i++) {
final ItemStack item = items[i];
if (item == null) {
continue;
}
if (item.getType() == Material.FILLED_MAP && item.hasItemMeta()) {
if (MaterialUtil.isFilledMap(item.getType()) && item.hasItemMeta()) {
items[i] = function.apply(item);
} else if (item.getItemMeta() instanceof BlockStateMeta b && b.getBlockState() instanceof ShulkerBox box) {
forEachMap(box.getInventory().getContents(), function);
} else if (item.getItemMeta() instanceof BlockStateMeta &&
((BlockStateMeta) item.getItemMeta()).getBlockState() instanceof ShulkerBox
) {
BlockStateMeta b = (BlockStateMeta) item.getItemMeta();
ShulkerBox box = (ShulkerBox) b.getBlockState();
private$forEachMap(box.getInventory().getContents(), function);
b.setBlockState(box);
}
}
@ -105,7 +110,7 @@ public interface BukkitMapPersister {
}
@NotNull
private ItemStack persistMapView(@NotNull ItemStack map, @NotNull Player delegateRenderer) {
default ItemStack persistMapView(@NotNull ItemStack map, @NotNull Player delegateRenderer) {
final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta());
if (!meta.hasMapView()) {
return map;
@ -139,7 +144,7 @@ public interface BukkitMapPersister {
}
@NotNull
private ItemStack applyMapView(@NotNull ItemStack map) {
default ItemStack private$applyMapView(@NotNull ItemStack map) {
final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta());
NBT.get(map, nbt -> {
if (!nbt.hasTag(MAP_DATA_KEY)) {
@ -186,8 +191,8 @@ public interface BukkitMapPersister {
}
// Add a renderer to the map with the data and save to file
final MapView view = generateRenderedMap(canvasData);
final String worldUid = getDefaultMapWorld().getUID().toString();
final MapView view = private$generateRenderedMap(canvasData);
final String worldUid = private$getDefaultMapWorld().getUID().toString();
meta.setMapView(view);
map.setItemMeta(meta);
saveMapToFile(canvasData, view.getId());
@ -204,7 +209,7 @@ public interface BukkitMapPersister {
}
default void renderMapFromFile(@NotNull MapView view) {
final File mapFile = new File(getMapCacheFolder(), view.getId() + ".dat");
final File mapFile = new File(private$getMapCacheFolder(), view.getId() + ".dat");
if (!mapFile.exists()) {
return;
}
@ -232,7 +237,7 @@ public interface BukkitMapPersister {
default void saveMapToFile(@NotNull MapData data, int id) {
getPlugin().runAsync(() -> {
final File mapFile = new File(getMapCacheFolder(), id + ".dat");
final File mapFile = new File(private$getMapCacheFolder(), id + ".dat");
if (mapFile.exists()) {
return;
}
@ -248,7 +253,7 @@ public interface BukkitMapPersister {
}
@NotNull
private File getMapCacheFolder() {
default File private$getMapCacheFolder() {
final File mapCache = new File(getPlugin().getDataFolder(), "maps");
if (!mapCache.exists() && !mapCache.mkdirs()) {
getPlugin().log(Level.WARNING, "Failed to create maps folder");
@ -258,8 +263,8 @@ public interface BukkitMapPersister {
// Sets the renderer of a map, and returns the generated MapView
@NotNull
private MapView generateRenderedMap(@NotNull MapData canvasData) {
final MapView view = Bukkit.createMap(getDefaultMapWorld());
default MapView private$generateRenderedMap(@NotNull MapData canvasData) {
final MapView view = Bukkit.createMap(private$getDefaultMapWorld());
view.getRenderers().clear();
// Create a new map view renderer with the map data color at each pixel
@ -275,7 +280,7 @@ public interface BukkitMapPersister {
}
@NotNull
private static World getDefaultMapWorld() {
static World private$getDefaultMapWorld() {
final World world = Bukkit.getWorlds().get(0);
if (world == null) {
throw new IllegalStateException("No worlds are loaded on the server!");
@ -318,35 +323,86 @@ public interface BukkitMapPersister {
cursors.removeCursor(cursors.getCursor(0));
}
canvasData.getBanners().forEach(banner -> cursors.addCursor(createBannerCursor(banner)));
canvasData.getBanners().forEach(banner -> cursors.addCursor(private$createBannerCursor(banner)));
canvas.setCursors(cursors);
}
}
@NotNull
private static MapCursor createBannerCursor(@NotNull MapBanner banner) {
static MapCursor private$createBannerCursor(@NotNull MapBanner banner) {
MapCursor.Type type;
switch (banner.getColor().toLowerCase(Locale.ENGLISH)) {
case "white": {
type = MapCursor.Type.BANNER_WHITE;
break;
}
case "orange": {
type = MapCursor.Type.BANNER_ORANGE;
break;
}
case "magenta": {
type = MapCursor.Type.BANNER_MAGENTA;
break;
}
case "light_blue": {
type = MapCursor.Type.BANNER_LIGHT_BLUE;
break;
}
case "yellow": {
type = MapCursor.Type.BANNER_YELLOW;
break;
}
case "lime": {
type = MapCursor.Type.BANNER_LIME;
break;
}
case "pink": {
type = MapCursor.Type.BANNER_PINK;
break;
}
case "gray": {
type = MapCursor.Type.BANNER_GRAY;
break;
}
case "light_gray": {
type = MapCursor.Type.BANNER_LIGHT_GRAY;
break;
}
case "cyan": {
type = MapCursor.Type.BANNER_CYAN;
break;
}
case "purple": {
type = MapCursor.Type.BANNER_PURPLE;
break;
}
case "blue": {
type = MapCursor.Type.BANNER_BLUE;
break;
}
case "brown": {
type = MapCursor.Type.BANNER_BROWN;
break;
}
case "green": {
type = MapCursor.Type.BANNER_GREEN;
break;
}
case "red": {
type = MapCursor.Type.BANNER_RED;
break;
}
default: {
type = MapCursor.Type.BANNER_BLACK;
break;
}
}
return new MapCursor(
(byte) banner.getPosition().getX(),
(byte) banner.getPosition().getZ(),
(byte) 8, // Always rotate banners upright
switch (banner.getColor().toLowerCase(Locale.ENGLISH)) {
case "white" -> MapCursor.Type.BANNER_WHITE;
case "orange" -> MapCursor.Type.BANNER_ORANGE;
case "magenta" -> MapCursor.Type.BANNER_MAGENTA;
case "light_blue" -> MapCursor.Type.BANNER_LIGHT_BLUE;
case "yellow" -> MapCursor.Type.BANNER_YELLOW;
case "lime" -> MapCursor.Type.BANNER_LIME;
case "pink" -> MapCursor.Type.BANNER_PINK;
case "gray" -> MapCursor.Type.BANNER_GRAY;
case "light_gray" -> MapCursor.Type.BANNER_LIGHT_GRAY;
case "cyan" -> MapCursor.Type.BANNER_CYAN;
case "purple" -> MapCursor.Type.BANNER_PURPLE;
case "blue" -> MapCursor.Type.BANNER_BLUE;
case "brown" -> MapCursor.Type.BANNER_BROWN;
case "green" -> MapCursor.Type.BANNER_GREEN;
case "red" -> MapCursor.Type.BANNER_RED;
default -> MapCursor.Type.BANNER_BLACK;
},
type,
true,
banner.getText().isEmpty() ? null : banner.getText()
);
@ -409,11 +465,14 @@ public interface BukkitMapPersister {
@NotNull
private String getDimension() {
return mapView.getWorld() != null ? switch (mapView.getWorld().getEnvironment()) {
case NETHER -> "minecraft:the_nether";
case THE_END -> "minecraft:the_end";
default -> "minecraft:overworld";
} : "minecraft:overworld";
if (mapView.getWorld() == null) {
return "minecraft:overworld";
}
switch (mapView.getWorld().getEnvironment()) {
case NETHER: return "minecraft:the_nether";
case THE_END: return "minecraft:the_end";
default: return "minecraft:overworld";
}
}
/**
@ -442,7 +501,7 @@ public interface BukkitMapPersister {
}
} catch (Throwable ignored) {
}
return MapData.fromPixels(pixels, getDimension(), (byte) 2, banners, List.of());
return MapData.fromPixels(pixels, getDimension(), (byte) 2, banners, Collections.emptyList());
}
}

@ -0,0 +1,58 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.util;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import net.william278.husksync.util.ref.RefUtil;
import org.bukkit.Material;
import java.lang.invoke.MethodHandle;
@UtilityClass
public class MaterialUtil {
private static final MethodHandle v$isAirMethod = RefUtil.bootstrapVirtualMethod("Lorg/bukkit/Material;isAir()Z");
@SneakyThrows
public static boolean isAir(Material $this) {
if (v$isAirMethod != null) {
return (boolean) v$isAirMethod.invoke($this);
}
switch ($this.name()) {
case "AIR":
case "CAVE_AIR":
case "VOID_AIR":
case "LEGACY_AIR":
return true;
default:
return false;
}
}
public static boolean isFilledMap(Material $this) {
switch ($this.name()) {
case "FILLED_MAP":
case "LEGACY_MAP":
return true;
default:
return false;
}
}
}

@ -0,0 +1,220 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.util.ref;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.Arrays;
public final class RefUtil {
private RefUtil() {
throw new UnsupportedOperationException("This class cannot be instantiated");
}
private static final MethodHandles.Lookup lookup = MethodHandles.lookup();
public static MethodHandle bootstrapStaticMethod(String methodDesc) {
String[] descParts = splitMethodDesc(methodDesc);
Class<?> owner;
try {
owner = Class.forName(descParts[0].replace('/', '.'));
} catch (ClassNotFoundException e) {
return null;
}
MethodType methodType;
try {
methodType = MethodType.fromMethodDescriptorString(descParts[2], owner.getClassLoader());
} catch (TypeNotPresentException e) {
return null;
}
try {
return lookup.findStatic(owner, descParts[1], methodType);
} catch (NoSuchMethodException e) {
return null;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public static MethodHandle bootstrapVirtualMethod(String methodDesc) {
String[] descParts = splitMethodDesc(methodDesc);
Class<?> owner;
try {
owner = Class.forName(descParts[0].replace('/', '.'));
} catch (ClassNotFoundException e) {
return null;
}
MethodType methodType;
try {
methodType = MethodType.fromMethodDescriptorString(descParts[2], owner.getClassLoader());
} catch (TypeNotPresentException e) {
return null;
}
try {
return lookup.findVirtual(owner, descParts[1], methodType);
} catch (NoSuchMethodException e) {
return null;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
private static String[] splitMethodDesc(String methodDesc) {
if (methodDesc.length() < 5) { // L;()V
throw new IllegalArgumentException("Invalid method descriptor: " + methodDesc);
}
int descSplit = methodDesc.indexOf(';');
if (descSplit == -1) {
throw new IllegalArgumentException("Invalid method descriptor: " + methodDesc);
}
int argsStart = methodDesc.indexOf('(');
if (argsStart == -1) {
throw new IllegalArgumentException("Invalid method descriptor: " + methodDesc);
}
return new String[] {
methodDesc.substring(1, descSplit), // java/lang/Object
methodDesc.substring(descSplit + 1, argsStart), // method name
methodDesc.substring(argsStart) // (Ljava/lang/Object;)V
};
}
public static <T extends Throwable> RuntimeException sneakyThrow(Throwable t) throws T {
throw (T) t;
}
public static MethodHandle bootstrapStaticFieldGet(String fieldDesc) {
String[] descParts = splitFieldDesc(fieldDesc);
Class<?> owner;
try {
owner = Class.forName(descParts[0].replace('/', '.'));
} catch (ClassNotFoundException e) {
return null;
}
Class<?> fieldType;
try {
fieldType = MethodType.fromMethodDescriptorString("()" + descParts[2], owner.getClassLoader()).returnType();
} catch (TypeNotPresentException e) {
return null;
}
try {
return lookup.findStaticGetter(owner, descParts[1], fieldType);
} catch (NoSuchFieldException e) {
return null;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public static MethodHandle bootstrapStaticFieldSet(String fieldDesc) {
String[] descParts = splitFieldDesc(fieldDesc);
Class<?> owner;
try {
owner = Class.forName(descParts[0].replace('/', '.'));
} catch (ClassNotFoundException e) {
return null;
}
Class<?> fieldType;
try {
fieldType = MethodType.fromMethodDescriptorString("()" + descParts[2], owner.getClassLoader()).returnType();
} catch (TypeNotPresentException e) {
return null;
}
try {
return lookup.findStaticSetter(owner, descParts[1], fieldType);
} catch (NoSuchFieldException e) {
return null;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public static MethodHandle bootstrapVirtualFieldGet(String fieldDesc) {
String[] descParts = splitFieldDesc(fieldDesc);
Class<?> owner;
try {
owner = Class.forName(descParts[0].replace('/', '.'));
} catch (ClassNotFoundException e) {
return null;
}
Class<?> fieldType;
try {
fieldType = MethodType.fromMethodDescriptorString("()" + descParts[2], owner.getClassLoader()).returnType();
} catch (TypeNotPresentException e) {
return null;
}
try {
return lookup.findGetter(owner, descParts[1], fieldType);
} catch (NoSuchFieldException e) {
return null;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public static MethodHandle bootstrapVirtualFieldSet(String fieldDesc) {
String[] descParts = splitFieldDesc(fieldDesc);
Class<?> owner;
try {
owner = Class.forName(descParts[0].replace('/', '.'));
} catch (ClassNotFoundException e) {
return null;
}
Class<?> fieldType;
try {
fieldType = MethodType.fromMethodDescriptorString("()" + descParts[2], owner.getClassLoader()).returnType();
} catch (TypeNotPresentException e) {
return null;
}
try {
return lookup.findSetter(owner, descParts[1], fieldType);
} catch (NoSuchFieldException e) {
return null;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
private static String[] splitFieldDesc(String fieldDesc) {
if (fieldDesc.length() < 3) { // L;:
throw new IllegalArgumentException("Invalid field descriptor: " + fieldDesc);
}
int descSplit = fieldDesc.indexOf(';');
if (descSplit == -1) {
throw new IllegalArgumentException("Invalid field descriptor: " + fieldDesc);
}
int argsStart = fieldDesc.indexOf(':');
if (argsStart == -1) {
throw new IllegalArgumentException("Invalid field descriptor: " + fieldDesc);
}
return new String[] {
fieldDesc.substring(1, descSplit), // java/lang/Object
fieldDesc.substring(descSplit + 1, argsStart), // field name
fieldDesc.substring(argsStart + 1) // Ljava/lang/Object;
};
}
}

@ -1,20 +1,13 @@
name: 'HuskSync'
version: '${version}'
main: 'net.william278.husksync.BukkitHuskSync'
api-version: 1.17
author: 'William278'
api-version: 1.13
author: 'William278, InkerBot'
description: '${description}'
website: 'https://william278.net'
website: 'https://git.inker.bot/HuskLegacy/HuskSync'
folia-supported: true
softdepend:
- 'packetevents'
- 'ProtocolLib'
- 'MysqlPlayerDataBridge'
- 'Plan'
libraries:
- 'redis.clients:jedis:${jedis_version}'
- 'com.mysql:mysql-connector-j:${mysql_driver_version}'
- 'org.mariadb.jdbc:mariadb-java-client:${mariadb_driver_version}'
- 'org.postgresql:postgresql:${postgres_driver_version}'
- 'org.mongodb:mongodb-driver-sync:${mongodb_driver_version}'
- 'org.xerial.snappy:snappy-java:${snappy_version}'

@ -3,26 +3,26 @@ plugins {
}
dependencies {
api 'org.inksnow.cputil:logger:1.13'
api 'org.inksnow.cputil:database:1.13'
api 'commons-io:commons-io:2.16.1'
api 'org.apache.commons:commons-text:1.12.0'
api 'net.william278:minedown:1.8.2'
api 'de.themoep:minedown-adventure:1.7.3-SNAPSHOT'
api 'org.json:json:20240303'
api 'com.google.code.gson:gson:2.11.0'
api 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2'
api 'de.exlll:configlib-yaml:4.5.0'
api 'net.william278:paginedown:1.1.2'
api 'net.william278:DesertWell:2.0.4'
api('com.zaxxer:HikariCP:5.1.0') {
exclude module: 'slf4j-api'
}
api 'org.inksnow.husk:configlib-yaml:4.5.4'
api 'org.inksnow.husk:paginedown:1.1.3-ac18d11'
api 'org.inksnow.husk:desertwell:2.0.5-9962d59:all'
api 'com.mojang:brigadier:1.1.8'
compileOnly 'net.william278.uniform:uniform-common:1.2.1'
compileOnly 'com.mojang:brigadier:1.1.8'
compileOnly 'org.inksnow.husk.uniform:uniform-common:1.2.1-ad25f16'
compileOnly 'org.projectlombok:lombok:1.18.34'
compileOnly 'org.jetbrains:annotations:24.1.0'
compileOnly 'net.kyori:adventure-api:4.17.0'
compileOnly 'net.kyori:adventure-platform-api:4.3.4'
compileOnly 'com.google.guava:guava:33.3.0-jre'
compileOnly 'com.google.guava:guava:33.2.1-jre'
compileOnly 'com.github.plan-player-analytics:Plan:5.5.2272'
compileOnly "redis.clients:jedis:$jedis_version"
compileOnly "com.mysql:mysql-connector-j:$mysql_driver_version"
@ -33,9 +33,9 @@ dependencies {
testImplementation "redis.clients:jedis:$jedis_version"
testImplementation "org.xerial.snappy:snappy-java:$snappy_version"
testImplementation 'com.google.guava:guava:33.3.0-jre'
testImplementation 'com.google.guava:guava:33.2.1-jre'
testImplementation 'com.github.plan-player-analytics:Plan:5.5.2272'
testCompileOnly 'de.exlll:configlib-yaml:4.5.0'
testCompileOnly 'org.inksnow.husk:configlib-yaml:4.5.4'
testCompileOnly 'org.jetbrains:annotations:24.1.0'
annotationProcessor 'org.projectlombok:lombok:1.18.34'

@ -196,7 +196,7 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
// Get the debug log message format
@NotNull
private String getDebugString(@NotNull String message) {
default String getDebugString(@NotNull String message) {
return String.format("[DEBUG] [%s] %s", new SimpleDateFormat("mm:ss.SSS").format(new Date()), message);
}
@ -327,16 +327,16 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
*/
final class FailedToLoadException extends IllegalStateException {
private static final String FORMAT = """
HuskSync has failed to load! The plugin will not be enabled and no data will be synchronized.
Please make sure the plugin has been setup correctly (https://william278.net/docs/husksync/setup):
1) Make sure you've entered your MySQL, MariaDB or MongoDB database details correctly in config.yml
2) Make sure your Redis server details are also correct in config.yml
3) Make sure your config is up-to-date (https://william278.net/docs/husksync/config-file)
4) Check the error below for more details
Caused by: %s""";
private static final String FORMAT =
"HuskSync has failed to load! The plugin will not be enabled and no data will be synchronized.\n" +
"Please make sure the plugin has been setup correctly (https://william278.net/docs/husksync/setup):\n" +
"\n" +
"1) Make sure you've entered your MySQL, MariaDB or MongoDB database details correctly in config.yml\n" +
"2) Make sure your Redis server details are also correct in config.yml\n" +
"3) Make sure your config is up-to-date (https://william278.net/docs/husksync/config-file)\n" +
"4) Check the error below for more details\n" +
"\n" +
"Caused by: %s";
FailedToLoadException(@NotNull String message, @NotNull Throwable cause) {
super(String.format(FORMAT, message), cause);

@ -29,15 +29,19 @@ import net.william278.husksync.data.Serializer;
import net.william278.husksync.sync.DataSyncer;
import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.user.User;
import net.william278.husksync.util.OptionalUtil;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Formatter;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* The common implementation of the HuskSync API, containing cross-platform API calls.
@ -138,7 +142,7 @@ public class HuskSyncAPI {
public CompletableFuture<Optional<DataSnapshot.Unpacked>> getCurrentData(@NotNull User user) {
return plugin.getRedisManager()
.getUserData(UUID.randomUUID(), user)
.thenApply(data -> data.or(() -> plugin.getDatabase().getLatestSnapshot(user)))
.thenApply(data -> OptionalUtil.or(data, () -> plugin.getDatabase().getLatestSnapshot(user)))
.thenApply(data -> data.map(snapshot -> snapshot.unpack(plugin)));
}
@ -154,8 +158,8 @@ public class HuskSyncAPI {
*/
public void setCurrentData(@NotNull User user, @NotNull DataSnapshot data) {
plugin.runAsync(() -> {
final DataSnapshot.Packed packed = data instanceof DataSnapshot.Unpacked unpacked
? unpacked.pack(plugin) : (DataSnapshot.Packed) data;
final DataSnapshot.Packed packed = data instanceof DataSnapshot.Unpacked
? ((DataSnapshot.Unpacked) data).pack(plugin) : (DataSnapshot.Packed) data;
addSnapshot(user, packed);
plugin.getRedisManager().sendUserDataUpdate(user, packed);
});
@ -190,7 +194,7 @@ public class HuskSyncAPI {
return plugin.supplyAsync(
() -> plugin.getDatabase().getAllSnapshots(user).stream()
.map(snapshot -> snapshot.unpack(plugin))
.toList()
.collect(Collectors.toList())
);
}
@ -205,9 +209,10 @@ public class HuskSyncAPI {
*/
public CompletableFuture<List<DataSnapshot.Unpacked>> getSnapshot(@NotNull User user, @NotNull UUID versionId) {
return plugin.supplyAsync(
() -> plugin.getDatabase().getSnapshot(user, versionId).stream()
() -> plugin.getDatabase().getSnapshot(user, versionId)
.map(Stream::of).orElseGet(Stream::empty)
.map(snapshot -> snapshot.unpack(plugin))
.toList()
.collect(Collectors.toList())
);
}
@ -274,8 +279,8 @@ public class HuskSyncAPI {
@Nullable BiConsumer<User, DataSnapshot.Packed> callback) {
plugin.runAsync(() -> plugin.getDataSyncer().saveData(
user,
snapshot instanceof DataSnapshot.Unpacked unpacked
? unpacked.pack(plugin) : (DataSnapshot.Packed) snapshot,
snapshot instanceof DataSnapshot.Unpacked
? ((DataSnapshot.Unpacked) snapshot).pack(plugin) : (DataSnapshot.Packed) snapshot,
callback
));
}
@ -304,8 +309,8 @@ public class HuskSyncAPI {
*/
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot) {
plugin.runAsync(() -> plugin.getDatabase().updateSnapshot(
user, snapshot instanceof DataSnapshot.Unpacked unpacked
? unpacked.pack(plugin) : (DataSnapshot.Packed) snapshot
user, snapshot instanceof DataSnapshot.Unpacked
? ((DataSnapshot.Unpacked) snapshot).pack(plugin) : (DataSnapshot.Packed) snapshot
));
}
@ -439,8 +444,8 @@ public class HuskSyncAPI {
* @since 3.0
*/
public int getSnapshotFileSize(@NotNull DataSnapshot snapshot) {
return (snapshot instanceof DataSnapshot.Packed packed)
? packed.getFileSize(plugin)
return (snapshot instanceof DataSnapshot.Packed)
? ((DataSnapshot.Packed) snapshot).getFileSize(plugin)
: ((DataSnapshot.Unpacked) snapshot).pack(plugin).getFileSize(plugin);
}
@ -511,15 +516,14 @@ public class HuskSyncAPI {
*/
static final class NotRegisteredException extends IllegalStateException {
private static final String REASONS = """
This may be because:
1) HuskSync has failed to enable successfully
2) Your plugin isn't set to load after HuskSync has
(Check if it set as a (soft)depend in plugin.yml or to load: BEFORE in paper-plugin.yml?)
3) You are attempting to access HuskSync on plugin construction/before your plugin has enabled.""";
private static final String REASONS = "This may be because:\n" +
"1) HuskSync has failed to enable successfully\n" +
"2) Your plugin isn't set to load after HuskSync has\n" +
" (Check if it set as a (soft)depend in plugin.yml or to load: BEFORE in paper-plugin.yml?)\n" +
"3) You are attempting to access HuskSync on plugin construction/before your plugin has enabled.";
NotRegisteredException(@NotNull String reasons) {
super("Could not access the HuskSync API as it has not yet been registered. %s".formatted(reasons));
super("Could not access the HuskSync API as it has not yet been registered. " + reasons);
}
NotRegisteredException() {

@ -19,6 +19,7 @@
package net.william278.husksync.command;
import com.google.common.collect.ImmutableList;
import de.themoep.minedown.adventure.MineDown;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.Data;
@ -37,14 +38,14 @@ import java.util.Optional;
public class EnderChestCommand extends ItemsCommand {
public EnderChestCommand(@NotNull HuskSync plugin) {
super("enderchest", List.of("echest", "openechest"), plugin);
super("enderchest", ImmutableList.of("echest", "openechest"), plugin);
}
@Override
protected void showItems(@NotNull OnlineUser viewer, @NotNull DataSnapshot.Unpacked snapshot,
@NotNull User user, boolean allowEdit) {
final Optional<Data.Items.EnderChest> optionalEnderChest = snapshot.getEnderChest();
if (optionalEnderChest.isEmpty()) {
if (!optionalEnderChest.isPresent()) {
plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage);
return;
@ -76,7 +77,7 @@ public class EnderChestCommand extends ItemsCommand {
@SuppressWarnings("DuplicatedCode")
private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User holder) {
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
if (latestData.isEmpty()) {
if (!latestData.isPresent()) {
plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage);
return;

@ -19,6 +19,7 @@
package net.william278.husksync.command;
import com.google.common.collect.ImmutableList;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import de.themoep.minedown.adventure.MineDown;
@ -30,9 +31,11 @@ import net.kyori.adventure.text.format.TextColor;
import net.william278.desertwell.about.AboutMenu;
import net.william278.desertwell.util.UpdateChecker;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.Identifier;
import net.william278.husksync.database.Database;
import net.william278.husksync.migrator.Migrator;
import net.william278.husksync.user.CommandUser;
import net.william278.husksync.util.StringUtil;
import net.william278.uniform.BaseCommand;
import net.william278.uniform.CommandProvider;
import net.william278.uniform.Permission;
@ -41,7 +44,7 @@ import org.apache.commons.text.WordUtils;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.stream.Collectors;
@ -52,7 +55,7 @@ public class HuskSyncCommand extends PluginCommand {
private final AboutMenu aboutMenu;
public HuskSyncCommand(@NotNull HuskSync plugin) {
super("husksync", List.of(), Permission.Default.TRUE, ExecutionScope.ALL, plugin);
super("husksync", ImmutableList.of(), Permission.Default.TRUE, ExecutionScope.ALL, plugin);
this.updateChecker = plugin.getUpdateChecker();
this.aboutMenu = AboutMenu.builder()
.title(Component.text("HuskSync"))
@ -110,7 +113,8 @@ public class HuskSyncCommand extends PluginCommand {
plugin.getLocales().getLocale("system_status_header").ifPresent(user::sendMessage);
user.sendMessage(Component.join(
JoinConfiguration.newlines(),
Arrays.stream(StatusLine.values()).map(s -> s.get(plugin)).toList()
Arrays.stream(StatusLine.values()).map(s -> s.get(plugin))
.collect(Collectors.toList())
));
});
}
@ -202,7 +206,7 @@ public class HuskSyncCommand extends PluginCommand {
private enum StatusLine {
PLUGIN_VERSION(plugin -> Component.text("v" + plugin.getPluginVersion().toStringWithoutMetadata())
.appendSpace().append(plugin.getPluginVersion().getMetadata().isBlank() ? Component.empty()
.appendSpace().append(StringUtil.isBlank(plugin.getPluginVersion().getMetadata()) ? Component.empty()
: Component.text("(build " + plugin.getPluginVersion().getMetadata() + ")"))),
SERVER_VERSION(plugin -> Component.text(plugin.getServerVersion())),
LANGUAGE(plugin -> Component.text(plugin.getSettings().getLanguage())),
@ -210,7 +214,7 @@ public class HuskSyncCommand extends PluginCommand {
JAVA_VERSION(plugin -> Component.text(System.getProperty("java.version"))),
JAVA_VENDOR(plugin -> Component.text(System.getProperty("java.vendor"))),
SERVER_NAME(plugin -> Component.text(plugin.getServerName())),
CLUSTER_ID(plugin -> Component.text(plugin.getSettings().getClusterId().isBlank() ? "None" : plugin.getSettings().getClusterId())),
CLUSTER_ID(plugin -> Component.text(StringUtil.isBlank(plugin.getSettings().getClusterId()) ? "None" : plugin.getSettings().getClusterId())),
SYNC_MODE(plugin -> Component.text(WordUtils.capitalizeFully(
plugin.getSettings().getSynchronization().getMode().toString()
))),
@ -224,10 +228,10 @@ public class HuskSyncCommand extends PluginCommand {
),
IS_DATABASE_LOCAL(plugin -> getLocalhostBoolean(plugin.getSettings().getDatabase().getCredentials().getHost())),
USING_REDIS_SENTINEL(plugin -> getBoolean(
!plugin.getSettings().getRedis().getSentinel().getMaster().isBlank()
!StringUtil.isBlank(plugin.getSettings().getRedis().getSentinel().getMaster())
)),
USING_REDIS_PASSWORD(plugin -> getBoolean(
!plugin.getSettings().getRedis().getCredentials().getPassword().isBlank()
!StringUtil.isBlank(plugin.getSettings().getRedis().getCredentials().getPassword())
)),
REDIS_USING_SSL(plugin -> getBoolean(
plugin.getSettings().getRedis().getCredentials().isUseSsl()
@ -243,13 +247,13 @@ public class HuskSyncCommand extends PluginCommand {
.hoverEvent(HoverEvent.showText(
Component.text(i.isEnabled() ? "Enabled" : "Disabled")
.append(Component.newline())
.append(Component.text("Dependencies: %s".formatted(i.getDependencies()
.isEmpty() ? "(None)" : i.getDependencies().stream()
.map(d -> "%s (%s)".formatted(
d.getKey().value(), d.isRequired() ? "Required" : "Optional"
)).collect(Collectors.joining(", ")))
.append(i.getDependencies().isEmpty()
? Component.text("Dependencies: None").color(NamedTextColor.GRAY)
: Component.text("Dependencies: " + i.getDependencies().stream()
.map(d -> d.getKey().value() + " (" + (d.isRequired() ? "Required" : "Optional") + ")")
.collect(Collectors.joining(", "))
).color(NamedTextColor.GRAY))
))).toList()
))).collect(Collectors.toList())
));
private final Function<HuskSync, Component> supplier;

@ -19,6 +19,7 @@
package net.william278.husksync.command;
import com.google.common.collect.ImmutableList;
import de.themoep.minedown.adventure.MineDown;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.Data;
@ -37,14 +38,14 @@ import java.util.Optional;
public class InventoryCommand extends ItemsCommand {
public InventoryCommand(@NotNull HuskSync plugin) {
super("inventory", List.of("invsee", "openinv"), plugin);
super("inventory", ImmutableList.of("invsee", "openinv"), plugin);
}
@Override
protected void showItems(@NotNull OnlineUser viewer, @NotNull DataSnapshot.Unpacked snapshot,
@NotNull User user, boolean allowEdit) {
final Optional<Data.Items.Inventory> optionalInventory = snapshot.getInventory();
if (optionalInventory.isEmpty()) {
if (!optionalInventory.isPresent()) {
viewer.sendMessage(new MineDown("what the FUCK is happening"));
plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage);
@ -77,7 +78,7 @@ public class InventoryCommand extends ItemsCommand {
@SuppressWarnings("DuplicatedCode")
private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User holder) {
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
if (latestData.isEmpty()) {
if (!latestData.isPresent()) {
plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage);
return;

@ -24,6 +24,7 @@ import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.CommandUser;
import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.user.User;
import net.william278.husksync.util.OptionalUtil;
import net.william278.uniform.BaseCommand;
import net.william278.uniform.Permission;
import org.jetbrains.annotations.NotNull;
@ -44,32 +45,32 @@ public abstract class ItemsCommand extends PluginCommand {
final User user = ctx.getArgument("username", User.class);
final UUID version = ctx.getArgument("version", UUID.class);
final CommandUser executor = user(command, ctx);
if (!(executor instanceof OnlineUser online)) {
if (!(executor instanceof OnlineUser)) {
plugin.getLocales().getLocale("error_in_game_command_only")
.ifPresent(executor::sendMessage);
return;
}
this.showSnapshotItems(online, user, version);
this.showSnapshotItems((OnlineUser) executor, user, version);
}, user("username"), uuid("version"));
command.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
final CommandUser executor = user(command, ctx);
if (!(executor instanceof OnlineUser online)) {
if (!(executor instanceof OnlineUser)) {
plugin.getLocales().getLocale("error_in_game_command_only")
.ifPresent(executor::sendMessage);
return;
}
this.showLatestItems(online, user);
this.showLatestItems((OnlineUser) executor, user);
}, user("username"));
}
// View (and edit) the latest user data
private void showLatestItems(@NotNull OnlineUser viewer, @NotNull User user) {
plugin.getRedisManager().getUserData(user.getUuid(), user).thenAccept(data -> data
.or(() -> plugin.getDatabase().getLatestSnapshot(user))
.or(() -> {
plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage);
plugin.getRedisManager().getUserData(user.getUuid(), user).thenAccept(data -> OptionalUtil.or(
OptionalUtil.or(data, () ->
plugin.getDatabase().getLatestSnapshot(user)),
() -> {
plugin.getLocales().getLocale("error_no_data_to_display").ifPresent(viewer::sendMessage);
return Optional.empty();
})
.flatMap(packed -> {
@ -87,8 +88,8 @@ public abstract class ItemsCommand extends PluginCommand {
// View a specific version of the user data
private void showSnapshotItems(@NotNull OnlineUser viewer, @NotNull User user, @NotNull UUID version) {
plugin.getDatabase().getSnapshot(user, version)
.or(() -> {
OptionalUtil.or(plugin.getDatabase().getSnapshot(user, version),
() -> {
plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(viewer::sendMessage);
return Optional.empty();

@ -46,7 +46,7 @@ public abstract class PluginCommand extends Command {
}
private static String getDescription(@NotNull HuskSync plugin, @NotNull String name) {
return plugin.getLocales().getRawLocale("%s_command_description".formatted(name)).orElse("");
return plugin.getLocales().getRawLocale(name + "_command_description").orElse("");
}
@NotNull
@ -72,7 +72,10 @@ public abstract class PluginCommand extends Command {
@NotNull
protected CommandUser adapt(net.william278.uniform.CommandUser user) {
return user.getUuid() == null ? plugin.getConsole() : plugin.getOnlineUser(user.getUuid()).orElseThrow();
return user.getUuid() == null
? plugin.getConsole()
: plugin.getOnlineUser(user.getUuid())
.orElseThrow(() -> new IllegalStateException("Online not found"));
}
@NotNull

@ -29,12 +29,14 @@ import net.william278.husksync.user.User;
import net.william278.husksync.util.DataDumper;
import net.william278.husksync.util.DataSnapshotList;
import net.william278.husksync.util.DataSnapshotOverview;
import net.william278.husksync.util.OptionalUtil;
import net.william278.uniform.BaseCommand;
import net.william278.uniform.CommandProvider;
import net.william278.uniform.Permission;
import net.william278.uniform.element.ArgumentElement;
import org.jetbrains.annotations.NotNull;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
@ -44,7 +46,7 @@ import java.util.logging.Level;
public class UserDataCommand extends PluginCommand {
public UserDataCommand(@NotNull HuskSync plugin) {
super("userdata", List.of("playerdata"), Permission.Default.IF_OP, ExecutionScope.ALL, plugin);
super("userdata", Collections.singletonList("playerdata"), Permission.Default.IF_OP, ExecutionScope.ALL, plugin);
}
@Override
@ -59,7 +61,8 @@ public class UserDataCommand extends PluginCommand {
// Show the latest snapshot
private void viewLatestSnapshot(@NotNull CommandUser executor, @NotNull User user) {
plugin.getDatabase().getLatestSnapshot(user).ifPresentOrElse(
OptionalUtil.ifPresentOrElse(
plugin.getDatabase().getLatestSnapshot(user),
data -> {
if (data.isInvalid()) {
plugin.getLocales().getLocale("error_invalid_data", data.getInvalidReason(plugin))
@ -76,7 +79,8 @@ public class UserDataCommand extends PluginCommand {
// Show the specified snapshot
private void viewSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) {
plugin.getDatabase().getSnapshot(user, version).ifPresentOrElse(
OptionalUtil.ifPresentOrElse(
plugin.getDatabase().getSnapshot(user, version),
data -> {
if (data.isInvalid()) {
plugin.getLocales().getLocale("error_invalid_data", data.getInvalidReason(plugin))
@ -121,7 +125,7 @@ public class UserDataCommand extends PluginCommand {
// Restore a snapshot
private void restoreSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) {
final Optional<DataSnapshot.Packed> optionalData = plugin.getDatabase().getSnapshot(user, version);
if (optionalData.isEmpty()) {
if (!optionalData.isPresent()) {
plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(executor::sendMessage);
return;
@ -155,7 +159,7 @@ public class UserDataCommand extends PluginCommand {
// Pin a snapshot
private void pinSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) {
final Optional<DataSnapshot.Packed> optionalData = plugin.getDatabase().getSnapshot(user, version);
if (optionalData.isEmpty()) {
if (!optionalData.isPresent()) {
plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(executor::sendMessage);
return;
@ -177,7 +181,7 @@ public class UserDataCommand extends PluginCommand {
private void dumpSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version,
@NotNull DumpType type) {
final Optional<DataSnapshot.Packed> data = plugin.getDatabase().getSnapshot(user, version);
if (data.isEmpty()) {
if (!data.isPresent()) {
plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(executor::sendMessage);
return;
@ -265,12 +269,15 @@ public class UserDataCommand extends PluginCommand {
private <S> ArgumentElement<S, DumpType> dumpType() {
return new ArgumentElement<>("type", reader -> {
final String type = reader.readString();
return switch (type.toLowerCase(Locale.ENGLISH)) {
case "web" -> DumpType.WEB;
case "file" -> DumpType.FILE;
default -> throw CommandSyntaxException.BUILT_IN_EXCEPTIONS
switch (type.toLowerCase(Locale.ENGLISH)) {
case "web":
return DumpType.WEB;
case "file":
return DumpType.FILE;
default:
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS
.dispatcherUnknownArgument().createWithContext(reader);
};
}
}, (context, builder) -> {
builder.suggest("web");
builder.suggest("file");

@ -42,14 +42,14 @@ import java.util.Optional;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Locales {
static final String CONFIG_HEADER = """
HuskSync - Locales
Developed by William278
See plugin about menu for international locale credits
Formatted in MineDown: https://github.com/Phoenix616/MineDown
Translate HuskSync: https://william278.net/docs/husksync/translations""";
static final String CONFIG_HEADER =
"┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n" +
"┃ HuskSync - Locales ┃\n" +
"┃ Developed by William278 ┃\n" +
"┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n" +
"┣╸ See plugin about menu for international locale credits\n" +
"┣╸ Formatted in MineDown: https://github.com/Phoenix616/MineDown\n" +
"┗╸ Translate HuskSync: https://william278.net/docs/husksync/translations";
protected static final String DEFAULT_LOCALE = "en-gb";
@ -202,9 +202,9 @@ public class Locales {
/**
* Displays the notification in an Advancement Toast
*
* @deprecated No longer supported
* @deprecated No longer supported, since 3.6.7
*/
@Deprecated(since = "3.6.7")
@Deprecated
TOAST,
/**

@ -27,6 +27,7 @@ import lombok.NoArgsConstructor;
import org.jetbrains.annotations.NotNull;
import java.nio.file.Path;
import java.nio.file.Paths;
@Getter
@Configuration
@ -34,13 +35,13 @@ import java.nio.file.Path;
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Server {
static final String CONFIG_HEADER = """
HuskSync - Server ID
Developed by William278
This file should contain the ID of this server as defined in your proxy config.
If you join it using /server alpha, then set it to 'alpha' (case-sensitive)""";
static final String CONFIG_HEADER =
"┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n" +
"┃ HuskSync - Server ID ┃\n" +
"┃ Developed by William278 ┃\n" +
"┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n" +
"┣╸ This file should contain the ID of this server as defined in your proxy config.\n" +
"┗╸ If you join it using /server alpha, then set it to 'alpha' (case-sensitive)";
private String name = getDefault();
@ -55,14 +56,14 @@ public class Server {
@NotNull
private static String getDefault() {
final String serverFolder = System.getProperty("user.dir");
return serverFolder == null ? "server" : Path.of(serverFolder).getFileName().toString().trim();
return serverFolder == null ? "server" : Paths.get(serverFolder).getFileName().toString().trim();
}
@Override
public boolean equals(@NotNull Object other) {
// If the name of this server matches another, the servers are the same.
if (other instanceof Server server) {
return server.getName().equalsIgnoreCase(this.getName());
if (other instanceof Server) {
return ((Server) other).getName().equalsIgnoreCase(this.getName());
}
return super.equals(other);
}

@ -19,6 +19,7 @@
package net.william278.husksync.config;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import de.exlll.configlib.Comment;
import de.exlll.configlib.Configuration;
@ -47,15 +48,14 @@ import java.util.Map;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Settings {
protected static final String CONFIG_HEADER = """
HuskSync Config
Developed by William278
Information: https://william278.net/project/husksync
Config Help: https://william278.net/docs/husksync/config-file/
Documentation: https://william278.net/docs/husksync""";
protected static final String CONFIG_HEADER =
"┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n" +
"┃ HuskSync Config ┃\n" +
"┃ Developed by William278 ┃\n" +
"┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n" +
"┣╸ Information: https://william278.net/project/husksync\n" +
"┣╸ Config Help: https://william278.net/docs/husksync/config-file/\n" +
"┗╸ Documentation: https://william278.net/docs/husksync";
// Top-level settings
@Comment({"Locale of the default language file to use.", "Docs: https://william278.net/docs/husksync/translations"})
private String language = Locales.DEFAULT_LOCALE;
@ -207,7 +207,7 @@ public class Settings {
@Comment({"List of save cause IDs for which a snapshot will be automatically pinned (so it won't be rotated).",
"Docs: https://william278.net/docs/husksync/data-rotation#save-causes"})
@Getter(AccessLevel.NONE)
private List<String> autoPinnedSaveCauses = List.of(
private List<String> autoPinnedSaveCauses = ImmutableList.of(
DataSnapshot.SaveCause.INVENTORY_COMMAND.name(),
DataSnapshot.SaveCause.ENDERCHEST_COMMAND.name(),
DataSnapshot.SaveCause.BACKUP_RESTORE.name(),
@ -265,7 +265,7 @@ public class Settings {
private Map<String, Boolean> features = Identifier.getConfigMap();
@Comment("Commands which should be blocked before a player has finished syncing (Use * to block all commands)")
private List<String> blacklistedCommandsWhileLocked = new ArrayList<>(List.of("*"));
private List<String> blacklistedCommandsWhileLocked = new ArrayList<>(ImmutableList.of("*"));
@Comment("Configuration for how to sync attributes")
private AttributeSettings attributes = new AttributeSettings();
@ -278,21 +278,21 @@ public class Settings {
@Comment({"Which attributes should not be saved when syncing users. Supports wildcard matching.",
"(e.g. ['minecraft:generic.max_health', 'minecraft:generic.*'])"})
@Getter(AccessLevel.NONE)
private List<String> ignoredAttributes = new ArrayList<>(List.of(""));
private List<String> ignoredAttributes = new ArrayList<>(ImmutableList.of(""));
@Comment({"Which modifiers should not be saved when syncing users. Supports wildcard matching.",
"(e.g. ['minecraft:effect.speed', 'minecraft:effect.*'])"})
@Getter(AccessLevel.NONE)
private List<String> ignoredModifiers = new ArrayList<>(List.of(
private List<String> ignoredModifiers = new ArrayList<>(ImmutableList.of(
"minecraft:effect.*", "minecraft:creative_mode_*"
));
private boolean matchesWildcard(@NotNull String pat, @NotNull String value) {
if (!pat.contains(":")) {
pat = "minecraft:%s".formatted(pat);
pat = "minecraft:" + pat;
}
if (!value.contains(":")) {
value = "minecraft:%s".formatted(value);
value = "minecraft:" + value;
}
return pat.contains("*") ? value.matches(pat.replace("*", ".*")) : pat.equals(value);
}

@ -25,6 +25,7 @@ import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Value;
import lombok.experimental.Accessors;
import net.kyori.adventure.key.Key;
import net.william278.husksync.HuskSync;
@ -65,9 +66,13 @@ public interface Data {
return getStack().length;
}
record Stack(@NotNull String material, int amount, @Nullable String name,
@Nullable List<String> lore, @NotNull List<String> enchantments) {
@Value
class Stack {
@NotNull String material;
int amount;
@Nullable String name;
@Nullable List<String> lore;
@NotNull List<String> enchantments;
}
default boolean isEmpty() {
@ -129,23 +134,32 @@ public interface Data {
@NotNull
List<Effect> getActiveEffects();
@Value
class Effect {
/**
* Represents a potion effect
*
* @param type the type of potion effect
* @param amplifier the amplifier of the potion effect
* @param duration the duration of the potion effect
* @param isAmbient whether the potion effect is ambient
* @param showParticles whether the potion effect shows particles
* @param hasIcon whether the potion effect displays a HUD icon
* The type of potion effect
*/
record Effect(@SerializedName("type") @NotNull String type,
@SerializedName("amplifier") int amplifier,
@SerializedName("duration") int duration,
@SerializedName("is_ambient") boolean isAmbient,
@SerializedName("show_particles") boolean showParticles,
@SerializedName("has_icon") boolean hasIcon) {
@SerializedName("type") @NotNull String type;
/**
* The amplifier of the potion effect
*/
@SerializedName("amplifier") int amplifier;
/**
* The duration of the potion effect
*/
@SerializedName("duration") int duration;
/**
* Whether the potion effect is ambient
*/
@SerializedName("is_ambient") boolean isAmbient;
/**
* Whether the potion effect shows particles
*/
@SerializedName("show_particles") boolean showParticles;
/**
* Whether the potion effect displays a HUD icon
*/
@SerializedName("has_icon") boolean hasIcon;
}
}
@ -162,7 +176,9 @@ public interface Data {
@NotNull
default List<Advancement> getCompletedExcludingRecipes() {
return getCompleted().stream().filter(adv -> !adv.getKey().startsWith(RECIPE_ADVANCEMENT)).toList();
return getCompleted().stream()
.filter(adv -> !adv.getKey().startsWith(RECIPE_ADVANCEMENT))
.collect(Collectors.toList());
}
void setCompleted(@NotNull List<Advancement> completed);
@ -249,11 +265,11 @@ public interface Data {
void setWorld(@NotNull World world);
record World(
@SerializedName("name") @NotNull String name,
@SerializedName("uuid") @NotNull UUID uuid,
@SerializedName("environment") @NotNull String environment
) {
@Value
class World {
@SerializedName("name") @NotNull String name;
@SerializedName("uuid") @NotNull UUID uuid;
@SerializedName("environment") @NotNull String environment;
}
}
@ -295,17 +311,17 @@ public interface Data {
void setHealth(double health);
/**
* @deprecated Use {@link Attributes#getMaxHealth()} instead
* @deprecated Use {@link Attributes#getMaxHealth()} instead, since 3.5
*/
@Deprecated(forRemoval = true, since = "3.5")
@Deprecated
default double getMaxHealth() {
return getHealth();
}
/**
* @deprecated Use {@link Attributes#setMaxHealth(double)} instead
* @deprecated Use {@link Attributes#setMaxHealth(double)} instead, since 3.5
*/
@Deprecated(forRemoval = true, since = "3.5")
@Deprecated
default void setMaxHealth(double maxHealth) {
}
@ -323,11 +339,11 @@ public interface Data {
List<Attribute> getAttributes();
record Attribute(
@NotNull String name,
double baseValue,
@NotNull Set<Modifier> modifiers
) {
@Value
class Attribute {
@NotNull String name;
double baseValue;
@NotNull Set<Modifier> modifiers;
public double getValue() {
double value = baseValue;
@ -366,7 +382,8 @@ public interface Data {
@Override
public boolean equals(Object obj) {
if (obj instanceof Modifier other) {
if (obj instanceof Modifier) {
Modifier other = (Modifier) obj;
if (uuid == null || other.uuid == null) {
return name.equals(other.name);
}
@ -376,12 +393,16 @@ public interface Data {
}
public double modify(double value) {
return switch (operationType) {
case 0 -> value + amount;
case 1 -> value * amount;
case 2 -> value * (1 + amount);
default -> value;
};
switch (operationType) {
case 0:
return value + amount;
case 1:
return value * amount;
case 2:
return value * (1 + amount);
default:
return value;
}
}
public boolean hasUuid() {
@ -397,12 +418,12 @@ public interface Data {
default Optional<Attribute> getAttribute(@NotNull Key key) {
return getAttributes().stream()
.filter(attribute -> attribute.name().equals(key.asString()))
.filter(attribute -> attribute.getName().equals(key.asString()))
.findFirst();
}
default void removeAttribute(@NotNull Key key) {
getAttributes().removeIf(attribute -> attribute.name().equals(key.asString()));
getAttributes().removeIf(attribute -> attribute.getName().equals(key.asString()));
}
default double getMaxHealth() {
@ -481,9 +502,9 @@ public interface Data {
*
* @return {@code false} since v3.5
* @deprecated Moved to its own data type. This will always return {@code false}.
* Use {@link FlightStatus#isAllowFlight()} instead
* Use {@link FlightStatus#isAllowFlight()} instead, since 3.5
*/
@Deprecated(forRemoval = true, since = "3.5")
@Deprecated
default boolean getAllowFlight() {
return false;
}
@ -492,9 +513,9 @@ public interface Data {
* Set if the player can fly.
*
* @deprecated Moved to its own data type.
* Use {@link FlightStatus#setAllowFlight(boolean)} instead
* Use {@link FlightStatus#setAllowFlight(boolean)} instead, since 3.5
*/
@Deprecated(forRemoval = true, since = "3.5")
@Deprecated
default void setAllowFlight(boolean allowFlight) {
}
@ -503,9 +524,9 @@ public interface Data {
*
* @return {@code false} since v3.5
* @deprecated Moved to its own data type. This will always return {@code false}.
* Use {@link FlightStatus#isFlying()} instead
* Use {@link FlightStatus#isFlying()} instead, since 3.5
*/
@Deprecated(forRemoval = true, since = "3.5")
@Deprecated
default boolean getIsFlying() {
return false;
}
@ -514,9 +535,9 @@ public interface Data {
* Set if the player is flying.
*
* @deprecated Moved to its own data type.
* Use {@link FlightStatus#setFlying(boolean)} instead
* Use {@link FlightStatus#setFlying(boolean)} instead, since 3.5
*/
@Deprecated(forRemoval = true, since = "3.5")
@Deprecated
default void setIsFlying(boolean isFlying) {
}

@ -383,7 +383,7 @@ public class DataSnapshot {
private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
@NotNull String saveCause, @NotNull String serverName, @NotNull TreeMap<Identifier, Data> data,
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion) {
super(id, pinned, timestamp, saveCause, serverName, Map.of(), minecraftVersion, platformType, formatVersion);
super(id, pinned, timestamp, saveCause, serverName, Collections.emptyMap(), minecraftVersion, platformType, formatVersion);
this.deserialized = data;
}
@ -392,7 +392,10 @@ public class DataSnapshot {
private TreeMap<Identifier, Data> deserializeData(@NotNull HuskSync plugin) {
return data.entrySet().stream()
.filter(e -> plugin.getIdentifier(e.getKey()).isPresent())
.map(entry -> Map.entry(plugin.getIdentifier(entry.getKey()).orElseThrow(), entry.getValue()))
.map(entry -> Maps.immutableEntry(
plugin.getIdentifier(entry.getKey()).orElseThrow(() -> new IllegalStateException("Invalid identifier")),
entry.getValue()
))
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> plugin.deserializeData(entry.getKey(), entry.getValue(), getMinecraftVersion()),
@ -939,7 +942,7 @@ public class DataSnapshot {
@NotNull
public String getLocale(@NotNull HuskSync plugin) {
return plugin.getLocales()
.getRawLocale("save_cause_%s".formatted(name().toLowerCase(Locale.ENGLISH)))
.getRawLocale("save_cause_" + name().toLowerCase(Locale.ENGLISH))
.orElse(getDisplayName());
}

@ -19,6 +19,9 @@
package net.william278.husksync.data;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import lombok.*;
import net.kyori.adventure.key.InvalidKeyException;
import net.kyori.adventure.key.Key;
@ -150,7 +153,7 @@ public class Identifier {
private static Identifier huskSync(@Subst("null") @NotNull String name,
@SuppressWarnings("SameParameterValue") boolean configDefault,
@NotNull Dependency... dependents) throws InvalidKeyException {
return new Identifier(Key.key("husksync", name), configDefault, Set.of(dependents));
return new Identifier(Key.key("husksync", name), configDefault, ImmutableSet.copyOf(dependents));
}
/**
@ -163,7 +166,7 @@ public class Identifier {
@ApiStatus.Internal
@SuppressWarnings("unchecked")
public static Map<String, Boolean> getConfigMap() {
return Map.ofEntries(Stream.of(
return ImmutableMap.ofEntries(Stream.of(
INVENTORY, ENDER_CHEST, POTION_EFFECTS, ADVANCEMENTS, LOCATION, STATISTICS,
HEALTH, HUNGER, ATTRIBUTES, EXPERIENCE, GAME_MODE, FLIGHT_STATUS, PERSISTENT_DATA
)
@ -230,7 +233,8 @@ public class Identifier {
*/
@Override
public boolean equals(Object obj) {
if (obj instanceof Identifier other) {
if (obj instanceof Identifier) {
Identifier other = (Identifier) obj;
return key.equals(other.key);
}
return false;
@ -239,7 +243,7 @@ public class Identifier {
// Get the config entry for the identifier
@NotNull
private Map.Entry<String, Boolean> getConfigEntry() {
return Map.entry(getKeyValue(), enabledByDefault);
return Maps.immutableEntry(getKeyValue(), enabledByDefault);
}
/**
@ -260,7 +264,7 @@ public class Identifier {
if (i1.dependsOn(i2)) {
if (i2.dependsOn(i1)) {
throw new IllegalArgumentException(
"Found circular dependency between %s and %s".formatted(i1.getKey(), i2.getKey())
"Found circular dependency between " + i1.getKey() + " and " + i2.getKey()
);
}
return 1;
@ -310,7 +314,8 @@ public class Identifier {
@Override
public boolean equals(Object obj) {
if (obj instanceof Dependency other) {
if (obj instanceof Dependency) {
Dependency other = (Dependency) obj;
return key.equals(other.key);
}
return false;

@ -26,6 +26,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.logging.Level;
import java.util.stream.Collectors;
public interface SerializerRegistry {
@ -52,7 +53,7 @@ public interface SerializerRegistry {
@SuppressWarnings("unchecked")
default void registerSerializer(@NotNull Identifier id, @NotNull Serializer<? extends Data> serializer) {
if (id.isCustom()) {
getPlugin().log(Level.INFO, "Registered custom data type: %s".formatted(id));
getPlugin().log(Level.INFO, "Registered custom data type: " + id);
}
id.setEnabled(id.isCustom() || getPlugin().getSettings().getSynchronization().isFeatureEnabled(id));
getSerializers().put(id, (Serializer<Data>) serializer);
@ -72,11 +73,11 @@ public interface SerializerRegistry {
final List<String> unmet = identifier.getDependencies().stream()
.filter(Identifier.Dependency::isRequired)
.filter(dep -> !isDataTypeAvailable(dep.getKey().asString()))
.map(dep -> dep.getKey().asString()).toList();
.map(dep -> dep.getKey().asString())
.collect(Collectors.toList());
if (!unmet.isEmpty()) {
identifier.setEnabled(false);
getPlugin().log(Level.WARNING, "Disabled %s syncing as the following types need to be on: %s"
.formatted(identifier, String.join(", ", unmet)));
getPlugin().log(Level.WARNING, "Disabled " + identifier + " syncing as the following types need to be on: " + String.join(", ", unmet));
}
});
}
@ -116,7 +117,7 @@ public interface SerializerRegistry {
@NotNull
default String serializeData(@NotNull Identifier identifier, @NotNull Data data) throws IllegalStateException {
return getSerializer(identifier).map(serializer -> serializer.serialize(data))
.orElseThrow(() -> new IllegalStateException("No serializer found for %s".formatted(identifier)));
.orElseThrow(() -> new IllegalStateException("No serializer found for " + identifier));
}
/**
@ -133,7 +134,7 @@ public interface SerializerRegistry {
default Data deserializeData(@NotNull Identifier identifier, @NotNull String data,
@NotNull Version dataMcVersion) throws IllegalStateException {
return getSerializer(identifier).map(serializer -> serializer.deserialize(data, dataMcVersion)).orElseThrow(
() -> new IllegalStateException("No serializer found for %s".formatted(identifier))
() -> new IllegalStateException("No serializer found for " + identifier)
);
}
@ -144,10 +145,10 @@ public interface SerializerRegistry {
* @param data the data to deserialize
* @return the deserialized data
* @since 3.5.4
* @deprecated Use {@link #deserializeData(Identifier, String, Version)} instead
* @deprecated Use {@link #deserializeData(Identifier, String, Version)} instead, since 3.6.5
*/
@NotNull
@Deprecated(since = "3.6.5")
@Deprecated
default Data deserializeData(@NotNull Identifier identifier, @NotNull String data) {
return deserializeData(identifier, data, getPlugin().getMinecraftVersion());
}
@ -164,7 +165,7 @@ public interface SerializerRegistry {
}
// Returns if a data type is available and enabled in the config
private boolean isDataTypeAvailable(@NotNull String key) {
default boolean isDataTypeAvailable(@NotNull String key) {
return getIdentifier(key).map(Identifier::isEnabled).orElse(false);
}

@ -19,6 +19,7 @@
package net.william278.husksync.data;
import com.google.common.collect.Maps;
import net.william278.desertwell.util.ThrowingConsumer;
import net.william278.husksync.HuskSync;
import org.jetbrains.annotations.ApiStatus;
@ -44,7 +45,7 @@ public interface UserDataHolder extends DataHolder {
default Map<Identifier, Data> getData() {
return getPlugin().getRegisteredDataTypes().stream()
.filter(Identifier::isEnabled)
.map(id -> Map.entry(id, getData(id)))
.map(id -> Maps.immutableEntry(id, getData(id)))
.filter(data -> data.getValue().isPresent())
.collect(HashMap::new, (map, data) -> map.put(data.getKey(), data.getValue().get()), HashMap::putAll);
}

@ -19,11 +19,14 @@
package net.william278.husksync.database;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import lombok.Getter;
import net.william278.husksync.HuskSync;
import net.william278.husksync.config.Settings;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.User;
import net.william278.husksync.util.InputStreamUtil;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull;
@ -56,8 +59,10 @@ public abstract class Database {
@SuppressWarnings("SameParameterValue")
@NotNull
protected final String[] getSchemaStatements(@NotNull String schemaFileName) throws IOException {
return formatStatementTables(new String(Objects.requireNonNull(plugin.getResource(schemaFileName))
.readAllBytes(), StandardCharsets.UTF_8)).split(";");
return formatStatementTables(new String(
InputStreamUtil.readAllBytes(Objects.requireNonNull(plugin.getResource(schemaFileName))),
StandardCharsets.UTF_8
)).split(";");
}
/**
@ -285,13 +290,13 @@ public abstract class Database {
@NotNull
private Map.Entry<String, String> toEntry() {
return Map.entry(name().toLowerCase(Locale.ENGLISH), defaultName);
return Maps.immutableEntry(name().toLowerCase(Locale.ENGLISH), defaultName);
}
@SuppressWarnings("unchecked")
@NotNull
public static Map<String, String> getDefaults() {
return Map.ofEntries(Arrays.stream(values())
return ImmutableMap.ofEntries(Arrays.stream(values())
.map(TableName::toEntry)
.toArray(Map.Entry[]::new));
}

@ -30,6 +30,7 @@ import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.database.mongo.MongoCollectionHelper;
import net.william278.husksync.database.mongo.MongoConnectionHandler;
import net.william278.husksync.user.User;
import net.william278.husksync.util.OptionalUtil;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.bson.types.Binary;
@ -43,6 +44,7 @@ import java.util.Optional;
import java.util.TimeZone;
import java.util.UUID;
import java.util.logging.Level;
import java.util.stream.Collectors;
public class MongoDbDatabase extends Database {
private MongoConnectionHandler mongoConnectionHandler;
@ -103,7 +105,7 @@ public class MongoDbDatabase extends Database {
@Override
public void ensureUser(@NotNull User user) {
try {
getUser(user.getUuid()).ifPresentOrElse(
OptionalUtil.ifPresentOrElse(getUser(user.getUuid()),
existingUser -> {
if (!existingUser.getUsername().equals(user.getUsername())) {
// Update a user's name if it has changed in the database
@ -277,7 +279,8 @@ public class MongoDbDatabase extends Database {
protected void rotateSnapshots(@NotNull User user) {
try {
final List<DataSnapshot.Packed> unpinnedUserData = getAllSnapshots(user).stream()
.filter(dataSnapshot -> !dataSnapshot.isPinned()).toList();
.filter(dataSnapshot -> !dataSnapshot.isPinned())
.collect(Collectors.toList());
final int maxSnapshots = plugin.getSettings().getSynchronization().getMaxUserDataSnapshots();
if (unpinnedUserData.size() > maxSnapshots) {

@ -20,11 +20,12 @@
package net.william278.husksync.database;
import com.google.common.collect.Lists;
import com.zaxxer.hikari.HikariDataSource;
import net.william278.husksync.HuskSync;
import net.william278.husksync.adapter.DataAdapter;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.User;
import net.william278.husksync.util.OptionalUtil;
import org.inksnow.cputil.db.AuroraDatabase;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull;
@ -34,6 +35,7 @@ import java.sql.*;
import java.time.OffsetDateTime;
import java.util.*;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static net.william278.husksync.config.Settings.DatabaseSettings;
@ -41,15 +43,17 @@ public class MySqlDatabase extends Database {
private static final String DATA_POOL_NAME = "HuskSyncHikariPool";
private final String flavor;
private final String driverClass;
private HikariDataSource dataSource;
private final org.inksnow.cputil.db.Database databaseType;
private AuroraDatabase dataSource;
public MySqlDatabase(@NotNull HuskSync plugin) {
super(plugin);
final Type type = plugin.getSettings().getDatabase().getType();
this.flavor = type.getProtocol();
this.driverClass = type == Type.MARIADB ? "org.mariadb.jdbc.Driver" : "com.mysql.cj.jdbc.Driver";
this.databaseType = type == Type.MARIADB
? new org.inksnow.cputil.db.mariadb.MariadbDatabase()
: new org.inksnow.cputil.db.mysql.MysqlDatabase();
}
/**
@ -72,21 +76,20 @@ public class MySqlDatabase extends Database {
public void initialize() throws IllegalStateException {
// Initialize the Hikari pooled connection
final DatabaseSettings.DatabaseCredentials credentials = plugin.getSettings().getDatabase().getCredentials();
dataSource = new HikariDataSource();
dataSource.setDriverClassName(driverClass);
dataSource.setJdbcUrl(String.format("jdbc:%s://%s:%s/%s%s",
try {
dataSource = AuroraDatabase.builder()
.databaseType(databaseType)
.jdbcUrl(String.format("jdbc:%s://%s:%s/%s%s",
flavor,
credentials.getHost(),
credentials.getPort(),
credentials.getDatabase(),
credentials.getParameters()
));
// Authenticate with the database
dataSource.setUsername(credentials.getUsername());
dataSource.setPassword(credentials.getPassword());
// Set connection pool options
))
.username(credentials.getUsername())
.password(credentials.getPassword())
.extension(dataSource -> {
final DatabaseSettings.PoolSettings pool = plugin.getSettings().getDatabase().getConnectionPool();
dataSource.setMaximumPoolSize(pool.getMaximumPoolSize());
dataSource.setMinimumIdle(pool.getMinimumIdle());
@ -94,26 +97,23 @@ public class MySqlDatabase extends Database {
dataSource.setKeepaliveTime(pool.getKeepaliveTime());
dataSource.setConnectionTimeout(pool.getConnectionTimeout());
dataSource.setPoolName(DATA_POOL_NAME);
// Set additional connection pool properties
final Properties properties = new Properties();
properties.putAll(
Map.of("cachePrepStmts", "true",
"prepStmtCacheSize", "250",
"prepStmtCacheSqlLimit", "2048",
"useServerPrepStmts", "true",
"useLocalSessionState", "true",
"useLocalTransactionState", "true"
));
properties.putAll(
Map.of(
"rewriteBatchedStatements", "true",
"cacheResultSetMetadata", "true",
"cacheServerConfiguration", "true",
"elideSetAutoCommits", "true",
"maintainTimeStats", "false")
);
dataSource.setDataSourceProperties(properties);
})
.driverProperty("cachePrepStmts", "true")
.driverProperty("prepStmtCacheSize", "250")
.driverProperty("prepStmtCacheSqlLimit", "2048")
.driverProperty("useServerPrepStmts", "true")
.driverProperty("useLocalSessionState", "true")
.driverProperty("useLocalTransactionState", "true")
.driverProperty("rewriteBatchedStatements", "true")
.driverProperty("cacheResultSetMetadata", "true")
.driverProperty("cacheServerConfiguration", "true")
.driverProperty("elideSetAutoCommits", "true")
.driverProperty("maintainTimeStats", "false")
.build();
} catch (IOException e) {
throw new IllegalStateException("Failed to initialize the Aurora database", e);
}
// Prepare database schema; make tables if they don't exist
try (Connection connection = dataSource.getConnection()) {
@ -135,15 +135,14 @@ public class MySqlDatabase extends Database {
@Blocking
@Override
public void ensureUser(@NotNull User user) {
getUser(user.getUuid()).ifPresentOrElse(
OptionalUtil.ifPresentOrElse(getUser(user.getUuid()),
existingUser -> {
if (!existingUser.getUsername().equals(user.getUsername())) {
// Update a user's name if it has changed in the database
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
UPDATE `%users_table%`
SET `username`=?
WHERE `uuid`=?"""))) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(
"UPDATE `%users_table%` SET `username`=? WHERE `uuid`=?"
))) {
statement.setString(1, user.getUsername());
statement.setString(2, existingUser.getUuid().toString());
@ -158,9 +157,9 @@ public class MySqlDatabase extends Database {
() -> {
// Insert new player data into the database
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
INSERT INTO `%users_table%` (`uuid`,`username`)
VALUES (?,?);"""))) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(
"INSERT INTO `%users_table%` (`uuid`,`username`) VALUES (?,?);"
))) {
statement.setString(1, user.getUuid().toString());
statement.setString(2, user.getUsername());
@ -177,10 +176,9 @@ public class MySqlDatabase extends Database {
@Override
public Optional<User> getUser(@NotNull UUID uuid) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT `uuid`, `username`
FROM `%users_table%`
WHERE `uuid`=?"""))) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(
"SELECT `uuid`, `username` FROM `%users_table%` WHERE `uuid`=?"
))) {
statement.setString(1, uuid.toString());
@ -200,10 +198,9 @@ public class MySqlDatabase extends Database {
@Override
public Optional<User> getUserByName(@NotNull String username) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT `uuid`, `username`
FROM `%users_table%`
WHERE `username`=?"""))) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(
"SELECT `uuid`, `username` FROM `%users_table%` WHERE `username`=?"
))) {
statement.setString(1, username);
final ResultSet resultSet = statement.executeQuery();
@ -222,12 +219,13 @@ public class MySqlDatabase extends Database {
@Override
public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT `version_uuid`, `timestamp`, `data`
FROM `%user_data_table%`
WHERE `player_uuid`=?
ORDER BY `timestamp` DESC
LIMIT 1;"""))) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(
"SELECT `version_uuid`, `timestamp`, `data` " +
"FROM `%user_data_table%` " +
"WHERE `player_uuid`=? " +
"ORDER BY `timestamp` DESC " +
"LIMIT 1;"
))) {
statement.setString(1, user.getUuid().toString());
final ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) {
@ -253,11 +251,12 @@ public class MySqlDatabase extends Database {
public List<DataSnapshot.Packed> getAllSnapshots(@NotNull User user) {
final List<DataSnapshot.Packed> retrievedData = Lists.newArrayList();
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT `version_uuid`, `timestamp`, `data`
FROM `%user_data_table%`
WHERE `player_uuid`=?
ORDER BY `timestamp` DESC;"""))) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(
"SELECT `version_uuid`, `timestamp`, `data` " +
"FROM `%user_data_table%` " +
"WHERE `player_uuid`=? " +
"ORDER BY `timestamp` DESC;"
))) {
statement.setString(1, user.getUuid().toString());
final ResultSet resultSet = statement.executeQuery();
while (resultSet.next()) {
@ -282,12 +281,13 @@ public class MySqlDatabase extends Database {
@Override
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT `version_uuid`, `timestamp`, `data`
FROM `%user_data_table%`
WHERE `player_uuid`=? AND `version_uuid`=?
ORDER BY `timestamp` DESC
LIMIT 1;"""))) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(
"SELECT `version_uuid`, `timestamp`, `data` " +
"FROM `%user_data_table%` " +
"WHERE `player_uuid`=? AND `version_uuid`=? " +
"ORDER BY `timestamp` DESC " +
"LIMIT 1;"
))) {
statement.setString(1, user.getUuid().toString());
statement.setString(2, versionUuid.toString());
final ResultSet resultSet = statement.executeQuery();
@ -311,17 +311,17 @@ public class MySqlDatabase extends Database {
@Override
protected void rotateSnapshots(@NotNull User user) {
final List<DataSnapshot.Packed> unpinnedUserData = getAllSnapshots(user).stream()
.filter(dataSnapshot -> !dataSnapshot.isPinned()).toList();
.filter(dataSnapshot -> !dataSnapshot.isPinned())
.collect(Collectors.toList());
final int maxSnapshots = plugin.getSettings().getSynchronization().getMaxUserDataSnapshots();
if (unpinnedUserData.size() > maxSnapshots) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
DELETE FROM `%user_data_table%`
WHERE `player_uuid`=?
AND `pinned` IS FALSE
ORDER BY `timestamp` ASC
LIMIT %entry_count%;""".replace("%entry_count%",
Integer.toString(unpinnedUserData.size() - maxSnapshots))))) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(
"DELETE FROM `%user_data_table%` " +
"WHERE `player_uuid`=? " +
"AND `pinned` IS FALSE " +
"ORDER BY `timestamp` ASC " +
"LIMIT " + (unpinnedUserData.size() - maxSnapshots) + ";"))) {
statement.setString(1, user.getUuid().toString());
statement.executeUpdate();
}
@ -335,10 +335,11 @@ public class MySqlDatabase extends Database {
@Override
public boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
DELETE FROM `%user_data_table%`
WHERE `player_uuid`=? AND `version_uuid`=?
LIMIT 1;"""))) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(
"DELETE FROM `%user_data_table%` " +
"WHERE `player_uuid`=? AND `version_uuid`=? " +
"LIMIT 1;"
))) {
statement.setString(1, user.getUuid().toString());
statement.setString(2, versionUuid.toString());
return statement.executeUpdate() > 0;
@ -353,11 +354,12 @@ public class MySqlDatabase extends Database {
@Override
protected void rotateLatestSnapshot(@NotNull User user, @NotNull OffsetDateTime within) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
DELETE FROM `%user_data_table%`
WHERE `player_uuid`=? AND `timestamp`>? AND `pinned` IS FALSE
ORDER BY `timestamp` ASC
LIMIT 1;"""))) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(
"DELETE FROM `%user_data_table%` " +
"WHERE `player_uuid`=? AND `timestamp`>? AND `pinned` IS FALSE " +
"ORDER BY `timestamp` ASC " +
"LIMIT 1;"
))) {
statement.setString(1, user.getUuid().toString());
statement.setTimestamp(2, Timestamp.from(within.toInstant()));
statement.executeUpdate();
@ -371,10 +373,11 @@ public class MySqlDatabase extends Database {
@Override
protected void createSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
INSERT INTO `%user_data_table%`
(`player_uuid`,`version_uuid`,`timestamp`,`save_cause`,`pinned`,`data`)
VALUES (?,?,?,?,?,?);"""))) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(
"INSERT INTO `%user_data_table%` " +
"(`player_uuid`,`version_uuid`,`timestamp`,`save_cause`,`pinned`,`data`) " +
"VALUES (?,?,?,?,?,?);"
))) {
statement.setString(1, user.getUuid().toString());
statement.setString(2, data.getId().toString());
statement.setTimestamp(3, Timestamp.from(data.getTimestamp().toInstant()));
@ -392,11 +395,12 @@ public class MySqlDatabase extends Database {
@Override
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
UPDATE `%user_data_table%`
SET `save_cause`=?,`pinned`=?,`data`=?
WHERE `player_uuid`=? AND `version_uuid`=?
LIMIT 1;"""))) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(
"UPDATE `%user_data_table%` " +
"SET `save_cause`=?,`pinned`=?,`data`=? " +
"WHERE `player_uuid`=? AND `version_uuid`=? " +
"LIMIT 1;"
))) {
statement.setString(1, data.getSaveCause().name());
statement.setBoolean(2, data.isPinned());
statement.setBlob(3, new ByteArrayInputStream(data.asBytes(plugin)));

@ -25,6 +25,8 @@ import net.william278.husksync.HuskSync;
import net.william278.husksync.adapter.DataAdapter;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.User;
import net.william278.husksync.util.OptionalUtil;
import org.inksnow.cputil.db.AuroraDatabase;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull;
@ -33,6 +35,7 @@ import java.sql.*;
import java.time.OffsetDateTime;
import java.util.*;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static net.william278.husksync.config.Settings.DatabaseSettings;
@ -40,7 +43,7 @@ public class PostgresDatabase extends Database {
private static final String DATA_POOL_NAME = "HuskSyncHikariPool";
private final String flavor;
private final String driverClass;
private final org.inksnow.cputil.db.Database databaseType;
private HikariDataSource dataSource;
public PostgresDatabase(@NotNull HuskSync plugin) {
@ -48,7 +51,7 @@ public class PostgresDatabase extends Database {
final Type type = plugin.getSettings().getDatabase().getType();
this.flavor = type.getProtocol();
this.driverClass = "org.postgresql.Driver";
this.databaseType = new org.inksnow.cputil.db.postgres.PostgresqlDatabase();
}
/**
@ -71,21 +74,18 @@ public class PostgresDatabase extends Database {
public void initialize() throws IllegalStateException {
// Initialize the Hikari pooled connection
final DatabaseSettings.DatabaseCredentials credentials = plugin.getSettings().getDatabase().getCredentials();
dataSource = new HikariDataSource();
dataSource.setDriverClassName(driverClass);
dataSource.setJdbcUrl(String.format("jdbc:%s://%s:%s/%s%s",
try {
dataSource = AuroraDatabase.builder()
.databaseType(databaseType)
.jdbcUrl(String.format("jdbc:%s://%s:%s/%s%s",
flavor,
credentials.getHost(),
credentials.getPort(),
credentials.getDatabase(),
credentials.getParameters()
));
// Authenticate with the database
dataSource.setUsername(credentials.getUsername());
dataSource.setPassword(credentials.getPassword());
// Set connection pool options
))
.extension(dataSource -> {
final DatabaseSettings.PoolSettings pool = plugin.getSettings().getDatabase().getConnectionPool();
dataSource.setMaximumPoolSize(pool.getMaximumPoolSize());
dataSource.setMinimumIdle(pool.getMinimumIdle());
@ -93,26 +93,25 @@ public class PostgresDatabase extends Database {
dataSource.setKeepaliveTime(pool.getKeepaliveTime());
dataSource.setConnectionTimeout(pool.getConnectionTimeout());
dataSource.setPoolName(DATA_POOL_NAME);
// Set additional connection pool properties
final Properties properties = new Properties();
properties.putAll(
Map.of("cachePrepStmts", "true",
"prepStmtCacheSize", "250",
"prepStmtCacheSqlLimit", "2048",
"useServerPrepStmts", "true",
"useLocalSessionState", "true",
"useLocalTransactionState", "true"
));
properties.putAll(
Map.of(
"rewriteBatchedStatements", "true",
"cacheResultSetMetadata", "true",
"cacheServerConfiguration", "true",
"elideSetAutoCommits", "true",
"maintainTimeStats", "false")
);
dataSource.setDataSourceProperties(properties);
})
.username(credentials.getUsername())
.password(credentials.getPassword())
.driverProperty("cachePrepStmts", "true")
.driverProperty("prepStmtCacheSize", "250")
.driverProperty("prepStmtCacheSqlLimit", "2048")
.driverProperty("useServerPrepStmts", "true")
.driverProperty("useLocalSessionState", "true")
.driverProperty("useLocalTransactionState", "true")
.driverProperty("rewriteBatchedStatements", "true")
.driverProperty("cacheResultSetMetadata", "true")
.driverProperty("cacheServerConfiguration", "true")
.driverProperty("elideSetAutoCommits", "true")
.driverProperty("maintainTimeStats", "false")
.build();
} catch (IOException e) {
throw new IllegalStateException("Failed to initialize the Aurora database", e);
}
// Prepare database schema; make tables if they don't exist
try (Connection connection = dataSource.getConnection()) {
@ -134,15 +133,16 @@ public class PostgresDatabase extends Database {
@Blocking
@Override
public void ensureUser(@NotNull User user) {
getUser(user.getUuid()).ifPresentOrElse(
OptionalUtil.ifPresentOrElse(getUser(user.getUuid()),
existingUser -> {
if (!existingUser.getUsername().equals(user.getUsername())) {
// Update a user's name if it has changed in the database
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
UPDATE "%users_table%"
SET "username"=?
WHERE "uuid"=?"""))) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(
"UPDATE \"%users_table%\" " +
"SET \"username\"=? " +
"WHERE \"uuid\"=?"
))) {
statement.setString(1, user.getUsername());
statement.setObject(2, existingUser.getUuid());
@ -157,9 +157,10 @@ public class PostgresDatabase extends Database {
() -> {
// Insert new player data into the database
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
INSERT INTO "%users_table%" ("uuid","username")
VALUES (?,?);"""))) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(
"INSERT INTO \"%users_table%\" (\"uuid\",\"username\") " +
"VALUES (?,?);"
))) {
statement.setObject(1, user.getUuid());
statement.setString(2, user.getUsername());
@ -176,10 +177,11 @@ public class PostgresDatabase extends Database {
@Override
public Optional<User> getUser(@NotNull UUID uuid) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT "uuid", "username"
FROM "%users_table%"
WHERE "uuid"=?"""))) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(
"SELECT \"uuid\", \"username\" " +
"FROM \"%users_table%\" " +
"WHERE \"uuid\"=?"
))) {
statement.setObject(1, uuid);
@ -199,10 +201,11 @@ public class PostgresDatabase extends Database {
@Override
public Optional<User> getUserByName(@NotNull String username) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT "uuid", "username"
FROM "%users_table%"
WHERE "username"=?"""))) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(
"SELECT \"uuid\", \"username\" " +
"FROM \"%users_table%\" " +
"WHERE \"username\"=?"
))) {
statement.setString(1, username);
final ResultSet resultSet = statement.executeQuery();
@ -221,12 +224,13 @@ public class PostgresDatabase extends Database {
@Override
public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT "version_uuid", "timestamp", "data"
FROM "%user_data_table%"
WHERE "player_uuid"=?
ORDER BY "timestamp" DESC
LIMIT 1;"""))) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(
"SELECT \"version_uuid\", \"timestamp\", \"data\" " +
"FROM \"%user_data_table%\" " +
"WHERE \"player_uuid\"=? " +
"ORDER BY \"timestamp\" DESC " +
"LIMIT 1;"
))) {
statement.setObject(1, user.getUuid());
final ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) {
@ -250,11 +254,12 @@ public class PostgresDatabase extends Database {
public List<DataSnapshot.Packed> getAllSnapshots(@NotNull User user) {
final List<DataSnapshot.Packed> retrievedData = Lists.newArrayList();
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT "version_uuid", "timestamp", "data"
FROM "%user_data_table%"
WHERE "player_uuid"=?
ORDER BY "timestamp" DESC;"""))) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(
"SELECT \"version_uuid\", \"timestamp\", \"data\" " +
"FROM \"%user_data_table%\" " +
"WHERE \"player_uuid\"=? " +
"ORDER BY \"timestamp\" DESC;"
))) {
statement.setObject(1, user.getUuid());
final ResultSet resultSet = statement.executeQuery();
while (resultSet.next()) {
@ -277,12 +282,13 @@ public class PostgresDatabase extends Database {
@Override
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT "version_uuid", "timestamp", "data"
FROM "%user_data_table%"
WHERE "player_uuid"=? AND "version_uuid"=?
ORDER BY "timestamp" DESC
LIMIT 1;"""))) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(
"SELECT \"version_uuid\", \"timestamp\", \"data\" " +
"FROM \"%user_data_table%\" " +
"WHERE \"player_uuid\"=? AND \"version_uuid\"=? " +
"ORDER BY \"timestamp\" DESC " +
"LIMIT 1;"
))) {
statement.setObject(1, user.getUuid());
statement.setObject(2, versionUuid);
final ResultSet resultSet = statement.executeQuery();
@ -304,17 +310,18 @@ public class PostgresDatabase extends Database {
@Override
protected void rotateSnapshots(@NotNull User user) {
final List<DataSnapshot.Packed> unpinnedUserData = getAllSnapshots(user).stream()
.filter(dataSnapshot -> !dataSnapshot.isPinned()).toList();
.filter(dataSnapshot -> !dataSnapshot.isPinned())
.collect(Collectors.toList());
final int maxSnapshots = plugin.getSettings().getSynchronization().getMaxUserDataSnapshots();
if (unpinnedUserData.size() > maxSnapshots) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
DELETE FROM "%user_data_table%"
WHERE "player_uuid"=?
AND "pinned" = FALSE
ORDER BY "timestamp" ASC
LIMIT %entry_count%;""".replace("%entry_count%",
Integer.toString(unpinnedUserData.size() - maxSnapshots))))) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(
"DELETE FROM \"%user_data_table%\"\n" +
"WHERE \"player_uuid\"=?\n" +
"AND \"pinned\" = FALSE\n" +
"ORDER BY \"timestamp\" ASC\n" +
"LIMIT " + (unpinnedUserData.size() - maxSnapshots) + ";"
))) {
statement.setObject(1, user.getUuid());
statement.executeUpdate();
}
@ -328,10 +335,11 @@ public class PostgresDatabase extends Database {
@Override
public boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
DELETE FROM "%user_data_table%"
WHERE "player_uuid"=? AND "version_uuid"=?
LIMIT 1;"""))) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(
"DELETE FROM \"%user_data_table%\" " +
"WHERE \"player_uuid\"=? AND \"version_uuid\"=? " +
"LIMIT 1;"
))) {
statement.setObject(1, user.getUuid());
statement.setString(2, versionUuid.toString());
return statement.executeUpdate() > 0;
@ -346,15 +354,16 @@ public class PostgresDatabase extends Database {
@Override
protected void rotateLatestSnapshot(@NotNull User user, @NotNull OffsetDateTime within) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
DELETE FROM "%user_data_table%"
WHERE "player_uuid"=? AND "timestamp" = (
SELECT "timestamp"
FROM "%user_data_table%"
WHERE "player_uuid"=? AND "timestamp" > ? AND "pinned" = FALSE
ORDER BY "timestamp" ASC
LIMIT 1
);"""))) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(
"DELETE FROM \"%user_data_table%\" " +
"WHERE \"player_uuid\"=? AND \"timestamp\" = ( " +
" SELECT \"timestamp\" " +
" FROM \"%user_data_table%\" " +
" WHERE \"player_uuid\"=? AND \"timestamp\" > ? AND \"pinned\" = FALSE " +
" ORDER BY \"timestamp\" ASC " +
" LIMIT 1 " +
");"
))) {
statement.setObject(1, user.getUuid());
statement.setObject(2, user.getUuid());
statement.setTimestamp(3, Timestamp.from(within.toInstant()));
@ -369,10 +378,11 @@ public class PostgresDatabase extends Database {
@Override
protected void createSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
INSERT INTO "%user_data_table%"
("player_uuid","version_uuid","timestamp","save_cause","pinned","data")
VALUES (?,?,?,?,?,?);"""))) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(
"INSERT INTO \"%user_data_table%\" " +
"(\"player_uuid\",\"version_uuid\",\"timestamp\",\"save_cause\",\"pinned\",\"data\") " +
"VALUES (?,?,?,?,?,?);"
))) {
statement.setObject(1, user.getUuid());
statement.setObject(2, data.getId());
statement.setTimestamp(3, Timestamp.from(data.getTimestamp().toInstant()));
@ -390,11 +400,12 @@ public class PostgresDatabase extends Database {
@Override
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
UPDATE "%user_data_table%"
SET "save_cause"=?,"pinned"=?,"data"=?
WHERE "player_uuid"=? AND "version_uuid"=?
LIMIT 1;"""))) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(
"UPDATE \"%user_data_table%\" " +
"SET \"save_cause\"=?,\"pinned\"=?,\"data\"=? " +
"WHERE \"player_uuid\"=? AND \"version_uuid\"=? " +
"LIMIT 1;"
))) {
statement.setString(1, data.getSaveCause().name());
statement.setBoolean(2, data.isPinned());
statement.setBytes(3, data.asBytes(plugin));

@ -34,6 +34,7 @@ import net.william278.husksync.data.DataSnapshot;
import org.jetbrains.annotations.NotNull;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.logging.Level;
@ -248,7 +249,8 @@ public class PlanHook {
return getLatestSnapshot(playerUUID)
.flatMap(DataHolder::getAdvancements)
.map(Data.Advancements::getCompleted)
.stream().count();
.map(List::size)
.orElse(0);
}
@Conditional("hasSynced")

@ -19,6 +19,8 @@
package net.william278.husksync.listener;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.Data;
import net.william278.husksync.data.DataSnapshot;
@ -159,13 +161,13 @@ public abstract class EventListener {
@NotNull
private Map.Entry<String, String> toEntry() {
return Map.entry(name().toLowerCase(), defaultPriority.name());
return Maps.immutableEntry(name().toLowerCase(), defaultPriority.name());
}
@SuppressWarnings("unchecked")
@NotNull
public static Map<String, String> getDefaults() {
return Map.ofEntries(Arrays.stream(values())
return ImmutableMap.ofEntries(Arrays.stream(values())
.map(ListenerType::toEntry)
.toArray(Map.Entry[]::new));
}

@ -19,6 +19,7 @@
package net.william278.husksync.migrator;
import com.google.common.base.Strings;
import net.william278.husksync.HuskSync;
import org.jetbrains.annotations.NotNull;
@ -56,7 +57,9 @@ public abstract class Migrator {
* @return The data string obfuscated with stars (*)
*/
protected final String obfuscateDataString(@NotNull String dataString) {
return (dataString.length() > 1 ? dataString.charAt(0) + "*".repeat(dataString.length() - 1) : "");
return (dataString.length() > 1
? dataString.charAt(0) + Strings.repeat("*", dataString.length() - 1)
: "");
}
@NotNull

@ -23,6 +23,7 @@ import net.william278.husksync.HuskSync;
import net.william278.husksync.config.Settings;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.User;
import net.william278.husksync.util.CompletableFutureUtil;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull;
import redis.clients.jedis.*;
@ -127,8 +128,7 @@ public class RedisManager extends JedisPubSub {
}
if (reconnected) {
plugin.log(Level.WARNING, "Redis Server connection lost. Attempting reconnect in %ss..."
.formatted(RECONNECTION_TIME / 1000), t);
plugin.log(Level.WARNING, "Redis Server connection lost. Attempting reconnect in " + (RECONNECTION_TIME / 1000) + "s...", t);
}
try {
this.unsubscribe();
@ -157,7 +157,8 @@ public class RedisManager extends JedisPubSub {
final RedisMessage redisMessage = RedisMessage.fromJson(plugin, message);
switch (messageType) {
case UPDATE_USER_DATA -> plugin.getOnlineUser(redisMessage.getTargetUuid()).ifPresent(
case UPDATE_USER_DATA: {
plugin.getOnlineUser(redisMessage.getTargetUuid()).ifPresent(
user -> {
try {
final DataSnapshot.Packed data = DataSnapshot.deserialize(plugin, redisMessage.getPayload());
@ -168,13 +169,18 @@ public class RedisManager extends JedisPubSub {
}
}
);
case REQUEST_USER_DATA -> plugin.getOnlineUser(redisMessage.getTargetUuid()).ifPresent(
break;
}
case REQUEST_USER_DATA: {
plugin.getOnlineUser(redisMessage.getTargetUuid()).ifPresent(
user -> RedisMessage.create(
UUID.fromString(new String(redisMessage.getPayload(), StandardCharsets.UTF_8)),
user.createSnapshot(DataSnapshot.SaveCause.INVENTORY_COMMAND).asBytes(plugin)
).dispatch(plugin, RedisMessage.Type.RETURN_USER_DATA)
);
case RETURN_USER_DATA -> {
break;
}
case RETURN_USER_DATA: {
final CompletableFuture<Optional<DataSnapshot.Packed>> future = pendingRequests.get(
redisMessage.getTargetUuid()
);
@ -188,6 +194,10 @@ public class RedisManager extends JedisPubSub {
}
pendingRequests.remove(redisMessage.getTargetUuid());
}
break;
}
default: {
// Do nothing
}
}
}
@ -234,8 +244,7 @@ public class RedisManager extends JedisPubSub {
);
redisMessage.dispatch(plugin, RedisMessage.Type.REQUEST_USER_DATA);
});
return future
.orTimeout(
return CompletableFutureUtil.orTimeout(future,
plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds(),
TimeUnit.MILLISECONDS
)

@ -26,6 +26,7 @@ import net.william278.husksync.database.Database;
import net.william278.husksync.redis.RedisManager;
import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.user.User;
import net.william278.husksync.util.OptionalUtil;
import net.william278.husksync.util.Task;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Blocking;
@ -158,12 +159,12 @@ public abstract class DataSyncer {
@ApiStatus.Internal
protected void setUserFromDatabase(@NotNull OnlineUser user) {
try {
getDatabase().getLatestSnapshot(user).ifPresentOrElse(
OptionalUtil.ifPresentOrElse(getDatabase().getLatestSnapshot(user),
snapshot -> user.applySnapshot(snapshot, DataSnapshot.UpdateCause.SYNCHRONIZED),
() -> user.completeSync(true, DataSnapshot.UpdateCause.NEW_USER, plugin)
);
} catch (Throwable e) {
plugin.log(Level.WARNING, "Failed to set %s's data from the database".formatted(user.getUsername()), e);
plugin.log(Level.WARNING, "Failed to set " + user.getUsername() + "'s data from the database", e);
user.completeSync(false, DataSnapshot.UpdateCause.SYNCHRONIZED, plugin);
}
}

@ -23,6 +23,7 @@ import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.redis.RedisKeyType;
import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.util.OptionalUtil;
import org.jetbrains.annotations.NotNull;
public class LockstepDataSyncer extends DataSyncer {
@ -49,7 +50,7 @@ public class LockstepDataSyncer extends DataSyncer {
return false;
}
getRedis().setUserCheckedOut(user, true);
getRedis().getUserData(user).ifPresentOrElse(
OptionalUtil.ifPresentOrElse(getRedis().getUserData(user),
data -> user.applySnapshot(data, DataSnapshot.UpdateCause.SYNCHRONIZED),
() -> this.setUserFromDatabase(user)
);

@ -89,9 +89,9 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
* @param description the description of the toast
* @param iconMaterial the namespace-keyed material to use as an hasIcon of the toast
* @param backgroundType the background ("ToastType") of the toast
* @deprecated No longer supported
* @deprecated No longer supported, since 3.6.7
*/
@Deprecated(since = "3.6.7")
@Deprecated
public abstract void sendToast(@NotNull MineDown title, @NotNull MineDown description,
@NotNull String iconMaterial, @NotNull String backgroundType);
@ -145,8 +145,17 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
public void completeSync(boolean succeeded, @NotNull DataSnapshot.UpdateCause cause, @NotNull HuskSync plugin) {
if (succeeded) {
switch (plugin.getSettings().getSynchronization().getNotificationDisplaySlot()) {
case CHAT -> cause.getCompletedLocale(plugin).ifPresent(this::sendMessage);
case ACTION_BAR -> cause.getCompletedLocale(plugin).ifPresent(this::sendActionBar);
case CHAT: {
cause.getCompletedLocale(plugin).ifPresent(this::sendMessage);
break;
}
case ACTION_BAR:{
cause.getCompletedLocale(plugin).ifPresent(this::sendActionBar);
break;
}
default: {
// do nothing
}
}
plugin.fireEvent(
plugin.getSyncCompleteEvent(this),

@ -55,7 +55,8 @@ public class User {
@Override
public boolean equals(Object object) {
if (object instanceof User other) {
if (object instanceof User) {
User other = (User) object;
return this.getUuid().equals(other.getUuid());
}
return super.equals(object);

@ -0,0 +1,86 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.util;
import lombok.experimental.UtilityClass;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.BiConsumer;
@UtilityClass
public class CompletableFutureUtil {
public static <T>CompletableFuture<T> orTimeout(CompletableFuture<T> $this, long timeout, TimeUnit unit) {
if (unit == null)
throw new NullPointerException();
if (!$this.isDone()) {
$this.whenComplete(new Canceller(Delayer.delay(new Timeout($this),
timeout, unit)));
}
return $this;
}
static final class Canceller implements BiConsumer<Object, Throwable> {
final Future<?> f;
Canceller(Future<?> f) { this.f = f; }
public void accept(Object ignore, Throwable ex) {
if (ex == null && f != null && !f.isDone())
f.cancel(false);
}
}
static final class Delayer {
static ScheduledFuture<?> delay(Runnable command, long delay,
TimeUnit unit) {
return delayer.schedule(command, delay, unit);
}
static final class DaemonThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
t.setName("CompletableFutureDelayScheduler");
return t;
}
}
static final ScheduledThreadPoolExecutor delayer;
static {
(delayer = new ScheduledThreadPoolExecutor(
1, new Delayer.DaemonThreadFactory())).
setRemoveOnCancelPolicy(true);
}
}
static final class Timeout implements Runnable {
final CompletableFuture<?> f;
Timeout(CompletableFuture<?> f) { this.f = f; }
public void run() {
if (f != null && !f.isDone())
f.completeExceptionally(new TimeoutException());
}
}
}

@ -21,6 +21,7 @@ package net.william278.husksync.util;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import lombok.SneakyThrows;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.User;
@ -124,8 +125,9 @@ public class DataDumper {
}
@NotNull
@SneakyThrows
private String getWebContentField() {
return "content=" + URLEncoder.encode(toString(), StandardCharsets.UTF_8);
return "content=" + URLEncoder.encode(toString(), "UTF-8");
}
/**
@ -136,7 +138,7 @@ public class DataDumper {
@NotNull
public String toFile() throws IOException {
final Path filePath = getFilePath();
try (final FileWriter writer = new FileWriter(filePath.toFile(), StandardCharsets.UTF_8, false)) {
try (final Writer writer = Files.newBufferedWriter(filePath, StandardCharsets.UTF_8)) {
writer.write(toString()); // Write the data from #getString to the file using a writer
return filePath.toString();
} catch (IOException e) {

@ -30,6 +30,7 @@ import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
/**
* Represents a chat-viewable paginated list of {@link net.william278.husksync.data.DataSnapshot}s
@ -60,7 +61,8 @@ public class DataSnapshotList {
snapshot.getSaveCause().getLocale(plugin),
String.format("%.2fKiB", snapshot.getFileSize(plugin) / 1024f),
snapshot.isInvalid() ? snapshot.getInvalidReason(plugin) : "")
.orElse("• " + snapshot.getId())).toList(),
.orElse("• " + snapshot.getId()))
.collect(Collectors.toList()),
plugin.getLocales().getBaseChatList(6)
.setHeaderFormat(plugin.getLocales()
.getRawLocale("data_list_title", dataOwner.getUsername(),

@ -0,0 +1,108 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.util;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@UtilityClass
public class InputStreamUtil {
// MAX_SKIP_BUFFER_SIZE is used to determine the maximum buffer size to
// use when skipping.
private static final int MAX_SKIP_BUFFER_SIZE = 2048;
private static final int DEFAULT_BUFFER_SIZE = 8192;
private static final int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;
public static byte[] readAllBytes(InputStream $this) throws IOException {
return readNBytes($this, Integer.MAX_VALUE);
}
public static byte[] readNBytes(InputStream $this, int len) throws IOException {
if (len < 0) {
throw new IllegalArgumentException("len < 0");
}
List<byte[]> bufs = null;
byte[] result = null;
int total = 0;
int remaining = len;
int n;
do {
byte[] buf = new byte[Math.min(remaining, DEFAULT_BUFFER_SIZE)];
int nread = 0;
// read to EOF which may read more or less than buffer size
while ((n = $this.read(buf, nread,
Math.min(buf.length - nread, remaining))) > 0) {
nread += n;
remaining -= n;
}
if (nread > 0) {
if (MAX_BUFFER_SIZE - total < nread) {
throw new OutOfMemoryError("Required array size too large");
}
if (nread < buf.length) {
buf = Arrays.copyOfRange(buf, 0, nread);
}
total += nread;
if (result == null) {
result = buf;
} else {
if (bufs == null) {
bufs = new ArrayList<>();
bufs.add(result);
}
bufs.add(buf);
}
}
// if the last call to read returned -1 or the number of bytes
// requested have been read then break
} while (n >= 0 && remaining > 0);
if (bufs == null) {
if (result == null) {
return new byte[0];
}
return result.length == total ?
result : Arrays.copyOf(result, total);
}
result = new byte[total];
int offset = 0;
remaining = total;
for (byte[] b : bufs) {
int count = Math.min(b.length, remaining);
System.arraycopy(b, 0, result, offset, count);
offset += count;
remaining -= count;
}
return result;
}
}

@ -0,0 +1,54 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.util;
import lombok.experimental.UtilityClass;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Stream;
@UtilityClass
public class OptionalUtil {
public static <T> Optional<T> or(Optional<T> $this, Supplier<? extends Optional<? extends T>> supplier) {
Objects.requireNonNull(supplier);
if ($this.isPresent()) {
return $this;
} else {
@SuppressWarnings("unchecked")
Optional<T> r = (Optional<T>) supplier.get();
return Objects.requireNonNull(r);
}
}
public static <T> Stream<T> stream(Optional<T> $this) {
return $this.map(Stream::of).orElseGet(Stream::empty);
}
public static <T> void ifPresentOrElse(Optional<T> $this, Consumer<? super T> action, Runnable emptyAction) {
if ($this.isPresent()) {
action.accept($this.get());
} else {
emptyAction.run();
}
}
}

@ -0,0 +1,38 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.util;
import lombok.experimental.UtilityClass;
@UtilityClass
public class StringUtil {
public static boolean isBlank(String $this) {
final int strLen = $this.length();
if (strLen == 0) {
return true;
}
for (int i = 0; i < strLen; i++) {
if (!Character.isWhitespace($this.charAt(i))) {
return false;
}
}
return true;
}
}

@ -16,10 +16,16 @@ dependencies {
}
shadowJar {
mergeServiceFiles()
dependencies {
exclude(dependency('com.mojang:brigadier'))
}
relocate 'org.inksnow.cputil', 'net.william278.husksync.libraries.cputil'
relocate 'org.slf4j', 'net.william278.husksync.libraries.slf4j'
relocate 'org.objectweb.asm', 'net.william278.husksync.libraries.asm'
relocate 'org.apache.commons.io', 'net.william278.husksync.libraries.commons.io'
relocate 'org.apache.commons.text', 'net.william278.husksync.libraries.commons.text'
relocate 'org.apache.commons.lang3', 'net.william278.husksync.libraries.commons.lang3'
@ -43,8 +49,6 @@ shadowJar {
relocate 'dev.triumphteam.gui', 'net.william278.husksync.libraries.triumphgui'
relocate 'space.arim.morepaperlib', 'net.william278.husksync.libraries.paperlib'
relocate 'de.tr7zw.changeme.nbtapi', 'net.william278.husksync.libraries.nbtapi'
minimize()
}
tasks {

@ -32,6 +32,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.InputStream;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@ -63,7 +64,7 @@ public class PaperHuskSyncLoader implements PluginLoader {
} catch (Throwable e) {
classpathBuilder.getContext().getLogger().error("Failed to resolve libraries", e);
}
return List.of();
return Collections.emptyList();
}
@Nullable

@ -61,10 +61,18 @@ public class PaperEventListener extends BukkitEventListener {
// Paper - support saving the player's items to keep if enabled
final int maxInventorySize = BukkitData.Items.Inventory.INVENTORY_SLOT_COUNT;
final List<ItemStack> itemsToSave = switch (settings.getItemsToSave()) {
case DROPS -> event.getDrops();
case ITEMS_TO_KEEP -> preserveOrder(event.getEntity().getInventory(), event.getItemsToKeep());
};
final List<ItemStack> itemsToSave;
switch (settings.getItemsToSave()) {
case DROPS:{
itemsToSave = event.getDrops();
break;
}
case ITEMS_TO_KEEP: {
itemsToSave = preserveOrder(event.getEntity().getInventory(), event.getItemsToKeep());
break;
}
default: throw new IllegalStateException("Unexpected value: " + settings.getItemsToSave());
}
if (itemsToSave.size() > maxInventorySize) {
itemsToSave.subList(maxInventorySize, itemsToSave.size()).clear();
}

@ -10,5 +10,5 @@ include(
'common',
'bukkit',
'paper',
'fabric'
// 'fabric'
)
Loading…
Cancel
Save