9
0
mirror of https://github.com/WiIIiam278/HuskSync.git synced 2025-12-26 18:19:10 +00:00

Compare commits

...

41 Commits
3.6.3 ... 3.6.8

Author SHA1 Message Date
William
52ec138273 fix: suppress map cursor paper exception 2024-08-11 12:52:23 +01:00
William
0f7a866652 test: bump test deps to 1.21.1 2024-08-11 12:25:43 +01:00
William
eeb52ac41e deps: bump item-nbt-api to 2.13.2
support MC 1.21.1
2024-08-11 12:25:03 +01:00
William
4c7ec9ec21 docs: update config file 2024-08-09 16:53:26 +01:00
William
2f9064c4c6 refactor: revert "disable attributes/potion effects by default" 2024-08-09 16:48:40 +01:00
William
5c234cdb1d feat: improve server version status text 2024-08-09 15:03:23 +01:00
William
7d8a74381b build: bump runtime dependencies 2024-08-09 14:49:53 +01:00
William
04a7793585 refactor: auto-reformat code 2024-08-09 14:43:54 +01:00
William
ea068529f6 fix: stop syncing ambient effects, close #289
Effects from beacons, conduits, and The Warden will no longer sync.
2024-08-09 14:39:56 +01:00
William
fead3df0d8 fix: add boot warning to fabric 2024-08-09 14:25:17 +01:00
William
0c5a42a344 fix: Cancel outbound PacketEvents packets, close #344 2024-08-09 14:22:53 +01:00
William
75a2378ea8 feat: deprecate Toast notifications 2024-08-09 14:19:34 +01:00
William
662fc96ad5 refactor: disable potion effects & attributes by default 2024-08-09 14:11:19 +01:00
William
07da1c04ce fix: don't apply <1.21 attribute modifiers on >1.21 servers 2024-08-02 18:07:28 +01:00
William
845abf370a fix: more tweaks to fix attribute issues 2024-07-28 18:07:34 +01:00
William
83b5209a75 fix: "attribute modifier already applied" error, close #348 2024-07-26 16:44:04 +01:00
William
8e9850dd19 refactor: make potion effects an optional dep of attributes 2024-07-26 16:35:43 +01:00
William
1d24209b68 feat: add attribute config, don't sync potion modifiers, close #349 2024-07-26 14:26:52 +01:00
William
da70a54d78 ci: add bones publishing to CI 2024-07-24 23:49:20 +01:00
Preva1l
32ac57e2a4 fix: cme on potion effect syncing (#354)
* Started impl for mongo

* fix silly mistake with postgresql

* fix: race condition
2024-07-21 15:14:48 +01:00
William
c949c976d6 fix: more checkout key debug logging 2024-07-21 01:11:44 +01:00
William
ab736829f2 refactor: clarify data syncer method names 2024-07-21 01:04:14 +01:00
William
4433926ce7 build: bump to 3.6.7 2024-07-19 17:35:25 +01:00
William
f819fd4d5e fix: Fabric thread exhaustion/deadlock causing crashes 2024-07-19 17:33:04 +01:00
Stampede
e7659255fe feat: add ModLoaded callback event on Fabric (#346)
Co-authored-by: William <will27528@gmail.com>
2024-07-14 17:09:26 +01:00
Code Toad
0dee2e8319 build: correct grgit null reference in git-less environments, close #345 (#347)
Issue Summary:
when sources are downloaded (not through git) the build fails because of a null reference.
Fix:
replace null reference with empty string

Co-authored-by: codetoad <softwareenginer@pm.me>
2024-07-14 17:08:03 +01:00
William
7b35c47315 fix: wrong syntax processing on husksync migrate set 2024-07-11 13:12:44 +01:00
William
5056a794d8 fix: Set execution scopes in commands 2024-07-06 14:19:04 +01:00
William
5e6068431a build: bump to 3.6.6 2024-07-06 14:11:34 +01:00
William
8d69508689 Merge remote-tracking branch 'origin/master' 2024-07-06 14:11:26 +01:00
William
efb6d8a7de deps: bump lombok and commons 2024-07-06 14:11:20 +01:00
William
79d9778378 deps: bump Uniform to 1.2.1 2024-07-06 14:10:27 +01:00
dependabot[bot]
6a6695e447 build(deps): bump certifi from 2023.7.22 to 2024.7.4 in /test (#337)
Bumps [certifi](https://github.com/certifi/python-certifi) from 2023.7.22 to 2024.7.4.
- [Commits](https://github.com/certifi/python-certifi/compare/2023.07.22...2024.07.04)

---
updated-dependencies:
- dependency-name: certifi
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-06 14:10:01 +01:00
William
8862e6cd70 feat: don't check if dependency is loaded 2024-06-24 19:53:13 +01:00
William
0b29de9efc fix: update documentation, help menu access for migrators 2024-06-23 15:49:28 +01:00
William
962cdfce0b build: bundle postgres in HuskSync for Fabric 2024-06-22 18:15:01 +01:00
William
0c527202e5 fix: NMS exceptions being thrown when applying modifiers
Spigot's validation for this is like my hoover: it sucks.
2024-06-22 18:07:53 +01:00
William
d4e33aa9d2 fix: ensure data version is passed to deserialize methods
Fixes an issue where upgraded stacks would only have a size of 1
2024-06-22 18:06:17 +01:00
William
2fcd58fc18 feat: correctly apply keyed attribute modifiers, close #326
We need to construct attributes with their key if possible to avoid stacking. Uses reflection :( to do this.

Also adds a bit of error checking to health scale syncing
2024-06-21 13:17:53 +01:00
William
3d10b2324f feat: update DataFixer 2024-06-21 11:56:49 +01:00
William
31419f3b97 deps: bump Item NBT API to 2.13.1 2024-06-21 11:56:01 +01:00
59 changed files with 714 additions and 437 deletions

View File

@@ -42,3 +42,23 @@ jobs:
- name: Get Version - name: Get Version
run: | run: |
echo "version_name=${{steps.fetch-version.outputs.VERSION_NAME}}" >> $GITHUB_ENV echo "version_name=${{steps.fetch-version.outputs.VERSION_NAME}}" >> $GITHUB_ENV
- name: 'Publish to William278.net 🚀'
uses: WiIIiam278/bones-publish-action@v1
with:
api-key: ${{ secrets.BONES_API_KEY }}
project: 'husksync'
channel: 'alpha'
version: ${{ env.version_name }}
changelog: ${{ github.event.head_commit.message }}
distro-names: |
paper
fabric-1.20.1
distro-groups: |
paper
fabric
distro-descriptions: |
Paper
Fabric 1.20.1
files: |
target/HuskSync-Paper-${{ env.version_name }}.jar
target/HuskSync-Fabric-${{ env.version_name }}+mc.1.20.1.jar

View File

@@ -31,3 +31,23 @@ jobs:
if: success() || failure() # Continue on failure if: success() || failure() # Continue on failure
with: with:
report_paths: '**/build/test-results/test/TEST-*.xml' report_paths: '**/build/test-results/test/TEST-*.xml'
- name: 'Publish to William278.net 🚀'
uses: WiIIiam278/bones-publish-action@v1
with:
api-key: ${{ secrets.BONES_API_KEY }}
project: 'husksync'
channel: 'release'
version: ${{ github.event.release.tag_name }}
changelog: ${{ github.event.release.body }}
distro-names: |
paper
fabric-1.20.1
distro-groups: |
paper
fabric
distro-descriptions: |
Paper
Fabric 1.20.1
files: |
target/HuskSync-Paper-${{ github.event.release.tag_name }}.jar
target/HuskSync-Fabric-${{ github.event.release.tag_name }}+mc.1.20.1.jar

View File

@@ -83,9 +83,9 @@ allprojects {
} }
dependencies { dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.3'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.2' testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.3'
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.2' testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.3'
} }
test { test {
@@ -99,9 +99,11 @@ allprojects {
} }
processResources { processResources {
def tokenMap = rootProject.ext.properties
tokenMap.merge("grgit",'',(s, s2) -> s)
filesMatching(['**/*.json', '**/*.yml']) { filesMatching(['**/*.json', '**/*.yml']) {
filter ReplaceTokens as Class, beginToken: '${', endToken: '}', filter ReplaceTokens as Class, beginToken: '${', endToken: '}',
tokens: rootProject.ext.properties tokens: tokenMap
} }
} }
} }

View File

@@ -1,21 +1,20 @@
dependencies { dependencies {
implementation project(path: ':common') implementation project(path: ':common')
implementation 'net.william278.uniform:uniform-bukkit:1.1.8' implementation 'net.william278.uniform:uniform-bukkit:1.2.1'
implementation 'net.william278:mpdbdataconverter:1.0.1' implementation 'net.william278:mpdbdataconverter:1.0.1'
implementation 'net.william278:hsldataconverter:1.0' implementation 'net.william278:hsldataconverter:1.0'
implementation 'net.william278:mapdataapi:1.0.3' implementation 'net.william278:mapdataapi:1.0.3'
implementation 'net.william278:andjam:1.0.2'
implementation 'org.bstats:bstats-bukkit:3.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.3'
implementation 'dev.triumphteam:triumph-gui:3.1.10' implementation 'dev.triumphteam:triumph-gui:3.1.10'
implementation 'space.arim.morepaperlib:morepaperlib:0.4.4' implementation 'space.arim.morepaperlib:morepaperlib:0.4.4'
implementation 'de.tr7zw:item-nbt-api:2.13.1-SNAPSHOT' implementation 'de.tr7zw:item-nbt-api:2.13.2'
compileOnly 'org.spigotmc:spigot-api:1.17.1-R0.1-SNAPSHOT' compileOnly 'org.spigotmc:spigot-api:1.17.1-R0.1-SNAPSHOT'
compileOnly 'com.github.retrooper.packetevents:spigot:2.3.0' compileOnly 'com.github.retrooper.packetevents:spigot:2.3.0'
compileOnly 'com.comphenix.protocol:ProtocolLib:5.1.0' compileOnly 'com.comphenix.protocol:ProtocolLib:5.1.0'
compileOnly 'org.projectlombok:lombok:1.18.32' compileOnly 'org.projectlombok:lombok:1.18.34'
compileOnly 'commons-io:commons-io:2.16.1' compileOnly 'commons-io:commons-io:2.16.1'
compileOnly 'org.json:json:20240303' compileOnly 'org.json:json:20240303'
compileOnly 'net.william278:minedown:1.8.2' compileOnly 'net.william278:minedown:1.8.2'
@@ -25,7 +24,7 @@ dependencies {
compileOnly 'net.william278:AdvancementAPI:97a9583413' compileOnly 'net.william278:AdvancementAPI:97a9583413'
compileOnly "redis.clients:jedis:$jedis_version" compileOnly "redis.clients:jedis:$jedis_version"
annotationProcessor 'org.projectlombok:lombok:1.18.32' annotationProcessor 'org.projectlombok:lombok:1.18.34'
} }
shadowJar { shadowJar {
@@ -46,7 +45,6 @@ shadowJar {
relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell' relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell'
relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown' relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown'
relocate 'net.william278.mapdataapi', 'net.william278.husksync.libraries.mapdataapi' 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.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'
relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter' relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter'
relocate 'org.json', 'net.william278.husksync.libraries.json' relocate 'org.json', 'net.william278.husksync.libraries.json'

View File

@@ -290,7 +290,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
@Override @Override
public boolean isDependencyLoaded(@NotNull String name) { public boolean isDependencyLoaded(@NotNull String name) {
final Plugin plugin = getServer().getPluginManager().getPlugin(name); final Plugin plugin = getServer().getPluginManager().getPlugin(name);
return plugin != null && plugin.isEnabled(); return plugin != null;
} }
// Register bStats metrics // Register bStats metrics
@@ -333,6 +333,12 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
return PLATFORM_TYPE_ID; return PLATFORM_TYPE_ID;
} }
@Override
@NotNull
public String getServerVersion() {
return String.format("%s/%s", getServer().getName(), getServer().getVersion());
}
@Override @Override
public Optional<LegacyConverter> getLegacyConverter() { public Optional<LegacyConverter> getLegacyConverter() {
return Optional.of(legacyConverter); return Optional.of(legacyConverter);

View File

@@ -62,7 +62,7 @@ public class BukkitHuskSyncAPI extends HuskSyncAPI {
public static BukkitHuskSyncAPI getInstance() { public static BukkitHuskSyncAPI getInstance() {
if (!JavaPlugin.getProvidingPlugin(BukkitHuskSyncAPI.class).getName().equals("HuskSync")) { if (!JavaPlugin.getProvidingPlugin(BukkitHuskSyncAPI.class).getName().equals("HuskSync")) {
throw new NotRegisteredException("This is likely because you have shaded HuskSync into your plugin JAR " + 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) { if (instance == null) {
throw new NotRegisteredException(); throw new NotRegisteredException();

View File

@@ -25,16 +25,15 @@ import com.google.gson.annotations.SerializedName;
import de.tr7zw.changeme.nbtapi.NBTCompound; import de.tr7zw.changeme.nbtapi.NBTCompound;
import de.tr7zw.changeme.nbtapi.NBTPersistentDataContainer; import de.tr7zw.changeme.nbtapi.NBTPersistentDataContainer;
import lombok.*; import lombok.*;
import net.kyori.adventure.util.TriState;
import net.william278.desertwell.util.ThrowingConsumer; import net.william278.desertwell.util.ThrowingConsumer;
import net.william278.desertwell.util.Version; import net.william278.desertwell.util.Version;
import net.william278.husksync.BukkitHuskSync; import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import net.william278.husksync.adapter.Adaptable; import net.william278.husksync.adapter.Adaptable;
import net.william278.husksync.config.Settings.SynchronizationSettings.AttributeSettings;
import net.william278.husksync.user.BukkitUser; import net.william278.husksync.user.BukkitUser;
import org.bukkit.Bukkit; import org.bukkit.*;
import org.bukkit.Material;
import org.bukkit.Registry;
import org.bukkit.Statistic;
import org.bukkit.advancement.AdvancementProgress; import org.bukkit.advancement.AdvancementProgress;
import org.bukkit.attribute.AttributeInstance; import org.bukkit.attribute.AttributeInstance;
import org.bukkit.attribute.AttributeModifier; import org.bukkit.attribute.AttributeModifier;
@@ -48,7 +47,9 @@ import org.bukkit.potion.PotionEffectType;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Range; import org.jetbrains.annotations.Range;
import org.jetbrains.annotations.Unmodifiable;
import java.lang.reflect.Constructor;
import java.util.*; import java.util.*;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -236,8 +237,9 @@ public abstract class BukkitData implements Data {
private final Collection<PotionEffect> effects; private final Collection<PotionEffect> effects;
@NotNull @NotNull
public static BukkitData.PotionEffects from(@NotNull Collection<PotionEffect> effects) { public static BukkitData.PotionEffects from(@NotNull Collection<PotionEffect> sei) {
return new BukkitData.PotionEffects(effects); return new BukkitData.PotionEffects(Lists.newArrayList(sei.stream().filter(e -> !e.isAmbient()).toList()));
} }
@NotNull @NotNull
@@ -261,7 +263,7 @@ public abstract class BukkitData implements Data {
@NotNull @NotNull
@SuppressWarnings("unused") @SuppressWarnings("unused")
public static BukkitData.PotionEffects empty() { public static BukkitData.PotionEffects empty() {
return new BukkitData.PotionEffects(List.of()); return new BukkitData.PotionEffects(Lists.newArrayList());
} }
@Override @Override
@@ -277,6 +279,7 @@ public abstract class BukkitData implements Data {
@NotNull @NotNull
@Override @Override
@Unmodifiable
public List<Effect> getActiveEffects() { public List<Effect> getActiveEffects() {
return effects.stream() return effects.stream()
.map(potionEffect -> new Effect( .map(potionEffect -> new Effect(
@@ -364,7 +367,7 @@ public abstract class BukkitData implements Data {
// Set player experience and level (prevent advancement awards applying twice), reset game rule // Set player experience and level (prevent advancement awards applying twice), reset game rule
if (!toAward.isEmpty() if (!toAward.isEmpty()
&& (player.getLevel() != expLevel || player.getExp() != expProgress)) { && (player.getLevel() != expLevel || player.getExp() != expProgress)) {
player.setLevel(expLevel); player.setLevel(expLevel);
player.setExp(expProgress); player.setExp(expProgress);
} }
@@ -565,19 +568,24 @@ public abstract class BukkitData implements Data {
@NoArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class Attributes extends BukkitData implements Data.Attributes, Adaptable { public static class Attributes extends BukkitData implements Data.Attributes, Adaptable {
private static final String EQUIPMENT_SLOT_GROUP = "org.bukkit.inventory.EquipmentSlotGroup";
private static final String EQUIPMENT_SLOT_GROUP$ANY = "ANY";
private static final String EQUIPMENT_SLOT$getGroup = "getGroup";
private static TriState USE_KEYED_MODIFIERS = TriState.NOT_SET;
private List<Attribute> attributes; private List<Attribute> attributes;
@NotNull @NotNull
public static BukkitData.Attributes adapt(@NotNull Player player, @NotNull HuskSync plugin) { public static BukkitData.Attributes adapt(@NotNull Player player, @NotNull HuskSync plugin) {
final List<Attribute> attributes = Lists.newArrayList(); final List<Attribute> attributes = Lists.newArrayList();
final AttributeSettings settings = plugin.getSettings().getSynchronization().getAttributes();
Registry.ATTRIBUTE.forEach(id -> { Registry.ATTRIBUTE.forEach(id -> {
final AttributeInstance instance = player.getAttribute(id); final AttributeInstance instance = player.getAttribute(id);
if (instance == null || instance.getValue() == instance.getDefaultValue() || plugin if (instance == null || Double.compare(instance.getValue(), instance.getDefaultValue()) == 0
.getSettings().getSynchronization().isIgnoredAttribute(id.getKey().toString())) { || settings.isIgnoredAttribute(id.getKey().toString())) {
// We don't sync unmodified or disabled attributes return; // We don't sync unmodified or disabled attributes
return;
} }
attributes.add(adapt(instance, plugin.getMinecraftVersion())); attributes.add(adapt(instance, settings));
}); });
return new BukkitData.Attributes(attributes); return new BukkitData.Attributes(attributes);
} }
@@ -596,18 +604,20 @@ public abstract class BukkitData implements Data {
} }
@NotNull @NotNull
private static Attribute adapt(@NotNull AttributeInstance instance, @NotNull Version version) { private static Attribute adapt(@NotNull AttributeInstance instance, @NotNull AttributeSettings settings) {
return new Attribute( return new Attribute(
instance.getAttribute().getKey().toString(), instance.getAttribute().getKey().toString(),
instance.getBaseValue(), instance.getBaseValue(),
instance.getModifiers().stream().map(m -> adapt(m, version)).collect(Collectors.toSet()) instance.getModifiers().stream()
.filter(modifier -> !settings.isIgnoredModifier(modifier.getName()))
.map(BukkitData.Attributes::adapt).collect(Collectors.toSet())
); );
} }
@NotNull @NotNull
private static Modifier adapt(@NotNull AttributeModifier modifier, @NotNull Version version) { private static Modifier adapt(@NotNull AttributeModifier modifier) {
return new Modifier( return new Modifier(
version.compareTo(Version.fromString("1.21")) >= 0 ? null : modifier.getUniqueId(), getModifierId(modifier),
modifier.getName(), modifier.getName(),
modifier.getAmount(), modifier.getAmount(),
modifier.getOperation().ordinal(), modifier.getOperation().ordinal(),
@@ -615,28 +625,83 @@ public abstract class BukkitData implements Data {
); );
} }
@Override @Nullable
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException { private static UUID getModifierId(@NotNull AttributeModifier modifier) {
Registry.ATTRIBUTE.forEach(id -> applyAttribute(user.getPlayer().getAttribute(id), getAttribute(id).orElse(null))); try {
return modifier.getUniqueId();
} catch (Throwable e) {
return null;
}
} }
private static void applyAttribute(@Nullable AttributeInstance instance, @Nullable Attribute attribute) { private static boolean useKeyedModifiers(@NotNull HuskSync plugin) {
if (USE_KEYED_MODIFIERS == TriState.NOT_SET) {
boolean is1_21 = plugin.getMinecraftVersion().compareTo(Version.fromString("1.21")) >= 0;
USE_KEYED_MODIFIERS = TriState.byBoolean(is1_21);
return is1_21;
}
return Boolean.TRUE.equals(USE_KEYED_MODIFIERS.toBoolean());
}
private static void applyAttribute(@Nullable AttributeInstance instance, @Nullable Attribute attribute,
@NotNull HuskSync plugin) {
if (instance == null) { if (instance == null) {
return; return;
} }
instance.setBaseValue(attribute == null ? instance.getDefaultValue() : attribute.baseValue()); instance.setBaseValue(attribute == null ? instance.getDefaultValue() : attribute.baseValue());
instance.getModifiers().forEach(instance::removeModifier); instance.getModifiers().forEach(instance::removeModifier);
if (attribute != null) { if (attribute != null) {
attribute.modifiers().forEach(modifier -> instance.addModifier(new AttributeModifier( attribute.modifiers().stream()
modifier.uuid(), .filter(mod -> instance.getModifiers().stream().map(AttributeModifier::getName)
modifier.name(), .noneMatch(n -> n.equals(mod.name())))
modifier.amount(), .distinct()
AttributeModifier.Operation.values()[modifier.operationType()], .filter(mod -> useKeyedModifiers(plugin) == !mod.hasUuid())
modifier.equipmentSlot() != -1 ? EquipmentSlot.values()[modifier.equipmentSlot()] : null .forEach(mod -> instance.addModifier(adapt(mod, plugin)));
)));
} }
} }
@SuppressWarnings("JavaReflectionMemberAccess")
@NotNull
private static AttributeModifier adapt(@NotNull Modifier modifier, @NotNull HuskSync plugin) {
final int slotId = modifier.equipmentSlot();
if (useKeyedModifiers(plugin)) {
try {
// Reflexively create a modern keyed attribute modifier instance. Remove in favor of API long-term.
final EquipmentSlot slot = slotId != -1 ? EquipmentSlot.values()[slotId] : null;
final Class<?> slotGroup = Class.forName(EQUIPMENT_SLOT_GROUP);
final String modifierName = modifier.name() == null ? modifier.uuid().toString() : modifier.name();
final NamespacedKey modifierKey = Objects.requireNonNull(NamespacedKey.fromString(modifierName),
"Modifier key returned null");
final Constructor<AttributeModifier> constructor = AttributeModifier.class.getDeclaredConstructor(
NamespacedKey.class, double.class, AttributeModifier.Operation.class, slotGroup);
return constructor.newInstance(
modifierKey,
modifier.amount(),
AttributeModifier.Operation.values()[modifier.operationType()],
slot == null ? slotGroup.getField(EQUIPMENT_SLOT_GROUP$ANY).get(null)
: EquipmentSlot.class.getDeclaredMethod(EQUIPMENT_SLOT$getGroup).invoke(slot)
);
} catch (Throwable e) {
plugin.log(Level.WARNING, "Error reflectively creating keyed attribute modifier", e);
USE_KEYED_MODIFIERS = TriState.FALSE;
}
}
return new AttributeModifier(
modifier.uuid(),
modifier.name(),
modifier.amount(),
AttributeModifier.Operation.values()[modifier.operationType()],
slotId != -1 ? EquipmentSlot.values()[slotId] : null
);
}
@Override
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
Registry.ATTRIBUTE.forEach(id -> applyAttribute(
user.getPlayer().getAttribute(id), getAttribute(id).orElse(null), plugin
));
}
} }
@Getter @Getter
@@ -696,11 +761,12 @@ public abstract class BukkitData implements Data {
} }
// Set health scale // Set health scale
double scale = healthScale <= 0 ? player.getMaxHealth() : healthScale;
try { try {
player.setHealthScale(healthScale); player.setHealthScale(scale);
player.setHealthScaled(isHealthScaled); player.setHealthScaled(isHealthScaled);
} catch (Throwable e) { } catch (Throwable e) {
plugin.log(Level.WARNING, "Error setting %s's health scale to %s".formatted(player.getName(), healthScale), e); plugin.log(Level.WARNING, "Error setting %s's health scale to %s".formatted(player.getName(), scale), e);
} }
} }

View File

@@ -165,6 +165,7 @@ public class BukkitSerializer {
case "1.20", "1.20.1", "1.20.2" -> DataFixerUtil.VERSION1_20_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.3", "1.20.4" -> DataFixerUtil.VERSION1_20_4;
case "1.20.5", "1.20.6" -> DataFixerUtil.VERSION1_20_5; case "1.20.5", "1.20.6" -> DataFixerUtil.VERSION1_20_5;
case "1.21" -> DataFixerUtil.VERSION1_21;
default -> DataFixerUtil.getCurrentVersion(); default -> DataFixerUtil.getCurrentVersion();
}; };
} }

View File

@@ -23,6 +23,7 @@ import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.event.PacketListenerAbstract; import com.github.retrooper.packetevents.event.PacketListenerAbstract;
import com.github.retrooper.packetevents.event.PacketListenerPriority; import com.github.retrooper.packetevents.event.PacketListenerPriority;
import com.github.retrooper.packetevents.event.PacketReceiveEvent; import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType; import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder; import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder;
@@ -78,7 +79,20 @@ public class BukkitPacketEventsLockedPacketListener extends BukkitLockedEventLis
@Override @Override
public void onPacketReceive(PacketReceiveEvent event) { public void onPacketReceive(PacketReceiveEvent event) {
if(!(event.getPacketType() instanceof PacketType.Play.Client client)) { if (!(event.getPacketType() instanceof PacketType.Play.Client client)) {
return;
}
if (!CANCEL_PACKETS.contains(client)) {
return;
}
if (listener.cancelPlayerEvent(event.getUser().getUUID())) {
event.setCancelled(true);
}
}
@Override
public void onPacketSend(PacketSendEvent event) {
if (!(event.getPacketType() instanceof PacketType.Play.Client client)) {
return; return;
} }
if (!CANCEL_PACKETS.contains(client)) { if (!CANCEL_PACKETS.contains(client)) {

View File

@@ -204,10 +204,10 @@ public class LegacyMigrator extends Migrator {
}) { }) {
plugin.log(Level.INFO, getHelpMenu()); plugin.log(Level.INFO, getHelpMenu());
plugin.log(Level.INFO, "Successfully set " + args[0] + " to " + plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
obfuscateDataString(args[1])); obfuscateDataString(args[1]));
} else { } else {
plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " + 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 { } else {
plugin.log(Level.INFO, getHelpMenu()); plugin.log(Level.INFO, getHelpMenu());

View File

@@ -201,10 +201,10 @@ public class MpdbMigrator extends Migrator {
}) { }) {
plugin.log(Level.INFO, getHelpMenu()); plugin.log(Level.INFO, getHelpMenu());
plugin.log(Level.INFO, "Successfully set " + args[0] + " to " + plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
obfuscateDataString(args[1])); obfuscateDataString(args[1]));
} else { } else {
plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " + 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 { } else {
plugin.log(Level.INFO, getHelpMenu()); plugin.log(Level.INFO, getHelpMenu());
@@ -255,7 +255,7 @@ public class MpdbMigrator extends Migrator {
If any of these are not correct, please correct them If any of these are not correct, please correct them
using the command: using the command:
"husksync migrate mpdb set <parameter> <value>" "husksync migrate mpdb set <parameter> <value>"
(e.g.: "husksync migrate mpdb set host 1.2.3.4") (e.g.: "husksync migrate set mpdb host 1.2.3.4")
STEP 3] HuskSync will migrate data into the database STEP 3] HuskSync will migrate data into the database
tables configures in the config.yml file of this tables configures in the config.yml file of this
@@ -263,7 +263,7 @@ public class MpdbMigrator extends Migrator {
before proceeding. before proceeding.
STEP 4] To start the migration, please run: STEP 4] To start the migration, please run:
"husksync migrate mpdb start" "husksync migrate start mpdb"
NOTE: This migrator currently WORKS WITH MPDB version NOTE: This migrator currently WORKS WITH MPDB version
v4.9.2 and below! v4.9.2 and below!

View File

@@ -23,14 +23,10 @@ import de.themoep.minedown.adventure.MineDown;
import dev.triumphteam.gui.builder.gui.StorageBuilder; import dev.triumphteam.gui.builder.gui.StorageBuilder;
import dev.triumphteam.gui.guis.Gui; import dev.triumphteam.gui.guis.Gui;
import dev.triumphteam.gui.guis.StorageGui; 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.HuskSync;
import net.william278.husksync.data.BukkitData; import net.william278.husksync.data.BukkitData;
import net.william278.husksync.data.BukkitUserDataHolder; import net.william278.husksync.data.BukkitUserDataHolder;
import net.william278.husksync.data.Data; import net.william278.husksync.data.Data;
import org.bukkit.Material;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
@@ -40,8 +36,6 @@ import java.util.Arrays;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.logging.Level; import java.util.logging.Level;
import static net.william278.husksync.util.BukkitKeyedAdapter.matchMaterial;
/** /**
* Bukkit platform implementation of an {@link OnlineUser} * Bukkit platform implementation of an {@link OnlineUser}
*/ */
@@ -68,20 +62,12 @@ public class BukkitUser extends OnlineUser implements BukkitUserDataHolder {
} }
@Override @Override
@Deprecated(since = "3.6.7")
public void sendToast(@NotNull MineDown title, @NotNull MineDown description, public void sendToast(@NotNull MineDown title, @NotNull MineDown description,
@NotNull String iconMaterial, @NotNull String backgroundType) { @NotNull String iconMaterial, @NotNull String backgroundType) {
try { plugin.log(Level.WARNING, "Toast notifications are deprecated. " +
final Material material = matchMaterial(iconMaterial); "Please change your notification display slot to CHAT, ACTION_BAR or NONE.");
Toast.builder((BukkitHuskSync) plugin) this.sendActionBar(title);
.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);
}
} }
@Override @Override

View File

@@ -155,8 +155,8 @@ public interface BukkitMapPersister {
Optional<String> world = Optional.empty(); Optional<String> world = Optional.empty();
for (String worldUid : mapIds.getKeys()) { for (String worldUid : mapIds.getKeys()) {
world = getPlugin().getServer().getWorlds().stream() world = getPlugin().getServer().getWorlds().stream()
.map(w -> w.getUID().toString()).filter(u -> u.equals(worldUid)) .map(w -> w.getUID().toString()).filter(u -> u.equals(worldUid))
.findFirst(); .findFirst();
if (world.isPresent()) { if (world.isPresent()) {
break; break;
} }
@@ -179,7 +179,7 @@ public interface BukkitMapPersister {
try { try {
getPlugin().debug("Deserializing map data from NBT and generating view..."); getPlugin().debug("Deserializing map data from NBT and generating view...");
canvasData = MapData.fromByteArray(Objects.requireNonNull(mapData.getByteArray(MAP_PIXEL_DATA_KEY), canvasData = MapData.fromByteArray(Objects.requireNonNull(mapData.getByteArray(MAP_PIXEL_DATA_KEY),
"Map pixel data is null")); "Map pixel data is null"));
} catch (Throwable e) { } catch (Throwable e) {
getPlugin().log(Level.WARNING, "Failed to deserialize map data from NBT", e); getPlugin().log(Level.WARNING, "Failed to deserialize map data from NBT", e);
return; return;
@@ -195,8 +195,8 @@ public interface BukkitMapPersister {
// Set the map view ID in NBT // Set the map view ID in NBT
NBT.modify(map, editable -> { NBT.modify(map, editable -> {
Objects.requireNonNull(editable.getCompound(MAP_VIEW_ID_MAPPINGS_KEY), Objects.requireNonNull(editable.getCompound(MAP_VIEW_ID_MAPPINGS_KEY),
"Map view ID mappings compound is null") "Map view ID mappings compound is null")
.setInteger(worldUid, view.getId()); .setInteger(worldUid, view.getId());
}); });
getPlugin().debug(String.format("Generated view (#%s) and updated map (UID: %s)", view.getId(), worldUid)); getPlugin().debug(String.format("Generated view (#%s) and updated map (UID: %s)", view.getId(), worldUid));
}); });
@@ -326,29 +326,29 @@ public interface BukkitMapPersister {
@NotNull @NotNull
private static MapCursor createBannerCursor(@NotNull MapBanner banner) { private static MapCursor createBannerCursor(@NotNull MapBanner banner) {
return new MapCursor( return new MapCursor(
(byte) banner.getPosition().getX(), (byte) banner.getPosition().getX(),
(byte) banner.getPosition().getZ(), (byte) banner.getPosition().getZ(),
(byte) 8, // Always rotate banners upright (byte) 8, // Always rotate banners upright
switch (banner.getColor().toLowerCase(Locale.ENGLISH)) { switch (banner.getColor().toLowerCase(Locale.ENGLISH)) {
case "white" -> MapCursor.Type.BANNER_WHITE; case "white" -> MapCursor.Type.BANNER_WHITE;
case "orange" -> MapCursor.Type.BANNER_ORANGE; case "orange" -> MapCursor.Type.BANNER_ORANGE;
case "magenta" -> MapCursor.Type.BANNER_MAGENTA; case "magenta" -> MapCursor.Type.BANNER_MAGENTA;
case "light_blue" -> MapCursor.Type.BANNER_LIGHT_BLUE; case "light_blue" -> MapCursor.Type.BANNER_LIGHT_BLUE;
case "yellow" -> MapCursor.Type.BANNER_YELLOW; case "yellow" -> MapCursor.Type.BANNER_YELLOW;
case "lime" -> MapCursor.Type.BANNER_LIME; case "lime" -> MapCursor.Type.BANNER_LIME;
case "pink" -> MapCursor.Type.BANNER_PINK; case "pink" -> MapCursor.Type.BANNER_PINK;
case "gray" -> MapCursor.Type.BANNER_GRAY; case "gray" -> MapCursor.Type.BANNER_GRAY;
case "light_gray" -> MapCursor.Type.BANNER_LIGHT_GRAY; case "light_gray" -> MapCursor.Type.BANNER_LIGHT_GRAY;
case "cyan" -> MapCursor.Type.BANNER_CYAN; case "cyan" -> MapCursor.Type.BANNER_CYAN;
case "purple" -> MapCursor.Type.BANNER_PURPLE; case "purple" -> MapCursor.Type.BANNER_PURPLE;
case "blue" -> MapCursor.Type.BANNER_BLUE; case "blue" -> MapCursor.Type.BANNER_BLUE;
case "brown" -> MapCursor.Type.BANNER_BROWN; case "brown" -> MapCursor.Type.BANNER_BROWN;
case "green" -> MapCursor.Type.BANNER_GREEN; case "green" -> MapCursor.Type.BANNER_GREEN;
case "red" -> MapCursor.Type.BANNER_RED; case "red" -> MapCursor.Type.BANNER_RED;
default -> MapCursor.Type.BANNER_BLACK; default -> MapCursor.Type.BANNER_BLACK;
}, },
true, true,
banner.getText().isEmpty() ? null : banner.getText() banner.getText().isEmpty() ? null : banner.getText()
); );
} }
@@ -424,19 +424,23 @@ public interface BukkitMapPersister {
@NotNull @NotNull
private MapData extractMapData() { private MapData extractMapData() {
final List<MapBanner> banners = Lists.newArrayList(); final List<MapBanner> banners = Lists.newArrayList();
final String BANNER_PREFIX = "banner_"; try {
for (int i = 0; i < getCursors().size(); i++) { final String BANNER_PREFIX = "banner_";
final MapCursor cursor = getCursors().getCursor(i); for (int i = 0; i < getCursors().size(); i++) {
final String type = cursor.getType().name().toLowerCase(Locale.ENGLISH); final MapCursor cursor = getCursors().getCursor(i);
if (type.startsWith(BANNER_PREFIX)) { final String type = cursor.getType().name().toLowerCase(Locale.ENGLISH);
banners.add(new MapBanner( if (type.startsWith(BANNER_PREFIX)) {
type.replaceAll(BANNER_PREFIX, ""), banners.add(new MapBanner(
cursor.getCaption() == null ? "" : cursor.getCaption(), type.replaceAll(BANNER_PREFIX, ""),
cursor.getX(), cursor.getCaption() == null ? "" : cursor.getCaption(),
mapView.getWorld() != null ? mapView.getWorld().getSeaLevel() : 128, cursor.getX(),
cursor.getY() 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, List.of());
} }

View File

@@ -16,9 +16,9 @@ dependencies {
exclude module: 'slf4j-api' exclude module: 'slf4j-api'
} }
compileOnly 'net.william278.uniform:uniform-common:1.1.8' compileOnly 'net.william278.uniform:uniform-common:1.2.1'
compileOnly 'com.mojang:brigadier:1.1.8' compileOnly 'com.mojang:brigadier:1.1.8'
compileOnly 'org.projectlombok:lombok:1.18.32' compileOnly 'org.projectlombok:lombok:1.18.34'
compileOnly 'org.jetbrains:annotations:24.1.0' compileOnly 'org.jetbrains:annotations:24.1.0'
compileOnly 'net.kyori:adventure-api:4.17.0' compileOnly 'net.kyori:adventure-api:4.17.0'
compileOnly 'net.kyori:adventure-platform-api:4.3.3' compileOnly 'net.kyori:adventure-platform-api:4.3.3'
@@ -38,5 +38,5 @@ dependencies {
testCompileOnly 'de.exlll:configlib-yaml:4.5.0' testCompileOnly 'de.exlll:configlib-yaml:4.5.0'
testCompileOnly 'org.jetbrains:annotations:24.1.0' testCompileOnly 'org.jetbrains:annotations:24.1.0'
annotationProcessor 'org.projectlombok:lombok:1.18.32' annotationProcessor 'org.projectlombok:lombok:1.18.34'
} }

View File

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

View File

@@ -46,29 +46,29 @@ public class EnderChestCommand extends ItemsCommand {
final Optional<Data.Items.EnderChest> optionalEnderChest = snapshot.getEnderChest(); final Optional<Data.Items.EnderChest> optionalEnderChest = snapshot.getEnderChest();
if (optionalEnderChest.isEmpty()) { if (optionalEnderChest.isEmpty()) {
plugin.getLocales().getLocale("error_no_data_to_display") plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage); .ifPresent(viewer::sendMessage);
return; return;
} }
// Display opening message // Display opening message
plugin.getLocales().getLocale("ender_chest_viewer_opened", user.getUsername(), plugin.getLocales().getLocale("ender_chest_viewer_opened", user.getUsername(),
snapshot.getTimestamp().format(DateTimeFormatter snapshot.getTimestamp().format(DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT))) .ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
.ifPresent(viewer::sendMessage); .ifPresent(viewer::sendMessage);
// Show GUI // Show GUI
final Data.Items.EnderChest enderChest = optionalEnderChest.get(); final Data.Items.EnderChest enderChest = optionalEnderChest.get();
viewer.showGui( viewer.showGui(
enderChest, enderChest,
plugin.getLocales().getLocale("ender_chest_viewer_menu_title", user.getUsername()) plugin.getLocales().getLocale("ender_chest_viewer_menu_title", user.getUsername())
.orElse(new MineDown(String.format("%s's Ender Chest", user.getUsername()))), .orElse(new MineDown(String.format("%s's Ender Chest", user.getUsername()))),
allowEdit, allowEdit,
enderChest.getSlotCount(), enderChest.getSlotCount(),
(itemsOnClose) -> { (itemsOnClose) -> {
if (allowEdit && !enderChest.equals(itemsOnClose)) { if (allowEdit && !enderChest.equals(itemsOnClose)) {
plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user)); plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user));
}
} }
}
); );
} }
@@ -78,7 +78,7 @@ public class EnderChestCommand extends ItemsCommand {
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder); final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
if (latestData.isEmpty()) { if (latestData.isEmpty()) {
plugin.getLocales().getLocale("error_no_data_to_display") plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage); .ifPresent(viewer::sendMessage);
return; return;
} }
@@ -88,7 +88,7 @@ public class EnderChestCommand extends ItemsCommand {
data.getEnderChest().ifPresent(enderChest -> enderChest.setContents(items)); data.getEnderChest().ifPresent(enderChest -> enderChest.setContents(items));
data.setSaveCause(DataSnapshot.SaveCause.ENDERCHEST_COMMAND); data.setSaveCause(DataSnapshot.SaveCause.ENDERCHEST_COMMAND);
data.setPinned( data.setPinned(
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.ENDERCHEST_COMMAND) plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.ENDERCHEST_COMMAND)
); );
}); });

View File

@@ -52,41 +52,41 @@ public class HuskSyncCommand extends PluginCommand {
private final AboutMenu aboutMenu; private final AboutMenu aboutMenu;
public HuskSyncCommand(@NotNull HuskSync plugin) { public HuskSyncCommand(@NotNull HuskSync plugin) {
super("husksync", List.of(), Permission.Default.TRUE, plugin); super("husksync", List.of(), Permission.Default.TRUE, ExecutionScope.ALL, plugin);
this.updateChecker = plugin.getUpdateChecker(); this.updateChecker = plugin.getUpdateChecker();
this.aboutMenu = AboutMenu.builder() this.aboutMenu = AboutMenu.builder()
.title(Component.text("HuskSync")) .title(Component.text("HuskSync"))
.description(Component.text("A modern, cross-server player data synchronization system")) .description(Component.text("A modern, cross-server player data synchronization system"))
.version(plugin.getPluginVersion()) .version(plugin.getPluginVersion())
.credits("Author", .credits("Author",
AboutMenu.Credit.of("William278").description("Click to visit website").url("https://william278.net")) AboutMenu.Credit.of("William278").description("Click to visit website").url("https://william278.net"))
.credits("Contributors", .credits("Contributors",
AboutMenu.Credit.of("HarvelsX").description("Code"), AboutMenu.Credit.of("HarvelsX").description("Code"),
AboutMenu.Credit.of("HookWoods").description("Code"), AboutMenu.Credit.of("HookWoods").description("Code"),
AboutMenu.Credit.of("Preva1l").description("Code"), AboutMenu.Credit.of("Preva1l").description("Code"),
AboutMenu.Credit.of("hanbings").description("Code (Fabric porting)"), AboutMenu.Credit.of("hanbings").description("Code (Fabric porting)"),
AboutMenu.Credit.of("Stampede2011").description("Code (Fabric mixins)")) AboutMenu.Credit.of("Stampede2011").description("Code (Fabric mixins)"))
.credits("Translators", .credits("Translators",
AboutMenu.Credit.of("Namiu").description("Japanese (ja-jp)"), AboutMenu.Credit.of("Namiu").description("Japanese (ja-jp)"),
AboutMenu.Credit.of("anchelthe").description("Spanish (es-es)"), AboutMenu.Credit.of("anchelthe").description("Spanish (es-es)"),
AboutMenu.Credit.of("Melonzio").description("Spanish (es-es)"), AboutMenu.Credit.of("Melonzio").description("Spanish (es-es)"),
AboutMenu.Credit.of("Ceddix").description("German (de-de)"), AboutMenu.Credit.of("Ceddix").description("German (de-de)"),
AboutMenu.Credit.of("Pukejoy_1").description("Bulgarian (bg-bg)"), AboutMenu.Credit.of("Pukejoy_1").description("Bulgarian (bg-bg)"),
AboutMenu.Credit.of("mateusneresrb").description("Brazilian Portuguese (pt-br)"), AboutMenu.Credit.of("mateusneresrb").description("Brazilian Portuguese (pt-br)"),
AboutMenu.Credit.of("小蔡").description("Traditional Chinese (zh-tw)"), AboutMenu.Credit.of("小蔡").description("Traditional Chinese (zh-tw)"),
AboutMenu.Credit.of("Ghost-chu").description("Simplified Chinese (zh-cn)"), AboutMenu.Credit.of("Ghost-chu").description("Simplified Chinese (zh-cn)"),
AboutMenu.Credit.of("DJelly4K").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("Thourgard").description("Ukrainian (uk-ua)"),
AboutMenu.Credit.of("xF3d3").description("Italian (it-it)"), AboutMenu.Credit.of("xF3d3").description("Italian (it-it)"),
AboutMenu.Credit.of("cada3141").description("Korean (ko-kr)"), AboutMenu.Credit.of("cada3141").description("Korean (ko-kr)"),
AboutMenu.Credit.of("Wirayuda5620").description("Indonesian (id-id)"), AboutMenu.Credit.of("Wirayuda5620").description("Indonesian (id-id)"),
AboutMenu.Credit.of("WinTone01").description("Turkish (tr-tr)"), AboutMenu.Credit.of("WinTone01").description("Turkish (tr-tr)"),
AboutMenu.Credit.of("IbanEtchep").description("French (fr-fr)")) AboutMenu.Credit.of("IbanEtchep").description("French (fr-fr)"))
.buttons( .buttons(
AboutMenu.Link.of("https://william278.net/docs/husksync").text("Documentation").icon(""), 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://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))) AboutMenu.Link.of("https://discord.gg/tVYhJfyDWG").text("Discord").icon("").color(TextColor.color(0x6773f5)))
.build(); .build();
} }
@Override @Override
@@ -109,8 +109,8 @@ public class HuskSyncCommand extends PluginCommand {
final CommandUser user = user(sub, ctx); final CommandUser user = user(sub, ctx);
plugin.getLocales().getLocale("system_status_header").ifPresent(user::sendMessage); plugin.getLocales().getLocale("system_status_header").ifPresent(user::sendMessage);
user.sendMessage(Component.join( user.sendMessage(Component.join(
JoinConfiguration.newlines(), JoinConfiguration.newlines(),
Arrays.stream(StatusLine.values()).map(s -> s.get(plugin)).toList() Arrays.stream(StatusLine.values()).map(s -> s.get(plugin)).toList()
)); ));
}); });
} }
@@ -126,7 +126,7 @@ public class HuskSyncCommand extends PluginCommand {
plugin.getLocales().getLocale("reload_complete").ifPresent(user::sendMessage); plugin.getLocales().getLocale("reload_complete").ifPresent(user::sendMessage);
} catch (Throwable e) { } catch (Throwable e) {
user.sendMessage(new MineDown( 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); plugin.log(Level.SEVERE, "Failed to reload the plugin", e);
} }
@@ -139,11 +139,11 @@ public class HuskSyncCommand extends PluginCommand {
final CommandUser user = user(sub, ctx); final CommandUser user = user(sub, ctx);
if (checked.isUpToDate()) { if (checked.isUpToDate()) {
plugin.getLocales().getLocale("up_to_date", plugin.getPluginVersion().toString()) plugin.getLocales().getLocale("up_to_date", plugin.getPluginVersion().toString())
.ifPresent(user::sendMessage); .ifPresent(user::sendMessage);
return; return;
} }
plugin.getLocales().getLocale("update_available", checked.getLatestVersion().toString(), plugin.getLocales().getLocale("update_available", checked.getLatestVersion().toString(),
plugin.getPluginVersion().toString()).ifPresent(user::sendMessage); plugin.getPluginVersion().toString()).ifPresent(user::sendMessage);
})); }));
} }
@@ -152,14 +152,18 @@ public class HuskSyncCommand extends PluginCommand {
return (sub) -> { return (sub) -> {
sub.setCondition((ctx) -> sub.getUser(ctx).isConsole()); sub.setCondition((ctx) -> sub.getUser(ctx).isConsole());
sub.setDefaultExecutor((ctx) -> { sub.setDefaultExecutor((ctx) -> {
plugin.log(Level.INFO, "Please choose a migrator, then run \"husksync migrate <migrator>\""); plugin.log(Level.INFO, "Please choose a migrator, then run \"husksync migrate start <migrator>\"");
plugin.log(Level.INFO, String.format( plugin.log(Level.INFO, String.format(
"List of available migrators:\nMigrator ID / Migrator Name:\n%s", "List of available migrators:\nMigrator ID / Migrator Name:\n%s",
plugin.getAvailableMigrators().stream() plugin.getAvailableMigrators().stream()
.map(migrator -> String.format("%s - %s", migrator.getIdentifier(), migrator.getName())) .map(migrator -> String.format("%s - %s", migrator.getIdentifier(), migrator.getName()))
.collect(Collectors.joining("\n")) .collect(Collectors.joining("\n"))
)); ));
}); });
sub.addSubCommand("help", (help) -> help.addSyntax((cmd) -> {
final Migrator migrator = cmd.getArgument("migrator", Migrator.class);
plugin.log(Level.INFO, migrator.getHelpMenu());
}, migrator()));
sub.addSubCommand("start", (start) -> start.addSyntax((cmd) -> { sub.addSubCommand("start", (start) -> start.addSyntax((cmd) -> {
final Migrator migrator = cmd.getArgument("migrator", Migrator.class); final Migrator migrator = cmd.getArgument("migrator", Migrator.class);
migrator.start().thenAccept(succeeded -> { migrator.start().thenAccept(succeeded -> {
@@ -173,7 +177,7 @@ public class HuskSyncCommand extends PluginCommand {
sub.addSubCommand("set", (set) -> set.addSyntax((cmd) -> { sub.addSubCommand("set", (set) -> set.addSyntax((cmd) -> {
final Migrator migrator = cmd.getArgument("migrator", Migrator.class); final Migrator migrator = cmd.getArgument("migrator", Migrator.class);
final String[] args = cmd.getArgument("args", String.class).split(" "); final String[] args = cmd.getArgument("args", String.class).split(" ");
migrator.handleConfigurationCommand(Arrays.copyOfRange(args, 3, args.length)); migrator.handleConfigurationCommand(args);
}, migrator(), BaseCommand.greedyString("args"))); }, migrator(), BaseCommand.greedyString("args")));
}; };
} }
@@ -183,7 +187,7 @@ public class HuskSyncCommand extends PluginCommand {
return new ArgumentElement<>("migrator", reader -> { return new ArgumentElement<>("migrator", reader -> {
final String id = reader.readString(); final String id = reader.readString();
final Migrator migrator = plugin.getAvailableMigrators().stream() 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) { if (migrator == null) {
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader); throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader);
} }
@@ -198,54 +202,54 @@ public class HuskSyncCommand extends PluginCommand {
private enum StatusLine { private enum StatusLine {
PLUGIN_VERSION(plugin -> Component.text("v" + plugin.getPluginVersion().toStringWithoutMetadata()) PLUGIN_VERSION(plugin -> Component.text("v" + plugin.getPluginVersion().toStringWithoutMetadata())
.appendSpace().append(plugin.getPluginVersion().getMetadata().isBlank() ? Component.empty() .appendSpace().append(plugin.getPluginVersion().getMetadata().isBlank() ? Component.empty()
: Component.text("(build " + plugin.getPluginVersion().getMetadata() + ")"))), : Component.text("(build " + plugin.getPluginVersion().getMetadata() + ")"))),
PLATFORM_TYPE(plugin -> Component.text(WordUtils.capitalizeFully(plugin.getPlatformType()))), SERVER_VERSION(plugin -> Component.text(plugin.getServerVersion())),
LANGUAGE(plugin -> Component.text(plugin.getSettings().getLanguage())), LANGUAGE(plugin -> Component.text(plugin.getSettings().getLanguage())),
MINECRAFT_VERSION(plugin -> Component.text(plugin.getMinecraftVersion().toString())), MINECRAFT_VERSION(plugin -> Component.text(plugin.getMinecraftVersion().toString())),
JAVA_VERSION(plugin -> Component.text(System.getProperty("java.version"))), JAVA_VERSION(plugin -> Component.text(System.getProperty("java.version"))),
JAVA_VENDOR(plugin -> Component.text(System.getProperty("java.vendor"))), JAVA_VENDOR(plugin -> Component.text(System.getProperty("java.vendor"))),
SYNC_MODE(plugin -> Component.text(WordUtils.capitalizeFully(
plugin.getSettings().getSynchronization().getMode().toString()
))),
DELAY_LATENCY(plugin -> Component.text(
plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds() + "ms"
)),
SERVER_NAME(plugin -> Component.text(plugin.getServerName())), SERVER_NAME(plugin -> Component.text(plugin.getServerName())),
CLUSTER_ID(plugin -> Component.text(plugin.getSettings().getClusterId().isBlank() ? "None" : plugin.getSettings().getClusterId())), CLUSTER_ID(plugin -> Component.text(plugin.getSettings().getClusterId().isBlank() ? "None" : plugin.getSettings().getClusterId())),
SYNC_MODE(plugin -> Component.text(WordUtils.capitalizeFully(
plugin.getSettings().getSynchronization().getMode().toString()
))),
DELAY_LATENCY(plugin -> Component.text(
plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds() + "ms"
)),
DATABASE_TYPE(plugin -> DATABASE_TYPE(plugin ->
Component.text(plugin.getSettings().getDatabase().getType().getDisplayName() + Component.text(plugin.getSettings().getDatabase().getType().getDisplayName() +
(plugin.getSettings().getDatabase().getType() == Database.Type.MONGO ? (plugin.getSettings().getDatabase().getType() == Database.Type.MONGO ?
(plugin.getSettings().getDatabase().getMongoSettings().isUsingAtlas() ? " Atlas" : "") : "")) (plugin.getSettings().getDatabase().getMongoSettings().isUsingAtlas() ? " Atlas" : "") : ""))
), ),
IS_DATABASE_LOCAL(plugin -> getLocalhostBoolean(plugin.getSettings().getDatabase().getCredentials().getHost())), IS_DATABASE_LOCAL(plugin -> getLocalhostBoolean(plugin.getSettings().getDatabase().getCredentials().getHost())),
USING_REDIS_SENTINEL(plugin -> getBoolean( USING_REDIS_SENTINEL(plugin -> getBoolean(
!plugin.getSettings().getRedis().getSentinel().getMaster().isBlank() !plugin.getSettings().getRedis().getSentinel().getMaster().isBlank()
)), )),
USING_REDIS_PASSWORD(plugin -> getBoolean( USING_REDIS_PASSWORD(plugin -> getBoolean(
!plugin.getSettings().getRedis().getCredentials().getPassword().isBlank() !plugin.getSettings().getRedis().getCredentials().getPassword().isBlank()
)), )),
REDIS_USING_SSL(plugin -> getBoolean( REDIS_USING_SSL(plugin -> getBoolean(
plugin.getSettings().getRedis().getCredentials().isUseSsl() plugin.getSettings().getRedis().getCredentials().isUseSsl()
)), )),
IS_REDIS_LOCAL(plugin -> getLocalhostBoolean( IS_REDIS_LOCAL(plugin -> getLocalhostBoolean(
plugin.getSettings().getRedis().getCredentials().getHost() plugin.getSettings().getRedis().getCredentials().getHost()
)), )),
DATA_TYPES(plugin -> Component.join( DATA_TYPES(plugin -> Component.join(
JoinConfiguration.commas(true), JoinConfiguration.commas(true),
plugin.getRegisteredDataTypes().stream().map(i -> Component.textOfChildren(Component.text(i.toString()) plugin.getRegisteredDataTypes().stream().map(i -> Component.textOfChildren(Component.text(i.toString())
.appendSpace().append(Component.text(i.isEnabled() ? '✔' : '❌'))) .appendSpace().append(Component.text(i.isEnabled() ? '✔' : '❌')))
.color(i.isEnabled() ? NamedTextColor.GREEN : NamedTextColor.RED) .color(i.isEnabled() ? NamedTextColor.GREEN : NamedTextColor.RED)
.hoverEvent(HoverEvent.showText( .hoverEvent(HoverEvent.showText(
Component.text(i.isEnabled() ? "Enabled" : "Disabled") Component.text(i.isEnabled() ? "Enabled" : "Disabled")
.append(Component.newline()) .append(Component.newline())
.append(Component.text("Dependencies: %s".formatted(i.getDependencies() .append(Component.text("Dependencies: %s".formatted(i.getDependencies()
.isEmpty() ? "(None)" : i.getDependencies().stream() .isEmpty() ? "(None)" : i.getDependencies().stream()
.map(d -> "%s (%s)".formatted( .map(d -> "%s (%s)".formatted(
d.getKey().value(), d.isRequired() ? "Required" : "Optional" d.getKey().value(), d.isRequired() ? "Required" : "Optional"
)).collect(Collectors.joining(", "))) )).collect(Collectors.joining(", ")))
).color(NamedTextColor.GRAY)) ).color(NamedTextColor.GRAY))
))).toList() ))).toList()
)); ));
private final Function<HuskSync, Component> supplier; private final Function<HuskSync, Component> supplier;
@@ -257,13 +261,13 @@ public class HuskSyncCommand extends PluginCommand {
@NotNull @NotNull
private Component get(@NotNull HuskSync plugin) { private Component get(@NotNull HuskSync plugin) {
return Component return Component
.text("").appendSpace() .text("").appendSpace()
.append(Component.text( .append(Component.text(
WordUtils.capitalizeFully(name().replaceAll("_", " ")), WordUtils.capitalizeFully(name().replaceAll("_", " ")),
TextColor.color(0x848484) TextColor.color(0x848484)
)) ))
.append(Component.text(':')).append(Component.space().color(NamedTextColor.WHITE)) .append(Component.text(':')).append(Component.space().color(NamedTextColor.WHITE))
.append(supplier.apply(plugin)); .append(supplier.apply(plugin));
} }
@NotNull @NotNull
@@ -274,7 +278,7 @@ public class HuskSyncCommand extends PluginCommand {
@NotNull @NotNull
private static Component getLocalhostBoolean(@NotNull String value) { private static Component getLocalhostBoolean(@NotNull String value) {
return getBoolean(value.equals("127.0.0.1") || value.equals("0.0.0.0") 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"));
} }
} }

View File

@@ -47,29 +47,29 @@ public class InventoryCommand extends ItemsCommand {
if (optionalInventory.isEmpty()) { if (optionalInventory.isEmpty()) {
viewer.sendMessage(new MineDown("what the FUCK is happening")); viewer.sendMessage(new MineDown("what the FUCK is happening"));
plugin.getLocales().getLocale("error_no_data_to_display") plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage); .ifPresent(viewer::sendMessage);
return; return;
} }
// Display opening message // Display opening message
plugin.getLocales().getLocale("inventory_viewer_opened", user.getUsername(), plugin.getLocales().getLocale("inventory_viewer_opened", user.getUsername(),
snapshot.getTimestamp().format(DateTimeFormatter snapshot.getTimestamp().format(DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT))) .ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
.ifPresent(viewer::sendMessage); .ifPresent(viewer::sendMessage);
// Show GUI // Show GUI
final Data.Items.Inventory inventory = optionalInventory.get(); final Data.Items.Inventory inventory = optionalInventory.get();
viewer.showGui( viewer.showGui(
inventory, inventory,
plugin.getLocales().getLocale("inventory_viewer_menu_title", user.getUsername()) plugin.getLocales().getLocale("inventory_viewer_menu_title", user.getUsername())
.orElse(new MineDown(String.format("%s's Inventory", user.getUsername()))), .orElse(new MineDown(String.format("%s's Inventory", user.getUsername()))),
allowEdit, allowEdit,
inventory.getSlotCount(), inventory.getSlotCount(),
(itemsOnClose) -> { (itemsOnClose) -> {
if (allowEdit && !inventory.equals(itemsOnClose)) { if (allowEdit && !inventory.equals(itemsOnClose)) {
plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user)); plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user));
}
} }
}
); );
} }
@@ -79,7 +79,7 @@ public class InventoryCommand extends ItemsCommand {
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder); final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
if (latestData.isEmpty()) { if (latestData.isEmpty()) {
plugin.getLocales().getLocale("error_no_data_to_display") plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage); .ifPresent(viewer::sendMessage);
return; return;
} }
@@ -89,7 +89,7 @@ public class InventoryCommand extends ItemsCommand {
data.getInventory().ifPresent(inventory -> inventory.setContents(items)); data.getInventory().ifPresent(inventory -> inventory.setContents(items));
data.setSaveCause(DataSnapshot.SaveCause.INVENTORY_COMMAND); data.setSaveCause(DataSnapshot.SaveCause.INVENTORY_COMMAND);
data.setPinned( data.setPinned(
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.INVENTORY_COMMAND) plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.INVENTORY_COMMAND)
); );
}); });

View File

@@ -35,7 +35,7 @@ import java.util.UUID;
public abstract class ItemsCommand extends PluginCommand { public abstract class ItemsCommand extends PluginCommand {
protected ItemsCommand(@NotNull String name, @NotNull List<String> aliases, @NotNull HuskSync plugin) { protected ItemsCommand(@NotNull String name, @NotNull List<String> aliases, @NotNull HuskSync plugin) {
super(name, aliases, Permission.Default.IF_OP, plugin); super(name, aliases, Permission.Default.IF_OP, ExecutionScope.IN_GAME, plugin);
} }
@Override @Override
@@ -46,7 +46,7 @@ public abstract class ItemsCommand extends PluginCommand {
final CommandUser executor = user(command, ctx); final CommandUser executor = user(command, ctx);
if (!(executor instanceof OnlineUser online)) { if (!(executor instanceof OnlineUser online)) {
plugin.getLocales().getLocale("error_in_game_command_only") plugin.getLocales().getLocale("error_in_game_command_only")
.ifPresent(executor::sendMessage); .ifPresent(executor::sendMessage);
return; return;
} }
this.showSnapshotItems(online, user, version); this.showSnapshotItems(online, user, version);
@@ -56,7 +56,7 @@ public abstract class ItemsCommand extends PluginCommand {
final CommandUser executor = user(command, ctx); final CommandUser executor = user(command, ctx);
if (!(executor instanceof OnlineUser online)) { if (!(executor instanceof OnlineUser online)) {
plugin.getLocales().getLocale("error_in_game_command_only") plugin.getLocales().getLocale("error_in_game_command_only")
.ifPresent(executor::sendMessage); .ifPresent(executor::sendMessage);
return; return;
} }
this.showLatestItems(online, user); this.showLatestItems(online, user);
@@ -66,44 +66,44 @@ public abstract class ItemsCommand extends PluginCommand {
// View (and edit) the latest user data // View (and edit) the latest user data
private void showLatestItems(@NotNull OnlineUser viewer, @NotNull User user) { private void showLatestItems(@NotNull OnlineUser viewer, @NotNull User user) {
plugin.getRedisManager().getUserData(user.getUuid(), user).thenAccept(data -> data plugin.getRedisManager().getUserData(user.getUuid(), user).thenAccept(data -> data
.or(() -> plugin.getDatabase().getLatestSnapshot(user)) .or(() -> plugin.getDatabase().getLatestSnapshot(user))
.or(() -> { .or(() -> {
plugin.getLocales().getLocale("error_no_data_to_display") plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage); .ifPresent(viewer::sendMessage);
return Optional.empty();
})
.flatMap(packed -> {
if (packed.isInvalid()) {
plugin.getLocales().getLocale("error_invalid_data", packed.getInvalidReason(plugin))
.ifPresent(viewer::sendMessage);
return Optional.empty(); return Optional.empty();
} })
return Optional.of(packed.unpack(plugin)); .flatMap(packed -> {
}) if (packed.isInvalid()) {
.ifPresent(snapshot -> this.showItems( plugin.getLocales().getLocale("error_invalid_data", packed.getInvalidReason(plugin))
viewer, snapshot, user, viewer.hasPermission(getPermission("edit")) .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 // View a specific version of the user data
private void showSnapshotItems(@NotNull OnlineUser viewer, @NotNull User user, @NotNull UUID version) { private void showSnapshotItems(@NotNull OnlineUser viewer, @NotNull User user, @NotNull UUID version) {
plugin.getDatabase().getSnapshot(user, version) plugin.getDatabase().getSnapshot(user, version)
.or(() -> { .or(() -> {
plugin.getLocales().getLocale("error_invalid_version_uuid") plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(viewer::sendMessage); .ifPresent(viewer::sendMessage);
return Optional.empty();
})
.flatMap(packed -> {
if (packed.isInvalid()) {
plugin.getLocales().getLocale("error_invalid_data", packed.getInvalidReason(plugin))
.ifPresent(viewer::sendMessage);
return Optional.empty(); return Optional.empty();
} })
return Optional.of(packed.unpack(plugin)); .flatMap(packed -> {
}) if (packed.isInvalid()) {
.ifPresent(snapshot -> this.showItems( plugin.getLocales().getLocale("error_invalid_data", packed.getInvalidReason(plugin))
viewer, snapshot, user, false .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 // Show a GUI menu with the correct item data from the snapshot

View File

@@ -39,9 +39,9 @@ public abstract class PluginCommand extends Command {
protected final HuskSync plugin; protected final HuskSync plugin;
protected PluginCommand(@NotNull String name, @NotNull List<String> aliases, protected PluginCommand(@NotNull String name, @NotNull List<String> aliases, @NotNull Permission.Default defPerm,
@NotNull Permission.Default permissionDefault, @NotNull HuskSync plugin) { @NotNull ExecutionScope scope, @NotNull HuskSync plugin) {
super(name, aliases, getDescription(plugin, name), new Permission(createPermission(name), permissionDefault)); super(name, aliases, getDescription(plugin, name), new Permission(createPermission(name), defPerm), scope);
this.plugin = plugin; this.plugin = plugin;
} }

View File

@@ -44,7 +44,7 @@ import java.util.logging.Level;
public class UserDataCommand extends PluginCommand { public class UserDataCommand extends PluginCommand {
public UserDataCommand(@NotNull HuskSync plugin) { public UserDataCommand(@NotNull HuskSync plugin) {
super("userdata", List.of("playerdata"), Permission.Default.IF_OP, plugin); super("userdata", List.of("playerdata"), Permission.Default.IF_OP, ExecutionScope.ALL, plugin);
} }
@Override @Override

View File

@@ -193,14 +193,20 @@ public class Locales {
* Displays the notification in the action bar * Displays the notification in the action bar
*/ */
ACTION_BAR, ACTION_BAR,
/** /**
* Displays the notification in the chat * Displays the notification in the chat
*/ */
CHAT, CHAT,
/** /**
* Displays the notification in an Advancement Toast * Displays the notification in an Advancement Toast
*
* @deprecated No longer supported
*/ */
@Deprecated(since = "3.6.7")
TOAST, TOAST,
/** /**
* Does not display the notification * Does not display the notification
*/ */

View File

@@ -64,7 +64,7 @@ public class Settings {
private boolean checkForUpdates = true; private boolean checkForUpdates = true;
@Comment("Specify a common ID for grouping servers running HuskSync. " @Comment("Specify a common ID for grouping servers running HuskSync. "
+ "Don't modify this unless you know what you're doing!") + "Don't modify this unless you know what you're doing!")
private String clusterId = ""; private String clusterId = "";
@Comment("Enable development debug logging") @Comment("Enable development debug logging")
@@ -229,7 +229,7 @@ public class Settings {
private boolean enabled = false; private boolean enabled = false;
@Comment("What items to save in death snapshots? (DROPS or ITEMS_TO_KEEP). " @Comment("What items to save in death snapshots? (DROPS or ITEMS_TO_KEEP). "
+ "Note that ITEMS_TO_KEEP (suggested for keepInventory servers) requires a Paper 1.19.4+ server.") + "Note that ITEMS_TO_KEEP (suggested for keepInventory servers) requires a Paper 1.19.4+ server.")
private DeathItemsMode itemsToSave = DeathItemsMode.DROPS; private DeathItemsMode itemsToSave = DeathItemsMode.DROPS;
@Comment("Should a death snapshot still be created even if the items to save on the player's death are empty?") @Comment("Should a death snapshot still be created even if the items to save on the player's death are empty?")
@@ -250,14 +250,14 @@ public class Settings {
@Comment("Whether to use the snappy data compression algorithm. Keep on unless you know what you're doing") @Comment("Whether to use the snappy data compression algorithm. Keep on unless you know what you're doing")
private boolean compressData = true; 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; private Locales.NotificationSlot notificationDisplaySlot = Locales.NotificationSlot.ACTION_BAR;
@Comment("Persist maps locked in a Cartography Table to let them be viewed on any server") @Comment("Persist maps locked in a Cartography Table to let them be viewed on any server")
private boolean persistLockedMaps = true; private boolean persistLockedMaps = true;
@Comment("If using the DELAY sync method, how long should this server listen for Redis key data updates before " @Comment("If using the DELAY sync method, how long should this server listen for Redis key data updates before "
+ "pulling data from the database instead (i.e., if the user did not change servers).") + "pulling data from the database instead (i.e., if the user did not change servers).")
private int networkLatencyMilliseconds = 500; private int networkLatencyMilliseconds = 500;
@Comment({"Which data types to synchronize.", "Docs: https://william278.net/docs/husksync/sync-features"}) @Comment({"Which data types to synchronize.", "Docs: https://william278.net/docs/husksync/sync-features"})
@@ -267,10 +267,45 @@ public class Settings {
@Comment("Commands which should be blocked before a player has finished syncing (Use * to block all commands)") @Comment("Commands which should be blocked before a player has finished syncing (Use * to block all commands)")
private List<String> blacklistedCommandsWhileLocked = new ArrayList<>(List.of("*")); private List<String> blacklistedCommandsWhileLocked = new ArrayList<>(List.of("*"));
@Comment({"For attribute syncing, which attributes should be ignored/skipped when syncing", @Comment("Configuration for how to sync attributes")
"(e.g. ['minecraft:generic.max_health', 'minecraft:generic.attack_damage'])"}) private AttributeSettings attributes = new AttributeSettings();
@Getter(AccessLevel.NONE)
private List<String> ignoredAttributes = new ArrayList<>(List.of("")); @Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class AttributeSettings {
@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(""));
@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(
"minecraft:effect.*", "minecraft:creative_mode_*"
));
private boolean matchesWildcard(@NotNull String pat, @NotNull String value) {
if (!pat.contains(":")) {
pat = "minecraft:%s".formatted(pat);
}
if (!value.contains(":")) {
value = "minecraft:%s".formatted(value);
}
return pat.contains("*") ? value.matches(pat.replace("*", ".*")) : pat.equals(value);
}
public boolean isIgnoredAttribute(@NotNull String attribute) {
return ignoredAttributes.stream().anyMatch(wildcard -> matchesWildcard(wildcard, attribute));
}
public boolean isIgnoredModifier(@NotNull String modifier) {
return ignoredModifiers.stream().anyMatch(wildcard -> matchesWildcard(wildcard, modifier));
}
}
@Comment("Event priorities for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts") @Comment("Event priorities for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts")
@Getter(AccessLevel.NONE) @Getter(AccessLevel.NONE)
@@ -284,10 +319,6 @@ public class Settings {
return id.isCustom() || features.getOrDefault(id.getKeyValue(), id.isEnabledByDefault()); return id.isCustom() || features.getOrDefault(id.getKeyValue(), id.isEnabledByDefault());
} }
public boolean isIgnoredAttribute(@NotNull String attribute) {
return ignoredAttributes.contains(attribute);
}
@NotNull @NotNull
public EventListener.Priority getEventPriority(@NotNull EventListener.ListenerType type) { public EventListener.Priority getEventPriority(@NotNull EventListener.ListenerType type) {
try { try {

View File

@@ -155,14 +155,14 @@ public interface Data {
*/ */
interface Advancements extends Data { interface Advancements extends Data {
String RECIPE_ADVANCEMENT = "minecraft:recipe";
@NotNull @NotNull
List<Advancement> getCompleted(); List<Advancement> getCompleted();
@NotNull @NotNull
default List<Advancement> getCompletedExcludingRecipes() { default List<Advancement> getCompletedExcludingRecipes() {
return getCompleted().stream() return getCompleted().stream().filter(adv -> !adv.getKey().startsWith(RECIPE_ADVANCEMENT)).toList();
.filter(advancement -> !advancement.getKey().startsWith("minecraft:recipe"))
.collect(Collectors.toList());
} }
void setCompleted(@NotNull List<Advancement> completed); void setCompleted(@NotNull List<Advancement> completed);
@@ -191,13 +191,13 @@ public interface Data {
@NotNull @NotNull
private static Map<String, Long> adaptDateMap(@NotNull Map<String, Date> dateMap) { private static Map<String, Long> adaptDateMap(@NotNull Map<String, Date> dateMap) {
return dateMap.entrySet().stream() return dateMap.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getTime())); .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getTime()));
} }
@NotNull @NotNull
private static Map<String, Date> adaptLongMap(@NotNull Map<String, Long> dateMap) { private static Map<String, Date> adaptLongMap(@NotNull Map<String, Long> dateMap) {
return dateMap.entrySet().stream() return dateMap.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> new Date(e.getValue()))); .collect(Collectors.toMap(Map.Entry::getKey, e -> new Date(e.getValue())));
} }
@NotNull @NotNull
@@ -250,9 +250,9 @@ public interface Data {
void setWorld(@NotNull World world); void setWorld(@NotNull World world);
record World( record World(
@SerializedName("name") @NotNull String name, @SerializedName("name") @NotNull String name,
@SerializedName("uuid") @NotNull UUID uuid, @SerializedName("uuid") @NotNull UUID uuid,
@SerializedName("environment") @NotNull String environment @SerializedName("environment") @NotNull String environment
) { ) {
} }
} }
@@ -324,9 +324,9 @@ public interface Data {
List<Attribute> getAttributes(); List<Attribute> getAttributes();
record Attribute( record Attribute(
@NotNull String name, @NotNull String name,
double baseValue, double baseValue,
@NotNull Set<Modifier> modifiers @NotNull Set<Modifier> modifiers
) { ) {
public double getValue() { public double getValue() {
@@ -366,7 +366,13 @@ public interface Data {
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
return obj instanceof Modifier modifier && modifier.uuid().equals(uuid()); if (obj instanceof Modifier other) {
if (uuid == null || other.uuid == null) {
return name.equals(other.name);
}
return uuid.equals(other.uuid);
}
return super.equals(obj);
} }
public double modify(double value) { public double modify(double value) {
@@ -378,6 +384,10 @@ public interface Data {
}; };
} }
public boolean hasUuid() {
return uuid != null;
}
@NotNull @NotNull
public UUID uuid() { public UUID uuid() {
return uuid != null ? uuid : UUID.nameUUIDFromBytes(name.getBytes()); return uuid != null ? uuid : UUID.nameUUIDFromBytes(name.getBytes());
@@ -387,8 +397,8 @@ public interface Data {
default Optional<Attribute> getAttribute(@NotNull Key key) { default Optional<Attribute> getAttribute(@NotNull Key key) {
return getAttributes().stream() return getAttributes().stream()
.filter(attribute -> attribute.name().equals(key.asString())) .filter(attribute -> attribute.name().equals(key.asString()))
.findFirst(); .findFirst();
} }
default void removeAttribute(@NotNull Key key) { default void removeAttribute(@NotNull Key key) {
@@ -397,8 +407,8 @@ public interface Data {
default double getMaxHealth() { default double getMaxHealth() {
return getAttribute(MAX_HEALTH_KEY) return getAttribute(MAX_HEALTH_KEY)
.map(Attribute::getValue) .map(Attribute::getValue)
.orElse(20.0); .orElse(20.0);
} }
default void setMaxHealth(double maxHealth) { default void setMaxHealth(double maxHealth) {

View File

@@ -45,13 +45,13 @@ public class DataException extends IllegalStateException {
@AllArgsConstructor @AllArgsConstructor
public enum Reason { public enum Reason {
INVALID_MINECRAFT_VERSION((plugin, snapshot) -> String.format("The Minecraft version of the snapshot (%s) is " + 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())), snapshot.getMinecraftVersion(), plugin.getMinecraftVersion())),
INVALID_FORMAT_VERSION((plugin, snapshot) -> String.format("The format version of the snapshot (%s) is newer " + 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)), snapshot.getFormatVersion(), DataSnapshot.CURRENT_FORMAT_VERSION)),
INVALID_PLATFORM_TYPE((plugin, snapshot) -> String.format("The platform type of the snapshot (%s) does " + 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())), snapshot.getPlatformType(), plugin.getPlatformType())),
NO_LEGACY_CONVERTER((plugin, snapshot) -> String.format("No legacy converter to convert format version: %s", NO_LEGACY_CONVERTER((plugin, snapshot) -> String.format("No legacy converter to convert format version: %s",
snapshot.getFormatVersion())); snapshot.getFormatVersion()));

View File

@@ -395,7 +395,7 @@ public class DataSnapshot {
.map(entry -> Map.entry(plugin.getIdentifier(entry.getKey()).orElseThrow(), entry.getValue())) .map(entry -> Map.entry(plugin.getIdentifier(entry.getKey()).orElseThrow(), entry.getValue()))
.collect(Collectors.toMap( .collect(Collectors.toMap(
Map.Entry::getKey, Map.Entry::getKey,
entry -> plugin.deserializeData(entry.getKey(), entry.getValue()), entry -> plugin.deserializeData(entry.getKey(), entry.getValue(), getMinecraftVersion()),
(a, b) -> b, () -> Maps.newTreeMap(SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR) (a, b) -> b, () -> Maps.newTreeMap(SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR)
)); ));
} }
@@ -535,9 +535,9 @@ public class DataSnapshot {
public Builder timestamp(@NotNull OffsetDateTime timestamp) { public Builder timestamp(@NotNull OffsetDateTime timestamp) {
if (timestamp.isAfter(OffsetDateTime.now())) { if (timestamp.isAfter(OffsetDateTime.now())) {
throw new IllegalArgumentException("Data snapshots cannot have a timestamp set in the future! " throw new IllegalArgumentException("Data snapshots cannot have a timestamp set in the future! "
+ "Make sure your database server time matches the server time.\n" + "Make sure your database server time matches the server time.\n"
+ "Current game server timestamp: " + OffsetDateTime.now() + " / " + "Current game server timestamp: " + OffsetDateTime.now() + " / "
+ "Snapshot timestamp: " + timestamp); + "Snapshot timestamp: " + timestamp);
} }
this.timestamp = timestamp; this.timestamp = timestamp;
return this; return this;

View File

@@ -50,7 +50,8 @@ public class Identifier {
Dependency.optional("game_mode") Dependency.optional("game_mode")
); );
public static final Identifier ATTRIBUTES = huskSync("attributes", true, public static final Identifier ATTRIBUTES = huskSync("attributes", true,
Dependency.required("potion_effects") Dependency.optional("inventory"),
Dependency.optional("potion_effects")
); );
public static final Identifier HEALTH = huskSync("health", true, public static final Identifier HEALTH = huskSync("health", true,
Dependency.optional("attributes") Dependency.optional("attributes")

View File

@@ -19,6 +19,7 @@
package net.william278.husksync.data; package net.william278.husksync.data;
import net.william278.desertwell.util.Version;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -119,19 +120,36 @@ public interface SerializerRegistry {
} }
/** /**
* Deserialize data for the given {@link Identifier} * Deserialize data of a given {@link Version Minecraft version} for the given {@link Identifier data identifier}
*
* @param identifier the {@link Identifier} to deserialize data for
* @param data the data to deserialize
* @param dataMcVersion the Minecraft version of the data
* @return the deserialized data
* @throws IllegalStateException if no serializer is found for the given {@link Identifier}
* @since 3.6.4
*/
@NotNull
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))
);
}
/**
* Deserialize data for the given {@link Identifier data identifier}
* *
* @param identifier the {@link Identifier} to deserialize data for * @param identifier the {@link Identifier} to deserialize data for
* @param data the data to deserialize * @param data the data to deserialize
* @return the deserialized data * @return the deserialized data
* @throws IllegalStateException if no serializer is found for the given {@link Identifier}
* @since 3.5.4 * @since 3.5.4
* @deprecated Use {@link #deserializeData(Identifier, String, Version)} instead
*/ */
@NotNull @NotNull
default Data deserializeData(@NotNull Identifier identifier, @NotNull String data) throws IllegalStateException { @Deprecated(since = "3.6.5")
return getSerializer(identifier).map(serializer -> serializer.deserialize(data)).orElseThrow( default Data deserializeData(@NotNull Identifier identifier, @NotNull String data) {
() -> new IllegalStateException("No serializer found for %s".formatted(identifier)) return deserializeData(identifier, data, getPlugin().getMinecraftVersion());
);
} }
/** /**

View File

@@ -50,6 +50,7 @@ public class MongoDbDatabase extends Database {
private final String usersTable; private final String usersTable;
private final String userDataTable; private final String userDataTable;
public MongoDbDatabase(@NotNull HuskSync plugin) { public MongoDbDatabase(@NotNull HuskSync plugin) {
super(plugin); super(plugin);
this.usersTable = plugin.getSettings().getDatabase().getTableName(TableName.USERS); this.usersTable = plugin.getSettings().getDatabase().getTableName(TableName.USERS);
@@ -76,7 +77,7 @@ public class MongoDbDatabase extends Database {
} }
} catch (Exception e) { } catch (Exception e) {
throw new IllegalStateException("Failed to establish a connection to the MongoDB database. " + 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);
} }
} }
@@ -376,7 +377,7 @@ public class MongoDbDatabase extends Database {
/** /**
* Update a saved {@link DataSnapshot} by given version UUID * 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 * @param data The {@link DataSnapshot} to update
*/ */
@Blocking @Blocking

View File

@@ -124,11 +124,11 @@ public class MySqlDatabase extends Database {
} }
} catch (SQLException e) { } catch (SQLException e) {
throw new IllegalStateException("Failed to create database tables. Please ensure you are running MySQL v8.0+ " + 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) { } catch (SQLException | IOException e) {
throw new IllegalStateException("Failed to establish a connection to the MySQL database. " + 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);
} }
} }

View File

@@ -123,11 +123,11 @@ public class PostgresDatabase extends Database {
} }
} catch (SQLException e) { } catch (SQLException e) {
throw new IllegalStateException("Failed to create database tables. Please ensure you are running PostgreSQL " + 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) { } catch (SQLException | IOException e) {
throw new IllegalStateException("Failed to establish a connection to the PostgreSQL database. " + 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);
} }
} }

View File

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

View File

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

View File

@@ -53,7 +53,7 @@ public abstract class EventListener {
return; return;
} }
plugin.lockPlayer(user.getUuid()); plugin.lockPlayer(user.getUuid());
plugin.getDataSyncer().setUserData(user); plugin.getDataSyncer().syncApplyUserData(user);
} }
/** /**
@@ -66,7 +66,7 @@ public abstract class EventListener {
return; return;
} }
plugin.lockPlayer(user.getUuid()); plugin.lockPlayer(user.getUuid());
plugin.getDataSyncer().saveUserData(user); plugin.getDataSyncer().syncSaveUserData(user);
} }
/** /**
@@ -94,7 +94,7 @@ public abstract class EventListener {
protected void saveOnPlayerDeath(@NotNull OnlineUser user, @NotNull Data.Items items) { protected void saveOnPlayerDeath(@NotNull OnlineUser user, @NotNull Data.Items items) {
final SaveOnDeathSettings settings = plugin.getSettings().getSynchronization().getSaveOnDeath(); final SaveOnDeathSettings settings = plugin.getSettings().getSynchronization().getSaveOnDeath();
if (plugin.isDisabling() || !settings.isEnabled() || plugin.isLocked(user.getUuid()) if (plugin.isDisabling() || !settings.isEnabled() || plugin.isLocked(user.getUuid())
|| user.isNpc() || (!settings.isSaveEmptyItems() && items.isEmpty())) { || user.isNpc() || (!settings.isSaveEmptyItems() && items.isEmpty())) {
return; return;
} }

View File

@@ -92,7 +92,7 @@ public class RedisManager extends JedisPubSub {
jedisPool.getResource().ping(); jedisPool.getResource().ping();
} catch (JedisException e) { } catch (JedisException e) {
throw new IllegalStateException("Failed to establish connection with Redis. " throw new IllegalStateException("Failed to establish connection with Redis. "
+ "Please check the supplied credentials in the config file", e); + "Please check the supplied credentials in the config file", e);
} }
// Subscribe using a thread (rather than a task) // Subscribe using a thread (rather than a task)
@@ -281,16 +281,21 @@ public class RedisManager extends JedisPubSub {
@Blocking @Blocking
public void setUserCheckedOut(@NotNull User user, boolean checkedOut) { public void setUserCheckedOut(@NotNull User user, boolean checkedOut) {
try (Jedis jedis = jedisPool.getResource()) { try (Jedis jedis = jedisPool.getResource()) {
final String key = getKeyString(RedisKeyType.DATA_CHECKOUT, user.getUuid(), clusterId);
if (checkedOut) { if (checkedOut) {
jedis.set( jedis.set(
getKey(RedisKeyType.DATA_CHECKOUT, user.getUuid(), clusterId), key.getBytes(StandardCharsets.UTF_8),
plugin.getServerName().getBytes(StandardCharsets.UTF_8) plugin.getServerName().getBytes(StandardCharsets.UTF_8)
); );
} else { } else {
jedis.del(getKey(RedisKeyType.DATA_CHECKOUT, user.getUuid(), clusterId)); if (jedis.del(key.getBytes(StandardCharsets.UTF_8)) == 0) {
plugin.debug(String.format("[%s] %s key not set on Redis when attempting removal (%s)",
user.getUsername(), RedisKeyType.DATA_CHECKOUT, key));
return;
}
} }
plugin.debug(String.format("[%s] %s %s key to/from Redis", user.getUsername(), plugin.debug(String.format("[%s] %s %s key %s Redis (%s)", user.getUsername(),
checkedOut ? "Set" : "Removed", RedisKeyType.DATA_CHECKOUT)); checkedOut ? "Set" : "Removed", RedisKeyType.DATA_CHECKOUT, checkedOut ? "to" : "from", key));
} catch (Throwable e) { } catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred setting checkout to", e); plugin.log(Level.SEVERE, "An exception occurred setting checkout to", e);
} }
@@ -418,7 +423,12 @@ public class RedisManager extends JedisPubSub {
} }
private static byte[] getKey(@NotNull RedisKeyType keyType, @NotNull UUID uuid, @NotNull String clusterId) { private static byte[] getKey(@NotNull RedisKeyType keyType, @NotNull UUID uuid, @NotNull String clusterId) {
return String.format("%s:%s", keyType.getKeyPrefix(clusterId), uuid).getBytes(StandardCharsets.UTF_8); return getKeyString(keyType, uuid, clusterId).getBytes(StandardCharsets.UTF_8);
}
@NotNull
private static String getKeyString(@NotNull RedisKeyType keyType, @NotNull UUID uuid, @NotNull String clusterId) {
return String.format("%s:%s", keyType.getKeyPrefix(clusterId), uuid);
} }
} }

View File

@@ -81,18 +81,18 @@ public abstract class DataSyncer {
} }
/** /**
* Called when a user's data should be fetched and applied to them * Called when a user's data should be fetched and applied to them as part of a synchronization process
* *
* @param user the user to fetch data for * @param user the user to fetch data for
*/ */
public abstract void setUserData(@NotNull OnlineUser user); public abstract void syncApplyUserData(@NotNull OnlineUser user);
/** /**
* Called when a user's data should be serialized and saved * Called when a user's data should be serialized and saved as part of a synchronization process
* *
* @param user the user to save * @param user the user to save
*/ */
public abstract void saveUserData(@NotNull OnlineUser user); public abstract void syncSaveUserData(@NotNull OnlineUser user);
/** /**
* Save a {@link DataSnapshot.Packed user's data snapshot} to the database, * Save a {@link DataSnapshot.Packed user's data snapshot} to the database,
@@ -150,7 +150,7 @@ public abstract class DataSyncer {
private long getMaxListenAttempts() { private long getMaxListenAttempts() {
return BASE_LISTEN_ATTEMPTS + ( return BASE_LISTEN_ATTEMPTS + (
(Math.max(100, plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds()) / 1000) (Math.max(100, plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds()) / 1000)
* 20 / LISTEN_DELAY * 20 / LISTEN_DELAY
); );
} }

View File

@@ -35,7 +35,7 @@ public class DelayDataSyncer extends DataSyncer {
} }
@Override @Override
public void setUserData(@NotNull OnlineUser user) { public void syncApplyUserData(@NotNull OnlineUser user) {
plugin.runAsyncDelayed( plugin.runAsyncDelayed(
() -> { () -> {
// Fetch from the database if the user isn't changing servers // Fetch from the database if the user isn't changing servers
@@ -58,7 +58,7 @@ public class DelayDataSyncer extends DataSyncer {
} }
@Override @Override
public void saveUserData(@NotNull OnlineUser onlineUser) { public void syncSaveUserData(@NotNull OnlineUser onlineUser) {
plugin.runAsync(() -> { plugin.runAsync(() -> {
getRedis().setUserServerSwitch(onlineUser); getRedis().setUserServerSwitch(onlineUser);
saveData( saveData(

View File

@@ -43,7 +43,7 @@ public class LockstepDataSyncer extends DataSyncer {
// Consume their data when they are checked in // Consume their data when they are checked in
@Override @Override
public void setUserData(@NotNull OnlineUser user) { public void syncApplyUserData(@NotNull OnlineUser user) {
this.listenForRedisData(user, () -> { this.listenForRedisData(user, () -> {
if (getRedis().getUserCheckedOut(user).isPresent()) { if (getRedis().getUserCheckedOut(user).isPresent()) {
return false; return false;
@@ -58,7 +58,7 @@ public class LockstepDataSyncer extends DataSyncer {
} }
@Override @Override
public void saveUserData(@NotNull OnlineUser onlineUser) { public void syncSaveUserData(@NotNull OnlineUser onlineUser) {
plugin.runAsync(() -> saveData( plugin.runAsync(() -> saveData(
onlineUser, onlineUser.createSnapshot(DataSnapshot.SaveCause.DISCONNECT), onlineUser, onlineUser.createSnapshot(DataSnapshot.SaveCause.DISCONNECT),
(user, data) -> { (user, data) -> {

View File

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

View File

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

View File

@@ -178,11 +178,11 @@ public class DataDumper {
@NotNull @NotNull
private String getFileName() { private String getFileName() {
return new StringJoiner("_") return new StringJoiner("_")
.add(user.getUsername()) .add(user.getUsername())
.add(snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"))) .add(snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")))
.add(snapshot.getSaveCause().name().toLowerCase(Locale.ENGLISH)) .add(snapshot.getSaveCause().name().toLowerCase(Locale.ENGLISH))
.add(snapshot.getShortId()) .add(snapshot.getShortId())
+ ".json"; + ".json";
} }
} }

View File

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

View File

@@ -109,7 +109,7 @@ synchronization:
sync_dead_players_changing_server: true sync_dead_players_changing_server: true
# Whether to use the snappy data compression algorithm. Keep on unless you know what you're doing # Whether to use the snappy data compression algorithm. Keep on unless you know what you're doing
compress_data: true 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 notification_display_slot: ACTION_BAR
# Persist maps locked in a Cartography Table to let them be viewed on any server # Persist maps locked in a Cartography Table to let them be viewed on any server
persist_locked_maps: true 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) # Commands which should be blocked before a player has finished syncing (Use * to block all commands)
blacklisted_commands_while_locked: blacklisted_commands_while_locked:
- '*' - '*'
# For attribute syncing, which attributes should be ignored/skipped when syncing # Configuration for how to sync attributes
# (e.g. ['minecraft:generic.max_health', 'minecraft:generic.attack_damage']) attributes:
ignored_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 for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts
event_priorities: event_priorities:
quit_listener: LOWEST quit_listener: LOWEST

View File

@@ -18,12 +18,12 @@ This guide will walk you through how to upgrade from HuskSync v1.4.x to HuskSync
### 3. Configure the migrator ### 3. Configure the migrator
- With your servers back on and correctly configured to run HuskSync v3.x, ensure nobody is online. - With your servers back on and correctly configured to run HuskSync v3.x, ensure nobody is online.
- Use the console on one of your Spigot servers to enter: `husksync migrate legacy` - Use the console on one of your Spigot servers to enter: `husksync migrate help legacy`
- Carefully read the migration configuration instructions. In most cases, you won't have to change the settings, but if you do need to adjust them, use `husksync migrate legacy set <setting> <value>`. - Carefully read the migration configuration instructions. In most cases, you won't have to change the settings, but if you do need to adjust them, use `husksync migrate set legacy <setting> <value>`.
- Migration will be carried out *from* the database you specify with the settings in console *to* the database configured in `config.yml`. If you're migrating from multiple clusters, ensure you run the migrator on the correct servers corresponding to the migrator. - Migration will be carried out *from* the database you specify with the settings in console *to* the database configured in `config.yml`. If you're migrating from multiple clusters, ensure you run the migrator on the correct servers corresponding to the migrator.
### 4. Start the migrator ### 4. Start the migrator
- Run `husksync migrate legacy start` to begin the migration process. This may take some time, depending on the amount of data you're migrating. - Run `husksync migrate start legacy` to begin the migration process. This may take some time, depending on the amount of data you're migrating.
### 5. Ensure the migration was successful ### 5. Ensure the migration was successful
- HuskSync will notify in console when migration is complete. Verify that the migration went OK by logging in and using the `/userdata list <username>` command to see if the data was imported with the `legacy migration` saveCause. - HuskSync will notify in console when migration is complete. Verify that the migration went OK by logging in and using the `/userdata list <username>` command to see if the data was imported with the `legacy migration` saveCause.

View File

@@ -13,12 +13,12 @@ This guide will walk you through how to migrate from MySQLPlayerDataBridge (MPDB
### 2. Configure the migrator ### 2. Configure the migrator
- With your servers back on and correctly configured to run HuskSync v3.x, ensure nobody is online. - With your servers back on and correctly configured to run HuskSync v3.x, ensure nobody is online.
- Use the console on one of your Spigot servers to enter: `husksync migrate mpdb`. If the MPDB migrator is not available, ensure MySQLPlayerDataBridge is still installed. - Use the console on one of your Spigot servers to enter: `husksync migrate help mpdb`. If the MPDB migrator is not available, ensure MySQLPlayerDataBridge is still installed.
- Adjust the migration setting as needed using the following command: `husksync migrate mpdb set <setting> <value>`. - Adjust the migration setting as needed using the following command: `husksync migrate set mpdb <setting> <value>`.
- Note that migration will be carried out *from* the database you specify with the settings in console *to* the database configured in `config.yml`. - Note that migration will be carried out *from* the database you specify with the settings in console *to* the database configured in `config.yml`.
### 3. Start the migrator ### 3. Start the migrator
- Run `husksync migrate mpdb start` to begin the migration process. This may take some time, depending on the amount of data you're migrating. - Run `husksync migrate start mpdb` to begin the migration process. This may take some time, depending on the amount of data you're migrating.
### 4. Uninstall MySQLPlayerDataBridge ### 4. Uninstall MySQLPlayerDataBridge
- HuskSync will display a message in console when data migration is complete. - HuskSync will display a message in console when data migration is complete.

View File

@@ -18,20 +18,21 @@ dependencies {
modImplementation include("net.kyori:adventure-platform-fabric:${adventure_platform_fabric_version}") modImplementation include("net.kyori:adventure-platform-fabric:${adventure_platform_fabric_version}")
modImplementation include("me.lucko:fabric-permissions-api:${fabric_permissions_api_version}") modImplementation include("me.lucko:fabric-permissions-api:${fabric_permissions_api_version}")
modImplementation include("eu.pb4:sgui:${sgui_version}") modImplementation include("eu.pb4:sgui:${sgui_version}")
modImplementation include('net.william278.uniform:uniform-fabric:1.1.8+1.20.1') modImplementation include('net.william278.uniform:uniform-fabric:1.2.1+1.20.1')
modCompileOnly "net.fabricmc.fabric-api:fabric-api:${fabric_api_version}" modCompileOnly "net.fabricmc.fabric-api:fabric-api:${fabric_api_version}"
implementation include('org.apache.commons:commons-pool2:2.12.0') implementation include('org.apache.commons:commons-pool2:2.12.0')
implementation include("redis.clients:jedis:$jedis_version") implementation include("redis.clients:jedis:$jedis_version")
implementation include("com.mysql:mysql-connector-j:$mysql_driver_version") implementation include("com.mysql:mysql-connector-j:$mysql_driver_version")
implementation include("org.mariadb.jdbc:mariadb-java-client:$mariadb_driver_version") implementation include("org.mariadb.jdbc:mariadb-java-client:$mariadb_driver_version")
implementation include("org.postgresql:postgresql:$postgres_driver_version")
implementation include("org.xerial.snappy:snappy-java:$snappy_version") implementation include("org.xerial.snappy:snappy-java:$snappy_version")
compileOnly 'org.jetbrains:annotations:24.1.0' compileOnly 'org.jetbrains:annotations:24.1.0'
compileOnly 'net.william278:DesertWell:2.0.4' compileOnly 'net.william278:DesertWell:2.0.4'
compileOnly 'org.projectlombok:lombok:1.18.32' compileOnly 'org.projectlombok:lombok:1.18.34'
annotationProcessor 'org.projectlombok:lombok:1.18.32' annotationProcessor 'org.projectlombok:lombok:1.18.34'
shadow project(path: ":common") shadow project(path: ":common")
} }

View File

@@ -49,6 +49,7 @@ import net.william278.husksync.database.MongoDbDatabase;
import net.william278.husksync.database.MySqlDatabase; import net.william278.husksync.database.MySqlDatabase;
import net.william278.husksync.database.PostgresDatabase; import net.william278.husksync.database.PostgresDatabase;
import net.william278.husksync.event.FabricEventDispatcher; import net.william278.husksync.event.FabricEventDispatcher;
import net.william278.husksync.event.ModLoadedCallback;
import net.william278.husksync.hook.PlanHook; import net.william278.husksync.hook.PlanHook;
import net.william278.husksync.listener.EventListener; import net.william278.husksync.listener.EventListener;
import net.william278.husksync.listener.FabricEventListener; import net.william278.husksync.listener.FabricEventListener;
@@ -78,12 +79,12 @@ import java.util.logging.Level;
@Getter @Getter
@NoArgsConstructor @NoArgsConstructor
public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync, FabricTask.Supplier, public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync, FabricTask.Supplier,
FabricEventDispatcher { FabricEventDispatcher {
private static final String PLATFORM_TYPE_ID = "fabric"; private static final String PLATFORM_TYPE_ID = "fabric";
private final TreeMap<Identifier, Serializer<? extends Data>> serializers = Maps.newTreeMap( 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<UUID, Map<Identifier, Data>> playerCustomDataStore = Maps.newConcurrentMap();
private final Map<String, Boolean> permissions = Maps.newHashMap(); private final Map<String, Boolean> permissions = Maps.newHashMap();
@@ -206,6 +207,16 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
// Check for updates // Check for updates
this.checkForUpdates(); 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());
} }
private void onDisable() { private void onDisable() {
@@ -264,15 +275,15 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
@Nullable @Nullable
public InputStream getResource(@NotNull String name) { public InputStream getResource(@NotNull String name) {
return this.mod.findPath(name) return this.mod.findPath(name)
.map(path -> { .map(path -> {
try { try {
return Files.newInputStream(path); return Files.newInputStream(path);
} catch (IOException e) { } catch (IOException e) {
log(Level.WARNING, "Failed to load resource: " + name, e); log(Level.WARNING, "Failed to load resource: " + name, e);
} }
return null; return null;
}) })
.orElse(this.getClass().getClassLoader().getResourceAsStream(name)); .orElse(this.getClass().getClassLoader().getResourceAsStream(name));
} }
@Override @Override
@@ -292,11 +303,11 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
@Override @Override
public void log(@NotNull Level level, @NotNull String message, @NotNull Throwable... throwable) { public void log(@NotNull Level level, @NotNull String message, @NotNull Throwable... throwable) {
LoggingEventBuilder logEvent = logger.makeLoggingEventBuilder( LoggingEventBuilder logEvent = logger.makeLoggingEventBuilder(
switch (level.getName()) { switch (level.getName()) {
case "WARNING" -> org.slf4j.event.Level.WARN; case "WARNING" -> org.slf4j.event.Level.WARN;
case "SEVERE" -> org.slf4j.event.Level.ERROR; case "SEVERE" -> org.slf4j.event.Level.ERROR;
default -> org.slf4j.event.Level.INFO; default -> org.slf4j.event.Level.INFO;
} }
); );
if (throwable.length >= 1) { if (throwable.length >= 1) {
logEvent = logEvent.setCause(throwable[0]); logEvent = logEvent.setCause(throwable[0]);
@@ -328,6 +339,14 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
return PLATFORM_TYPE_ID; 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 @Override
public Optional<LegacyConverter> getLegacyConverter() { public Optional<LegacyConverter> getLegacyConverter() {
return Optional.empty(); return Optional.empty();

View File

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

View File

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

View File

@@ -0,0 +1,39 @@
/*
* 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.event;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.william278.husksync.api.HuskSyncAPI;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
public interface ModLoadedCallback {
@NotNull
Event<ModLoadedCallback> EVENT = EventFactory.createArrayBacked(
ModLoadedCallback.class,
(listeners) -> (api) -> Arrays.stream(listeners).forEach(listener -> listener.post(api))
);
void post(@NotNull HuskSyncAPI api);
}

View File

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

View File

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

View File

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

View File

@@ -40,6 +40,7 @@ import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.logging.Level;
public class FabricUser extends OnlineUser implements FabricUserDataHolder { public class FabricUser extends OnlineUser implements FabricUserDataHolder {
@@ -70,9 +71,12 @@ public class FabricUser extends OnlineUser implements FabricUserDataHolder {
} }
@Override @Override
@Deprecated(since = "3.6.7")
public void sendToast(@NotNull MineDown title, @NotNull MineDown description, @NotNull String iconMaterial, public void sendToast(@NotNull MineDown title, @NotNull MineDown description, @NotNull String iconMaterial,
@NotNull String backgroundType) { @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 @Override

View File

@@ -19,18 +19,21 @@
package net.william278.husksync.util; package net.william278.husksync.util;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import net.william278.husksync.FabricHuskSync; import net.william278.husksync.FabricHuskSync;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import net.william278.husksync.data.UserDataHolder; import net.william278.husksync.data.UserDataHolder;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public interface FabricTask extends Task { public interface FabricTask extends Task {
ScheduledExecutorService ASYNC_EXEC = Executors.newScheduledThreadPool(4,
new ThreadFactoryBuilder()
.setDaemon(true)
.setNameFormat("HuskSync-ThreadPool")
.build());
class Sync extends Task.Sync implements FabricTask { class Sync extends Task.Sync implements FabricTask {
@@ -46,7 +49,7 @@ public interface FabricTask extends Task {
@Override @Override
public void run() { public void run() {
if (!cancelled) { if (!cancelled) {
Executors.newSingleThreadScheduledExecutor().schedule( ASYNC_EXEC.schedule(
() -> ((FabricHuskSync) getPlugin()).getMinecraftServer().executeSync(runnable), () -> ((FabricHuskSync) getPlugin()).getMinecraftServer().executeSync(runnable),
delayTicks * 50, delayTicks * 50,
TimeUnit.MILLISECONDS TimeUnit.MILLISECONDS
@@ -73,7 +76,7 @@ public interface FabricTask extends Task {
@Override @Override
public void run() { public void run() {
if (!cancelled) { if (!cancelled) {
this.task = CompletableFuture.runAsync(runnable, ((FabricHuskSync) getPlugin()).getMinecraftServer()); this.task = CompletableFuture.runAsync(runnable, ASYNC_EXEC);
} }
} }
} }
@@ -97,7 +100,7 @@ public interface FabricTask extends Task {
@Override @Override
public void run() { public void run() {
if (!cancelled) { if (!cancelled) {
this.task = Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate( this.task = ASYNC_EXEC.scheduleAtFixedRate(
runnable, runnable,
0, 0,
repeatingTicks * 50, repeatingTicks * 50,
@@ -129,7 +132,7 @@ public interface FabricTask extends Task {
@Override @Override
default void cancelTasks() { default void cancelTasks() {
// Do nothing ASYNC_EXEC.shutdownNow();
} }
} }

View File

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

View File

@@ -6,13 +6,13 @@ dependencies {
implementation project(':bukkit') implementation project(':bukkit')
compileOnly project(':common') compileOnly project(':common')
implementation 'net.william278.uniform:uniform-paper:1.1.8' implementation 'net.william278.uniform:uniform-paper:1.2.1'
compileOnly 'io.papermc.paper:paper-api:1.19.4-R0.1-SNAPSHOT' compileOnly 'io.papermc.paper:paper-api:1.19.4-R0.1-SNAPSHOT'
compileOnly 'org.jetbrains:annotations:24.1.0' compileOnly 'org.jetbrains:annotations:24.1.0'
compileOnly 'org.projectlombok:lombok:1.18.32' compileOnly 'org.projectlombok:lombok:1.18.34'
annotationProcessor 'org.projectlombok:lombok:1.18.32' annotationProcessor 'org.projectlombok:lombok:1.18.34'
} }
shadowJar { shadowJar {
@@ -34,7 +34,6 @@ shadowJar {
relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell' relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell'
relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown' relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown'
relocate 'net.william278.mapdataapi', 'net.william278.husksync.libraries.mapdataapi' 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.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'
relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter' relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter'
relocate 'org.json', 'net.william278.husksync.libraries.json' relocate 'org.json', 'net.william278.husksync.libraries.json'
@@ -50,10 +49,6 @@ shadowJar {
tasks { tasks {
runServer { runServer {
minecraftVersion('1.20.4') minecraftVersion('1.21.1')
downloadPlugins {
url('https://download.luckperms.net/1549/bukkit/loader/LuckPerms-Bukkit-5.4.134.jar')
}
} }
} }

View File

@@ -1,4 +1,4 @@
certifi==2023.7.22 certifi==2024.7.4
charset-normalizer==3.2.0 charset-normalizer==3.2.0
colorama==0.4.6 colorama==0.4.6
idna==3.7 idna==3.7

View File

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