diff --git a/README.md b/README.md index a16d5043..dd092aa6 100644 --- a/README.md +++ b/README.md @@ -44,15 +44,15 @@ **Ready?** [It's syncing time!](https://william278.net/docs/husksync/setup) ## Setup -Requires a MySQL/Mongo/PostgreSQL database, a Redis (v5.0+) server and any number of Spigot-based 1.17.1+ Minecraft servers, running Java 17+. +Requires a MySQL/Mongo/PostgreSQL database, a Redis (v5.0+) server and a network of Spigot (1.17.1+) or Fabric (1.20.1) Minecraft servers, running Java 17+. -1. Place the plugin jar file in the /plugins/ directory of each Spigot server. You do not need to install HuskSync as a proxy plugin. +1. Place the plugin jar file in the `/plugins` or `/mods` directory of each Spigot/Fabric server. You do not need to install HuskSync as a proxy plugin. 2. Start, then stop every server to let HuskSync generate the config file. -3. Navigate to the HuskSync config file on each server (~/plugins/HuskSync/config.yml) and fill in both your database and Redis server credentials. +3. Navigate to the HuskSync config file on each server and fill in both your database and Redis server credentials. 4. Start every server again and synchronization will begin. ## Development -To build HuskSync, simply run the following in the root of the repository: +To build HuskSync, simply run the following in the root of the repository (building requires Java 17). Builds will be output in `/target`: ```bash ./gradlew clean build @@ -66,7 +66,7 @@ HuskSync is licensed under the Apache 2.0 license. Contributions to the project are welcome—feel free to open a pull request with new features, improvements and/or fixes! ### Support -Due to its complexity, official binaries and customer support for HuskSync is provided through a paid model. This means that support is only available to users who have purchased a license to the plugin from Spigot, Polymart, Craftaro, or BuiltByBit and have provided proof of purchase. Please join our Discord server if you have done so and need help! +Due to its complexity, official binaries and customer support for HuskSync is provided through a paid model. This means that support is only available to users who have purchased a license to the plugin from Spigot, Polymart, or BuiltByBit and have provided proof of purchase. Please join our Discord server if you have done so and need help! ### Translations Translations of the plugin locales are welcome to help make the plugin more accessible. Please submit a pull request with your translations as a `.yml` file. diff --git a/build.gradle b/build.gradle index ac5dff76..2c946fe9 100644 --- a/build.gradle +++ b/build.gradle @@ -69,6 +69,7 @@ allprojects { repositories { mavenLocal() mavenCentral() + maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' } maven { url "https://repo.dmulloy2.net/repository/public/" } maven { url 'https://repo.codemc.io/repository/maven-public/' } @@ -117,8 +118,13 @@ subprojects { archiveClassifier.set('') } + // Append the Minecraft to the version for Fabric projects + if (project.name == 'fabric') { + version += "+mc.${fabric_minecraft_version}" + } + // API publishing - if (['common', 'bukkit'].contains(project.name)) { + if (['common', 'bukkit', 'fabric'].contains(project.name)) { java { withSourcesJar() withJavadocJar() @@ -157,6 +163,19 @@ subprojects { } } } + + if (['fabric'].contains(project.name)) { + publications { + mavenJavaFabric(MavenPublication) { + groupId = 'net.william278.husksync' + artifactId = 'husksync-fabric' + version = "$rootProject.version" + artifact shadowJar + artifact sourcesJar + artifact javadocJar + } + } + } } } diff --git a/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java b/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java index 87467e95..48d96c49 100644 --- a/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java +++ b/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java @@ -150,15 +150,15 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S registerSerializer(Identifier.INVENTORY, new BukkitSerializer.Inventory(this)); registerSerializer(Identifier.ENDER_CHEST, new BukkitSerializer.EnderChest(this)); registerSerializer(Identifier.ADVANCEMENTS, new BukkitSerializer.Advancements(this)); - registerSerializer(Identifier.STATISTICS, new BukkitSerializer.Json<>(this, BukkitData.Statistics.class)); + registerSerializer(Identifier.STATISTICS, new Serializer.Json<>(this, BukkitData.Statistics.class)); registerSerializer(Identifier.POTION_EFFECTS, new BukkitSerializer.PotionEffects(this)); - registerSerializer(Identifier.GAME_MODE, new BukkitSerializer.Json<>(this, BukkitData.GameMode.class)); - registerSerializer(Identifier.FLIGHT_STATUS, new BukkitSerializer.Json<>(this, BukkitData.FlightStatus.class)); - registerSerializer(Identifier.ATTRIBUTES, new BukkitSerializer.Json<>(this, BukkitData.Attributes.class)); - registerSerializer(Identifier.HEALTH, new BukkitSerializer.Json<>(this, BukkitData.Health.class)); - registerSerializer(Identifier.HUNGER, new BukkitSerializer.Json<>(this, BukkitData.Hunger.class)); - registerSerializer(Identifier.EXPERIENCE, new BukkitSerializer.Json<>(this, BukkitData.Experience.class)); - registerSerializer(Identifier.LOCATION, new BukkitSerializer.Json<>(this, BukkitData.Location.class)); + registerSerializer(Identifier.GAME_MODE, new Serializer.Json<>(this, BukkitData.GameMode.class)); + registerSerializer(Identifier.FLIGHT_STATUS, new Serializer.Json<>(this, BukkitData.FlightStatus.class)); + registerSerializer(Identifier.ATTRIBUTES, new Serializer.Json<>(this, BukkitData.Attributes.class)); + registerSerializer(Identifier.HEALTH, new Serializer.Json<>(this, BukkitData.Health.class)); + registerSerializer(Identifier.HUNGER, new Serializer.Json<>(this, BukkitData.Hunger.class)); + registerSerializer(Identifier.EXPERIENCE, new Serializer.Json<>(this, BukkitData.Experience.class)); + registerSerializer(Identifier.LOCATION, new Serializer.Json<>(this, BukkitData.Location.class)); validateDependencies(); }); diff --git a/bukkit/src/main/java/net/william278/husksync/data/BukkitData.java b/bukkit/src/main/java/net/william278/husksync/data/BukkitData.java index 9acd06e0..ce61087b 100644 --- a/bukkit/src/main/java/net/william278/husksync/data/BukkitData.java +++ b/bukkit/src/main/java/net/william278/husksync/data/BukkitData.java @@ -69,7 +69,6 @@ public abstract class BukkitData implements Data { private final @Nullable ItemStack @NotNull [] contents; private Items(@Nullable ItemStack @NotNull [] contents) { - this.contents = Arrays.stream(contents.clone()) .map(i -> i == null || i.getType() == Material.AIR ? null : i) .toArray(ItemStack[]::new); @@ -127,8 +126,6 @@ public abstract class BukkitData implements Data { @Getter public static class Inventory extends BukkitData.Items implements Data.Items.Inventory { - public static final int INVENTORY_SLOT_COUNT = 41; - @Range(from = 0, to = 8) private int heldItemSlot; @@ -175,15 +172,18 @@ public abstract class BukkitData implements Data { public static class EnderChest extends BukkitData.Items implements Data.Items.EnderChest { - public static final int ENDER_CHEST_SLOT_COUNT = 27; - - private EnderChest(@NotNull ItemStack[] contents) { + private EnderChest(@Nullable ItemStack @NotNull [] contents) { super(contents); } @NotNull - public static BukkitData.Items.EnderChest adapt(@NotNull ItemStack[] items) { - return new BukkitData.Items.EnderChest(items); + public static BukkitData.Items.EnderChest adapt(@Nullable ItemStack @NotNull [] contents) { + return new BukkitData.Items.EnderChest(contents); + } + + @NotNull + public static BukkitData.Items.EnderChest adapt(@NotNull Collection items) { + return adapt(items.toArray(ItemStack[]::new)); } @NotNull @@ -200,7 +200,7 @@ public abstract class BukkitData implements Data { public static class ItemArray extends BukkitData.Items implements Data.Items { - private ItemArray(@NotNull ItemStack[] contents) { + private ItemArray(@Nullable ItemStack @NotNull [] contents) { super(contents); } @@ -210,7 +210,7 @@ public abstract class BukkitData implements Data { } @NotNull - public static ItemArray adapt(@NotNull ItemStack[] drops) { + public static ItemArray adapt(@Nullable ItemStack @NotNull [] drops) { return new ItemArray(drops); } @@ -341,9 +341,12 @@ public abstract class BukkitData implements Data { })); } - private void setAdvancement(@NotNull HuskSync plugin, @NotNull org.bukkit.advancement.Advancement advancement, - @NotNull Player player, @NotNull BukkitUser user, - @NotNull Collection toAward, @NotNull Collection toRevoke) { + private void setAdvancement(@NotNull HuskSync plugin, + @NotNull org.bukkit.advancement.Advancement advancement, + @NotNull Player player, + @NotNull BukkitUser user, + @NotNull Collection toAward, + @NotNull Collection toRevoke) { plugin.runSync(() -> { // Track player exp level & progress final int expLevel = player.getLevel(); @@ -355,7 +358,8 @@ public abstract class BukkitData implements Data { toRevoke.forEach(progress::revokeCriteria); // Set player experience and level (prevent advancement awards applying twice), reset game rule - if (!toAward.isEmpty() && player.getLevel() != expLevel || player.getExp() != expProgress) { + if (!toAward.isEmpty() + && (player.getLevel() != expLevel || player.getExp() != expProgress)) { player.setLevel(expLevel); player.setExp(expProgress); } diff --git a/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java b/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java index ef3bd752..97b14a0f 100644 --- a/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java +++ b/bukkit/src/main/java/net/william278/husksync/data/BukkitSerializer.java @@ -29,6 +29,7 @@ import de.tr7zw.changeme.nbtapi.utils.DataFixerUtil; import lombok.AccessLevel; import lombok.AllArgsConstructor; 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.api.HuskSyncAPI; @@ -41,6 +42,8 @@ import org.jetbrains.annotations.Nullable; import java.util.List; import static net.william278.husksync.data.BukkitData.Items.Inventory.INVENTORY_SLOT_COUNT; +import static net.william278.husksync.data.Data.Items.Inventory.HELD_ITEM_SLOT_TAG; +import static net.william278.husksync.data.Data.Items.Inventory.ITEMS_TAG; @AllArgsConstructor(access = AccessLevel.PRIVATE) public class BukkitSerializer { @@ -60,8 +63,6 @@ public class BukkitSerializer { public static class Inventory extends BukkitSerializer implements Serializer, ItemDeserializer { - private static final String ITEMS_TAG = "items"; - private static final String HELD_ITEM_SLOT_TAG = "held_item_slot"; public Inventory(@NotNull HuskSync plugin) { super(plugin); @@ -74,7 +75,7 @@ public class BukkitSerializer { final ReadWriteNBT items = root.hasTag(ITEMS_TAG) ? root.getCompound(ITEMS_TAG) : null; return BukkitData.Items.Inventory.from( items != null ? getItems(items, dataMcVersion) : new ItemStack[INVENTORY_SLOT_COUNT], - root.getInteger(HELD_ITEM_SLOT_TAG) + root.hasTag(HELD_ITEM_SLOT_TAG) ? root.getInteger(HELD_ITEM_SLOT_TAG) : 0 ); } @@ -126,15 +127,15 @@ public class BukkitSerializer { @Nullable default ItemStack[] getItems(@NotNull ReadWriteNBT tag, @NotNull Version mcVersion) { if (mcVersion.compareTo(getPlugin().getMinecraftVersion()) < 0) { - return upgradeItemStack((NBTCompound) tag, mcVersion); + return upgradeItemStacks((NBTCompound) tag, mcVersion); } return NBT.itemStackArrayFromNBT(tag); } @NotNull - private ItemStack @NotNull [] upgradeItemStack(@NotNull NBTCompound compound, @NotNull Version mcVersion) { - final ReadWriteNBTCompoundList items = compound.getCompoundList("items"); - final ItemStack[] itemStacks = new ItemStack[compound.getInteger("size")]; + private ItemStack @NotNull [] upgradeItemStacks(@NotNull NBTCompound itemsNbt, @NotNull Version mcVersion) { + final ReadWriteNBTCompoundList items = itemsNbt.getCompoundList("items"); + final ItemStack[] itemStacks = new ItemStack[itemsNbt.getInteger("size")]; for (int i = 0; i < items.size(); i++) { if (items.get(i) == null) { itemStacks[i] = new ItemStack(Material.AIR); @@ -163,6 +164,7 @@ public class BukkitSerializer { case "1.19", "1.19.1", "1.19.2" -> DataFixerUtil.VERSION1_19_2; case "1.20", "1.20.1", "1.20.2" -> DataFixerUtil.VERSION1_20_2; case "1.20.3", "1.20.4" -> DataFixerUtil.VERSION1_20_4; + case "1.20.5", "1.20.6" -> DataFixerUtil.VERSION1_20_5; default -> DataFixerUtil.getCurrentVersion(); }; } @@ -237,24 +239,19 @@ public class BukkitSerializer { } - public static class Json extends BukkitSerializer implements Serializer { + /** + * @deprecated Use {@link Serializer.Json} in the common module instead + */ + @Deprecated(since = "2.6") + public class Json extends Serializer.Json { - private final Class type; - - public Json(@NotNull HuskSync plugin, Class type) { - super(plugin); - this.type = type; - } - - @Override - public T deserialize(@NotNull String serialized) throws DeserializationException { - return plugin.getDataAdapter().fromJson(serialized, type); + public Json(@NotNull HuskSync plugin, @NotNull Class type) { + super(plugin, type); } @NotNull - @Override - public String serialize(@NotNull T element) throws SerializationException { - return plugin.getDataAdapter().toJson(element); + public BukkitHuskSync getPlugin() { + return (BukkitHuskSync) plugin; } } diff --git a/bukkit/src/main/java/net/william278/husksync/data/BukkitUserDataHolder.java b/bukkit/src/main/java/net/william278/husksync/data/BukkitUserDataHolder.java index da51ff32..9eb93aaf 100644 --- a/bukkit/src/main/java/net/william278/husksync/data/BukkitUserDataHolder.java +++ b/bukkit/src/main/java/net/william278/husksync/data/BukkitUserDataHolder.java @@ -67,9 +67,9 @@ public interface BukkitUserDataHolder extends UserDataHolder { .isSyncDeadPlayersChangingServer())) { return Optional.of(BukkitData.Items.Inventory.empty()); } - final PlayerInventory inventory = getBukkitPlayer().getInventory(); + final PlayerInventory inventory = getPlayer().getInventory(); return Optional.of(BukkitData.Items.Inventory.from( - getMapPersister().persistLockedMaps(inventory.getContents(), getBukkitPlayer()), + getMapPersister().persistLockedMaps(inventory.getContents(), getPlayer()), inventory.getHeldItemSlot() )); } @@ -78,80 +78,89 @@ public interface BukkitUserDataHolder extends UserDataHolder { @Override default Optional getEnderChest() { return Optional.of(BukkitData.Items.EnderChest.adapt( - getMapPersister().persistLockedMaps(getBukkitPlayer().getEnderChest().getContents(), getBukkitPlayer()) + getMapPersister().persistLockedMaps(getPlayer().getEnderChest().getContents(), getPlayer()) )); } @NotNull @Override default Optional getPotionEffects() { - return Optional.of(BukkitData.PotionEffects.from(getBukkitPlayer().getActivePotionEffects())); + return Optional.of(BukkitData.PotionEffects.from(getPlayer().getActivePotionEffects())); } @NotNull @Override default Optional getAdvancements() { - return Optional.of(BukkitData.Advancements.adapt(getBukkitPlayer())); + return Optional.of(BukkitData.Advancements.adapt(getPlayer())); } @NotNull @Override default Optional getLocation() { - return Optional.of(BukkitData.Location.adapt(getBukkitPlayer().getLocation())); + return Optional.of(BukkitData.Location.adapt(getPlayer().getLocation())); } @NotNull @Override default Optional getStatistics() { - return Optional.of(BukkitData.Statistics.adapt(getBukkitPlayer())); + return Optional.of(BukkitData.Statistics.adapt(getPlayer())); } @NotNull @Override default Optional getHealth() { - return Optional.of(BukkitData.Health.adapt(getBukkitPlayer())); + return Optional.of(BukkitData.Health.adapt(getPlayer())); } @NotNull @Override default Optional getHunger() { - return Optional.of(BukkitData.Hunger.adapt(getBukkitPlayer())); + return Optional.of(BukkitData.Hunger.adapt(getPlayer())); } @NotNull @Override default Optional getAttributes() { - return Optional.of(BukkitData.Attributes.adapt(getBukkitPlayer(), getPlugin())); + return Optional.of(BukkitData.Attributes.adapt(getPlayer(), getPlugin())); } @NotNull @Override default Optional getExperience() { - return Optional.of(BukkitData.Experience.adapt(getBukkitPlayer())); + return Optional.of(BukkitData.Experience.adapt(getPlayer())); } @NotNull @Override default Optional getGameMode() { - return Optional.of(BukkitData.GameMode.adapt(getBukkitPlayer())); + return Optional.of(BukkitData.GameMode.adapt(getPlayer())); } @NotNull @Override default Optional getFlightStatus() { - return Optional.of(BukkitData.FlightStatus.adapt(getBukkitPlayer())); + return Optional.of(BukkitData.FlightStatus.adapt(getPlayer())); } @NotNull @Override default Optional getPersistentData() { - return Optional.of(BukkitData.PersistentData.adapt(getBukkitPlayer().getPersistentDataContainer())); + return Optional.of(BukkitData.PersistentData.adapt(getPlayer().getPersistentDataContainer())); } boolean isDead(); @NotNull - Player getBukkitPlayer(); + Player getPlayer(); + + /** + * @deprecated Use {@link #getPlayer()} instead + */ + @Deprecated(since = "3.6") + @NotNull + default Player getBukkitPlayer() { + return getPlayer(); + } @NotNull default BukkitMapPersister getMapPersister() { diff --git a/bukkit/src/main/java/net/william278/husksync/user/BukkitUser.java b/bukkit/src/main/java/net/william278/husksync/user/BukkitUser.java index b2218ca8..3be35d81 100644 --- a/bukkit/src/main/java/net/william278/husksync/user/BukkitUser.java +++ b/bukkit/src/main/java/net/william278/husksync/user/BukkitUser.java @@ -62,17 +62,6 @@ public class BukkitUser extends OnlineUser implements BukkitUserDataHolder { return new BukkitUser(player, plugin); } - /** - * Get the Bukkit {@link Player} instance of this user - * - * @return the {@link Player} instance - * @since 3.0 - */ - @NotNull - public Player getPlayer() { - return player; - } - @Override public boolean isOffline() { return player == null || !player.isOnline(); @@ -132,9 +121,14 @@ public class BukkitUser extends OnlineUser implements BukkitUserDataHolder { return player.hasMetadata("NPC"); } + /** + * Get the Bukkit {@link Player} instance of this user + * + * @return the {@link Player} instance + * @since 3.6 + */ @NotNull - @Override - public Player getBukkitPlayer() { + public Player getPlayer() { return player; } diff --git a/common/build.gradle b/common/build.gradle index a81fc971..6b567862 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -5,6 +5,7 @@ plugins { dependencies { api 'commons-io:commons-io:2.16.1' api 'org.apache.commons:commons-text:1.12.0' + api 'org.apache.commons:commons-pool2:2.12.0' api 'net.william278:minedown:1.8.2' api 'org.json:json:20240303' api 'com.google.code.gson:gson:2.11.0' diff --git a/common/src/main/java/net/william278/husksync/HuskSync.java b/common/src/main/java/net/william278/husksync/HuskSync.java index ecc68d8e..189e8cc2 100644 --- a/common/src/main/java/net/william278/husksync/HuskSync.java +++ b/common/src/main/java/net/william278/husksync/HuskSync.java @@ -43,7 +43,6 @@ import net.william278.husksync.util.LegacyConverter; import net.william278.husksync.util.Task; import org.jetbrains.annotations.NotNull; -import java.io.File; import java.io.InputStream; import java.text.SimpleDateFormat; import java.util.*; @@ -86,7 +85,6 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider * * @return the {@link RedisManager} implementation */ - @NotNull RedisManager getRedisManager(); @@ -122,7 +120,17 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider List getAvailableMigrators(); @NotNull - Map getPlayerCustomDataStore(@NotNull OnlineUser user); + Map> getPlayerCustomDataStore(); + + @NotNull + default Map getPlayerCustomDataStore(@NotNull OnlineUser user) { + if (getPlayerCustomDataStore().containsKey(user.getUuid())) { + return getPlayerCustomDataStore().get(user.getUuid()); + } + final Map data = new HashMap<>(); + getPlayerCustomDataStore().put(user.getUuid(), data); + return data; + } /** * Initialize a faucet of the plugin. @@ -156,14 +164,6 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider */ InputStream getResource(@NotNull String name); - /** - * Returns the plugin data folder - * - * @return the plugin data folder as a {@link File} - */ - @NotNull - File getDataFolder(); - /** * Log a message to the console * diff --git a/common/src/main/java/net/william278/husksync/command/Command.java b/common/src/main/java/net/william278/husksync/command/Command.java index 37bfac63..48a802fa 100644 --- a/common/src/main/java/net/william278/husksync/command/Command.java +++ b/common/src/main/java/net/william278/husksync/command/Command.java @@ -51,6 +51,16 @@ public abstract class Command extends Node { public abstract void execute(@NotNull CommandUser executor, @NotNull String[] args); + @NotNull + protected String[] removeFirstArg(@NotNull String[] args) { + if (args.length <= 1) { + return new String[0]; + } + String[] newArgs = new String[args.length - 1]; + System.arraycopy(args, 1, newArgs, 0, args.length - 1); + return newArgs; + } + @NotNull public final String getRawUsage() { return usage; diff --git a/common/src/main/java/net/william278/husksync/command/HuskSyncCommand.java b/common/src/main/java/net/william278/husksync/command/HuskSyncCommand.java index f7f96679..ecd09b4e 100644 --- a/common/src/main/java/net/william278/husksync/command/HuskSyncCommand.java +++ b/common/src/main/java/net/william278/husksync/command/HuskSyncCommand.java @@ -68,7 +68,9 @@ public class HuskSyncCommand extends Command implements TabProvider { .credits("Contributors", AboutMenu.Credit.of("HarvelsX").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("Stampede2011").description("Code (Fabric mixins)")) .credits("Translators", AboutMenu.Credit.of("Namiu").description("Japanese (ja-jp)"), AboutMenu.Credit.of("anchelthe").description("Spanish (es-es)"), diff --git a/common/src/main/java/net/william278/husksync/command/InventoryCommand.java b/common/src/main/java/net/william278/husksync/command/InventoryCommand.java index ad710147..5dc3847d 100644 --- a/common/src/main/java/net/william278/husksync/command/InventoryCommand.java +++ b/common/src/main/java/net/william278/husksync/command/InventoryCommand.java @@ -45,6 +45,7 @@ public class InventoryCommand extends ItemsCommand { @NotNull User user, boolean allowEdit) { final Optional optionalInventory = snapshot.getInventory(); if (optionalInventory.isEmpty()) { + viewer.sendMessage(new MineDown("what the FUCK is happening")); plugin.getLocales().getLocale("error_no_data_to_display") .ifPresent(viewer::sendMessage); return; diff --git a/common/src/main/java/net/william278/husksync/data/Data.java b/common/src/main/java/net/william278/husksync/data/Data.java index fc4de513..7cba6059 100644 --- a/common/src/main/java/net/william278/husksync/data/Data.java +++ b/common/src/main/java/net/william278/husksync/data/Data.java @@ -78,6 +78,7 @@ public interface Data { */ interface Inventory extends Items { + int INVENTORY_SLOT_COUNT = 41; String ITEMS_TAG = "items"; String HELD_ITEM_SLOT_TAG = "held_item_slot"; @@ -110,7 +111,7 @@ public interface Data { * Data container holding data for ender chests */ interface EnderChest extends Items { - + int ENDER_CHEST_SLOT_COUNT = 27; } } diff --git a/common/src/main/java/net/william278/husksync/data/DataHolder.java b/common/src/main/java/net/william278/husksync/data/DataHolder.java index 752ffe79..b901ba4f 100644 --- a/common/src/main/java/net/william278/husksync/data/DataHolder.java +++ b/common/src/main/java/net/william278/husksync/data/DataHolder.java @@ -30,8 +30,8 @@ public interface DataHolder { @NotNull Map getData(); - default Optional getData(@NotNull Identifier identifier) { - return Optional.ofNullable(getData().get(identifier)); + default Optional getData(@NotNull Identifier id) { + return getData().entrySet().stream().filter(e -> e.getKey().equals(id)).map(Map.Entry::getValue).findFirst(); } default void setData(@NotNull Identifier identifier, @NotNull Data data) { diff --git a/common/src/main/java/net/william278/husksync/data/Serializer.java b/common/src/main/java/net/william278/husksync/data/Serializer.java index 9b4a12b0..c8c8bd4e 100644 --- a/common/src/main/java/net/william278/husksync/data/Serializer.java +++ b/common/src/main/java/net/william278/husksync/data/Serializer.java @@ -20,6 +20,8 @@ package net.william278.husksync.data; import net.william278.desertwell.util.Version; +import net.william278.husksync.HuskSync; +import net.william278.husksync.adapter.Adaptable; import org.jetbrains.annotations.NotNull; public interface Serializer { @@ -46,4 +48,26 @@ public interface Serializer { } + class Json implements Serializer { + + private final HuskSync plugin; + private final Class type; + + public Json(@NotNull HuskSync plugin, @NotNull Class type) { + this.type = type; + this.plugin = plugin; + } + + @Override + public T deserialize(@NotNull String serialized) throws DeserializationException { + return plugin.getDataAdapter().fromJson(serialized, type); + } + + @NotNull + @Override + public String serialize(@NotNull T element) throws SerializationException { + return plugin.getDataAdapter().toJson(element); + } + + } } diff --git a/common/src/main/java/net/william278/husksync/event/Cancellable.java b/common/src/main/java/net/william278/husksync/event/Cancellable.java index ef0becd5..156e9f96 100644 --- a/common/src/main/java/net/william278/husksync/event/Cancellable.java +++ b/common/src/main/java/net/william278/husksync/event/Cancellable.java @@ -19,7 +19,6 @@ package net.william278.husksync.event; -@SuppressWarnings("unused") public interface Cancellable extends Event { default boolean isCancelled() { diff --git a/common/src/main/java/net/william278/husksync/event/PreSyncEvent.java b/common/src/main/java/net/william278/husksync/event/PreSyncEvent.java index c47352d5..f43c58db 100644 --- a/common/src/main/java/net/william278/husksync/event/PreSyncEvent.java +++ b/common/src/main/java/net/william278/husksync/event/PreSyncEvent.java @@ -27,7 +27,7 @@ import org.jetbrains.annotations.NotNull; import java.util.function.Consumer; @SuppressWarnings("unused") -public interface PreSyncEvent extends PlayerEvent { +public interface PreSyncEvent extends PlayerEvent, Cancellable { @NotNull DataSnapshot.Packed getData(); diff --git a/common/src/main/java/net/william278/husksync/sync/LockstepDataSyncer.java b/common/src/main/java/net/william278/husksync/sync/LockstepDataSyncer.java index b51485a8..dbafcdae 100644 --- a/common/src/main/java/net/william278/husksync/sync/LockstepDataSyncer.java +++ b/common/src/main/java/net/william278/husksync/sync/LockstepDataSyncer.java @@ -59,16 +59,13 @@ public class LockstepDataSyncer extends DataSyncer { @Override public void saveUserData(@NotNull OnlineUser onlineUser) { - plugin.runAsync(() -> { - getRedis().setUserServerSwitch(onlineUser); - saveData( - onlineUser, onlineUser.createSnapshot(DataSnapshot.SaveCause.DISCONNECT), - (user, data) -> { - getRedis().setUserData(user, data, RedisKeyType.TTL_1_YEAR); - getRedis().setUserCheckedOut(user, false); - } - ); - }); + plugin.runAsync(() -> saveData( + onlineUser, onlineUser.createSnapshot(DataSnapshot.SaveCause.DISCONNECT), + (user, data) -> { + getRedis().setUserData(user, data, RedisKeyType.TTL_1_YEAR); + getRedis().setUserCheckedOut(user, false); + } + )); } } diff --git a/common/src/main/java/net/william278/husksync/util/DataDumper.java b/common/src/main/java/net/william278/husksync/util/DataDumper.java index f0f510a1..1cb0495b 100644 --- a/common/src/main/java/net/william278/husksync/util/DataDumper.java +++ b/common/src/main/java/net/william278/husksync/util/DataDumper.java @@ -31,6 +31,8 @@ import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.time.format.DateTimeFormatter; import java.util.Locale; import java.util.StringJoiner; @@ -133,16 +135,13 @@ public class DataDumper { */ @NotNull public String toFile() throws IOException { - final File filePath = getFilePath(); - - // Write the data from #getString to the file using a writer - try (final FileWriter writer = new FileWriter(filePath, StandardCharsets.UTF_8, false)) { - writer.write(toString()); + final Path filePath = getFilePath(); + try (final FileWriter writer = new FileWriter(filePath.toFile(), StandardCharsets.UTF_8, false)) { + writer.write(toString()); // Write the data from #getString to the file using a writer + return filePath.toString(); } catch (IOException e) { throw new IOException("Failed to write data to file", e); } - - return "~/plugins/HuskSync/dumps/" + filePath.getName(); } /** @@ -152,8 +151,8 @@ public class DataDumper { * @throws IOException if the prerequisite dumps parent folder could not be created */ @NotNull - private File getFilePath() throws IOException { - return new File(getDumpsFolder(), getFileName()); + private Path getFilePath() throws IOException { + return getDumpsFolder().resolve(getFileName()); } /** @@ -163,14 +162,12 @@ public class DataDumper { * @throws IOException if the folder could not be created */ @NotNull - private File getDumpsFolder() throws IOException { - final File dumpsFolder = new File(plugin.getDataFolder(), "dumps"); - if (!dumpsFolder.exists()) { - if (!dumpsFolder.mkdirs()) { - throw new IOException("Failed to create user data dumps folder"); - } + private Path getDumpsFolder() throws IOException { + final Path dumps = plugin.getConfigDirectory().resolve("dumps"); + if (!Files.exists(dumps)) { + Files.createDirectory(dumps); } - return dumpsFolder; + return dumps; } /** diff --git a/docs/API-Events.md b/docs/API-Events.md index ecb7bcbe..6f0b3ad0 100644 --- a/docs/API-Events.md +++ b/docs/API-Events.md @@ -4,11 +4,20 @@ Consult the Javadocs for more information. Please note that carrying out expensi ## Bukkit Platform Events > **Tip:** Don't forget to register your listener when listening for these event calls. -> + | Bukkit Event class | Cancellable | Description | |---------------------------|:-----------:|---------------------------------------------------------------------------------------------| | `BukkitDataSaveEvent` | ✅ | Called when player data snapshot is created, saved and cached due to a DataSaveCause | | `BukkitPreSyncEvent` | ✅ | Called before a player has their data updated from the cache or database, just after login | | `BukkitSyncCompleteEvent` | ❌ | Called once a player has completed their data synchronization on login successfully† | +## Fabric Platform Callbacks +> Access the callback via the static EVENT field in each interface class. + +| Fabric Callback | Cancellable | Description | +|------------------------------|:-----------:|---------------------------------------------------------------------------------------------| +| `FabricDataSaveCallback` | ✅ | Called when player data snapshot is created, saved and cached due to a DataSaveCause | +| `FabricPreSyncCallback` | ✅ | Called before a player has their data updated from the cache or database, just after login | +| `FabricSyncCompleteCallback` | ❌ | Called once a player has completed their data synchronization on login successfully† | + †This can also fire when a user's data is updated while the player is logged in; i.e., when an admin rolls back the user, updates their inventory or Ender Chest through the respective commands, or when an API call is made forcing the user to have their data updated. diff --git a/docs/API.md b/docs/API.md index b5642ad6..e6d3a973 100644 --- a/docs/API.md +++ b/docs/API.md @@ -17,6 +17,7 @@ The HuskSync API shares version numbering with the plugin itself for consistency The HuskSync API is available for the following platforms: * `bukkit` - Bukkit, Spigot, Paper, etc. Provides Bukkit API event listeners and adapters to `org.bukkit` objects. +* `fabric` - Fabric API for Minecraft. Provides Fabric API event listeners and adapters to `net.minecraft` objects. * `common` - Common API for all platforms. diff --git a/docs/FAQs.md b/docs/FAQs.md index 13480682..bf48aa44 100644 --- a/docs/FAQs.md +++ b/docs/FAQs.md @@ -12,23 +12,29 @@ HuskSync supports synchronising a wide range of different data elements, each of
 Are modded items supported? -If you're running HuskSync on Arclight or similar, please note we will not be able to provide you with support, but have been reported to save & sync correctly with HuskSync v3.x+. +On Fabric, modded items should usually sync as you would expect with HuskSync. Note that mods which store additional data separate from item NBT on each server may not work as expected. Mod developers — check out the [[Custom Data API]] for information on how to get your mod's data syncing! -**TL;DR** — modded items may work, but since we can't guarantee compatibility, we do not officially mark them as supported. Be sure to test thoroughly before deploying on production! +On Spigot, if you're running HuskSync on Arclight or similar, please note we will not be able to provide you with support, but have been reported to save & sync correctly with HuskSync v3.x+. + +Please note we cannot guarantee compatibility with everything — test thoroughly!
 Are MMOItems / SlimeFun / ItemsAdder items supported? -These plugins, which provide custom items, should be supported as of HuskSync v3.x+; but do note we cannot guarantee compatibility with all methods of injecting custom data to create custom items. Be sure to test thoroughly before deploying on production! +These custom item Spigot plugins should work as expected provided they inject data into item NBT in a standard way. + +Please note we cannot guarantee compatibility with everything — test thoroughly!
 Is Redis required? What is Redis? -HuskSync requires Redis to operate (for reasons demonstrated below). Redis is an in-memory database server used for caching data at scale and sending messages across a network. You have a Redis server in a similar fashion to the way you have a MySQL database server. If you're using a Minecraft hosting company, you'll want to contact their support and ask if they offer Redis. If you're looking for a host, I have a list of some popular hosts and whether they support Redis [available to read here.](https://william278.net/redis-hosts) +Yes! HuskSync requires Redis to operate (for reasons demonstrated below). + +Redis is an in-memory database server used for caching data at scale and sending messages across a network. You have a Redis server in a similar fashion to the way you have a MySQL database server. If you're using a Minecraft hosting company, you'll want to contact their support and ask if they offer Redis. If you're looking for a host, I have a list of some popular hosts and whether they support Redis [available to read here.](https://william278.net/redis-hosts)
diff --git a/docs/Home.md b/docs/Home.md index 9b8eaa13..e148f287 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -30,5 +30,5 @@ Welcome! This is the plugin documentation for HuskSync v3.x+. Please click throu * 📂 [Buy HuskSync](https://william278.net/project/husksync/) * 🚰 [Spigot](https://www.spigotmc.org/resources/husksync.97144/) * 🛒 [Polymart](https://polymart.org/resource/husksync.1634) - * ⚒️ [Craftaro](https://craftaro.com/marketplace/product/husksync.758) + * ⚒️ [BuiltByBit](https://craftaro.com/marketplace/product/husksync.758) * 💬 [Discord Support](https://discord.gg/tVYhJfyDWG) \ No newline at end of file diff --git a/docs/Setup.md b/docs/Setup.md index 1774e263..01caac52 100644 --- a/docs/Setup.md +++ b/docs/Setup.md @@ -1,25 +1,31 @@ -This will walk you through installing HuskSync on your network of Spigot servers. +> **Warning:** Fabric support is currently in beta and is not production ready yet. Customers can get in touch on Discord to request the Fabric build, or you can self-compile. + +This will walk you through installing HuskSync on your network of Spigot or Fabric servers. ## Requirements -> **Note:** If the plugin fails to load, please check that you are not running an [incompatible version combination](Unsupported-Versions) +> **Warning:** Mixing and matching Fabric/Spigot servers is not supported, and all servers must be running the same Minecraft version. + +> **Note:** Please also note some specific legacy Paper/Purpur versions are [not compatible](Unsupported-Versions) with HuskSync. -* A MySQL Database (v8.0+) (MariaDB, PostrgreSQL or MongoDB are also supported) +* A MySQL Database (v8.0+) + * **OR** a MariaDB, PostrgreSQL or MongoDB database, which are also supported * A Redis Database (v5.0+) — see [[FAQs]] for more details. * Any number of Spigot servers, connected by a BungeeCord or Velocity-based proxy (Minecraft v1.17.1+, running Java 17+) + * **OR** a network of Fabric servers, connected by a Fabric proxy (Minecraft v1.20.1, running Java 17+) ## Setup Instructions ### 1. Install the jar -- Place the plugin jar file in the `/plugins/` directory of each Spigot server. +- Place the plugin jar file in the `/plugins/` or `/mods/` directory of each Spigot/Fabric server respectively. - You do not need to install HuskSync as a proxy plugin. -- You can additionally install [ProtocolLib](https://www.spigotmc.org/resources/protocollib.1997/) or [PacketEvents](https://www.spigotmc.org/resources/packetevents-api.80279/) for better locked user handling, and [Plan](https://www.spigotmc.org/resources/plan-player-analytics.32536/) for analytics. +- _Spigot users_: You can additionally install [ProtocolLib](https://www.spigotmc.org/resources/protocollib.1997/) or [PacketEvents](https://www.spigotmc.org/resources/packetevents-api.80279/) for better locked user handling. +- _Fabric users_: Ensure the latest Fabric API mod jar is installed! ### 2. Restart servers - Start, then stop every server to let HuskSync generate the [[config file]]. - HuskSync will throw an error in the console and disable itself as it is unable to connect to the database. You haven't set the credentials yet, so this is expected. -- Advanced users: If you'd prefer, you can create one config.yml file and create symbolic links in each `/plugins/HuskSync/` folder to it to make updating it easier. ### 3. Enter Mysql & Redis database credentials -- Navigate to the HuskSync config file on each server (`~/plugins/HuskSync/config.yml`) +- Navigate to the new config file on each server (`~/plugins/HuskSync/config.yml` on Spigot, `~/config/husksync/config.yml` on Fabric) - Under `credentials` in the `database` section, enter the credentials of your (MySQL/MariaDB/MongoDB/PostgreSQL) Database. You shouldn't touch the `connection_pool` properties. - Under `credentials` in the `redis` section, enter the credentials of your Redis Database. If your Redis server doesn't have a password, leave the password blank as it is. - Unless you want to have multiple clusters of servers within your network, each with separate user data, you should not change the value of `cluster_id`. @@ -44,7 +50,7 @@ This will walk you through installing HuskSync on your network of Spigot servers ### 4. Set server names in server.yml files -- Navigate to the HuskSync server name file on each server (`~/plugins/HuskSync/server.yml`) +- Navigate to the server name file on each server (`~/plugins/HuskSync/server.yml` on Spigot, `~/config/husksync/server.yml` on Fabric) - Set the `name:` of the server in this file to the ID of this server as defined in the config of your proxy (e.g., if this is the "hub" server you access with `/server hub`, put `'hub'` here) ### 5. Start every server again diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md index b184ba3d..290984e1 100644 --- a/docs/_Sidebar.md +++ b/docs/_Sidebar.md @@ -27,6 +27,5 @@ * 📂 [Buy HuskSync](https://william278.net/project/husksync/) * 🚰 [Spigot](https://www.spigotmc.org/resources/husksync.97144/) * 🛒 [Polymart](https://polymart.org/resource/husksync.1634) - * ⚒️ [Craftaro](https://craftaro.com/marketplace/product/husksync.758) - * 🛒 [BuiltByBit](https://craftaro.com/marketplace/product/husksync.758) + * ⚒️ [BuiltByBit](https://craftaro.com/marketplace/product/husksync.758) * 💬 [Discord Support](https://discord.gg/tVYhJfyDWG) diff --git a/fabric/build.gradle b/fabric/build.gradle new file mode 100644 index 00000000..3d799dd6 --- /dev/null +++ b/fabric/build.gradle @@ -0,0 +1,71 @@ +plugins { + id 'fabric-loom' version '1.6-SNAPSHOT' +} + +apply plugin: 'fabric-loom' +loom.serverOnlyMinecraftJar() + +repositories { + maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots/' } + maven { url 'https://maven.nucleoid.xyz' } +} + +dependencies { + minecraft "com.mojang:minecraft:${fabric_minecraft_version}" + mappings "net.fabricmc:yarn:${fabric_yarn_mappings}:v2" + modImplementation "net.fabricmc:fabric-loader:${fabric_loader_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("eu.pb4:sgui:${sgui_version}") + modCompileOnly "net.fabricmc.fabric-api:fabric-api:${fabric_api_version}" + + // Runtime dependencies on Bukkit; "include" them on Fabric. (todo: minify JAR?) + implementation include("redis.clients:jedis:$jedis_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.xerial.snappy:snappy-java:$snappy_version") + + compileOnly 'org.jetbrains:annotations:24.0.1' + compileOnly 'net.william278:DesertWell:2.0.4' + compileOnly 'org.projectlombok:lombok:1.18.30' + + annotationProcessor 'org.projectlombok:lombok:1.18.30' + + shadow project(path: ":common") +} + +shadowJar { + configurations = [project.configurations.shadow] + destinationDirectory.set(file("$projectDir/build/libs")) + + exclude('net.fabricmc:.*') + exclude('net.kyori:.*') + exclude '/mappings/*' + + relocate 'org.apache.commons.io', 'net.william278.husksync.libraries.commons.io' + relocate 'org.apache.commons.text', 'net.william278.husksync.libraries.commons.text' + relocate 'org.apache.commons.lang3', 'net.william278.husksync.libraries.commons.lang3' + relocate 'com.google.gson', 'net.william278.husksync.libraries.gson' + relocate 'com.fatboyindustrial', 'net.william278.husksync.libraries' + relocate 'de.themoep', 'net.william278.husksync.libraries' + relocate 'org.jetbrains', 'net.william278.husksync.libraries' + relocate 'org.intellij', 'net.william278.husksync.libraries' + relocate 'com.zaxxer', 'net.william278.husksync.libraries' + relocate 'de.exlll', 'net.william278.husksync.libraries' + relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell' + relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown' + relocate 'org.json', 'net.william278.husksync.libraries.json' +} + +remapJar { + dependsOn tasks.shadowJar + mustRunAfter tasks.shadowJar + inputFile = shadowJar.archiveFile.get() + addNestedDependencies = true + + destinationDirectory.set(file("$rootDir/target/")) + archiveClassifier.set('') +} + +shadowJar.finalizedBy(remapJar) \ No newline at end of file diff --git a/fabric/src/main/java/net/william278/husksync/FabricHuskSync.java b/fabric/src/main/java/net/william278/husksync/FabricHuskSync.java new file mode 100644 index 00000000..6bb4f5ae --- /dev/null +++ b/fabric/src/main/java/net/william278/husksync/FabricHuskSync.java @@ -0,0 +1,341 @@ +/* + * This file is part of HuskSync, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * 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; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.gson.Gson; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import net.fabricmc.api.DedicatedServerModInitializer; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.ModContainer; +import net.kyori.adventure.platform.AudienceProvider; +import net.kyori.adventure.platform.fabric.FabricServerAudiences; +import net.minecraft.server.MinecraftServer; +import net.william278.desertwell.util.Version; +import net.william278.husksync.adapter.DataAdapter; +import net.william278.husksync.adapter.GsonAdapter; +import net.william278.husksync.adapter.SnappyGsonAdapter; +import net.william278.husksync.api.FabricHuskSyncAPI; +import net.william278.husksync.command.Command; +import net.william278.husksync.command.FabricCommand; +import net.william278.husksync.config.Locales; +import net.william278.husksync.config.Server; +import net.william278.husksync.config.Settings; +import net.william278.husksync.data.*; +import net.william278.husksync.database.Database; +import net.william278.husksync.database.MySqlDatabase; +import net.william278.husksync.event.FabricEventDispatcher; +import net.william278.husksync.hook.PlanHook; +import net.william278.husksync.listener.EventListener; +import net.william278.husksync.listener.FabricEventListener; +import net.william278.husksync.migrator.Migrator; +import net.william278.husksync.redis.RedisManager; +import net.william278.husksync.sync.DataSyncer; +import net.william278.husksync.user.ConsoleUser; +import net.william278.husksync.user.FabricUser; +import net.william278.husksync.user.OnlineUser; +import net.william278.husksync.util.FabricTask; +import net.william278.husksync.util.LegacyConverter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.spi.LoggingEventBuilder; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.logging.Level; +import java.util.stream.Collectors; + +@Getter +@NoArgsConstructor +public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync, FabricTask.Supplier, + FabricEventDispatcher { + + private static final String PLATFORM_TYPE_ID = "fabric"; + + private final TreeMap> serializers = Maps.newTreeMap( + SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR + ); + private final Map> playerCustomDataStore = Maps.newConcurrentMap(); + private final Map permissions = Maps.newHashMap(); + private final List availableMigrators = Lists.newArrayList(); + private final Set lockedPlayers = Sets.newConcurrentHashSet(); + + private Logger logger; + private ModContainer mod; + private MinecraftServer minecraftServer; + private boolean disabling; + private Gson gson; + private AudienceProvider audiences; + private Database database; + private RedisManager redisManager; + private EventListener eventListener; + private DataAdapter dataAdapter; + @Setter + private DataSyncer dataSyncer; + @Setter + private Settings settings; + @Setter + private Locales locales; + @Setter + @Getter(AccessLevel.NONE) + private Server serverName; + + @Override + public void onInitializeServer() { + // Get the logger and mod container + this.logger = LoggerFactory.getLogger("HuskSync"); + this.mod = FabricLoader.getInstance().getModContainer("husksync").orElseThrow(); + this.disabling = false; + this.gson = createGson(); + + // Load settings and locales + initialize("plugin config & locale files", (plugin) -> { + loadSettings(); + loadLocales(); + loadServer(); + }); + + // Register commands + initialize("commands", (plugin) -> this.registerCommands()); + + // Load HuskSync after server startup + ServerLifecycleEvents.SERVER_STARTED.register(server -> { + this.minecraftServer = server; + this.onEnable(); + }); + + // Unload HuskSync before server shutdown + ServerLifecycleEvents.SERVER_STOPPING.register(server -> this.onDisable()); + } + + private void onEnable() { + // Initial plugin setup + this.audiences = FabricServerAudiences.of(minecraftServer); + + // Prepare data adapter + initialize("data adapter", (plugin) -> { + if (getSettings().getSynchronization().isCompressData()) { + this.dataAdapter = new SnappyGsonAdapter(this); + } else { + this.dataAdapter = new GsonAdapter(this); + } + }); + + initialize("data serializers", (plugin) -> { + // PERSISTENT_DATA is not registered / available on the Fabric platform + registerSerializer(Identifier.INVENTORY, new FabricSerializer.Inventory(this)); + registerSerializer(Identifier.ENDER_CHEST, new FabricSerializer.EnderChest(this)); + registerSerializer(Identifier.ADVANCEMENTS, new FabricSerializer.Advancements(this)); + registerSerializer(Identifier.STATISTICS, new Serializer.Json<>(this, FabricData.Statistics.class)); // TODO APPLY + registerSerializer(Identifier.POTION_EFFECTS, new FabricSerializer.PotionEffects(this)); + registerSerializer(Identifier.GAME_MODE, new Serializer.Json<>(this, FabricData.GameMode.class)); + registerSerializer(Identifier.FLIGHT_STATUS, new Serializer.Json<>(this, FabricData.FlightStatus.class)); + registerSerializer(Identifier.ATTRIBUTES, new Serializer.Json<>(this, FabricData.Attributes.class)); + registerSerializer(Identifier.HEALTH, new Serializer.Json<>(this, FabricData.Health.class)); + registerSerializer(Identifier.HUNGER, new Serializer.Json<>(this, FabricData.Hunger.class)); + registerSerializer(Identifier.EXPERIENCE, new Serializer.Json<>(this, FabricData.Experience.class)); + registerSerializer(Identifier.LOCATION, new Serializer.Json<>(this, FabricData.Location.class)); + validateDependencies(); + }); + + // Initialize the database + initialize(getSettings().getDatabase().getType().getDisplayName() + " database connection", (plugin) -> { + this.database = new MySqlDatabase(this); + this.database.initialize(); + }); + + // Prepare redis connection + initialize("Redis server connection", (plugin) -> { + this.redisManager = new RedisManager(this); + this.redisManager.initialize(); + }); + + // Prepare data syncer + initialize("data syncer", (plugin) -> { + dataSyncer = getSettings().getSynchronization().getMode().create(this); + dataSyncer.initialize(); + }); + + // Register events + initialize("events", (plugin) -> this.eventListener = new FabricEventListener(this)); + + // Register plugin hooks + initialize("hooks", (plugin) -> { + if (isDependencyLoaded("Plan") && getSettings().isEnablePlanHook()) { + new PlanHook(this).hookIntoPlan(); + } + }); + + // Register API + initialize("api", (plugin) -> { + FabricHuskSyncAPI.register(this); + }); + + // Check for updates + this.checkForUpdates(); + } + + private void onDisable() { + // Handle shutdown + this.disabling = true; + + // Close the event listener / data syncer + if (this.dataSyncer != null) { + this.dataSyncer.terminate(); + } + if (this.eventListener != null) { + this.eventListener.handlePluginDisable(); + } + + // Cancel tasks, close audiences + if (audiences != null) { + this.audiences.close(); + } + this.cancelTasks(); + + // Complete shutdown + log(Level.INFO, "Successfully disabled HuskSync v" + getPluginVersion()); + } + + private void registerCommands() { + final List commands = FabricCommand.Type.getCommands(this); + CommandRegistrationCallback.EVENT.register((dispatcher, registry, environment) -> + commands.forEach(command -> new FabricCommand(command, this).register(dispatcher)) + ); + } + + @NotNull + @Override + public String getServerName() { + return serverName.getName(); + } + + @Override + public boolean isDependencyLoaded(@NotNull String name) { + return FabricLoader.getInstance().isModLoaded(name); + } + + @Override + @NotNull + public Set getOnlineUsers() { + return minecraftServer.getPlayerManager().getPlayerList() + .stream().map(user -> (OnlineUser) FabricUser.adapt(user, this)) + .collect(Collectors.toSet()); + } + + @Override + @NotNull + public Optional getOnlineUser(@NotNull UUID uuid) { + return Optional.ofNullable(minecraftServer.getPlayerManager().getPlayer(uuid)) + .map(user -> FabricUser.adapt(user, this)); + } + + @Override + @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)); + } + + @Override + @NotNull + public Path getConfigDirectory() { + final Path path = FabricLoader.getInstance().getConfigDir().resolve("husksync"); + if (!Files.isDirectory(path)) { + try { + Files.createDirectory(path); + } catch (IOException e) { + log(Level.SEVERE, "Failed to create config directory", e); + } + } + return path; + } + + @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; + } + ); + if (throwable.length >= 1) { + logEvent = logEvent.setCause(throwable[0]); + } + logEvent.log(message); + } + + @NotNull + @Override + public ConsoleUser getConsole() { + return new ConsoleUser(audiences); + } + + @Override + @NotNull + public Version getPluginVersion() { + return Version.fromString(mod.getMetadata().getVersion().getFriendlyString(), "-"); + } + + @Override + @NotNull + public Version getMinecraftVersion() { + return Version.fromString(minecraftServer.getVersion()); + } + + @NotNull + @Override + public String getPlatformType() { + return PLATFORM_TYPE_ID; + } + + @Override + public Optional getLegacyConverter() { + return Optional.empty(); + } + + @Override + @NotNull + public FabricHuskSync getPlugin() { + return this; + } + +} diff --git a/fabric/src/main/java/net/william278/husksync/api/FabricHuskSyncAPI.java b/fabric/src/main/java/net/william278/husksync/api/FabricHuskSyncAPI.java new file mode 100644 index 00000000..6b9bd820 --- /dev/null +++ b/fabric/src/main/java/net/william278/husksync/api/FabricHuskSyncAPI.java @@ -0,0 +1,260 @@ +/* + * This file is part of HuskSync, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * 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.api; + +import net.minecraft.item.ItemStack; +import net.minecraft.server.network.ServerPlayerEntity; +import net.william278.desertwell.util.ThrowingConsumer; +import net.william278.husksync.FabricHuskSync; +import net.william278.husksync.data.DataHolder; +import net.william278.husksync.data.FabricData; +import net.william278.husksync.user.FabricUser; +import net.william278.husksync.user.OnlineUser; +import net.william278.husksync.user.User; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +/** + * The HuskSync API implementation for the Fabric platform + *

+ * Retrieve an instance of the API class via {@link #getInstance()}. + */ +@SuppressWarnings("unused") +public class FabricHuskSyncAPI extends HuskSyncAPI { + + /** + * (Internal use only) - Constructor, instantiating the API. + */ + @ApiStatus.Internal + private FabricHuskSyncAPI(@NotNull FabricHuskSync plugin) { + super(plugin); + } + + /** + * Entrypoint to the HuskSync API on the Fabric platform - returns an instance of the API + * + * @return instance of the HuskSync API + * @since 3.6 + */ + @NotNull + public static FabricHuskSyncAPI getInstance() { + if (instance == null) { + throw new NotRegisteredException(); + } + return (FabricHuskSyncAPI) instance; + } + + /** + * (Internal use only) - Register the API for this platform. + * + * @param plugin the plugin instance + * @since 3.6 + */ + @ApiStatus.Internal + public static void register(@NotNull FabricHuskSync plugin) { + instance = new FabricHuskSyncAPI(plugin); + } + + + /** + * Returns a {@link OnlineUser} instance for the given Fabric {@link ServerPlayerEntity}. + * + * @param player the Fabric player to get the {@link OnlineUser} instance for + * @return the {@link OnlineUser} instance for the given Fabric {@link ServerPlayerEntity} + * @since 2.0 + */ + @NotNull + public FabricUser getUser(@NotNull ServerPlayerEntity player) { + return FabricUser.adapt(player, plugin); + } + + /** + * Get the current {@link FabricData.Items.Inventory} of the given {@link User} + * + * @param user the user to get the inventory of + * @return the {@link FabricData.Items.Inventory} of the given {@link User} + * @since 3.6 + */ + public CompletableFuture> getCurrentInventory(@NotNull User user) { + return getCurrentData(user).thenApply(data -> data.flatMap(DataHolder::getInventory) + .map(FabricData.Items.Inventory.class::cast)); + } + + /** + * Get the current {@link FabricData.Items.Inventory} of the given {@link ServerPlayerEntity} + * + * @param user the user to get the inventory of + * @return the {@link FabricData.Items.Inventory} of the given {@link ServerPlayerEntity} + * @since 3.6 + */ + public CompletableFuture> getCurrentInventoryContents(@NotNull User user) { + return getCurrentInventory(user) + .thenApply(inventory -> inventory.map(FabricData.Items.Inventory::getContents)); + } + + /** + * Set the current {@link FabricData.Items.Inventory} of the given {@link User} + * + * @param user the user to set the inventory of + * @param contents the contents to set the inventory to + * @since 3.6 + */ + public void setCurrentInventory(@NotNull User user, @NotNull FabricData.Items.Inventory contents) { + editCurrentData(user, dataHolder -> dataHolder.setInventory(contents)); + } + + /** + * Set the current {@link FabricData.Items.Inventory} of the given {@link User} + * + * @param user the user to set the inventory of + * @param contents the contents to set the inventory to + * @since 3.6 + */ + public void setCurrentInventoryContents(@NotNull User user, @NotNull ItemStack[] contents) { + editCurrentData( + user, + dataHolder -> dataHolder.getInventory().ifPresent( + inv -> inv.setContents(adaptItems(contents)) + ) + ); + } + + /** + * Edit the current {@link FabricData.Items.Inventory} of the given {@link User} + * + * @param user the user to edit the inventory of + * @param editor the editor to apply to the inventory + * @since 3.6 + */ + public void editCurrentInventory(@NotNull User user, ThrowingConsumer editor) { + editCurrentData(user, dataHolder -> dataHolder.getInventory() + .map(FabricData.Items.Inventory.class::cast) + .ifPresent(editor)); + } + + /** + * Edit the current {@link FabricData.Items.Inventory} of the given {@link User} + * + * @param user the user to edit the inventory of + * @param editor the editor to apply to the inventory + * @since 3.6 + */ + public void editCurrentInventoryContents(@NotNull User user, ThrowingConsumer editor) { + editCurrentData(user, dataHolder -> dataHolder.getInventory() + .map(FabricData.Items.Inventory.class::cast) + .ifPresent(inventory -> editor.accept(inventory.getContents()))); + } + + /** + * Get the current {@link FabricData.Items.EnderChest} of the given {@link User} + * + * @param user the user to get the ender chest of + * @return the {@link FabricData.Items.EnderChest} of the given {@link User}, or {@link Optional#empty()} if the + * user data could not be found + * @since 3.6 + */ + public CompletableFuture> getCurrentEnderChest(@NotNull User user) { + return getCurrentData(user).thenApply(data -> data.flatMap(DataHolder::getEnderChest) + .map(FabricData.Items.EnderChest.class::cast)); + } + + /** + * Get the current {@link FabricData.Items.EnderChest} of the given {@link ServerPlayerEntity} + * + * @param user the user to get the ender chest of + * @return the {@link FabricData.Items.EnderChest} of the given {@link ServerPlayerEntity}, or {@link Optional#empty()} if the + * user data could not be found + * @since 3.6 + */ + public CompletableFuture> getCurrentEnderChestContents(@NotNull User user) { + return getCurrentEnderChest(user) + .thenApply(enderChest -> enderChest.map(FabricData.Items.EnderChest::getContents)); + } + + /** + * Set the current {@link FabricData.Items.EnderChest} of the given {@link User} + * + * @param user the user to set the ender chest of + * @param contents the contents to set the ender chest to + * @since 3.6 + */ + public void setCurrentEnderChest(@NotNull User user, @NotNull FabricData.Items.EnderChest contents) { + editCurrentData(user, dataHolder -> dataHolder.setEnderChest(contents)); + } + + /** + * Set the current {@link FabricData.Items.EnderChest} of the given {@link User} + * + * @param user the user to set the ender chest of + * @param contents the contents to set the ender chest to + * @since 3.6 + */ + public void setCurrentEnderChestContents(@NotNull User user, @NotNull ItemStack[] contents) { + editCurrentData( + user, + dataHolder -> dataHolder.getEnderChest().ifPresent( + enderChest -> enderChest.setContents(adaptItems(contents)) + ) + ); + } + + /** + * Edit the current {@link FabricData.Items.EnderChest} of the given {@link User} + * + * @param user the user to edit the ender chest of + * @param editor the editor to apply to the ender chest + * @since 3.6 + */ + public void editCurrentEnderChest(@NotNull User user, Consumer editor) { + editCurrentData(user, dataHolder -> dataHolder.getEnderChest() + .map(FabricData.Items.EnderChest.class::cast) + .ifPresent(editor)); + } + + /** + * Edit the current {@link FabricData.Items.EnderChest} of the given {@link User} + * + * @param user the user to edit the ender chest of + * @param editor the editor to apply to the ender chest + * @since 3.6 + */ + public void editCurrentEnderChestContents(@NotNull User user, Consumer editor) { + editCurrentData(user, dataHolder -> dataHolder.getEnderChest() + .map(FabricData.Items.EnderChest.class::cast) + .ifPresent(enderChest -> editor.accept(enderChest.getContents()))); + } + + /** + * Adapts an array of {@link ItemStack} to a {@link FabricData.Items} instance + * + * @param contents the contents to adapt + * @return the adapted {@link FabricData.Items} instance + * @since 3.6 + */ + @NotNull + public FabricData.Items adaptItems(@NotNull ItemStack[] contents) { + return FabricData.Items.ItemArray.adapt(contents); + } + +} diff --git a/fabric/src/main/java/net/william278/husksync/command/FabricCommand.java b/fabric/src/main/java/net/william278/husksync/command/FabricCommand.java new file mode 100644 index 00000000..14b02cf9 --- /dev/null +++ b/fabric/src/main/java/net/william278/husksync/command/FabricCommand.java @@ -0,0 +1,153 @@ +/* + * This file is part of HuskSync, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * 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.command; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.tree.LiteralCommandNode; +import me.lucko.fabric.api.permissions.v0.PermissionCheckEvent; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.fabricmc.fabric.api.util.TriState; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayerEntity; +import net.william278.husksync.FabricHuskSync; +import net.william278.husksync.HuskSync; +import net.william278.husksync.user.CommandUser; +import net.william278.husksync.user.FabricUser; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Predicate; + +import static com.mojang.brigadier.arguments.StringArgumentType.greedyString; +import static net.minecraft.server.command.CommandManager.argument; +import static net.minecraft.server.command.CommandManager.literal; + +public class FabricCommand { + + private final FabricHuskSync plugin; + private final Command command; + + public FabricCommand(@NotNull Command command, @NotNull FabricHuskSync plugin) { + this.command = command; + this.plugin = plugin; + } + + public void register(@NotNull CommandDispatcher dispatcher) { + // Register brigadier command + final Predicate predicate = Permissions + .require(command.getPermission(), command.isOperatorCommand() ? 3 : 0); + final LiteralArgumentBuilder builder = literal(command.getName()) + .requires(predicate).executes(getBrigadierExecutor()); + plugin.getPermissions().put(command.getPermission(), command.isOperatorCommand()); + if (!command.getRawUsage().isBlank()) { + builder.then(argument(command.getRawUsage().replaceAll("[<>\\[\\]]", ""), greedyString()) + .executes(getBrigadierExecutor()) + .suggests(getBrigadierSuggester())); + } + + // Register additional permissions + final Map permissions = command.getAdditionalPermissions(); + permissions.forEach((permission, isOp) -> plugin.getPermissions().put(permission, isOp)); + PermissionCheckEvent.EVENT.register((player, node) -> { + if (permissions.containsKey(node) && permissions.get(node) && player.hasPermissionLevel(3)) { + return TriState.TRUE; + } + return TriState.DEFAULT; + }); + + // Register aliases + final LiteralCommandNode node = dispatcher.register(builder); + dispatcher.register(literal("husksync:" + command.getName()) + .requires(predicate).executes(getBrigadierExecutor()).redirect(node)); + command.getAliases().forEach(alias -> dispatcher.register(literal(alias) + .requires(predicate).executes(getBrigadierExecutor()).redirect(node))); + } + + private com.mojang.brigadier.Command getBrigadierExecutor() { + return (context) -> { + command.onExecuted( + resolveExecutor(context.getSource()), + command.removeFirstArg(context.getInput().split(" ")) + ); + return 1; + }; + } + + private com.mojang.brigadier.suggestion.SuggestionProvider getBrigadierSuggester() { + if (!(command instanceof TabProvider provider)) { + return (context, builder) -> com.mojang.brigadier.suggestion.Suggestions.empty(); + } + return (context, builder) -> { + final String[] args = command.removeFirstArg(context.getInput().split(" ", -1)); + provider.getSuggestions(resolveExecutor(context.getSource()), args).stream() + .map(suggestion -> { + final String completedArgs = String.join(" ", args); + int lastIndex = completedArgs.lastIndexOf(" "); + if (lastIndex == -1) { + return suggestion; + } + return completedArgs.substring(0, lastIndex + 1) + suggestion; + }) + .forEach(builder::suggest); + return builder.buildFuture(); + }; + } + + private CommandUser resolveExecutor(@NotNull ServerCommandSource source) { + if (source.getEntity() instanceof ServerPlayerEntity player) { + return FabricUser.adapt(player, plugin); + } + return plugin.getConsole(); + } + + + /** + * Commands available on the Fabric HuskSync implementation. + */ + public enum Type { + + HUSKSYNC_COMMAND(HuskSyncCommand::new), + USERDATA_COMMAND(UserDataCommand::new), + INVENTORY_COMMAND(InventoryCommand::new), + ENDER_CHEST_COMMAND(EnderChestCommand::new); + + private final Function supplier; + + Type(@NotNull Function supplier) { + this.supplier = supplier; + } + + @NotNull + public Command createCommand(@NotNull HuskSync plugin) { + return supplier.apply(plugin); + } + + @NotNull + public static List getCommands(@NotNull FabricHuskSync plugin) { + return Arrays.stream(values()).map(type -> type.createCommand(plugin)).toList(); + } + + } + +} \ No newline at end of file diff --git a/fabric/src/main/java/net/william278/husksync/data/FabricData.java b/fabric/src/main/java/net/william278/husksync/data/FabricData.java new file mode 100644 index 00000000..0f3eb7a6 --- /dev/null +++ b/fabric/src/main/java/net/william278/husksync/data/FabricData.java @@ -0,0 +1,808 @@ +/* + * This file is part of HuskSync, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * 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.data; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.gson.annotations.SerializedName; +import lombok.*; +import net.fabricmc.fabric.api.dimension.v1.FabricDimensions; +import net.minecraft.advancement.AdvancementProgress; +import net.minecraft.advancement.PlayerAdvancementTracker; +import net.minecraft.enchantment.EnchantmentHelper; +import net.minecraft.entity.attribute.EntityAttribute; +import net.minecraft.entity.attribute.EntityAttributeInstance; +import net.minecraft.entity.attribute.EntityAttributeModifier; +import net.minecraft.entity.effect.StatusEffect; +import net.minecraft.entity.effect.StatusEffectInstance; +import net.minecraft.entity.player.HungerManager; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.registry.Registries; +import net.minecraft.registry.Registry; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.stat.StatType; +import net.minecraft.stat.Stats; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.Vec3d; +import net.minecraft.world.TeleportTarget; +import net.william278.desertwell.util.ThrowingConsumer; +import net.william278.husksync.FabricHuskSync; +import net.william278.husksync.HuskSync; +import net.william278.husksync.adapter.Adaptable; +import net.william278.husksync.user.FabricUser; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Range; + +import java.util.*; + +import static net.william278.husksync.util.FabricKeyedAdapter.*; + +public abstract class FabricData implements Data { + + @Override + public void apply(@NotNull UserDataHolder user, @NotNull HuskSync plugin) { + this.apply((FabricUser) user, (FabricHuskSync) plugin); + } + + protected abstract void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin); + + @Getter + public static abstract class Items extends FabricData implements Data.Items { + + private final @Nullable ItemStack @NotNull [] contents; + + private Items(@Nullable ItemStack @NotNull [] contents) { + this.contents = Arrays.stream(contents.clone()) + .map(i -> i == null || i.isEmpty() ? null : i) + .toArray(ItemStack[]::new); + } + + @Nullable + @Override + public Stack @NotNull [] getStack() { + return Arrays.stream(contents) + .map(stack -> stack != null ? new Stack( + stack.getItem().toString(), + stack.getCount(), + stack.getName().getString(), + Optional.ofNullable(stack.getSubNbt(ItemStack.DISPLAY_KEY)) + .flatMap(display -> Optional.ofNullable(display.get(ItemStack.LORE_KEY)) + .map(lore -> ((List) lore).stream().toList())) //todo check this is ok + .orElse(null), + stack.getEnchantments().stream() + .map(element -> EnchantmentHelper.getIdFromNbt((NbtCompound) element)) + .filter(Objects::nonNull).map(Identifier::toString) + .toList() + ) : null) + .toArray(Stack[]::new); + } + + @Override + public void clear() { + Arrays.fill(contents, null); + } + + @Override + public void setContents(@NotNull Data.Items contents) { + this.setContents(((FabricData.Items) contents).getContents()); + } + + public void setContents(@Nullable ItemStack @NotNull [] contents) { + // Ensure the array is the correct length for the inventory + if (contents.length != this.contents.length) { + contents = Arrays.copyOf(contents, this.contents.length); + } + System.arraycopy(contents, 0, this.contents, 0, this.contents.length); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof FabricData.Items items) { + return Arrays.equals(contents, items.getContents()); + } + return false; + } + + @Setter + @Getter + public static class Inventory extends FabricData.Items implements Data.Items.Inventory { + + @Range(from = 0, to = 8) + private int heldItemSlot; + + public Inventory(@Nullable ItemStack @NotNull [] contents, int heldItemSlot) { + super(contents); + this.heldItemSlot = heldItemSlot; + } + + @NotNull + public static FabricData.Items.Inventory from(@Nullable ItemStack @NotNull [] contents, int heldItemSlot) { + return new FabricData.Items.Inventory(contents, heldItemSlot); + } + + @NotNull + public static FabricData.Items.Inventory from(@NotNull Collection contents, int heldItemSlot) { + return from(contents.toArray(ItemStack[]::new), heldItemSlot); + } + + @NotNull + public static FabricData.Items.Inventory empty() { + return new FabricData.Items.Inventory(new ItemStack[INVENTORY_SLOT_COUNT], 0); + } + + @Override + public int getSlotCount() { + return INVENTORY_SLOT_COUNT; + } + + @Override + public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException { + final ServerPlayerEntity player = user.getPlayer(); + this.clearInventoryCraftingSlots(player); + player.currentScreenHandler.setCursorStack(ItemStack.EMPTY); + final ItemStack[] items = getContents(); + for (int slot = 0; slot < player.getInventory().size(); slot++) { + player.getInventory().setStack( + slot, items[slot] == null ? ItemStack.EMPTY : items[slot] + ); + } + player.getInventory().selectedSlot = heldItemSlot; + player.playerScreenHandler.sendContentUpdates(); + player.getInventory().updateItems(); + } + + private void clearInventoryCraftingSlots(@NotNull ServerPlayerEntity player) { + player.playerScreenHandler.clearCraftingSlots(); + } + + } + + public static class EnderChest extends FabricData.Items implements Data.Items.EnderChest { + + private EnderChest(@Nullable ItemStack @NotNull [] contents) { + super(contents); + } + + @NotNull + public static FabricData.Items.EnderChest adapt(@Nullable ItemStack @NotNull [] contents) { + return new FabricData.Items.EnderChest(contents); + } + + @NotNull + public static FabricData.Items.EnderChest adapt(@NotNull Collection items) { + return adapt(items.toArray(ItemStack[]::new)); + } + + @NotNull + public static FabricData.Items.EnderChest empty() { + return new FabricData.Items.EnderChest(new ItemStack[ENDER_CHEST_SLOT_COUNT]); + } + + @Override + public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException { + final ItemStack[] items = getContents(); + for (int slot = 0; slot < user.getPlayer().getEnderChestInventory().size(); slot++) { + user.getPlayer().getEnderChestInventory().setStack( + slot, items[slot] == null ? ItemStack.EMPTY : items[slot] + ); + } + } + + } + + public static class ItemArray extends FabricData.Items implements Data.Items { + + private ItemArray(@Nullable ItemStack @NotNull [] contents) { + super(contents); + } + + @NotNull + public static ItemArray adapt(@NotNull Collection drops) { + return new ItemArray(drops.toArray(ItemStack[]::new)); + } + + @NotNull + public static ItemArray adapt(@Nullable ItemStack @NotNull [] drops) { + return new ItemArray(drops); + } + + @Override + public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException { + throw new UnsupportedOperationException("A generic item array cannot be applied to a player"); + } + + } + + } + + @Getter + @AllArgsConstructor(access = AccessLevel.PRIVATE) + public static class PotionEffects extends FabricData implements Data.PotionEffects { + + private final Collection effects; + + @NotNull + public static FabricData.PotionEffects from(@NotNull Collection effects) { + return new FabricData.PotionEffects(effects); + } + + @NotNull + public static FabricData.PotionEffects adapt(@NotNull Collection effects) { + return from(effects.stream() + .map(effect -> { + final StatusEffect type = matchEffectType(effect.type()); + return type != null ? new StatusEffectInstance( + type, + effect.duration(), + effect.amplifier(), + effect.isAmbient(), + effect.showParticles(), + effect.hasIcon() + ) : null; + }) + .filter(Objects::nonNull) + .toList() + ); + } + + @NotNull + @SuppressWarnings("unused") + public static FabricData.PotionEffects empty() { + return new FabricData.PotionEffects(List.of()); + } + + @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)); + getEffects().forEach(player::addStatusEffect); + } + + @NotNull + @Override + public List getActiveEffects() { + return effects.stream() + .map(potionEffect -> { + final String key = getEffectId(potionEffect.getEffectType()); + return key != null ? new Effect( + key, + potionEffect.getAmplifier(), + potionEffect.getDuration(), + potionEffect.isAmbient(), + potionEffect.shouldShowParticles(), + potionEffect.shouldShowIcon() + ) : null; + }) + .filter(Objects::nonNull) + .toList(); + } + + } + + @Getter + @Setter + @AllArgsConstructor(access = AccessLevel.PRIVATE) + @NoArgsConstructor(access = AccessLevel.PRIVATE) + public static class Advancements extends FabricData implements Data.Advancements { + + private List completed; + + @NotNull + public static FabricData.Advancements adapt(@NotNull ServerPlayerEntity player) { + final MinecraftServer server = Objects.requireNonNull(player.getServer(), "Server is null"); + final List advancements = Lists.newArrayList(); + forEachAdvancement(server, advancement -> { + final AdvancementProgress advancementProgress = player.getAdvancementTracker().getProgress(advancement); + final Map awardedCriteria = Maps.newHashMap(); + + advancementProgress.getObtainedCriteria().forEach((criteria) -> awardedCriteria.put(criteria, + advancementProgress.getEarliestProgressObtainDate())); + + // Only save the advancement if criteria has been completed + if (!awardedCriteria.isEmpty()) { + advancements.add(Advancement.adapt(advancement.getId().asString(), awardedCriteria)); + } + }); + return new FabricData.Advancements(advancements); + } + + @NotNull + public static FabricData.Advancements from(@NotNull List advancements) { + return new FabricData.Advancements(advancements); + } + + @Override + public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException { + final ServerPlayerEntity player = user.getPlayer(); + final MinecraftServer server = Objects.requireNonNull(player.getServer(), "Server is null"); + plugin.runAsync(() -> forEachAdvancement(server, advancement -> { + final AdvancementProgress progress = player.getAdvancementTracker().getProgress(advancement); + final Optional record = completed.stream() + .filter(r -> r.getKey().equals(advancement.getId().toString())) + .findFirst(); + if (record.isEmpty()) { + return; + } + + final Map criteria = record.get().getCompletedCriteria(); + final List awarded = Lists.newArrayList(progress.getObtainedCriteria()); + this.setAdvancement( + plugin, advancement, player, user, + criteria.keySet().stream().filter(key -> !awarded.contains(key)).toList(), + awarded.stream().filter(key -> !criteria.containsKey(key)).toList() + ); + })); + } + + private void setAdvancement(@NotNull FabricHuskSync plugin, + @NotNull net.minecraft.advancement.Advancement advancement, + @NotNull ServerPlayerEntity player, + @NotNull FabricUser user, + @NotNull List toAward, + @NotNull List toRevoke) { + plugin.runSync(() -> { + // Track player exp level & progress + final int expLevel = player.experienceLevel; + final float expProgress = player.experienceProgress; + + // Award and revoke advancement criteria + final PlayerAdvancementTracker progress = player.getAdvancementTracker(); + toAward.forEach(a -> progress.grantCriterion(advancement, a)); + toRevoke.forEach(r -> progress.revokeCriterion(advancement, r)); + + // Restore player exp level & progress + if (!toAward.isEmpty() + && (player.experienceLevel != expLevel || player.experienceProgress != expProgress)) { + player.setExperienceLevel(expLevel); + player.setExperiencePoints((int) (player.getNextLevelExperience() * expProgress)); + } + }); + } + + // Performs a consuming function for every advancement registered on the server + private static void forEachAdvancement(@NotNull MinecraftServer server, + @NotNull ThrowingConsumer con) { + server.getAdvancementLoader().getAdvancements().forEach(con); + } + + } + + @Getter + @Setter + @NoArgsConstructor(access = AccessLevel.PRIVATE) + @AllArgsConstructor(access = AccessLevel.PRIVATE) + public static class Location extends FabricData implements Data.Location, Adaptable { + @SerializedName("x") + private double x; + @SerializedName("y") + private double y; + @SerializedName("z") + private double z; + @SerializedName("yaw") + private float yaw; + @SerializedName("pitch") + private float pitch; + @SerializedName("world") + private World world; + + @NotNull + public static FabricData.Location from(double x, double y, double z, + float yaw, float pitch, @NotNull World world) { + return new FabricData.Location(x, y, z, yaw, pitch, world); + } + + @NotNull + public static FabricData.Location adapt(@NotNull ServerPlayerEntity player) { + return from( + player.getX(), + player.getY(), + player.getZ(), + player.getYaw(), + player.getPitch(), + new World( + Objects.requireNonNull( + player.getWorld(), "World is null" + ).getRegistryKey().getValue().toString(), + UUID.nameUUIDFromBytes( + player.getWorld().getDimensionKey().getValue().toString().getBytes() + ), + player.getWorld().getDimensionKey().getValue().toString() + ) + ); + } + + @Override + public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException { + final ServerPlayerEntity player = user.getPlayer(); + final MinecraftServer server = plugin.getMinecraftServer(); + try { + player.dismountVehicle(); + FabricDimensions.teleport( + player, + server.getWorld(server.getWorldRegistryKeys().stream() + .filter(key -> key.getValue().equals(Identifier.tryParse(world.name()))) + .findFirst().orElseThrow( + () -> new IllegalStateException("Invalid world") + )), + new TeleportTarget( + new Vec3d(x, y, z), + Vec3d.ZERO, + yaw, + pitch + ) + ); + } catch (Throwable e) { + throw new IllegalStateException("Failed to apply location", e); + } + } + + } + + @Getter + @AllArgsConstructor(access = AccessLevel.PRIVATE) + @NoArgsConstructor(access = AccessLevel.PRIVATE) + public static class Statistics extends FabricData implements Data.Statistics, Adaptable { + + private static final String BLOCK_STAT_TYPE = "block"; + private static final String ITEM_STAT_TYPE = "item"; + private static final String ENTITY_STAT_TYPE = "entity_type"; + + @SerializedName("generic") + private Map genericStatistics; + @SerializedName("blocks") + private Map> blockStatistics; + @SerializedName("items") + private Map> itemStatistics; + @SerializedName("entities") + private Map> entityStatistics; + + @NotNull + public static FabricData.Statistics adapt(@NotNull ServerPlayerEntity player) throws IllegalStateException { + // Adapt typed stats + final Map> blocks = Maps.newHashMap(), + items = Maps.newHashMap(), entities = Maps.newHashMap(); + Registries.STAT_TYPE.getEntrySet().forEach(stat -> { + final Registry registry = stat.getValue().getRegistry(); + + final String registryId = registry.getKey().getValue().value(); + if (registryId.equals("custom_stat")) { + return; + } + final Map map = (switch (registryId) { + case BLOCK_STAT_TYPE -> blocks; + case ITEM_STAT_TYPE -> items; + case ENTITY_STAT_TYPE -> entities; + default -> throw new IllegalStateException("Unexpected value: %s".formatted(registryId)); + }).compute(stat.getKey().getValue().asString(), (k, v) -> v == null ? Maps.newHashMap() : v); + + registry.getEntrySet().forEach(entry -> { + @SuppressWarnings({"unchecked", "rawtypes"}) final int value = player.getStatHandler() + .getStat((StatType) stat.getValue(), entry.getValue()); + if (value != 0) { + map.put(entry.getKey().getValue().asString(), value); + } + }); + }); + + // Add generic stats + final Map generic = Maps.newHashMap(); + Registries.CUSTOM_STAT.getEntrySet().forEach(stat -> { + final int value = player.getStatHandler().getStat(Stats.CUSTOM.getOrCreateStat(stat.getValue())); + if (value != 0) { + generic.put(stat.getKey().getValue().asString(), value); + } + }); + + return new FabricData.Statistics(generic, blocks, items, entities); + } + + @NotNull + public static FabricData.Statistics from(@NotNull Map generic, + @NotNull Map> blocks, + @NotNull Map> items, + @NotNull Map> entities) { + return new FabricData.Statistics(generic, blocks, items, entities); + } + + @Override + public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) { + final ServerPlayerEntity player = user.getPlayer(); + genericStatistics.forEach((id, v) -> applyStat(player, id, null, v)); + blockStatistics.forEach((id, m) -> m.forEach((b, v) -> applyStat(player, id, BLOCK_STAT_TYPE, v, b))); + itemStatistics.forEach((id, m) -> m.forEach((i, v) -> applyStat(player, id, ITEM_STAT_TYPE, v, i))); + entityStatistics.forEach((id, m) -> m.forEach((e, v) -> applyStat(player, id, ENTITY_STAT_TYPE, v, e))); + player.getStatHandler().updateStatSet(); + player.getStatHandler().sendStats(player); + } + + @SuppressWarnings("unchecked") + private void applyStat(@NotNull ServerPlayerEntity player, @NotNull String id, + @Nullable String type, int value, @NotNull String... key) { + final Identifier statId = Identifier.tryParse(id); + if (statId == null) { + return; + } + if (type == null) { + player.getStatHandler().setStat( + player, + Stats.CUSTOM.getOrCreateStat(Registries.CUSTOM_STAT.get(statId)), + value + ); + return; + } + final Identifier typeId = Identifier.tryParse(type); + final StatType statType = (StatType) Registries.STAT_TYPE.get(typeId); + if (statType == null) { + return; + } + + final Registry typeReg = statType.getRegistry(); + final T typeInstance = typeReg.get(Identifier.tryParse(key[0])); + if (typeInstance == null) { + return; + } + + player.getStatHandler().setStat(player, statType.getOrCreateStat(typeInstance), value); + } + + } + + @Getter + @AllArgsConstructor(access = AccessLevel.PRIVATE) + @NoArgsConstructor(access = AccessLevel.PRIVATE) + public static class Attributes extends FabricData implements Data.Attributes, Adaptable { + + private List attributes; + + @NotNull + public static FabricData.Attributes adapt(@NotNull ServerPlayerEntity player, @NotNull HuskSync plugin) { + final List attributes = Lists.newArrayList(); + Registries.ATTRIBUTE.forEach(id -> { + final EntityAttributeInstance instance = player.getAttributeInstance(id); + final Identifier key = Registries.ATTRIBUTE.getId(id); + if (instance == null || key == null) { + return; + } + final Set modifiers = Sets.newHashSet(); + instance.getModifiers().forEach(modifier -> modifiers.add(new Modifier( + modifier.getId(), + modifier.getName(), + modifier.getValue(), + modifier.getOperation().getId(), + -1 + ))); + attributes.add(new Attribute( + key.asString(), + instance.getBaseValue(), + modifiers + )); + }); + return new FabricData.Attributes(attributes); + } + + public Optional getAttribute(@NotNull EntityAttribute id) { + return Optional.ofNullable(Registries.ATTRIBUTE.getId(id)).map(Identifier::asString) + .flatMap(key -> attributes.stream().filter(attribute -> attribute.name().equals(key)).findFirst()); + } + + @SuppressWarnings("unused") + public Optional getAttribute(@NotNull String key) { + final EntityAttribute attribute = matchAttribute(key); + if (attribute == null) { + return Optional.empty(); + } + return getAttribute(attribute); + } + + @Override + protected void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) { + Registries.ATTRIBUTE.forEach(id -> applyAttribute( + user.getPlayer().getAttributeInstance(id), + getAttribute(id).orElse(null) + )); + + } + + private static void applyAttribute(@Nullable EntityAttributeInstance instance, + @Nullable Attribute attribute) { + if (instance == null) { + return; + } + instance.setBaseValue(attribute == null ? instance.getAttribute().getDefaultValue() : attribute.baseValue()); + instance.getModifiers().forEach(instance::removeModifier); + if (attribute != null) { + attribute.modifiers().forEach(modifier -> instance.addPersistentModifier(new EntityAttributeModifier( + modifier.uuid(), + modifier.name(), + modifier.amount(), + EntityAttributeModifier.Operation.fromId(modifier.operationType()) + ))); + } + } + + } + + @Getter + @Setter + @AllArgsConstructor(access = AccessLevel.PRIVATE) + @NoArgsConstructor(access = AccessLevel.PRIVATE) + public static class Health extends FabricData implements Data.Health, Adaptable { + @SerializedName("health") + private double health; + @SerializedName("health_scale") + private double healthScale; + @SerializedName("is_health_scaled") + private boolean isHealthScaled; + + + @NotNull + public static FabricData.Health from(double health, double scale, boolean isScaled) { + return new FabricData.Health(health, scale, isScaled); + } + + @NotNull + public static FabricData.Health adapt(@NotNull ServerPlayerEntity player) { + return from( + player.getHealth(), + 20.0f, false // Health scale is a Bukkit API feature, not used in Fabric + ); + } + + @Override + public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException { + final ServerPlayerEntity player = user.getPlayer(); + player.setHealth((float) health); + } + + } + + + @Getter + @Setter + @AllArgsConstructor(access = AccessLevel.PRIVATE) + @NoArgsConstructor(access = AccessLevel.PRIVATE) + public static class Hunger extends FabricData implements Data.Hunger, Adaptable { + + @SerializedName("food_level") + private int foodLevel; + @SerializedName("saturation") + private float saturation; + @SerializedName("exhaustion") + private float exhaustion; + + @NotNull + public static FabricData.Hunger adapt(@NotNull ServerPlayerEntity player) { + final HungerManager hunger = player.getHungerManager(); + return from(hunger.getFoodLevel(), hunger.getSaturationLevel(), hunger.getExhaustion()); + } + + @NotNull + public static FabricData.Hunger from(int foodLevel, float saturation, float exhaustion) { + return new FabricData.Hunger(foodLevel, saturation, exhaustion); + } + + @Override + public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException { + final ServerPlayerEntity player = user.getPlayer(); + final HungerManager hunger = player.getHungerManager(); + hunger.setFoodLevel(foodLevel); + hunger.setSaturationLevel(saturation); + hunger.setExhaustion(exhaustion); + } + + } + + @Getter + @Setter + @AllArgsConstructor(access = AccessLevel.PRIVATE) + @NoArgsConstructor(access = AccessLevel.PRIVATE) + public static class Experience extends FabricData implements Data.Experience, Adaptable { + + @SerializedName("total_experience") + private int totalExperience; + + @SerializedName("exp_level") + private int expLevel; + + @SerializedName("exp_progress") + private float expProgress; + + @NotNull + public static FabricData.Experience from(int totalExperience, int expLevel, float expProgress) { + return new FabricData.Experience(totalExperience, expLevel, expProgress); + } + + @NotNull + public static FabricData.Experience adapt(@NotNull ServerPlayerEntity player) { + return from(player.totalExperience, player.experienceLevel, player.experienceProgress); + } + + @Override + public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException { + final ServerPlayerEntity player = user.getPlayer(); + player.totalExperience = totalExperience; + player.setExperienceLevel(expLevel); + player.setExperiencePoints((int) (player.getNextLevelExperience() * expProgress)); + } + + } + + @Getter + @Setter + @AllArgsConstructor(access = AccessLevel.PRIVATE) + @NoArgsConstructor(access = AccessLevel.PRIVATE) + public static class GameMode extends FabricData implements Data.GameMode, Adaptable { + + @SerializedName("game_mode") + private String gameMode; + + @NotNull + public static FabricData.GameMode from(@NotNull String gameMode) { + return new FabricData.GameMode(gameMode); + } + + @NotNull + public static FabricData.GameMode adapt(@NotNull ServerPlayerEntity player) { + return from(player.interactionManager.getGameMode().asString()); + } + + @Override + public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException { + user.getPlayer().interactionManager.changeGameMode(net.minecraft.world.GameMode.byName(gameMode)); + } + + } + + @Getter + @Setter + @AllArgsConstructor(access = AccessLevel.PRIVATE) + @NoArgsConstructor(access = AccessLevel.PRIVATE) + public static class FlightStatus extends FabricData implements Data.FlightStatus, Adaptable { + + @SerializedName("allow_flight") + private boolean allowFlight; + @SerializedName("is_flying") + private boolean flying; + + @NotNull + public static FabricData.FlightStatus from(boolean allowFlight, boolean flying) { + return new FabricData.FlightStatus(allowFlight, allowFlight && flying); + } + + @NotNull + public static FabricData.FlightStatus adapt(@NotNull ServerPlayerEntity player) { + return from(player.getAbilities().allowFlying, player.getAbilities().flying); + } + + @Override + public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException { + final ServerPlayerEntity player = user.getPlayer(); + player.getAbilities().allowFlying = allowFlight; + player.getAbilities().flying = allowFlight && flying; + player.sendAbilitiesUpdate(); + } + + } + +} diff --git a/fabric/src/main/java/net/william278/husksync/data/FabricSerializer.java b/fabric/src/main/java/net/william278/husksync/data/FabricSerializer.java new file mode 100644 index 00000000..0a02295f --- /dev/null +++ b/fabric/src/main/java/net/william278/husksync/data/FabricSerializer.java @@ -0,0 +1,288 @@ +/* + * This file is part of HuskSync, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * 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.data; + +import com.google.gson.reflect.TypeToken; +import com.mojang.serialization.Dynamic; +import com.mojang.serialization.DynamicOps; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import net.minecraft.datafixer.TypeReferences; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.*; +import net.william278.desertwell.util.Version; +import net.william278.husksync.FabricHuskSync; +import net.william278.husksync.HuskSync; +import net.william278.husksync.api.HuskSyncAPI; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.List; + +import static net.william278.husksync.data.Data.Items.Inventory.*; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public abstract class FabricSerializer { + + @ApiStatus.Internal + protected final HuskSync plugin; + + @SuppressWarnings("unused") + public FabricSerializer(@NotNull HuskSyncAPI api) { + this.plugin = api.getPlugin(); + } + + @ApiStatus.Internal + @NotNull + public HuskSync getPlugin() { + return plugin; + } + + public static class Inventory extends FabricSerializer implements Serializer, + ItemDeserializer { + + public Inventory(@NotNull HuskSync plugin) { + super(plugin); + } + + @Override + public FabricData.Items.Inventory deserialize(@NotNull String serialized, @NotNull Version dataMcVersion) + throws DeserializationException { + // Read item NBT from string + final FabricHuskSync plugin = (FabricHuskSync) getPlugin(); + final NbtCompound root; + try { + root = StringNbtReader.parse(serialized); + } catch (Throwable e) { + throw new DeserializationException("Failed to read item NBT from string (%s)".formatted(serialized), e); + } + + // 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 + ); + } + + @Override + public FabricData.Items.Inventory deserialize(@NotNull String serialized) { + return deserialize(serialized, plugin.getMinecraftVersion()); + } + + @NotNull + @Override + public String serialize(@NotNull FabricData.Items.Inventory data) throws SerializationException { + try { + final NbtCompound root = new NbtCompound(); + root.put(ITEMS_TAG, serializeItemArray(data.getContents())); + root.putInt(HELD_ITEM_SLOT_TAG, data.getHeldItemSlot()); + return root.toString(); + } catch (Throwable e) { + throw new SerializationException("Failed to serialize inventory item NBT to string", e); + } + } + + } + + public static class EnderChest extends FabricSerializer implements Serializer, + ItemDeserializer { + + public EnderChest(@NotNull HuskSync plugin) { + super(plugin); + } + + @Override + public FabricData.Items.EnderChest deserialize(@NotNull String serialized, @NotNull Version dataMcVersion) + throws DeserializationException { + final FabricHuskSync plugin = (FabricHuskSync) getPlugin(); + try { + final NbtCompound items = StringNbtReader.parse(serialized); + return FabricData.Items.EnderChest.adapt(getItems(items, dataMcVersion, plugin)); + } catch (Throwable e) { + throw new DeserializationException("Failed to read item NBT from string (%s)".formatted(serialized), e); + } + } + + @Override + public FabricData.Items.EnderChest deserialize(@NotNull String serialized) { + return deserialize(serialized, plugin.getMinecraftVersion()); + } + + @NotNull + @Override + public String serialize(@NotNull FabricData.Items.EnderChest data) throws SerializationException { + try { + return serializeItemArray(data.getContents()).toString(); + } catch (Throwable e) { + throw new SerializationException("Failed to serialize ender chest item NBT to string", e); + } + } + } + + private interface ItemDeserializer { + + int VERSION1_16_5 = 2586; + int VERSION1_17_1 = 2730; + int VERSION1_18_2 = 2975; + int VERSION1_19_2 = 3120; + int VERSION1_19_4 = 3337; + int VERSION1_20_1 = 3465; + int VERSION1_20_2 = 3578; // Future + int VERSION1_20_4 = 3700; // Future + int VERSION1_20_5 = 3837; // Future + + @NotNull + default ItemStack[] getItems(@NotNull NbtCompound tag, @NotNull Version mcVersion, @NotNull FabricHuskSync plugin) { + try { + if (mcVersion.compareTo(plugin.getMinecraftVersion()) < 0) { + return upgradeItemStacks(tag, mcVersion, plugin); + } + + final int size = tag.getInt("size"); + final NbtList items = tag.getList("items", NbtElement.COMPOUND_TYPE); + final ItemStack[] itemStacks = new ItemStack[size]; + for (int i = 0; i < size; i++) { + final NbtCompound compound = items.getCompound(i); + final int slot = compound.getInt("Slot"); + itemStacks[slot] = ItemStack.fromNbt(compound); + } + return itemStacks; + } catch (Throwable e) { + throw new Serializer.DeserializationException("Failed to read item NBT string (%s)".formatted(tag), e); + } + } + + // Serialize items slot-by-slot + @NotNull + default NbtCompound serializeItemArray(@Nullable ItemStack @NotNull [] items) { + final NbtCompound container = new NbtCompound(); + container.putInt("size", items.length); + final NbtList itemList = new NbtList(); + for (int i = 0; i < items.length; i++) { + final ItemStack item = items[i]; + if (item == null || item.isEmpty()) { + continue; + } + NbtCompound entry = new NbtCompound(); + entry.putInt("Slot", i); + item.writeNbt(entry); + itemList.add(entry); + } + container.put(ITEMS_TAG, itemList); + return container; + } + + @NotNull + private ItemStack @NotNull [] upgradeItemStacks(@NotNull NbtCompound items, @NotNull Version mcVersion, + @NotNull FabricHuskSync plugin) { + final int size = items.getInt("size"); + final NbtList list = items.getList("items", NbtElement.COMPOUND_TYPE); + final ItemStack[] itemStacks = new ItemStack[size]; + Arrays.fill(itemStacks, ItemStack.EMPTY); + for (int i = 0; i < size; i++) { + if (list.getCompound(i) == null) { + continue; + } + final NbtCompound compound = list.getCompound(i); + final int slot = compound.getInt("Slot"); + itemStacks[slot] = ItemStack.fromNbt(upgradeItemData(list.getCompound(i), mcVersion, plugin)); + } + return itemStacks; + } + + + @NotNull + @SuppressWarnings({"rawtypes", "unchecked"}) // For NBTOps lookup + private NbtCompound upgradeItemData(@NotNull NbtCompound tag, @NotNull Version mcVersion, + @NotNull FabricHuskSync plugin) { + return (NbtCompound) plugin.getMinecraftServer().getDataFixer().update( + TypeReferences.ITEM_STACK, new Dynamic((DynamicOps) NbtOps.INSTANCE, tag), + getDataVersion(mcVersion), getDataVersion(plugin.getMinecraftVersion()) + ).getValue(); + } + + private int getDataVersion(@NotNull Version mcVersion) { + return switch (mcVersion.toStringWithoutMetadata()) { + case "1.16", "1.16.1", "1.16.2", "1.16.3", "1.16.4", "1.16.5" -> VERSION1_16_5; + case "1.17", "1.17.1" -> VERSION1_17_1; + case "1.18", "1.18.1", "1.18.2" -> VERSION1_18_2; + case "1.19", "1.19.1", "1.19.2" -> VERSION1_19_2; + case "1.19.4" -> VERSION1_19_4; + case "1.20", "1.20.1" -> VERSION1_20_1; + case "1.20.2" -> VERSION1_20_2; // Future + case "1.20.4" -> VERSION1_20_4; // Future + case "1.20.5", "1.20.6" -> VERSION1_20_5; // Future + default -> VERSION1_20_1; // Current supported ver + }; + } + + } + + public static class PotionEffects extends FabricSerializer implements Serializer { + + private static final TypeToken> TYPE = new TypeToken<>() { + }; + + public PotionEffects(@NotNull HuskSync plugin) { + super(plugin); + } + + @Override + public FabricData.PotionEffects deserialize(@NotNull String serialized) throws DeserializationException { + return FabricData.PotionEffects.adapt( + plugin.getGson().fromJson(serialized, TYPE.getType()) + ); + } + + @NotNull + @Override + public String serialize(@NotNull FabricData.PotionEffects element) throws SerializationException { + return plugin.getGson().toJson(element.getActiveEffects()); + } + + } + + public static class Advancements extends FabricSerializer implements Serializer { + + private static final TypeToken> TYPE = new TypeToken<>() { + }; + + public Advancements(@NotNull HuskSync plugin) { + super(plugin); + } + + @Override + public FabricData.Advancements deserialize(@NotNull String serialized) throws DeserializationException { + return FabricData.Advancements.from( + plugin.getGson().fromJson(serialized, TYPE.getType()) + ); + } + + @NotNull + @Override + public String serialize(@NotNull FabricData.Advancements element) throws SerializationException { + return plugin.getGson().toJson(element.getCompleted()); + } + } + +} diff --git a/fabric/src/main/java/net/william278/husksync/data/FabricUserDataHolder.java b/fabric/src/main/java/net/william278/husksync/data/FabricUserDataHolder.java new file mode 100644 index 00000000..298fa4e8 --- /dev/null +++ b/fabric/src/main/java/net/william278/husksync/data/FabricUserDataHolder.java @@ -0,0 +1,186 @@ +/* + * This file is part of HuskSync, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * 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.data; + +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.server.network.ServerPlayerEntity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; +import java.util.Optional; + +import static net.william278.husksync.config.Settings.SynchronizationSettings.SaveOnDeathSettings; + +public interface FabricUserDataHolder extends UserDataHolder { + + @Override + default Optional getData(@NotNull Identifier id) { + if (!id.isCustom()) { + try { + return switch (id.getKeyValue()) { + case "inventory" -> getInventory(); + case "ender_chest" -> getEnderChest(); + case "potion_effects" -> getPotionEffects(); + case "advancements" -> getAdvancements(); + case "location" -> getLocation(); + case "statistics" -> getStatistics(); + case "health" -> getHealth(); + case "hunger" -> getHunger(); + case "attributes" -> getAttributes(); + case "experience" -> getExperience(); + case "game_mode" -> getGameMode(); + case "flight_status" -> getFlightStatus(); + case "persistent_data" -> getPersistentData(); + default -> throw new IllegalStateException(String.format("Unexpected data type: %s", id)); + }; + } catch (Throwable e) { + getPlugin().debug("Failed to get data for key: " + id.getKeyValue(), e); + } + } + return Optional.ofNullable(getCustomDataStore().get(id)); + } + + @Override + default void setData(@NotNull Identifier id, @NotNull Data data) { + if (id.isCustom()) { + getCustomDataStore().put(id, data); + } + UserDataHolder.super.setData(id, data); + } + + @NotNull + @Override + default Optional getInventory() { + final SaveOnDeathSettings death = getPlugin().getSettings().getSynchronization().getSaveOnDeath(); + if ((isDead() && !death.isSyncDeadPlayersChangingServer())) { + return Optional.of(FabricData.Items.Inventory.empty()); + } + final PlayerInventory inventory = getPlayer().getInventory(); + return Optional.of(FabricData.Items.Inventory.from( + getCombinedInventory(inventory), + inventory.selectedSlot + )); + } + + // Gets the player's combined inventory; their inventory, plus offhand and armor. + @Nullable + private ItemStack @NotNull [] getCombinedInventory(@NotNull PlayerInventory inv) { + final ItemStack[] combined = new ItemStack[inv.main.size() + inv.armor.size() + inv.offHand.size()]; + System.arraycopy( + inv.main.toArray(new ItemStack[0]), 0, combined, + 0, inv.main.size() + ); + System.arraycopy( + inv.armor.toArray(new ItemStack[0]), 0, combined, + inv.main.size(), inv.armor.size() + ); + System.arraycopy( + inv.offHand.toArray(new ItemStack[0]), 0, combined, + inv.main.size() + inv.armor.size(), inv.offHand.size() + ); + return combined; + } + + @NotNull + @Override + default Optional getEnderChest() { + return Optional.of(FabricData.Items.EnderChest.adapt( + getPlayer().getEnderChestInventory().stacks + )); + } + + @NotNull + @Override + default Optional getPotionEffects() { + return Optional.of(FabricData.PotionEffects.from(getPlayer().getActiveStatusEffects().values())); + } + + @NotNull + @Override + default Optional getAdvancements() { + return Optional.of(FabricData.Advancements.adapt(getPlayer())); + } + + @NotNull + @Override + default Optional getLocation() { + return Optional.of(FabricData.Location.adapt(getPlayer())); + } + + @NotNull + @Override + default Optional getStatistics() { + return Optional.of(FabricData.Statistics.adapt(getPlayer())); + } + + @Override + @NotNull + default Optional getAttributes() { + return Optional.of(FabricData.Attributes.adapt(getPlayer(), getPlugin())); + } + + @NotNull + @Override + default Optional getHealth() { + return Optional.of(FabricData.Health.adapt(getPlayer())); + } + + @NotNull + @Override + default Optional getHunger() { + return Optional.of(FabricData.Hunger.adapt(getPlayer())); + } + + @NotNull + @Override + default Optional getExperience() { + return Optional.of(FabricData.Experience.adapt(getPlayer())); + } + + @NotNull + @Override + default Optional getGameMode() { + return Optional.of(FabricData.GameMode.adapt(getPlayer())); + } + + @NotNull + @Override + default Optional getFlightStatus() { + return Optional.of(FabricData.FlightStatus.adapt(getPlayer())); + } + + @NotNull + @Override + default Optional getPersistentData() { + return Optional.empty(); // Not implemented on Fabric, but maybe we'll do data keys or something + } + + boolean isDead(); + + @NotNull + ServerPlayerEntity getPlayer(); + + @NotNull + @Override + Map getCustomDataStore(); + +} diff --git a/fabric/src/main/java/net/william278/husksync/event/FabricDataSaveCallback.java b/fabric/src/main/java/net/william278/husksync/event/FabricDataSaveCallback.java new file mode 100644 index 00000000..1f709f37 --- /dev/null +++ b/fabric/src/main/java/net/william278/husksync/event/FabricDataSaveCallback.java @@ -0,0 +1,90 @@ +/* + * This file is part of HuskSync, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * 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.minecraft.util.ActionResult; +import net.william278.husksync.HuskSync; +import net.william278.husksync.data.DataSnapshot; +import net.william278.husksync.user.User; +import org.apache.commons.lang3.function.TriFunction; +import org.jetbrains.annotations.NotNull; + +public interface FabricDataSaveCallback extends FabricEventCallback { + + @NotNull + Event EVENT = EventFactory.createArrayBacked(FabricDataSaveCallback.class, + (listeners) -> (event) -> { + for (FabricDataSaveCallback listener : listeners) { + final ActionResult result = listener.invoke(event); + if (event.isCancelled()) { + return ActionResult.CONSUME; + } else if (result != ActionResult.PASS) { + event.setCancelled(true); + return result; + } + } + + return ActionResult.PASS; + }); + + @NotNull + TriFunction SUPPLIER = (user, data, plugin) -> + + new DataSaveEvent() { + private boolean cancelled = false; + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancelled) { + this.cancelled = cancelled; + } + + @NotNull + @Override + public DataSnapshot.Packed getData() { + return data; + } + + @NotNull + @Override + public HuskSync getPlugin() { + return plugin; + } + + @NotNull + @Override + public User getUser() { + return user; + } + + @NotNull + @SuppressWarnings("unused") + public Event getEvent() { + return EVENT; + } + }; + +} diff --git a/fabric/src/main/java/net/william278/husksync/event/FabricEventCallback.java b/fabric/src/main/java/net/william278/husksync/event/FabricEventCallback.java new file mode 100644 index 00000000..406ae21b --- /dev/null +++ b/fabric/src/main/java/net/william278/husksync/event/FabricEventCallback.java @@ -0,0 +1,30 @@ +/* + * This file is part of HuskSync, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * 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.minecraft.util.ActionResult; +import org.jetbrains.annotations.NotNull; + +public interface FabricEventCallback { + + @NotNull + ActionResult invoke(@NotNull E event); + +} diff --git a/fabric/src/main/java/net/william278/husksync/event/FabricEventDispatcher.java b/fabric/src/main/java/net/william278/husksync/event/FabricEventDispatcher.java new file mode 100644 index 00000000..a6f028c5 --- /dev/null +++ b/fabric/src/main/java/net/william278/husksync/event/FabricEventDispatcher.java @@ -0,0 +1,70 @@ +/* + * This file is part of HuskSync, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * 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.minecraft.util.ActionResult; +import net.william278.husksync.data.DataSnapshot; +import net.william278.husksync.user.OnlineUser; +import net.william278.husksync.user.User; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.logging.Level; + +public interface FabricEventDispatcher extends EventDispatcher { + + @SuppressWarnings("unchecked") + @Override + default boolean fireIsCancelled(@NotNull T event) { + try { + final Method field = event.getClass().getDeclaredMethod("getEvent"); + field.setAccessible(true); + + net.fabricmc.fabric.api.event.Event fabricEvent = + (net.fabricmc.fabric.api.event.Event) field.invoke(event); + + final FabricEventCallback invoker = (FabricEventCallback) fabricEvent.invoker(); + return invoker.invoke(event) == ActionResult.FAIL; + } catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException e) { + getPlugin().log(Level.WARNING, "Failed to fire event (" + event.getClass().getName() + ")", e); + return false; + } + } + + @NotNull + @Override + default PreSyncEvent getPreSyncEvent(@NotNull OnlineUser user, @NotNull DataSnapshot.Packed userData) { + return FabricPreSyncCallback.SUPPLIER.apply(user, userData, getPlugin()); + } + + @NotNull + @Override + default DataSaveEvent getDataSaveEvent(@NotNull User user, @NotNull DataSnapshot.Packed saveCause) { + return FabricDataSaveCallback.SUPPLIER.apply(user, saveCause, getPlugin()); + } + + @NotNull + @Override + default SyncCompleteEvent getSyncCompleteEvent(@NotNull OnlineUser user) { + return FabricSyncCompleteCallback.SUPPLIER.apply(user, getPlugin()); + } + +} diff --git a/fabric/src/main/java/net/william278/husksync/event/FabricPreSyncCallback.java b/fabric/src/main/java/net/william278/husksync/event/FabricPreSyncCallback.java new file mode 100644 index 00000000..1b17f868 --- /dev/null +++ b/fabric/src/main/java/net/william278/husksync/event/FabricPreSyncCallback.java @@ -0,0 +1,90 @@ +/* + * This file is part of HuskSync, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * 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.minecraft.util.ActionResult; +import net.william278.husksync.HuskSync; +import net.william278.husksync.data.DataSnapshot; +import net.william278.husksync.user.OnlineUser; +import org.apache.commons.lang3.function.TriFunction; +import org.jetbrains.annotations.NotNull; + +public interface FabricPreSyncCallback extends FabricEventCallback { + + @NotNull + Event EVENT = EventFactory.createArrayBacked(FabricPreSyncCallback.class, + (listeners) -> (event) -> { + for (FabricPreSyncCallback listener : listeners) { + final ActionResult result = listener.invoke(event); + if (event.isCancelled()) { + return ActionResult.CONSUME; + } else if (result != ActionResult.PASS) { + event.setCancelled(true); + return result; + } + } + + return ActionResult.PASS; + }); + + @NotNull + TriFunction SUPPLIER = (user, data, plugin) -> + + new PreSyncEvent() { + private boolean cancelled = false; + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancelled) { + this.cancelled = cancelled; + } + + @NotNull + @Override + public DataSnapshot.Packed getData() { + return data; + } + + @NotNull + @Override + public HuskSync getPlugin() { + return plugin; + } + + @NotNull + @Override + public OnlineUser getUser() { + return user; + } + + @NotNull + @SuppressWarnings("unused") + public Event getEvent() { + return EVENT; + } + }; + +} diff --git a/fabric/src/main/java/net/william278/husksync/event/FabricSyncCompleteCallback.java b/fabric/src/main/java/net/william278/husksync/event/FabricSyncCompleteCallback.java new file mode 100644 index 00000000..53d78557 --- /dev/null +++ b/fabric/src/main/java/net/william278/husksync/event/FabricSyncCompleteCallback.java @@ -0,0 +1,61 @@ +/* + * This file is part of HuskSync, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * 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.minecraft.util.ActionResult; +import net.william278.husksync.HuskSync; +import net.william278.husksync.user.OnlineUser; +import org.jetbrains.annotations.NotNull; + +import java.util.function.BiFunction; + +public interface FabricSyncCompleteCallback extends FabricEventCallback { + + @NotNull + Event EVENT = EventFactory.createArrayBacked(FabricSyncCompleteCallback.class, + (listeners) -> (event) -> { + for (FabricSyncCompleteCallback listener : listeners) { + listener.invoke(event); + } + + return ActionResult.PASS; + }); + + @NotNull + BiFunction SUPPLIER = (user, plugin) -> + + new SyncCompleteEvent() { + + @NotNull + @Override + public OnlineUser getUser() { + return user; + } + + @NotNull + @SuppressWarnings("unused") + public Event getEvent() { + return EVENT; + } + }; + +} diff --git a/fabric/src/main/java/net/william278/husksync/event/InventoryClickCallback.java b/fabric/src/main/java/net/william278/husksync/event/InventoryClickCallback.java new file mode 100644 index 00000000..c027ae2b --- /dev/null +++ b/fabric/src/main/java/net/william278/husksync/event/InventoryClickCallback.java @@ -0,0 +1,48 @@ +/* + * This file is part of HuskSync, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * 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.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.util.ActionResult; +import org.jetbrains.annotations.NotNull; + +public interface InventoryClickCallback { + + @NotNull + Event EVENT = EventFactory.createArrayBacked(InventoryClickCallback.class, + (listeners) -> (player, itemStack) -> { + for (InventoryClickCallback listener : listeners) { + ActionResult result = listener.interact(player, itemStack); + + if (result != ActionResult.PASS) { + return result; + } + } + + return ActionResult.PASS; + }); + + @NotNull + ActionResult interact(PlayerEntity player, ItemStack sheep); + +} diff --git a/fabric/src/main/java/net/william278/husksync/event/ItemDropCallback.java b/fabric/src/main/java/net/william278/husksync/event/ItemDropCallback.java new file mode 100644 index 00000000..edc60e74 --- /dev/null +++ b/fabric/src/main/java/net/william278/husksync/event/ItemDropCallback.java @@ -0,0 +1,48 @@ +/* + * This file is part of HuskSync, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * 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.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.util.ActionResult; +import org.jetbrains.annotations.NotNull; + +public interface ItemDropCallback { + + @NotNull + Event EVENT = EventFactory.createArrayBacked(ItemDropCallback.class, + (listeners) -> (player, itemStack) -> { + for (ItemDropCallback listener : listeners) { + ActionResult result = listener.interact(player, itemStack); + + if (result != ActionResult.PASS) { + return result; + } + } + + return ActionResult.PASS; + }); + + @NotNull + ActionResult interact(PlayerEntity player, ItemStack sheep); + +} diff --git a/fabric/src/main/java/net/william278/husksync/event/ItemPickupCallback.java b/fabric/src/main/java/net/william278/husksync/event/ItemPickupCallback.java new file mode 100644 index 00000000..f788bcf7 --- /dev/null +++ b/fabric/src/main/java/net/william278/husksync/event/ItemPickupCallback.java @@ -0,0 +1,48 @@ +/* + * This file is part of HuskSync, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * 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.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.util.ActionResult; +import org.jetbrains.annotations.NotNull; + +public interface ItemPickupCallback { + + @NotNull + Event EVENT = EventFactory.createArrayBacked(ItemPickupCallback.class, + (listeners) -> (player, itemStack) -> { + for (ItemPickupCallback listener : listeners) { + ActionResult result = listener.interact(player, itemStack); + + if (result != ActionResult.PASS) { + return result; + } + } + + return ActionResult.PASS; + }); + + @NotNull + ActionResult interact(PlayerEntity player, ItemStack sheep); + +} diff --git a/fabric/src/main/java/net/william278/husksync/event/PlayerCommandCallback.java b/fabric/src/main/java/net/william278/husksync/event/PlayerCommandCallback.java new file mode 100644 index 00000000..b1ac5122 --- /dev/null +++ b/fabric/src/main/java/net/william278/husksync/event/PlayerCommandCallback.java @@ -0,0 +1,47 @@ +/* + * This file is part of HuskSync, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * 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.minecraft.entity.player.PlayerEntity; +import net.minecraft.util.ActionResult; +import org.jetbrains.annotations.NotNull; + +public interface PlayerCommandCallback { + + @NotNull + Event EVENT = EventFactory.createArrayBacked(PlayerCommandCallback.class, + (listeners) -> (player, command) -> { + for (PlayerCommandCallback listener : listeners) { + ActionResult result = listener.interact(player, command); + + if (result != ActionResult.PASS) { + return result; + } + } + + return ActionResult.PASS; + }); + + @NotNull + ActionResult interact(PlayerEntity player, String command); + +} diff --git a/fabric/src/main/java/net/william278/husksync/event/PlayerDeathDropsCallback.java b/fabric/src/main/java/net/william278/husksync/event/PlayerDeathDropsCallback.java new file mode 100644 index 00000000..12463ec6 --- /dev/null +++ b/fabric/src/main/java/net/william278/husksync/event/PlayerDeathDropsCallback.java @@ -0,0 +1,44 @@ +/* + * This file is part of HuskSync, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * 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.minecraft.item.ItemStack; +import net.minecraft.server.network.ServerPlayerEntity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; + +public interface PlayerDeathDropsCallback { + + @NotNull + Event EVENT = EventFactory.createArrayBacked( + PlayerDeathDropsCallback.class, + (listeners) -> (player, itemsToKeep, itemsToDrop) -> Arrays.stream(listeners) + .forEach(listener -> listener.drops(player, itemsToKeep, itemsToDrop)) + ); + + void drops(@NotNull ServerPlayerEntity player, + @Nullable ItemStack @NotNull [] itemsToKeep, + @Nullable ItemStack @NotNull [] itemsToDrop); + +} diff --git a/fabric/src/main/java/net/william278/husksync/event/WorldSaveCallback.java b/fabric/src/main/java/net/william278/husksync/event/WorldSaveCallback.java new file mode 100644 index 00000000..21cdb629 --- /dev/null +++ b/fabric/src/main/java/net/william278/husksync/event/WorldSaveCallback.java @@ -0,0 +1,39 @@ +/* + * This file is part of HuskSync, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * 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.minecraft.server.world.ServerWorld; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; + +public interface WorldSaveCallback { + + @NotNull + Event EVENT = EventFactory.createArrayBacked( + WorldSaveCallback.class, + (listeners) -> (world) -> Arrays.stream(listeners).forEach(listener -> listener.save(world)) + ); + + void save(@NotNull ServerWorld world); + +} diff --git a/fabric/src/main/java/net/william278/husksync/listener/FabricEventListener.java b/fabric/src/main/java/net/william278/husksync/listener/FabricEventListener.java new file mode 100644 index 00000000..2533b5dc --- /dev/null +++ b/fabric/src/main/java/net/william278/husksync/listener/FabricEventListener.java @@ -0,0 +1,153 @@ +/* + * This file is part of HuskSync, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * 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.listener; + +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents; +import net.fabricmc.fabric.api.event.player.PlayerBlockBreakEvents; +import net.fabricmc.fabric.api.event.player.UseBlockCallback; +import net.fabricmc.fabric.api.event.player.UseEntityCallback; +import net.fabricmc.fabric.api.event.player.UseItemCallback; +import net.fabricmc.fabric.api.networking.v1.PacketSender; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.damage.DamageSource; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.TypedActionResult; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.hit.EntityHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.william278.husksync.HuskSync; +import net.william278.husksync.config.Settings.SynchronizationSettings.SaveOnDeathSettings; +import net.william278.husksync.data.FabricData; +import net.william278.husksync.event.*; +import net.william278.husksync.user.FabricUser; +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) { + super(plugin); + this.registerEvents(); + } + + public void registerEvents() { + ServerPlayConnectionEvents.JOIN.register(this::handlePlayerJoin); + ServerPlayConnectionEvents.DISCONNECT.register(this::handlePlayerQuit); + WorldSaveCallback.EVENT.register(this::handleWorldSave); + PlayerDeathDropsCallback.EVENT.register(this::handlePlayerDeathDrops); + + // TODO: Events of extra things to cancel if the player has not been set yet + ItemPickupCallback.EVENT.register(this::handleItemPickup); + ItemDropCallback.EVENT.register(this::handleItemDrop); + UseBlockCallback.EVENT.register(this::handleBlockInteract); + UseEntityCallback.EVENT.register(this::handleEntityInteract); + UseItemCallback.EVENT.register(this::handleItemInteract); + PlayerBlockBreakEvents.BEFORE.register(this::handleBlockBreak); + ServerLivingEntityEvents.ALLOW_DAMAGE.register(this::handleEntityDamage); + InventoryClickCallback.EVENT.register(this::handleInventoryClick); + PlayerCommandCallback.EVENT.register(this::handlePlayerCommand); + } + + private void handlePlayerJoin(@NotNull ServerPlayNetworkHandler handler, @NotNull PacketSender sender, + @NotNull MinecraftServer server) { + handlePlayerJoin(FabricUser.adapt(handler.player, plugin)); + } + + private void handlePlayerQuit(@NotNull ServerPlayNetworkHandler handler, @NotNull MinecraftServer server) { + handlePlayerQuit(FabricUser.adapt(handler.player, plugin)); + } + + private void handleWorldSave(@NotNull ServerWorld world) { + saveOnWorldSave(world.getPlayers().stream() + .map(player -> (OnlineUser) FabricUser.adapt(player, plugin)).collect(Collectors.toList())); + } + + private void handlePlayerDeathDrops(@NotNull ServerPlayerEntity player, @Nullable ItemStack @NotNull [] toKeep, + @Nullable ItemStack @NotNull [] toDrop) { + final SaveOnDeathSettings settings = plugin.getSettings().getSynchronization().getSaveOnDeath(); + saveOnPlayerDeath( + FabricUser.adapt(player, plugin), + FabricData.Items.ItemArray.adapt( + settings.getItemsToSave() == SaveOnDeathSettings.DeathItemsMode.DROPS ? toDrop : toKeep + ) + ); + } + + private ActionResult handleItemPickup(PlayerEntity player, ItemStack itemStack) { + return (cancelPlayerEvent(player.getUuid())) ? ActionResult.FAIL : ActionResult.PASS; + } + + private ActionResult handleItemDrop(PlayerEntity player, ItemStack itemStack) { + return (cancelPlayerEvent(player.getUuid())) ? ActionResult.FAIL : ActionResult.PASS; + } + + private ActionResult handleBlockInteract(PlayerEntity player, World world, Hand hand, BlockHitResult blockHitResult) { + return (cancelPlayerEvent(player.getUuid())) ? ActionResult.FAIL : ActionResult.PASS; + } + + private ActionResult handleEntityInteract(PlayerEntity player, World world, Hand hand, Entity entity, EntityHitResult entityHitResult) { + return (cancelPlayerEvent(player.getUuid())) ? ActionResult.FAIL : ActionResult.PASS; + } + + private TypedActionResult handleItemInteract(PlayerEntity player, World world, Hand hand) { + ItemStack stackInHand = player.getStackInHand(hand); + return (cancelPlayerEvent(player.getUuid())) ? TypedActionResult.fail(stackInHand) : TypedActionResult.pass(stackInHand); + } + + private boolean handleBlockBreak(World world, PlayerEntity player, BlockPos blockPos, BlockState blockState, BlockEntity blockEntity) { + return !cancelPlayerEvent(player.getUuid()); + } + + private boolean handleEntityDamage(LivingEntity livingEntity, DamageSource damageSource, float v) { + if (livingEntity instanceof ServerPlayerEntity player) { + return !cancelPlayerEvent(player.getUuid()); + } + return true; + } + + private ActionResult handleInventoryClick(PlayerEntity player, ItemStack itemStack) { + return (cancelPlayerEvent(player.getUuid())) ? ActionResult.FAIL : ActionResult.PASS; + } + + private ActionResult handlePlayerCommand(PlayerEntity player, String s) { + return (cancelPlayerEvent(player.getUuid())) ? ActionResult.FAIL : ActionResult.PASS; + } + + @Override + @NotNull + public HuskSync getPlugin() { + return plugin; + } +} diff --git a/fabric/src/main/java/net/william278/husksync/mixins/ItemEntityMixin.java b/fabric/src/main/java/net/william278/husksync/mixins/ItemEntityMixin.java new file mode 100644 index 00000000..7a7ebfd0 --- /dev/null +++ b/fabric/src/main/java/net/william278/husksync/mixins/ItemEntityMixin.java @@ -0,0 +1,41 @@ +/* + * This file is part of HuskSync, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * 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.mixins; + +import net.minecraft.entity.ItemEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.util.ActionResult; +import net.william278.husksync.event.ItemPickupCallback; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(ItemEntity.class) +public class ItemEntityMixin { + + @Redirect(at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/player/PlayerInventory;insertStack(Lnet/minecraft/item/ItemStack;)Z"), + method = "onPlayerCollision") + public boolean onPlayerCollision(PlayerInventory inventory, ItemStack stack) { + ActionResult result = ItemPickupCallback.EVENT.invoker().interact(inventory.player, stack); + return (result != ActionResult.FAIL && inventory.insertStack(stack)); + } + +} diff --git a/fabric/src/main/java/net/william278/husksync/mixins/PlayerEntityMixin.java b/fabric/src/main/java/net/william278/husksync/mixins/PlayerEntityMixin.java new file mode 100644 index 00000000..c7e186f4 --- /dev/null +++ b/fabric/src/main/java/net/william278/husksync/mixins/PlayerEntityMixin.java @@ -0,0 +1,81 @@ +/* + * This file is part of HuskSync, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * 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.mixins; + +import net.minecraft.enchantment.EnchantmentHelper; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.server.network.ServerPlayerEntity; +import net.william278.husksync.event.PlayerDeathDropsCallback; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(PlayerEntity.class) +public class PlayerEntityMixin { + + @Final + @Shadow + private PlayerInventory inventory; + + @Inject(method = "dropInventory", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/player/PlayerEntity;vanishCursedItems()V")) + protected void dropInventory(@NotNull CallbackInfo ci) { + final PlayerEntity player = (PlayerEntity) (Object) this; + PlayerDeathDropsCallback.EVENT.invoker().drops((ServerPlayerEntity) player, getItemsToKeep(), getItemsToDrop()); + } + + @Unique + @Nullable + private ItemStack @NotNull [] getItemsToKeep() { + final @Nullable ItemStack @NotNull [] toKeep = new ItemStack[inventory.size()]; + for (int i = 0; i < inventory.size(); ++i) { + ItemStack itemStack = inventory.getStack(i); + if (!itemStack.isEmpty() && EnchantmentHelper.hasVanishingCurse(itemStack)) { + toKeep[i] = null; + continue; + } + toKeep[i] = itemStack; + } + return toKeep; + } + + @Unique + @Nullable + private ItemStack @NotNull [] getItemsToDrop() { + final @Nullable ItemStack @NotNull [] toDrop = new ItemStack[inventory.size()]; + for (int i = 0; i < inventory.size(); ++i) { + ItemStack itemStack = inventory.getStack(i); + if (!itemStack.isEmpty() && EnchantmentHelper.hasVanishingCurse(itemStack)) { + toDrop[i] = itemStack; + continue; + } + toDrop[i] = null; + } + return toDrop; + } + +} diff --git a/fabric/src/main/java/net/william278/husksync/mixins/ServerPlayNetworkHandlerMixin.java b/fabric/src/main/java/net/william278/husksync/mixins/ServerPlayNetworkHandlerMixin.java new file mode 100644 index 00000000..344692bc --- /dev/null +++ b/fabric/src/main/java/net/william278/husksync/mixins/ServerPlayNetworkHandlerMixin.java @@ -0,0 +1,107 @@ +/* + * This file is part of HuskSync, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * 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.mixins; + +import net.minecraft.item.ItemStack; +import net.minecraft.network.packet.Packet; +import net.minecraft.network.packet.c2s.play.ClickSlotC2SPacket; +import net.minecraft.network.packet.c2s.play.CommandExecutionC2SPacket; +import net.minecraft.network.packet.c2s.play.CreativeInventoryActionC2SPacket; +import net.minecraft.network.packet.c2s.play.PlayerActionC2SPacket; +import net.minecraft.network.packet.s2c.play.ScreenHandlerSlotUpdateS2CPacket; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.william278.husksync.event.ItemDropCallback; +import net.william278.husksync.event.PlayerCommandCallback; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +// Adapted from simplerauth (https://github.com/lolicode-org/simplerauth), which is licensed under the MIT License +@Mixin(ServerPlayNetworkHandler.class) +public abstract class ServerPlayNetworkHandlerMixin { + @Shadow + public ServerPlayerEntity player; + + @Shadow + public abstract void sendPacket(Packet packet); + + @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) { + ItemStack stack = player.getStackInHand(Hand.MAIN_HAND); + ActionResult result = ItemDropCallback.EVENT.invoker().interact(player, stack); + + if (result == ActionResult.FAIL) { + ci.cancel(); + this.sendPacket(new ScreenHandlerSlotUpdateS2CPacket( + -2, + 1, + player.getInventory().getSlotWithStack(stack), + stack + )); + } + } + } + + @Inject(method = "onClickSlot", at = @At("HEAD"), cancellable = true) + public void onClickSlot(ClickSlotC2SPacket packet, CallbackInfo ci) { + int slot = packet.getSlot(); + if (slot < 0) return; + + ItemStack stack = this.player.getInventory().getStack(slot); + ActionResult result = ItemDropCallback.EVENT.invoker().interact(player, stack); + + if (result == ActionResult.FAIL) { + ci.cancel(); + this.sendPacket(new ScreenHandlerSlotUpdateS2CPacket(-2, 1, slot, stack)); + this.sendPacket(new ScreenHandlerSlotUpdateS2CPacket(-1, 1, -1, ItemStack.EMPTY)); + } + } + + @Inject(method = "onCreativeInventoryAction", at = @At("HEAD"), cancellable = true) + public void onCreativeInventoryAction(CreativeInventoryActionC2SPacket packet, CallbackInfo ci) { + int slot = packet.getSlot(); + if (slot < 0) return; + + ItemStack stack = this.player.getInventory().getStack(slot); + ActionResult result = ItemDropCallback.EVENT.invoker().interact(player, stack); + + if (result == ActionResult.FAIL) { + ci.cancel(); + this.sendPacket(new ScreenHandlerSlotUpdateS2CPacket(-2, 1, slot, stack)); + this.sendPacket(new ScreenHandlerSlotUpdateS2CPacket(-1, 1, -1, ItemStack.EMPTY)); + } + } + + @Inject(method = "onCommandExecution", at = @At("HEAD"), cancellable = true) + public void onCommandExecution(CommandExecutionC2SPacket packet, CallbackInfo ci) { + ActionResult result = PlayerCommandCallback.EVENT.invoker().interact(player, packet.command()); + + if (result == ActionResult.FAIL) { + ci.cancel(); + } + } +} diff --git a/fabric/src/main/java/net/william278/husksync/mixins/ServerPlayerEntityMixin.java b/fabric/src/main/java/net/william278/husksync/mixins/ServerPlayerEntityMixin.java new file mode 100644 index 00000000..fa27c7c7 --- /dev/null +++ b/fabric/src/main/java/net/william278/husksync/mixins/ServerPlayerEntityMixin.java @@ -0,0 +1,46 @@ +/* + * This file is part of HuskSync, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * 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.mixins; + +import net.minecraft.entity.ItemEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.ActionResult; +import net.william278.husksync.event.ItemDropCallback; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(ServerPlayerEntity.class) +public class ServerPlayerEntityMixin { + + @Inject(method = "dropItem", at = @At("HEAD"), cancellable = true) + private void onPlayerDropItem(ItemStack stack, boolean dropAtFeet, boolean saveThrower, + final CallbackInfoReturnable ci) { + ServerPlayerEntity player = (ServerPlayerEntity) (Object) this; + ActionResult result = ItemDropCallback.EVENT.invoker().interact(player, stack); + + if (result == ActionResult.FAIL) { + ci.cancel(); + } + } + +} diff --git a/fabric/src/main/java/net/william278/husksync/mixins/ServerWorldMixin.java b/fabric/src/main/java/net/william278/husksync/mixins/ServerWorldMixin.java new file mode 100644 index 00000000..8ccea757 --- /dev/null +++ b/fabric/src/main/java/net/william278/husksync/mixins/ServerWorldMixin.java @@ -0,0 +1,37 @@ +/* + * This file is part of HuskSync, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * 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.mixins; + +import net.minecraft.server.world.ServerWorld; +import net.william278.husksync.event.WorldSaveCallback; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ServerWorld.class) +public class ServerWorldMixin { + + @Inject(method = "saveLevel", at = @At("HEAD")) + public void saveLevel(CallbackInfo ci) { + WorldSaveCallback.EVENT.invoker().save((ServerWorld) (Object) this); + } + +} diff --git a/fabric/src/main/java/net/william278/husksync/user/FabricUser.java b/fabric/src/main/java/net/william278/husksync/user/FabricUser.java new file mode 100644 index 00000000..33cd97db --- /dev/null +++ b/fabric/src/main/java/net/william278/husksync/user/FabricUser.java @@ -0,0 +1,175 @@ +/* + * This file is part of HuskSync, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * 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.user; + +import de.themoep.minedown.adventure.MineDown; +import eu.pb4.sgui.api.ClickType; +import eu.pb4.sgui.api.elements.GuiElementInterface; +import eu.pb4.sgui.api.gui.SimpleGui; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.platform.fabric.FabricServerAudiences; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.GenericContainerScreenHandler; +import net.minecraft.screen.ScreenHandlerType; +import net.minecraft.screen.slot.SlotActionType; +import net.minecraft.server.network.ServerPlayerEntity; +import net.william278.husksync.FabricHuskSync; +import net.william278.husksync.HuskSync; +import net.william278.husksync.data.Data; +import net.william278.husksync.data.FabricData; +import net.william278.husksync.data.FabricUserDataHolder; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Consumer; + +public class FabricUser extends OnlineUser implements FabricUserDataHolder { + + private final HuskSync plugin; + private final ServerPlayerEntity player; + + private FabricUser(@NotNull ServerPlayerEntity player, @NotNull HuskSync plugin) { + super(player.getUuid(), player.getName().getString()); + this.player = player; + this.plugin = plugin; + } + + @NotNull + @ApiStatus.Internal + public static FabricUser adapt(@NotNull ServerPlayerEntity player, @NotNull HuskSync plugin) { + return new FabricUser(player, plugin); + } + + @Override + public boolean isOffline() { + return player == null || player.isDisconnected(); + } + + @NotNull + @Override + public Audience getAudience() { + return plugin.getAudiences().player(player.getUuid()); + } + + @Override + public void sendToast(@NotNull MineDown title, @NotNull MineDown description, @NotNull String iconMaterial, + @NotNull String backgroundType) { + player.sendActionBar(title.toComponent()); // Toasts unimplemented for now + } + + @Override + public void showGui(@NotNull Data.Items.Items items, @NotNull MineDown title, boolean editable, int size, + @NotNull Consumer onClose) { + plugin.runSync( + () -> new ItemViewerGui(size, player, title, (FabricData.Items) items, onClose, editable, plugin).open() + ); + } + + private static class ItemViewerGui extends SimpleGui { + + private final Consumer onClose; + private final int size; + private final boolean editable; + + public ItemViewerGui(int size, @NotNull ServerPlayerEntity player, @NotNull MineDown title, + @NotNull FabricData.Items items, @NotNull Consumer onClose, + boolean editable, @NotNull HuskSync plugin) { + super(getScreenHandler(size), player, false); + this.onClose = onClose; + this.size = size; + this.editable = editable; + + // Set title, items + this.setTitle(((FabricServerAudiences) plugin.getAudiences()).toNative(title.toComponent())); + this.setLockPlayerInventory(!editable); + for (int i = 0; i < size; i++) { + final ItemStack item = items.getContents()[i]; + this.setSlot(i, item == null ? ItemStack.EMPTY : item); + } + } + + @Override + public void onClose() { + final ItemStack[] contents = new ItemStack[size]; + for (int i = 0; i < size; i++) { + contents[i] = this.getSlot(i) == null ? null : this.getSlot(i).getItemStack(); + } + onClose.accept(FabricData.Items.ItemArray.adapt(contents)); + } + + @Override + public boolean onAnyClick(int index, @NotNull ClickType type, @NotNull SlotActionType action) { + return editable; + } + + @Override + public boolean onClick(int index, @NotNull ClickType type, @NotNull SlotActionType action, + @NotNull GuiElementInterface element) { + return editable; + } + + @NotNull + private static ScreenHandlerType getScreenHandler(int size) { + return switch (size / 9 + (size % 9 == 0 ? 0 : 1)) { + case 3 -> ScreenHandlerType.GENERIC_9X3; + case 4 -> ScreenHandlerType.GENERIC_9X4; + case 5 -> ScreenHandlerType.GENERIC_9X5; + default -> ScreenHandlerType.GENERIC_9X6; + }; + } + } + + @Override + public boolean hasPermission(@NotNull String node) { + final boolean requiresOp = Boolean.TRUE.equals( + ((FabricHuskSync) plugin).getPermissions().getOrDefault(node, true) + ); + return Permissions.check(player, node, !requiresOp || player.hasPermissionLevel(3)); + } + + @Override + public boolean isDead() { + return player.getHealth() <= 0.0f; + } + + @Override + public boolean isLocked() { + return plugin.getLockedPlayers().contains(player.getUuid()); + } + + @Override + public boolean isNpc() { + return false; + } + + @Override + @NotNull + public ServerPlayerEntity getPlayer() { + return player; + } + + @NotNull + @Override + @ApiStatus.Internal + public HuskSync getPlugin() { + return plugin; + } +} diff --git a/fabric/src/main/java/net/william278/husksync/util/FabricKeyedAdapter.java b/fabric/src/main/java/net/william278/husksync/util/FabricKeyedAdapter.java new file mode 100644 index 00000000..864e9363 --- /dev/null +++ b/fabric/src/main/java/net/william278/husksync/util/FabricKeyedAdapter.java @@ -0,0 +1,76 @@ +/* + * This file is part of HuskSync, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * Copyright (c) contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.william278.husksync.util; + +import net.minecraft.entity.EntityType; +import net.minecraft.entity.attribute.EntityAttribute; +import net.minecraft.entity.effect.StatusEffect; +import net.minecraft.registry.Registries; +import net.minecraft.registry.Registry; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +// Utility class for adapting "Keyed" Minecraft objects +public final class FabricKeyedAdapter { + + @Nullable + public static EntityType matchEntityType(@NotNull String key) { + return getRegistryValue(Registries.ENTITY_TYPE, key); + } + + @Nullable + public static String getEntityTypeId(@NotNull EntityType entityType) { + return getRegistryKey(Registries.ENTITY_TYPE, entityType); + } + + @Nullable + public static EntityAttribute matchAttribute(@NotNull String key) { + return getRegistryValue(Registries.ATTRIBUTE, key); + } + + @Nullable + public static String getAttributeId(@NotNull EntityAttribute attribute) { + return getRegistryKey(Registries.ATTRIBUTE, attribute); + } + + @Nullable + public static StatusEffect matchEffectType(@NotNull String key) { + return getRegistryValue(Registries.STATUS_EFFECT, key); + } + + @Nullable + public static String getEffectId(@NotNull StatusEffect effect) { + return getRegistryKey(Registries.STATUS_EFFECT, effect); + } + + @Nullable + private static T getRegistryValue(@NotNull Registry registry, @NotNull String keyString) { + final Identifier key = Identifier.tryParse(keyString); + return key != null ? registry.get(key) : null; + } + + @Nullable + private static String getRegistryKey(@NotNull Registry registry, @NotNull T value) { + final Identifier key = registry.getId(value); + return key != null ? key.toString() : null; + } + +} diff --git a/fabric/src/main/java/net/william278/husksync/util/FabricTask.java b/fabric/src/main/java/net/william278/husksync/util/FabricTask.java new file mode 100644 index 00000000..73556db9 --- /dev/null +++ b/fabric/src/main/java/net/william278/husksync/util/FabricTask.java @@ -0,0 +1,137 @@ +/* + * This file is part of HuskSync, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * Copyright (c) contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.william278.husksync.util; + +import 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; + +public interface FabricTask extends Task { + + class Sync extends Task.Sync implements FabricTask { + + protected Sync(@NotNull HuskSync plugin, @NotNull Runnable runnable, long delayTicks) { + super(plugin, runnable, delayTicks); + } + + @Override + public void cancel() { + super.cancel(); + } + + @Override + public void run() { + if (!cancelled) { + Executors.newSingleThreadScheduledExecutor().schedule( + () -> ((FabricHuskSync) getPlugin()).getMinecraftServer().executeSync(runnable), + delayTicks * 50, + TimeUnit.MILLISECONDS + ); + } + } + } + + class Async extends Task.Async implements FabricTask { + private CompletableFuture task; + + protected Async(@NotNull HuskSync plugin, @NotNull Runnable runnable, long delayTicks) { + super(plugin, runnable, delayTicks); + } + + @Override + public void cancel() { + if (task != null && !cancelled) { + task.cancel(true); + } + super.cancel(); + } + + @Override + public void run() { + if (!cancelled) { + this.task = CompletableFuture.runAsync(runnable, ((FabricHuskSync) getPlugin()).getMinecraftServer()); + } + } + } + + class Repeating extends Task.Repeating implements FabricTask { + + private ScheduledFuture task; + + protected Repeating(@NotNull HuskSync plugin, @NotNull Runnable runnable, long repeatingTicks) { + super(plugin, runnable, repeatingTicks); + } + + @Override + public void cancel() { + if (task != null && !cancelled) { + task.cancel(true); + } + super.cancel(); + } + + @Override + public void run() { + if (!cancelled) { + this.task = Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate( + runnable, + 0, + repeatingTicks * 50, + TimeUnit.MILLISECONDS + ); + } + } + } + + interface Supplier extends Task.Supplier { + + @NotNull + @Override + default Task.Sync getSyncTask(@NotNull Runnable runnable, @Nullable UserDataHolder user, long delayTicks) { + return new Sync(getPlugin(), runnable, delayTicks); + } + + @NotNull + @Override + default Task.Async getAsyncTask(@NotNull Runnable runnable, long delayTicks) { + return new Async(getPlugin(), runnable, delayTicks); + } + + @NotNull + @Override + default Task.Repeating getRepeatingTask(@NotNull Runnable runnable, long repeatingTicks) { + return new Repeating(getPlugin(), runnable, repeatingTicks); + } + + @Override + default void cancelTasks() { + // Do nothing + } + + } + +} diff --git a/fabric/src/main/resources/assets/husksync/icon.png b/fabric/src/main/resources/assets/husksync/icon.png new file mode 100644 index 00000000..b932dabc Binary files /dev/null and b/fabric/src/main/resources/assets/husksync/icon.png differ diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json new file mode 100644 index 00000000..8fe5d0bf --- /dev/null +++ b/fabric/src/main/resources/fabric.mod.json @@ -0,0 +1,55 @@ +{ + "schemaVersion": 1, + "id": "husksync", + "version": "${version}", + "name": "husksync", + "icon": "assets/husksync/icon.png", + "description": "${description}", + "authors": [ + { + "name": "William278", + "contact": { + "sources": "https://github.com/WiIIiam278", + "homepage": "https://william278.net" + } + }, + { + "name": "hanbings", + "contact": { + "sources": "https://github.com/hanbings" + } + }, + { + "name": "Stampede2011", + "contact": { + "sources": "https://github.com/Stampede2011" + } + } + ], + "license": "Apache-2.0", + "contact": { + "homepage": "https://william278.net/project/husksync", + "repo": "https://github.com/WiIIiam278/HuskSync", + "issues": "https://github.com/WiIIiam278/HuskSync/issues" + }, + "environment": "server", + "entrypoints": { + "server": [ + "net.william278.husksync.FabricHuskSync" + ] + }, + "depends": { + "fabricloader": ">=${fabric_loader_version}", + "minecraft": ">=${fabric_minecraft_version}", + "fabric-api": "*" + }, + "suggests": { + "plan": "*" + }, + "mixins": [ + "husksync.mixins.json" + ], + "custom": { + "modmenu:api": true + } +} diff --git a/fabric/src/main/resources/husksync.mixins.json b/fabric/src/main/resources/husksync.mixins.json new file mode 100644 index 00000000..0e9ddce3 --- /dev/null +++ b/fabric/src/main/resources/husksync.mixins.json @@ -0,0 +1,17 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "net.william278.husksync.mixins", + "compatibilityLevel": "JAVA_17", + "server": [ + "ItemEntityMixin", + "PlayerEntityMixin", + "ServerPlayerEntityMixin", + "ServerPlayNetworkHandlerMixin", + "ServerWorldMixin" + ], + "client": [], + "injectors": { + "defaultRequire": 1 + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index e3d4036e..797566c4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ org.gradle.jvmargs='-Dfile.encoding=UTF-8' org.gradle.daemon=true javaVersion=17 -plugin_version=3.5.3 +plugin_version=3.6 plugin_archive=husksync plugin_description=A modern, cross-server player data synchronization system @@ -13,3 +13,11 @@ mariadb_driver_version=3.4.0 postgres_driver_version=42.7.3 mongodb_driver_version=5.1.0 snappy_version=1.1.10.5 + +fabric_minecraft_version=1.20.1 +fabric_loader_version=0.15.11 +fabric_yarn_mappings=1.20.1+build.10 +fabric_api_version=0.92.2+1.20.1 +adventure_platform_fabric_version=5.9.0 +fabric_permissions_api_version=0.2-SNAPSHOT +sgui_version=1.2.2+1.20 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index ccebba77..c1962a79 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index bdc9a83b..509c4a29 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 79a61d42..aeb74cbb 100755 --- a/gradlew +++ b/gradlew @@ -85,9 +85,6 @@ done APP_BASE_NAME=${0##*/} APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -197,6 +194,10 @@ if "$cygwin" || "$msys" ; then done fi + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + # Collect all arguments for the java command; # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # shell script including quotes and variable substitutions, so put them in diff --git a/settings.gradle b/settings.gradle index f0cfe17e..fefb6984 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,7 @@ pluginManagement { repositories { gradlePluginPortal() + maven { url 'https://maven.fabricmc.net/' } } } @@ -8,5 +9,6 @@ rootProject.name = 'HuskSync' include( 'common', 'bukkit', - 'paper' + 'paper', + 'fabric' ) \ No newline at end of file