feat: add support for Fabric targeting Minecraft 1.20.1 (#217)

* Upgrade the Fabric version and rewrite the code.

* Migrate the completed code of version 1.19.2.

* fabric: some events.

* Updated open source license to Apache 2.0.

* Add Plan analyzer support.

* Fix build.

* `UnsupportedOperationException`

* More fabric implementation work, update to v3's structure

* Suppress compiler warnings

* Add commands, adjust registration order

* Inventory and ender chest data/serializers

* Update license headers

* Fixup shaded library relocations

* Fix build

* Potion effects & location serializers

* Catch `Files.createDirectory(path);` in `#getDataFolder`

* Update fabric.mod.json metadata, correct icon

* Events for Fabric (#218)

* Added apache commons pool2 dependency

A NoClassDefFoundError would get thrown without this dependency. Relocation appears to not work very well either, so it has been excluded for now

* Added in Item Pickup and Drop events and mixins

* Update husksync.mixins.json

* Switch drop item event to using Network Handler mixin

* Implemented even more events

- Interact block (place too)
- Interact Entity
- Use Item
- Block Break
- Player damage
- Inventory Click (handles drops)
- Player Commands

* Re-implement the dropItem mixin

* Set dropItem mixin as cancellable

* deps: Include all bukkit runtime deps

* fix/fabric: Supply AudienceProvider to `ConsoleUser` constructor

* docs: credit Fabric porters :)

* fix: Item deserialization now working

* refactor: Remove inventory debug log

* docs: Update `fabric.mod.json`

* refactor: update with upstream changes

* fix: dangling JD comment

* fix: config file reference fixes

* refactor: optimize imports, fix relocation

* refactor: move tag references to common

* refactor: use lombok for data / serializer methods

* fix: bad annotating

* refactor: adjust callback formatting

* fabric: bump deps, refactor to match main branch

* fabric: more serializer type work

* feat: register more fabric data serializers

also fixes a compile issue on bukkit, and refactors the JSON serializer to be in the common module

* feat: implement remaining Fabric serializers

* feat: add on-the-fly DFU for Fabric

Now auto-upgrades item data to support version bumps. Also improved the schema a lil' bit.

* feat: add missing mixins

* feat: implement toKeep/toDrop option on Fabric

* feat: apply stats on sync

* build: append fabric MC version to file name

* feat: add HuskSync API support for Fabric

Also updates the docs

* refactor: fixup a deprecation in the wrong spot

* refactor: optimize fabric item serializing in-line with Bukkit

* feat: implement viewer GUIs on Fabric

* docs: Fabric is in Alpha for now

---------

Co-authored-by: hanbings <hanbings@hanbings.io>
Co-authored-by: Stampede <carterblowers01@gmail.com>
feat/data-edit-commands
William 5 months ago committed by GitHub
parent e3fb1762a1
commit 89368778f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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&mdash;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.

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

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

@ -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<ItemStack> 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<String> toAward, @NotNull Collection<String> toRevoke) {
private void setAdvancement(@NotNull HuskSync plugin,
@NotNull org.bukkit.advancement.Advancement advancement,
@NotNull Player player,
@NotNull BukkitUser user,
@NotNull Collection<String> toAward,
@NotNull Collection<String> 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);
}

@ -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<BukkitData.Items.Inventory>,
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<T extends Data & Adaptable> extends BukkitSerializer implements Serializer<T> {
/**
* @deprecated Use {@link Serializer.Json} in the common module instead
*/
@Deprecated(since = "2.6")
public class Json<T extends Data & Adaptable> extends Serializer.Json<T> {
private final Class<T> type;
public Json(@NotNull HuskSync plugin, Class<T> 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<T> 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;
}
}

@ -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<Data.Items.EnderChest> getEnderChest() {
return Optional.of(BukkitData.Items.EnderChest.adapt(
getMapPersister().persistLockedMaps(getBukkitPlayer().getEnderChest().getContents(), getBukkitPlayer())
getMapPersister().persistLockedMaps(getPlayer().getEnderChest().getContents(), getPlayer())
));
}
@NotNull
@Override
default Optional<Data.PotionEffects> getPotionEffects() {
return Optional.of(BukkitData.PotionEffects.from(getBukkitPlayer().getActivePotionEffects()));
return Optional.of(BukkitData.PotionEffects.from(getPlayer().getActivePotionEffects()));
}
@NotNull
@Override
default Optional<Data.Advancements> getAdvancements() {
return Optional.of(BukkitData.Advancements.adapt(getBukkitPlayer()));
return Optional.of(BukkitData.Advancements.adapt(getPlayer()));
}
@NotNull
@Override
default Optional<Data.Location> getLocation() {
return Optional.of(BukkitData.Location.adapt(getBukkitPlayer().getLocation()));
return Optional.of(BukkitData.Location.adapt(getPlayer().getLocation()));
}
@NotNull
@Override
default Optional<Data.Statistics> getStatistics() {
return Optional.of(BukkitData.Statistics.adapt(getBukkitPlayer()));
return Optional.of(BukkitData.Statistics.adapt(getPlayer()));
}
@NotNull
@Override
default Optional<Data.Health> getHealth() {
return Optional.of(BukkitData.Health.adapt(getBukkitPlayer()));
return Optional.of(BukkitData.Health.adapt(getPlayer()));
}
@NotNull
@Override
default Optional<Data.Hunger> getHunger() {
return Optional.of(BukkitData.Hunger.adapt(getBukkitPlayer()));
return Optional.of(BukkitData.Hunger.adapt(getPlayer()));
}
@NotNull
@Override
default Optional<Data.Attributes> getAttributes() {
return Optional.of(BukkitData.Attributes.adapt(getBukkitPlayer(), getPlugin()));
return Optional.of(BukkitData.Attributes.adapt(getPlayer(), getPlugin()));
}
@NotNull
@Override
default Optional<Data.Experience> getExperience() {
return Optional.of(BukkitData.Experience.adapt(getBukkitPlayer()));
return Optional.of(BukkitData.Experience.adapt(getPlayer()));
}
@NotNull
@Override
default Optional<Data.GameMode> getGameMode() {
return Optional.of(BukkitData.GameMode.adapt(getBukkitPlayer()));
return Optional.of(BukkitData.GameMode.adapt(getPlayer()));
}
@NotNull
@Override
default Optional<Data.FlightStatus> getFlightStatus() {
return Optional.of(BukkitData.FlightStatus.adapt(getBukkitPlayer()));
return Optional.of(BukkitData.FlightStatus.adapt(getPlayer()));
}
@NotNull
@Override
default Optional<Data.PersistentData> 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() {

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

@ -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'

@ -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<Migrator> getAvailableMigrators();
@NotNull
Map<Identifier, Data> getPlayerCustomDataStore(@NotNull OnlineUser user);
Map<UUID, Map<Identifier, Data>> getPlayerCustomDataStore();
@NotNull
default Map<Identifier, Data> getPlayerCustomDataStore(@NotNull OnlineUser user) {
if (getPlayerCustomDataStore().containsKey(user.getUuid())) {
return getPlayerCustomDataStore().get(user.getUuid());
}
final Map<Identifier, Data> 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
*

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

@ -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)"),

@ -45,6 +45,7 @@ public class InventoryCommand extends ItemsCommand {
@NotNull User user, boolean allowEdit) {
final Optional<Data.Items.Inventory> 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;

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

@ -30,8 +30,8 @@ public interface DataHolder {
@NotNull
Map<Identifier, Data> getData();
default Optional<? extends Data> getData(@NotNull Identifier identifier) {
return Optional.ofNullable(getData().get(identifier));
default Optional<? extends Data> 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) {

@ -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<T extends Data> {
@ -46,4 +48,26 @@ public interface Serializer<T extends Data> {
}
class Json<T extends Data & Adaptable> implements Serializer<T> {
private final HuskSync plugin;
private final Class<T> type;
public Json(@NotNull HuskSync plugin, @NotNull Class<T> 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);
}
}
}

@ -19,7 +19,6 @@
package net.william278.husksync.event;
@SuppressWarnings("unused")
public interface Cancellable extends Event {
default boolean isCancelled() {

@ -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();

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

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

@ -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&dagger; |
## 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&dagger; |
&dagger;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.

@ -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.

@ -12,23 +12,29 @@ HuskSync supports synchronising a wide range of different data elements, each of
<details>
<summary>&nbsp;<b>Are modded items supported?</b></summary>
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 &mdash; check out the [[Custom Data API]] for information on how to get your mod's data syncing!
**TL;DR** &mdash; 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 &mdash; test thoroughly!
</details>
<details>
<summary>&nbsp;<b>Are MMOItems / SlimeFun / ItemsAdder items supported?</b></summary>
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 &mdash; test thoroughly!
</details>
<details>
<summary>&nbsp;<b>Is Redis required? What is Redis?</b></summary>
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)
</details>

@ -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)

@ -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+) &mdash; 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
</details>
### 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

@ -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)

@ -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)

@ -0,0 +1,341 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync;
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<Identifier, Serializer<? extends Data>> serializers = Maps.newTreeMap(
SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR
);
private final Map<UUID, Map<Identifier, Data>> playerCustomDataStore = Maps.newConcurrentMap();
private final Map<String, Boolean> permissions = Maps.newHashMap();
private final List<Migrator> availableMigrators = Lists.newArrayList();
private final Set<UUID> 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<Command> 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<OnlineUser> getOnlineUsers() {
return minecraftServer.getPlayerManager().getPlayerList()
.stream().map(user -> (OnlineUser) FabricUser.adapt(user, this))
.collect(Collectors.toSet());
}
@Override
@NotNull
public Optional<OnlineUser> 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<LegacyConverter> getLegacyConverter() {
return Optional.empty();
}
@Override
@NotNull
public FabricHuskSync getPlugin() {
return this;
}
}

@ -0,0 +1,260 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.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
* </p>
* Retrieve an instance of the API class via {@link #getInstance()}.
*/
@SuppressWarnings("unused")
public class FabricHuskSyncAPI extends HuskSyncAPI {
/**
* <b>(Internal use only)</b> - 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;
}
/**
* <b>(Internal use only)</b> - 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<Optional<FabricData.Items.Inventory>> 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<Optional<ItemStack[]>> 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<FabricData.Items.Inventory> 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<ItemStack[]> 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<Optional<FabricData.Items.EnderChest>> 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<Optional<ItemStack[]>> 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<FabricData.Items.EnderChest> 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<ItemStack[]> 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);
}
}

@ -0,0 +1,153 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.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<ServerCommandSource> dispatcher) {
// Register brigadier command
final Predicate<ServerCommandSource> predicate = Permissions
.require(command.getPermission(), command.isOperatorCommand() ? 3 : 0);
final LiteralArgumentBuilder<ServerCommandSource> 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<String, Boolean> 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<ServerCommandSource> 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<ServerCommandSource> getBrigadierExecutor() {
return (context) -> {
command.onExecuted(
resolveExecutor(context.getSource()),
command.removeFirstArg(context.getInput().split(" "))
);
return 1;
};
}
private com.mojang.brigadier.suggestion.SuggestionProvider<ServerCommandSource> 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<HuskSync, Command> supplier;
Type(@NotNull Function<HuskSync, Command> supplier) {
this.supplier = supplier;
}
@NotNull
public Command createCommand(@NotNull HuskSync plugin) {
return supplier.apply(plugin);
}
@NotNull
public static List<Command> getCommands(@NotNull FabricHuskSync plugin) {
return Arrays.stream(values()).map(type -> type.createCommand(plugin)).toList();
}
}
}

@ -0,0 +1,808 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.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<String>) 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<ItemStack> 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<ItemStack> 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<ItemStack> 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<StatusEffectInstance> effects;
@NotNull
public static FabricData.PotionEffects from(@NotNull Collection<StatusEffectInstance> effects) {
return new FabricData.PotionEffects(effects);
}
@NotNull
public static FabricData.PotionEffects adapt(@NotNull Collection<Effect> 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<Effect> 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<Advancement> completed;
@NotNull
public static FabricData.Advancements adapt(@NotNull ServerPlayerEntity player) {
final MinecraftServer server = Objects.requireNonNull(player.getServer(), "Server is null");
final List<Advancement> advancements = Lists.newArrayList();
forEachAdvancement(server, advancement -> {
final AdvancementProgress advancementProgress = player.getAdvancementTracker().getProgress(advancement);
final Map<String, Date> 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<Advancement> 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<Advancement> record = completed.stream()
.filter(r -> r.getKey().equals(advancement.getId().toString()))
.findFirst();
if (record.isEmpty()) {
return;
}
final Map<String, Date> criteria = record.get().getCompletedCriteria();
final List<String> 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<String> toAward,
@NotNull List<String> 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<net.minecraft.advancement.Advancement> 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<String, Integer> genericStatistics;
@SerializedName("blocks")
private Map<String, Map<String, Integer>> blockStatistics;
@SerializedName("items")
private Map<String, Map<String, Integer>> itemStatistics;
@SerializedName("entities")
private Map<String, Map<String, Integer>> entityStatistics;
@NotNull
public static FabricData.Statistics adapt(@NotNull ServerPlayerEntity player) throws IllegalStateException {
// Adapt typed stats
final Map<String, Map<String, Integer>> 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<String, Integer> 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<String, Integer> 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<String, Integer> generic,
@NotNull Map<String, Map<String, Integer>> blocks,
@NotNull Map<String, Map<String, Integer>> items,
@NotNull Map<String, Map<String, Integer>> 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 <T> 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<T> statType = (StatType<T>) Registries.STAT_TYPE.get(typeId);
if (statType == null) {
return;
}
final Registry<T> 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<Attribute> attributes;
@NotNull
public static FabricData.Attributes adapt(@NotNull ServerPlayerEntity player, @NotNull HuskSync plugin) {
final List<Attribute> 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<Modifier> 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<Attribute> 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<Attribute> 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();
}
}
}

@ -0,0 +1,288 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.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<FabricData.Items.Inventory>,
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<FabricData.Items.EnderChest>,
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<Object>((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<FabricData.PotionEffects> {
private static final TypeToken<List<Data.PotionEffects.Effect>> 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<FabricData.Advancements> {
private static final TypeToken<List<Data.Advancements.Advancement>> 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());
}
}
}

@ -0,0 +1,186 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.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<? extends Data> 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<Data.Items.Inventory> 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<Data.Items.EnderChest> getEnderChest() {
return Optional.of(FabricData.Items.EnderChest.adapt(
getPlayer().getEnderChestInventory().stacks
));
}
@NotNull
@Override
default Optional<Data.PotionEffects> getPotionEffects() {
return Optional.of(FabricData.PotionEffects.from(getPlayer().getActiveStatusEffects().values()));
}
@NotNull
@Override
default Optional<Data.Advancements> getAdvancements() {
return Optional.of(FabricData.Advancements.adapt(getPlayer()));
}
@NotNull
@Override
default Optional<Data.Location> getLocation() {
return Optional.of(FabricData.Location.adapt(getPlayer()));
}
@NotNull
@Override
default Optional<Data.Statistics> getStatistics() {
return Optional.of(FabricData.Statistics.adapt(getPlayer()));
}
@Override
@NotNull
default Optional<Data.Attributes> getAttributes() {
return Optional.of(FabricData.Attributes.adapt(getPlayer(), getPlugin()));
}
@NotNull
@Override
default Optional<Data.Health> getHealth() {
return Optional.of(FabricData.Health.adapt(getPlayer()));
}
@NotNull
@Override
default Optional<Data.Hunger> getHunger() {
return Optional.of(FabricData.Hunger.adapt(getPlayer()));
}
@NotNull
@Override
default Optional<Data.Experience> getExperience() {
return Optional.of(FabricData.Experience.adapt(getPlayer()));
}
@NotNull
@Override
default Optional<Data.GameMode> getGameMode() {
return Optional.of(FabricData.GameMode.adapt(getPlayer()));
}
@NotNull
@Override
default Optional<Data.FlightStatus> getFlightStatus() {
return Optional.of(FabricData.FlightStatus.adapt(getPlayer()));
}
@NotNull
@Override
default Optional<Data.PersistentData> 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<Identifier, Data> getCustomDataStore();
}

@ -0,0 +1,90 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.event;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.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<DataSaveEvent> {
@NotNull
Event<FabricDataSaveCallback> 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<User, DataSnapshot.Packed, HuskSync, DataSaveEvent> 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<FabricDataSaveCallback> getEvent() {
return EVENT;
}
};
}

@ -0,0 +1,30 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.event;
import net.minecraft.util.ActionResult;
import org.jetbrains.annotations.NotNull;
public interface FabricEventCallback<E extends Event> {
@NotNull
ActionResult invoke(@NotNull E event);
}

@ -0,0 +1,70 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.event;
import net.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 <T extends Event> 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<T> invoker = (FabricEventCallback<T>) 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());
}
}

@ -0,0 +1,90 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.event;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.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<PreSyncEvent> {
@NotNull
Event<FabricPreSyncCallback> 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<OnlineUser, DataSnapshot.Packed, HuskSync, PreSyncEvent> 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<FabricPreSyncCallback> getEvent() {
return EVENT;
}
};
}

@ -0,0 +1,61 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.event;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.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<SyncCompleteEvent> {
@NotNull
Event<FabricSyncCompleteCallback> EVENT = EventFactory.createArrayBacked(FabricSyncCompleteCallback.class,
(listeners) -> (event) -> {
for (FabricSyncCompleteCallback listener : listeners) {
listener.invoke(event);
}
return ActionResult.PASS;
});
@NotNull
BiFunction<OnlineUser, HuskSync, SyncCompleteEvent> SUPPLIER = (user, plugin) ->
new SyncCompleteEvent() {
@NotNull
@Override
public OnlineUser getUser() {
return user;
}
@NotNull
@SuppressWarnings("unused")
public Event<FabricSyncCompleteCallback> getEvent() {
return EVENT;
}
};
}

@ -0,0 +1,48 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.event;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.util.ActionResult;
import org.jetbrains.annotations.NotNull;
public interface InventoryClickCallback {
@NotNull
Event<InventoryClickCallback> 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);
}

@ -0,0 +1,48 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.event;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.util.ActionResult;
import org.jetbrains.annotations.NotNull;
public interface ItemDropCallback {
@NotNull
Event<ItemDropCallback> 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);
}

@ -0,0 +1,48 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.event;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.util.ActionResult;
import org.jetbrains.annotations.NotNull;
public interface ItemPickupCallback {
@NotNull
Event<ItemPickupCallback> 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);
}

@ -0,0 +1,47 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.event;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.util.ActionResult;
import org.jetbrains.annotations.NotNull;
public interface PlayerCommandCallback {
@NotNull
Event<PlayerCommandCallback> 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);
}

@ -0,0 +1,44 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.event;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.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<PlayerDeathDropsCallback> 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);
}

@ -0,0 +1,39 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.event;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
import net.minecraft.server.world.ServerWorld;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
public interface WorldSaveCallback {
@NotNull
Event<WorldSaveCallback> EVENT = EventFactory.createArrayBacked(
WorldSaveCallback.class,
(listeners) -> (world) -> Arrays.stream(listeners).forEach(listener -> listener.save(world))
);
void save(@NotNull ServerWorld world);
}

@ -0,0 +1,153 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.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<ItemStack> 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;
}
}

@ -0,0 +1,41 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.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));
}
}

@ -0,0 +1,81 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.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;
}
}

@ -0,0 +1,107 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.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();
}
}
}

@ -0,0 +1,46 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.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<ItemEntity> ci) {
ServerPlayerEntity player = (ServerPlayerEntity) (Object) this;
ActionResult result = ItemDropCallback.EVENT.invoker().interact(player, stack);
if (result == ActionResult.FAIL) {
ci.cancel();
}
}
}

@ -0,0 +1,37 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.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);
}
}

@ -0,0 +1,175 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.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<Data.Items> onClose) {
plugin.runSync(
() -> new ItemViewerGui(size, player, title, (FabricData.Items) items, onClose, editable, plugin).open()
);
}
private static class ItemViewerGui extends SimpleGui {
private final Consumer<Data.Items> 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<Data.Items> 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<GenericContainerScreenHandler> 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;
}
}

@ -0,0 +1,76 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.util;
import 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> T getRegistryValue(@NotNull Registry<T> registry, @NotNull String keyString) {
final Identifier key = Identifier.tryParse(keyString);
return key != null ? registry.get(key) : null;
}
@Nullable
private static <T> String getRegistryKey(@NotNull Registry<T> registry, @NotNull T value) {
final Identifier key = registry.getId(value);
return key != null ? key.toString() : null;
}
}

@ -0,0 +1,137 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.util;
import 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<Void> 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
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

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

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

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

Binary file not shown.

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

7
gradlew vendored

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

@ -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'
)
Loading…
Cancel
Save