Paper plugin support, save player itemsToKeep rather than drops if not empty (#179)

* Paper plugin support, save itemsToKeep if present, close #172

* Fixup wrong packages, suppress a warning

* Update docs, add settings for death saving, reorganise config slightly

* Improve default server name lookup

* docs: Add note on Unsupported Versions

* docs: Minor Sync Modes tweaks
feat/data-edit-commands
William 1 year ago committed by GitHub
parent 6d9e68a65b
commit 7db3ed678f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -77,7 +77,11 @@ subprojects {
from '../LICENSE'
}
if (['bukkit', 'plugin'].contains(project.name)) {
if (['paper'].contains(project.name)) {
compileJava.options.release.set 17
}
if (['bukkit', 'paper', 'plugin'].contains(project.name)) {
shadowJar {
destinationDirectory.set(file("$rootDir/target"))
archiveClassifier.set('')

@ -168,7 +168,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
});
// Register events
initialize("events", (plugin) -> this.eventListener = new BukkitEventListener(this));
initialize("events", (plugin) -> this.eventListener = createEventListener());
// Register commands
initialize("commands", (plugin) -> BukkitCommand.Type.registerCommands(this));
@ -209,6 +209,11 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
log(Level.INFO, "Successfully disabled HuskSync v" + getPluginVersion());
}
@NotNull
protected BukkitEventListener createEventListener() {
return new BukkitEventListener(this);
}
@Override
@NotNull
public Set<OnlineUser> getOnlineUsers() {
@ -259,6 +264,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
@NotNull
@Override
@SuppressWarnings("unchecked")
public Map<Identifier, Serializer<? extends Data>> getSerializers() {
return serializers;
}

@ -294,10 +294,16 @@ public interface HuskSync extends Task.Supplier, EventDispatcher {
default void loadConfigs() {
try {
// Load settings
setSettings(Annotaml.create(new File(getDataFolder(), "config.yml"), Settings.class).get());
setSettings(Annotaml.create(
new File(getDataFolder(), "config.yml"),
Settings.class
).get());
// Load server name
setServer(Annotaml.create(new File(getDataFolder(), "server.yml"), Server.class).get());
setServer(Annotaml.create(
new File(getDataFolder(), "server.yml"),
Server.getDefault(this)
).get());
// Load locales from language preset default
final Locales languagePresets = Annotaml.create(

@ -19,11 +19,15 @@
package net.william278.husksync.config;
import net.william278.annotaml.Annotaml;
import net.william278.annotaml.YamlFile;
import net.william278.annotaml.YamlKey;
import net.william278.husksync.HuskSync;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.nio.file.Path;
import java.util.List;
/**
* Represents a server on a proxied network.
@ -37,26 +41,44 @@ import java.nio.file.Path;
If you join it using /server alpha, then set it to 'alpha' (case-sensitive)""")
public class Server {
@YamlKey("name")
private String serverName;
private Server(@NotNull String serverName) {
this.serverName = serverName;
}
@SuppressWarnings("unused")
private Server() {
}
@NotNull
public static Server getDefault(@NotNull HuskSync plugin) {
return new Server(getDefaultServerName(plugin));
}
/**
* Default server identifier.
* Find a sensible default name for the server name property
*/
@NotNull
public static String getDefaultServerName() {
private static String getDefaultServerName(@NotNull HuskSync plugin) {
try {
// Fetch server default from supported plugins if present
for (String s : List.of("HuskHomes", "HuskTowns")) {
final File serverFile = Path.of(plugin.getDataFolder().getParent(), s, "server.yml").toFile();
if (serverFile.exists()) {
return Annotaml.create(serverFile, Server.class).get().getName();
}
}
// Fetch server default from user dir name
final Path serverDirectory = Path.of(System.getProperty("user.dir"));
return serverDirectory.getFileName().toString().trim();
} catch (Exception e) {
} catch (Throwable e) {
return "server";
}
}
@YamlKey("name")
private String serverName = getDefaultServerName();
@SuppressWarnings("unused")
private Server() {
}
@Override
public boolean equals(@NotNull Object other) {
// If the name of this server matches another, the servers are the same.

@ -165,12 +165,21 @@ public class Settings {
private boolean saveOnWorldSave = true;
@YamlComment("Whether to create a snapshot for users when they die (containing their death drops)")
@YamlKey("synchronization.save_on_death")
@YamlKey("synchronization.save_on_death.enabled")
private boolean saveOnDeath = false;
@YamlComment("Whether to save empty death drops for users when they die")
@YamlKey("synchronization.save_empty_drops_on_death")
private boolean saveEmptyDropsOnDeath = true;
@YamlComment("What items to save in death snapshots? (DROPS or ITEMS_TO_KEEP). "
+ " Note that ITEMS_TO_KEEP (suggested for keepInventory servers) requires a Paper 1.19.4+ server.")
@YamlKey("synchronization.save_on_death.items_to_save")
private DeathItemsMode deathItemsMode = DeathItemsMode.DROPS;
@YamlComment("Should a death snapshot still be created even if the items to save on the player's death are empty?")
@YamlKey("synchronization.save_on_death.save_empty_items")
private boolean saveEmptyDeathItems = true;
@YamlComment("Whether dead players who log out and log in to a different server should have their items saved.")
@YamlKey("synchronization.save_on_death.sync_dead_players_changing_server")
private boolean synchronizeDeadPlayersChangingServer = true;
@YamlComment("Whether to use the snappy data compression algorithm. Keep on unless you know what you're doing")
@YamlKey("synchronization.compress_data")
@ -188,11 +197,6 @@ public class Settings {
@YamlKey("synchronization.synchronize_max_health")
private boolean synchronizeMaxHealth = true;
@YamlComment("Whether dead players who log out and log in to a different server should have their items saved. "
+ "You may need to modify this if you're using the keepInventory gamerule.")
@YamlKey("synchronization.synchronize_dead_players_changing_server")
private boolean synchronizeDeadPlayersChangingServer = true;
@YamlComment("If using the DELAY sync method, how long should this server listen for Redis key data updates before "
+ "pulling data from the database instead (i.e., if the user did not change servers).")
@YamlKey("synchronization.network_latency_milliseconds")
@ -341,8 +345,13 @@ public class Settings {
return saveOnDeath;
}
public boolean doSaveEmptyDropsOnDeath() {
return saveEmptyDropsOnDeath;
@NotNull
public DeathItemsMode getDeathItemsMode() {
return deathItemsMode;
}
public boolean doSaveEmptyDeathItems() {
return saveEmptyDeathItems;
}
public boolean doCompressData() {
@ -397,6 +406,14 @@ public class Settings {
}
}
/**
* Represents the mode of saving items on death
*/
public enum DeathItemsMode {
DROPS,
ITEMS_TO_KEEP
}
/**
* Represents the names of tables in the database
*/

@ -88,16 +88,16 @@ public abstract class EventListener {
* Handles the saving of data when a player dies
*
* @param user The user who died
* @param drops The items that this user would have dropped
* @param items The items that should be saved for this user on their death
*/
protected void saveOnPlayerDeath(@NotNull OnlineUser user, @NotNull Data.Items drops) {
protected void saveOnPlayerDeath(@NotNull OnlineUser user, @NotNull Data.Items items) {
if (plugin.isDisabling() || !plugin.getSettings().doSaveOnDeath() || plugin.isLocked(user.getUuid())
|| user.isNpc() || (!plugin.getSettings().doSaveEmptyDropsOnDeath() && drops.isEmpty())) {
|| user.isNpc() || (!plugin.getSettings().doSaveEmptyDeathItems() && items.isEmpty())) {
return;
}
final DataSnapshot.Packed snapshot = user.createSnapshot(DataSnapshot.SaveCause.DEATH);
snapshot.edit(plugin, (data -> data.getInventory().ifPresent(inventory -> inventory.setContents(drops))));
snapshot.edit(plugin, (data -> data.getInventory().ifPresent(inventory -> inventory.setContents(items))));
plugin.getDatabase().addSnapshot(user, snapshot);
}

@ -75,10 +75,15 @@ synchronization:
- MPDB_MIGRATION
# Whether to create a snapshot for users on a world when the server saves that world
save_on_world_save: true
save_on_death:
# Whether to create a snapshot for users when they die (containing their death drops)
save_on_death: false
# Whether to save empty death drops for users when they die
save_empty_drops_on_death: true
enabled: true
# What items to save in death snapshots? (DROPS or ITEMS_TO_KEEP). Note that ITEMS_TO_KEEP (suggested for keepInventory servers) requires a Paper 1.19.4+ server
items_to_save: DROPS
# Should a death snapshot still be created even if the items to save on the player's death are empty?
save_empty_items: false
# Whether dead players who log out and log in to a different server should have their items saved.
sync_dead_players_changing_server: true
# Whether to use the snappy data compression algorithm. Keep on unless you know what you're doing
compress_data: true
# Where to display sync notifications (ACTION_BAR, CHAT, TOAST or NONE)
@ -87,8 +92,6 @@ synchronization:
persist_locked_maps: true
# Whether to synchronize player max health (requires health syncing to be enabled)
synchronize_max_health: true
# Whether dead players who log out and log in to a different server should have their items saved. You may need to modify this if you're using the keepInventory gamerule.
synchronize_dead_players_changing_server: true
# If using the DELAY sync method, how long should this server listen for Redis key data updates before pulling data from the database instead (i.e., if the user did not change servers).
network_latency_milliseconds: 500
# Which data types to synchronize (Docs: https://william278.net/docs/husksync/sync-features)

@ -8,7 +8,7 @@ HuskSync has some special handling when players die, to account for scenarios wh
* **Snapshot creation on death**&mdash;HuskSync can create a special snapshot for backup purposes when a player dies, formed by taking their drops and setting this to their inventory. When `keepInventory` is enabled, the player drops are empty, so this creates an inaccurate snapshot. This option is disabled by default.
## How can this be fixed?
You will need to set the `synchronization.save_on_death` (which controls making snapshots on death), `save_empty_drops_on_death` (which controls whether snapshots of players who have no items to drop should be created), and `synchronization.synchronize_dead_players_changing_server` (which controls whether to sync dead players when they change servers) options to `false` in `config.yml`.
You should change the `items_to_save` mode to `ITEMS_TO_KEEP` instead of drops. Also, ensure `save_empty_items` and `sync_dead_players_changing_server` are enabled.
<details>
<summary>Example in config.yml</summary>
@ -16,15 +16,19 @@ You will need to set the `synchronization.save_on_death` (which controls making
```yml
synchronization:
#...
save_on_death: false # <-- Set this to false
save_empty_drops_on_death: false # <-- Set this to false
save_on_death:
# Whether to create a snapshot for users when they die (containing their death drops)
enabled: true
# What items to save in death snapshots? (DROPS or ITEMS_TO_KEEP). Note that ITEMS_TO_KEEP (suggested for keepInventory servers) requires a Paper 1.19.4+ server
items_to_save: ITEMS_TO_KEEP
# Should a death snapshot still be created even if the items to save on the player's death are empty?
save_empty_items: true
# Whether dead players who log out and log in to a different server should have their items saved.
sync_dead_players_changing_server: true
#...
synchronize_dead_players_changing_server: false # <-- Set this to false
```
</details>
## Troubleshooting with custom keepInventory setups
If the above doesn't work for you, you may need to do more things to get this to work properly.

@ -1,6 +1,8 @@
This will walk you through installing HuskSync on your network of Spigot servers.
## Requirements
> **Note:** If the plugin fails to load, please check that you are not running an [incompatible version combination](Unsupported-Versions)
* A MySQL Database (v8.0+)
* A Redis Database (v5.0+) &mdash; see [[FAQs]] for more details.
* Any number of Spigot servers, connected by a BungeeCord or Velocity-based proxy (Minecraft v1.16.5+, running Java 16+)

@ -1,5 +1,8 @@
HuskSync offers two built-in **synchronization modes**. These sync modes change the way data is synced between servers. This page details the two sync modes available and how they work.
HuskSync offers two built-in **synchronization modes** that utilise Redis and MySQL to optimally sync data as users change servers (illustrated below). These sync modes change the way data is synced between servers, and can be changed in the `config.yml` file.
![Overall architecture of the synchronisation systems](https://raw.githubusercontent.com/WiIIiam278/HuskSync/master/images/system-diagram.png)
## Available Modes
* The `DELAY` sync mode is the default sync mode, that use the `network_latency_miliseconds` value to apply a delay before listening to Redis data
* The `LOCKSTEP` sync mode uses a data checkout system to ensure that all servers are in sync regardless of network latency or tick rate fluctuations. This mode was introduced in HuskSync v3.1
@ -18,8 +21,6 @@ synchronization:
</details>
## Delay
![Delay diagram](https://raw.githubusercontent.com/WiIIiam278/HuskSync/master/images/system-diagram.png)
The `DELAY` sync mode works as described below:
* When a user disconnects from a server, a `SERVER_SWITCH` key is immediately set on Redis, followed by a `DATA_UPDATE` key which contains the user's packed and serialized Data Snapshot.
* When the user connects to a server, they are marked as locked (unable to break blocks, use containers, etc.)

@ -0,0 +1,10 @@
This plugin does not support the following software-Minecraft version combinations. The plugin will fail to load if you attempt to run it with these versions. Apologies for the inconvenience.
## Incompatibility table
| Minecraft Versions | Server Software | Notes |
|--------------------|-------------------------------------------|----------------------------------------|
| 1.19.4 | Only: `Purpur, Pufferfish`&dagger; | Older Paper builds also not supported. |
| 1.19.3 | Only: `Paper, Purpur, Pufferfish`&dagger; | Upgrade to 1.19.4 or use Spigot |
| below 1.16.5 | _All_ | Upgrade to 1.16.5 |
&dagger;Further downstream forks of this server software are also affected.

@ -0,0 +1,41 @@
dependencies {
implementation project(':bukkit')
compileOnly project(':common')
compileOnly 'io.papermc.paper:paper-api:1.19.4-R0.1-SNAPSHOT'
}
shadowJar {
dependencies {
exclude(dependency('com.mojang:brigadier'))
}
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 'org.json', 'net.william278.husksync.libraries.json'
relocate 'com.fatboyindustrial', 'net.william278.husksync.libraries'
relocate 'de.themoep', 'net.william278.husksync.libraries'
relocate 'net.kyori', '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 'dev.dejvokep', 'net.william278.husksync.libraries'
relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell'
relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown'
relocate 'net.william278.mapdataapi', 'net.william278.husksync.libraries.mapdataapi'
relocate 'net.william278.andjam', 'net.william278.husksync.libraries.andjam'
relocate 'net.querz', 'net.william278.husksync.libraries.nbtparser'
relocate 'net.roxeez', 'net.william278.husksync.libraries'
relocate 'me.lucko.commodore', 'net.william278.husksync.libraries.commodore'
relocate 'net.byteflux.libby', 'net.william278.husksync.libraries.libby'
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
relocate 'dev.triumphteam.gui', 'net.william278.husksync.libraries.triumphgui'
relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'
relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter'
relocate 'net.william278.annotaml', 'net.william278.husksync.libraries.annotaml'
relocate 'space.arim.morepaperlib', 'net.william278.husksync.libraries.paperlib'
relocate 'de.tr7zw.changeme.nbtapi', 'net.william278.husksync.libraries.nbtapi'
}

@ -0,0 +1,35 @@
/*
* 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 net.william278.husksync.listener.BukkitEventListener;
import net.william278.husksync.listener.PaperEventListener;
import org.jetbrains.annotations.NotNull;
@SuppressWarnings("unused")
public class PaperHuskSync extends BukkitHuskSync {
@NotNull
@Override
protected BukkitEventListener createEventListener() {
return new PaperEventListener(this);
}
}

@ -0,0 +1,82 @@
/*
* 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 io.papermc.paper.plugin.loader.PluginClasspathBuilder;
import io.papermc.paper.plugin.loader.PluginLoader;
import io.papermc.paper.plugin.loader.library.impl.MavenLibraryResolver;
import net.william278.annotaml.Annotaml;
import net.william278.annotaml.YamlFile;
import net.william278.annotaml.YamlKey;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.repository.RemoteRepository;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.InputStream;
import java.util.List;
import java.util.Objects;
@SuppressWarnings({"UnstableApiUsage", "unused"})
public class PaperHuskSyncLoader implements PluginLoader {
@Override
public void classloader(@NotNull PluginClasspathBuilder classpathBuilder) {
MavenLibraryResolver resolver = new MavenLibraryResolver();
resolveLibraries(classpathBuilder).stream()
.map(DefaultArtifact::new)
.forEach(artifact -> resolver.addDependency(new Dependency(artifact, null)));
resolver.addRepository(new RemoteRepository.Builder(
"maven", "default", "https://repo.maven.apache.org/maven2/"
).build());
classpathBuilder.addLibrary(resolver);
}
@NotNull
private static List<String> resolveLibraries(@NotNull PluginClasspathBuilder classpathBuilder) {
try (InputStream input = getLibraryListFile()) {
return Annotaml.create(PaperLibraries.class, Objects.requireNonNull(input)).get().libraries;
} catch (Exception e) {
classpathBuilder.getContext().getLogger().error("Failed to resolve libraries", e);
}
return List.of();
}
@Nullable
private static InputStream getLibraryListFile() {
return PaperHuskSyncLoader.class.getClassLoader().getResourceAsStream("paper-libraries.yml");
}
@YamlFile(header = "Dependencies for HuskSync on Paper")
public static class PaperLibraries {
@YamlKey("libraries")
private List<String> libraries;
@SuppressWarnings("unused")
private PaperLibraries() {
}
}
}

@ -0,0 +1,65 @@
/*
* 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.william278.husksync.BukkitHuskSync;
import net.william278.husksync.data.BukkitData;
import net.william278.husksync.user.BukkitUser;
import net.william278.husksync.user.OnlineUser;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class PaperEventListener extends BukkitEventListener {
public PaperEventListener(@NotNull BukkitHuskSync plugin) {
super(plugin);
}
@Override
public void handlePlayerDeath(@NotNull PlayerDeathEvent event) {
// If the player is locked or the plugin disabling, clear their drops
final OnlineUser user = BukkitUser.adapt(event.getEntity(), plugin);
if (cancelPlayerEvent(user.getUuid())) {
event.getDrops().clear();
event.getItemsToKeep().clear();
return;
}
// Handle saving player data snapshots on death
if (!plugin.getSettings().doSaveOnDeath()) {
return;
}
// Paper - support saving the player's items to keep if enabled
final int maxInventorySize = BukkitData.Items.Inventory.INVENTORY_SLOT_COUNT;
final List<ItemStack> itemsToSave = switch (plugin.getSettings().getDeathItemsMode()) {
case DROPS -> event.getDrops();
case ITEMS_TO_KEEP -> event.getItemsToKeep();
};
if (itemsToSave.size() > maxInventorySize) {
itemsToSave.subList(maxInventorySize, itemsToSave.size()).clear();
}
super.saveOnPlayerDeath(user, BukkitData.Items.ItemArray.adapt(itemsToSave));
}
}

@ -0,0 +1,6 @@
# Dependencies for HuskSync on Paper
libraries:
- 'redis.clients:jedis:${jedis_version}'
- 'com.mysql:mysql-connector-j:${mysql_driver_version}'
- 'org.mariadb.jdbc:mariadb-java-client:${mariadb_driver_version}'
- 'org.xerial.snappy:snappy-java:${snappy_version}'

@ -0,0 +1,18 @@
name: 'HuskSync'
description: '${description}'
author: 'William278'
website: 'https://william278.net/'
main: 'net.william278.husksync.PaperHuskSync'
loader: 'net.william278.husksync.PaperHuskSyncLoader'
version: '${version}'
api-version: '1.19'
dependencies:
server:
MysqlPlayerDataBridge:
required: false
load: BEFORE
join-classpath: true
Plan:
required: false
load: BEFORE
join-classpath: true

@ -1,3 +1,4 @@
dependencies {
implementation project(path: ':bukkit', configuration: 'shadow')
runtimeOnly project(path: ':paper')
}

@ -5,7 +5,9 @@ pluginManagement {
}
rootProject.name = 'HuskSync'
include 'common'
include 'bukkit'
include 'plugin'
include(
'common',
'bukkit',
'paper',
'plugin'
)
Loading…
Cancel
Save