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

Compare commits

...

24 Commits
3.6.6 ... 3.6.7

Author SHA1 Message Date
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
49 changed files with 567 additions and 365 deletions

View File

@@ -41,4 +41,24 @@ jobs:
id: fetch-version
- name: Get Version
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

@@ -30,4 +30,24 @@ jobs:
uses: mikepenz/action-junit-report@v4
if: success() || failure() # Continue on failure
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

@@ -99,9 +99,11 @@ allprojects {
}
processResources {
def tokenMap = rootProject.ext.properties
tokenMap.merge("grgit",'',(s, s2) -> s)
filesMatching(['**/*.json', '**/*.yml']) {
filter ReplaceTokens as Class, beginToken: '${', endToken: '}',
tokens: rootProject.ext.properties
tokens: tokenMap
}
}
}

View File

@@ -5,7 +5,6 @@ dependencies {
implementation 'net.william278:mpdbdataconverter:1.0.1'
implementation 'net.william278:hsldataconverter:1.0'
implementation 'net.william278:mapdataapi:1.0.3'
implementation 'net.william278:andjam:1.0.2'
implementation 'org.bstats:bstats-bukkit:3.0.2'
implementation 'net.kyori:adventure-platform-bukkit:4.3.3'
implementation 'dev.triumphteam:triumph-gui:3.1.10'
@@ -46,7 +45,6 @@ shadowJar {
relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell'
relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown'
relocate 'net.william278.mapdataapi', 'net.william278.husksync.libraries.mapdataapi'
relocate 'net.william278.andjam', 'net.william278.husksync.libraries.andjam'
relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'
relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter'
relocate 'org.json', 'net.william278.husksync.libraries.json'

View File

@@ -333,6 +333,12 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
return PLATFORM_TYPE_ID;
}
@Override
@NotNull
public String getServerVersion() {
return String.format("%s/%s", getServer().getName(), getServer().getVersion());
}
@Override
public Optional<LegacyConverter> getLegacyConverter() {
return Optional.of(legacyConverter);

View File

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

View File

@@ -31,12 +31,9 @@ import net.william278.desertwell.util.Version;
import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.HuskSync;
import net.william278.husksync.adapter.Adaptable;
import net.william278.husksync.config.Settings.SynchronizationSettings.AttributeSettings;
import net.william278.husksync.user.BukkitUser;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.Registry;
import org.bukkit.Statistic;
import org.bukkit.NamespacedKey;
import org.bukkit.*;
import org.bukkit.advancement.AdvancementProgress;
import org.bukkit.attribute.AttributeInstance;
import org.bukkit.attribute.AttributeModifier;
@@ -50,7 +47,9 @@ import org.bukkit.potion.PotionEffectType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Range;
import org.jetbrains.annotations.Unmodifiable;
import java.lang.reflect.Constructor;
import java.util.*;
import java.util.logging.Level;
import java.util.stream.Collectors;
@@ -238,8 +237,9 @@ public abstract class BukkitData implements Data {
private final Collection<PotionEffect> effects;
@NotNull
public static BukkitData.PotionEffects from(@NotNull Collection<PotionEffect> effects) {
return new BukkitData.PotionEffects(effects);
public static BukkitData.PotionEffects from(@NotNull Collection<PotionEffect> sei) {
return new BukkitData.PotionEffects(Lists.newArrayList(sei.stream().filter(e -> !e.isAmbient()).toList()));
}
@NotNull
@@ -263,7 +263,7 @@ public abstract class BukkitData implements Data {
@NotNull
@SuppressWarnings("unused")
public static BukkitData.PotionEffects empty() {
return new BukkitData.PotionEffects(List.of());
return new BukkitData.PotionEffects(Lists.newArrayList());
}
@Override
@@ -279,6 +279,7 @@ public abstract class BukkitData implements Data {
@NotNull
@Override
@Unmodifiable
public List<Effect> getActiveEffects() {
return effects.stream()
.map(potionEffect -> new Effect(
@@ -366,7 +367,7 @@ public abstract class BukkitData implements Data {
// Set player experience and level (prevent advancement awards applying twice), reset game rule
if (!toAward.isEmpty()
&& (player.getLevel() != expLevel || player.getExp() != expProgress)) {
&& (player.getLevel() != expLevel || player.getExp() != expProgress)) {
player.setLevel(expLevel);
player.setExp(expProgress);
}
@@ -577,14 +578,14 @@ public abstract class BukkitData implements Data {
@NotNull
public static BukkitData.Attributes adapt(@NotNull Player player, @NotNull HuskSync plugin) {
final List<Attribute> attributes = Lists.newArrayList();
final AttributeSettings settings = plugin.getSettings().getSynchronization().getAttributes();
Registry.ATTRIBUTE.forEach(id -> {
final AttributeInstance instance = player.getAttribute(id);
if (instance == null || Double.compare(instance.getValue(), instance.getDefaultValue()) == 0
|| plugin.getSettings().getSynchronization().isIgnoredAttribute(id.getKey().toString())) {
// We don't sync unmodified or disabled attributes
return;
|| settings.isIgnoredAttribute(id.getKey().toString())) {
return; // We don't sync unmodified or disabled attributes
}
attributes.add(adapt(instance));
attributes.add(adapt(instance, settings));
});
return new BukkitData.Attributes(attributes);
}
@@ -603,11 +604,13 @@ public abstract class BukkitData implements Data {
}
@NotNull
private static Attribute adapt(@NotNull AttributeInstance instance) {
private static Attribute adapt(@NotNull AttributeInstance instance, @NotNull AttributeSettings settings) {
return new Attribute(
instance.getAttribute().getKey().toString(),
instance.getBaseValue(),
instance.getModifiers().stream().map(BukkitData.Attributes::adapt).collect(Collectors.toSet())
instance.getModifiers().stream()
.filter(modifier -> !settings.isIgnoredModifier(modifier.getName()))
.map(BukkitData.Attributes::adapt).collect(Collectors.toSet())
);
}
@@ -631,6 +634,15 @@ public abstract class BukkitData implements Data {
}
}
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) {
@@ -639,7 +651,12 @@ public abstract class BukkitData implements Data {
instance.setBaseValue(attribute == null ? instance.getDefaultValue() : attribute.baseValue());
instance.getModifiers().forEach(instance::removeModifier);
if (attribute != null) {
attribute.modifiers().forEach(modifier -> instance.addModifier(adapt(modifier, plugin)));
attribute.modifiers().stream()
.filter(mod -> instance.getModifiers().stream().map(AttributeModifier::getName)
.noneMatch(n -> n.equals(mod.name())))
.distinct()
.filter(mod -> useKeyedModifiers(plugin) == !mod.hasUuid())
.forEach(mod -> instance.addModifier(adapt(mod, plugin)));
}
}
@@ -647,20 +664,18 @@ public abstract class BukkitData implements Data {
@NotNull
private static AttributeModifier adapt(@NotNull Modifier modifier, @NotNull HuskSync plugin) {
final int slotId = modifier.equipmentSlot();
if (USE_KEYED_MODIFIERS == TriState.NOT_SET) {
USE_KEYED_MODIFIERS = TriState.byBoolean(plugin.getMinecraftVersion()
.compareTo(Version.fromString("1.21")) >= 0);
}
if (USE_KEYED_MODIFIERS == TriState.TRUE) {
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();
return AttributeModifier.class.getDeclaredConstructor(
NamespacedKey.class, double.class, AttributeModifier.Operation.class, slotGroup
).newInstance(
NamespacedKey.fromString(modifierName),
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)

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.PacketListenerPriority;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.google.common.collect.Sets;
import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder;
@@ -78,7 +79,20 @@ public class BukkitPacketEventsLockedPacketListener extends BukkitLockedEventLis
@Override
public void onPacketReceive(PacketReceiveEvent event) {
if(!(event.getPacketType() instanceof PacketType.Play.Client client)) {
if (!(event.getPacketType() instanceof PacketType.Play.Client 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;
}
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, "Successfully set " + args[0] + " to " +
obfuscateDataString(args[1]));
obfuscateDataString(args[1]));
} else {
plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
obfuscateDataString(args[1]) + " (is it a valid option?)");
obfuscateDataString(args[1]) + " (is it a valid option?)");
}
} else {
plugin.log(Level.INFO, getHelpMenu());

View File

@@ -201,10 +201,10 @@ public class MpdbMigrator extends Migrator {
}) {
plugin.log(Level.INFO, getHelpMenu());
plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
obfuscateDataString(args[1]));
obfuscateDataString(args[1]));
} else {
plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
obfuscateDataString(args[1]) + " (is it a valid option?)");
obfuscateDataString(args[1]) + " (is it a valid option?)");
}
} else {
plugin.log(Level.INFO, getHelpMenu());

View File

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

View File

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

View File

@@ -255,6 +255,14 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
@NotNull
String getPlatformType();
/**
* Returns the server software version
*
* @return the server software version string
*/
@NotNull
String getServerVersion();
/**
* Returns the legacy data converter if it exists
*
@@ -265,10 +273,10 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
@NotNull
default UpdateChecker getUpdateChecker() {
return UpdateChecker.builder()
.currentVersion(getPluginVersion())
.endpoint(UpdateChecker.Endpoint.SPIGOT)
.resource(Integer.toString(SPIGOT_RESOURCE_ID))
.build();
.currentVersion(getPluginVersion())
.endpoint(UpdateChecker.Endpoint.SPIGOT)
.resource(Integer.toString(SPIGOT_RESOURCE_ID))
.build();
}
default void checkForUpdates() {
@@ -276,8 +284,8 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
getUpdateChecker().check().thenAccept(checked -> {
if (!checked.isUpToDate()) {
log(Level.WARNING, String.format(
"A new version of HuskSync is available: v%s (running v%s)",
checked.getLatestVersion(), getPluginVersion())
"A new version of HuskSync is available: v%s (running v%s)",
checked.getLatestVersion(), getPluginVersion())
);
}
});
@@ -320,15 +328,15 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
final class FailedToLoadException extends IllegalStateException {
private static final String FORMAT = """
HuskSync has failed to load! The plugin will not be enabled and no data will be synchronized.
Please make sure the plugin has been setup correctly (https://william278.net/docs/husksync/setup):
1) Make sure you've entered your MySQL, MariaDB or MongoDB database details correctly in config.yml
2) Make sure your Redis server details are also correct in config.yml
3) Make sure your config is up-to-date (https://william278.net/docs/husksync/config-file)
4) Check the error below for more details
Caused by: %s""";
HuskSync has failed to load! The plugin will not be enabled and no data will be synchronized.
Please make sure the plugin has been setup correctly (https://william278.net/docs/husksync/setup):
1) Make sure you've entered your MySQL, MariaDB or MongoDB database details correctly in config.yml
2) Make sure your Redis server details are also correct in config.yml
3) Make sure your config is up-to-date (https://william278.net/docs/husksync/config-file)
4) Check the error below for more details
Caused by: %s""";
FailedToLoadException(@NotNull String message, @NotNull Throwable 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();
if (optionalEnderChest.isEmpty()) {
plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage);
.ifPresent(viewer::sendMessage);
return;
}
// Display opening message
plugin.getLocales().getLocale("ender_chest_viewer_opened", user.getUsername(),
snapshot.getTimestamp().format(DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
.ifPresent(viewer::sendMessage);
snapshot.getTimestamp().format(DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
.ifPresent(viewer::sendMessage);
// Show GUI
final Data.Items.EnderChest enderChest = optionalEnderChest.get();
viewer.showGui(
enderChest,
plugin.getLocales().getLocale("ender_chest_viewer_menu_title", user.getUsername())
.orElse(new MineDown(String.format("%s's Ender Chest", user.getUsername()))),
allowEdit,
enderChest.getSlotCount(),
(itemsOnClose) -> {
if (allowEdit && !enderChest.equals(itemsOnClose)) {
plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user));
enderChest,
plugin.getLocales().getLocale("ender_chest_viewer_menu_title", user.getUsername())
.orElse(new MineDown(String.format("%s's Ender Chest", user.getUsername()))),
allowEdit,
enderChest.getSlotCount(),
(itemsOnClose) -> {
if (allowEdit && !enderChest.equals(itemsOnClose)) {
plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user));
}
}
}
);
}
@@ -78,7 +78,7 @@ public class EnderChestCommand extends ItemsCommand {
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
if (latestData.isEmpty()) {
plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage);
.ifPresent(viewer::sendMessage);
return;
}
@@ -88,7 +88,7 @@ public class EnderChestCommand extends ItemsCommand {
data.getEnderChest().ifPresent(enderChest -> enderChest.setContents(items));
data.setSaveCause(DataSnapshot.SaveCause.ENDERCHEST_COMMAND);
data.setPinned(
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.ENDERCHEST_COMMAND)
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.ENDERCHEST_COMMAND)
);
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -64,7 +64,7 @@ public class Settings {
private boolean checkForUpdates = true;
@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 = "";
@Comment("Enable development debug logging")
@@ -229,7 +229,7 @@ public class Settings {
private boolean enabled = false;
@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;
@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")
private boolean compressData = true;
@Comment("Where to display sync notifications (ACTION_BAR, CHAT, TOAST or NONE)")
@Comment("Where to display sync notifications (ACTION_BAR, CHAT or NONE)")
private Locales.NotificationSlot notificationDisplaySlot = Locales.NotificationSlot.ACTION_BAR;
@Comment("Persist maps locked in a Cartography Table to let them be viewed on any server")
private boolean persistLockedMaps = true;
@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;
@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)")
private List<String> blacklistedCommandsWhileLocked = new ArrayList<>(List.of("*"));
@Comment({"For attribute syncing, which attributes should be ignored/skipped when syncing",
"(e.g. ['minecraft:generic.max_health', 'minecraft:generic.attack_damage'])"})
@Getter(AccessLevel.NONE)
private List<String> ignoredAttributes = new ArrayList<>(List.of(""));
@Comment("Configuration for how to sync attributes")
private AttributeSettings attributes = new AttributeSettings();
@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")
@Getter(AccessLevel.NONE)
@@ -284,10 +319,6 @@ public class Settings {
return id.isCustom() || features.getOrDefault(id.getKeyValue(), id.isEnabledByDefault());
}
public boolean isIgnoredAttribute(@NotNull String attribute) {
return ignoredAttributes.contains(attribute);
}
@NotNull
public EventListener.Priority getEventPriority(@NotNull EventListener.ListenerType type) {
try {

View File

@@ -366,7 +366,13 @@ public interface Data {
@Override
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) {
@@ -378,6 +384,10 @@ public interface Data {
};
}
public boolean hasUuid() {
return uuid != null;
}
@NotNull
public UUID uuid() {
return uuid != null ? uuid : UUID.nameUUIDFromBytes(name.getBytes());

View File

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

View File

@@ -535,9 +535,9 @@ public class DataSnapshot {
public Builder timestamp(@NotNull OffsetDateTime timestamp) {
if (timestamp.isAfter(OffsetDateTime.now())) {
throw new IllegalArgumentException("Data snapshots cannot have a timestamp set in the future! "
+ "Make sure your database server time matches the server time.\n"
+ "Current game server timestamp: " + OffsetDateTime.now() + " / "
+ "Snapshot timestamp: " + timestamp);
+ "Make sure your database server time matches the server time.\n"
+ "Current game server timestamp: " + OffsetDateTime.now() + " / "
+ "Snapshot timestamp: " + timestamp);
}
this.timestamp = timestamp;
return this;

View File

@@ -50,7 +50,8 @@ public class Identifier {
Dependency.optional("game_mode")
);
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,
Dependency.optional("attributes")

View File

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

View File

@@ -124,11 +124,11 @@ public class MySqlDatabase extends Database {
}
} catch (SQLException e) {
throw new IllegalStateException("Failed to create database tables. Please ensure you are running MySQL v8.0+ " +
"and that your connecting user account has privileges to create tables.", e);
"and that your connecting user account has privileges to create tables.", e);
}
} catch (SQLException | IOException e) {
throw new IllegalStateException("Failed to establish a connection to the MySQL database. " +
"Please check the supplied database credentials in the config file", e);
"Please check the supplied database credentials in the config file", e);
}
}

View File

@@ -123,11 +123,11 @@ public class PostgresDatabase extends Database {
}
} catch (SQLException e) {
throw new IllegalStateException("Failed to create database tables. Please ensure you are running PostgreSQL " +
"and that your connecting user account has privileges to create tables.", e);
"and that your connecting user account has privileges to create tables.", e);
}
} catch (SQLException | IOException e) {
throw new IllegalStateException("Failed to establish a connection to the PostgreSQL database. " +
"Please check the supplied database credentials in the config file", e);
"Please check the supplied database credentials in the config file", e);
}
}

View File

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

View File

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

View File

@@ -53,7 +53,7 @@ public abstract class EventListener {
return;
}
plugin.lockPlayer(user.getUuid());
plugin.getDataSyncer().setUserData(user);
plugin.getDataSyncer().syncApplyUserData(user);
}
/**
@@ -66,7 +66,7 @@ public abstract class EventListener {
return;
}
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) {
final SaveOnDeathSettings settings = plugin.getSettings().getSynchronization().getSaveOnDeath();
if (plugin.isDisabling() || !settings.isEnabled() || plugin.isLocked(user.getUuid())
|| user.isNpc() || (!settings.isSaveEmptyItems() && items.isEmpty())) {
|| user.isNpc() || (!settings.isSaveEmptyItems() && items.isEmpty())) {
return;
}

View File

@@ -92,7 +92,7 @@ public class RedisManager extends JedisPubSub {
jedisPool.getResource().ping();
} catch (JedisException e) {
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)
@@ -281,16 +281,21 @@ public class RedisManager extends JedisPubSub {
@Blocking
public void setUserCheckedOut(@NotNull User user, boolean checkedOut) {
try (Jedis jedis = jedisPool.getResource()) {
final String key = getKeyString(RedisKeyType.DATA_CHECKOUT, user.getUuid(), clusterId);
if (checkedOut) {
jedis.set(
getKey(RedisKeyType.DATA_CHECKOUT, user.getUuid(), clusterId),
key.getBytes(StandardCharsets.UTF_8),
plugin.getServerName().getBytes(StandardCharsets.UTF_8)
);
} 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(),
checkedOut ? "Set" : "Removed", RedisKeyType.DATA_CHECKOUT));
plugin.debug(String.format("[%s] %s %s key %s Redis (%s)", user.getUsername(),
checkedOut ? "Set" : "Removed", RedisKeyType.DATA_CHECKOUT, checkedOut ? "to" : "from", key));
} catch (Throwable 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) {
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
*/
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
*/
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,
@@ -150,7 +150,7 @@ public abstract class DataSyncer {
private long getMaxListenAttempts() {
return BASE_LISTEN_ATTEMPTS + (
(Math.max(100, plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds()) / 1000)
* 20 / LISTEN_DELAY
* 20 / LISTEN_DELAY
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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.Nullable;
import java.util.stream.Collectors;
public class FabricEventListener extends EventListener implements LockedHandler {
public FabricEventListener(@NotNull HuskSync plugin) {

View File

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

View File

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

View File

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

View File

@@ -19,18 +19,21 @@
package net.william278.husksync.util;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import net.william278.husksync.FabricHuskSync;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.UserDataHolder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.*;
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 {
@@ -46,7 +49,7 @@ public interface FabricTask extends Task {
@Override
public void run() {
if (!cancelled) {
Executors.newSingleThreadScheduledExecutor().schedule(
ASYNC_EXEC.schedule(
() -> ((FabricHuskSync) getPlugin()).getMinecraftServer().executeSync(runnable),
delayTicks * 50,
TimeUnit.MILLISECONDS
@@ -73,7 +76,7 @@ public interface FabricTask extends Task {
@Override
public void run() {
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
public void run() {
if (!cancelled) {
this.task = Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(
this.task = ASYNC_EXEC.scheduleAtFixedRate(
runnable,
0,
repeatingTicks * 50,
@@ -129,7 +132,7 @@ public interface FabricTask extends Task {
@Override
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
javaVersion=17
plugin_version=3.6.6
plugin_version=3.6.7
plugin_archive=husksync
plugin_description=A modern, cross-server player data synchronization system
jedis_version=5.1.3
mysql_driver_version=8.4.0
mariadb_driver_version=3.4.0
jedis_version=5.1.4
mysql_driver_version=9.0.0
mariadb_driver_version=3.4.1
postgres_driver_version=42.7.3
mongodb_driver_version=5.1.0
snappy_version=1.1.10.5
mongodb_driver_version=5.1.2
snappy_version=1.1.10.6
fabric_minecraft_version=1.20.1
fabric_loader_version=0.15.11

View File

@@ -34,7 +34,6 @@ shadowJar {
relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell'
relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown'
relocate 'net.william278.mapdataapi', 'net.william278.husksync.libraries.mapdataapi'
relocate 'net.william278.andjam', 'net.william278.husksync.libraries.andjam'
relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'
relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter'
relocate 'org.json', 'net.william278.husksync.libraries.json'