Compare commits

..

1 Commits

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

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

@ -1,15 +1,22 @@
dependencies { dependencies {
implementation project(path: ':common') 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: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 'net.william278:mapdataapi:1.0.3'
implementation 'org.bstats:bstats-bukkit:3.0.2' implementation 'org.bstats:bstats-bukkit:3.0.2'
implementation 'net.kyori:adventure-platform-bukkit:4.3.4' implementation 'net.kyori:adventure-platform-bukkit:4.3.4'
implementation 'dev.triumphteam:triumph-gui:3.1.10' implementation 'dev.triumphteam:triumph-gui:3.1.10'
implementation 'space.arim.morepaperlib:morepaperlib:0.4.4' implementation 'space.arim.morepaperlib:morepaperlib:0.4.4'
implementation 'de.tr7zw:item-nbt-api:2.13.2' 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 'org.spigotmc:spigot-api:1.17.1-R0.1-SNAPSHOT'
compileOnly 'com.github.retrooper.packetevents:spigot:2.3.0' compileOnly 'com.github.retrooper.packetevents:spigot:2.3.0'
@ -17,24 +24,27 @@ dependencies {
compileOnly 'org.projectlombok:lombok:1.18.34' compileOnly 'org.projectlombok:lombok:1.18.34'
compileOnly 'commons-io:commons-io:2.16.1' compileOnly 'commons-io:commons-io:2.16.1'
compileOnly 'org.json:json:20240303' compileOnly 'org.json:json:20240303'
compileOnly 'net.william278:minedown:1.8.2' compileOnly 'de.themoep:minedown-adventure:1.7.3-SNAPSHOT'
compileOnly 'de.exlll:configlib-yaml:4.5.0' compileOnly 'org.inksnow.husk:configlib-yaml:4.5.4'
compileOnly 'com.zaxxer:HikariCP:5.1.0' compileOnly 'com.zaxxer:HikariCP:4.0.3'
compileOnly 'net.william278:DesertWell:2.0.4' compileOnly 'org.inksnow.husk:desertwell:2.0.5-9962d59:all'
compileOnly 'net.william278:AdvancementAPI:97a9583413' compileOnly 'net.william278:AdvancementAPI:97a9583413'
compileOnly "redis.clients:jedis:$jedis_version"
annotationProcessor 'org.projectlombok:lombok:1.18.34' annotationProcessor 'org.projectlombok:lombok:1.18.34'
} }
shadowJar { shadowJar {
dependencies { mergeServiceFiles()
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.io', 'net.william278.husksync.libraries.commons.io'
relocate 'org.apache.commons.text', 'net.william278.husksync.libraries.commons.text' relocate 'org.apache.commons.text', 'net.william278.husksync.libraries.commons.text'
relocate 'org.apache.commons.lang3', 'net.william278.husksync.libraries.commons.lang3' relocate 'org.apache.commons.lang3', 'net.william278.husksync.libraries.commons.lang3'
relocate 'com.google.gson', 'net.william278.husksync.libraries.gson' 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 'com.fatboyindustrial', 'net.william278.husksync.libraries'
relocate 'de.themoep', 'net.william278.husksync.libraries' relocate 'de.themoep', 'net.william278.husksync.libraries'
relocate 'org.jetbrains', '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 'dev.triumphteam.gui', 'net.william278.husksync.libraries.triumphgui'
relocate 'space.arim.morepaperlib', 'net.william278.husksync.libraries.paperlib' relocate 'space.arim.morepaperlib', 'net.william278.husksync.libraries.paperlib'
relocate 'de.tr7zw.changeme.nbtapi', 'net.william278.husksync.libraries.nbtapi' 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.BukkitMapPersister;
import net.william278.husksync.util.BukkitTask; import net.william278.husksync.util.BukkitTask;
import net.william278.husksync.util.LegacyConverter; 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.Uniform;
import net.william278.uniform.bukkit.BukkitUniform; import net.william278.uniform.bukkit.BukkitUniform;
import org.bstats.bukkit.Metrics; import org.bstats.bukkit.Metrics;
@ -64,6 +66,9 @@ import org.bukkit.entity.Player;
import org.bukkit.map.MapView; import org.bukkit.map.MapView;
import org.bukkit.plugin.Plugin; import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin; 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 org.jetbrains.annotations.NotNull;
import space.arim.morepaperlib.MorePaperLib; import space.arim.morepaperlib.MorePaperLib;
import space.arim.morepaperlib.scheduling.AsynchronousScheduler; import space.arim.morepaperlib.scheduling.AsynchronousScheduler;
@ -81,10 +86,21 @@ import java.util.stream.Collectors;
public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.Supplier, public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.Supplier,
BukkitEventDispatcher, BukkitMapPersister { 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 static final String PLATFORM_TYPE_ID = "bukkit";
private final TreeMap<Identifier, Serializer<? extends Data>> serializers = Maps.newTreeMap( 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 // Prepare serializers
initialize("data serializers", (plugin) -> { 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.PERSISTENT_DATA, new BukkitSerializer.PersistentData(this));
}
registerSerializer(Identifier.INVENTORY, new BukkitSerializer.Inventory(this)); registerSerializer(Identifier.INVENTORY, new BukkitSerializer.Inventory(this));
registerSerializer(Identifier.ENDER_CHEST, new BukkitSerializer.EnderChest(this)); registerSerializer(Identifier.ENDER_CHEST, new BukkitSerializer.EnderChest(this));
registerSerializer(Identifier.ADVANCEMENTS, new BukkitSerializer.Advancements(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.STATISTICS, new Serializer.Json<>(this, BukkitData.Statistics.class));
}
registerSerializer(Identifier.POTION_EFFECTS, new BukkitSerializer.PotionEffects(this)); registerSerializer(Identifier.POTION_EFFECTS, new BukkitSerializer.PotionEffects(this));
registerSerializer(Identifier.GAME_MODE, new Serializer.Json<>(this, BukkitData.GameMode.class)); registerSerializer(Identifier.GAME_MODE, new Serializer.Json<>(this, BukkitData.GameMode.class));
registerSerializer(Identifier.FLIGHT_STATUS, new Serializer.Json<>(this, BukkitData.FlightStatus.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.ATTRIBUTES, new Serializer.Json<>(this, BukkitData.Attributes.class));
}
registerSerializer(Identifier.HEALTH, new Serializer.Json<>(this, BukkitData.Health.class)); registerSerializer(Identifier.HEALTH, new Serializer.Json<>(this, BukkitData.Health.class));
registerSerializer(Identifier.HUNGER, new Serializer.Json<>(this, BukkitData.Hunger.class)); registerSerializer(Identifier.HUNGER, new Serializer.Json<>(this, BukkitData.Hunger.class));
registerSerializer(Identifier.EXPERIENCE, new Serializer.Json<>(this, BukkitData.Experience.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 the database
initialize(getSettings().getDatabase().getType().getDisplayName() + " database connection", (plugin) -> { initialize(getSettings().getDatabase().getType().getDisplayName() + " database connection", (plugin) -> {
this.database = switch (settings.getDatabase().getType()) { switch (settings.getDatabase().getType()) {
case MYSQL, MARIADB -> new MySqlDatabase(this); case MYSQL:
case POSTGRES -> new PostgresDatabase(this); case MARIADB: {
case MONGO -> new MongoDbDatabase(this); 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(); this.database.initialize();
}); });
@ -295,14 +330,14 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
// Register bStats metrics // Register bStats metrics
public void registerMetrics(int metricsId) { public void registerMetrics(int metricsId) {
if (!getPluginVersion().getMetadata().isBlank()) { if (!StringUtil.isBlank(getPluginVersion().getMetadata())) {
return; return;
} }
try { try {
new Metrics(this, metricsId); new Metrics(this, metricsId);
} catch (Throwable e) { } 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.adapter.Adaptable;
import net.william278.husksync.config.Settings.SynchronizationSettings.AttributeSettings; import net.william278.husksync.config.Settings.SynchronizationSettings.AttributeSettings;
import net.william278.husksync.user.BukkitUser; import net.william278.husksync.user.BukkitUser;
import net.william278.husksync.util.ref.RefUtil;
import org.bukkit.*; import org.bukkit.*;
import org.bukkit.advancement.AdvancementProgress; import org.bukkit.advancement.AdvancementProgress;
import org.bukkit.attribute.AttributeInstance; import org.bukkit.attribute.AttributeInstance;
@ -92,8 +93,8 @@ public abstract class BukkitData implements Data {
stack.hasItemMeta() && Objects.requireNonNull(stack.getItemMeta()).hasEnchants() ? stack.hasItemMeta() && Objects.requireNonNull(stack.getItemMeta()).hasEnchants() ?
stack.getItemMeta().getEnchants().keySet().stream() stack.getItemMeta().getEnchants().keySet().stream()
.map(enchantment -> enchantment.getKey().getKey()) .map(enchantment -> enchantment.getKey().getKey())
.toList() .collect(Collectors.toList())
: List.of() : Collections.emptyList()
) : null) ) : null)
.toArray(Stack[]::new); .toArray(Stack[]::new);
} }
@ -118,7 +119,8 @@ public abstract class BukkitData implements Data {
@Override @Override
public boolean equals(Object obj) { 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 Arrays.equals(contents, items.getContents());
} }
return false; return false;
@ -189,7 +191,7 @@ public abstract class BukkitData implements Data {
@NotNull @NotNull
public static BukkitData.Items.EnderChest adapt(@NotNull Collection<ItemStack> items) { public static BukkitData.Items.EnderChest adapt(@NotNull Collection<ItemStack> items) {
return adapt(items.toArray(ItemStack[]::new)); return adapt(items.toArray(new ItemStack[0]));
} }
@NotNull @NotNull
@ -212,7 +214,7 @@ public abstract class BukkitData implements Data {
@NotNull @NotNull
public static ItemArray adapt(@NotNull Collection<ItemStack> drops) { public static ItemArray adapt(@NotNull Collection<ItemStack> drops) {
return new ItemArray(drops.toArray(ItemStack[]::new)); return new ItemArray(drops.toArray(new ItemStack[0]));
} }
@NotNull @NotNull
@ -238,26 +240,28 @@ public abstract class BukkitData implements Data {
@NotNull @NotNull
public static BukkitData.PotionEffects from(@NotNull Collection<PotionEffect> sei) { 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 @NotNull
public static BukkitData.PotionEffects adapt(@NotNull Collection<Effect> effects) { public static BukkitData.PotionEffects adapt(@NotNull Collection<Effect> effects) {
return from(effects.stream() return from(effects.stream()
.map(effect -> { .map(effect -> {
final PotionEffectType type = matchEffectType(effect.type()); final PotionEffectType type = matchEffectType(effect.getType());
return type != null ? new PotionEffect( return type != null ? new PotionEffect(
type, type,
effect.duration(), effect.getDuration(),
effect.amplifier(), effect.getAmplifier(),
effect.isAmbient(), effect.isAmbient(),
effect.showParticles(), effect.isShowParticles(),
effect.hasIcon() effect.isHasIcon()
) : null; ) : null;
}) })
.filter(Objects::nonNull) .filter(Objects::nonNull)
.toList()); .collect(Collectors.toList()));
} }
@NotNull @NotNull
@ -290,7 +294,7 @@ public abstract class BukkitData implements Data {
potionEffect.hasParticles(), potionEffect.hasParticles(),
potionEffect.hasIcon() potionEffect.hasIcon()
)) ))
.toList(); .collect(Collectors.toList());
} }
} }
@ -335,16 +339,19 @@ public abstract class BukkitData implements Data {
final Optional<Advancement> record = completed.stream() final Optional<Advancement> record = completed.stream()
.filter(r -> r.getKey().equals(advancement.getKey().toString())) .filter(r -> r.getKey().equals(advancement.getKey().toString()))
.findFirst(); .findFirst();
if (record.isEmpty()) { if (!record.isPresent()) {
this.setAdvancement(plugin, advancement, player, user, List.of(), progress.getAwardedCriteria()); this.setAdvancement(plugin, advancement, player, user, Collections.emptyList(), progress.getAwardedCriteria());
return; return;
} }
final Map<String, Date> criteria = record.get().getCompletedCriteria(); final Map<String, Date> criteria = record.get().getCompletedCriteria();
this.setAdvancement( this.setAdvancement(
plugin, advancement, player, user, plugin, advancement, player, user,
criteria.keySet().stream().filter(key -> !progress.getAwardedCriteria().contains(key)).toList(), criteria.keySet().stream()
progress.getAwardedCriteria().stream().filter(key -> !criteria.containsKey(key)).toList() .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 { public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
try { try {
final org.bukkit.Location location = new org.bukkit.Location( 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); user.getPlayer().teleport(location);
} catch (Throwable e) { } catch (Throwable e) {
@ -457,10 +464,25 @@ public abstract class BukkitData implements Data {
items = Maps.newHashMap(), entities = Maps.newHashMap(); items = Maps.newHashMap(), entities = Maps.newHashMap();
Registry.STATISTIC.forEach(id -> { Registry.STATISTIC.forEach(id -> {
switch (id.getType()) { switch (id.getType()) {
case UNTYPED -> addStatistic(player, id, generic); case UNTYPED: {
case BLOCK -> addMaterialStatistic(player, id, blocks, true); addStatistic(player, id, generic);
case ITEM -> addMaterialStatistic(player, id, items, false); break;
case ENTITY -> addEntityStatistic(player, id, entities); }
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); return new BukkitData.Statistics(generic, blocks, items, entities);
@ -527,9 +549,22 @@ public abstract class BukkitData implements Data {
try { try {
switch (type) { switch (type) {
case UNTYPED -> player.setStatistic(stat, value); case UNTYPED: {
case BLOCK, ITEM -> player.setStatistic(stat, Objects.requireNonNull(matchMaterial(key[0])), value); player.setStatistic(stat, value);
case ENTITY -> player.setStatistic(stat, Objects.requireNonNull(matchEntityType(key[0])), 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) { } catch (Throwable ignored) {
} }
@ -591,7 +626,7 @@ public abstract class BukkitData implements Data {
} }
public Optional<Attribute> getAttribute(@NotNull org.bukkit.attribute.Attribute id) { 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") @SuppressWarnings("unused")
@ -648,10 +683,10 @@ public abstract class BukkitData implements Data {
if (instance == null) { if (instance == null) {
return; return;
} }
instance.setBaseValue(attribute == null ? instance.getDefaultValue() : attribute.baseValue()); instance.setBaseValue(attribute == null ? instance.getDefaultValue() : attribute.getBaseValue());
instance.getModifiers().forEach(instance::removeModifier); instance.getModifiers().forEach(instance::removeModifier);
if (attribute != null) { if (attribute != null) {
attribute.modifiers().stream() attribute.getModifiers().stream()
.filter(mod -> instance.getModifiers().stream().map(AttributeModifier::getName) .filter(mod -> instance.getModifiers().stream().map(AttributeModifier::getName)
.noneMatch(n -> n.equals(mod.name()))) .noneMatch(n -> n.equals(mod.name())))
.distinct() .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 @NotNull
@Deprecated(since = "3.5.4") @Deprecated
public static BukkitData.Health from(double health, double scale) { public static BukkitData.Health from(double health, double scale) {
return from(health, scale, false); 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 @NotNull
@Deprecated(forRemoval = true, since = "3.5") @Deprecated
public static BukkitData.Health from(double health, @SuppressWarnings("unused") double max, double scale) { public static BukkitData.Health from(double health, @SuppressWarnings("unused") double max, double scale) {
return from(health, scale, false); return from(health, scale, false);
} }
@ -757,7 +792,7 @@ public abstract class BukkitData implements Data {
try { try {
player.setHealth(Math.min(health, player.getMaxHealth())); player.setHealth(Math.min(health, player.getMaxHealth()));
} catch (Throwable e) { } 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 // Set health scale
@ -766,7 +801,7 @@ public abstract class BukkitData implements Data {
player.setHealthScale(scale); player.setHealthScale(scale);
player.setHealthScaled(isHealthScaled); player.setHealthScaled(isHealthScaled);
} catch (Throwable e) { } 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 @NotNull
@Deprecated(forRemoval = true, since = "3.5") @Deprecated
@SuppressWarnings("unused") @SuppressWarnings("unused")
public static BukkitData.GameMode from(@NotNull String gameMode, boolean allowFlight, boolean isFlying) { public static BukkitData.GameMode from(@NotNull String gameMode, boolean allowFlight, boolean isFlying) {
return new BukkitData.GameMode(gameMode); return new BukkitData.GameMode(gameMode);

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

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

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

@ -23,6 +23,7 @@ import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.data.BukkitData; import net.william278.husksync.data.BukkitData;
import net.william278.husksync.user.BukkitUser; import net.william278.husksync.user.BukkitUser;
import net.william278.husksync.user.OnlineUser; import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.util.MaterialUtil;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
@ -82,7 +83,7 @@ public class BukkitEventListener extends EventListener implements BukkitJoinEven
public void handlePlayerQuit(@NotNull BukkitUser bukkitUser) { public void handlePlayerQuit(@NotNull BukkitUser bukkitUser) {
final Player player = bukkitUser.getPlayer(); final Player player = bukkitUser.getPlayer();
final ItemStack itemOnCursor = player.getItemOnCursor(); final ItemStack itemOnCursor = player.getItemOnCursor();
if (!bukkitUser.isLocked() && !itemOnCursor.getType().isAir()) { if (!bukkitUser.isLocked() && !MaterialUtil.isAir(itemOnCursor.getType())) {
player.setItemOnCursor(null); player.setItemOnCursor(null);
player.getWorld().dropItem(player.getLocation(), itemOnCursor); player.getWorld().dropItem(player.getLocation(), itemOnCursor);
plugin.debug("Dropped " + itemOnCursor + " for " + player.getName() + " on quit"); 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) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onProjectileLaunch(@NotNull ProjectileLaunchEvent event) { public void onProjectileLaunch(@NotNull ProjectileLaunchEvent event) {
final Projectile projectile = event.getEntity(); final Projectile projectile = event.getEntity();
if (projectile.getShooter() instanceof Player player) { if (projectile.getShooter() instanceof Player) {
cancelPlayerEvent(player.getUniqueId(), event); cancelPlayerEvent(((Player) projectile.getShooter()).getUniqueId(), event);
} }
} }
@ -72,8 +72,8 @@ public class BukkitLockedEventListener implements LockedHandler, Listener {
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPickupItem(@NotNull EntityPickupItemEvent event) { public void onPickupItem(@NotNull EntityPickupItemEvent event) {
if (event.getEntity() instanceof Player player) { if (event.getEntity() instanceof Player) {
cancelPlayerEvent(player.getUniqueId(), event); cancelPlayerEvent(event.getEntity().getUniqueId(), event);
} }
} }
@ -104,8 +104,8 @@ public class BukkitLockedEventListener implements LockedHandler, Listener {
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onInventoryOpen(@NotNull InventoryOpenEvent event) { public void onInventoryOpen(@NotNull InventoryOpenEvent event) {
if (event.getPlayer() instanceof Player player) { if (event.getPlayer() instanceof Player) {
cancelPlayerEvent(player.getUniqueId(), event); cancelPlayerEvent(event.getPlayer().getUniqueId(), event);
} }
} }
@ -116,8 +116,8 @@ public class BukkitLockedEventListener implements LockedHandler, Listener {
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerTakeDamage(@NotNull EntityDamageEvent event) { public void onPlayerTakeDamage(@NotNull EntityDamageEvent event) {
if (event.getEntity() instanceof Player player) { if (event.getEntity() instanceof Player) {
cancelPlayerEvent(player.getUniqueId(), event); 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.PacketReceiveEvent;
import com.github.retrooper.packetevents.event.PacketSendEvent; import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType; import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder; import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder;
import net.william278.husksync.BukkitHuskSync; import net.william278.husksync.BukkitHuskSync;
@ -59,7 +60,7 @@ public class BukkitPacketEventsLockedPacketListener extends BukkitLockedEventLis
private static class PlayerPacketAdapter extends PacketListenerAbstract { 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.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.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 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 @Override
public void onPacketReceive(PacketReceiveEvent event) { public void onPacketReceive(PacketReceiveEvent event) {
if (!(event.getPacketType() instanceof PacketType.Play.Client client)) { if (!(event.getPacketType() instanceof PacketType.Play.Client)) {
return; return;
} }
if (!CANCEL_PACKETS.contains(client)) { if (!CANCEL_PACKETS.contains((PacketType.Play.Client) event.getPacketType())) {
return; return;
} }
if (listener.cancelPlayerEvent(event.getUser().getUUID())) { if (listener.cancelPlayerEvent(event.getUser().getUUID())) {
@ -92,10 +93,10 @@ public class BukkitPacketEventsLockedPacketListener extends BukkitLockedEventLis
@Override @Override
public void onPacketSend(PacketSendEvent event) { public void onPacketSend(PacketSendEvent event) {
if (!(event.getPacketType() instanceof PacketType.Play.Client client)) { if (!(event.getPacketType() instanceof PacketType.Play.Client)) {
return; return;
} }
if (!CANCEL_PACKETS.contains(client)) { if (!CANCEL_PACKETS.contains((PacketType.Play.Client) event.getPacketType())) {
return; return;
} }
if (listener.cancelPlayerEvent(event.getUser().getUUID())) { 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.ListenerPriority;
import com.comphenix.protocol.events.PacketAdapter; import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.events.PacketEvent; import com.comphenix.protocol.events.PacketEvent;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import net.william278.husksync.BukkitHuskSync; import net.william278.husksync.BukkitHuskSync;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -50,7 +51,7 @@ public class BukkitProtocolLibLockedPacketListener extends BukkitLockedEventList
private static class PlayerPacketAdapter extends PacketAdapter { private static class PlayerPacketAdapter extends PacketAdapter {
// Packets we want the player to still be able to send/receiver to/from the server // 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.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.CHAT_COMMAND, Client.CLIENT_COMMAND, Client.CHAT, Client.CHAT_SESSION_UPDATE, // Chat / command packets
Client.POSITION, Client.POSITION_LOOK, Client.LOOK, // Movement 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.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.HikariDataSource;
import lombok.Value;
import me.william278.husksync.bukkit.data.DataSerializer; import me.william278.husksync.bukkit.data.DataSerializer;
import net.william278.hslmigrator.HSLConverter; import net.william278.hslmigrator.HSLConverter;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
@ -43,6 +44,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static net.william278.husksync.config.Settings.DatabaseSettings; 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)..."); plugin.log(Level.INFO, "Downloading raw data from the legacy database (this might take a while)...");
final List<LegacyData> dataToMigrate = Lists.newArrayList(); final List<LegacyData> dataToMigrate = Lists.newArrayList();
try (final Connection connection = connectionPool.getConnection()) { try (final Connection connection = connectionPool.getConnection()) {
try (final PreparedStatement statement = connection.prepareStatement(""" 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` "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%` "FROM `" + sourcePlayersTable + "` " +
INNER JOIN `%source_data_table%` "INNER JOIN `" + sourceDataTable + "` " +
ON `%source_players_table%`.`id` = `%source_data_table%`.`player_id` "ON `" + sourcePlayersTable + "`.`id` = `" + sourceDataTable + "`.`player_id` " +
WHERE `username` IS NOT NULL; "WHERE `username` IS NOT NULL;")) {
""".replaceAll(Pattern.quote("%source_players_table%"), sourcePlayersTable)
.replaceAll(Pattern.quote("%source_data_table%"), sourceDataTable))) {
try (final ResultSet resultSet = statement.executeQuery()) { try (final ResultSet resultSet = statement.executeQuery()) {
int playersMigrated = 0; int playersMigrated = 0;
while (resultSet.next()) { while (resultSet.next()) {
@ -142,11 +142,11 @@ public class LegacyMigrator extends Migrator {
final AtomicInteger playersConverted = new AtomicInteger(); final AtomicInteger playersConverted = new AtomicInteger();
dataToMigrate.forEach(data -> { dataToMigrate.forEach(data -> {
final DataSnapshot.Packed convertedData = data.toUserData(hslConverter, plugin); final DataSnapshot.Packed convertedData = data.toUserData(hslConverter, plugin);
plugin.getDatabase().ensureUser(data.user()); plugin.getDatabase().ensureUser(data.getUser());
try { try {
plugin.getDatabase().addSnapshot(data.user(), convertedData); plugin.getDatabase().addSnapshot(data.getUser(), convertedData);
} catch (Throwable e) { } 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; return;
} }
@ -167,41 +167,53 @@ public class LegacyMigrator extends Migrator {
@Override @Override
public void handleConfigurationCommand(@NotNull String[] args) { public void handleConfigurationCommand(@NotNull String[] args) {
if (args.length == 2) { if (args.length == 2) {
if (switch (args[0].toLowerCase(Locale.ENGLISH)) { boolean $yield;
case "host" -> { switch (args[0].toLowerCase(Locale.ENGLISH)) {
case "host": {
this.sourceHost = args[1]; this.sourceHost = args[1];
yield true; $yield = true;
break;
} }
case "port" -> { case "port": {
try { try {
this.sourcePort = Integer.parseInt(args[1]); this.sourcePort = Integer.parseInt(args[1]);
yield true; $yield = true;
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
yield false; $yield = false;
} }
break;
} }
case "username" -> { case "username": {
this.sourceUsername = args[1]; this.sourceUsername = args[1];
yield true; $yield = true;
break;
} }
case "password" -> { case "password": {
this.sourcePassword = args[1]; this.sourcePassword = args[1];
yield true; $yield = true;
break;
} }
case "database" -> { case "database": {
this.sourceDatabase = args[1]; this.sourceDatabase = args[1];
yield true; $yield = true;
break;
} }
case "players_table" -> { case "players_table": {
this.sourcePlayersTable = args[1]; this.sourcePlayersTable = args[1];
yield true; $yield = true;
break;
} }
case "data_table" -> { case "data_table": {
this.sourceDataTable = args[1]; 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, getHelpMenu());
plugin.log(Level.INFO, "Successfully set " + args[0] + " to " + plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
obfuscateDataString(args[1])); obfuscateDataString(args[1]));
@ -229,59 +241,65 @@ public class LegacyMigrator extends Migrator {
@NotNull @NotNull
@Override @Override
public String getHelpMenu() { public String getHelpMenu() {
return """ return "=== HuskSync v1.x --> v3.x Migration Wizard =========\n" +
=== HuskSync v1.x --> v3.x Migration Wizard ========= "This will migrate all user data from HuskSync v1.x to\n" +
This will migrate all user data from HuskSync v1.x to "HuskSync v3.x's new format. To perform the migration,\n" +
HuskSync v3.x's new format. To perform the migration, "please follow the steps below carefully.\n" +
please follow the steps below carefully. "\n" +
"[!] Existing data in the database will be wiped. [!]\n" +
[!] Existing data in the database will be wiped. [!] "\n" +
"STEP 1] Please ensure no players are on any servers.\n" +
STEP 1] Please ensure no players are on any servers. "\n" +
"STEP 2] HuskSync will need to connect to the database\n" +
STEP 2] HuskSync will need to connect to the database "used to hold the existing, legacy HuskSync data.\n" +
used to hold the existing, legacy HuskSync data. "If this is the same database as the one you are\n" +
If this is the same database as the one you are "currently using, you probably don't need to change\n" +
currently using, you probably don't need to change "anything.\n" +
anything. "Please check that the credentials below are the\n" +
Please check that the credentials below are the "correct credentials of the source legacy HuskSync\n" +
correct credentials of the source legacy HuskSync "database.\n" +
database. "- host: " + obfuscateDataString(sourceHost) + "\n" +
- host: %source_host% "- port: " + sourcePort + "\n" +
- port: %source_port% "- username: " + obfuscateDataString(sourceUsername) + "\n" +
- username: %source_username% "- password: " + obfuscateDataString(sourcePassword) + "\n" +
- password: %source_password% "- database: " + sourceDatabase + "\n" +
- database: %source_database% "- players_table: " + sourcePlayersTable + "\n" +
- players_table: %source_players_table% "- data_table: " + sourceDataTable + "\n" +
- data_table: %source_data_table% "If any of these are not correct, please correct them\n" +
If any of these are not correct, please correct them "using the command:\n" +
using the command: "\"husksync migrate legacy set <parameter> <value>\"\n" +
"husksync migrate legacy set <parameter> <value>" "(e.g.: \"husksync migrate legacy set host 1.2.3.4\")\n" +
(e.g.: "husksync migrate legacy set host 1.2.3.4") "\n" +
"STEP 3] HuskSync will migrate data into the database\n" +
STEP 3] HuskSync will migrate data into the database "tables configures in the config.yml file of this\n" +
tables configures in the config.yml file of this "server. Please make sure you're happy with this\n" +
server. Please make sure you're happy with this "before proceeding.\n" +
before proceeding. "\n" +
"STEP 4] To start the migration, please run:\n" +
STEP 4] To start the migration, please run: "\"husksync migrate legacy start\"\n";
"husksync migrate legacy start" }
""".replaceAll(Pattern.quote("%source_host%"), obfuscateDataString(sourceHost))
.replaceAll(Pattern.quote("%source_port%"), Integer.toString(sourcePort)) @Value
.replaceAll(Pattern.quote("%source_username%"), obfuscateDataString(sourceUsername)) private class LegacyData {
.replaceAll(Pattern.quote("%source_password%"), obfuscateDataString(sourcePassword)) @NotNull User user;
.replaceAll(Pattern.quote("%source_database%"), sourceDatabase) @NotNull String serializedInventory;
.replaceAll(Pattern.quote("%source_players_table%"), sourcePlayersTable) @NotNull String serializedEnderChest;
.replaceAll(Pattern.quote("%source_data_table%"), sourceDataTable); double health;
} double maxHealth;
double healthScale;
private record LegacyData(@NotNull User user, int hunger;
@NotNull String serializedInventory, @NotNull String serializedEnderChest, float saturation;
double health, double maxHealth, double healthScale, int hunger, float saturation, float saturationExhaustion;
float saturationExhaustion, int selectedSlot, @NotNull String serializedPotionEffects, int selectedSlot;
int totalExp, int expLevel, float expProgress, @NotNull String serializedPotionEffects;
@NotNull String gameMode, @NotNull String serializedStatistics, boolean isFlying, int totalExp;
@NotNull String serializedAdvancements, @NotNull String serializedLocation) { int expLevel;
float expProgress;
@NotNull String gameMode;
@NotNull String serializedStatistics;
boolean isFlying;
@NotNull String serializedAdvancements;
@NotNull String serializedLocation;
@NotNull @NotNull
public DataSnapshot.Packed toUserData(@NotNull HSLConverter converter, @NotNull HuskSync plugin) { public DataSnapshot.Packed toUserData(@NotNull HSLConverter converter, @NotNull HuskSync plugin) {
@ -319,7 +337,7 @@ public class LegacyMigrator extends Migrator {
.advancements(BukkitData.Advancements.from(converter .advancements(BukkitData.Advancements.from(converter
.deserializeAdvancementData(serializedAdvancements).stream() .deserializeAdvancementData(serializedAdvancements).stream()
.map(data -> Data.Advancements.Advancement.adapt(data.key(), data.criteriaMap())) .map(data -> Data.Advancements.Advancement.adapt(data.key(), data.criteriaMap()))
.toList())) .collect(Collectors.toList())))
// Stats // Stats
.statistics(BukkitData.Statistics.from( .statistics(BukkitData.Statistics.from(

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

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

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

@ -69,7 +69,7 @@ public interface BukkitMapPersister {
if (!getPlugin().getSettings().getSynchronization().isPersistLockedMaps()) { if (!getPlugin().getSettings().getSynchronization().isPersistLockedMaps()) {
return items; 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()) { if (!getPlugin().getSettings().getSynchronization().isPersistLockedMaps()) {
return items; 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 // Perform an operation on each map in an array of ItemStacks
@NotNull @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++) { for (int i = 0; i < items.length; i++) {
final ItemStack item = items[i]; final ItemStack item = items[i];
if (item == null) { if (item == null) {
continue; continue;
} }
if (item.getType() == Material.FILLED_MAP && item.hasItemMeta()) { if (MaterialUtil.isFilledMap(item.getType()) && item.hasItemMeta()) {
items[i] = function.apply(item); items[i] = function.apply(item);
} else if (item.getItemMeta() instanceof BlockStateMeta b && b.getBlockState() instanceof ShulkerBox box) { } else if (item.getItemMeta() instanceof BlockStateMeta &&
forEachMap(box.getInventory().getContents(), function); ((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); b.setBlockState(box);
} }
} }
@ -105,7 +110,7 @@ public interface BukkitMapPersister {
} }
@NotNull @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()); final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta());
if (!meta.hasMapView()) { if (!meta.hasMapView()) {
return map; return map;
@ -139,7 +144,7 @@ public interface BukkitMapPersister {
} }
@NotNull @NotNull
private ItemStack applyMapView(@NotNull ItemStack map) { default ItemStack private$applyMapView(@NotNull ItemStack map) {
final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta()); final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta());
NBT.get(map, nbt -> { NBT.get(map, nbt -> {
if (!nbt.hasTag(MAP_DATA_KEY)) { 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 // Add a renderer to the map with the data and save to file
final MapView view = generateRenderedMap(canvasData); final MapView view = private$generateRenderedMap(canvasData);
final String worldUid = getDefaultMapWorld().getUID().toString(); final String worldUid = private$getDefaultMapWorld().getUID().toString();
meta.setMapView(view); meta.setMapView(view);
map.setItemMeta(meta); map.setItemMeta(meta);
saveMapToFile(canvasData, view.getId()); saveMapToFile(canvasData, view.getId());
@ -204,7 +209,7 @@ public interface BukkitMapPersister {
} }
default void renderMapFromFile(@NotNull MapView view) { 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()) { if (!mapFile.exists()) {
return; return;
} }
@ -232,7 +237,7 @@ public interface BukkitMapPersister {
default void saveMapToFile(@NotNull MapData data, int id) { default void saveMapToFile(@NotNull MapData data, int id) {
getPlugin().runAsync(() -> { getPlugin().runAsync(() -> {
final File mapFile = new File(getMapCacheFolder(), id + ".dat"); final File mapFile = new File(private$getMapCacheFolder(), id + ".dat");
if (mapFile.exists()) { if (mapFile.exists()) {
return; return;
} }
@ -248,7 +253,7 @@ public interface BukkitMapPersister {
} }
@NotNull @NotNull
private File getMapCacheFolder() { default File private$getMapCacheFolder() {
final File mapCache = new File(getPlugin().getDataFolder(), "maps"); final File mapCache = new File(getPlugin().getDataFolder(), "maps");
if (!mapCache.exists() && !mapCache.mkdirs()) { if (!mapCache.exists() && !mapCache.mkdirs()) {
getPlugin().log(Level.WARNING, "Failed to create maps folder"); 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 // Sets the renderer of a map, and returns the generated MapView
@NotNull @NotNull
private MapView generateRenderedMap(@NotNull MapData canvasData) { default MapView private$generateRenderedMap(@NotNull MapData canvasData) {
final MapView view = Bukkit.createMap(getDefaultMapWorld()); final MapView view = Bukkit.createMap(private$getDefaultMapWorld());
view.getRenderers().clear(); view.getRenderers().clear();
// Create a new map view renderer with the map data color at each pixel // Create a new map view renderer with the map data color at each pixel
@ -275,7 +280,7 @@ public interface BukkitMapPersister {
} }
@NotNull @NotNull
private static World getDefaultMapWorld() { static World private$getDefaultMapWorld() {
final World world = Bukkit.getWorlds().get(0); final World world = Bukkit.getWorlds().get(0);
if (world == null) { if (world == null) {
throw new IllegalStateException("No worlds are loaded on the server!"); throw new IllegalStateException("No worlds are loaded on the server!");
@ -318,35 +323,86 @@ public interface BukkitMapPersister {
cursors.removeCursor(cursors.getCursor(0)); 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); canvas.setCursors(cursors);
} }
} }
@NotNull @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( return new MapCursor(
(byte) banner.getPosition().getX(), (byte) banner.getPosition().getX(),
(byte) banner.getPosition().getZ(), (byte) banner.getPosition().getZ(),
(byte) 8, // Always rotate banners upright (byte) 8, // Always rotate banners upright
switch (banner.getColor().toLowerCase(Locale.ENGLISH)) { type,
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;
},
true, true,
banner.getText().isEmpty() ? null : banner.getText() banner.getText().isEmpty() ? null : banner.getText()
); );
@ -409,11 +465,14 @@ public interface BukkitMapPersister {
@NotNull @NotNull
private String getDimension() { private String getDimension() {
return mapView.getWorld() != null ? switch (mapView.getWorld().getEnvironment()) { if (mapView.getWorld() == null) {
case NETHER -> "minecraft:the_nether"; return "minecraft:overworld";
case THE_END -> "minecraft:the_end"; }
default -> "minecraft:overworld"; switch (mapView.getWorld().getEnvironment()) {
} : "minecraft:overworld"; 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) { } 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' name: 'HuskSync'
version: '${version}' version: '${version}'
main: 'net.william278.husksync.BukkitHuskSync' main: 'net.william278.husksync.BukkitHuskSync'
api-version: 1.17 api-version: 1.13
author: 'William278' author: 'William278, InkerBot'
description: '${description}' description: '${description}'
website: 'https://william278.net' website: 'https://git.inker.bot/HuskLegacy/HuskSync'
folia-supported: true folia-supported: true
softdepend: softdepend:
- 'packetevents' - 'packetevents'
- 'ProtocolLib' - 'ProtocolLib'
- 'MysqlPlayerDataBridge' - 'MysqlPlayerDataBridge'
- 'Plan' - '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 { dependencies {
api 'org.inksnow.cputil:logger:1.13'
api 'org.inksnow.cputil:database:1.13'
api 'commons-io:commons-io:2.16.1' api 'commons-io:commons-io:2.16.1'
api 'org.apache.commons:commons-text:1.12.0' 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 'org.json:json:20240303'
api 'com.google.code.gson:gson:2.11.0' api 'com.google.code.gson:gson:2.11.0'
api 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2' api 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2'
api 'de.exlll:configlib-yaml:4.5.0' api 'org.inksnow.husk:configlib-yaml:4.5.4'
api 'net.william278:paginedown:1.1.2' api 'org.inksnow.husk:paginedown:1.1.3-ac18d11'
api 'net.william278:DesertWell:2.0.4' api 'org.inksnow.husk:desertwell:2.0.5-9962d59:all'
api('com.zaxxer:HikariCP:5.1.0') { api 'com.mojang:brigadier:1.1.8'
exclude module: 'slf4j-api'
}
compileOnly 'net.william278.uniform:uniform-common:1.2.1' compileOnly 'org.inksnow.husk.uniform:uniform-common:1.2.1-ad25f16'
compileOnly 'com.mojang:brigadier:1.1.8'
compileOnly 'org.projectlombok:lombok:1.18.34' compileOnly 'org.projectlombok:lombok:1.18.34'
compileOnly 'org.jetbrains:annotations:24.1.0' compileOnly 'org.jetbrains:annotations:24.1.0'
compileOnly 'net.kyori:adventure-api:4.17.0' compileOnly 'net.kyori:adventure-api:4.17.0'
compileOnly 'net.kyori:adventure-platform-api:4.3.4' 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 'com.github.plan-player-analytics:Plan:5.5.2272'
compileOnly "redis.clients:jedis:$jedis_version" compileOnly "redis.clients:jedis:$jedis_version"
compileOnly "com.mysql:mysql-connector-j:$mysql_driver_version" compileOnly "com.mysql:mysql-connector-j:$mysql_driver_version"
@ -33,9 +33,9 @@ dependencies {
testImplementation "redis.clients:jedis:$jedis_version" testImplementation "redis.clients:jedis:$jedis_version"
testImplementation "org.xerial.snappy:snappy-java:$snappy_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' 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' testCompileOnly 'org.jetbrains:annotations:24.1.0'
annotationProcessor 'org.projectlombok:lombok:1.18.34' 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 // Get the debug log message format
@NotNull @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); 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 { final class FailedToLoadException extends IllegalStateException {
private static final String FORMAT = """ private static final String FORMAT =
HuskSync has failed to load! The plugin will not be enabled and no data will be synchronized. "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): "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 "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 "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) "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 "4) Check the error below for more details\n" +
"\n" +
Caused by: %s"""; "Caused by: %s";
FailedToLoadException(@NotNull String message, @NotNull Throwable cause) { FailedToLoadException(@NotNull String message, @NotNull Throwable cause) {
super(String.format(FORMAT, message), 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.sync.DataSyncer;
import net.william278.husksync.user.OnlineUser; import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.user.User; import net.william278.husksync.user.User;
import net.william278.husksync.util.OptionalUtil;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.Formatter;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer; 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. * 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) { public CompletableFuture<Optional<DataSnapshot.Unpacked>> getCurrentData(@NotNull User user) {
return plugin.getRedisManager() return plugin.getRedisManager()
.getUserData(UUID.randomUUID(), user) .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))); .thenApply(data -> data.map(snapshot -> snapshot.unpack(plugin)));
} }
@ -154,8 +158,8 @@ public class HuskSyncAPI {
*/ */
public void setCurrentData(@NotNull User user, @NotNull DataSnapshot data) { public void setCurrentData(@NotNull User user, @NotNull DataSnapshot data) {
plugin.runAsync(() -> { plugin.runAsync(() -> {
final DataSnapshot.Packed packed = data instanceof DataSnapshot.Unpacked unpacked final DataSnapshot.Packed packed = data instanceof DataSnapshot.Unpacked
? unpacked.pack(plugin) : (DataSnapshot.Packed) data; ? ((DataSnapshot.Unpacked) data).pack(plugin) : (DataSnapshot.Packed) data;
addSnapshot(user, packed); addSnapshot(user, packed);
plugin.getRedisManager().sendUserDataUpdate(user, packed); plugin.getRedisManager().sendUserDataUpdate(user, packed);
}); });
@ -190,7 +194,7 @@ public class HuskSyncAPI {
return plugin.supplyAsync( return plugin.supplyAsync(
() -> plugin.getDatabase().getAllSnapshots(user).stream() () -> plugin.getDatabase().getAllSnapshots(user).stream()
.map(snapshot -> snapshot.unpack(plugin)) .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) { public CompletableFuture<List<DataSnapshot.Unpacked>> getSnapshot(@NotNull User user, @NotNull UUID versionId) {
return plugin.supplyAsync( 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)) .map(snapshot -> snapshot.unpack(plugin))
.toList() .collect(Collectors.toList())
); );
} }
@ -274,8 +279,8 @@ public class HuskSyncAPI {
@Nullable BiConsumer<User, DataSnapshot.Packed> callback) { @Nullable BiConsumer<User, DataSnapshot.Packed> callback) {
plugin.runAsync(() -> plugin.getDataSyncer().saveData( plugin.runAsync(() -> plugin.getDataSyncer().saveData(
user, user,
snapshot instanceof DataSnapshot.Unpacked unpacked snapshot instanceof DataSnapshot.Unpacked
? unpacked.pack(plugin) : (DataSnapshot.Packed) snapshot, ? ((DataSnapshot.Unpacked) snapshot).pack(plugin) : (DataSnapshot.Packed) snapshot,
callback callback
)); ));
} }
@ -304,8 +309,8 @@ public class HuskSyncAPI {
*/ */
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot) { public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot) {
plugin.runAsync(() -> plugin.getDatabase().updateSnapshot( plugin.runAsync(() -> plugin.getDatabase().updateSnapshot(
user, snapshot instanceof DataSnapshot.Unpacked unpacked user, snapshot instanceof DataSnapshot.Unpacked
? unpacked.pack(plugin) : (DataSnapshot.Packed) snapshot ? ((DataSnapshot.Unpacked) snapshot).pack(plugin) : (DataSnapshot.Packed) snapshot
)); ));
} }
@ -439,8 +444,8 @@ public class HuskSyncAPI {
* @since 3.0 * @since 3.0
*/ */
public int getSnapshotFileSize(@NotNull DataSnapshot snapshot) { public int getSnapshotFileSize(@NotNull DataSnapshot snapshot) {
return (snapshot instanceof DataSnapshot.Packed packed) return (snapshot instanceof DataSnapshot.Packed)
? packed.getFileSize(plugin) ? ((DataSnapshot.Packed) snapshot).getFileSize(plugin)
: ((DataSnapshot.Unpacked) snapshot).pack(plugin).getFileSize(plugin); : ((DataSnapshot.Unpacked) snapshot).pack(plugin).getFileSize(plugin);
} }
@ -511,15 +516,14 @@ public class HuskSyncAPI {
*/ */
static final class NotRegisteredException extends IllegalStateException { static final class NotRegisteredException extends IllegalStateException {
private static final String REASONS = """ private static final String REASONS = "This may be because:\n" +
This may be because: "1) HuskSync has failed to enable successfully\n" +
1) HuskSync has failed to enable successfully "2) Your plugin isn't set to load after HuskSync has\n" +
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?)\n" +
(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.";
3) You are attempting to access HuskSync on plugin construction/before your plugin has enabled.""";
NotRegisteredException(@NotNull String reasons) { 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() { NotRegisteredException() {

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

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

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

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

@ -46,7 +46,7 @@ public abstract class PluginCommand extends Command {
} }
private static String getDescription(@NotNull HuskSync plugin, @NotNull String name) { 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 @NotNull
@ -72,7 +72,10 @@ public abstract class PluginCommand extends Command {
@NotNull @NotNull
protected CommandUser adapt(net.william278.uniform.CommandUser user) { 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 @NotNull

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

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

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

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

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

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

@ -19,6 +19,9 @@
package net.william278.husksync.data; 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 lombok.*;
import net.kyori.adventure.key.InvalidKeyException; import net.kyori.adventure.key.InvalidKeyException;
import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Key;
@ -150,7 +153,7 @@ public class Identifier {
private static Identifier huskSync(@Subst("null") @NotNull String name, private static Identifier huskSync(@Subst("null") @NotNull String name,
@SuppressWarnings("SameParameterValue") boolean configDefault, @SuppressWarnings("SameParameterValue") boolean configDefault,
@NotNull Dependency... dependents) throws InvalidKeyException { @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 @ApiStatus.Internal
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public static Map<String, Boolean> getConfigMap() { public static Map<String, Boolean> getConfigMap() {
return Map.ofEntries(Stream.of( return ImmutableMap.ofEntries(Stream.of(
INVENTORY, ENDER_CHEST, POTION_EFFECTS, ADVANCEMENTS, LOCATION, STATISTICS, INVENTORY, ENDER_CHEST, POTION_EFFECTS, ADVANCEMENTS, LOCATION, STATISTICS,
HEALTH, HUNGER, ATTRIBUTES, EXPERIENCE, GAME_MODE, FLIGHT_STATUS, PERSISTENT_DATA HEALTH, HUNGER, ATTRIBUTES, EXPERIENCE, GAME_MODE, FLIGHT_STATUS, PERSISTENT_DATA
) )
@ -230,7 +233,8 @@ public class Identifier {
*/ */
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (obj instanceof Identifier other) { if (obj instanceof Identifier) {
Identifier other = (Identifier) obj;
return key.equals(other.key); return key.equals(other.key);
} }
return false; return false;
@ -239,7 +243,7 @@ public class Identifier {
// Get the config entry for the identifier // Get the config entry for the identifier
@NotNull @NotNull
private Map.Entry<String, Boolean> getConfigEntry() { 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 (i1.dependsOn(i2)) {
if (i2.dependsOn(i1)) { if (i2.dependsOn(i1)) {
throw new IllegalArgumentException( 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; return 1;
@ -310,7 +314,8 @@ public class Identifier {
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (obj instanceof Dependency other) { if (obj instanceof Dependency) {
Dependency other = (Dependency) obj;
return key.equals(other.key); return key.equals(other.key);
} }
return false; return false;

@ -26,6 +26,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.*; import java.util.*;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors;
public interface SerializerRegistry { public interface SerializerRegistry {
@ -52,7 +53,7 @@ public interface SerializerRegistry {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
default void registerSerializer(@NotNull Identifier id, @NotNull Serializer<? extends Data> serializer) { default void registerSerializer(@NotNull Identifier id, @NotNull Serializer<? extends Data> serializer) {
if (id.isCustom()) { 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)); id.setEnabled(id.isCustom() || getPlugin().getSettings().getSynchronization().isFeatureEnabled(id));
getSerializers().put(id, (Serializer<Data>) serializer); getSerializers().put(id, (Serializer<Data>) serializer);
@ -72,11 +73,11 @@ public interface SerializerRegistry {
final List<String> unmet = identifier.getDependencies().stream() final List<String> unmet = identifier.getDependencies().stream()
.filter(Identifier.Dependency::isRequired) .filter(Identifier.Dependency::isRequired)
.filter(dep -> !isDataTypeAvailable(dep.getKey().asString())) .filter(dep -> !isDataTypeAvailable(dep.getKey().asString()))
.map(dep -> dep.getKey().asString()).toList(); .map(dep -> dep.getKey().asString())
.collect(Collectors.toList());
if (!unmet.isEmpty()) { if (!unmet.isEmpty()) {
identifier.setEnabled(false); identifier.setEnabled(false);
getPlugin().log(Level.WARNING, "Disabled %s syncing as the following types need to be on: %s" getPlugin().log(Level.WARNING, "Disabled " + identifier + " syncing as the following types need to be on: " + String.join(", ", unmet));
.formatted(identifier, String.join(", ", unmet)));
} }
}); });
} }
@ -116,7 +117,7 @@ public interface SerializerRegistry {
@NotNull @NotNull
default String serializeData(@NotNull Identifier identifier, @NotNull Data data) throws IllegalStateException { default String serializeData(@NotNull Identifier identifier, @NotNull Data data) throws IllegalStateException {
return getSerializer(identifier).map(serializer -> serializer.serialize(data)) 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, default Data deserializeData(@NotNull Identifier identifier, @NotNull String data,
@NotNull Version dataMcVersion) throws IllegalStateException { @NotNull Version dataMcVersion) throws IllegalStateException {
return getSerializer(identifier).map(serializer -> serializer.deserialize(data, dataMcVersion)).orElseThrow( 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 * @param data the data to deserialize
* @return the deserialized data * @return the deserialized data
* @since 3.5.4 * @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 @NotNull
@Deprecated(since = "3.6.5") @Deprecated
default Data deserializeData(@NotNull Identifier identifier, @NotNull String data) { default Data deserializeData(@NotNull Identifier identifier, @NotNull String data) {
return deserializeData(identifier, data, getPlugin().getMinecraftVersion()); 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 // 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); return getIdentifier(key).map(Identifier::isEnabled).orElse(false);
} }

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

@ -19,11 +19,14 @@
package net.william278.husksync.database; package net.william278.husksync.database;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import lombok.Getter; import lombok.Getter;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import net.william278.husksync.config.Settings; import net.william278.husksync.config.Settings;
import net.william278.husksync.data.DataSnapshot; import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.User; import net.william278.husksync.user.User;
import net.william278.husksync.util.InputStreamUtil;
import org.jetbrains.annotations.Blocking; import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -56,8 +59,10 @@ public abstract class Database {
@SuppressWarnings("SameParameterValue") @SuppressWarnings("SameParameterValue")
@NotNull @NotNull
protected final String[] getSchemaStatements(@NotNull String schemaFileName) throws IOException { protected final String[] getSchemaStatements(@NotNull String schemaFileName) throws IOException {
return formatStatementTables(new String(Objects.requireNonNull(plugin.getResource(schemaFileName)) return formatStatementTables(new String(
.readAllBytes(), StandardCharsets.UTF_8)).split(";"); InputStreamUtil.readAllBytes(Objects.requireNonNull(plugin.getResource(schemaFileName))),
StandardCharsets.UTF_8
)).split(";");
} }
/** /**
@ -285,13 +290,13 @@ public abstract class Database {
@NotNull @NotNull
private Map.Entry<String, String> toEntry() { private Map.Entry<String, String> toEntry() {
return Map.entry(name().toLowerCase(Locale.ENGLISH), defaultName); return Maps.immutableEntry(name().toLowerCase(Locale.ENGLISH), defaultName);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@NotNull @NotNull
public static Map<String, String> getDefaults() { public static Map<String, String> getDefaults() {
return Map.ofEntries(Arrays.stream(values()) return ImmutableMap.ofEntries(Arrays.stream(values())
.map(TableName::toEntry) .map(TableName::toEntry)
.toArray(Map.Entry[]::new)); .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.MongoCollectionHelper;
import net.william278.husksync.database.mongo.MongoConnectionHandler; import net.william278.husksync.database.mongo.MongoConnectionHandler;
import net.william278.husksync.user.User; import net.william278.husksync.user.User;
import net.william278.husksync.util.OptionalUtil;
import org.bson.Document; import org.bson.Document;
import org.bson.conversions.Bson; import org.bson.conversions.Bson;
import org.bson.types.Binary; import org.bson.types.Binary;
@ -43,6 +44,7 @@ import java.util.Optional;
import java.util.TimeZone; import java.util.TimeZone;
import java.util.UUID; import java.util.UUID;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors;
public class MongoDbDatabase extends Database { public class MongoDbDatabase extends Database {
private MongoConnectionHandler mongoConnectionHandler; private MongoConnectionHandler mongoConnectionHandler;
@ -103,7 +105,7 @@ public class MongoDbDatabase extends Database {
@Override @Override
public void ensureUser(@NotNull User user) { public void ensureUser(@NotNull User user) {
try { try {
getUser(user.getUuid()).ifPresentOrElse( OptionalUtil.ifPresentOrElse(getUser(user.getUuid()),
existingUser -> { existingUser -> {
if (!existingUser.getUsername().equals(user.getUsername())) { if (!existingUser.getUsername().equals(user.getUsername())) {
// Update a user's name if it has changed in the database // 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) { protected void rotateSnapshots(@NotNull User user) {
try { try {
final List<DataSnapshot.Packed> unpinnedUserData = getAllSnapshots(user).stream() 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(); final int maxSnapshots = plugin.getSettings().getSynchronization().getMaxUserDataSnapshots();
if (unpinnedUserData.size() > maxSnapshots) { if (unpinnedUserData.size() > maxSnapshots) {

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

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

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

@ -19,6 +19,7 @@
package net.william278.husksync.migrator; package net.william278.husksync.migrator;
import com.google.common.base.Strings;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -56,7 +57,9 @@ public abstract class Migrator {
* @return The data string obfuscated with stars (*) * @return The data string obfuscated with stars (*)
*/ */
protected final String obfuscateDataString(@NotNull String dataString) { 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 @NotNull

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

@ -26,6 +26,7 @@ import net.william278.husksync.database.Database;
import net.william278.husksync.redis.RedisManager; import net.william278.husksync.redis.RedisManager;
import net.william278.husksync.user.OnlineUser; import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.user.User; import net.william278.husksync.user.User;
import net.william278.husksync.util.OptionalUtil;
import net.william278.husksync.util.Task; import net.william278.husksync.util.Task;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Blocking; import org.jetbrains.annotations.Blocking;
@ -158,12 +159,12 @@ public abstract class DataSyncer {
@ApiStatus.Internal @ApiStatus.Internal
protected void setUserFromDatabase(@NotNull OnlineUser user) { protected void setUserFromDatabase(@NotNull OnlineUser user) {
try { try {
getDatabase().getLatestSnapshot(user).ifPresentOrElse( OptionalUtil.ifPresentOrElse(getDatabase().getLatestSnapshot(user),
snapshot -> user.applySnapshot(snapshot, DataSnapshot.UpdateCause.SYNCHRONIZED), snapshot -> user.applySnapshot(snapshot, DataSnapshot.UpdateCause.SYNCHRONIZED),
() -> user.completeSync(true, DataSnapshot.UpdateCause.NEW_USER, plugin) () -> user.completeSync(true, DataSnapshot.UpdateCause.NEW_USER, plugin)
); );
} catch (Throwable e) { } 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); 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.data.DataSnapshot;
import net.william278.husksync.redis.RedisKeyType; import net.william278.husksync.redis.RedisKeyType;
import net.william278.husksync.user.OnlineUser; import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.util.OptionalUtil;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public class LockstepDataSyncer extends DataSyncer { public class LockstepDataSyncer extends DataSyncer {
@ -49,7 +50,7 @@ public class LockstepDataSyncer extends DataSyncer {
return false; return false;
} }
getRedis().setUserCheckedOut(user, true); getRedis().setUserCheckedOut(user, true);
getRedis().getUserData(user).ifPresentOrElse( OptionalUtil.ifPresentOrElse(getRedis().getUserData(user),
data -> user.applySnapshot(data, DataSnapshot.UpdateCause.SYNCHRONIZED), data -> user.applySnapshot(data, DataSnapshot.UpdateCause.SYNCHRONIZED),
() -> this.setUserFromDatabase(user) () -> this.setUserFromDatabase(user)
); );

@ -89,9 +89,9 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
* @param description the description of the toast * @param description the description of the toast
* @param iconMaterial the namespace-keyed material to use as an hasIcon 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 * @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, public abstract void sendToast(@NotNull MineDown title, @NotNull MineDown description,
@NotNull String iconMaterial, @NotNull String backgroundType); @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) { public void completeSync(boolean succeeded, @NotNull DataSnapshot.UpdateCause cause, @NotNull HuskSync plugin) {
if (succeeded) { if (succeeded) {
switch (plugin.getSettings().getSynchronization().getNotificationDisplaySlot()) { switch (plugin.getSettings().getSynchronization().getNotificationDisplaySlot()) {
case CHAT -> cause.getCompletedLocale(plugin).ifPresent(this::sendMessage); case CHAT: {
case ACTION_BAR -> cause.getCompletedLocale(plugin).ifPresent(this::sendActionBar); cause.getCompletedLocale(plugin).ifPresent(this::sendMessage);
break;
}
case ACTION_BAR:{
cause.getCompletedLocale(plugin).ifPresent(this::sendActionBar);
break;
}
default: {
// do nothing
}
} }
plugin.fireEvent( plugin.fireEvent(
plugin.getSyncCompleteEvent(this), plugin.getSyncCompleteEvent(this),

@ -55,7 +55,8 @@ public class User {
@Override @Override
public boolean equals(Object object) { 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 this.getUuid().equals(other.getUuid());
} }
return super.equals(object); 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.JsonObject;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import lombok.SneakyThrows;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSnapshot; import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.User; import net.william278.husksync.user.User;
@ -124,8 +125,9 @@ public class DataDumper {
} }
@NotNull @NotNull
@SneakyThrows
private String getWebContentField() { 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 @NotNull
public String toFile() throws IOException { public String toFile() throws IOException {
final Path filePath = getFilePath(); 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 writer.write(toString()); // Write the data from #getString to the file using a writer
return filePath.toString(); return filePath.toString();
} catch (IOException e) { } catch (IOException e) {

@ -30,6 +30,7 @@ import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle; import java.time.format.FormatStyle;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicInteger; 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 * 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), snapshot.getSaveCause().getLocale(plugin),
String.format("%.2fKiB", snapshot.getFileSize(plugin) / 1024f), String.format("%.2fKiB", snapshot.getFileSize(plugin) / 1024f),
snapshot.isInvalid() ? snapshot.getInvalidReason(plugin) : "") snapshot.isInvalid() ? snapshot.getInvalidReason(plugin) : "")
.orElse("• " + snapshot.getId())).toList(), .orElse("• " + snapshot.getId()))
.collect(Collectors.toList()),
plugin.getLocales().getBaseChatList(6) plugin.getLocales().getBaseChatList(6)
.setHeaderFormat(plugin.getLocales() .setHeaderFormat(plugin.getLocales()
.getRawLocale("data_list_title", dataOwner.getUsername(), .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 { shadowJar {
mergeServiceFiles()
dependencies { dependencies {
exclude(dependency('com.mojang:brigadier')) 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.io', 'net.william278.husksync.libraries.commons.io'
relocate 'org.apache.commons.text', 'net.william278.husksync.libraries.commons.text' relocate 'org.apache.commons.text', 'net.william278.husksync.libraries.commons.text'
relocate 'org.apache.commons.lang3', 'net.william278.husksync.libraries.commons.lang3' 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 'dev.triumphteam.gui', 'net.william278.husksync.libraries.triumphgui'
relocate 'space.arim.morepaperlib', 'net.william278.husksync.libraries.paperlib' relocate 'space.arim.morepaperlib', 'net.william278.husksync.libraries.paperlib'
relocate 'de.tr7zw.changeme.nbtapi', 'net.william278.husksync.libraries.nbtapi' relocate 'de.tr7zw.changeme.nbtapi', 'net.william278.husksync.libraries.nbtapi'
minimize()
} }
tasks { tasks {

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

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

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