Compare commits

..

16 Commits

Author SHA1 Message Date
InkerBot 29d8f2ecf4 add legacy support 6 months ago
dependabot[bot] 2df9fd897a
deps: bump net.kyori:adventure-platform-bukkit from 4.3.3 to 4.3.4 (#360)
Bumps [net.kyori:adventure-platform-bukkit](https://github.com/KyoriPowered/adventure-platform) from 4.3.3 to 4.3.4.
- [Release notes](https://github.com/KyoriPowered/adventure-platform/releases)
- [Commits](https://github.com/KyoriPowered/adventure-platform/compare/v4.3.3...v4.3.4)

---
updated-dependencies:
- dependency-name: net.kyori:adventure-platform-bukkit
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6 months ago
dependabot[bot] ff2531539e
deps: bump net.kyori:adventure-platform-api from 4.3.3 to 4.3.4 (#361)
Bumps [net.kyori:adventure-platform-api](https://github.com/KyoriPowered/adventure-platform) from 4.3.3 to 4.3.4.
- [Release notes](https://github.com/KyoriPowered/adventure-platform/releases)
- [Commits](https://github.com/KyoriPowered/adventure-platform/compare/v4.3.3...v4.3.4)

---
updated-dependencies:
- dependency-name: net.kyori:adventure-platform-api
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6 months ago
William 52ec138273
fix: suppress map cursor paper exception 7 months ago
William 0f7a866652
test: bump test deps to 1.21.1 7 months ago
William eeb52ac41e
deps: bump item-nbt-api to 2.13.2
support MC 1.21.1
7 months ago
William 4c7ec9ec21
docs: update config file 7 months ago
William 2f9064c4c6
refactor: revert "disable attributes/potion effects by default" 7 months ago
William 5c234cdb1d
feat: improve server version status text 7 months ago
William 7d8a74381b
build: bump runtime dependencies 7 months ago
William 04a7793585
refactor: auto-reformat code 7 months ago
William ea068529f6
fix: stop syncing ambient effects, close #289
Effects from beacons, conduits, and The Warden will no longer sync.
7 months ago
William fead3df0d8
fix: add boot warning to fabric 7 months ago
William 0c5a42a344
fix: Cancel outbound PacketEvents packets, close #344 7 months ago
William 75a2378ea8
feat: deprecate Toast notifications 7 months ago
William 662fc96ad5
refactor: disable potion effects & attributes by default 7 months ago

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

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

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

@ -62,7 +62,7 @@ public class BukkitHuskSyncAPI extends HuskSyncAPI {
public static BukkitHuskSyncAPI getInstance() {
if (!JavaPlugin.getProvidingPlugin(BukkitHuskSyncAPI.class).getName().equals("HuskSync")) {
throw new NotRegisteredException("This is likely because you have shaded HuskSync into your plugin JAR " +
"and need to fix your maven/gradle/build script so that it *compiles against* HuskSync instead.");
"and need to fix your maven/gradle/build script so that it *compiles against* HuskSync instead.");
}
if (instance == null) {
throw new NotRegisteredException();

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

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

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

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

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

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

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

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

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

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

@ -23,14 +23,10 @@ import de.themoep.minedown.adventure.MineDown;
import dev.triumphteam.gui.builder.gui.StorageBuilder;
import dev.triumphteam.gui.guis.Gui;
import dev.triumphteam.gui.guis.StorageGui;
import net.roxeez.advancement.display.FrameType;
import net.william278.andjam.Toast;
import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.BukkitData;
import net.william278.husksync.data.BukkitUserDataHolder;
import net.william278.husksync.data.Data;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.ApiStatus;
@ -40,8 +36,6 @@ import java.util.Arrays;
import java.util.function.Consumer;
import java.util.logging.Level;
import static net.william278.husksync.util.BukkitKeyedAdapter.matchMaterial;
/**
* Bukkit platform implementation of an {@link OnlineUser}
*/
@ -68,20 +62,12 @@ public class BukkitUser extends OnlineUser implements BukkitUserDataHolder {
}
@Override
@Deprecated
public void sendToast(@NotNull MineDown title, @NotNull MineDown description,
@NotNull String iconMaterial, @NotNull String backgroundType) {
try {
final Material material = matchMaterial(iconMaterial);
Toast.builder((BukkitHuskSync) plugin)
.setTitle(title.toComponent())
.setDescription(description.toComponent())
.setIcon(material != null ? material : Material.BARRIER)
.setFrameType(FrameType.valueOf(backgroundType))
.build()
.show(player);
} catch (Throwable e) {
plugin.log(Level.WARNING, "Failed to send toast to player " + player.getName(), e);
}
plugin.log(Level.WARNING, "Toast notifications are deprecated. " +
"Please change your notification display slot to CHAT, ACTION_BAR or NONE.");
this.sendActionBar(title);
}
@Override

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

@ -69,7 +69,7 @@ public interface BukkitMapPersister {
if (!getPlugin().getSettings().getSynchronization().isPersistLockedMaps()) {
return items;
}
return forEachMap(items, map -> this.persistMapView(map, delegateRenderer));
return private$forEachMap(items, map -> this.persistMapView(map, delegateRenderer));
}
/**
@ -83,21 +83,26 @@ public interface BukkitMapPersister {
if (!getPlugin().getSettings().getSynchronization().isPersistLockedMaps()) {
return items;
}
return forEachMap(items, this::applyMapView);
return private$forEachMap(items, this::private$applyMapView);
}
// Perform an operation on each map in an array of ItemStacks
@NotNull
private ItemStack[] forEachMap(ItemStack[] items, @NotNull Function<ItemStack, ItemStack> function) {
default ItemStack[] private$forEachMap(ItemStack[] items, @NotNull Function<ItemStack, ItemStack> function) {
for (int i = 0; i < items.length; i++) {
final ItemStack item = items[i];
if (item == null) {
continue;
}
if (item.getType() == Material.FILLED_MAP && item.hasItemMeta()) {
if (MaterialUtil.isFilledMap(item.getType()) && item.hasItemMeta()) {
items[i] = function.apply(item);
} else if (item.getItemMeta() instanceof BlockStateMeta b && b.getBlockState() instanceof ShulkerBox box) {
forEachMap(box.getInventory().getContents(), function);
} else if (item.getItemMeta() instanceof BlockStateMeta &&
((BlockStateMeta) item.getItemMeta()).getBlockState() instanceof ShulkerBox
) {
BlockStateMeta b = (BlockStateMeta) item.getItemMeta();
ShulkerBox box = (ShulkerBox) b.getBlockState();
private$forEachMap(box.getInventory().getContents(), function);
b.setBlockState(box);
}
}
@ -105,7 +110,7 @@ public interface BukkitMapPersister {
}
@NotNull
private ItemStack persistMapView(@NotNull ItemStack map, @NotNull Player delegateRenderer) {
default ItemStack persistMapView(@NotNull ItemStack map, @NotNull Player delegateRenderer) {
final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta());
if (!meta.hasMapView()) {
return map;
@ -139,7 +144,7 @@ public interface BukkitMapPersister {
}
@NotNull
private ItemStack applyMapView(@NotNull ItemStack map) {
default ItemStack private$applyMapView(@NotNull ItemStack map) {
final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta());
NBT.get(map, nbt -> {
if (!nbt.hasTag(MAP_DATA_KEY)) {
@ -155,8 +160,8 @@ public interface BukkitMapPersister {
Optional<String> world = Optional.empty();
for (String worldUid : mapIds.getKeys()) {
world = getPlugin().getServer().getWorlds().stream()
.map(w -> w.getUID().toString()).filter(u -> u.equals(worldUid))
.findFirst();
.map(w -> w.getUID().toString()).filter(u -> u.equals(worldUid))
.findFirst();
if (world.isPresent()) {
break;
}
@ -179,15 +184,15 @@ public interface BukkitMapPersister {
try {
getPlugin().debug("Deserializing map data from NBT and generating view...");
canvasData = MapData.fromByteArray(Objects.requireNonNull(mapData.getByteArray(MAP_PIXEL_DATA_KEY),
"Map pixel data is null"));
"Map pixel data is null"));
} catch (Throwable e) {
getPlugin().log(Level.WARNING, "Failed to deserialize map data from NBT", e);
return;
}
// Add a renderer to the map with the data and save to file
final MapView view = generateRenderedMap(canvasData);
final String worldUid = getDefaultMapWorld().getUID().toString();
final MapView view = private$generateRenderedMap(canvasData);
final String worldUid = private$getDefaultMapWorld().getUID().toString();
meta.setMapView(view);
map.setItemMeta(meta);
saveMapToFile(canvasData, view.getId());
@ -195,8 +200,8 @@ public interface BukkitMapPersister {
// Set the map view ID in NBT
NBT.modify(map, editable -> {
Objects.requireNonNull(editable.getCompound(MAP_VIEW_ID_MAPPINGS_KEY),
"Map view ID mappings compound is null")
.setInteger(worldUid, view.getId());
"Map view ID mappings compound is null")
.setInteger(worldUid, view.getId());
});
getPlugin().debug(String.format("Generated view (#%s) and updated map (UID: %s)", view.getId(), worldUid));
});
@ -204,7 +209,7 @@ public interface BukkitMapPersister {
}
default void renderMapFromFile(@NotNull MapView view) {
final File mapFile = new File(getMapCacheFolder(), view.getId() + ".dat");
final File mapFile = new File(private$getMapCacheFolder(), view.getId() + ".dat");
if (!mapFile.exists()) {
return;
}
@ -232,7 +237,7 @@ public interface BukkitMapPersister {
default void saveMapToFile(@NotNull MapData data, int id) {
getPlugin().runAsync(() -> {
final File mapFile = new File(getMapCacheFolder(), id + ".dat");
final File mapFile = new File(private$getMapCacheFolder(), id + ".dat");
if (mapFile.exists()) {
return;
}
@ -248,7 +253,7 @@ public interface BukkitMapPersister {
}
@NotNull
private File getMapCacheFolder() {
default File private$getMapCacheFolder() {
final File mapCache = new File(getPlugin().getDataFolder(), "maps");
if (!mapCache.exists() && !mapCache.mkdirs()) {
getPlugin().log(Level.WARNING, "Failed to create maps folder");
@ -258,8 +263,8 @@ public interface BukkitMapPersister {
// Sets the renderer of a map, and returns the generated MapView
@NotNull
private MapView generateRenderedMap(@NotNull MapData canvasData) {
final MapView view = Bukkit.createMap(getDefaultMapWorld());
default MapView private$generateRenderedMap(@NotNull MapData canvasData) {
final MapView view = Bukkit.createMap(private$getDefaultMapWorld());
view.getRenderers().clear();
// Create a new map view renderer with the map data color at each pixel
@ -275,7 +280,7 @@ public interface BukkitMapPersister {
}
@NotNull
private static World getDefaultMapWorld() {
static World private$getDefaultMapWorld() {
final World world = Bukkit.getWorlds().get(0);
if (world == null) {
throw new IllegalStateException("No worlds are loaded on the server!");
@ -318,37 +323,88 @@ public interface BukkitMapPersister {
cursors.removeCursor(cursors.getCursor(0));
}
canvasData.getBanners().forEach(banner -> cursors.addCursor(createBannerCursor(banner)));
canvasData.getBanners().forEach(banner -> cursors.addCursor(private$createBannerCursor(banner)));
canvas.setCursors(cursors);
}
}
@NotNull
private static MapCursor createBannerCursor(@NotNull MapBanner banner) {
static MapCursor private$createBannerCursor(@NotNull MapBanner banner) {
MapCursor.Type type;
switch (banner.getColor().toLowerCase(Locale.ENGLISH)) {
case "white": {
type = MapCursor.Type.BANNER_WHITE;
break;
}
case "orange": {
type = MapCursor.Type.BANNER_ORANGE;
break;
}
case "magenta": {
type = MapCursor.Type.BANNER_MAGENTA;
break;
}
case "light_blue": {
type = MapCursor.Type.BANNER_LIGHT_BLUE;
break;
}
case "yellow": {
type = MapCursor.Type.BANNER_YELLOW;
break;
}
case "lime": {
type = MapCursor.Type.BANNER_LIME;
break;
}
case "pink": {
type = MapCursor.Type.BANNER_PINK;
break;
}
case "gray": {
type = MapCursor.Type.BANNER_GRAY;
break;
}
case "light_gray": {
type = MapCursor.Type.BANNER_LIGHT_GRAY;
break;
}
case "cyan": {
type = MapCursor.Type.BANNER_CYAN;
break;
}
case "purple": {
type = MapCursor.Type.BANNER_PURPLE;
break;
}
case "blue": {
type = MapCursor.Type.BANNER_BLUE;
break;
}
case "brown": {
type = MapCursor.Type.BANNER_BROWN;
break;
}
case "green": {
type = MapCursor.Type.BANNER_GREEN;
break;
}
case "red": {
type = MapCursor.Type.BANNER_RED;
break;
}
default: {
type = MapCursor.Type.BANNER_BLACK;
break;
}
}
return new MapCursor(
(byte) banner.getPosition().getX(),
(byte) banner.getPosition().getZ(),
(byte) 8, // Always rotate banners upright
switch (banner.getColor().toLowerCase(Locale.ENGLISH)) {
case "white" -> MapCursor.Type.BANNER_WHITE;
case "orange" -> MapCursor.Type.BANNER_ORANGE;
case "magenta" -> MapCursor.Type.BANNER_MAGENTA;
case "light_blue" -> MapCursor.Type.BANNER_LIGHT_BLUE;
case "yellow" -> MapCursor.Type.BANNER_YELLOW;
case "lime" -> MapCursor.Type.BANNER_LIME;
case "pink" -> MapCursor.Type.BANNER_PINK;
case "gray" -> MapCursor.Type.BANNER_GRAY;
case "light_gray" -> MapCursor.Type.BANNER_LIGHT_GRAY;
case "cyan" -> MapCursor.Type.BANNER_CYAN;
case "purple" -> MapCursor.Type.BANNER_PURPLE;
case "blue" -> MapCursor.Type.BANNER_BLUE;
case "brown" -> MapCursor.Type.BANNER_BROWN;
case "green" -> MapCursor.Type.BANNER_GREEN;
case "red" -> MapCursor.Type.BANNER_RED;
default -> MapCursor.Type.BANNER_BLACK;
},
true,
banner.getText().isEmpty() ? null : banner.getText()
(byte) banner.getPosition().getX(),
(byte) banner.getPosition().getZ(),
(byte) 8, // Always rotate banners upright
type,
true,
banner.getText().isEmpty() ? null : banner.getText()
);
}
@ -409,11 +465,14 @@ public interface BukkitMapPersister {
@NotNull
private String getDimension() {
return mapView.getWorld() != null ? switch (mapView.getWorld().getEnvironment()) {
case NETHER -> "minecraft:the_nether";
case THE_END -> "minecraft:the_end";
default -> "minecraft:overworld";
} : "minecraft:overworld";
if (mapView.getWorld() == null) {
return "minecraft:overworld";
}
switch (mapView.getWorld().getEnvironment()) {
case NETHER: return "minecraft:the_nether";
case THE_END: return "minecraft:the_end";
default: return "minecraft:overworld";
}
}
/**
@ -424,21 +483,25 @@ public interface BukkitMapPersister {
@NotNull
private MapData extractMapData() {
final List<MapBanner> banners = Lists.newArrayList();
final String BANNER_PREFIX = "banner_";
for (int i = 0; i < getCursors().size(); i++) {
final MapCursor cursor = getCursors().getCursor(i);
final String type = cursor.getType().name().toLowerCase(Locale.ENGLISH);
if (type.startsWith(BANNER_PREFIX)) {
banners.add(new MapBanner(
type.replaceAll(BANNER_PREFIX, ""),
cursor.getCaption() == null ? "" : cursor.getCaption(),
cursor.getX(),
mapView.getWorld() != null ? mapView.getWorld().getSeaLevel() : 128,
cursor.getY()
));
try {
final String BANNER_PREFIX = "banner_";
for (int i = 0; i < getCursors().size(); i++) {
final MapCursor cursor = getCursors().getCursor(i);
final String type = cursor.getType().name().toLowerCase(Locale.ENGLISH);
if (type.startsWith(BANNER_PREFIX)) {
banners.add(new MapBanner(
type.replaceAll(BANNER_PREFIX, ""),
cursor.getCaption() == null ? "" : cursor.getCaption(),
cursor.getX(),
mapView.getWorld() != null ? mapView.getWorld().getSeaLevel() : 128,
cursor.getY()
));
}
}
} catch (Throwable ignored) {
}
return MapData.fromPixels(pixels, getDimension(), (byte) 2, banners, List.of());
return MapData.fromPixels(pixels, getDimension(), (byte) 2, banners, Collections.emptyList());
}
}

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

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

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

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

@ -196,7 +196,7 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
// Get the debug log message format
@NotNull
private String getDebugString(@NotNull String message) {
default String getDebugString(@NotNull String message) {
return String.format("[DEBUG] [%s] %s", new SimpleDateFormat("mm:ss.SSS").format(new Date()), message);
}
@ -255,6 +255,14 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
@NotNull
String getPlatformType();
/**
* Returns the server software version
*
* @return the server software version string
*/
@NotNull
String getServerVersion();
/**
* Returns the legacy data converter if it exists
*
@ -265,10 +273,10 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
@NotNull
default UpdateChecker getUpdateChecker() {
return UpdateChecker.builder()
.currentVersion(getPluginVersion())
.endpoint(UpdateChecker.Endpoint.SPIGOT)
.resource(Integer.toString(SPIGOT_RESOURCE_ID))
.build();
.currentVersion(getPluginVersion())
.endpoint(UpdateChecker.Endpoint.SPIGOT)
.resource(Integer.toString(SPIGOT_RESOURCE_ID))
.build();
}
default void checkForUpdates() {
@ -276,8 +284,8 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
getUpdateChecker().check().thenAccept(checked -> {
if (!checked.isUpToDate()) {
log(Level.WARNING, String.format(
"A new version of HuskSync is available: v%s (running v%s)",
checked.getLatestVersion(), getPluginVersion())
"A new version of HuskSync is available: v%s (running v%s)",
checked.getLatestVersion(), getPluginVersion())
);
}
});
@ -319,16 +327,16 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
*/
final class FailedToLoadException extends IllegalStateException {
private static final String FORMAT = """
HuskSync has failed to load! The plugin will not be enabled and no data will be synchronized.
Please make sure the plugin has been setup correctly (https://william278.net/docs/husksync/setup):
1) Make sure you've entered your MySQL, MariaDB or MongoDB database details correctly in config.yml
2) Make sure your Redis server details are also correct in config.yml
3) Make sure your config is up-to-date (https://william278.net/docs/husksync/config-file)
4) Check the error below for more details
Caused by: %s""";
private static final String FORMAT =
"HuskSync has failed to load! The plugin will not be enabled and no data will be synchronized.\n" +
"Please make sure the plugin has been setup correctly (https://william278.net/docs/husksync/setup):\n" +
"\n" +
"1) Make sure you've entered your MySQL, MariaDB or MongoDB database details correctly in config.yml\n" +
"2) Make sure your Redis server details are also correct in config.yml\n" +
"3) Make sure your config is up-to-date (https://william278.net/docs/husksync/config-file)\n" +
"4) Check the error below for more details\n" +
"\n" +
"Caused by: %s";
FailedToLoadException(@NotNull String message, @NotNull Throwable cause) {
super(String.format(FORMAT, message), cause);

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

@ -19,6 +19,7 @@
package net.william278.husksync.command;
import com.google.common.collect.ImmutableList;
import de.themoep.minedown.adventure.MineDown;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.Data;
@ -37,38 +38,38 @@ import java.util.Optional;
public class EnderChestCommand extends ItemsCommand {
public EnderChestCommand(@NotNull HuskSync plugin) {
super("enderchest", List.of("echest", "openechest"), plugin);
super("enderchest", ImmutableList.of("echest", "openechest"), plugin);
}
@Override
protected void showItems(@NotNull OnlineUser viewer, @NotNull DataSnapshot.Unpacked snapshot,
@NotNull User user, boolean allowEdit) {
final Optional<Data.Items.EnderChest> optionalEnderChest = snapshot.getEnderChest();
if (optionalEnderChest.isEmpty()) {
if (!optionalEnderChest.isPresent()) {
plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage);
.ifPresent(viewer::sendMessage);
return;
}
// Display opening message
plugin.getLocales().getLocale("ender_chest_viewer_opened", user.getUsername(),
snapshot.getTimestamp().format(DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
.ifPresent(viewer::sendMessage);
snapshot.getTimestamp().format(DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
.ifPresent(viewer::sendMessage);
// Show GUI
final Data.Items.EnderChest enderChest = optionalEnderChest.get();
viewer.showGui(
enderChest,
plugin.getLocales().getLocale("ender_chest_viewer_menu_title", user.getUsername())
.orElse(new MineDown(String.format("%s's Ender Chest", user.getUsername()))),
allowEdit,
enderChest.getSlotCount(),
(itemsOnClose) -> {
if (allowEdit && !enderChest.equals(itemsOnClose)) {
plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user));
enderChest,
plugin.getLocales().getLocale("ender_chest_viewer_menu_title", user.getUsername())
.orElse(new MineDown(String.format("%s's Ender Chest", user.getUsername()))),
allowEdit,
enderChest.getSlotCount(),
(itemsOnClose) -> {
if (allowEdit && !enderChest.equals(itemsOnClose)) {
plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user));
}
}
}
);
}
@ -76,9 +77,9 @@ public class EnderChestCommand extends ItemsCommand {
@SuppressWarnings("DuplicatedCode")
private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User holder) {
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
if (latestData.isEmpty()) {
if (!latestData.isPresent()) {
plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage);
.ifPresent(viewer::sendMessage);
return;
}
@ -88,7 +89,7 @@ public class EnderChestCommand extends ItemsCommand {
data.getEnderChest().ifPresent(enderChest -> enderChest.setContents(items));
data.setSaveCause(DataSnapshot.SaveCause.ENDERCHEST_COMMAND);
data.setPinned(
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.ENDERCHEST_COMMAND)
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.ENDERCHEST_COMMAND)
);
});

@ -19,6 +19,7 @@
package net.william278.husksync.command;
import com.google.common.collect.ImmutableList;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import de.themoep.minedown.adventure.MineDown;
@ -30,9 +31,11 @@ import net.kyori.adventure.text.format.TextColor;
import net.william278.desertwell.about.AboutMenu;
import net.william278.desertwell.util.UpdateChecker;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.Identifier;
import net.william278.husksync.database.Database;
import net.william278.husksync.migrator.Migrator;
import net.william278.husksync.user.CommandUser;
import net.william278.husksync.util.StringUtil;
import net.william278.uniform.BaseCommand;
import net.william278.uniform.CommandProvider;
import net.william278.uniform.Permission;
@ -41,7 +44,7 @@ import org.apache.commons.text.WordUtils;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.stream.Collectors;
@ -52,41 +55,41 @@ public class HuskSyncCommand extends PluginCommand {
private final AboutMenu aboutMenu;
public HuskSyncCommand(@NotNull HuskSync plugin) {
super("husksync", List.of(), Permission.Default.TRUE, ExecutionScope.ALL, plugin);
super("husksync", ImmutableList.of(), Permission.Default.TRUE, ExecutionScope.ALL, plugin);
this.updateChecker = plugin.getUpdateChecker();
this.aboutMenu = AboutMenu.builder()
.title(Component.text("HuskSync"))
.description(Component.text("A modern, cross-server player data synchronization system"))
.version(plugin.getPluginVersion())
.credits("Author",
AboutMenu.Credit.of("William278").description("Click to visit website").url("https://william278.net"))
.credits("Contributors",
AboutMenu.Credit.of("HarvelsX").description("Code"),
AboutMenu.Credit.of("HookWoods").description("Code"),
AboutMenu.Credit.of("Preva1l").description("Code"),
AboutMenu.Credit.of("hanbings").description("Code (Fabric porting)"),
AboutMenu.Credit.of("Stampede2011").description("Code (Fabric mixins)"))
.credits("Translators",
AboutMenu.Credit.of("Namiu").description("Japanese (ja-jp)"),
AboutMenu.Credit.of("anchelthe").description("Spanish (es-es)"),
AboutMenu.Credit.of("Melonzio").description("Spanish (es-es)"),
AboutMenu.Credit.of("Ceddix").description("German (de-de)"),
AboutMenu.Credit.of("Pukejoy_1").description("Bulgarian (bg-bg)"),
AboutMenu.Credit.of("mateusneresrb").description("Brazilian Portuguese (pt-br)"),
AboutMenu.Credit.of("小蔡").description("Traditional Chinese (zh-tw)"),
AboutMenu.Credit.of("Ghost-chu").description("Simplified Chinese (zh-cn)"),
AboutMenu.Credit.of("DJelly4K").description("Simplified Chinese (zh-cn)"),
AboutMenu.Credit.of("Thourgard").description("Ukrainian (uk-ua)"),
AboutMenu.Credit.of("xF3d3").description("Italian (it-it)"),
AboutMenu.Credit.of("cada3141").description("Korean (ko-kr)"),
AboutMenu.Credit.of("Wirayuda5620").description("Indonesian (id-id)"),
AboutMenu.Credit.of("WinTone01").description("Turkish (tr-tr)"),
AboutMenu.Credit.of("IbanEtchep").description("French (fr-fr)"))
.buttons(
AboutMenu.Link.of("https://william278.net/docs/husksync").text("Documentation").icon("⛏"),
AboutMenu.Link.of("https://github.com/WiIIiam278/HuskSync/issues").text("Issues").icon("❌").color(TextColor.color(0xff9f0f)),
AboutMenu.Link.of("https://discord.gg/tVYhJfyDWG").text("Discord").icon("⭐").color(TextColor.color(0x6773f5)))
.build();
.title(Component.text("HuskSync"))
.description(Component.text("A modern, cross-server player data synchronization system"))
.version(plugin.getPluginVersion())
.credits("Author",
AboutMenu.Credit.of("William278").description("Click to visit website").url("https://william278.net"))
.credits("Contributors",
AboutMenu.Credit.of("HarvelsX").description("Code"),
AboutMenu.Credit.of("HookWoods").description("Code"),
AboutMenu.Credit.of("Preva1l").description("Code"),
AboutMenu.Credit.of("hanbings").description("Code (Fabric porting)"),
AboutMenu.Credit.of("Stampede2011").description("Code (Fabric mixins)"))
.credits("Translators",
AboutMenu.Credit.of("Namiu").description("Japanese (ja-jp)"),
AboutMenu.Credit.of("anchelthe").description("Spanish (es-es)"),
AboutMenu.Credit.of("Melonzio").description("Spanish (es-es)"),
AboutMenu.Credit.of("Ceddix").description("German (de-de)"),
AboutMenu.Credit.of("Pukejoy_1").description("Bulgarian (bg-bg)"),
AboutMenu.Credit.of("mateusneresrb").description("Brazilian Portuguese (pt-br)"),
AboutMenu.Credit.of("小蔡").description("Traditional Chinese (zh-tw)"),
AboutMenu.Credit.of("Ghost-chu").description("Simplified Chinese (zh-cn)"),
AboutMenu.Credit.of("DJelly4K").description("Simplified Chinese (zh-cn)"),
AboutMenu.Credit.of("Thourgard").description("Ukrainian (uk-ua)"),
AboutMenu.Credit.of("xF3d3").description("Italian (it-it)"),
AboutMenu.Credit.of("cada3141").description("Korean (ko-kr)"),
AboutMenu.Credit.of("Wirayuda5620").description("Indonesian (id-id)"),
AboutMenu.Credit.of("WinTone01").description("Turkish (tr-tr)"),
AboutMenu.Credit.of("IbanEtchep").description("French (fr-fr)"))
.buttons(
AboutMenu.Link.of("https://william278.net/docs/husksync").text("Documentation").icon("⛏"),
AboutMenu.Link.of("https://github.com/WiIIiam278/HuskSync/issues").text("Issues").icon("❌").color(TextColor.color(0xff9f0f)),
AboutMenu.Link.of("https://discord.gg/tVYhJfyDWG").text("Discord").icon("⭐").color(TextColor.color(0x6773f5)))
.build();
}
@Override
@ -109,8 +112,9 @@ public class HuskSyncCommand extends PluginCommand {
final CommandUser user = user(sub, ctx);
plugin.getLocales().getLocale("system_status_header").ifPresent(user::sendMessage);
user.sendMessage(Component.join(
JoinConfiguration.newlines(),
Arrays.stream(StatusLine.values()).map(s -> s.get(plugin)).toList()
JoinConfiguration.newlines(),
Arrays.stream(StatusLine.values()).map(s -> s.get(plugin))
.collect(Collectors.toList())
));
});
}
@ -126,7 +130,7 @@ public class HuskSyncCommand extends PluginCommand {
plugin.getLocales().getLocale("reload_complete").ifPresent(user::sendMessage);
} catch (Throwable e) {
user.sendMessage(new MineDown(
"[Error:](#ff3300) [Failed to reload the plugin. Check console for errors.](#ff7e5e)"
"[Error:](#ff3300) [Failed to reload the plugin. Check console for errors.](#ff7e5e)"
));
plugin.log(Level.SEVERE, "Failed to reload the plugin", e);
}
@ -139,11 +143,11 @@ public class HuskSyncCommand extends PluginCommand {
final CommandUser user = user(sub, ctx);
if (checked.isUpToDate()) {
plugin.getLocales().getLocale("up_to_date", plugin.getPluginVersion().toString())
.ifPresent(user::sendMessage);
.ifPresent(user::sendMessage);
return;
}
plugin.getLocales().getLocale("update_available", checked.getLatestVersion().toString(),
plugin.getPluginVersion().toString()).ifPresent(user::sendMessage);
plugin.getPluginVersion().toString()).ifPresent(user::sendMessage);
}));
}
@ -154,10 +158,10 @@ public class HuskSyncCommand extends PluginCommand {
sub.setDefaultExecutor((ctx) -> {
plugin.log(Level.INFO, "Please choose a migrator, then run \"husksync migrate start <migrator>\"");
plugin.log(Level.INFO, String.format(
"List of available migrators:\nMigrator ID / Migrator Name:\n%s",
plugin.getAvailableMigrators().stream()
.map(migrator -> String.format("%s - %s", migrator.getIdentifier(), migrator.getName()))
.collect(Collectors.joining("\n"))
"List of available migrators:\nMigrator ID / Migrator Name:\n%s",
plugin.getAvailableMigrators().stream()
.map(migrator -> String.format("%s - %s", migrator.getIdentifier(), migrator.getName()))
.collect(Collectors.joining("\n"))
));
});
sub.addSubCommand("help", (help) -> help.addSyntax((cmd) -> {
@ -187,7 +191,7 @@ public class HuskSyncCommand extends PluginCommand {
return new ArgumentElement<>("migrator", reader -> {
final String id = reader.readString();
final Migrator migrator = plugin.getAvailableMigrators().stream()
.filter(m -> m.getIdentifier().equalsIgnoreCase(id)).findFirst().orElse(null);
.filter(m -> m.getIdentifier().equalsIgnoreCase(id)).findFirst().orElse(null);
if (migrator == null) {
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader);
}
@ -202,54 +206,54 @@ public class HuskSyncCommand extends PluginCommand {
private enum StatusLine {
PLUGIN_VERSION(plugin -> Component.text("v" + plugin.getPluginVersion().toStringWithoutMetadata())
.appendSpace().append(plugin.getPluginVersion().getMetadata().isBlank() ? Component.empty()
: Component.text("(build " + plugin.getPluginVersion().getMetadata() + ")"))),
PLATFORM_TYPE(plugin -> Component.text(WordUtils.capitalizeFully(plugin.getPlatformType()))),
.appendSpace().append(StringUtil.isBlank(plugin.getPluginVersion().getMetadata()) ? Component.empty()
: Component.text("(build " + plugin.getPluginVersion().getMetadata() + ")"))),
SERVER_VERSION(plugin -> Component.text(plugin.getServerVersion())),
LANGUAGE(plugin -> Component.text(plugin.getSettings().getLanguage())),
MINECRAFT_VERSION(plugin -> Component.text(plugin.getMinecraftVersion().toString())),
JAVA_VERSION(plugin -> Component.text(System.getProperty("java.version"))),
JAVA_VENDOR(plugin -> Component.text(System.getProperty("java.vendor"))),
SERVER_NAME(plugin -> Component.text(plugin.getServerName())),
CLUSTER_ID(plugin -> Component.text(StringUtil.isBlank(plugin.getSettings().getClusterId()) ? "None" : plugin.getSettings().getClusterId())),
SYNC_MODE(plugin -> Component.text(WordUtils.capitalizeFully(
plugin.getSettings().getSynchronization().getMode().toString()
plugin.getSettings().getSynchronization().getMode().toString()
))),
DELAY_LATENCY(plugin -> Component.text(
plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds() + "ms"
plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds() + "ms"
)),
SERVER_NAME(plugin -> Component.text(plugin.getServerName())),
CLUSTER_ID(plugin -> Component.text(plugin.getSettings().getClusterId().isBlank() ? "None" : plugin.getSettings().getClusterId())),
DATABASE_TYPE(plugin ->
Component.text(plugin.getSettings().getDatabase().getType().getDisplayName() +
(plugin.getSettings().getDatabase().getType() == Database.Type.MONGO ?
(plugin.getSettings().getDatabase().getMongoSettings().isUsingAtlas() ? " Atlas" : "") : ""))
Component.text(plugin.getSettings().getDatabase().getType().getDisplayName() +
(plugin.getSettings().getDatabase().getType() == Database.Type.MONGO ?
(plugin.getSettings().getDatabase().getMongoSettings().isUsingAtlas() ? " Atlas" : "") : ""))
),
IS_DATABASE_LOCAL(plugin -> getLocalhostBoolean(plugin.getSettings().getDatabase().getCredentials().getHost())),
USING_REDIS_SENTINEL(plugin -> getBoolean(
!plugin.getSettings().getRedis().getSentinel().getMaster().isBlank()
!StringUtil.isBlank(plugin.getSettings().getRedis().getSentinel().getMaster())
)),
USING_REDIS_PASSWORD(plugin -> getBoolean(
!plugin.getSettings().getRedis().getCredentials().getPassword().isBlank()
!StringUtil.isBlank(plugin.getSettings().getRedis().getCredentials().getPassword())
)),
REDIS_USING_SSL(plugin -> getBoolean(
plugin.getSettings().getRedis().getCredentials().isUseSsl()
plugin.getSettings().getRedis().getCredentials().isUseSsl()
)),
IS_REDIS_LOCAL(plugin -> getLocalhostBoolean(
plugin.getSettings().getRedis().getCredentials().getHost()
plugin.getSettings().getRedis().getCredentials().getHost()
)),
DATA_TYPES(plugin -> Component.join(
JoinConfiguration.commas(true),
plugin.getRegisteredDataTypes().stream().map(i -> Component.textOfChildren(Component.text(i.toString())
.appendSpace().append(Component.text(i.isEnabled() ? '✔' : '❌')))
.color(i.isEnabled() ? NamedTextColor.GREEN : NamedTextColor.RED)
.hoverEvent(HoverEvent.showText(
Component.text(i.isEnabled() ? "Enabled" : "Disabled")
.append(Component.newline())
.append(Component.text("Dependencies: %s".formatted(i.getDependencies()
.isEmpty() ? "(None)" : i.getDependencies().stream()
.map(d -> "%s (%s)".formatted(
d.getKey().value(), d.isRequired() ? "Required" : "Optional"
)).collect(Collectors.joining(", ")))
).color(NamedTextColor.GRAY))
))).toList()
JoinConfiguration.commas(true),
plugin.getRegisteredDataTypes().stream().map(i -> Component.textOfChildren(Component.text(i.toString())
.appendSpace().append(Component.text(i.isEnabled() ? '✔' : '❌')))
.color(i.isEnabled() ? NamedTextColor.GREEN : NamedTextColor.RED)
.hoverEvent(HoverEvent.showText(
Component.text(i.isEnabled() ? "Enabled" : "Disabled")
.append(Component.newline())
.append(i.getDependencies().isEmpty()
? Component.text("Dependencies: None").color(NamedTextColor.GRAY)
: Component.text("Dependencies: " + i.getDependencies().stream()
.map(d -> d.getKey().value() + " (" + (d.isRequired() ? "Required" : "Optional") + ")")
.collect(Collectors.joining(", "))
).color(NamedTextColor.GRAY))
))).collect(Collectors.toList())
));
private final Function<HuskSync, Component> supplier;
@ -261,13 +265,13 @@ public class HuskSyncCommand extends PluginCommand {
@NotNull
private Component get(@NotNull HuskSync plugin) {
return Component
.text("•").appendSpace()
.append(Component.text(
WordUtils.capitalizeFully(name().replaceAll("_", " ")),
TextColor.color(0x848484)
))
.append(Component.text(':')).append(Component.space().color(NamedTextColor.WHITE))
.append(supplier.apply(plugin));
.text("•").appendSpace()
.append(Component.text(
WordUtils.capitalizeFully(name().replaceAll("_", " ")),
TextColor.color(0x848484)
))
.append(Component.text(':')).append(Component.space().color(NamedTextColor.WHITE))
.append(supplier.apply(plugin));
}
@NotNull
@ -278,7 +282,7 @@ public class HuskSyncCommand extends PluginCommand {
@NotNull
private static Component getLocalhostBoolean(@NotNull String value) {
return getBoolean(value.equals("127.0.0.1") || value.equals("0.0.0.0")
|| value.equals("localhost") || value.equals("::1"));
|| value.equals("localhost") || value.equals("::1"));
}
}

@ -19,6 +19,7 @@
package net.william278.husksync.command;
import com.google.common.collect.ImmutableList;
import de.themoep.minedown.adventure.MineDown;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.Data;
@ -37,39 +38,39 @@ import java.util.Optional;
public class InventoryCommand extends ItemsCommand {
public InventoryCommand(@NotNull HuskSync plugin) {
super("inventory", List.of("invsee", "openinv"), plugin);
super("inventory", ImmutableList.of("invsee", "openinv"), plugin);
}
@Override
protected void showItems(@NotNull OnlineUser viewer, @NotNull DataSnapshot.Unpacked snapshot,
@NotNull User user, boolean allowEdit) {
final Optional<Data.Items.Inventory> optionalInventory = snapshot.getInventory();
if (optionalInventory.isEmpty()) {
if (!optionalInventory.isPresent()) {
viewer.sendMessage(new MineDown("what the FUCK is happening"));
plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage);
.ifPresent(viewer::sendMessage);
return;
}
// Display opening message
plugin.getLocales().getLocale("inventory_viewer_opened", user.getUsername(),
snapshot.getTimestamp().format(DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
.ifPresent(viewer::sendMessage);
snapshot.getTimestamp().format(DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
.ifPresent(viewer::sendMessage);
// Show GUI
final Data.Items.Inventory inventory = optionalInventory.get();
viewer.showGui(
inventory,
plugin.getLocales().getLocale("inventory_viewer_menu_title", user.getUsername())
.orElse(new MineDown(String.format("%s's Inventory", user.getUsername()))),
allowEdit,
inventory.getSlotCount(),
(itemsOnClose) -> {
if (allowEdit && !inventory.equals(itemsOnClose)) {
plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user));
inventory,
plugin.getLocales().getLocale("inventory_viewer_menu_title", user.getUsername())
.orElse(new MineDown(String.format("%s's Inventory", user.getUsername()))),
allowEdit,
inventory.getSlotCount(),
(itemsOnClose) -> {
if (allowEdit && !inventory.equals(itemsOnClose)) {
plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user));
}
}
}
);
}
@ -77,9 +78,9 @@ public class InventoryCommand extends ItemsCommand {
@SuppressWarnings("DuplicatedCode")
private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User holder) {
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
if (latestData.isEmpty()) {
if (!latestData.isPresent()) {
plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage);
.ifPresent(viewer::sendMessage);
return;
}
@ -89,7 +90,7 @@ public class InventoryCommand extends ItemsCommand {
data.getInventory().ifPresent(inventory -> inventory.setContents(items));
data.setSaveCause(DataSnapshot.SaveCause.INVENTORY_COMMAND);
data.setPinned(
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.INVENTORY_COMMAND)
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.INVENTORY_COMMAND)
);
});

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

@ -22,7 +22,6 @@ package net.william278.husksync.command;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.Identifier;
import net.william278.husksync.user.CommandUser;
import net.william278.husksync.user.User;
import net.william278.uniform.BaseCommand;
@ -47,7 +46,7 @@ public abstract class PluginCommand extends Command {
}
private static String getDescription(@NotNull HuskSync plugin, @NotNull String name) {
return plugin.getLocales().getRawLocale("%s_command_description".formatted(name)).orElse("");
return plugin.getLocales().getRawLocale(name + "_command_description").orElse("");
}
@NotNull
@ -73,7 +72,10 @@ public abstract class PluginCommand extends Command {
@NotNull
protected CommandUser adapt(net.william278.uniform.CommandUser user) {
return user.getUuid() == null ? plugin.getConsole() : plugin.getOnlineUser(user.getUuid()).orElseThrow();
return user.getUuid() == null
? plugin.getConsole()
: plugin.getOnlineUser(user.getUuid())
.orElseThrow(() -> new IllegalStateException("Online not found"));
}
@NotNull
@ -100,18 +102,6 @@ public abstract class PluginCommand extends Command {
}, (context, builder) -> builder.buildFuture());
}
@NotNull
protected <S> ArgumentElement<S, Identifier> identifier(@NotNull String name) {
return new ArgumentElement<>(name, reader -> {
final String prefixed = !name.contains(":") ? "husksync:%s".formatted(name) : name;
return plugin.getIdentifier(prefixed).orElseThrow(() -> CommandSyntaxException.
BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader));
}, (context, builder) -> {
plugin.getRegisteredDataTypes().stream().map(d -> d.getKey().asString()).forEach(builder::suggest);
return builder.buildFuture();
});
}
public enum Type {
HUSKSYNC_COMMAND(HuskSyncCommand::new),

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

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

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

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

@ -25,11 +25,11 @@ import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Value;
import lombok.experimental.Accessors;
import net.kyori.adventure.key.Key;
import net.william278.husksync.HuskSync;
import net.william278.husksync.user.OnlineUser;
import org.apache.commons.text.WordUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -66,9 +66,13 @@ public interface Data {
return getStack().length;
}
record Stack(@NotNull String material, int amount, @Nullable String name,
@Nullable List<String> lore, @NotNull List<String> enchantments) {
@Value
class Stack {
@NotNull String material;
int amount;
@Nullable String name;
@Nullable List<String> lore;
@NotNull List<String> enchantments;
}
default boolean isEmpty() {
@ -130,29 +134,32 @@ public interface Data {
@NotNull
List<Effect> getActiveEffects();
/**
* Represents a potion effect
*
* @param type the type of potion effect
* @param amplifier the amplifier of the potion effect
* @param duration the duration of the potion effect
* @param isAmbient whether the potion effect is ambient
* @param showParticles whether the potion effect shows particles
* @param hasIcon whether the potion effect displays a HUD icon
*/
record Effect(@SerializedName("type") @NotNull String type,
@SerializedName("amplifier") int amplifier,
@SerializedName("duration") int duration,
@SerializedName("is_ambient") boolean isAmbient,
@SerializedName("show_particles") boolean showParticles,
@SerializedName("has_icon") boolean hasIcon) {
@NotNull
private String asString() {
return "[❌] %s %s (⏰ %d:%02d)".formatted(WordUtils.capitalizeFully(type()),
amplifier(), duration() / (60 * 1000), (duration() / 1000) % 60);
}
@Value
class Effect {
/**
* The type of potion effect
*/
@SerializedName("type") @NotNull String type;
/**
* The amplifier of the potion effect
*/
@SerializedName("amplifier") int amplifier;
/**
* The duration of the potion effect
*/
@SerializedName("duration") int duration;
/**
* Whether the potion effect is ambient
*/
@SerializedName("is_ambient") boolean isAmbient;
/**
* Whether the potion effect shows particles
*/
@SerializedName("show_particles") boolean showParticles;
/**
* Whether the potion effect displays a HUD icon
*/
@SerializedName("has_icon") boolean hasIcon;
}
}
@ -169,12 +176,13 @@ public interface Data {
@NotNull
default List<Advancement> getCompletedExcludingRecipes() {
return getCompleted().stream().filter(adv -> !adv.getKey().startsWith(RECIPE_ADVANCEMENT)).toList();
return getCompleted().stream()
.filter(adv -> !adv.getKey().startsWith(RECIPE_ADVANCEMENT))
.collect(Collectors.toList());
}
void setCompleted(@NotNull List<Advancement> completed);
@NoArgsConstructor(access = AccessLevel.PRIVATE)
class Advancement {
@SerializedName("key")
private String key;
@ -187,6 +195,10 @@ public interface Data {
this.completedCriteria = adaptDateMap(completedCriteria);
}
@SuppressWarnings("unused")
private Advancement() {
}
@NotNull
public static Advancement adapt(@NotNull String key, @NotNull Map<String, Date> completedCriteria) {
return new Advancement(key, completedCriteria);
@ -253,11 +265,11 @@ public interface Data {
void setWorld(@NotNull World world);
record World(
@SerializedName("name") @NotNull String name,
@SerializedName("uuid") @NotNull UUID uuid,
@SerializedName("environment") @NotNull String environment
) {
@Value
class World {
@SerializedName("name") @NotNull String name;
@SerializedName("uuid") @NotNull UUID uuid;
@SerializedName("environment") @NotNull String environment;
}
}
@ -299,17 +311,17 @@ public interface Data {
void setHealth(double health);
/**
* @deprecated Use {@link Attributes#getMaxHealth()} instead
* @deprecated Use {@link Attributes#getMaxHealth()} instead, since 3.5
*/
@Deprecated(forRemoval = true, since = "3.5")
@Deprecated
default double getMaxHealth() {
return getHealth();
}
/**
* @deprecated Use {@link Attributes#setMaxHealth(double)} instead
* @deprecated Use {@link Attributes#setMaxHealth(double)} instead, since 3.5
*/
@Deprecated(forRemoval = true, since = "3.5")
@Deprecated
default void setMaxHealth(double maxHealth) {
}
@ -327,11 +339,11 @@ public interface Data {
List<Attribute> getAttributes();
record Attribute(
@NotNull String name,
double baseValue,
@NotNull Set<Modifier> modifiers
) {
@Value
class Attribute {
@NotNull String name;
double baseValue;
@NotNull Set<Modifier> modifiers;
public double getValue() {
double value = baseValue;
@ -370,7 +382,8 @@ public interface Data {
@Override
public boolean equals(Object obj) {
if (obj instanceof Modifier other) {
if (obj instanceof Modifier) {
Modifier other = (Modifier) obj;
if (uuid == null || other.uuid == null) {
return name.equals(other.name);
}
@ -380,12 +393,16 @@ public interface Data {
}
public double modify(double value) {
return switch (operationType) {
case 0 -> value + amount;
case 1 -> value * amount;
case 2 -> value * (1 + amount);
default -> value;
};
switch (operationType) {
case 0:
return value + amount;
case 1:
return value * amount;
case 2:
return value * (1 + amount);
default:
return value;
}
}
public boolean hasUuid() {
@ -401,12 +418,12 @@ public interface Data {
default Optional<Attribute> getAttribute(@NotNull Key key) {
return getAttributes().stream()
.filter(attribute -> attribute.name().equals(key.asString()))
.filter(attribute -> attribute.getName().equals(key.asString()))
.findFirst();
}
default void removeAttribute(@NotNull Key key) {
getAttributes().removeIf(attribute -> attribute.name().equals(key.asString()));
getAttributes().removeIf(attribute -> attribute.getName().equals(key.asString()));
}
default double getMaxHealth() {
@ -485,9 +502,9 @@ public interface Data {
*
* @return {@code false} since v3.5
* @deprecated Moved to its own data type. This will always return {@code false}.
* Use {@link FlightStatus#isAllowFlight()} instead
* Use {@link FlightStatus#isAllowFlight()} instead, since 3.5
*/
@Deprecated(forRemoval = true, since = "3.5")
@Deprecated
default boolean getAllowFlight() {
return false;
}
@ -496,9 +513,9 @@ public interface Data {
* Set if the player can fly.
*
* @deprecated Moved to its own data type.
* Use {@link FlightStatus#setAllowFlight(boolean)} instead
* Use {@link FlightStatus#setAllowFlight(boolean)} instead, since 3.5
*/
@Deprecated(forRemoval = true, since = "3.5")
@Deprecated
default void setAllowFlight(boolean allowFlight) {
}
@ -507,9 +524,9 @@ public interface Data {
*
* @return {@code false} since v3.5
* @deprecated Moved to its own data type. This will always return {@code false}.
* Use {@link FlightStatus#isFlying()} instead
* Use {@link FlightStatus#isFlying()} instead, since 3.5
*/
@Deprecated(forRemoval = true, since = "3.5")
@Deprecated
default boolean getIsFlying() {
return false;
}
@ -518,9 +535,9 @@ public interface Data {
* Set if the player is flying.
*
* @deprecated Moved to its own data type.
* Use {@link FlightStatus#setFlying(boolean)} instead
* Use {@link FlightStatus#setFlying(boolean)} instead, since 3.5
*/
@Deprecated(forRemoval = true, since = "3.5")
@Deprecated
default void setIsFlying(boolean isFlying) {
}

@ -45,13 +45,13 @@ public class DataException extends IllegalStateException {
@AllArgsConstructor
public enum Reason {
INVALID_MINECRAFT_VERSION((plugin, snapshot) -> String.format("The Minecraft version of the snapshot (%s) is " +
"newer than the server's version (%s). Ensure each server is on the same version of Minecraft.",
"newer than the server's version (%s). Ensure each server is on the same version of Minecraft.",
snapshot.getMinecraftVersion(), plugin.getMinecraftVersion())),
INVALID_FORMAT_VERSION((plugin, snapshot) -> String.format("The format version of the snapshot (%s) is newer " +
"than the server's version (%s). Ensure each server is running the same version of HuskSync.",
"than the server's version (%s). Ensure each server is running the same version of HuskSync.",
snapshot.getFormatVersion(), DataSnapshot.CURRENT_FORMAT_VERSION)),
INVALID_PLATFORM_TYPE((plugin, snapshot) -> String.format("The platform type of the snapshot (%s) does " +
"not match the server's platform type (%s). Ensure each server has the same platform type.",
"not match the server's platform type (%s). Ensure each server has the same platform type.",
snapshot.getPlatformType(), plugin.getPlatformType())),
NO_LEGACY_CONVERTER((plugin, snapshot) -> String.format("No legacy converter to convert format version: %s",
snapshot.getFormatVersion()));

@ -383,7 +383,7 @@ public class DataSnapshot {
private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
@NotNull String saveCause, @NotNull String serverName, @NotNull TreeMap<Identifier, Data> data,
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion) {
super(id, pinned, timestamp, saveCause, serverName, Map.of(), minecraftVersion, platformType, formatVersion);
super(id, pinned, timestamp, saveCause, serverName, Collections.emptyMap(), minecraftVersion, platformType, formatVersion);
this.deserialized = data;
}
@ -392,7 +392,10 @@ public class DataSnapshot {
private TreeMap<Identifier, Data> deserializeData(@NotNull HuskSync plugin) {
return data.entrySet().stream()
.filter(e -> plugin.getIdentifier(e.getKey()).isPresent())
.map(entry -> Map.entry(plugin.getIdentifier(entry.getKey()).orElseThrow(), entry.getValue()))
.map(entry -> Maps.immutableEntry(
plugin.getIdentifier(entry.getKey()).orElseThrow(() -> new IllegalStateException("Invalid identifier")),
entry.getValue()
))
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> plugin.deserializeData(entry.getKey(), entry.getValue(), getMinecraftVersion()),
@ -535,9 +538,9 @@ public class DataSnapshot {
public Builder timestamp(@NotNull OffsetDateTime timestamp) {
if (timestamp.isAfter(OffsetDateTime.now())) {
throw new IllegalArgumentException("Data snapshots cannot have a timestamp set in the future! "
+ "Make sure your database server time matches the server time.\n"
+ "Current game server timestamp: " + OffsetDateTime.now() + " / "
+ "Snapshot timestamp: " + timestamp);
+ "Make sure your database server time matches the server time.\n"
+ "Current game server timestamp: " + OffsetDateTime.now() + " / "
+ "Snapshot timestamp: " + timestamp);
}
this.timestamp = timestamp;
return this;
@ -939,7 +942,7 @@ public class DataSnapshot {
@NotNull
public String getLocale(@NotNull HuskSync plugin) {
return plugin.getLocales()
.getRawLocale("save_cause_%s".formatted(name().toLowerCase(Locale.ENGLISH)))
.getRawLocale("save_cause_" + name().toLowerCase(Locale.ENGLISH))
.orElse(getDisplayName());
}

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

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

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

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

@ -30,6 +30,7 @@ import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.database.mongo.MongoCollectionHelper;
import net.william278.husksync.database.mongo.MongoConnectionHandler;
import net.william278.husksync.user.User;
import net.william278.husksync.util.OptionalUtil;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.bson.types.Binary;
@ -43,6 +44,7 @@ import java.util.Optional;
import java.util.TimeZone;
import java.util.UUID;
import java.util.logging.Level;
import java.util.stream.Collectors;
public class MongoDbDatabase extends Database {
private MongoConnectionHandler mongoConnectionHandler;
@ -50,6 +52,7 @@ public class MongoDbDatabase extends Database {
private final String usersTable;
private final String userDataTable;
public MongoDbDatabase(@NotNull HuskSync plugin) {
super(plugin);
this.usersTable = plugin.getSettings().getDatabase().getTableName(TableName.USERS);
@ -76,7 +79,7 @@ public class MongoDbDatabase extends Database {
}
} catch (Exception e) {
throw new IllegalStateException("Failed to establish a connection to the MongoDB database. " +
"Please check the supplied database credentials in the config file", e);
"Please check the supplied database credentials in the config file", e);
}
}
@ -102,7 +105,7 @@ public class MongoDbDatabase extends Database {
@Override
public void ensureUser(@NotNull User user) {
try {
getUser(user.getUuid()).ifPresentOrElse(
OptionalUtil.ifPresentOrElse(getUser(user.getUuid()),
existingUser -> {
if (!existingUser.getUsername().equals(user.getUsername())) {
// Update a user's name if it has changed in the database
@ -276,7 +279,8 @@ public class MongoDbDatabase extends Database {
protected void rotateSnapshots(@NotNull User user) {
try {
final List<DataSnapshot.Packed> unpinnedUserData = getAllSnapshots(user).stream()
.filter(dataSnapshot -> !dataSnapshot.isPinned()).toList();
.filter(dataSnapshot -> !dataSnapshot.isPinned())
.collect(Collectors.toList());
final int maxSnapshots = plugin.getSettings().getSynchronization().getMaxUserDataSnapshots();
if (unpinnedUserData.size() > maxSnapshots) {
@ -376,7 +380,7 @@ public class MongoDbDatabase extends Database {
/**
* Update a saved {@link DataSnapshot} by given version UUID
*
* @param user The user whose data snapshot
* @param user The user whose data snapshot
* @param data The {@link DataSnapshot} to update
*/
@Blocking

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

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

@ -29,6 +29,7 @@ public class MongoCollectionHelper {
/**
* Initialize the collection helper
*
* @param database Instance of {@link MongoConnectionHandler}
*/
public MongoCollectionHelper(@NotNull MongoConnectionHandler database) {
@ -37,6 +38,7 @@ public class MongoCollectionHelper {
/**
* Create a collection
*
* @param collectionName the collection name
*/
public void createCollection(@NotNull String collectionName) {
@ -45,6 +47,7 @@ public class MongoCollectionHelper {
/**
* Delete a collection
*
* @param collectionName the collection name
*/
public void deleteCollection(@NotNull String collectionName) {
@ -53,6 +56,7 @@ public class MongoCollectionHelper {
/**
* Get a collection
*
* @param collectionName the collection name
* @return MongoCollection<Document>
*/
@ -62,8 +66,9 @@ public class MongoCollectionHelper {
/**
* Add a document to a collection
*
* @param collectionName collection to add to
* @param document Document to add
* @param document Document to add
*/
public void insertDocument(@NotNull String collectionName, @NotNull Document document) {
MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName);
@ -72,9 +77,10 @@ public class MongoCollectionHelper {
/**
* Update a document
*
* @param collectionName collection the document is in
* @param document filter of document
* @param updates Bson of updates
* @param document filter of document
* @param updates Bson of updates
*/
public void updateDocument(@NotNull String collectionName, @NotNull Document document, @NotNull Bson updates) {
MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName);
@ -83,8 +89,9 @@ public class MongoCollectionHelper {
/**
* Delete a document
*
* @param collectionName collection the document is in
* @param document filter to remove
* @param document filter to remove
*/
public void deleteDocument(@NotNull String collectionName, @NotNull Document document) {
MongoCollection<Document> collection = database.getDatabase().getCollection(collectionName);

@ -35,9 +35,10 @@ public class MongoConnectionHandler {
/**
* Initiate a connection to a Mongo Server
*
* @param uri The connection string
*/
public MongoConnectionHandler(@NotNull ConnectionString uri, @NotNull String databaseName) {
public MongoConnectionHandler(@NotNull ConnectionString uri, @NotNull String databaseName) {
try {
final MongoClientSettings settings = MongoClientSettings.builder()
.applyConnectionString(uri)
@ -48,7 +49,7 @@ public class MongoConnectionHandler {
this.database = mongoClient.getDatabase(databaseName);
} catch (Exception e) {
throw new IllegalStateException("Failed to establish a connection to the MongoDB database. " +
"Please check the supplied database credentials in the config file", e);
"Please check the supplied database credentials in the config file", e);
}
}

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

@ -19,6 +19,8 @@
package net.william278.husksync.listener;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.Data;
import net.william278.husksync.data.DataSnapshot;
@ -94,7 +96,7 @@ public abstract class EventListener {
protected void saveOnPlayerDeath(@NotNull OnlineUser user, @NotNull Data.Items items) {
final SaveOnDeathSettings settings = plugin.getSettings().getSynchronization().getSaveOnDeath();
if (plugin.isDisabling() || !settings.isEnabled() || plugin.isLocked(user.getUuid())
|| user.isNpc() || (!settings.isSaveEmptyItems() && items.isEmpty())) {
|| user.isNpc() || (!settings.isSaveEmptyItems() && items.isEmpty())) {
return;
}
@ -159,13 +161,13 @@ public abstract class EventListener {
@NotNull
private Map.Entry<String, String> toEntry() {
return Map.entry(name().toLowerCase(), defaultPriority.name());
return Maps.immutableEntry(name().toLowerCase(), defaultPriority.name());
}
@SuppressWarnings("unchecked")
@NotNull
public static Map<String, String> getDefaults() {
return Map.ofEntries(Arrays.stream(values())
return ImmutableMap.ofEntries(Arrays.stream(values())
.map(ListenerType::toEntry)
.toArray(Map.Entry[]::new));
}

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

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

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

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

@ -24,7 +24,7 @@ import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull;
public interface CommandUser {
public interface CommandUser {
@NotNull
Audience getAudience();

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

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

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

@ -21,6 +21,7 @@ package net.william278.husksync.util;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import lombok.SneakyThrows;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.User;
@ -124,8 +125,9 @@ public class DataDumper {
}
@NotNull
@SneakyThrows
private String getWebContentField() {
return "content=" + URLEncoder.encode(toString(), StandardCharsets.UTF_8);
return "content=" + URLEncoder.encode(toString(), "UTF-8");
}
/**
@ -136,7 +138,7 @@ public class DataDumper {
@NotNull
public String toFile() throws IOException {
final Path filePath = getFilePath();
try (final FileWriter writer = new FileWriter(filePath.toFile(), StandardCharsets.UTF_8, false)) {
try (final Writer writer = Files.newBufferedWriter(filePath, StandardCharsets.UTF_8)) {
writer.write(toString()); // Write the data from #getString to the file using a writer
return filePath.toString();
} catch (IOException e) {
@ -178,11 +180,11 @@ public class DataDumper {
@NotNull
private String getFileName() {
return new StringJoiner("_")
.add(user.getUsername())
.add(snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")))
.add(snapshot.getSaveCause().name().toLowerCase(Locale.ENGLISH))
.add(snapshot.getShortId())
+ ".json";
.add(user.getUsername())
.add(snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")))
.add(snapshot.getSaveCause().name().toLowerCase(Locale.ENGLISH))
.add(snapshot.getShortId())
+ ".json";
}
}

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

@ -106,7 +106,7 @@ public class DataSnapshotOverview {
.ifPresent(user::sendMessage);
if (user.hasPermission("husksync.command.inventory.edit")
&& user.hasPermission("husksync.command.enderchest.edit")) {
&& user.hasPermission("husksync.command.enderchest.edit")) {
locales.getLocale("data_manager_item_buttons", dataOwner.getUsername(), snapshot.getId().toString())
.ifPresent(user::sendMessage);
}

@ -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;
}
}

@ -109,7 +109,7 @@ synchronization:
sync_dead_players_changing_server: true
# Whether to use the snappy data compression algorithm. Keep on unless you know what you're doing
compress_data: true
# Where to display sync notifications (ACTION_BAR, CHAT, TOAST or NONE)
# Where to display sync notifications (ACTION_BAR, CHAT or NONE)
notification_display_slot: ACTION_BAR
# Persist maps locked in a Cartography Table to let them be viewed on any server
persist_locked_maps: true
@ -134,9 +134,14 @@ synchronization:
# Commands which should be blocked before a player has finished syncing (Use * to block all commands)
blacklisted_commands_while_locked:
- '*'
# For attribute syncing, which attributes should be ignored/skipped when syncing
# (e.g. ['minecraft:generic.max_health', 'minecraft:generic.attack_damage'])
ignored_attributes: []
# Configuration for how to sync attributes
attributes:
# Which attributes should not be saved when syncing users. Supports wildcard matching.
# (e.g. ['minecraft:generic.max_health', 'minecraft:generic.*'])
ignored_attributes: []
# Which modifiers should not be saved when syncing users. Supports wildcard matching.
# (e.g. ['minecraft:effect.speed', 'minecraft:effect.*'])
ignored_modifiers: ['minecraft:effect.*', 'minecraft:creative_mode_*']
# Event priorities for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts
event_priorities:
quit_listener: LOWEST

@ -79,12 +79,12 @@ import java.util.logging.Level;
@Getter
@NoArgsConstructor
public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync, FabricTask.Supplier,
FabricEventDispatcher {
FabricEventDispatcher {
private static final String PLATFORM_TYPE_ID = "fabric";
private final TreeMap<Identifier, Serializer<? extends Data>> serializers = Maps.newTreeMap(
SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR
SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR
);
private final Map<UUID, Map<Identifier, Data>> playerCustomDataStore = Maps.newConcurrentMap();
private final Map<String, Boolean> permissions = Maps.newHashMap();
@ -208,6 +208,14 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
// Check for updates
this.checkForUpdates();
log(Level.WARNING, """
**************
WARNING:
HuskSync for Fabric is still in an alpha state and is
not considered production ready.
**************""");
ModLoadedCallback.EVENT.invoker().post(FabricHuskSyncAPI.getInstance());
}
@ -267,15 +275,15 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
@Nullable
public InputStream getResource(@NotNull String name) {
return this.mod.findPath(name)
.map(path -> {
try {
return Files.newInputStream(path);
} catch (IOException e) {
log(Level.WARNING, "Failed to load resource: " + name, e);
}
return null;
})
.orElse(this.getClass().getClassLoader().getResourceAsStream(name));
.map(path -> {
try {
return Files.newInputStream(path);
} catch (IOException e) {
log(Level.WARNING, "Failed to load resource: " + name, e);
}
return null;
})
.orElse(this.getClass().getClassLoader().getResourceAsStream(name));
}
@Override
@ -295,11 +303,11 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
@Override
public void log(@NotNull Level level, @NotNull String message, @NotNull Throwable... throwable) {
LoggingEventBuilder logEvent = logger.makeLoggingEventBuilder(
switch (level.getName()) {
case "WARNING" -> org.slf4j.event.Level.WARN;
case "SEVERE" -> org.slf4j.event.Level.ERROR;
default -> org.slf4j.event.Level.INFO;
}
switch (level.getName()) {
case "WARNING" -> org.slf4j.event.Level.WARN;
case "SEVERE" -> org.slf4j.event.Level.ERROR;
default -> org.slf4j.event.Level.INFO;
}
);
if (throwable.length >= 1) {
logEvent = logEvent.setCause(throwable[0]);
@ -331,6 +339,14 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
return PLATFORM_TYPE_ID;
}
@Override
@NotNull
public String getServerVersion() {
return String.format("%s %s/%s", getPlatformType(), FabricLoader.getInstance()
.getModContainer("fabricloader").map(l -> l.getMetadata().getVersion().getFriendlyString())
.orElse("unknown"), minecraftServer.getVersion());
}
@Override
public Optional<LegacyConverter> getLegacyConverter() {
return Optional.empty();

@ -53,6 +53,7 @@ import net.william278.husksync.user.FabricUser;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Range;
import org.jetbrains.annotations.Unmodifiable;
import java.util.*;
@ -237,8 +238,8 @@ public abstract class FabricData implements Data {
private final Collection<StatusEffectInstance> effects;
@NotNull
public static FabricData.PotionEffects from(@NotNull Collection<StatusEffectInstance> effects) {
return new FabricData.PotionEffects(effects);
public static FabricData.PotionEffects from(@NotNull Collection<StatusEffectInstance> sei) {
return new FabricData.PotionEffects(Lists.newArrayList(sei.stream().filter(e -> !e.isAmbient()).toList()));
}
@NotNull
@ -263,19 +264,21 @@ public abstract class FabricData implements Data {
@NotNull
@SuppressWarnings("unused")
public static FabricData.PotionEffects empty() {
return new FabricData.PotionEffects(List.of());
return new FabricData.PotionEffects(Lists.newArrayList());
}
@Override
public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException {
final ServerPlayerEntity player = user.getPlayer();
List<StatusEffect> effectsToRemove = new ArrayList<>(player.getActiveStatusEffects().keySet());
final List<StatusEffect> effectsToRemove = player.getActiveStatusEffects().entrySet().stream()
.filter(e -> !e.getValue().isAmbient()).map(Map.Entry::getKey).toList();
effectsToRemove.forEach(player::removeStatusEffect);
getEffects().forEach(player::addStatusEffect);
}
@NotNull
@Override
@Unmodifiable
public List<Effect> getActiveEffects() {
return effects.stream()
.map(potionEffect -> {
@ -368,7 +371,7 @@ public abstract class FabricData implements Data {
// Restore player exp level & progress
if (!toAward.isEmpty()
&& (player.experienceLevel != expLevel || player.experienceProgress != expProgress)) {
&& (player.experienceLevel != expLevel || player.experienceProgress != expProgress)) {
player.setExperienceLevel(expLevel);
player.setExperiencePoints((int) (player.getNextLevelExperience() * expProgress));
}

@ -58,7 +58,7 @@ public abstract class FabricSerializer {
}
public static class Inventory extends FabricSerializer implements Serializer<FabricData.Items.Inventory>,
ItemDeserializer {
ItemDeserializer {
public Inventory(@NotNull HuskSync plugin) {
super(plugin);
@ -66,7 +66,7 @@ public abstract class FabricSerializer {
@Override
public FabricData.Items.Inventory deserialize(@NotNull String serialized, @NotNull Version dataMcVersion)
throws DeserializationException {
throws DeserializationException {
// Read item NBT from string
final FabricHuskSync plugin = (FabricHuskSync) getPlugin();
final NbtCompound root;
@ -79,8 +79,8 @@ public abstract class FabricSerializer {
// Deserialize the inventory data
final NbtCompound items = root.contains(ITEMS_TAG) ? root.getCompound(ITEMS_TAG) : null;
return FabricData.Items.Inventory.from(
items != null ? getItems(items, dataMcVersion, plugin) : new ItemStack[INVENTORY_SLOT_COUNT],
root.contains(HELD_ITEM_SLOT_TAG) ? root.getInt(HELD_ITEM_SLOT_TAG) : 0
items != null ? getItems(items, dataMcVersion, plugin) : new ItemStack[INVENTORY_SLOT_COUNT],
root.contains(HELD_ITEM_SLOT_TAG) ? root.getInt(HELD_ITEM_SLOT_TAG) : 0
);
}
@ -105,7 +105,7 @@ public abstract class FabricSerializer {
}
public static class EnderChest extends FabricSerializer implements Serializer<FabricData.Items.EnderChest>,
ItemDeserializer {
ItemDeserializer {
public EnderChest(@NotNull HuskSync plugin) {
super(plugin);
@ -113,7 +113,7 @@ public abstract class FabricSerializer {
@Override
public FabricData.Items.EnderChest deserialize(@NotNull String serialized, @NotNull Version dataMcVersion)
throws DeserializationException {
throws DeserializationException {
final FabricHuskSync plugin = (FabricHuskSync) getPlugin();
try {
final NbtCompound items = StringNbtReader.parse(serialized);
@ -216,8 +216,8 @@ public abstract class FabricSerializer {
private NbtCompound upgradeItemData(@NotNull NbtCompound tag, @NotNull Version mcVersion,
@NotNull FabricHuskSync plugin) {
return (NbtCompound) plugin.getMinecraftServer().getDataFixer().update(
TypeReferences.ITEM_STACK, new Dynamic<Object>((DynamicOps) NbtOps.INSTANCE, tag),
getDataVersion(mcVersion), getDataVersion(plugin.getMinecraftVersion())
TypeReferences.ITEM_STACK, new Dynamic<Object>((DynamicOps) NbtOps.INSTANCE, tag),
getDataVersion(mcVersion), getDataVersion(plugin.getMinecraftVersion())
).getValue();
}
@ -251,7 +251,7 @@ public abstract class FabricSerializer {
@Override
public FabricData.PotionEffects deserialize(@NotNull String serialized) throws DeserializationException {
return FabricData.PotionEffects.adapt(
plugin.getGson().fromJson(serialized, TYPE.getType())
plugin.getGson().fromJson(serialized, TYPE.getType())
);
}
@ -275,7 +275,7 @@ public abstract class FabricSerializer {
@Override
public FabricData.Advancements deserialize(@NotNull String serialized) throws DeserializationException {
return FabricData.Advancements.from(
plugin.getGson().fromJson(serialized, TYPE.getType())
plugin.getGson().fromJson(serialized, TYPE.getType())
);
}

@ -54,8 +54,6 @@ import net.william278.husksync.user.OnlineUser;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.stream.Collectors;
public class FabricEventListener extends EventListener implements LockedHandler {
public FabricEventListener(@NotNull HuskSync plugin) {

@ -50,7 +50,7 @@ public abstract class ServerPlayNetworkHandlerMixin {
@Inject(method = "onPlayerAction", at = @At("HEAD"), cancellable = true)
public void onPlayerAction(PlayerActionC2SPacket packet, CallbackInfo ci) {
if (packet.getAction() == PlayerActionC2SPacket.Action.DROP_ITEM
|| packet.getAction() == PlayerActionC2SPacket.Action.DROP_ALL_ITEMS) {
|| packet.getAction() == PlayerActionC2SPacket.Action.DROP_ALL_ITEMS) {
ItemStack stack = player.getStackInHand(Hand.MAIN_HAND);
ActionResult result = ItemDropCallback.EVENT.invoker().interact(player, stack);

@ -21,7 +21,6 @@ package net.william278.husksync.mixins;
import net.minecraft.entity.ItemEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.ActionResult;
import net.william278.husksync.event.ItemDropCallback;

@ -40,6 +40,7 @@ import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import java.util.function.Consumer;
import java.util.logging.Level;
public class FabricUser extends OnlineUser implements FabricUserDataHolder {
@ -70,9 +71,12 @@ public class FabricUser extends OnlineUser implements FabricUserDataHolder {
}
@Override
@Deprecated(since = "3.6.7")
public void sendToast(@NotNull MineDown title, @NotNull MineDown description, @NotNull String iconMaterial,
@NotNull String backgroundType) {
getAudience().sendActionBar(title.toComponent()); // Toasts unimplemented for now
plugin.log(Level.WARNING, "Toast notifications are deprecated. " +
"Please change your notification display slot to CHAT, ACTION_BAR or NONE.");
this.sendActionBar(title);
}
@Override

@ -3,16 +3,16 @@ org.gradle.jvmargs='-Dfile.encoding=UTF-8'
org.gradle.daemon=true
javaVersion=17
plugin_version=3.6.7
plugin_version=3.6.8
plugin_archive=husksync
plugin_description=A modern, cross-server player data synchronization system
jedis_version=5.1.3
mysql_driver_version=8.4.0
mariadb_driver_version=3.4.0
jedis_version=5.1.4
mysql_driver_version=9.0.0
mariadb_driver_version=3.4.1
postgres_driver_version=42.7.3
mongodb_driver_version=5.1.0
snappy_version=1.1.10.5
mongodb_driver_version=5.1.2
snappy_version=1.1.10.6
fabric_minecraft_version=1.20.1
fabric_loader_version=0.15.11

@ -16,10 +16,16 @@ dependencies {
}
shadowJar {
mergeServiceFiles()
dependencies {
exclude(dependency('com.mojang:brigadier'))
}
relocate 'org.inksnow.cputil', 'net.william278.husksync.libraries.cputil'
relocate 'org.slf4j', 'net.william278.husksync.libraries.slf4j'
relocate 'org.objectweb.asm', 'net.william278.husksync.libraries.asm'
relocate 'org.apache.commons.io', 'net.william278.husksync.libraries.commons.io'
relocate 'org.apache.commons.text', 'net.william278.husksync.libraries.commons.text'
relocate 'org.apache.commons.lang3', 'net.william278.husksync.libraries.commons.lang3'
@ -34,7 +40,6 @@ shadowJar {
relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell'
relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown'
relocate 'net.william278.mapdataapi', 'net.william278.husksync.libraries.mapdataapi'
relocate 'net.william278.andjam', 'net.william278.husksync.libraries.andjam'
relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'
relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter'
relocate 'org.json', 'net.william278.husksync.libraries.json'
@ -44,16 +49,10 @@ shadowJar {
relocate 'dev.triumphteam.gui', 'net.william278.husksync.libraries.triumphgui'
relocate 'space.arim.morepaperlib', 'net.william278.husksync.libraries.paperlib'
relocate 'de.tr7zw.changeme.nbtapi', 'net.william278.husksync.libraries.nbtapi'
minimize()
}
tasks {
runServer {
minecraftVersion('1.21')
downloadPlugins {
url('https://download.luckperms.net/1549/bukkit/loader/LuckPerms-Bukkit-5.4.134.jar')
}
minecraftVersion('1.21.1')
}
}

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

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

@ -10,5 +10,5 @@ include(
'common',
'bukkit',
'paper',
'fabric'
// 'fabric'
)

@ -13,7 +13,7 @@ from tqdm import tqdm
class Parameters:
root_dir = './servers/'
proxy_version = "1.21"
minecraft_version = '1.21'
minecraft_version = '1.21.1'
eula_agreement = 'true'
backend_names = ['alpha', 'beta']

Loading…
Cancel
Save