mirror of
https://github.com/WiIIiam278/HuskSync.git
synced 2025-12-23 16:49:19 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24ba209f8f | ||
|
|
05d588f681 | ||
|
|
9aa3606f54 | ||
|
|
fc05e4b17a | ||
|
|
7b2b47de83 | ||
|
|
be0b4e3397 | ||
|
|
dd1ba594de | ||
|
|
89368778f3 | ||
|
|
e3fb1762a1 | ||
|
|
516c243df8 | ||
|
|
b7aa75fcd5 | ||
|
|
549f013e0f | ||
|
|
14c56af465 | ||
|
|
bee01dd15a | ||
|
|
e97551e67e | ||
|
|
97023e8425 | ||
|
|
4fa7106a46 | ||
|
|
e0b81e4c76 | ||
|
|
c4adec3082 | ||
|
|
107238360c | ||
|
|
6141adbdb9 | ||
|
|
eaa2ed74a6 | ||
|
|
44c652c452 | ||
|
|
78cf6bff63 | ||
|
|
8ad4158ec0 | ||
|
|
405e6d7162 | ||
|
|
cff1c8f982 | ||
|
|
f43ca2f043 | ||
|
|
3114ab1a62 | ||
|
|
2da9749b0c | ||
|
|
d4d510e100 | ||
|
|
550ea26097 | ||
|
|
2b1e72a42e |
10
README.md
10
README.md
@@ -44,15 +44,15 @@
|
||||
**Ready?** [It's syncing time!](https://william278.net/docs/husksync/setup)
|
||||
|
||||
## Setup
|
||||
Requires a MySQL/Mongo/PostgreSQL database, a Redis (v5.0+) server and any number of Spigot-based 1.17.1+ Minecraft servers, running Java 17+.
|
||||
Requires a MySQL/Mongo/PostgreSQL database, a Redis (v5.0+) server and a network of Spigot (1.17.1+) or Fabric (1.20.1) Minecraft servers, running Java 17+.
|
||||
|
||||
1. Place the plugin jar file in the /plugins/ directory of each Spigot server. You do not need to install HuskSync as a proxy plugin.
|
||||
1. Place the plugin jar file in the `/plugins` or `/mods` directory of each Spigot/Fabric server. You do not need to install HuskSync as a proxy plugin.
|
||||
2. Start, then stop every server to let HuskSync generate the config file.
|
||||
3. Navigate to the HuskSync config file on each server (~/plugins/HuskSync/config.yml) and fill in both your database and Redis server credentials.
|
||||
3. Navigate to the HuskSync config file on each server and fill in both your database and Redis server credentials.
|
||||
4. Start every server again and synchronization will begin.
|
||||
|
||||
## Development
|
||||
To build HuskSync, simply run the following in the root of the repository:
|
||||
To build HuskSync, simply run the following in the root of the repository (building requires Java 17). Builds will be output in `/target`:
|
||||
|
||||
```bash
|
||||
./gradlew clean build
|
||||
@@ -66,7 +66,7 @@ HuskSync is licensed under the Apache 2.0 license.
|
||||
Contributions to the project are welcome—feel free to open a pull request with new features, improvements and/or fixes!
|
||||
|
||||
### Support
|
||||
Due to its complexity, official binaries and customer support for HuskSync is provided through a paid model. This means that support is only available to users who have purchased a license to the plugin from Spigot, Polymart, Craftaro, or BuiltByBit and have provided proof of purchase. Please join our Discord server if you have done so and need help!
|
||||
Due to its complexity, official binaries and customer support for HuskSync is provided through a paid model. This means that support is only available to users who have purchased a license to the plugin from Spigot, Polymart, or BuiltByBit and have provided proof of purchase. Please join our Discord server if you have done so and need help!
|
||||
|
||||
### Translations
|
||||
Translations of the plugin locales are welcome to help make the plugin more accessible. Please submit a pull request with your translations as a `.yml` file.
|
||||
|
||||
21
build.gradle
21
build.gradle
@@ -69,6 +69,7 @@ allprojects {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
|
||||
maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' }
|
||||
maven { url "https://repo.dmulloy2.net/repository/public/" }
|
||||
maven { url 'https://repo.codemc.io/repository/maven-public/' }
|
||||
@@ -117,8 +118,13 @@ subprojects {
|
||||
archiveClassifier.set('')
|
||||
}
|
||||
|
||||
// Append the Minecraft to the version for Fabric projects
|
||||
if (project.name == 'fabric') {
|
||||
version += "+mc.${fabric_minecraft_version}"
|
||||
}
|
||||
|
||||
// API publishing
|
||||
if (['common', 'bukkit'].contains(project.name)) {
|
||||
if (['common', 'bukkit', 'fabric'].contains(project.name)) {
|
||||
java {
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
@@ -157,6 +163,19 @@ subprojects {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (['fabric'].contains(project.name)) {
|
||||
publications {
|
||||
mavenJavaFabric(MavenPublication) {
|
||||
groupId = 'net.william278.husksync'
|
||||
artifactId = 'husksync-fabric'
|
||||
version = "$rootProject.version"
|
||||
artifact shadowJar
|
||||
artifact sourcesJar
|
||||
artifact javadocJar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,13 @@ dependencies {
|
||||
implementation 'net.william278:mapdataapi:1.0.3'
|
||||
implementation 'net.william278:andjam:1.0.2'
|
||||
implementation 'me.lucko:commodore:2.2'
|
||||
implementation 'net.kyori:adventure-platform-bukkit:4.3.2'
|
||||
implementation 'dev.triumphteam:triumph-gui:3.1.7'
|
||||
implementation 'net.kyori:adventure-platform-bukkit:4.3.3'
|
||||
implementation 'dev.triumphteam:triumph-gui:3.1.10'
|
||||
implementation 'space.arim.morepaperlib:morepaperlib:0.4.4'
|
||||
implementation 'de.tr7zw:item-nbt-api:2.12.4'
|
||||
implementation 'de.tr7zw:item-nbt-api:2.13.0'
|
||||
|
||||
compileOnly 'org.spigotmc:spigot-api:1.17.1-R0.1-SNAPSHOT'
|
||||
compileOnly 'com.github.retrooper.packetevents:spigot:2.3.0'
|
||||
compileOnly 'com.comphenix.protocol:ProtocolLib:5.1.0'
|
||||
compileOnly 'org.projectlombok:lombok:1.18.32'
|
||||
compileOnly 'commons-io:commons-io:2.16.1'
|
||||
|
||||
@@ -46,7 +46,6 @@ import net.william278.husksync.database.PostgresDatabase;
|
||||
import net.william278.husksync.event.BukkitEventDispatcher;
|
||||
import net.william278.husksync.hook.PlanHook;
|
||||
import net.william278.husksync.listener.BukkitEventListener;
|
||||
import net.william278.husksync.listener.EventListener;
|
||||
import net.william278.husksync.migrator.LegacyMigrator;
|
||||
import net.william278.husksync.migrator.Migrator;
|
||||
import net.william278.husksync.migrator.MpdbMigrator;
|
||||
@@ -61,6 +60,7 @@ import net.william278.husksync.util.LegacyConverter;
|
||||
import org.bstats.bukkit.Metrics;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.map.MapView;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import space.arim.morepaperlib.MorePaperLib;
|
||||
@@ -86,7 +86,9 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
private static final int METRICS_ID = 13140;
|
||||
private static final String PLATFORM_TYPE_ID = "bukkit";
|
||||
|
||||
private final Map<Identifier, Serializer<? extends Data>> serializers = Maps.newLinkedHashMap();
|
||||
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<Integer, MapView> mapViews = Maps.newConcurrentMap();
|
||||
private final List<Migrator> availableMigrators = Lists.newArrayList();
|
||||
@@ -98,7 +100,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
private MorePaperLib paperLib;
|
||||
private Database database;
|
||||
private RedisManager redisManager;
|
||||
private EventListener eventListener;
|
||||
private BukkitEventListener eventListener;
|
||||
private DataAdapter dataAdapter;
|
||||
private DataSyncer dataSyncer;
|
||||
private LegacyConverter legacyConverter;
|
||||
@@ -113,11 +115,10 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
private Server serverName;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
public void onLoad() {
|
||||
// Initial plugin setup
|
||||
this.disabling = false;
|
||||
this.gson = createGson();
|
||||
this.audiences = BukkitAudiences.create(this);
|
||||
this.paperLib = new MorePaperLib(this);
|
||||
|
||||
// Load settings and locales
|
||||
@@ -127,6 +128,13 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
loadServer();
|
||||
});
|
||||
|
||||
this.eventListener = createEventListener();
|
||||
eventListener.onLoad();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
this.audiences = BukkitAudiences.create(this);
|
||||
// Prepare data adapter
|
||||
initialize("data adapter", (plugin) -> {
|
||||
if (settings.getSynchronization().isCompressData()) {
|
||||
@@ -138,19 +146,20 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
|
||||
// Prepare serializers
|
||||
initialize("data serializers", (plugin) -> {
|
||||
registerSerializer(Identifier.PERSISTENT_DATA, new BukkitSerializer.PersistentData(this));
|
||||
registerSerializer(Identifier.INVENTORY, new BukkitSerializer.Inventory(this));
|
||||
registerSerializer(Identifier.ENDER_CHEST, new BukkitSerializer.EnderChest(this));
|
||||
registerSerializer(Identifier.ADVANCEMENTS, new BukkitSerializer.Advancements(this));
|
||||
registerSerializer(Identifier.LOCATION, new BukkitSerializer.Json<>(this, BukkitData.Location.class));
|
||||
registerSerializer(Identifier.HEALTH, new BukkitSerializer.Json<>(this, BukkitData.Health.class));
|
||||
registerSerializer(Identifier.HUNGER, new BukkitSerializer.Json<>(this, BukkitData.Hunger.class));
|
||||
registerSerializer(Identifier.ATTRIBUTES, new BukkitSerializer.Json<>(this, BukkitData.Attributes.class));
|
||||
registerSerializer(Identifier.GAME_MODE, new BukkitSerializer.Json<>(this, BukkitData.GameMode.class));
|
||||
registerSerializer(Identifier.FLIGHT_STATUS, new BukkitSerializer.Json<>(this, BukkitData.FlightStatus.class));
|
||||
registerSerializer(Identifier.STATISTICS, new Serializer.Json<>(this, BukkitData.Statistics.class));
|
||||
registerSerializer(Identifier.POTION_EFFECTS, new BukkitSerializer.PotionEffects(this));
|
||||
registerSerializer(Identifier.STATISTICS, new BukkitSerializer.Json<>(this, BukkitData.Statistics.class));
|
||||
registerSerializer(Identifier.EXPERIENCE, new BukkitSerializer.Json<>(this, BukkitData.Experience.class));
|
||||
registerSerializer(Identifier.PERSISTENT_DATA, new BukkitSerializer.PersistentData(this));
|
||||
registerSerializer(Identifier.GAME_MODE, new Serializer.Json<>(this, BukkitData.GameMode.class));
|
||||
registerSerializer(Identifier.FLIGHT_STATUS, new Serializer.Json<>(this, BukkitData.FlightStatus.class));
|
||||
registerSerializer(Identifier.ATTRIBUTES, new Serializer.Json<>(this, BukkitData.Attributes.class));
|
||||
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();
|
||||
});
|
||||
|
||||
// Setup available migrators
|
||||
@@ -185,7 +194,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
});
|
||||
|
||||
// Register events
|
||||
initialize("events", (plugin) -> this.eventListener = createEventListener());
|
||||
initialize("events", (plugin) -> eventListener.onEnable());
|
||||
|
||||
// Register commands
|
||||
initialize("commands", (plugin) -> BukkitCommand.Type.registerCommands(this));
|
||||
@@ -272,7 +281,8 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
|
||||
@Override
|
||||
public boolean isDependencyLoaded(@NotNull String name) {
|
||||
return getServer().getPluginManager().getPlugin(name) != null;
|
||||
final Plugin plugin = getServer().getPluginManager().getPlugin(name);
|
||||
return plugin != null && plugin.isEnabled();
|
||||
}
|
||||
|
||||
// Register bStats metrics
|
||||
@@ -284,7 +294,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
|
||||
try {
|
||||
new Metrics(this, metricsId);
|
||||
} catch (Throwable e) {
|
||||
log(Level.WARNING, "Failed to register bStats metrics (" + e.getMessage() + ")");
|
||||
log(Level.WARNING, "Failed to register bStats metrics (%s)".formatted(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@@ -59,6 +60,10 @@ public class BukkitHuskSyncAPI extends HuskSyncAPI {
|
||||
*/
|
||||
@NotNull
|
||||
public static BukkitHuskSyncAPI getInstance() {
|
||||
if (!JavaPlugin.getProvidingPlugin(BukkitHuskSyncAPI.class).getName().equals("HuskSync")) {
|
||||
throw new NotRegisteredException("This is likely because you have shaded HuskSync into your plugin JAR " +
|
||||
"and need to fix your maven/gradle/build script so that it *compiles against* HuskSync instead.");
|
||||
}
|
||||
if (instance == null) {
|
||||
throw new NotRegisteredException();
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import de.tr7zw.changeme.nbtapi.NBTCompound;
|
||||
import de.tr7zw.changeme.nbtapi.NBTPersistentDataContainer;
|
||||
import lombok.*;
|
||||
import net.william278.desertwell.util.ThrowingConsumer;
|
||||
import net.william278.desertwell.util.Version;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.Adaptable;
|
||||
@@ -69,7 +70,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 +127,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 +173,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 +201,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 +211,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);
|
||||
}
|
||||
|
||||
@@ -237,21 +238,20 @@ public abstract class BukkitData implements Data {
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.PotionEffects adapt(@NotNull Collection<Effect> effects) {
|
||||
return from(
|
||||
effects.stream()
|
||||
.map(effect -> new PotionEffect(
|
||||
Objects.requireNonNull(
|
||||
PotionEffectType.getByName(effect.type()),
|
||||
"Invalid potion effect type"
|
||||
),
|
||||
effect.duration(),
|
||||
effect.amplifier(),
|
||||
effect.isAmbient(),
|
||||
effect.showParticles(),
|
||||
effect.hasIcon()
|
||||
))
|
||||
.toList()
|
||||
);
|
||||
return from(effects.stream()
|
||||
.map(effect -> {
|
||||
final PotionEffectType type = matchEffectType(effect.type());
|
||||
return type != null ? new PotionEffect(
|
||||
type,
|
||||
effect.duration(),
|
||||
effect.amplifier(),
|
||||
effect.isAmbient(),
|
||||
effect.showParticles(),
|
||||
effect.hasIcon()
|
||||
) : null;
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.toList());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -342,9 +342,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();
|
||||
@@ -356,7 +359,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);
|
||||
}
|
||||
@@ -569,7 +573,7 @@ public abstract class BukkitData implements Data {
|
||||
// We don't sync unmodified or disabled attributes
|
||||
return;
|
||||
}
|
||||
attributes.add(adapt(instance));
|
||||
attributes.add(adapt(instance, plugin.getMinecraftVersion()));
|
||||
});
|
||||
return new BukkitData.Attributes(attributes);
|
||||
}
|
||||
@@ -588,18 +592,18 @@ public abstract class BukkitData implements Data {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Attribute adapt(@NotNull AttributeInstance instance) {
|
||||
private static Attribute adapt(@NotNull AttributeInstance instance, @NotNull Version version) {
|
||||
return new Attribute(
|
||||
instance.getAttribute().getKey().toString(),
|
||||
instance.getBaseValue(),
|
||||
instance.getModifiers().stream().map(BukkitData.Attributes::adapt).collect(Collectors.toSet())
|
||||
instance.getModifiers().stream().map(m -> adapt(m, version)).collect(Collectors.toSet())
|
||||
);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Modifier adapt(@NotNull AttributeModifier modifier) {
|
||||
private static Modifier adapt(@NotNull AttributeModifier modifier, @NotNull Version version) {
|
||||
return new Modifier(
|
||||
modifier.getUniqueId(),
|
||||
version.compareTo(Version.fromString("1.21")) >= 0 ? null : modifier.getUniqueId(),
|
||||
modifier.getName(),
|
||||
modifier.getAmount(),
|
||||
modifier.getOperation().ordinal(),
|
||||
@@ -616,7 +620,7 @@ public abstract class BukkitData implements Data {
|
||||
if (instance == null) {
|
||||
return;
|
||||
}
|
||||
instance.setBaseValue(attribute == null ? instance.getDefaultValue() : instance.getBaseValue());
|
||||
instance.setBaseValue(attribute == null ? instance.getDefaultValue() : attribute.baseValue());
|
||||
instance.getModifiers().forEach(instance::removeModifier);
|
||||
if (attribute != null) {
|
||||
attribute.modifiers().forEach(modifier -> instance.addModifier(new AttributeModifier(
|
||||
@@ -640,24 +644,38 @@ public abstract class BukkitData implements Data {
|
||||
private double health;
|
||||
@SerializedName("health_scale")
|
||||
private double healthScale;
|
||||
@SerializedName("is_health_scaled")
|
||||
private boolean isHealthScaled;
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.Health from(double health, double healthScale) {
|
||||
return new BukkitData.Health(health, healthScale);
|
||||
public static BukkitData.Health from(double health, double scale, boolean isScaled) {
|
||||
return new BukkitData.Health(health, scale, isScaled);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #from(double, double, boolean)} instead
|
||||
*/
|
||||
@NotNull
|
||||
@Deprecated(since = "3.5.4")
|
||||
public static BukkitData.Health from(double health, double scale) {
|
||||
return from(health, scale, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #from(double, double, boolean)} instead
|
||||
*/
|
||||
@NotNull
|
||||
@Deprecated(forRemoval = true, since = "3.5")
|
||||
@SuppressWarnings("unused")
|
||||
public static BukkitData.Health from(double health, double maxHealth, double healthScale) {
|
||||
return from(health, healthScale);
|
||||
public static BukkitData.Health from(double health, @SuppressWarnings("unused") double max, double scale) {
|
||||
return from(health, scale, false);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static BukkitData.Health adapt(@NotNull Player player) {
|
||||
return from(
|
||||
player.getHealth(),
|
||||
player.isHealthScaled() ? player.getHealthScale() : 0d
|
||||
player.getHealthScale(),
|
||||
player.isHealthScaled()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -675,13 +693,8 @@ public abstract class BukkitData implements Data {
|
||||
|
||||
// Set health scale
|
||||
try {
|
||||
if (healthScale != 0d) {
|
||||
player.setHealthScaled(true);
|
||||
player.setHealthScale(healthScale);
|
||||
} else {
|
||||
player.setHealthScaled(false);
|
||||
player.setHealthScale(player.getMaxHealth());
|
||||
}
|
||||
player.setHealthScale(healthScale);
|
||||
player.setHealthScaled(isHealthScaled);
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.WARNING, "Error setting %s's health scale to %s".formatted(player.getName(), healthScale), e);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
package net.william278.husksync.listener;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.BukkitData;
|
||||
import net.william278.husksync.user.BukkitUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
@@ -40,21 +39,38 @@ import java.util.stream.Collectors;
|
||||
public class BukkitEventListener extends EventListener implements BukkitJoinEventListener, BukkitQuitEventListener,
|
||||
BukkitDeathEventListener, Listener {
|
||||
|
||||
protected final LockedHandler lockedHandler;
|
||||
protected LockedHandler lockedHandler;
|
||||
|
||||
public BukkitEventListener(@NotNull BukkitHuskSync plugin) {
|
||||
super(plugin);
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
this.lockedHandler = createLockedHandler(plugin);
|
||||
}
|
||||
|
||||
public void onLoad() {
|
||||
this.lockedHandler = createLockedHandler((BukkitHuskSync) plugin);
|
||||
}
|
||||
|
||||
public void onEnable() {
|
||||
getPlugin().getServer().getPluginManager().registerEvents(this, getPlugin());
|
||||
lockedHandler.onEnable();
|
||||
}
|
||||
|
||||
public void handlePluginDisable() {
|
||||
super.handlePluginDisable();
|
||||
lockedHandler.onDisable();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private LockedHandler createLockedHandler(@NotNull BukkitHuskSync plugin) {
|
||||
if (getPlugin().isDependencyLoaded("ProtocolLib") && getPlugin().getSettings().isCancelPackets()) {
|
||||
return new BukkitLockedPacketListener(plugin);
|
||||
} else {
|
||||
if (!getPlugin().getSettings().isCancelPackets()) {
|
||||
return new BukkitLockedEventListener(plugin);
|
||||
}
|
||||
if (getPlugin().isDependencyLoaded("PacketEvents")) {
|
||||
return new BukkitPacketEventsLockedPacketListener(plugin);
|
||||
} else if (getPlugin().isDependencyLoaded("ProtocolLib")) {
|
||||
return new BukkitProtocolLibLockedPacketListener(plugin);
|
||||
}
|
||||
|
||||
return new BukkitLockedEventListener(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -134,8 +150,8 @@ public class BukkitEventListener extends EventListener implements BukkitJoinEven
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public HuskSync getPlugin() {
|
||||
return plugin;
|
||||
public BukkitHuskSync getPlugin() {
|
||||
return (BukkitHuskSync) plugin;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -50,6 +50,10 @@ public class BukkitLockedEventListener implements LockedHandler, Listener {
|
||||
|
||||
protected BukkitLockedEventListener(@NotNull BukkitHuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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 com.github.retrooper.packetevents.PacketEvents;
|
||||
import com.github.retrooper.packetevents.event.PacketListenerAbstract;
|
||||
import com.github.retrooper.packetevents.event.PacketListenerPriority;
|
||||
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
|
||||
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
|
||||
import com.google.common.collect.Sets;
|
||||
import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BukkitPacketEventsLockedPacketListener extends BukkitLockedEventListener implements LockedHandler {
|
||||
|
||||
protected BukkitPacketEventsLockedPacketListener(@NotNull BukkitHuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
super.onLoad();
|
||||
PacketEvents.setAPI(SpigotPacketEventsBuilder.build(getPlugin()));
|
||||
PacketEvents.getAPI().getSettings().reEncodeByDefault(false)
|
||||
.checkForUpdates(false)
|
||||
.bStats(true);
|
||||
PacketEvents.getAPI().load();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
super.onEnable();
|
||||
PacketEvents.getAPI().getEventManager().registerListener(new PlayerPacketAdapter(this));
|
||||
PacketEvents.getAPI().init();
|
||||
plugin.log(Level.INFO, "Using PacketEvents to cancel packets for locked players");
|
||||
}
|
||||
|
||||
private static class PlayerPacketAdapter extends PacketListenerAbstract {
|
||||
|
||||
private static final Set<PacketType.Play.Client> ALLOWED_PACKETS = Set.of(
|
||||
PacketType.Play.Client.KEEP_ALIVE, PacketType.Play.Client.PONG, PacketType.Play.Client.PLUGIN_MESSAGE, // Connection packets
|
||||
PacketType.Play.Client.CHAT_MESSAGE, PacketType.Play.Client.CHAT_COMMAND, PacketType.Play.Client.CHAT_SESSION_UPDATE, // Chat / command packets
|
||||
PacketType.Play.Client.PLAYER_POSITION, PacketType.Play.Client.PLAYER_POSITION_AND_ROTATION, PacketType.Play.Client.PLAYER_ROTATION, // Movement packets
|
||||
PacketType.Play.Client.HELD_ITEM_CHANGE, PacketType.Play.Client.ANIMATION, PacketType.Play.Client.TELEPORT_CONFIRM, // Animation packets
|
||||
PacketType.Play.Client.CLIENT_SETTINGS // Video setting packets
|
||||
);
|
||||
|
||||
private static final Set<PacketType.Play.Client> CANCEL_PACKETS = getPacketsToListenFor();
|
||||
|
||||
|
||||
private final BukkitPacketEventsLockedPacketListener listener;
|
||||
|
||||
public PlayerPacketAdapter(@NotNull BukkitPacketEventsLockedPacketListener listener) {
|
||||
super(PacketListenerPriority.HIGH);
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPacketReceive(PacketReceiveEvent event) {
|
||||
if(!(event.getPacketType() instanceof PacketType.Play.Client client)) {
|
||||
return;
|
||||
}
|
||||
if (!CANCEL_PACKETS.contains(client)) {
|
||||
return;
|
||||
}
|
||||
if (listener.cancelPlayerEvent(event.getUser().getUUID())) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the set of ALL Server packets, excluding the set of allowed packets
|
||||
@NotNull
|
||||
private static Set<PacketType.Play.Client> getPacketsToListenFor() {
|
||||
return Sets.difference(
|
||||
Sets.newHashSet(PacketType.Play.Client.values()),
|
||||
ALLOWED_PACKETS
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -34,10 +34,15 @@ import java.util.stream.Collectors;
|
||||
|
||||
import static com.comphenix.protocol.PacketType.Play.Client;
|
||||
|
||||
public class BukkitLockedPacketListener extends BukkitLockedEventListener implements LockedHandler {
|
||||
public class BukkitProtocolLibLockedPacketListener extends BukkitLockedEventListener implements LockedHandler {
|
||||
|
||||
protected BukkitLockedPacketListener(@NotNull BukkitHuskSync plugin) {
|
||||
protected BukkitProtocolLibLockedPacketListener(@NotNull BukkitHuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
super.onEnable();
|
||||
ProtocolLibrary.getProtocolManager().addPacketListener(new PlayerPacketAdapter(this));
|
||||
plugin.log(Level.INFO, "Using ProtocolLib to cancel packets for locked players");
|
||||
}
|
||||
@@ -53,9 +58,9 @@ public class BukkitLockedPacketListener extends BukkitLockedEventListener implem
|
||||
Client.SETTINGS // Video setting packets
|
||||
);
|
||||
|
||||
private final BukkitLockedPacketListener listener;
|
||||
private final BukkitProtocolLibLockedPacketListener listener;
|
||||
|
||||
public PlayerPacketAdapter(@NotNull BukkitLockedPacketListener listener) {
|
||||
public PlayerPacketAdapter(@NotNull BukkitProtocolLibLockedPacketListener listener) {
|
||||
super(listener.getPlugin(), ListenerPriority.HIGHEST, getPacketsToListenFor());
|
||||
this.listener = listener;
|
||||
}
|
||||
@@ -330,7 +330,7 @@ public class LegacyMigrator extends Migrator {
|
||||
))
|
||||
|
||||
// Health, hunger, experience & game mode
|
||||
.health(BukkitData.Health.from(health, healthScale))
|
||||
.health(BukkitData.Health.from(health, healthScale, false))
|
||||
.hunger(BukkitData.Hunger.from(hunger, saturation, saturationExhaustion))
|
||||
.experience(BukkitData.Experience.from(totalExp, expLevel, expProgress))
|
||||
.gameMode(BukkitData.GameMode.from(gameMode))
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,38 +19,44 @@
|
||||
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Registry;
|
||||
import org.bukkit.Statistic;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.attribute.Attribute;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
// Utility class for adapting "Keyed" Bukkit objects
|
||||
public final class BukkitKeyedAdapter {
|
||||
|
||||
@Nullable
|
||||
public static Statistic matchStatistic(@NotNull String key) {
|
||||
return Registry.STATISTIC.get(Objects.requireNonNull(NamespacedKey.fromString(key), "Null key"));
|
||||
return getRegistryValue(Registry.STATISTIC, key);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static EntityType matchEntityType(@NotNull String key) {
|
||||
return Registry.ENTITY_TYPE.get(Objects.requireNonNull(NamespacedKey.fromString(key), "Null key"));
|
||||
return getRegistryValue(Registry.ENTITY_TYPE, key);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Material matchMaterial(@NotNull String key) {
|
||||
return Registry.MATERIAL.get(Objects.requireNonNull(NamespacedKey.fromString(key), "Null key"));
|
||||
return getRegistryValue(Registry.MATERIAL, key);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Attribute matchAttribute(@NotNull String key) {
|
||||
return Registry.ATTRIBUTE.get(Objects.requireNonNull(NamespacedKey.fromString(key), "Null key"));
|
||||
return getRegistryValue(Registry.ATTRIBUTE, key);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static PotionEffectType matchEffectType(@NotNull String key) {
|
||||
return PotionEffectType.getByName(key); // No registry for this in 1.17 API
|
||||
}
|
||||
|
||||
private static <T extends Keyed> T getRegistryValue(@NotNull Registry<T> registry, @NotNull String keyString) {
|
||||
final NamespacedKey key = NamespacedKey.fromString(keyString);
|
||||
return key != null ? registry.get(key) : null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -82,32 +82,33 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
||||
|
||||
final JSONObject status = object.getJSONObject("status_data");
|
||||
final HashMap<Identifier, Data> containers = Maps.newHashMap();
|
||||
if (shouldImport(Identifier.HEALTH)) {
|
||||
if (Identifier.HEALTH.isEnabled()) {
|
||||
containers.put(Identifier.HEALTH, BukkitData.Health.from(
|
||||
status.getDouble("health"),
|
||||
status.getDouble("health_scale")
|
||||
status.getDouble("health_scale"),
|
||||
false
|
||||
));
|
||||
}
|
||||
if (shouldImport(Identifier.HUNGER)) {
|
||||
if (Identifier.HUNGER.isEnabled()) {
|
||||
containers.put(Identifier.HUNGER, BukkitData.Hunger.from(
|
||||
status.getInt("hunger"),
|
||||
status.getFloat("saturation"),
|
||||
status.getFloat("saturation_exhaustion")
|
||||
));
|
||||
}
|
||||
if (shouldImport(Identifier.EXPERIENCE)) {
|
||||
if (Identifier.EXPERIENCE.isEnabled()) {
|
||||
containers.put(Identifier.EXPERIENCE, BukkitData.Experience.from(
|
||||
status.getInt("total_experience"),
|
||||
status.getInt("experience_level"),
|
||||
status.getFloat("experience_progress")
|
||||
));
|
||||
}
|
||||
if (shouldImport(Identifier.GAME_MODE)) {
|
||||
if (Identifier.GAME_MODE.isEnabled()) {
|
||||
containers.put(Identifier.GAME_MODE, BukkitData.GameMode.from(
|
||||
status.getString("game_mode")
|
||||
));
|
||||
}
|
||||
if (shouldImport(Identifier.FLIGHT_STATUS)) {
|
||||
if (Identifier.FLIGHT_STATUS.isEnabled()) {
|
||||
containers.put(Identifier.FLIGHT_STATUS, BukkitData.FlightStatus.from(
|
||||
status.getBoolean("is_flying"),
|
||||
status.getBoolean("is_flying")
|
||||
@@ -118,7 +119,7 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
||||
|
||||
@NotNull
|
||||
private Optional<Data.Items.Inventory> readInventory(@NotNull JSONObject object) {
|
||||
if (!object.has("inventory") || !shouldImport(Identifier.INVENTORY)) {
|
||||
if (!object.has("inventory") || !Identifier.INVENTORY.isEnabled()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@@ -130,7 +131,7 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
||||
|
||||
@NotNull
|
||||
private Optional<Data.Items.EnderChest> readEnderChest(@NotNull JSONObject object) {
|
||||
if (!object.has("ender_chest") || !shouldImport(Identifier.ENDER_CHEST)) {
|
||||
if (!object.has("ender_chest") || !Identifier.ENDER_CHEST.isEnabled()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@@ -142,7 +143,7 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
||||
|
||||
@NotNull
|
||||
private Optional<Data.Location> readLocation(@NotNull JSONObject object) {
|
||||
if (!object.has("location") || !shouldImport(Identifier.LOCATION)) {
|
||||
if (!object.has("location") || !Identifier.LOCATION.isEnabled()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@@ -163,7 +164,7 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
||||
|
||||
@NotNull
|
||||
private Optional<Data.Advancements> readAdvancements(@NotNull JSONObject object) {
|
||||
if (!object.has("advancements") || !shouldImport(Identifier.ADVANCEMENTS)) {
|
||||
if (!object.has("advancements") || !Identifier.ADVANCEMENTS.isEnabled()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@@ -186,7 +187,7 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
||||
|
||||
@NotNull
|
||||
private Optional<Data.Statistics> readStatistics(@NotNull JSONObject object) {
|
||||
if (!object.has("statistics") || !shouldImport(Identifier.STATISTICS)) {
|
||||
if (!object.has("statistics") || !Identifier.STATISTICS.isEnabled()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@@ -280,11 +281,6 @@ public class BukkitLegacyConverter extends LegacyConverter {
|
||||
return serializedItemStack != null ? ItemStack.deserialize((Map<String, Object>) serializedItemStack) : null;
|
||||
}
|
||||
|
||||
|
||||
private boolean shouldImport(@NotNull Identifier type) {
|
||||
return plugin.getSettings().getSynchronization().isFeatureEnabled(type);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Date parseDate(@NotNull String dateString) {
|
||||
try {
|
||||
|
||||
@@ -7,6 +7,7 @@ description: '${description}'
|
||||
website: 'https://william278.net'
|
||||
folia-supported: true
|
||||
softdepend:
|
||||
- 'packetevents'
|
||||
- 'ProtocolLib'
|
||||
- 'MysqlPlayerDataBridge'
|
||||
- 'Plan'
|
||||
|
||||
@@ -7,7 +7,7 @@ dependencies {
|
||||
api 'org.apache.commons:commons-text:1.12.0'
|
||||
api 'net.william278:minedown:1.8.2'
|
||||
api 'org.json:json:20240303'
|
||||
api 'com.google.code.gson:gson:2.10.1'
|
||||
api 'com.google.code.gson:gson:2.11.0'
|
||||
api 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2'
|
||||
api 'de.exlll:configlib-yaml:4.5.0'
|
||||
api 'net.william278:paginedown:1.1.2'
|
||||
@@ -18,9 +18,9 @@ dependencies {
|
||||
|
||||
compileOnly 'org.projectlombok:lombok:1.18.32'
|
||||
compileOnly 'org.jetbrains:annotations:24.1.0'
|
||||
compileOnly 'net.kyori:adventure-api:4.16.0'
|
||||
compileOnly 'net.kyori:adventure-platform-api:4.3.2'
|
||||
compileOnly 'com.google.guava:guava:33.2.0-jre'
|
||||
compileOnly 'net.kyori:adventure-api:4.17.0'
|
||||
compileOnly 'net.kyori:adventure-platform-api:4.3.3'
|
||||
compileOnly 'com.google.guava:guava:33.2.1-jre'
|
||||
compileOnly 'com.github.plan-player-analytics:Plan:5.5.2272'
|
||||
compileOnly "redis.clients:jedis:$jedis_version"
|
||||
compileOnly "com.mysql:mysql-connector-j:$mysql_driver_version"
|
||||
@@ -31,7 +31,7 @@ dependencies {
|
||||
|
||||
testImplementation "redis.clients:jedis:$jedis_version"
|
||||
testImplementation "org.xerial.snappy:snappy-java:$snappy_version"
|
||||
testImplementation 'com.google.guava:guava:33.2.0-jre'
|
||||
testImplementation 'com.google.guava:guava:33.2.1-jre'
|
||||
testImplementation 'com.github.plan-player-analytics:Plan:5.5.2272'
|
||||
testCompileOnly 'de.exlll:configlib-yaml:4.5.0'
|
||||
testCompileOnly 'org.jetbrains:annotations:24.1.0'
|
||||
|
||||
@@ -31,7 +31,7 @@ import net.william278.husksync.adapter.DataAdapter;
|
||||
import net.william278.husksync.config.ConfigProvider;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.Identifier;
|
||||
import net.william278.husksync.data.Serializer;
|
||||
import net.william278.husksync.data.SerializerRegistry;
|
||||
import net.william278.husksync.database.Database;
|
||||
import net.william278.husksync.event.EventDispatcher;
|
||||
import net.william278.husksync.migrator.Migrator;
|
||||
@@ -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.*;
|
||||
@@ -52,7 +51,7 @@ import java.util.logging.Level;
|
||||
/**
|
||||
* Abstract implementation of the HuskSync plugin.
|
||||
*/
|
||||
public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider {
|
||||
public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider, SerializerRegistry {
|
||||
|
||||
int SPIGOT_RESOURCE_ID = 97144;
|
||||
|
||||
@@ -86,7 +85,6 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
||||
*
|
||||
* @return the {@link RedisManager} implementation
|
||||
*/
|
||||
|
||||
@NotNull
|
||||
RedisManager getRedisManager();
|
||||
|
||||
@@ -98,43 +96,6 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
||||
@NotNull
|
||||
DataAdapter getDataAdapter();
|
||||
|
||||
/**
|
||||
* Returns the data serializer for the given {@link Identifier}
|
||||
*/
|
||||
@NotNull
|
||||
<T extends Data> Map<Identifier, Serializer<T>> getSerializers();
|
||||
|
||||
/**
|
||||
* Register a data serializer for the given {@link Identifier}
|
||||
*
|
||||
* @param identifier the {@link Identifier}
|
||||
* @param serializer the {@link Serializer}
|
||||
*/
|
||||
default void registerSerializer(@NotNull Identifier identifier,
|
||||
@NotNull Serializer<? extends Data> serializer) {
|
||||
if (identifier.isCustom()) {
|
||||
log(Level.INFO, String.format("Registered custom data type: %s", identifier));
|
||||
}
|
||||
getSerializers().put(identifier, (Serializer<Data>) serializer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Identifier} for the given key
|
||||
*/
|
||||
default Optional<Identifier> getIdentifier(@NotNull String key) {
|
||||
return getSerializers().keySet().stream().filter(identifier -> identifier.toString().equals(key)).findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the set of registered data types
|
||||
*
|
||||
* @return the set of registered data types
|
||||
*/
|
||||
@NotNull
|
||||
default Set<Identifier> getRegisteredDataTypes() {
|
||||
return getSerializers().keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data syncer implementation
|
||||
*
|
||||
@@ -159,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.
|
||||
@@ -193,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
|
||||
*
|
||||
|
||||
@@ -49,7 +49,7 @@ public class GsonAdapter implements DataAdapter {
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public <A extends Adaptable> A fromBytes(@NotNull byte[] data, @NotNull Class<A> type) throws AdaptionException {
|
||||
public <A extends Adaptable> A fromBytes(byte[] data, @NotNull Class<A> type) throws AdaptionException {
|
||||
return this.fromJson(new String(data, StandardCharsets.UTF_8), type);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ public class SnappyGsonAdapter extends GsonAdapter {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public <A extends Adaptable> byte[] toBytes(@NotNull A data) throws AdaptionException {
|
||||
try {
|
||||
@@ -43,7 +42,7 @@ public class SnappyGsonAdapter extends GsonAdapter {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public <A extends Adaptable> A fromBytes(@NotNull byte[] data, @NotNull Class<A> type) throws AdaptionException {
|
||||
public <A extends Adaptable> A fromBytes(byte[] data, @NotNull Class<A> type) throws AdaptionException {
|
||||
try {
|
||||
return super.fromBytes(decompressBytes(data), type);
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -378,6 +378,17 @@ public class HuskSyncAPI {
|
||||
plugin.registerSerializer(identifier, serializer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a registered data serializer by its identifier
|
||||
*
|
||||
* @param identifier The identifier of the data type to get the serializer for
|
||||
* @return The serializer for the given identifier, or an empty optional if the serializer isn't registered
|
||||
* @since 3.5.4
|
||||
*/
|
||||
public Optional<Serializer<Data>> getDataSerializer(@NotNull Identifier identifier) {
|
||||
return plugin.getSerializer(identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link DataSnapshot.Unpacked} from a {@link DataSnapshot.Packed}
|
||||
*
|
||||
@@ -500,17 +511,19 @@ public class HuskSyncAPI {
|
||||
*/
|
||||
static final class NotRegisteredException extends IllegalStateException {
|
||||
|
||||
private static final String MESSAGE = """
|
||||
Could not access the HuskSync API as it has not yet been registered. This could be because:
|
||||
private static final String REASONS = """
|
||||
This may be because:
|
||||
1) HuskSync has failed to enable successfully
|
||||
2) Your plugin isn't set to load after HuskSync has
|
||||
(Check if it set as a (soft)depend in plugin.yml or to load: BEFORE in paper-plugin.yml?)
|
||||
3) You are attempting to access HuskSync on plugin construction/before your plugin has enabled.
|
||||
4) You have shaded HuskSync into your plugin jar and need to fix your maven/gradle/build script
|
||||
to only include HuskSync as a dependency and not as a shaded dependency.""";
|
||||
3) You are attempting to access HuskSync on plugin construction/before your plugin has enabled.""";
|
||||
|
||||
NotRegisteredException(@NotNull String reasons) {
|
||||
super("Could not access the HuskSync API as it has not yet been registered. %s".formatted(reasons));
|
||||
}
|
||||
|
||||
NotRegisteredException() {
|
||||
super(MESSAGE);
|
||||
this(REASONS);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)"),
|
||||
@@ -239,13 +241,19 @@ public class HuskSyncCommand extends Command implements TabProvider {
|
||||
)),
|
||||
DATA_TYPES(plugin -> Component.join(
|
||||
JoinConfiguration.commas(true),
|
||||
plugin.getRegisteredDataTypes().stream().map(i -> {
|
||||
boolean enabled = plugin.getSettings().getSynchronization().isFeatureEnabled(i);
|
||||
return Component.textOfChildren(Component
|
||||
.text(i.toString()).appendSpace().append(Component.text(enabled ? '✔' : '❌')))
|
||||
.color(enabled ? NamedTextColor.GREEN : NamedTextColor.RED)
|
||||
.hoverEvent(HoverEvent.showText(Component.text(enabled ? "Enabled" : "Disabled")));
|
||||
}).toList()
|
||||
plugin.getRegisteredDataTypes().stream().map(i -> Component.textOfChildren(Component.text(i.toString())
|
||||
.appendSpace().append(Component.text(i.isEnabled() ? '✔' : '❌')))
|
||||
.color(i.isEnabled() ? NamedTextColor.GREEN : NamedTextColor.RED)
|
||||
.hoverEvent(HoverEvent.showText(
|
||||
Component.text(i.isEnabled() ? "Enabled" : "Disabled")
|
||||
.append(Component.newline())
|
||||
.append(Component.text("Dependencies: %s".formatted(i.getDependencies()
|
||||
.isEmpty() ? "(None)" : i.getDependencies().stream()
|
||||
.map(d -> "%s (%s)".formatted(
|
||||
d.getKey().value(), d.isRequired() ? "Required" : "Optional"
|
||||
)).collect(Collectors.joining(", ")))
|
||||
).color(NamedTextColor.GRAY))
|
||||
))).toList()
|
||||
));
|
||||
|
||||
private final Function<HuskSync, Component> supplier;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -99,7 +99,7 @@ public class UserDataCommand extends Command implements TabProvider {
|
||||
version -> dumpSnapshot(executor, user, version, parseStringArg(args, 3)
|
||||
.map(arg -> arg.equalsIgnoreCase("web")).orElse(false)),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata dump <web/file> <username> <version_uuid>")
|
||||
"/userdata dump <username> <version_uuid> <web/file>")
|
||||
.ifPresent(executor::sendMessage)
|
||||
);
|
||||
default -> plugin.getLocales().getLocale("error_invalid_syntax", getUsage())
|
||||
|
||||
@@ -75,7 +75,7 @@ public class Settings {
|
||||
@Comment({"Whether to enable the Player Analytics hook.", "Docs: https://william278.net/docs/husksync/plan-hook"})
|
||||
private boolean enablePlanHook = true;
|
||||
|
||||
@Comment("Whether to cancel game event packets directly when handling locked players if ProtocolLib is installed")
|
||||
@Comment("Whether to cancel game event packets directly when handling locked players if ProtocolLib or PacketEvents is installed")
|
||||
private boolean cancelPackets = true;
|
||||
|
||||
|
||||
|
||||
@@ -21,6 +21,11 @@ package net.william278.husksync.data;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
@@ -78,6 +83,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 +116,7 @@ public interface Data {
|
||||
* Data container holding data for ender chests
|
||||
*/
|
||||
interface EnderChest extends Items {
|
||||
|
||||
int ENDER_CHEST_SLOT_COUNT = 27;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -155,8 +161,8 @@ public interface Data {
|
||||
@NotNull
|
||||
default List<Advancement> getCompletedExcludingRecipes() {
|
||||
return getCompleted().stream()
|
||||
.filter(advancement -> !advancement.getKey().startsWith("minecraft:recipe"))
|
||||
.collect(Collectors.toList());
|
||||
.filter(advancement -> !advancement.getKey().startsWith("minecraft:recipe"))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
void setCompleted(@NotNull List<Advancement> completed);
|
||||
@@ -185,13 +191,13 @@ public interface Data {
|
||||
@NotNull
|
||||
private static Map<String, Long> adaptDateMap(@NotNull Map<String, Date> dateMap) {
|
||||
return dateMap.entrySet().stream()
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getTime()));
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getTime()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Map<String, Date> adaptLongMap(@NotNull Map<String, Long> dateMap) {
|
||||
return dateMap.entrySet().stream()
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> new Date(e.getValue())));
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> new Date(e.getValue())));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -244,9 +250,9 @@ public interface Data {
|
||||
void setWorld(@NotNull World world);
|
||||
|
||||
record World(
|
||||
@SerializedName("name") @NotNull String name,
|
||||
@SerializedName("uuid") @NotNull UUID uuid,
|
||||
@SerializedName("environment") @NotNull String environment
|
||||
@SerializedName("name") @NotNull String name,
|
||||
@SerializedName("uuid") @NotNull UUID uuid,
|
||||
@SerializedName("environment") @NotNull String environment
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -318,9 +324,9 @@ public interface Data {
|
||||
List<Attribute> getAttributes();
|
||||
|
||||
record Attribute(
|
||||
@NotNull String name,
|
||||
double baseValue,
|
||||
@NotNull Set<Modifier> modifiers
|
||||
@NotNull String name,
|
||||
double baseValue,
|
||||
@NotNull Set<Modifier> modifiers
|
||||
) {
|
||||
|
||||
public double getValue() {
|
||||
@@ -333,17 +339,34 @@ public interface Data {
|
||||
|
||||
}
|
||||
|
||||
record Modifier(
|
||||
@NotNull UUID uuid,
|
||||
@NotNull String name,
|
||||
double amount,
|
||||
@SerializedName("operation") int operationType,
|
||||
@SerializedName("equipment_slot") int equipmentSlot
|
||||
) {
|
||||
@Getter
|
||||
@Accessors(fluent = true)
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
final class Modifier {
|
||||
@Getter(AccessLevel.NONE)
|
||||
@Nullable
|
||||
@SerializedName("uuid")
|
||||
private UUID uuid;
|
||||
@SerializedName("name")
|
||||
private String name;
|
||||
@SerializedName("amount")
|
||||
private double amount;
|
||||
@SerializedName("operation")
|
||||
private int operationType;
|
||||
@SerializedName("equipment_slot")
|
||||
private int equipmentSlot;
|
||||
|
||||
public Modifier(@NotNull String name, double amount, int operationType, int equipmentSlot) {
|
||||
this.name = name;
|
||||
this.amount = amount;
|
||||
this.operationType = operationType;
|
||||
this.equipmentSlot = equipmentSlot;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return obj instanceof Modifier modifier && modifier.uuid.equals(uuid);
|
||||
return obj instanceof Modifier modifier && modifier.uuid().equals(uuid());
|
||||
}
|
||||
|
||||
public double modify(double value) {
|
||||
@@ -354,12 +377,18 @@ public interface Data {
|
||||
default -> value;
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public UUID uuid() {
|
||||
return uuid != null ? uuid : UUID.nameUUIDFromBytes(name.getBytes());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
default Optional<Attribute> getAttribute(@NotNull Key key) {
|
||||
return getAttributes().stream()
|
||||
.filter(attribute -> attribute.name().equals(key.asString()))
|
||||
.findFirst();
|
||||
.filter(attribute -> attribute.name().equals(key.asString()))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
default void removeAttribute(@NotNull Key key) {
|
||||
@@ -368,8 +397,8 @@ public interface Data {
|
||||
|
||||
default double getMaxHealth() {
|
||||
return getAttribute(MAX_HEALTH_KEY)
|
||||
.map(Attribute::getValue)
|
||||
.orElse(20.0);
|
||||
.map(Attribute::getValue)
|
||||
.orElse(20.0);
|
||||
}
|
||||
|
||||
default void setMaxHealth(double maxHealth) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -370,7 +370,7 @@ public class DataSnapshot {
|
||||
public static class Unpacked extends DataSnapshot implements DataHolder {
|
||||
|
||||
@Expose(serialize = false, deserialize = false)
|
||||
private final Map<Identifier, Data> deserialized;
|
||||
private final TreeMap<Identifier, Data> deserialized;
|
||||
|
||||
private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull String saveCause, @NotNull String serverName, @NotNull Map<String, String> data,
|
||||
@@ -381,7 +381,7 @@ public class DataSnapshot {
|
||||
}
|
||||
|
||||
private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull String saveCause, @NotNull String serverName, @NotNull Map<Identifier, Data> data,
|
||||
@NotNull String saveCause, @NotNull String serverName, @NotNull TreeMap<Identifier, Data> data,
|
||||
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion) {
|
||||
super(id, pinned, timestamp, saveCause, serverName, Map.of(), minecraftVersion, platformType, formatVersion);
|
||||
this.deserialized = data;
|
||||
@@ -389,25 +389,25 @@ public class DataSnapshot {
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
private Map<Identifier, Data> deserializeData(@NotNull HuskSync plugin) {
|
||||
private TreeMap<Identifier, Data> deserializeData(@NotNull HuskSync plugin) {
|
||||
return data.entrySet().stream()
|
||||
.map((entry) -> plugin.getIdentifier(entry.getKey()).map(id -> Map.entry(
|
||||
id, plugin.getSerializers().get(id).deserialize(entry.getValue(), getMinecraftVersion())
|
||||
)).orElse(null))
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
.filter(e -> plugin.getIdentifier(e.getKey()).isPresent())
|
||||
.map(entry -> Map.entry(plugin.getIdentifier(entry.getKey()).orElseThrow(), entry.getValue()))
|
||||
.collect(Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
entry -> plugin.deserializeData(entry.getKey(), entry.getValue()),
|
||||
(a, b) -> b, () -> Maps.newTreeMap(SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR)
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
private Map<String, String> serializeData(@NotNull HuskSync plugin) {
|
||||
return deserialized.entrySet().stream()
|
||||
.map((entry) -> Map.entry(entry.getKey().toString(),
|
||||
Objects.requireNonNull(
|
||||
plugin.getSerializers().get(entry.getKey()),
|
||||
String.format("No serializer found for %s", entry.getKey())
|
||||
).serialize(entry.getValue())))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
.collect(Collectors.toMap(
|
||||
entry -> entry.getKey().toString(),
|
||||
entry -> plugin.serializeData(entry.getKey(), entry.getValue())
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -453,12 +453,12 @@ public class DataSnapshot {
|
||||
private String serverName;
|
||||
private boolean pinned;
|
||||
private OffsetDateTime timestamp;
|
||||
private final Map<Identifier, Data> data;
|
||||
private final TreeMap<Identifier, Data> data;
|
||||
|
||||
private Builder(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
this.pinned = false;
|
||||
this.data = Maps.newHashMap();
|
||||
this.data = Maps.newTreeMap(SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR);
|
||||
this.timestamp = OffsetDateTime.now();
|
||||
this.id = UUID.randomUUID();
|
||||
this.serverName = plugin.getServerName();
|
||||
|
||||
@@ -19,40 +19,82 @@
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import lombok.*;
|
||||
import net.kyori.adventure.key.InvalidKeyException;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import org.intellij.lang.annotations.Subst;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Identifiers of different types of {@link Data}s
|
||||
*/
|
||||
@Getter
|
||||
public class Identifier {
|
||||
|
||||
public static Identifier INVENTORY = huskSync("inventory", true);
|
||||
public static Identifier ENDER_CHEST = huskSync("ender_chest", true);
|
||||
public static Identifier POTION_EFFECTS = huskSync("potion_effects", true);
|
||||
public static Identifier ADVANCEMENTS = huskSync("advancements", true);
|
||||
public static Identifier LOCATION = huskSync("location", false);
|
||||
public static Identifier STATISTICS = huskSync("statistics", true);
|
||||
public static Identifier HEALTH = huskSync("health", true);
|
||||
public static Identifier HUNGER = huskSync("hunger", true);
|
||||
public static Identifier ATTRIBUTES = huskSync("attributes", true);
|
||||
public static Identifier EXPERIENCE = huskSync("experience", true);
|
||||
public static Identifier GAME_MODE = huskSync("game_mode", true);
|
||||
public static Identifier FLIGHT_STATUS = huskSync("flight_status", true);
|
||||
public static Identifier PERSISTENT_DATA = huskSync("persistent_data", true);
|
||||
// Built-in identifiers
|
||||
public static final Identifier PERSISTENT_DATA = huskSync("persistent_data", true);
|
||||
public static final Identifier INVENTORY = huskSync("inventory", true);
|
||||
public static final Identifier ENDER_CHEST = huskSync("ender_chest", true);
|
||||
public static final Identifier ADVANCEMENTS = huskSync("advancements", true);
|
||||
public static final Identifier STATISTICS = huskSync("statistics", true);
|
||||
public static final Identifier POTION_EFFECTS = huskSync("potion_effects", true);
|
||||
public static final Identifier GAME_MODE = huskSync("game_mode", false);
|
||||
public static final Identifier FLIGHT_STATUS = huskSync("flight_status", true,
|
||||
Dependency.optional("game_mode")
|
||||
);
|
||||
public static final Identifier ATTRIBUTES = huskSync("attributes", true,
|
||||
Dependency.required("potion_effects")
|
||||
);
|
||||
public static final Identifier HEALTH = huskSync("health", true,
|
||||
Dependency.optional("attributes")
|
||||
);
|
||||
public static final Identifier HUNGER = huskSync("hunger", true,
|
||||
Dependency.optional("attributes")
|
||||
);
|
||||
public static final Identifier EXPERIENCE = huskSync("experience", true,
|
||||
Dependency.optional("advancements")
|
||||
);
|
||||
public static final Identifier LOCATION = huskSync("location", false,
|
||||
Dependency.optional("flight_status"),
|
||||
Dependency.optional("potion_effects")
|
||||
);
|
||||
|
||||
private final Key key;
|
||||
private final boolean configDefault;
|
||||
private final boolean enabledByDefault;
|
||||
@Getter
|
||||
private final Set<Dependency> dependencies;
|
||||
@Setter
|
||||
@Getter
|
||||
public boolean enabled;
|
||||
|
||||
private Identifier(@NotNull Key key, boolean configDefault) {
|
||||
private Identifier(@NotNull Key key, boolean enabledByDefault, @NotNull Set<Dependency> dependencies) {
|
||||
this.key = key;
|
||||
this.configDefault = configDefault;
|
||||
this.enabledByDefault = enabledByDefault;
|
||||
this.enabled = enabledByDefault;
|
||||
this.dependencies = dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an identifier from a {@link Key}
|
||||
*
|
||||
* @param key the key
|
||||
* @param dependencies the dependencies
|
||||
* @return the identifier
|
||||
* @since 3.5.4
|
||||
*/
|
||||
@NotNull
|
||||
public static Identifier from(@NotNull Key key, @NotNull Set<Dependency> dependencies) {
|
||||
if (key.namespace().equals("husksync")) {
|
||||
throw new IllegalArgumentException("You cannot register a key with \"husksync\" as the namespace!");
|
||||
}
|
||||
return new Identifier(key, true, dependencies);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,10 +106,7 @@ public class Identifier {
|
||||
*/
|
||||
@NotNull
|
||||
public static Identifier from(@NotNull Key key) {
|
||||
if (key.namespace().equals("husksync")) {
|
||||
throw new IllegalArgumentException("You cannot register a key with \"husksync\" as the namespace!");
|
||||
}
|
||||
return new Identifier(key, true);
|
||||
return from(key, Collections.emptySet());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,25 +122,34 @@ public class Identifier {
|
||||
return from(Key.key(plugin, name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an identifier from a namespace, value, and dependencies
|
||||
*
|
||||
* @param plugin the namespace
|
||||
* @param name the value
|
||||
* @param dependencies the dependencies
|
||||
* @return the identifier
|
||||
* @since 3.5.4
|
||||
*/
|
||||
@NotNull
|
||||
public static Identifier from(@Subst("plugin") @NotNull String plugin, @Subst("null") @NotNull String name,
|
||||
@NotNull Set<Dependency> dependencies) {
|
||||
return from(Key.key(plugin, name), dependencies);
|
||||
}
|
||||
|
||||
// Return an identifier with a HuskSync namespace
|
||||
@NotNull
|
||||
private static Identifier huskSync(@Subst("null") @NotNull String name,
|
||||
boolean configDefault) throws InvalidKeyException {
|
||||
return new Identifier(Key.key("husksync", name), configDefault);
|
||||
return new Identifier(Key.key("husksync", name), configDefault, Collections.emptySet());
|
||||
}
|
||||
|
||||
// Return an identifier with a HuskSync namespace
|
||||
@NotNull
|
||||
@SuppressWarnings("unused")
|
||||
private static Identifier parse(@NotNull String key) throws InvalidKeyException {
|
||||
return huskSync(key, true);
|
||||
}
|
||||
|
||||
public boolean isEnabledByDefault() {
|
||||
return configDefault;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Map.Entry<String, Boolean> getConfigEntry() {
|
||||
return Map.entry(getKeyValue(), configDefault);
|
||||
private static Identifier huskSync(@Subst("null") @NotNull String name,
|
||||
@SuppressWarnings("SameParameterValue") boolean configDefault,
|
||||
@NotNull Dependency... dependents) throws InvalidKeyException {
|
||||
return new Identifier(Key.key("husksync", name), configDefault, Set.of(dependents));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,6 +170,17 @@ public class Identifier {
|
||||
.toArray(Map.Entry[]::new));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the identifier depends on the given identifier
|
||||
*
|
||||
* @param identifier the identifier to check
|
||||
* @return {@code true} if the identifier depends on the given identifier
|
||||
* @since 3.5.4
|
||||
*/
|
||||
public boolean dependsOn(@NotNull Identifier identifier) {
|
||||
return dependencies.contains(Dependency.required(identifier.key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the namespace of the identifier
|
||||
*
|
||||
@@ -176,4 +235,85 @@ public class Identifier {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the config entry for the identifier
|
||||
@NotNull
|
||||
private Map.Entry<String, Boolean> getConfigEntry() {
|
||||
return Map.entry(getKeyValue(), enabledByDefault);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two identifiers based on their dependencies.
|
||||
* <p>
|
||||
* If this identifier contains a dependency on the other, it should come after & vice versa
|
||||
*
|
||||
* @since 3.5.4
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PACKAGE)
|
||||
static class DependencyOrderComparator implements Comparator<Identifier> {
|
||||
|
||||
@Override
|
||||
public int compare(@NotNull Identifier i1, @NotNull Identifier i2) {
|
||||
if (i1.equals(i2)) {
|
||||
return 0;
|
||||
}
|
||||
if (i1.dependsOn(i2)) {
|
||||
if (i2.dependsOn(i1)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Found circular dependency between %s and %s".formatted(i1.getKey(), i2.getKey())
|
||||
);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a data dependency of an identifier, used to determine the order in which data is applied to users
|
||||
*
|
||||
* @since 3.5.4
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public static class Dependency {
|
||||
/**
|
||||
* Key of the data dependency see {@code Identifier#key()}
|
||||
*/
|
||||
private Key key;
|
||||
/**
|
||||
* Whether the data dependency is required to be present & enabled for the dependant data to enabled
|
||||
*/
|
||||
private boolean required;
|
||||
|
||||
@NotNull
|
||||
protected static Dependency required(@NotNull Key identifier) {
|
||||
return new Dependency(identifier, true);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static Dependency optional(@NotNull Key identifier) {
|
||||
return new Dependency(identifier, false);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private static Dependency required(@Subst("null") @NotNull String identifier) {
|
||||
return required(Key.key("husksync", identifier));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Dependency optional(@Subst("null") @NotNull String identifier) {
|
||||
return optional(Key.key("husksync", identifier));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Dependency other) {
|
||||
return key.equals(other.key);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* 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.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public interface SerializerRegistry {
|
||||
|
||||
// Comparator for ordering identifiers based on dependency
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
Comparator<Identifier> DEPENDENCY_ORDER_COMPARATOR = new Identifier.DependencyOrderComparator();
|
||||
|
||||
/**
|
||||
* Returns the data serializer for the given {@link Identifier}
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
<T extends Data> TreeMap<Identifier, Serializer<T>> getSerializers();
|
||||
|
||||
/**
|
||||
* Register a data serializer for the given {@link Identifier}
|
||||
*
|
||||
* @param id the {@link Identifier}
|
||||
* @param serializer the {@link Serializer}
|
||||
* @since 3.0
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
default void registerSerializer(@NotNull Identifier id, @NotNull Serializer<? extends Data> serializer) {
|
||||
if (id.isCustom()) {
|
||||
getPlugin().log(Level.INFO, "Registered custom data type: %s".formatted(id));
|
||||
}
|
||||
id.setEnabled(id.isCustom() || getPlugin().getSettings().getSynchronization().isFeatureEnabled(id));
|
||||
getSerializers().put(id, (Serializer<Data>) serializer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure dependencies for identifiers that have required dependencies are met
|
||||
* <p>
|
||||
* This checks the dependencies of all registered identifiers and throws an {@link IllegalStateException}
|
||||
* if a dependency has not been registered or enabled via the config
|
||||
*
|
||||
* @since 3.5.4
|
||||
*/
|
||||
default void validateDependencies() throws IllegalStateException {
|
||||
getSerializers().keySet().stream().filter(Identifier::isEnabled)
|
||||
.forEach(identifier -> {
|
||||
final List<String> unmet = identifier.getDependencies().stream()
|
||||
.filter(Identifier.Dependency::isRequired)
|
||||
.filter(dep -> !isDataTypeAvailable(dep.getKey().asString()))
|
||||
.map(dep -> dep.getKey().asString()).toList();
|
||||
if (!unmet.isEmpty()) {
|
||||
identifier.setEnabled(false);
|
||||
getPlugin().log(Level.WARNING, "Disabled %s syncing as the following types need to be on: %s"
|
||||
.formatted(identifier, String.join(", ", unmet)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Identifier} for the given key
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
default Optional<Identifier> getIdentifier(@NotNull String key) {
|
||||
return getSerializers().keySet().stream()
|
||||
.filter(id -> id.getKey().asString().equals(key)).findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a data serializer for the given {@link Identifier}
|
||||
*
|
||||
* @param identifier the {@link Identifier} to get the serializer for
|
||||
* @return the {@link Serializer} for the given {@link Identifier}
|
||||
* @since 3.5.4
|
||||
*/
|
||||
default Optional<Serializer<Data>> getSerializer(@NotNull Identifier identifier) {
|
||||
return getSerializers().entrySet().stream()
|
||||
.filter(entry -> entry.getKey().getKey().equals(identifier.getKey()))
|
||||
.map(Map.Entry::getValue).findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize data for the given {@link Identifier}
|
||||
*
|
||||
* @param identifier the {@link Identifier} to serialize data for
|
||||
* @param data the data to serialize
|
||||
* @return the serialized data
|
||||
* @throws IllegalArgumentException if no serializer is found for the given {@link Identifier}
|
||||
* @since 3.5.4
|
||||
*/
|
||||
@NotNull
|
||||
default String serializeData(@NotNull Identifier identifier, @NotNull Data data) throws IllegalStateException {
|
||||
return getSerializer(identifier).map(serializer -> serializer.serialize(data))
|
||||
.orElseThrow(() -> new IllegalStateException("No serializer found for %s".formatted(identifier)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize data for the given {@link Identifier}
|
||||
*
|
||||
* @param identifier the {@link Identifier} to deserialize data for
|
||||
* @param data the data to deserialize
|
||||
* @return the deserialized data
|
||||
* @throws IllegalStateException if no serializer is found for the given {@link Identifier}
|
||||
* @since 3.5.4
|
||||
*/
|
||||
@NotNull
|
||||
default Data deserializeData(@NotNull Identifier identifier, @NotNull String data) throws IllegalStateException {
|
||||
return getSerializer(identifier).map(serializer -> serializer.deserialize(data)).orElseThrow(
|
||||
() -> new IllegalStateException("No serializer found for %s".formatted(identifier))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the set of registered data types
|
||||
*
|
||||
* @return the set of registered data types
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
default Set<Identifier> getRegisteredDataTypes() {
|
||||
return getSerializers().keySet();
|
||||
}
|
||||
|
||||
// Returns if a data type is available and enabled in the config
|
||||
private boolean isDataTypeAvailable(@NotNull String key) {
|
||||
return getIdentifier(key).map(Identifier::isEnabled).orElse(false);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
HuskSync getPlugin();
|
||||
|
||||
}
|
||||
@@ -34,7 +34,7 @@ import java.util.logging.Level;
|
||||
public interface UserDataHolder extends DataHolder {
|
||||
|
||||
/**
|
||||
* Get the data that is enabled for syncing in the config
|
||||
* Get the data enabled for syncing in the config
|
||||
*
|
||||
* @return the data that is enabled for syncing
|
||||
* @since 3.0
|
||||
@@ -43,7 +43,7 @@ public interface UserDataHolder extends DataHolder {
|
||||
@NotNull
|
||||
default Map<Identifier, Data> getData() {
|
||||
return getPlugin().getRegisteredDataTypes().stream()
|
||||
.filter(type -> type.isCustom() || getPlugin().getSettings().getSynchronization().isFeatureEnabled(type))
|
||||
.filter(Identifier::isEnabled)
|
||||
.map(id -> Map.entry(id, getData(id)))
|
||||
.filter(data -> data.getValue().isPresent())
|
||||
.collect(HashMap::new, (map, data) -> map.put(data.getKey(), data.getValue().get()), HashMap::putAll);
|
||||
@@ -79,7 +79,8 @@ public interface UserDataHolder extends DataHolder {
|
||||
* Deserialize and apply a data snapshot to this data owner
|
||||
* <p>
|
||||
* This method will deserialize the data on the current thread, then synchronously apply it on
|
||||
* the main server thread.
|
||||
* the main server thread. The order data will be applied is determined based on the dependencies of
|
||||
* each data type (see {@link Identifier.Dependency}).
|
||||
* </p>
|
||||
* The {@code runAfter} callback function will be run after the snapshot has been applied.
|
||||
*
|
||||
@@ -106,12 +107,15 @@ public interface UserDataHolder extends DataHolder {
|
||||
try {
|
||||
for (Map.Entry<Identifier, Data> entry : unpacked.getData().entrySet()) {
|
||||
final Identifier identifier = entry.getKey();
|
||||
if (plugin.getSettings().getSynchronization().isFeatureEnabled(identifier)) {
|
||||
if (identifier.isCustom()) {
|
||||
getCustomDataStore().put(identifier, entry.getValue());
|
||||
}
|
||||
entry.getValue().apply(this, plugin);
|
||||
if (!identifier.isEnabled()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apply the identified data
|
||||
if (identifier.isCustom()) {
|
||||
getCustomDataStore().put(identifier, entry.getValue());
|
||||
}
|
||||
entry.getValue().apply(this, plugin);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, String.format("Failed to apply data snapshot to %s", getUsername()), e);
|
||||
|
||||
@@ -113,7 +113,7 @@ public class MongoDbDatabase extends Database {
|
||||
throw new MongoException("User document returned null!");
|
||||
}
|
||||
|
||||
Bson updates = Updates.set("uuid", user.getUuid().toString());
|
||||
Bson updates = Updates.set("username", user.getUsername());
|
||||
mongoCollectionHelper.updateDocument(usersTable, doc, updates);
|
||||
} catch (MongoException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -107,7 +107,7 @@ public abstract class EventListener {
|
||||
/**
|
||||
* Handle the plugin disabling
|
||||
*/
|
||||
public final void handlePluginDisable() {
|
||||
public void handlePluginDisable() {
|
||||
// Save for all online players
|
||||
plugin.getOnlineUsers().stream()
|
||||
.filter(user -> !plugin.isLocked(user.getUuid()) && !user.isNpc())
|
||||
|
||||
@@ -54,4 +54,16 @@ public interface LockedHandler {
|
||||
@ApiStatus.Internal
|
||||
HuskSync getPlugin();
|
||||
|
||||
default void onLoad() {
|
||||
|
||||
}
|
||||
|
||||
default void onEnable() {
|
||||
|
||||
}
|
||||
|
||||
default void onDisable() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,7 +15,7 @@ CREATE TABLE IF NOT EXISTS "%user_data_table%"
|
||||
timestamp timestamp NOT NULL,
|
||||
save_cause varchar(32) NOT NULL,
|
||||
pinned boolean NOT NULL DEFAULT FALSE,
|
||||
data longblob NOT NULL,
|
||||
data bytea NOT NULL,
|
||||
|
||||
PRIMARY KEY (version_uuid, player_uuid),
|
||||
FOREIGN KEY (player_uuid) REFERENCES "%users_table%" (uuid) ON DELETE CASCADE
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
locales:
|
||||
synchronization_complete: '[⏵ 資料已同步!](#00fb9a)'
|
||||
synchronization_complete: '[⏵資料已同步!](#00fb9a)'
|
||||
synchronization_failed: '[⏵ 無法同步您的資料! 請聯繫管理員](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0%1% 的背包'
|
||||
ender_chest_viewer_menu_title: '&0%1% 的終界箱'
|
||||
inventory_viewer_opened: '[查看備份](#00fb9a) [%1%](#00fb9a bold) [於 ⌚ %2% 的背包備份](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[查看備份](#00fb9a) [%1%](#00fb9a bold) [於 ⌚ %2% 的終界箱備份](#00fb9a)'
|
||||
data_update_complete: '[🔔 你的數據資料已更新!](#00fb9a)'
|
||||
data_update_failed: '[🔔 無法更新你的數據資料! 請聯繫管理員](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ 用戶註冊完成!](#00fb9a)'
|
||||
data_manager_title: '[正在查看玩家](#00fb9a) [%3%](#00fb9a bold show_text=&7玩家UUID:\n&8%4%) [的數據備份](#00fb9a) [%1%](#00fb9a show_text=&7備份版本UUID:\n&8%2%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7備份時間:\n&8數據保存時間)'
|
||||
data_manager_pinned: '[※ 被標記的備份](#d8ff2b show_text=&7已標記:\n&8此玩家數據備份不會按照備份時間自動排序)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7保存原因:\n&8導致數據保存的原因)'
|
||||
data_manager_server: '[☁ %1%](#ff87b3-#f5538e show_text=&7伺服器:\n&8數據保存所在伺服器的名稱)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7備份大小:\n&8備份的估計文件大小(以KiB為單位))\n'
|
||||
inventory_viewer_opened: '[查看](#00fb9a) [%1%](#00fb9a bold) [於 ⌚ %2% 的背包快照資料](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[查看](#00fb9a) [%1%](#00fb9a bold) [於 ⌚ %2% 的終界箱快照資料](#00fb9a)'
|
||||
data_update_complete: '[🔔 你的資料已更新!](#00fb9a)'
|
||||
data_update_failed: '[🔔 無法更新您的資料! 請聯繫管理員](#ff7e5e)'
|
||||
user_registration_complete: '[⭐ 使用者註冊成功!](#00fb9a)'
|
||||
data_manager_title: '[查看](#00fb9a) [%3%](#00fb9a bold show_text=&7玩家 UUID:\n&8%4%) [的快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7快照時間:\n&8資料儲存時間)'
|
||||
data_manager_pinned: '[※ 被標記的快照](#d8ff2b show_text=&7標記:\n&8此快照資料不會自動輪換)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7保存原因:\n&8觸發儲存的原因)'
|
||||
data_manager_server: '[☁ %1%](#ff87b3-#f5538e show_text=&7伺服器:\n&8儲存資料的伺服器名稱)'
|
||||
data_manager_size: '[⏏ %1%](color=#62a9f5-#7ab8fa show_text=&7快照大小:\n&8快照的預估檔案大小(KiB))\n'
|
||||
data_manger_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7血量) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7飽食度) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7經驗等級) [🏹 %5%](dark_aqua show_text=&7遊戲模式)'
|
||||
data_manager_advancements_statistics: '[⭐ 成就: %1%](color=#ffc43b-#f5c962 show_text=&7已獲得的成就:\n&8%2%) [⌛ 遊玩時間: %3%小時](color=#62a9f5-#7ab8fa show_text=&7在遊戲内遊玩的時間\n&8⚠ 基於遊戲内的統計訊息)\n'
|
||||
data_manager_advancements_statistics: '[⭐ 成就: %1%](color=#ffc43b-#f5c962 show_text=&7已獲得的成就:\n&8%2%) [⌛ 遊戲時間: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7遊戲內的遊玩時間\n&8⚠ 根據遊戲內統計)\n'
|
||||
data_manager_item_buttons: '[查看:](gray) [[🪣 背包…]](color=#a17b5f-#f5b98c show_text=&7點擊查看 run_command=/inventory %1% %2%) [[⌀ 終界箱…]](#b649c4-#d254ff show_text=&7點擊查看 run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[管理:](gray) [[❌ 刪除…]](#ff3300 show_text=&7點擊刪除此玩家數據的備份.\n&8這不會影響玩家的當前數據.\n&#ff3300&⚠ 此操作無法撤銷! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ 恢復…]](#00fb9a show_text=&7點擊還原此玩家的數據.\n&8這將會讓用户的數據恢復到此備份.\n&#ff3300&⚠ %1%當前的數據將被覆蓋! suggest_command=/husksync:userdata restore %1% %2%) [[※ 標記/取消標記…]](#d8ff2b show_text=&7點擊標記或取消標記此玩家數據備份\n&8已標記的備份不會按照備份時間自動排序 run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[系統:](gray) [[⏷ 本地轉存…]](dark_gray show_text=&7點擊將此玩家數據資料轉存到本地文件中.\n&8轉存的資料可以在以下路徑找到~/plugins/HuskSync/dumps/中找到 run_command=/husksync:userdata dump %1% %2% file) [[☂ 雲端轉存…]](dark_gray show_text=&7點擊將此玩家數據資料轉存到 mc-logs 服務中\n&8您將獲得一個包含資料的URL. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: '以及其他 %1%…'
|
||||
data_list_title: '[%1%的玩家數據備份:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7玩家數據備份 %2%&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7已標記:\n&8標記的備份不會自動加載 run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7備份時間:&7\n&8數據保存時間\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7保存原因:\n&8導致數據保存的原因 run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7備份大小:&7\n&8備份的估計文件大小(以KiB為單位) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7User Data Snapshot for %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&Invalid Data Snapshot\n&#ff7e5e&Click to delete\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_deleted: '[❌ 成功刪除玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&7%4%) [的數據備份](#00fb9a) [%1%.](#00fb9a show_text=&7備份版本UUID:\n&7%2%)'
|
||||
data_restored: '[⏪ 成功恢復玩家](#00fb9a) [%1%](#00fb9a show_text=&7玩家 UUID:\n&7%2%)[的數據備份](#00fb9a) [%3%.](#00fb9a show_text=&7備份版本UUID:\n&7%4%)'
|
||||
data_pinned: '[※ 成功標記玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的數據備份](#00fb9a) [%1%.](#00fb9a show_text=&7備份版本UUID:\n&8%2%)'
|
||||
data_unpinned: '[※ 成功取消標記玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的數據備份](#00fb9a) [%1%.](#00fb9a show_text=&7備份版本UUID:\n&8%2%)'
|
||||
data_dumped: '[☂ 已成功將 %1% 的玩家數據快照 %2% 儲存至:](#00fb9a) &7%3%'
|
||||
list_footer: '\n%1%[頁數](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
data_manager_management_buttons: '[管理:](gray) [[❌ 刪除…]](#ff3300 show_text=&7點擊刪除這個快照\n&8這不會影像目前玩家的資料\n&#ff3300&⚠ 此操作不能取消! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ 恢復…]](#00fb9a show_text=&7點擊將玩家資料覆蓋為此快照\n&8這將導致玩家的資料會被此快照覆蓋\n&#ff3300&⚠ %1% 當前的資料將被覆蓋! suggest_command=/husksync:userdata restore %1% %2%) [[※ 標記…]](#d8ff2b show_text=&7點擊切換標記狀態\n&8被標記的快照將不會自動輪換更新 run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[系統:](gray) [[⏷ 本地轉存…]](dark_gray show_text=&7點擊將此玩家資料快照轉存到本地文件中\n&8轉存的資料可以在以下路徑找到 ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ 雲端轉存…]](dark_gray show_text=&7點擊將此玩家資料快照轉存到 mc-logs 服務\n&8您將獲得一個包含資料的 URL. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: '還有 %1% …'
|
||||
data_list_title: '[%1% 的玩家資料快照:](#00fb9a) [(%2%-%3% 共](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7玩家資料快照 %2%\n&8⚡ %4% run_command=/userdata view %2% %3%) [%5%](#d8ff2b show_text=&7已標記:\n&8標記的快照將不會自動輪換。 run_command=/userdata view %2% %3%) [%6%](color=#ffc43b-#f5c962 show_text=&7版本時間戳:\n&8資料儲存時間\n&8%7% run_command=/userdata view %2% %3%) [⚑ %8%](#23a825-#36f539 show_text=&7儲存原因:\n&8觸發儲存的原因 run_command=/userdata view %2% %3%) [⏏ %9%](color=#62a9f5-#7ab8fa show_text=&7快照大小:\n&8快照的預估檔案大小(KiB) run_command=/userdata view %2% %3%)'
|
||||
data_list_item_invalid: '[%1%](dark_gray show_text=&7玩家資料快照 %2%\n&8⚡ %4% suggest_command=/userdata delete %2% %3%) [%5%](dark_gray show_text=&7已標記:\n&8標記的快照將不會自動輪換。 suggest_command=/userdata delete %2% %3%) [%6% ⚑ %8% ⏏ %9%](gray strikethrough show_text=&#ff3300&無效的資料快照\n&#ff7e5e&點擊刪除\n\n&7⚠ %10% suggest_command=/userdata delete %2% %3%)'
|
||||
data_deleted: '[❌ 成功刪除:](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%)'
|
||||
data_restored: '[⏪ 成功將玩家](#00fb9a) [%1%](#00fb9a show_text=&7玩家 UUID:\n&8%2%)[的資料恢復為 快照:](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||
data_pinned: '[※ 成功標記](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%)'
|
||||
data_unpinned: '[※ 成功解除](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [的標記](#00fb9a)'
|
||||
data_dumped: '[☂ 成功將 %2% 資料快照 %1% 儲存至:](#00fb9a) &7%3%'
|
||||
list_footer: '\n%1%[頁面](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7查看上一頁 run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7查看下一頁 run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
@@ -36,30 +36,30 @@ locales:
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
save_cause_disconnect: '斷開連接'
|
||||
save_cause_world_save: '保存世界'
|
||||
save_cause_disconnect: '離線'
|
||||
save_cause_world_save: '世界儲存'
|
||||
save_cause_death: '死亡'
|
||||
save_cause_server_shutdown: '伺服器關閉'
|
||||
save_cause_inventory_command: '背包指令'
|
||||
save_cause_enderchest_command: '終界箱指令'
|
||||
save_cause_backup_restore: '備份還原'
|
||||
save_cause_api: 'API'
|
||||
save_cause_mpdb_migration: 'MPDB遷移'
|
||||
save_cause_mpdb_migration: 'MPDB 遷移'
|
||||
save_cause_legacy_migration: '舊版遷移'
|
||||
save_cause_converted_from_v2: '從v2轉換'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| 您運行的是最新版本的HuskSync(v%1%).](#00fb9a)'
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| 發現可用的新版本:v%1%(當前版本:v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| 已重新載入配置和訊息文件.](#00fb9a)\n[⚠ 確保所有伺服器上的配置文件都是最新的!](#00fb9a)\n[需要重新啟動配置更改才能生效.](#00fb9a italic)'
|
||||
save_cause_converted_from_v2: '從 v2 轉換'
|
||||
up_to_date: '[HuskSync](#00fb9a bold) [| 您運行的是最新版本的 HuskSync (v%1%).](#00fb9a)'
|
||||
update_available: '[HuskSync](#ff7e5e bold) [| 發現可用的新版本: v%1% (running: v%2%).](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| 配置和語言文件已重新加載。](#00fb9a)\n[⚠ 確保所有伺服器上的配置文件都是最新的!](#00fb9a)\n[重啟後配置變更才會生效。](#00fb9a italic)'
|
||||
system_status_header: '[HuskSync](#00fb9a bold) [| 系統狀態報告:](#00fb9a)'
|
||||
error_invalid_syntax: '[錯誤:](#ff3300) [語法不正確,用法:](#ff7e5e) [%1%](#ff7e5e italic show_text=&#ff7e5e&點擊建議 suggest_command=%1%)'
|
||||
error_invalid_player: '[錯誤:](#ff3300) [找不到這位玩家.](#ff7e5e)'
|
||||
error_invalid_data: '[Error:](#ff3300) [Failed to unpack user data as the snapshot is invalid or corrupt.](#ff7e5e) [(Details…)](gray show_text=&7⚠ %1%)'
|
||||
error_invalid_player: '[錯誤:](#ff3300) [找不到這位玩家](#ff7e5e)'
|
||||
error_invalid_data: '[錯誤:](#ff3300) [無法解壓使用者資料,因為快照無效或已損壞。](#ff7e5e) [(詳細資訊…)](gray show_text=&7⚠ %1%)'
|
||||
error_no_permission: '[錯誤:](#ff3300) [您沒有權限執行這個指令](#ff7e5e)'
|
||||
error_console_command_only: '[錯誤:](#ff3300) [該指令只能透過 控制台 執行](#ff7e5e)'
|
||||
error_in_game_command_only: '錯誤: 該指令只能在遊戲內執行.'
|
||||
error_no_data_to_display: '[錯誤:](#ff3300) [找不到任何可顯示的玩家數據.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[錯誤:](#ff3300) [找不到該版本UUID的任何玩家數據.](#ff7e5e)'
|
||||
husksync_command_description: '管理HuskSync插件'
|
||||
userdata_command_description: '查看、管理和恢復玩家用户數據'
|
||||
inventory_command_description: '查看和編輯玩家的背包'
|
||||
error_in_game_command_only: '[錯誤:](#ff3300) [該指令只能在遊戲內執行](#ff7e5e)'
|
||||
error_no_data_to_display: '[錯誤:](#ff3300) [找不到任何可顯示的用戶資料.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[錯誤:](#ff3300) [找不到正確的 Version UUID.](#ff7e5e)'
|
||||
husksync_command_description: '管理 HuskSync 插件'
|
||||
userdata_command_description: '查看、管理和還原玩家資料'
|
||||
inventory_command_description: '查看和編輯玩家的物品欄'
|
||||
enderchest_command_description: '查看和編輯玩家的終界箱'
|
||||
|
||||
@@ -4,11 +4,20 @@ Consult the Javadocs for more information. Please note that carrying out expensi
|
||||
|
||||
## Bukkit Platform Events
|
||||
> **Tip:** Don't forget to register your listener when listening for these event calls.
|
||||
>
|
||||
|
||||
| Bukkit Event class | Cancellable | Description |
|
||||
|---------------------------|:-----------:|---------------------------------------------------------------------------------------------|
|
||||
| `BukkitDataSaveEvent` | ✅ | Called when player data snapshot is created, saved and cached due to a DataSaveCause |
|
||||
| `BukkitPreSyncEvent` | ✅ | Called before a player has their data updated from the cache or database, just after login |
|
||||
| `BukkitSyncCompleteEvent` | ❌ | Called once a player has completed their data synchronization on login successfully† |
|
||||
|
||||
## Fabric Platform Callbacks
|
||||
> Access the callback via the static EVENT field in each interface class.
|
||||
|
||||
| Fabric Callback | Cancellable | Description |
|
||||
|------------------------------|:-----------:|---------------------------------------------------------------------------------------------|
|
||||
| `FabricDataSaveCallback` | ✅ | Called when player data snapshot is created, saved and cached due to a DataSaveCause |
|
||||
| `FabricPreSyncCallback` | ✅ | Called before a player has their data updated from the cache or database, just after login |
|
||||
| `FabricSyncCompleteCallback` | ❌ | Called once a player has completed their data synchronization on login successfully† |
|
||||
|
||||
†This can also fire when a user's data is updated while the player is logged in; i.e., when an admin rolls back the user, updates their inventory or Ender Chest through the respective commands, or when an API call is made forcing the user to have their data updated.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ brigadier_tab_completion: false
|
||||
# Whether to enable the Player Analytics hook.
|
||||
# Docs: https://william278.net/docs/husksync/plan-hook
|
||||
enable_plan_hook: true
|
||||
# Whether to cancel game event packets directly when handling locked players if ProtocolLib is installed
|
||||
# Whether to cancel game event packets directly when handling locked players if ProtocolLib or PacketEvents is installed
|
||||
cancel_packets: true
|
||||
# Database settings
|
||||
database:
|
||||
|
||||
@@ -93,7 +93,7 @@ public class LoginParticleData extends BukkitData implements Adaptable {
|
||||
public class LoginParticleSerializer extends BukkitSerializer.Json<LoginParticleData> implements Serializer<LoginParticleData> {
|
||||
|
||||
// We need to create a constructor that takes our instance of the API
|
||||
public GameMode(@NotNull HuskSyncAPI api) {
|
||||
public LoginParticleSerializer(@NotNull HuskSyncAPI api) {
|
||||
super(api, LoginParticleData.class); // We pass the class type here so that Gson knows what class we're serializing
|
||||
}
|
||||
|
||||
@@ -116,12 +116,26 @@ public static Identifier LOGIN_PARTICLES_ID = Identifier.from("myplugin", "login
|
||||
huskSyncAPI.registerSerializer(LOGIN_PARTICLES_ID, new LoginParticleSerializer(HuskSyncAPI.getInstance()));
|
||||
```
|
||||
|
||||
### 3.1 Identifier dependencies
|
||||
* HuskSync lets you specify a set of `Dependency` objects when creating an `Identifier`. These are used to deterministically apply data in a specific order.
|
||||
* Dependencies are references to other data type identifiers. HuskSync will apply data in dependency-order; that is, it will apply the data of the dependencies before applying the data of the dependent.
|
||||
* This is useful when you have data that relies on other data to be applied first; for example, if you're writing an add-on for additional modded inventory data and you need to apply the base inventory data first.
|
||||
* You can specify whether a dependency is required or optional. HuskSync will not sync data of a type that has a required dependency that is missing (for instance, if it is disabled in the config, or - if provided by another plugin - has failed to register).
|
||||
* Use `Identifer#from(String, String, Set<Dependency>)` or `Identifier#from(Key, Set<Dependency>)` to create an identifier with dependencies
|
||||
* Dependencies can be created with `Dependency.optional(Identifier)` or `Dependency.required(Identifier)` for optional or required dependencies respectively.
|
||||
|
||||
## 4. Setting and getting our Data to/from a User
|
||||
* Now that we've registered our `Data` and `Serializer` classes, we can set our data to a user, applying it to them.
|
||||
* To do this, we use the `OnlineUser#setData(Identifier, Data)` method.
|
||||
* This method will apply the data to the user, and store the data to the plugin player custom data map, to allow the data to be retrieved later and be saved to snapshots.
|
||||
* Snapshots created on servers where the data type is registered will now contain our data and synchronise between instances!
|
||||
|
||||
```java
|
||||
// Create an identifier for our data requiring the user's location to have been set first
|
||||
public static Identifier LOGIN_PARTICLES_ID = Identifier.from("myplugin", "login_particles", Set.of(Dependency.optional(Key.key("husksync", "location"))));
|
||||
// We can then register this as we did previously (...)
|
||||
```
|
||||
|
||||
```java
|
||||
// Create an instance of our data
|
||||
LoginParticleData loginParticleData = new LoginParticleData("FIREWORKS_SPARK", 10);
|
||||
@@ -134,7 +148,7 @@ LoginParticleData loginParticleData = (LoginParticleData) huskSyncAPI.getUser(pl
|
||||
```
|
||||
|
||||
### 4.1 Persisting custom data on the DataSaveEvent
|
||||
Add an EventListener to the `DataSaveEvent` and use the `#editData` consumer method to apply custom data during standard DataSaves. This will persist data to users any time the data save routine executes (on user logout, server shutdownm, world save, etc).
|
||||
Add an EventListener to the `DataSaveEvent` and use the `#editData` consumer method to apply custom data during standard DataSaves. This will persist data to users any time the data save routine executes (on user logout, server shutdown, world save, etc.)
|
||||
|
||||
```java
|
||||
@EventHandler
|
||||
|
||||
@@ -213,8 +213,8 @@ huskSyncAPI.getCurrentData(user).thenAccept(optionalSnapshot -> {
|
||||
// Get the health data
|
||||
Data.Health health = healthOptional.get();
|
||||
double currentHealth = health.getCurrentHealth(); // Current health
|
||||
double healthScale = health.getHealthScale(); // Health scale (e.g., 20 for 20 hearts)
|
||||
snapshot.setHealth(BukkitData.Health.from(20, 20));
|
||||
double healthScale = health.getHealthScale(); // Health scale (used to determine health/damage display hearts)
|
||||
snapshot.setHealth(BukkitData.Health.from(20, 20, true));
|
||||
// Need max health? Look at the Attributes data type.
|
||||
|
||||
// Get the game mode data
|
||||
|
||||
14
docs/FAQs.md
14
docs/FAQs.md
@@ -12,23 +12,29 @@ HuskSync supports synchronising a wide range of different data elements, each of
|
||||
<details>
|
||||
<summary> <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 — check out the [[Custom Data API]] for information on how to get your mod's data syncing!
|
||||
|
||||
**TL;DR** — modded items may work, but since we can't guarantee compatibility, we do not officially mark them as supported. Be sure to test thoroughly before deploying on production!
|
||||
On Spigot, if you're running HuskSync on Arclight or similar, please note we will not be able to provide you with support, but have been reported to save & sync correctly with HuskSync v3.x+.
|
||||
|
||||
Please note we cannot guarantee compatibility with everything — test thoroughly!
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary> <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 — test thoroughly!
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary> <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.
|
||||
|
||||
* A MySQL Database (v8.0+) (MariaDB, PostrgreSQL or MongoDB are also supported)
|
||||
> **Note:** Please also note some specific legacy Paper/Purpur versions are [not compatible](Unsupported-Versions) with HuskSync.
|
||||
|
||||
* A MySQL Database (v8.0+)
|
||||
* **OR** a MariaDB, PostrgreSQL or MongoDB database, which are also supported
|
||||
* A Redis Database (v5.0+) — see [[FAQs]] for more details.
|
||||
* Any number of Spigot servers, connected by a BungeeCord or Velocity-based proxy (Minecraft v1.17.1+, running Java 17+)
|
||||
* **OR** a network of Fabric servers, connected by a Fabric proxy (Minecraft v1.20.1, running Java 17+)
|
||||
|
||||
## Setup Instructions
|
||||
### 1. Install the jar
|
||||
- Place the plugin jar file in the `/plugins/` directory of each Spigot server.
|
||||
- Place the plugin jar file in the `/plugins/` or `/mods/` directory of each Spigot/Fabric server respectively.
|
||||
- You do not need to install HuskSync as a proxy plugin.
|
||||
- You can additionally install [ProtocolLib](https://www.spigotmc.org/resources/protocollib.1997/) 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)
|
||||
|
||||
72
fabric/build.gradle
Normal file
72
fabric/build.gradle
Normal file
@@ -0,0 +1,72 @@
|
||||
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('org.apache.commons:commons-pool2:2.12.0')
|
||||
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.1.0'
|
||||
compileOnly 'net.william278:DesertWell:2.0.4'
|
||||
compileOnly 'org.projectlombok:lombok:1.18.32'
|
||||
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.32'
|
||||
|
||||
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)
|
||||
347
fabric/src/main/java/net/william278/husksync/FabricHuskSync.java
Normal file
347
fabric/src/main/java/net/william278/husksync/FabricHuskSync.java
Normal file
@@ -0,0 +1,347 @@
|
||||
/*
|
||||
* 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.MongoDbDatabase;
|
||||
import net.william278.husksync.database.MySqlDatabase;
|
||||
import net.william278.husksync.database.PostgresDatabase;
|
||||
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 = switch (settings.getDatabase().getType()) {
|
||||
case MYSQL, MARIADB -> new MySqlDatabase(this);
|
||||
case POSTGRES -> new PostgresDatabase(this);
|
||||
case MONGO -> new MongoDbDatabase(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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
BIN
fabric/src/main/resources/assets/husksync/icon.png
Normal file
BIN
fabric/src/main/resources/assets/husksync/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
55
fabric/src/main/resources/fabric.mod.json
Normal file
55
fabric/src/main/resources/fabric.mod.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
17
fabric/src/main/resources/husksync.mixins.json
Normal file
17
fabric/src/main/resources/husksync.mixins.json
Normal file
@@ -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,13 +3,21 @@ org.gradle.jvmargs='-Dfile.encoding=UTF-8'
|
||||
org.gradle.daemon=true
|
||||
javaVersion=17
|
||||
|
||||
plugin_version=3.5.1
|
||||
plugin_version=3.6
|
||||
plugin_archive=husksync
|
||||
plugin_description=A modern, cross-server player data synchronization system
|
||||
|
||||
jedis_version=5.1.2
|
||||
mysql_driver_version=8.3.0
|
||||
mariadb_driver_version=3.3.3
|
||||
jedis_version=5.1.3
|
||||
mysql_driver_version=8.4.0
|
||||
mariadb_driver_version=3.4.0
|
||||
postgres_driver_version=42.7.3
|
||||
mongodb_driver_version=5.0.1
|
||||
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
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
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
|
||||
|
||||
@@ -9,6 +9,10 @@ api-version: '1.19'
|
||||
folia-supported: true
|
||||
dependencies:
|
||||
server:
|
||||
packetevents:
|
||||
required: false
|
||||
load: BEFORE
|
||||
join-classpath: true
|
||||
ProtocolLib:
|
||||
required: false
|
||||
load: BEFORE
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
@@ -2,6 +2,6 @@ certifi==2023.7.22
|
||||
charset-normalizer==3.2.0
|
||||
colorama==0.4.6
|
||||
idna==3.7
|
||||
requests==2.31.0
|
||||
requests==2.32.0
|
||||
tqdm==4.66.3
|
||||
urllib3==2.0.7
|
||||
|
||||
@@ -12,15 +12,15 @@ from tqdm import tqdm
|
||||
# Parameters for starting a network of Minecraft servers
|
||||
class Parameters:
|
||||
root_dir = './servers/'
|
||||
proxy_version = "1.20"
|
||||
minecraft_version = '1.20.6'
|
||||
proxy_version = "1.21"
|
||||
minecraft_version = '1.21'
|
||||
eula_agreement = 'true'
|
||||
|
||||
backend_names = ['alpha', 'beta']
|
||||
backend_ports = [25567, 25568]
|
||||
backend_type = 'paper'
|
||||
backend_ram = 2048
|
||||
backend_plugins = ['../target/HuskSync-Paper-*.jar', './ProtocolLib/ProtocolLib.jar']
|
||||
backend_plugins = ['../target/HuskSync-Paper-*.jar']
|
||||
backend_plugin_folders = ['./HuskSync']
|
||||
operator_names = ['William278']
|
||||
operator_uuids = ['5dfb0558-e306-44f4-bb9a-f9218d4eb787']
|
||||
@@ -33,7 +33,7 @@ class Parameters:
|
||||
proxy_plugins = []
|
||||
proxy_plugin_folders = []
|
||||
|
||||
just_update_plugins = False
|
||||
just_update_plugins = True
|
||||
|
||||
|
||||
def main(update=False):
|
||||
|
||||
Reference in New Issue
Block a user