9
0
mirror of https://github.com/WiIIiam278/HuskSync.git synced 2025-12-21 15:49:20 +00:00

Compare commits

..

59 Commits
2.1.1 ... 2.2.3

Author SHA1 Message Date
William
4288742052 Update dependencies, close #85, close #86, close #87, close #88, close #89 2023-02-13 13:29:52 +00:00
William
8205b9c169 Create dependabot.yml and funding.yml 2023-02-13 13:20:27 +00:00
William
1d7f6a8d8b refactor: Use mappings class for PDC tag type handling 2023-02-12 19:14:46 +00:00
William
3425c97245 Bump to v2.2.3 2023-02-12 18:18:29 +00:00
evlad
2d1d8f1ab6 Sync lock: Cancel item frame interaction, add command blacklist (#84)
Co-authored-by: William <will27528@gmail.com>
2023-02-12 18:08:46 +00:00
William
f322d31b03 [ci skip] Correct typos in README 2023-02-12 15:43:26 +00:00
William278
368e665ac3 [ci skip] Update README 2023-01-27 11:46:31 +00:00
William278
922eb2f19a Merge remote-tracking branch 'origin/master'
# Conflicts:
#	README.md
2023-01-27 11:45:50 +00:00
William278
23e0123004 Update README and banner 2023-01-27 11:45:04 +00:00
William
e98bac844a Hotfix bump to 2.2.2 - fix unchecked cast on trident lock check 2023-01-10 14:42:54 +00:00
William
d6d9a55f72 Fix unchecked cast on trident launch locking, fix #79 2023-01-10 14:42:28 +00:00
William
e3070a65ab Ensure player isn't locked before force-dropping items 2023-01-07 22:17:42 +00:00
William
a8b4696604 Minor style tweaks 2023-01-07 22:14:06 +00:00
William
7f5ca6206b Merge pull request #76 from ItsWagPvP/patch-1 2023-01-07 22:11:18 +00:00
William
ad885a9a15 Bump library versions, fix test dependencies 2023-01-07 22:11:05 +00:00
William
fe89e7b770 Fix tests 2023-01-07 21:31:14 +00:00
William
17ea62ed0b Merge pull request #78 from emmanuelvlad/master 2023-01-07 21:11:01 +00:00
evlad
94717637ba fix dupe with trident when player is locked 2023-01-07 22:06:47 +01:00
William
f6663f0c09 Refactor; consolidate Logger and ResourceReader, simplify certain method arguments 2023-01-05 19:06:04 +00:00
William
33588c2345 Bump to v2.2.1 2023-01-05 18:35:34 +00:00
William
c2c5a424fb Add checks against the user being an NPC 2023-01-05 18:35:10 +00:00
Gabriele Cabrini
ce41053e87 Fixed issue #74
Added the code that @alexdev03 suggested
2023-01-03 21:06:26 +01:00
William
5817de83e5 Fix locked map data not being applied in some cases 2023-01-03 12:38:17 +00:00
William
30dd48ce88 Add additional checks and error handling when setting health 2023-01-03 12:14:18 +00:00
William
cf7912a89e Bump to 2.2 2023-01-03 11:58:06 +00:00
William
9900b44858 Disable locked map syncing by default 2023-01-03 11:56:47 +00:00
William
9019181208 Merge remote-tracking branch 'origin/master' 2023-01-03 11:51:29 +00:00
William
99483387f1 Add additional error handling for player health and statistic updating 2023-01-03 11:51:25 +00:00
William
42177f2582 Merge pull request #75 from emmanuelvlad/expose-locked-players 2023-01-01 16:19:52 +00:00
William
e4e0743205 [ci skip] Update license year (2023) 2023-01-01 16:18:58 +00:00
evlad
105927a57f added dummy method 2022-12-31 04:41:20 +03:00
evlad
71706bf9ae expose locked players + add a method on OnlineUser 2022-12-31 04:30:45 +03:00
William
101e0c11d7 Fix unsynced players having data saved on world save / death 2022-12-27 19:31:14 +00:00
William
70323fb2e2 Merge remote-tracking branch 'origin/master' 2022-12-26 16:42:48 +00:00
William
9dc5577175 Clear the player's cursor when setting inventory contents 2022-12-26 16:06:22 +00:00
William
117d5edea2 [ci skip] Update badge 2022-12-19 16:04:35 +00:00
William
3f0f518037 [ci-skip] Clarify minimum Java version in README 2022-11-20 18:43:04 +00:00
William
2017ecc20f Minor refactoring / code improvements 2022-11-18 14:40:15 +00:00
William
ded89ad343 ACTION_BAR as default notification slot 2022-11-18 11:25:00 +00:00
William
c4b194f8d6 Fix unlocked maps getting wrongly locked 2022-11-17 02:46:40 +00:00
William
d682e6e6c6 Fix notifications through Toast AndJam library 2022-11-17 02:34:52 +00:00
William
6fef9c4eae Remove map itemstack debug logging mess 2022-11-16 22:40:23 +00:00
William278
16eee05065 Add notification slot configuration, support for toasts 2022-11-16 22:31:57 +00:00
William278
b664e2586d Bump gson to 2.10 2022-11-16 22:06:26 +00:00
William278
d594c9c257 Truncate long data save cause names, close #60 2022-11-16 14:44:32 +00:00
William278
532a65eca8 Ensure players remain locked on disconnect and shutdown, close #67 2022-11-16 12:06:01 +00:00
William
5af8ae0da5 Use canvas rendering approach, finish locked map synchronisation 2022-11-15 23:37:41 +00:00
William278
c0709f82bd Add the ability to synchronise/persist locked maps cross-server, close #14 2022-11-15 18:29:37 +00:00
William278
945b65e1bc Minor performance improvement to event cancelling, add checks against inventory clicks 2022-11-15 17:46:55 +00:00
William278
efcb36d345 Fix database username at wrong config path 2022-11-15 11:37:15 +00:00
William
30cd89c578 Remove unnecessary debug 2022-11-15 00:43:52 +00:00
William
bb3753b8e4 Fix typo 2022-11-15 00:42:34 +00:00
William
d5569ad3ed Fix event priorities in config, bump to 2.1.3 2022-11-15 00:36:45 +00:00
William
d8386fd2a2 Fix edit nodes not being respected 2022-11-14 18:25:44 +00:00
William
3bfea58f35 Make event priority configurable for three key events 2022-11-14 11:47:33 +00:00
William
51cf7beeb8 Remove redundant compiler warning suppressors 2022-11-14 11:04:41 +00:00
William
df247b41f4 Bump to v2.1.2 2022-11-06 22:32:46 +00:00
William
bac760165e Tweak logic for determining if a player is dead, fix issues with <1HP players being detected dead 2022-11-06 22:31:33 +00:00
William
dd39482ed1 Tweaked bukkit implementation of #isDead detection 2022-10-28 13:00:08 +01:00
51 changed files with 1279 additions and 779 deletions

7
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,7 @@
# Dependabot configuration file for GitHub
version: 2
updates:
- package-ecosystem: "gradle" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"

3
.github/funding.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
# Funding metadata for GitHub
github: WiIIiam278
custom: https://buymeacoff.ee/william278

View File

@@ -1,4 +1,4 @@
Copyright © William278 2022. All rights reserved Copyright © William278 2023. All rights reserved
LICENSE LICENSE
This source code is provided as reference to licensed individuals that have purchased the HuskSync This source code is provided as reference to licensed individuals that have purchased the HuskSync

View File

@@ -1,36 +1,59 @@
# [![HuskSync Banner](images/banner-graphic.png)](https://github.com/WiIIiam278/HuskSync) <p align="center">
[![GitHub CI](https://img.shields.io/github/workflow/status/WiIIiam278/HuskSync/Java%20CI?logo=github)](https://github.com/WiIIiam278/HuskSync/actions/workflows/java_ci.yml) <img src="images/banner.png" alt="HuskSync" />
[![JitPack API](https://img.shields.io/jitpack/version/net.william278/HuskSync?color=%2300fb9a&label=api&logo=gradle)](https://jitpack.io/#net.william278/HuskSync) <a href="https://github.com/WiIIiam278/HuskSync/actions/workflows/java_ci.yml">
[![Support Discord](https://img.shields.io/discord/818135932103557162.svg?label=&logo=discord&logoColor=fff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/tVYhJfyDWG) <img src="https://img.shields.io/github/actions/workflow/status/WiIIiam278/HuskSync/java_ci.yml?branch=master&logo=github"/>
</a>
[Documentation, Guides & API](https://william278.net/docs/husksync) · [Resource Page](https://www.spigotmc.org/resources/husksync.97144/) · [Bug Reports](https://github.com/WiIIiam278/HuskSync/issues) <a href="https://jitpack.io/#net.william278/HuskSync">
<img src="https://img.shields.io/jitpack/version/net.william278/HuskSync?color=%2300fb9a&label=api&logo=gradle" />
</a>
<a href="https://discord.gg/tVYhJfyDWG">
<img src="https://img.shields.io/discord/818135932103557162.svg?label=&logo=discord&logoColor=fff&color=7389D8&labelColor=6A7EC2" />
</a>
<br/>
<b>
<a href="https://www.spigotmc.org/resources/husksync.97144/">Spigot</a>
</b>
<b>
<a href="https://william278.net/docs/husksync/setup">Setup</a>
</b>
<b>
<a href="https://william278.net/docs/husksync/">Docs</a>
</b>
<b>
<a href="https://github.com/WiIIiam278/HuskSync/issues">Issues</a>
</b>
</p>
<br/>
**HuskSync** is a modern, cross-server player data synchronisation system that enables the comprehensive synchronisation of your user's data across multiple proxied servers. It does this by making use of Redis and MySQL to optimally cache data while players change servers. **HuskSync** is a modern, cross-server player data synchronisation system that enables the comprehensive synchronisation of your user's data across multiple proxied servers. It does this by making use of Redis and MySQL to optimally cache data while players change servers.
## Features ## Features
![Data snapshot viewer](images/data-snapshot-viewer.png) **⭐ Seamless synchronisation** &mdash; Utilises optimised Redis caching when players change server to sync player data super quickly for a seamless experience.
- Synchronise inventories, ender chests, advancements, statistics, experience points, health, max health, hunger, saturation, potion effects, persistent data container tags, game mode, location and more across multiple proxied servers. **⭐ Complete player synchronisation** &mdash; Sync inventories, Ender Chests, health, hunger, effects, advancements, statistics, locked maps & [more](https://william278.net/docs/husksync/sync-features)—no data left behind!
- Create and manage "snapshot" backups of user data and roll back users to previous states on-the-fly. (`/userdata`)
- Preview, list, delete, restore & pin user data snapshots in-game with an intuitive menu.
- Examine the contents of player's inventories and ender chests on-the-fly. (`/inventory`, `/enderchest`)
- Hooks with your [Player Analytics](https://github.com/plan-player-analytics/Plan) web panel to provide an overview of user data.
- Supports segregating synchronisation across multiple distinct clusters on one network.
## Requirements **⭐ Backup, restore & rotate** &mdash; Something gone wrong? Restore players back to a previous data state. Rotate and manage data snapshots in-game!
* A MySQL Database (v8.0+).
* A Redis Database (v5.0+) **⭐ Import existing data** &mdash; Import your MySQLPlayerDataBridge data—or from your existing world data! No server reset needed!
* Any number of proxied Spigot servers (Minecraft v1.16.5+)
**⭐ Works great with Plan** &mdash; Stay in touch with your community through HuskSync analytics on your Plan web panel.
**⭐ Extensible API & open-source** &mdash; Need more? Extend the plugin with the Developer API. Or, submit a pull request through our code bounty system!
**Ready?** [Let's head down town!](https://william278.net/docs/husksync/setup)
## Setup ## Setup
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. Requires a MySQL (v8.0+) database, a Redis (v5.0+) server and any number of Spigot-based 1.16.5+ Minecraft servers, running Java 16+.
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.
2. Start, then stop every server to let HuskSync generate the config file. 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 the MySQL and Redis database credentials. 3. Navigate to the HuskSync config file on each server (~/plugins/HuskSync/config.yml) and fill in both the MySQL and Redis database credentials.
4. Start every server again and synchronization will begin. 4. Start every server again and synchronization will begin.
## Building ## Building
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:
```
```bash
./gradlew clean build ./gradlew clean build
``` ```
@@ -42,23 +65,18 @@ HuskSync is a premium resource. This source code is provided as reference only f
## Contributing ## Contributing
A code bounty program is in place for HuskSync, where developers making significant code contributions to HuskSync may be entitled to a license at my discretion to use HuskSync in commercial contexts without having to purchase the resource. Please read the information for contributors in the LICENSE file before submitting a pull request. A code bounty program is in place for HuskSync, where developers making significant code contributions to HuskSync may be entitled to a license at my discretion to use HuskSync in commercial contexts without having to purchase the resource. Please read the information for contributors in the LICENSE file before submitting a pull request.
## Translation ## 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. 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.
- [Locales Directory](https://github.com/WiIIiam278/HuskSync/tree/master/common/src/main/resources/locales) - [Locales Directory](https://github.com/WiIIiam278/HuskSync/tree/master/common/src/main/resources/languages)
- [English Locales](https://github.com/WiIIiam278/HuskSync/tree/master/common/src/main/resources/locales/en-gb.yml) - [English Locales](https://github.com/WiIIiam278/HuskSync/tree/master/common/src/main/resources/languages/en-gb.yml)
## bStats
This plugin uses bStats to provide me with metrics about its usage:
- [bStats Metrics](https://bstats.org/plugin/bukkit/HuskSync%20-%20Bukkit/13140)
You can turn metric collection off by navigating to `~/plugins/bStats/config.yml` and editing the config to disable plugin metrics.
## Links ## Links
- [Documentation, Guides & API](https://william278.net/docs/husksync) - [Docs](https://william278.net/docs/husksync/) &mdash; Read the plugin documentation!
- [Resource Page](https://www.spigotmc.org/resources/husksync.97144/) - [Spigot](https://www.spigotmc.org/resources/husksync.97144/) &mdash; View the Spigot resource page (Also: [Polymart](https://polymart.org/resource/husksync.1634), [Songoda](https://marketplace.songoda.com/marketplace/product/husksync-a-modern-cross-server-player-data-synchronization-system.758))
- [Bug Reports](https://github.com/WiIIiam278/HuskSync/issues) - [Issues](https://github.com/WiIIiam278/HuskSync/issues) &mdash; File a bug report or feature request
- [Discord Support](https://discord.gg/tVYhJfyDWG) (Proof of purchase required) - [Discord](https://discord.gg/tVYhJfyDWG) &mdash; Get help, ask questions (Proof of purchase required)
- [bStats](https://bstats.org/plugin/bukkit/HuskSync%20-%20Bukkit/13140) &mdash; View plugin metrics
--- ---
&copy; [William278](https://william278.net/), 2022. All rights reserved. &copy; [William278](https://william278.net/), 2023. All rights reserved.

View File

@@ -1,10 +1,12 @@
dependencies { dependencies {
implementation project(path: ':common') implementation project(path: ':common')
implementation 'org.bstats:bstats-bukkit:3.0.0' implementation 'org.bstats:bstats-bukkit:3.0.1'
implementation 'net.william278:mpdbdataconverter:1.0.1' implementation 'net.william278:mpdbdataconverter:1.0.1'
implementation 'net.william278:hsldataconverter:1.0' implementation 'net.william278:hsldataconverter:1.0'
implementation 'net.william278:MapDataAPI:1.0.2'
implementation 'net.william278:AndJam:1.0.2'
implementation 'me.lucko:commodore:2.2' implementation 'me.lucko:commodore:2.2'
implementation 'net.kyori:adventure-platform-bukkit:4.1.2' implementation 'net.kyori:adventure-platform-bukkit:4.2.0'
implementation 'dev.triumphteam:triumph-gui:3.1.3' implementation 'dev.triumphteam:triumph-gui:3.1.3'
compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT' compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
@@ -12,8 +14,10 @@ dependencies {
compileOnly 'de.themoep:minedown-adventure:1.7.1-SNAPSHOT' compileOnly 'de.themoep:minedown-adventure:1.7.1-SNAPSHOT'
compileOnly 'dev.dejvokep:boosted-yaml:1.3' compileOnly 'dev.dejvokep:boosted-yaml:1.3'
compileOnly 'com.zaxxer:HikariCP:5.0.1' compileOnly 'com.zaxxer:HikariCP:5.0.1'
compileOnly 'redis.clients:jedis:' + jedis_version
compileOnly 'net.william278:DesertWell:1.1' compileOnly 'net.william278:DesertWell:1.1'
compileOnly 'net.william278:Annotaml:2.0' compileOnly 'net.william278:Annotaml:2.0.1'
compileOnly 'net.william278:AdvancementAPI:97a9583413'
} }
shadowJar { shadowJar {
@@ -31,6 +35,10 @@ shadowJar {
relocate 'dev.dejvokep', 'net.william278.husksync.libraries' relocate 'dev.dejvokep', 'net.william278.husksync.libraries'
relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell' relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell'
relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown' 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.nbt'
relocate 'net.roxeez', 'net.william278.husksync.libraries'
relocate 'me.lucko.commodore', 'net.william278.husksync.libraries.commodore' relocate 'me.lucko.commodore', 'net.william278.husksync.libraries.commodore'
relocate 'net.byteflux.libby', 'net.william278.husksync.libraries.libby' relocate 'net.byteflux.libby', 'net.william278.husksync.libraries.libby'

View File

@@ -24,10 +24,6 @@ import net.william278.husksync.migrator.MpdbMigrator;
import net.william278.husksync.player.BukkitPlayer; import net.william278.husksync.player.BukkitPlayer;
import net.william278.husksync.player.OnlineUser; import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.redis.RedisManager; import net.william278.husksync.redis.RedisManager;
import net.william278.husksync.util.BukkitLogger;
import net.william278.husksync.util.BukkitResourceReader;
import net.william278.husksync.util.Logger;
import net.william278.husksync.util.ResourceReader;
import org.bstats.bukkit.Metrics; import org.bstats.bukkit.Metrics;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.command.PluginCommand; import org.bukkit.command.PluginCommand;
@@ -54,8 +50,6 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
private static final int METRICS_ID = 13140; private static final int METRICS_ID = 13140;
private Database database; private Database database;
private RedisManager redisManager; private RedisManager redisManager;
private Logger logger;
private ResourceReader resourceReader;
private EventListener eventListener; private EventListener eventListener;
private DataAdapter dataAdapter; private DataAdapter dataAdapter;
private EventCannon eventCannon; private EventCannon eventCannon;
@@ -85,19 +79,14 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
// Initialize HuskSync // Initialize HuskSync
final AtomicBoolean initialized = new AtomicBoolean(true); final AtomicBoolean initialized = new AtomicBoolean(true);
try { try {
// Set the logging adapter and resource reader
this.logger = new BukkitLogger(this.getLogger());
this.resourceReader = new BukkitResourceReader(this);
// Create adventure audience // Create adventure audience
this.audiences = BukkitAudiences.create(this); this.audiences = BukkitAudiences.create(this);
// Load settings and locales // Load settings and locales
getLoggingAdapter().log(Level.INFO, "Loading plugin configuration settings & locales..."); log(Level.INFO, "Loading plugin configuration settings & locales...");
initialized.set(reload().join()); initialized.set(reload().join());
if (initialized.get()) { if (initialized.get()) {
logger.showDebugLogs(settings.debugLogging); log(Level.INFO, "Successfully loaded plugin configuration settings & locales");
getLoggingAdapter().log(Level.INFO, "Successfully loaded plugin configuration settings & locales");
} else { } else {
throw new HuskSyncInitializationException("Failed to load plugin configuration settings and/or locales"); throw new HuskSyncInitializationException("Failed to load plugin configuration settings and/or locales");
} }
@@ -121,11 +110,11 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
} }
// Prepare database connection // Prepare database connection
this.database = new MySqlDatabase(settings, resourceReader, logger, dataAdapter, eventCannon); this.database = new MySqlDatabase(this);
getLoggingAdapter().log(Level.INFO, "Attempting to establish connection to the database..."); log(Level.INFO, "Attempting to establish connection to the database...");
initialized.set(this.database.initialize()); initialized.set(this.database.initialize());
if (initialized.get()) { if (initialized.get()) {
getLoggingAdapter().log(Level.INFO, "Successfully established a connection to the database"); log(Level.INFO, "Successfully established a connection to the database");
} else { } else {
throw new HuskSyncInitializationException("Failed to establish a connection to the database. " + throw new HuskSyncInitializationException("Failed to establish a connection to the database. " +
"Please check the supplied database credentials in the config file"); "Please check the supplied database credentials in the config file");
@@ -133,22 +122,22 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
// Prepare redis connection // Prepare redis connection
this.redisManager = new RedisManager(this); this.redisManager = new RedisManager(this);
getLoggingAdapter().log(Level.INFO, "Attempting to establish connection to the Redis server..."); log(Level.INFO, "Attempting to establish connection to the Redis server...");
initialized.set(this.redisManager.initialize().join()); initialized.set(this.redisManager.initialize());
if (initialized.get()) { if (initialized.get()) {
getLoggingAdapter().log(Level.INFO, "Successfully established a connection to the Redis server"); log(Level.INFO, "Successfully established a connection to the Redis server");
} else { } else {
throw new HuskSyncInitializationException("Failed to establish a connection to the Redis server. " + throw new HuskSyncInitializationException("Failed to establish a connection to the Redis server. " +
"Please check the supplied Redis credentials in the config file"); "Please check the supplied Redis credentials in the config file");
} }
// Register events // Register events
getLoggingAdapter().log(Level.INFO, "Registering events..."); log(Level.INFO, "Registering events...");
this.eventListener = new BukkitEventListener(this); this.eventListener = new BukkitEventListener(this);
getLoggingAdapter().log(Level.INFO, "Successfully registered events listener"); log(Level.INFO, "Successfully registered events listener");
// Register permissions // Register permissions
getLoggingAdapter().log(Level.INFO, "Registering permissions & commands..."); log(Level.INFO, "Registering permissions & commands...");
Arrays.stream(Permission.values()).forEach(permission -> getServer().getPluginManager() Arrays.stream(Permission.values()).forEach(permission -> getServer().getPluginManager()
.addPermission(new org.bukkit.permissions.Permission(permission.node, switch (permission.defaultAccess) { .addPermission(new org.bukkit.permissions.Permission(permission.node, switch (permission.defaultAccess) {
case EVERYONE -> PermissionDefault.TRUE; case EVERYONE -> PermissionDefault.TRUE;
@@ -163,32 +152,32 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
new BukkitCommand(bukkitCommandType.commandBase, this).register(pluginCommand); new BukkitCommand(bukkitCommandType.commandBase, this).register(pluginCommand);
} }
} }
getLoggingAdapter().log(Level.INFO, "Successfully registered permissions & commands"); log(Level.INFO, "Successfully registered permissions & commands");
// Hook into plan // Hook into plan
if (Bukkit.getPluginManager().getPlugin("Plan") != null) { if (Bukkit.getPluginManager().getPlugin("Plan") != null) {
getLoggingAdapter().log(Level.INFO, "Enabling Plan integration..."); log(Level.INFO, "Enabling Plan integration...");
new PlanHook(database, logger).hookIntoPlan(); new PlanHook(this).hookIntoPlan();
getLoggingAdapter().log(Level.INFO, "Plan integration enabled!"); log(Level.INFO, "Plan integration enabled!");
} }
// Hook into bStats metrics // Hook into bStats metrics
try { try {
new Metrics(this, METRICS_ID); new Metrics(this, METRICS_ID);
} catch (final Exception e) { } catch (final Exception e) {
getLoggingAdapter().log(Level.WARNING, "Skipped bStats metrics initialization due to an exception."); log(Level.WARNING, "Skipped bStats metrics initialization due to an exception.");
} }
// Check for updates // Check for updates
if (settings.checkForUpdates) { if (settings.checkForUpdates) {
getLoggingAdapter().log(Level.INFO, "Checking for updates..."); log(Level.INFO, "Checking for updates...");
getLatestVersionIfOutdated().thenAccept(newestVersion -> getLatestVersionIfOutdated().thenAccept(newestVersion ->
newestVersion.ifPresent(newVersion -> getLoggingAdapter().log(Level.WARNING, newestVersion.ifPresent(newVersion -> log(Level.WARNING,
"An update is available for HuskSync, v" + newVersion "An update is available for HuskSync, v" + newVersion
+ " (Currently running v" + getPluginVersion() + ")"))); + " (Currently running v" + getPluginVersion() + ")")));
} }
} catch (HuskSyncInitializationException exception) { } catch (HuskSyncInitializationException exception) {
getLoggingAdapter().log(Level.SEVERE, """ log(Level.SEVERE, """
*************************************************** ***************************************************
Failed to initialize HuskSync! Failed to initialize HuskSync!
@@ -203,14 +192,14 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
.replaceAll("%error_message%", exception.getMessage())); .replaceAll("%error_message%", exception.getMessage()));
initialized.set(false); initialized.set(false);
} catch (Exception exception) { } catch (Exception exception) {
getLoggingAdapter().log(Level.SEVERE, "An unhandled exception occurred initializing HuskSync!", exception); log(Level.SEVERE, "An unhandled exception occurred initializing HuskSync!", exception);
initialized.set(false); initialized.set(false);
} finally { } finally {
// Validate initialization // Validate initialization
if (initialized.get()) { if (initialized.get()) {
getLoggingAdapter().log(Level.INFO, "Successfully enabled HuskSync v" + getPluginVersion()); log(Level.INFO, "Successfully enabled HuskSync v" + getPluginVersion());
} else { } else {
getLoggingAdapter().log(Level.SEVERE, "Failed to initialize HuskSync. The plugin will now be disabled"); log(Level.SEVERE, "Failed to initialize HuskSync. The plugin will now be disabled");
getServer().getPluginManager().disablePlugin(this); getServer().getPluginManager().disablePlugin(this);
} }
} }
@@ -221,7 +210,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
if (this.eventListener != null) { if (this.eventListener != null) {
this.eventListener.handlePluginDisable(); this.eventListener.handlePluginDisable();
} }
getLoggingAdapter().log(Level.INFO, "Successfully disabled HuskSync v" + getPluginVersion()); log(Level.INFO, "Successfully disabled HuskSync v" + getPluginVersion());
} }
@Override @Override
@@ -275,14 +264,8 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
} }
@Override @Override
public @NotNull Logger getLoggingAdapter() { public void log(@NotNull Level level, @NotNull String message, @NotNull Throwable... throwable) {
return logger; getLogger().log(level, message, throwable);
}
@NotNull
@Override
public ResourceReader getResourceReader() {
return resourceReader;
} }
@NotNull @NotNull
@@ -307,6 +290,11 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
return audiences; return audiences;
} }
@Override
public Set<UUID> getLockedPlayers() {
return this.eventListener.getLockedPlayers();
}
@Override @Override
public CompletableFuture<Boolean> reload() { public CompletableFuture<Boolean> reload() {
return CompletableFuture.supplyAsync(() -> { return CompletableFuture.supplyAsync(() -> {
@@ -322,7 +310,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
return true; return true;
} catch (IOException | NullPointerException | InvocationTargetException | IllegalAccessException | } catch (IOException | NullPointerException | InvocationTargetException | IllegalAccessException |
InstantiationException e) { InstantiationException e) {
getLoggingAdapter().log(Level.SEVERE, "Failed to load data from the config", e); log(Level.SEVERE, "Failed to load data from the config", e);
return false; return false;
} }
}); });

View File

@@ -18,13 +18,12 @@ public class BrigadierUtil {
protected static void registerCommodore(@NotNull BukkitHuskSync plugin, @NotNull PluginCommand pluginCommand, protected static void registerCommodore(@NotNull BukkitHuskSync plugin, @NotNull PluginCommand pluginCommand,
@NotNull CommandBase command) { @NotNull CommandBase command) {
// Register command descriptions via commodore (brigadier wrapper) // Register command descriptions via commodore (brigadier wrapper)
try (InputStream pluginFile = plugin.getResourceReader() try (InputStream pluginFile = plugin.getResource("commodore/" + command.command + ".commodore")) {
.getResource("commodore/" + command.command + ".commodore")) {
CommodoreProvider.getCommodore(plugin).register(pluginCommand, CommodoreProvider.getCommodore(plugin).register(pluginCommand,
CommodoreFileReader.INSTANCE.parse(pluginFile), CommodoreFileReader.INSTANCE.parse(pluginFile),
player -> player.hasPermission(command.permission)); player -> player.hasPermission(command.permission));
} catch (IOException e) { } catch (IOException e) {
plugin.getLoggingAdapter().log(Level.SEVERE, plugin.log(Level.SEVERE,
"Failed to load " + command.command + ".commodore command definitions", e); "Failed to load " + command.command + ".commodore command definitions", e);
} }
} }

View File

@@ -0,0 +1,207 @@
package net.william278.husksync.data;
import net.william278.husksync.BukkitHuskSync;
import net.william278.mapdataapi.MapData;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.MapMeta;
import org.bukkit.map.*;
import org.bukkit.persistence.PersistentDataType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.io.IOException;
import java.util.Objects;
import java.util.logging.Level;
/**
* Handles the persistence of {@link MapData} into {@link ItemStack}s.
*/
public class BukkitMapHandler {
private static final BukkitHuskSync plugin = BukkitHuskSync.getInstance();
private static final NamespacedKey MAP_DATA_KEY = new NamespacedKey(plugin, "map_data");
/**
* Get the {@link MapData} from the given {@link ItemStack} and persist it in its' data container
*
* @param itemStack the {@link ItemStack} to get the {@link MapData} from
*/
@SuppressWarnings("ConstantConditions")
public static void persistMapData(@Nullable ItemStack itemStack) {
if (itemStack == null || itemStack.getType() != Material.FILLED_MAP) {
return;
}
final MapMeta mapMeta = (MapMeta) itemStack.getItemMeta();
if (mapMeta == null || !mapMeta.hasMapView()) {
return;
}
// Get the map view from the map
final MapView mapView = mapMeta.getMapView();
if (mapView == null || !mapView.isLocked() || mapView.isVirtual()) {
return;
}
// Get the map data
plugin.debug("Rendering map view onto canvas for locked map");
final LockedMapCanvas canvas = new LockedMapCanvas(mapView);
for (MapRenderer renderer : mapView.getRenderers()) {
renderer.render(mapView, canvas, Bukkit.getServer()
.getOnlinePlayers().stream()
.findAny()
.orElse(null));
}
// Save the extracted rendered map data
plugin.debug("Saving pixel canvas data for locked map");
if (!mapMeta.getPersistentDataContainer().has(MAP_DATA_KEY, PersistentDataType.BYTE_ARRAY)) {
mapMeta.getPersistentDataContainer().set(MAP_DATA_KEY, PersistentDataType.BYTE_ARRAY,
canvas.extractMapData().toBytes());
itemStack.setItemMeta(mapMeta);
}
}
/**
* Set the map data of the given {@link ItemStack} to the given {@link MapData}, applying a map view to the item stack
*
* @param itemStack the {@link ItemStack} to set the map data of
*/
public static void setMapRenderer(@Nullable ItemStack itemStack) {
if (itemStack == null || itemStack.getType() != Material.FILLED_MAP) {
return;
}
final MapMeta mapMeta = (MapMeta) itemStack.getItemMeta();
if (mapMeta == null) {
return;
}
if (!itemStack.getItemMeta().getPersistentDataContainer().has(MAP_DATA_KEY, PersistentDataType.BYTE_ARRAY)) {
return;
}
try {
final byte[] serializedData = itemStack.getItemMeta().getPersistentDataContainer()
.get(MAP_DATA_KEY, PersistentDataType.BYTE_ARRAY);
final MapData mapData = MapData.fromByteArray(Objects.requireNonNull(serializedData));
plugin.debug("Setting deserialized map data for an item stack");
// Create a new map view renderer with the map data color at each pixel
final MapView view = Bukkit.createMap(Bukkit.getWorlds().get(0));
view.getRenderers().clear();
view.addRenderer(new PersistentMapRenderer(mapData));
view.setLocked(true);
view.setScale(MapView.Scale.NORMAL);
view.setTrackingPosition(false);
view.setUnlimitedTracking(false);
mapMeta.setMapView(view);
itemStack.setItemMeta(mapMeta);
plugin.debug("Successfully applied renderer to map item stack");
} catch (IOException | NullPointerException e) {
plugin.getLogger().log(Level.WARNING, "Failed to deserialize map data for a player", e);
}
}
/**
* A {@link MapRenderer} that can be used to render persistently serialized {@link MapData} to a {@link MapView}
*/
public static class PersistentMapRenderer extends MapRenderer {
private final MapData mapData;
private PersistentMapRenderer(@NotNull MapData mapData) {
super(false);
this.mapData = mapData;
}
@Override
public void render(@NotNull MapView map, @NotNull MapCanvas canvas, @NotNull Player player) {
for (int i = 0; i < 128; i++) {
for (int j = 0; j < 128; j++) {
// We set the pixels in this order to avoid the map being rendered upside down
canvas.setPixel(j, i, (byte) mapData.getColorAt(i, j));
}
}
}
}
/**
* A {@link MapCanvas} implementation used for pre-rendering maps to be converted into {@link MapData}
*/
public static class LockedMapCanvas implements MapCanvas {
private final MapView mapView;
private final int[][] pixels = new int[128][128];
private MapCursorCollection cursors;
private LockedMapCanvas(@NotNull MapView mapView) {
this.mapView = mapView;
}
@NotNull
@Override
public MapView getMapView() {
return mapView;
}
@NotNull
@Override
public MapCursorCollection getCursors() {
return cursors == null ? (cursors = new MapCursorCollection()) : cursors;
}
@Override
public void setCursors(@NotNull MapCursorCollection cursors) {
this.cursors = cursors;
}
@Override
public void setPixel(int x, int y, byte color) {
pixels[x][y] = color;
}
@Override
public byte getPixel(int x, int y) {
return (byte) pixels[x][y];
}
@Override
public byte getBasePixel(int x, int y) {
return getPixel(x, y);
}
@Override
public void drawImage(int x, int y, @NotNull Image image) {
// Not implemented
}
@Override
public void drawText(int x, int y, @NotNull MapFont font, @NotNull String text) {
// Not implemented
}
@NotNull
private String getDimension() {
return mapView.getWorld() == null ? "minecraft:overworld"
: switch (mapView.getWorld().getEnvironment()) {
case NETHER -> "minecraft:the_nether";
case THE_END -> "minecraft:the_end";
default -> "minecraft:overworld";
};
}
/**
* Extract the map data from the canvas. Must be rendered first
* @return the extracted map data
*/
@NotNull
private MapData extractMapData() {
return MapData.fromPixels(pixels, getDimension(), (byte) 2);
}
}
}

View File

@@ -0,0 +1,54 @@
package net.william278.husksync.data;
import org.bukkit.NamespacedKey;
import org.bukkit.entity.Player;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.jetbrains.annotations.NotNull;
import java.util.Objects;
import java.util.Optional;
public record BukkitPersistentTypeMapping<T, Z>(PersistentDataTagType type, PersistentDataType<T, Z> bukkitType) {
public static final BukkitPersistentTypeMapping<?, ?>[] PRIMITIVE_TYPE_MAPPINGS = new BukkitPersistentTypeMapping<?, ?>[]{
new BukkitPersistentTypeMapping<>(PersistentDataTagType.BYTE, PersistentDataType.BYTE),
new BukkitPersistentTypeMapping<>(PersistentDataTagType.SHORT, PersistentDataType.SHORT),
new BukkitPersistentTypeMapping<>(PersistentDataTagType.INTEGER, PersistentDataType.INTEGER),
new BukkitPersistentTypeMapping<>(PersistentDataTagType.LONG, PersistentDataType.LONG),
new BukkitPersistentTypeMapping<>(PersistentDataTagType.FLOAT, PersistentDataType.FLOAT),
new BukkitPersistentTypeMapping<>(PersistentDataTagType.DOUBLE, PersistentDataType.DOUBLE),
new BukkitPersistentTypeMapping<>(PersistentDataTagType.STRING, PersistentDataType.STRING),
new BukkitPersistentTypeMapping<>(PersistentDataTagType.BYTE_ARRAY, PersistentDataType.BYTE_ARRAY),
new BukkitPersistentTypeMapping<>(PersistentDataTagType.INTEGER_ARRAY, PersistentDataType.INTEGER_ARRAY),
new BukkitPersistentTypeMapping<>(PersistentDataTagType.LONG_ARRAY, PersistentDataType.LONG_ARRAY),
new BukkitPersistentTypeMapping<>(PersistentDataTagType.TAG_CONTAINER_ARRAY, PersistentDataType.TAG_CONTAINER_ARRAY),
new BukkitPersistentTypeMapping<>(PersistentDataTagType.TAG_CONTAINER, PersistentDataType.TAG_CONTAINER)
};
public BukkitPersistentTypeMapping(@NotNull PersistentDataTagType type, @NotNull PersistentDataType<T, Z> bukkitType) {
this.type = type;
this.bukkitType = bukkitType;
}
@NotNull
public PersistentDataTag<Z> getContainerValue(@NotNull PersistentDataContainer container, @NotNull NamespacedKey key) throws NullPointerException {
return new PersistentDataTag<>(type, Objects.requireNonNull(container.get(key, bukkitType)));
}
public void setContainerValue(@NotNull PersistentDataContainerData container, @NotNull Player player, @NotNull NamespacedKey key) throws NullPointerException {
container.getTagValue(key.toString(), bukkitType.getPrimitiveType())
.ifPresent(value -> player.getPersistentDataContainer().set(key, bukkitType, (Z) value));
}
public static Optional<BukkitPersistentTypeMapping<?, ?>> getMapping(@NotNull PersistentDataTagType type) {
for (BukkitPersistentTypeMapping<?, ?> mapping : PRIMITIVE_TYPE_MAPPINGS) {
if (mapping.type().equals(type)) {
return Optional.of(mapping);
}
}
return Optional.empty();
}
}

View File

@@ -1,6 +1,7 @@
package net.william278.husksync.data; package net.william278.husksync.data;
import net.william278.husksync.BukkitHuskSync; import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.config.Settings;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffect;
import org.bukkit.util.io.BukkitObjectInputStream; import org.bukkit.util.io.BukkitObjectInputStream;
@@ -40,14 +41,18 @@ public class BukkitSerializer {
bukkitOutputStream.writeInt(inventoryContents.length); bukkitOutputStream.writeInt(inventoryContents.length);
// Write each serialize each ItemStack to the output stream // Write each serialize each ItemStack to the output stream
final boolean persistLockedMaps = BukkitHuskSync.getInstance().getSettings().getSynchronizationFeature(Settings.SynchronizationFeature.LOCKED_MAPS);
for (ItemStack inventoryItem : inventoryContents) { for (ItemStack inventoryItem : inventoryContents) {
if (persistLockedMaps) {
BukkitMapHandler.persistMapData(inventoryItem);
}
bukkitOutputStream.writeObject(serializeItemStack(inventoryItem)); bukkitOutputStream.writeObject(serializeItemStack(inventoryItem));
} }
// Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion // Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion
return Base64Coder.encodeLines(byteOutputStream.toByteArray()); return Base64Coder.encodeLines(byteOutputStream.toByteArray());
} catch (IOException e) { } catch (IOException e) {
BukkitHuskSync.getInstance().getLoggingAdapter().log(Level.SEVERE, "Failed to serialize item stack data", e); BukkitHuskSync.getInstance().log(Level.SEVERE, "Failed to serialize item stack data", e);
throw new DataSerializationException("Failed to serialize item stack data", e); throw new DataSerializationException("Failed to serialize item stack data", e);
} }
}); });
@@ -89,8 +94,13 @@ public class BukkitSerializer {
// Set the ItemStacks in the array from deserialized ItemStack data // Set the ItemStacks in the array from deserialized ItemStack data
int slotIndex = 0; int slotIndex = 0;
final boolean persistLockedMaps = BukkitHuskSync.getInstance().getSettings().getSynchronizationFeature(Settings.SynchronizationFeature.LOCKED_MAPS);
for (ItemStack ignored : inventoryContents) { for (ItemStack ignored : inventoryContents) {
inventoryContents[slotIndex] = deserializeItemStack(bukkitInputStream.readObject()); final ItemStack deserialized = deserializeItemStack(bukkitInputStream.readObject());
if (persistLockedMaps) {
BukkitMapHandler.setMapRenderer(deserialized);
}
inventoryContents[slotIndex] = deserialized;
slotIndex++; slotIndex++;
} }
@@ -98,7 +108,7 @@ public class BukkitSerializer {
return inventoryContents; return inventoryContents;
} }
} catch (IOException | ClassNotFoundException e) { } catch (IOException | ClassNotFoundException e) {
BukkitHuskSync.getInstance().getLoggingAdapter().log(Level.SEVERE, "Failed to deserialize item stack data", e); BukkitHuskSync.getInstance().log(Level.SEVERE, "Failed to deserialize item stack data", e);
throw new DataSerializationException("Failed to deserialize item stack data", e); throw new DataSerializationException("Failed to deserialize item stack data", e);
} }
}); });
@@ -155,7 +165,7 @@ public class BukkitSerializer {
// Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion // Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion
return Base64Coder.encodeLines(byteOutputStream.toByteArray()); return Base64Coder.encodeLines(byteOutputStream.toByteArray());
} catch (IOException e) { } catch (IOException e) {
BukkitHuskSync.getInstance().getLoggingAdapter().log(Level.SEVERE, "Failed to serialize potion effect data", e); BukkitHuskSync.getInstance().log(Level.SEVERE, "Failed to serialize potion effect data", e);
throw new DataSerializationException("Failed to serialize potion effect data", e); throw new DataSerializationException("Failed to serialize potion effect data", e);
} }
}); });
@@ -191,7 +201,7 @@ public class BukkitSerializer {
return potionEffects; return potionEffects;
} }
} catch (IOException | ClassNotFoundException e) { } catch (IOException | ClassNotFoundException e) {
BukkitHuskSync.getInstance().getLoggingAdapter().log(Level.SEVERE, "Failed to deserialize potion effect data", e); BukkitHuskSync.getInstance().log(Level.SEVERE, "Failed to deserialize potion effect data", e);
throw new DataSerializationException("Failed to deserialize potion effects", e); throw new DataSerializationException("Failed to deserialize potion effects", e);
} }
}); });

View File

@@ -0,0 +1,37 @@
package net.william278.husksync.listener;
import net.william278.husksync.config.Settings;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.jetbrains.annotations.NotNull;
public interface BukkitDeathEventListener extends Listener {
boolean handleEvent(@NotNull Settings.EventType type, @NotNull Settings.EventPriority priority);
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
default void onPlayerDeathHighest(@NotNull PlayerDeathEvent event) {
if (handleEvent(Settings.EventType.DEATH_LISTENER, Settings.EventPriority.HIGHEST)) {
handlePlayerDeath(event);
}
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
default void onPlayerDeath(@NotNull PlayerDeathEvent event) {
if (handleEvent(Settings.EventType.DEATH_LISTENER, Settings.EventPriority.NORMAL)) {
handlePlayerDeath(event);
}
}
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
default void onPlayerDeathLowest(@NotNull PlayerDeathEvent event) {
if (handleEvent(Settings.EventType.DEATH_LISTENER, Settings.EventPriority.LOWEST)) {
handlePlayerDeath(event);
}
}
void handlePlayerDeath(@NotNull PlayerDeathEvent player);
}

View File

@@ -1,13 +1,16 @@
package net.william278.husksync.listener; package net.william278.husksync.listener;
import net.william278.husksync.BukkitHuskSync; import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.config.Settings;
import net.william278.husksync.data.BukkitInventoryMap; import net.william278.husksync.data.BukkitInventoryMap;
import net.william278.husksync.data.BukkitSerializer; import net.william278.husksync.data.BukkitSerializer;
import net.william278.husksync.data.ItemData; import net.william278.husksync.data.ItemData;
import net.william278.husksync.player.BukkitPlayer; import net.william278.husksync.player.BukkitPlayer;
import net.william278.husksync.player.OnlineUser; import net.william278.husksync.player.OnlineUser;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.entity.Projectile;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
@@ -16,51 +19,57 @@ import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.EntityDamageEvent; import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityPickupItemEvent; import org.bukkit.event.entity.EntityPickupItemEvent;
import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.entity.ProjectileLaunchEvent;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryOpenEvent; import org.bukkit.event.inventory.InventoryOpenEvent;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.event.player.PlayerDropItemEvent; import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.event.player.PlayerInteractEntityEvent;
import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.world.WorldSaveEvent; import org.bukkit.event.world.WorldSaveEvent;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class BukkitEventListener extends EventListener implements Listener { public class BukkitEventListener extends EventListener implements BukkitJoinEventListener, BukkitQuitEventListener,
BukkitDeathEventListener, Listener {
protected final List<String> blacklistedCommands;
public BukkitEventListener(@NotNull BukkitHuskSync huskSync) { public BukkitEventListener(@NotNull BukkitHuskSync huskSync) {
super(huskSync); super(huskSync);
this.blacklistedCommands = huskSync.getSettings().blacklistedCommandsWhileLocked;
Bukkit.getServer().getPluginManager().registerEvents(this, huskSync); Bukkit.getServer().getPluginManager().registerEvents(this, huskSync);
} }
@EventHandler(priority = EventPriority.LOWEST) @Override
public void onPlayerJoin(@NotNull PlayerJoinEvent event) { public boolean handleEvent(@NotNull Settings.EventType type, @NotNull Settings.EventPriority priority) {
super.handlePlayerJoin(BukkitPlayer.adapt(event.getPlayer())); return plugin.getSettings().getEventPriority(type).equals(priority);
} }
@EventHandler(priority = EventPriority.LOWEST) @Override
public void onPlayerQuit(@NotNull PlayerQuitEvent event) { public void handlePlayerQuit(@NotNull BukkitPlayer bukkitPlayer) {
super.handlePlayerQuit(BukkitPlayer.adapt(event.getPlayer())); final Player player = bukkitPlayer.getPlayer();
if (!bukkitPlayer.isLocked() && !player.getItemOnCursor().getType().isAir()) {
player.getWorld().dropItem(player.getLocation(), player.getItemOnCursor());
player.setItemOnCursor(null);
}
super.handlePlayerQuit(bukkitPlayer);
} }
@EventHandler(ignoreCancelled = true) @Override
public void onWorldSave(@NotNull WorldSaveEvent event) { public void handlePlayerJoin(@NotNull BukkitPlayer bukkitPlayer) {
// Handle saving player data snapshots when the world saves super.handlePlayerJoin(bukkitPlayer);
if (!plugin.getSettings().saveOnWorldSave) return;
CompletableFuture.runAsync(() -> super.saveOnWorldSave(event.getWorld().getPlayers()
.stream().map(BukkitPlayer::adapt)
.collect(Collectors.toList())));
} }
@EventHandler(ignoreCancelled = true) @Override
public void onPlayerDeath(PlayerDeathEvent event) { public void handlePlayerDeath(@NotNull PlayerDeathEvent event) {
final OnlineUser user = BukkitPlayer.adapt(event.getEntity()); final OnlineUser user = BukkitPlayer.adapt(event.getEntity());
// If the player is locked or the plugin disabling, clear their drops // If the player is locked or the plugin disabling, clear their drops
if (cancelPlayerEvent(user)) { if (cancelPlayerEvent(user.uuid)) {
event.getDrops().clear(); event.getDrops().clear();
return; return;
} }
@@ -77,50 +86,87 @@ public class BukkitEventListener extends EventListener implements Listener {
.thenAccept(serializedDrops -> super.saveOnPlayerDeath(user, new ItemData(serializedDrops))); .thenAccept(serializedDrops -> super.saveOnPlayerDeath(user, new ItemData(serializedDrops)));
} }
@EventHandler(ignoreCancelled = true)
public void onWorldSave(@NotNull WorldSaveEvent event) {
// Handle saving player data snapshots when the world saves
if (!plugin.getSettings().saveOnWorldSave) return;
CompletableFuture.runAsync(() -> super.saveOnWorldSave(event.getWorld().getPlayers()
.stream().map(BukkitPlayer::adapt)
.collect(Collectors.toList())));
}
/* /*
* Events to cancel if the player has not been set yet * Events to cancel if the player has not been set yet
*/ */
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onProjectileLaunch(@NotNull ProjectileLaunchEvent event) {
final Projectile projectile = event.getEntity();
if (projectile.getShooter() instanceof Player player && projectile.getType() == EntityType.TRIDENT) {
event.setCancelled(cancelPlayerEvent(player.getUniqueId()));
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onDropItem(@NotNull PlayerDropItemEvent event) { public void onDropItem(@NotNull PlayerDropItemEvent event) {
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(event.getPlayer()))); event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPickupItem(@NotNull EntityPickupItemEvent event) { public void onPickupItem(@NotNull EntityPickupItemEvent event) {
if (event.getEntity() instanceof Player player) { if (event.getEntity() instanceof Player player) {
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(player))); event.setCancelled(cancelPlayerEvent(player.getUniqueId()));
} }
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerInteract(@NotNull PlayerInteractEvent event) { public void onPlayerInteract(@NotNull PlayerInteractEvent event) {
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(event.getPlayer()))); event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerInteractEntity(@NotNull PlayerInteractEntityEvent event) {
event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onBlockPlace(@NotNull BlockPlaceEvent event) { public void onBlockPlace(@NotNull BlockPlaceEvent event) {
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(event.getPlayer()))); event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onBlockBreak(@NotNull BlockBreakEvent event) { public void onBlockBreak(@NotNull BlockBreakEvent event) {
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(event.getPlayer()))); event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onInventoryOpen(@NotNull InventoryOpenEvent event) { public void onInventoryOpen(@NotNull InventoryOpenEvent event) {
if (event.getPlayer() instanceof Player player) { if (event.getPlayer() instanceof Player player) {
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(player))); event.setCancelled(cancelPlayerEvent(player.getUniqueId()));
} }
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onInventoryClick(@NotNull InventoryClickEvent event) {
event.setCancelled(cancelPlayerEvent(event.getWhoClicked().getUniqueId()));
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerTakeDamage(@NotNull EntityDamageEvent event) { public void onPlayerTakeDamage(@NotNull EntityDamageEvent event) {
if (event.getEntity() instanceof Player player) { if (event.getEntity() instanceof Player player) {
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(player))); event.setCancelled(cancelPlayerEvent(player.getUniqueId()));
}
}
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
public void onPermissionCommand(@NotNull PlayerCommandPreprocessEvent event) {
String[] commandArgs = event.getMessage().substring(1).split(" ");
String commandLabel = commandArgs[0].toLowerCase();
if (blacklistedCommands.contains(commandLabel)) {
event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
} }
} }

View File

@@ -0,0 +1,38 @@
package net.william278.husksync.listener;
import net.william278.husksync.config.Settings;
import net.william278.husksync.player.BukkitPlayer;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.jetbrains.annotations.NotNull;
public interface BukkitJoinEventListener extends Listener {
boolean handleEvent(@NotNull Settings.EventType type, @NotNull Settings.EventPriority priority);
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
default void onPlayerJoinHighest(@NotNull PlayerJoinEvent event) {
if (handleEvent(Settings.EventType.JOIN_LISTENER, Settings.EventPriority.HIGHEST)) {
handlePlayerJoin(BukkitPlayer.adapt(event.getPlayer()));
}
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
default void onPlayerJoin(@NotNull PlayerJoinEvent event) {
if (handleEvent(Settings.EventType.JOIN_LISTENER, Settings.EventPriority.NORMAL)) {
handlePlayerJoin(BukkitPlayer.adapt(event.getPlayer()));
}
}
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
default void onPlayerJoinLowest(@NotNull PlayerJoinEvent event) {
if (handleEvent(Settings.EventType.JOIN_LISTENER, Settings.EventPriority.LOWEST)) {
handlePlayerJoin(BukkitPlayer.adapt(event.getPlayer()));
}
}
void handlePlayerJoin(@NotNull BukkitPlayer player);
}

View File

@@ -0,0 +1,38 @@
package net.william278.husksync.listener;
import net.william278.husksync.config.Settings;
import net.william278.husksync.player.BukkitPlayer;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
import org.jetbrains.annotations.NotNull;
public interface BukkitQuitEventListener extends Listener {
boolean handleEvent(@NotNull Settings.EventType type, @NotNull Settings.EventPriority priority);
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
default void onPlayerQuitHighest(@NotNull PlayerQuitEvent event) {
if (handleEvent(Settings.EventType.QUIT_LISTENER, Settings.EventPriority.HIGHEST)) {
handlePlayerQuit(BukkitPlayer.adapt(event.getPlayer()));
}
}
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
default void onPlayerQuit(@NotNull PlayerQuitEvent event) {
if (handleEvent(Settings.EventType.QUIT_LISTENER, Settings.EventPriority.NORMAL)) {
handlePlayerQuit(BukkitPlayer.adapt(event.getPlayer()));
}
}
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
default void onPlayerQuitLowest(@NotNull PlayerQuitEvent event) {
if (handleEvent(Settings.EventType.QUIT_LISTENER, Settings.EventPriority.LOWEST)) {
handlePlayerQuit(BukkitPlayer.adapt(event.getPlayer()));
}
}
void handlePlayerQuit(@NotNull BukkitPlayer player);
}

View File

@@ -49,26 +49,26 @@ public class LegacyMigrator extends Migrator {
@Override @Override
public CompletableFuture<Boolean> start() { public CompletableFuture<Boolean> start() {
plugin.getLoggingAdapter().log(Level.INFO, "Starting migration of legacy HuskSync v1.x data..."); plugin.log(Level.INFO, "Starting migration of legacy HuskSync v1.x data...");
final long startTime = System.currentTimeMillis(); final long startTime = System.currentTimeMillis();
return CompletableFuture.supplyAsync(() -> { return CompletableFuture.supplyAsync(() -> {
// Wipe the existing database, preparing it for data import // Wipe the existing database, preparing it for data import
plugin.getLoggingAdapter().log(Level.INFO, "Preparing existing database (wiping)..."); plugin.log(Level.INFO, "Preparing existing database (wiping)...");
plugin.getDatabase().wipeDatabase().join(); plugin.getDatabase().wipeDatabase().join();
plugin.getLoggingAdapter().log(Level.INFO, "Successfully wiped user data database (took " + (System.currentTimeMillis() - startTime) + "ms)"); plugin.log(Level.INFO, "Successfully wiped user data database (took " + (System.currentTimeMillis() - startTime) + "ms)");
// Create jdbc driver connection url // Create jdbc driver connection url
final String jdbcUrl = "jdbc:mysql://" + sourceHost + ":" + sourcePort + "/" + sourceDatabase; final String jdbcUrl = "jdbc:mysql://" + sourceHost + ":" + sourcePort + "/" + sourceDatabase;
// Create a new data source for the mpdb converter // Create a new data source for the mpdb converter
try (final HikariDataSource connectionPool = new HikariDataSource()) { try (final HikariDataSource connectionPool = new HikariDataSource()) {
plugin.getLoggingAdapter().log(Level.INFO, "Establishing connection to legacy database..."); plugin.log(Level.INFO, "Establishing connection to legacy database...");
connectionPool.setJdbcUrl(jdbcUrl); connectionPool.setJdbcUrl(jdbcUrl);
connectionPool.setUsername(sourceUsername); connectionPool.setUsername(sourceUsername);
connectionPool.setPassword(sourcePassword); connectionPool.setPassword(sourcePassword);
connectionPool.setPoolName((getIdentifier() + "_migrator_pool").toUpperCase()); connectionPool.setPoolName((getIdentifier() + "_migrator_pool").toUpperCase());
plugin.getLoggingAdapter().log(Level.INFO, "Downloading raw data from the legacy database (this might take a while)..."); plugin.log(Level.INFO, "Downloading raw data from the legacy database (this might take a while)...");
final List<LegacyData> dataToMigrate = new ArrayList<>(); final List<LegacyData> dataToMigrate = new ArrayList<>();
try (final Connection connection = connectionPool.getConnection()) { try (final Connection connection = connectionPool.getConnection()) {
try (final PreparedStatement statement = connection.prepareStatement(""" try (final PreparedStatement statement = connection.prepareStatement("""
@@ -106,33 +106,33 @@ public class LegacyMigrator extends Migrator {
)); ));
playersMigrated++; playersMigrated++;
if (playersMigrated % 50 == 0) { if (playersMigrated % 50 == 0) {
plugin.getLoggingAdapter().log(Level.INFO, "Downloaded legacy data for " + playersMigrated + " players..."); plugin.log(Level.INFO, "Downloaded legacy data for " + playersMigrated + " players...");
} }
} }
} }
} }
} }
plugin.getLoggingAdapter().log(Level.INFO, "Completed download of " + dataToMigrate.size() + " entries from the legacy database!"); plugin.log(Level.INFO, "Completed download of " + dataToMigrate.size() + " entries from the legacy database!");
plugin.getLoggingAdapter().log(Level.INFO, "Converting HuskSync 1.x data to the new user data format (this might take a while)..."); plugin.log(Level.INFO, "Converting HuskSync 1.x data to the new user data format (this might take a while)...");
final AtomicInteger playersConverted = new AtomicInteger(); final AtomicInteger playersConverted = new AtomicInteger();
dataToMigrate.forEach(data -> data.toUserData(hslConverter, minecraftVersion).thenAccept(convertedData -> { dataToMigrate.forEach(data -> data.toUserData(hslConverter, minecraftVersion).thenAccept(convertedData -> {
plugin.getDatabase().ensureUser(data.user()).thenRun(() -> plugin.getDatabase().ensureUser(data.user()).thenRun(() ->
plugin.getDatabase().setUserData(data.user(), convertedData, DataSaveCause.LEGACY_MIGRATION) plugin.getDatabase().setUserData(data.user(), convertedData, DataSaveCause.LEGACY_MIGRATION)
.exceptionally(exception -> { .exceptionally(exception -> {
plugin.getLoggingAdapter().log(Level.SEVERE, "Failed to migrate legacy data for " + data.user().username + ": " + exception.getMessage()); plugin.log(Level.SEVERE, "Failed to migrate legacy data for " + data.user().username + ": " + exception.getMessage());
return null; return null;
})).join(); })).join();
playersConverted.getAndIncrement(); playersConverted.getAndIncrement();
if (playersConverted.get() % 50 == 0) { if (playersConverted.get() % 50 == 0) {
plugin.getLoggingAdapter().log(Level.INFO, "Converted legacy data for " + playersConverted + " players..."); plugin.log(Level.INFO, "Converted legacy data for " + playersConverted + " players...");
} }
}).join()); }).join());
plugin.getLoggingAdapter().log(Level.INFO, "Migration complete for " + dataToMigrate.size() + " users in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds!"); plugin.log(Level.INFO, "Migration complete for " + dataToMigrate.size() + " users in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds!");
return true; return true;
} catch (Exception e) { } catch (Exception e) {
plugin.getLoggingAdapter().log(Level.SEVERE, "Error while migrating legacy data: " + e.getMessage() + " - are your source database credentials correct?"); plugin.log(Level.SEVERE, "Error while migrating legacy data: " + e.getMessage() + " - are your source database credentials correct?");
return false; return false;
} }
}); });
@@ -176,15 +176,15 @@ public class LegacyMigrator extends Migrator {
} }
default -> false; default -> false;
}) { }) {
plugin.getLoggingAdapter().log(Level.INFO, getHelpMenu()); plugin.log(Level.INFO, getHelpMenu());
plugin.getLoggingAdapter().log(Level.INFO, "Successfully set " + args[0] + " to " + plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
obfuscateDataString(args[1])); obfuscateDataString(args[1]));
} else { } else {
plugin.getLoggingAdapter().log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " + plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
obfuscateDataString(args[1]) + " (is it a valid option?)"); obfuscateDataString(args[1]) + " (is it a valid option?)");
} }
} else { } else {
plugin.getLoggingAdapter().log(Level.INFO, getHelpMenu()); plugin.log(Level.INFO, getHelpMenu());
} }
} }

View File

@@ -56,26 +56,26 @@ public class MpdbMigrator extends Migrator {
@Override @Override
public CompletableFuture<Boolean> start() { public CompletableFuture<Boolean> start() {
plugin.getLoggingAdapter().log(Level.INFO, "Starting migration from MySQLPlayerDataBridge to HuskSync..."); plugin.log(Level.INFO, "Starting migration from MySQLPlayerDataBridge to HuskSync...");
final long startTime = System.currentTimeMillis(); final long startTime = System.currentTimeMillis();
return CompletableFuture.supplyAsync(() -> { return CompletableFuture.supplyAsync(() -> {
// Wipe the existing database, preparing it for data import // Wipe the existing database, preparing it for data import
plugin.getLoggingAdapter().log(Level.INFO, "Preparing existing database (wiping)..."); plugin.log(Level.INFO, "Preparing existing database (wiping)...");
plugin.getDatabase().wipeDatabase().join(); plugin.getDatabase().wipeDatabase().join();
plugin.getLoggingAdapter().log(Level.INFO, "Successfully wiped user data database (took " + (System.currentTimeMillis() - startTime) + "ms)"); plugin.log(Level.INFO, "Successfully wiped user data database (took " + (System.currentTimeMillis() - startTime) + "ms)");
// Create jdbc driver connection url // Create jdbc driver connection url
final String jdbcUrl = "jdbc:mysql://" + sourceHost + ":" + sourcePort + "/" + sourceDatabase; final String jdbcUrl = "jdbc:mysql://" + sourceHost + ":" + sourcePort + "/" + sourceDatabase;
// Create a new data source for the mpdb converter // Create a new data source for the mpdb converter
try (final HikariDataSource connectionPool = new HikariDataSource()) { try (final HikariDataSource connectionPool = new HikariDataSource()) {
plugin.getLoggingAdapter().log(Level.INFO, "Establishing connection to MySQLPlayerDataBridge database..."); plugin.log(Level.INFO, "Establishing connection to MySQLPlayerDataBridge database...");
connectionPool.setJdbcUrl(jdbcUrl); connectionPool.setJdbcUrl(jdbcUrl);
connectionPool.setUsername(sourceUsername); connectionPool.setUsername(sourceUsername);
connectionPool.setPassword(sourcePassword); connectionPool.setPassword(sourcePassword);
connectionPool.setPoolName((getIdentifier() + "_migrator_pool").toUpperCase()); connectionPool.setPoolName((getIdentifier() + "_migrator_pool").toUpperCase());
plugin.getLoggingAdapter().log(Level.INFO, "Downloading raw data from the MySQLPlayerDataBridge database (this might take a while)..."); plugin.log(Level.INFO, "Downloading raw data from the MySQLPlayerDataBridge database (this might take a while)...");
final List<MpdbData> dataToMigrate = new ArrayList<>(); final List<MpdbData> dataToMigrate = new ArrayList<>();
try (final Connection connection = connectionPool.getConnection()) { try (final Connection connection = connectionPool.getConnection()) {
try (final PreparedStatement statement = connection.prepareStatement(""" try (final PreparedStatement statement = connection.prepareStatement("""
@@ -103,32 +103,32 @@ public class MpdbMigrator extends Migrator {
)); ));
playersMigrated++; playersMigrated++;
if (playersMigrated % 25 == 0) { if (playersMigrated % 25 == 0) {
plugin.getLoggingAdapter().log(Level.INFO, "Downloaded MySQLPlayerDataBridge data for " + playersMigrated + " players..."); plugin.log(Level.INFO, "Downloaded MySQLPlayerDataBridge data for " + playersMigrated + " players...");
} }
} }
} }
} }
} }
plugin.getLoggingAdapter().log(Level.INFO, "Completed download of " + dataToMigrate.size() + " entries from the MySQLPlayerDataBridge database!"); plugin.log(Level.INFO, "Completed download of " + dataToMigrate.size() + " entries from the MySQLPlayerDataBridge database!");
plugin.getLoggingAdapter().log(Level.INFO, "Converting raw MySQLPlayerDataBridge data to HuskSync user data (this might take a while)..."); plugin.log(Level.INFO, "Converting raw MySQLPlayerDataBridge data to HuskSync user data (this might take a while)...");
final AtomicInteger playersConverted = new AtomicInteger(); final AtomicInteger playersConverted = new AtomicInteger();
dataToMigrate.forEach(data -> data.toUserData(mpdbConverter, minecraftVersion).thenAccept(convertedData -> { dataToMigrate.forEach(data -> data.toUserData(mpdbConverter, minecraftVersion).thenAccept(convertedData -> {
plugin.getDatabase().ensureUser(data.user()).thenRun(() -> plugin.getDatabase().ensureUser(data.user()).thenRun(() ->
plugin.getDatabase().setUserData(data.user(), convertedData, DataSaveCause.MPDB_MIGRATION)) plugin.getDatabase().setUserData(data.user(), convertedData, DataSaveCause.MPDB_MIGRATION))
.exceptionally(exception -> { .exceptionally(exception -> {
plugin.getLoggingAdapter().log(Level.SEVERE, "Failed to migrate MySQLPlayerDataBridge data for " + data.user().username + ": " + exception.getMessage()); plugin.log(Level.SEVERE, "Failed to migrate MySQLPlayerDataBridge data for " + data.user().username + ": " + exception.getMessage());
return null; return null;
}).join(); }).join();
playersConverted.getAndIncrement(); playersConverted.getAndIncrement();
if (playersConverted.get() % 50 == 0) { if (playersConverted.get() % 50 == 0) {
plugin.getLoggingAdapter().log(Level.INFO, "Converted MySQLPlayerDataBridge data for " + playersConverted + " players..."); plugin.log(Level.INFO, "Converted MySQLPlayerDataBridge data for " + playersConverted + " players...");
} }
}).join()); }).join());
plugin.getLoggingAdapter().log(Level.INFO, "Migration complete for " + dataToMigrate.size() + " users in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds!"); plugin.log(Level.INFO, "Migration complete for " + dataToMigrate.size() + " users in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds!");
return true; return true;
} catch (Exception e) { } catch (Exception e) {
plugin.getLoggingAdapter().log(Level.SEVERE, "Error while migrating data: " + e.getMessage() + " - are your source database credentials correct?"); plugin.log(Level.SEVERE, "Error while migrating data: " + e.getMessage() + " - are your source database credentials correct?");
return false; return false;
} }
}); });
@@ -176,15 +176,15 @@ public class MpdbMigrator extends Migrator {
} }
default -> false; default -> false;
}) { }) {
plugin.getLoggingAdapter().log(Level.INFO, getHelpMenu()); plugin.log(Level.INFO, getHelpMenu());
plugin.getLoggingAdapter().log(Level.INFO, "Successfully set " + args[0] + " to " + plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
obfuscateDataString(args[1])); obfuscateDataString(args[1]));
} else { } else {
plugin.getLoggingAdapter().log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " + plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
obfuscateDataString(args[1]) + " (is it a valid option?)"); obfuscateDataString(args[1]) + " (is it a valid option?)");
} }
} else { } else {
plugin.getLoggingAdapter().log(Level.INFO, getHelpMenu()); plugin.log(Level.INFO, getHelpMenu());
} }
} }

View File

@@ -6,6 +6,8 @@ import dev.triumphteam.gui.builder.gui.StorageBuilder;
import dev.triumphteam.gui.guis.Gui; import dev.triumphteam.gui.guis.Gui;
import dev.triumphteam.gui.guis.StorageGui; import dev.triumphteam.gui.guis.StorageGui;
import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.Audience;
import net.roxeez.advancement.display.FrameType;
import net.william278.andjam.Toast;
import net.william278.desertwell.Version; import net.william278.desertwell.Version;
import net.william278.husksync.BukkitHuskSync; import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.config.Settings; import net.william278.husksync.config.Settings;
@@ -19,7 +21,6 @@ import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerTeleportEvent; import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataContainer; import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType; import org.bukkit.potion.PotionEffectType;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -35,19 +36,6 @@ import java.util.logging.Level;
*/ */
public class BukkitPlayer extends OnlineUser { public class BukkitPlayer extends OnlineUser {
private static final PersistentDataType<?, ?>[] PRIMITIVE_PERSISTENT_DATA_TYPES = new PersistentDataType<?, ?>[]{
PersistentDataType.BYTE,
PersistentDataType.SHORT,
PersistentDataType.INTEGER,
PersistentDataType.LONG,
PersistentDataType.FLOAT,
PersistentDataType.DOUBLE,
PersistentDataType.STRING,
PersistentDataType.BYTE_ARRAY,
PersistentDataType.INTEGER_ARRAY,
PersistentDataType.LONG_ARRAY,
PersistentDataType.TAG_CONTAINER_ARRAY,
PersistentDataType.TAG_CONTAINER};
private final Player player; private final Player player;
private final Audience audience; private final Audience audience;
@@ -58,6 +46,7 @@ public class BukkitPlayer extends OnlineUser {
this.audience = BukkitHuskSync.getInstance().getAudiences().player(player); this.audience = BukkitHuskSync.getInstance().getAudiences().player(player);
} }
@NotNull
public static BukkitPlayer adapt(@NotNull Player player) { public static BukkitPlayer adapt(@NotNull Player player) {
return new BukkitPlayer(player); return new BukkitPlayer(player);
} }
@@ -88,8 +77,8 @@ public class BukkitPlayer extends OnlineUser {
@Override @Override
public CompletableFuture<Void> setStatus(@NotNull StatusData statusData, @NotNull Settings settings) { public CompletableFuture<Void> setStatus(@NotNull StatusData statusData, @NotNull Settings settings) {
return CompletableFuture.runAsync(() -> { return CompletableFuture.runAsync(() -> {
double currentMaxHealth = Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)) // Set max health
.getBaseValue(); double currentMaxHealth = Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)).getBaseValue();
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.MAX_HEALTH)) { if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.MAX_HEALTH)) {
if (statusData.maxHealth != 0d) { if (statusData.maxHealth != 0d) {
Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)) Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH))
@@ -98,22 +87,33 @@ public class BukkitPlayer extends OnlineUser {
} }
} }
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.HEALTH)) { if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.HEALTH)) {
// Set health
final double currentHealth = player.getHealth(); final double currentHealth = player.getHealth();
if (statusData.health != currentHealth) { if (statusData.health != currentHealth) {
final double healthToSet = currentHealth > currentMaxHealth ? currentMaxHealth : statusData.health; final double healthToSet = currentHealth > currentMaxHealth ? currentMaxHealth : statusData.health;
if (healthToSet < 1) { final double maxHealth = currentMaxHealth;
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> player.setHealth(healthToSet)); Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
} else { try {
player.setHealth(healthToSet); player.setHealth(Math.min(healthToSet, maxHealth));
} } catch (IllegalArgumentException e) {
BukkitHuskSync.getInstance().getLogger().log(Level.WARNING,
"Failed to set health of player " + player.getName() + " to " + healthToSet);
}
});
} }
if (statusData.healthScale != 0d) { // Set health scale
player.setHealthScale(statusData.healthScale); try {
} else { if (statusData.healthScale != 0d) {
player.setHealthScale(statusData.maxHealth); player.setHealthScale(statusData.healthScale);
} else {
player.setHealthScale(statusData.maxHealth);
}
player.setHealthScaled(statusData.healthScale != 0D);
} catch (IllegalArgumentException e) {
BukkitHuskSync.getInstance().getLogger().log(Level.WARNING,
"Failed to set health scale of player " + player.getName() + " to " + statusData.healthScale);
} }
player.setHealthScaled(statusData.healthScale != 0D);
} }
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.HUNGER)) { if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.HUNGER)) {
player.setFoodLevel(statusData.hunger); player.setFoodLevel(statusData.hunger);
@@ -155,7 +155,9 @@ public class BukkitPlayer extends OnlineUser {
return BukkitSerializer.deserializeInventory(itemData.serializedItems).thenApplyAsync(contents -> { return BukkitSerializer.deserializeInventory(itemData.serializedItems).thenApplyAsync(contents -> {
final CompletableFuture<Void> inventorySetFuture = new CompletableFuture<>(); final CompletableFuture<Void> inventorySetFuture = new CompletableFuture<>();
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> { Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
player.setItemOnCursor(null);
player.getInventory().setContents(contents.getContents()); player.getInventory().setContents(contents.getContents());
player.updateInventory();
inventorySetFuture.complete(null); inventorySetFuture.complete(null);
}); });
return inventorySetFuture.join(); return inventorySetFuture.join();
@@ -351,32 +353,52 @@ public class BukkitPlayer extends OnlineUser {
@Override @Override
public CompletableFuture<Void> setStatistics(@NotNull StatisticsData statisticsData) { public CompletableFuture<Void> setStatistics(@NotNull StatisticsData statisticsData) {
return CompletableFuture.runAsync(() -> { return CompletableFuture.runAsync(() -> {
// Set untyped statistics // Set generic statistics
for (String statistic : statisticsData.untypedStatistics.keySet()) { for (String statistic : statisticsData.untypedStatistics.keySet()) {
player.setStatistic(Statistic.valueOf(statistic), statisticsData.untypedStatistics.get(statistic)); try {
player.setStatistic(Statistic.valueOf(statistic), statisticsData.untypedStatistics.get(statistic));
} catch (IllegalArgumentException e) {
BukkitHuskSync.getInstance().getLogger().log(Level.WARNING,
"Failed to set generic statistic " + statistic + " for " + username);
}
} }
// Set block statistics // Set block statistics
for (String statistic : statisticsData.blockStatistics.keySet()) { for (String statistic : statisticsData.blockStatistics.keySet()) {
for (String blockMaterial : statisticsData.blockStatistics.get(statistic).keySet()) { for (String blockMaterial : statisticsData.blockStatistics.get(statistic).keySet()) {
player.setStatistic(Statistic.valueOf(statistic), Material.valueOf(blockMaterial), try {
statisticsData.blockStatistics.get(statistic).get(blockMaterial)); player.setStatistic(Statistic.valueOf(statistic), Material.valueOf(blockMaterial),
statisticsData.blockStatistics.get(statistic).get(blockMaterial));
} catch (IllegalArgumentException e) {
BukkitHuskSync.getInstance().getLogger().log(Level.WARNING,
"Failed to set " + blockMaterial + " statistic " + statistic + " for " + username);
}
} }
} }
// Set item statistics // Set item statistics
for (String statistic : statisticsData.itemStatistics.keySet()) { for (String statistic : statisticsData.itemStatistics.keySet()) {
for (String itemMaterial : statisticsData.itemStatistics.get(statistic).keySet()) { for (String itemMaterial : statisticsData.itemStatistics.get(statistic).keySet()) {
player.setStatistic(Statistic.valueOf(statistic), Material.valueOf(itemMaterial), try {
statisticsData.itemStatistics.get(statistic).get(itemMaterial)); player.setStatistic(Statistic.valueOf(statistic), Material.valueOf(itemMaterial),
statisticsData.itemStatistics.get(statistic).get(itemMaterial));
} catch (IllegalArgumentException e) {
BukkitHuskSync.getInstance().getLogger().log(Level.WARNING,
"Failed to set " + itemMaterial + " statistic " + statistic + " for " + username);
}
} }
} }
// Set entity statistics // Set entity statistics
for (String statistic : statisticsData.entityStatistics.keySet()) { for (String statistic : statisticsData.entityStatistics.keySet()) {
for (String entityType : statisticsData.entityStatistics.get(statistic).keySet()) { for (String entityType : statisticsData.entityStatistics.get(statistic).keySet()) {
player.setStatistic(Statistic.valueOf(statistic), EntityType.valueOf(entityType), try {
statisticsData.entityStatistics.get(statistic).get(entityType)); player.setStatistic(Statistic.valueOf(statistic), EntityType.valueOf(entityType),
statisticsData.entityStatistics.get(statistic).get(entityType));
} catch (IllegalArgumentException e) {
BukkitHuskSync.getInstance().getLogger().log(Level.WARNING,
"Failed to set " + entityType + " statistic " + statistic + " for " + username);
}
} }
} }
}); });
@@ -414,65 +436,28 @@ public class BukkitPlayer extends OnlineUser {
@Override @Override
public CompletableFuture<PersistentDataContainerData> getPersistentDataContainer() { public CompletableFuture<PersistentDataContainerData> getPersistentDataContainer() {
return CompletableFuture.supplyAsync(() -> { final CompletableFuture<PersistentDataContainerData> future = new CompletableFuture<>();
final Map<String, PersistentDataTag<?>> persistentDataMap = new HashMap<>();
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
final PersistentDataContainer container = player.getPersistentDataContainer(); final PersistentDataContainer container = player.getPersistentDataContainer();
if (container.isEmpty()) { container.getKeys().forEach(key -> {
return new PersistentDataContainerData(new HashMap<>()); BukkitPersistentTypeMapping<?, ?> type = null;
} for (BukkitPersistentTypeMapping<?, ?> dataType : BukkitPersistentTypeMapping.PRIMITIVE_TYPE_MAPPINGS) {
final HashMap<String, PersistentDataTag<?>> persistentDataMap = new HashMap<>(); if (container.has(key, dataType.bukkitType())) {
for (final NamespacedKey key : container.getKeys()) {
PersistentDataType<?, ?> type = null;
for (PersistentDataType<?, ?> dataType : PRIMITIVE_PERSISTENT_DATA_TYPES) {
if (container.has(key, dataType)) {
type = dataType; type = dataType;
break; break;
} }
} }
if (type != null) { if (type != null) {
// This is absolutely disgusting code and needs to be swiftly put out of its misery with a refactor persistentDataMap.put(key.toString(), type.getContainerValue(container, key));
final Class<?> primitiveType = type.getPrimitiveType();
if (String.class.equals(primitiveType)) {
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.STRING,
Objects.requireNonNull(container.get(key, PersistentDataType.STRING))));
} else if (int.class.equals(primitiveType)) {
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.INTEGER,
Objects.requireNonNull(container.get(key, PersistentDataType.INTEGER))));
} else if (double.class.equals(primitiveType)) {
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.DOUBLE,
Objects.requireNonNull(container.get(key, PersistentDataType.DOUBLE))));
} else if (float.class.equals(primitiveType)) {
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.FLOAT,
Objects.requireNonNull(container.get(key, PersistentDataType.FLOAT))));
} else if (long.class.equals(primitiveType)) {
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.LONG,
Objects.requireNonNull(container.get(key, PersistentDataType.LONG))));
} else if (short.class.equals(primitiveType)) {
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.SHORT,
Objects.requireNonNull(container.get(key, PersistentDataType.SHORT))));
} else if (byte.class.equals(primitiveType)) {
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.BYTE,
Objects.requireNonNull(container.get(key, PersistentDataType.BYTE))));
} else if (byte[].class.equals(primitiveType)) {
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.BYTE_ARRAY,
Objects.requireNonNull(container.get(key, PersistentDataType.BYTE_ARRAY))));
} else if (int[].class.equals(primitiveType)) {
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.INTEGER_ARRAY,
Objects.requireNonNull(container.get(key, PersistentDataType.INTEGER_ARRAY))));
} else if (long[].class.equals(primitiveType)) {
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.LONG_ARRAY,
Objects.requireNonNull(container.get(key, PersistentDataType.LONG_ARRAY))));
} else if (PersistentDataContainer.class.equals(primitiveType)) {
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.TAG_CONTAINER,
Objects.requireNonNull(container.get(key, PersistentDataType.TAG_CONTAINER))));
} else if (PersistentDataContainer[].class.equals(primitiveType)) {
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.TAG_CONTAINER_ARRAY,
Objects.requireNonNull(container.get(key, PersistentDataType.TAG_CONTAINER_ARRAY))));
}
} }
} });
return new PersistentDataContainerData(persistentDataMap); future.complete(new PersistentDataContainerData(persistentDataMap));
}).exceptionally(throwable -> { });
BukkitHuskSync.getInstance().getLoggingAdapter().log(Level.WARNING,
return future.exceptionally(throwable -> {
BukkitHuskSync.getInstance().log(Level.WARNING,
"Could not read " + player.getName() + "'s persistent data map, skipping!"); "Could not read " + player.getName() + "'s persistent data map, skipping!");
throwable.printStackTrace(); throwable.printStackTrace();
return new PersistentDataContainerData(new HashMap<>()); return new PersistentDataContainerData(new HashMap<>());
@@ -480,65 +465,23 @@ public class BukkitPlayer extends OnlineUser {
} }
@Override @Override
public CompletableFuture<Void> setPersistentDataContainer(@NotNull PersistentDataContainerData persistentDataContainerData) { public CompletableFuture<Void> setPersistentDataContainer(@NotNull PersistentDataContainerData container) {
return CompletableFuture.runAsync(() -> { return CompletableFuture.runAsync(() -> {
player.getPersistentDataContainer().getKeys().forEach(namespacedKey -> player.getPersistentDataContainer().getKeys().forEach(namespacedKey ->
player.getPersistentDataContainer().remove(namespacedKey)); player.getPersistentDataContainer().remove(namespacedKey));
persistentDataContainerData.getTags().forEach(keyString -> { container.getTags().forEach(keyString -> {
final NamespacedKey key = NamespacedKey.fromString(keyString); final NamespacedKey key = NamespacedKey.fromString(keyString);
if (key != null) { if (key != null) {
// Set a tag with the given key and value. This is crying out for a refactor. container.getTagType(keyString)
persistentDataContainerData.getTagType(keyString).ifPresentOrElse(dataType -> { .flatMap(BukkitPersistentTypeMapping::getMapping)
switch (dataType) { .ifPresentOrElse(mapping -> mapping.setContainerValue(container, player, key),
case BYTE -> persistentDataContainerData.getTagValue(keyString, byte.class).ifPresent( () -> BukkitHuskSync.getInstance().log(Level.WARNING,
value -> player.getPersistentDataContainer().set(key, "Could not set " + player.getName() + "'s persistent data key " + keyString +
PersistentDataType.BYTE, value)); " as it has an invalid type. Skipping!"));
case SHORT -> persistentDataContainerData.getTagValue(keyString, short.class).ifPresent(
value -> player.getPersistentDataContainer().set(key,
PersistentDataType.SHORT, value));
case INTEGER -> persistentDataContainerData.getTagValue(keyString, int.class).ifPresent(
value -> player.getPersistentDataContainer().set(key,
PersistentDataType.INTEGER, value));
case LONG -> persistentDataContainerData.getTagValue(keyString, long.class).ifPresent(
value -> player.getPersistentDataContainer().set(key,
PersistentDataType.LONG, value));
case FLOAT -> persistentDataContainerData.getTagValue(keyString, float.class).ifPresent(
value -> player.getPersistentDataContainer().set(key,
PersistentDataType.FLOAT, value));
case DOUBLE -> persistentDataContainerData.getTagValue(keyString, double.class).ifPresent(
value -> player.getPersistentDataContainer().set(key,
PersistentDataType.DOUBLE, value));
case STRING -> persistentDataContainerData.getTagValue(keyString, String.class).ifPresent(
value -> player.getPersistentDataContainer().set(key,
PersistentDataType.STRING, value));
case BYTE_ARRAY ->
persistentDataContainerData.getTagValue(keyString, byte[].class).ifPresent(
value -> player.getPersistentDataContainer().set(key,
PersistentDataType.BYTE_ARRAY, value));
case INTEGER_ARRAY ->
persistentDataContainerData.getTagValue(keyString, int[].class).ifPresent(
value -> player.getPersistentDataContainer().set(key,
PersistentDataType.INTEGER_ARRAY, value));
case LONG_ARRAY ->
persistentDataContainerData.getTagValue(keyString, long[].class).ifPresent(
value -> player.getPersistentDataContainer().set(key,
PersistentDataType.LONG_ARRAY, value));
case TAG_CONTAINER ->
persistentDataContainerData.getTagValue(keyString, PersistentDataContainer.class).ifPresent(
value -> player.getPersistentDataContainer().set(key,
PersistentDataType.TAG_CONTAINER, value));
case TAG_CONTAINER_ARRAY ->
persistentDataContainerData.getTagValue(keyString, PersistentDataContainer[].class).ifPresent(
value -> player.getPersistentDataContainer().set(key,
PersistentDataType.TAG_CONTAINER_ARRAY, value));
}
}, () -> BukkitHuskSync.getInstance().getLoggingAdapter().log(Level.WARNING,
"Could not set " + player.getName() + "'s persistent data key " + keyString +
" as it has an invalid type. Skipping!"));
} }
}); });
}).exceptionally(throwable -> { }).exceptionally(throwable -> {
BukkitHuskSync.getInstance().getLoggingAdapter().log(Level.WARNING, BukkitHuskSync.getInstance().log(Level.WARNING,
"Could not write " + player.getName() + "'s persistent data map, skipping!"); "Could not write " + player.getName() + "'s persistent data map, skipping!");
throwable.printStackTrace(); throwable.printStackTrace();
return null; return null;
@@ -618,7 +561,7 @@ public class BukkitPlayer extends OnlineUser {
@Override @Override
public boolean isDead() { public boolean isDead() {
return player.getHealth() < 1; return player.getHealth() <= 0;
} }
@Override @Override
@@ -628,6 +571,23 @@ public class BukkitPlayer extends OnlineUser {
.replace().toComponent()); .replace().toComponent());
} }
@Override
public void sendToast(@NotNull MineDown title, @NotNull MineDown description,
@NotNull String iconMaterial, @NotNull String backgroundType) {
try {
final Material material = Material.matchMaterial(iconMaterial);
Toast.builder(BukkitHuskSync.getInstance())
.setTitle(title.toComponent())
.setDescription(description.toComponent())
.setIcon(material != null ? material : Material.BARRIER)
.setFrameType(FrameType.valueOf(backgroundType))
.build()
.show(player);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override @Override
public void sendMessage(@NotNull MineDown mineDown) { public void sendMessage(@NotNull MineDown mineDown) {
audience.sendMessage(mineDown audience.sendMessage(mineDown
@@ -654,4 +614,14 @@ public class BukkitPlayer extends OnlineUser {
return maxHealth; return maxHealth;
} }
@Override
public boolean isLocked() {
return BukkitHuskSync.getInstance().getLockedPlayers().contains(player.getUniqueId());
}
@Override
public boolean isNpc() {
return player.hasMetadata("NPC");
}
} }

View File

@@ -1,40 +0,0 @@
package net.william278.husksync.util;
import org.jetbrains.annotations.NotNull;
import java.util.logging.Level;
public class BukkitLogger extends Logger {
private final java.util.logging.Logger logger;
public BukkitLogger(@NotNull java.util.logging.Logger logger) {
this.logger = logger;
}
@Override
public void log(@NotNull Level level, @NotNull String message, @NotNull Throwable e) {
logger.log(level, message, e);
}
@Override
public void log(@NotNull Level level, @NotNull String message) {
logger.log(level, message);
}
@Override
public void info(@NotNull String message) {
logger.info(message);
}
@Override
public void severe(@NotNull String message) {
logger.severe(message);
}
@Override
public void config(@NotNull String message) {
logger.config(message);
}
}

View File

@@ -1,22 +0,0 @@
package net.william278.husksync.util;
import net.william278.husksync.BukkitHuskSync;
import org.jetbrains.annotations.NotNull;
import java.io.InputStream;
import java.util.Objects;
public class BukkitResourceReader implements ResourceReader {
private final BukkitHuskSync plugin;
public BukkitResourceReader(BukkitHuskSync plugin) {
this.plugin = plugin;
}
@Override
public @NotNull InputStream getResource(String fileName) {
return Objects.requireNonNull(plugin.getResource(fileName));
}
}

View File

@@ -2,16 +2,16 @@ dependencies {
implementation 'commons-io:commons-io:2.11.0' implementation 'commons-io:commons-io:2.11.0'
implementation 'de.themoep:minedown-adventure:1.7.1-SNAPSHOT' implementation 'de.themoep:minedown-adventure:1.7.1-SNAPSHOT'
implementation 'net.kyori:adventure-api:4.11.0' implementation 'net.kyori:adventure-api:4.11.0'
implementation 'com.google.code.gson:gson:2.9.0' implementation 'com.google.code.gson:gson:2.10.1'
implementation 'dev.dejvokep:boosted-yaml:1.3' implementation 'dev.dejvokep:boosted-yaml:1.3'
implementation 'net.william278:Annotaml:2.0' implementation 'net.william278:Annotaml:2.0.1'
implementation 'net.william278:DesertWell:1.1' implementation 'net.william278:DesertWell:1.1'
implementation 'net.william278:PagineDown:1.1' implementation 'net.william278:PagineDown:1.1'
implementation('com.zaxxer:HikariCP:5.0.1') { implementation('com.zaxxer:HikariCP:5.0.1') {
exclude module: 'slf4j-api' exclude module: 'slf4j-api'
} }
compileOnly 'org.jetbrains:annotations:23.0.0' compileOnly 'org.jetbrains:annotations:24.0.0'
compileOnly 'com.github.plan-player-analytics:Plan:5.4.1690' compileOnly 'com.github.plan-player-analytics:Plan:5.4.1690'
compileOnly 'redis.clients:jedis:' + jedis_version compileOnly 'redis.clients:jedis:' + jedis_version
compileOnly 'org.xerial.snappy:snappy-java:' + snappy_version compileOnly 'org.xerial.snappy:snappy-java:' + snappy_version
@@ -19,8 +19,11 @@ dependencies {
testImplementation 'org.xerial.snappy:snappy-java:1.1.8.4' testImplementation 'org.xerial.snappy:snappy-java:1.1.8.4'
testImplementation 'com.github.plan-player-analytics:Plan:5.4.1690' testImplementation 'com.github.plan-player-analytics:Plan:5.4.1690'
testImplementation 'redis.clients:jedis:' + jedis_version
testImplementation 'org.xerial.snappy:snappy-java:' + snappy_version
testImplementation 'org.apache.commons:commons-text:' + commons_text_version
testCompileOnly 'dev.dejvokep:boosted-yaml:1.3' testCompileOnly 'dev.dejvokep:boosted-yaml:1.3'
testCompileOnly 'org.jetbrains:annotations:23.0.0' testCompileOnly 'org.jetbrains:annotations:24.0.0'
} }
shadowJar { shadowJar {

View File

@@ -9,17 +9,17 @@ import net.william278.husksync.event.EventCannon;
import net.william278.husksync.migrator.Migrator; import net.william278.husksync.migrator.Migrator;
import net.william278.husksync.player.OnlineUser; import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.redis.RedisManager; import net.william278.husksync.redis.RedisManager;
import net.william278.husksync.util.Logger;
import net.william278.husksync.util.ResourceReader;
import net.william278.desertwell.Version; import net.william278.desertwell.Version;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.File; import java.io.File;
import java.io.InputStream;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
/** /**
* Abstract implementation of the HuskSync plugin. * Abstract implementation of the HuskSync plugin.
@@ -103,20 +103,33 @@ public interface HuskSync {
Locales getLocales(); Locales getLocales();
/** /**
* Returns the plugin {@link Logger} * Get a resource as an {@link InputStream} from the plugin jar
* *
* @return the {@link Logger} * @param name the path to the resource
* @return the {@link InputStream} of the resource
*/ */
@NotNull InputStream getResource(@NotNull String name);
Logger getLoggingAdapter();
/** /**
* Returns the plugin resource file reader * Log a message to the console
* *
* @return the {@link ResourceReader} * @param level the level of the message
* @param message the message to log
* @param throwable a throwable to log
*/ */
@NotNull void log(@NotNull Level level, @NotNull String message, @NotNull Throwable... throwable);
ResourceReader getResourceReader();
/**
* Send a debug message to the console, if debug logging is enabled
*
* @param message the message to log
* @param throwable a throwable to log
*/
default void debug(@NotNull String message, @NotNull Throwable... throwable) {
if (getSettings().debugLogging) {
log(Level.INFO, "[DEBUG] " + message, throwable);
}
}
/** /**
* Returns the plugin version * Returns the plugin version
@@ -165,4 +178,6 @@ public interface HuskSync {
*/ */
CompletableFuture<Boolean> reload(); CompletableFuture<Boolean> reload();
Set<UUID> getLockedPlayers();
} }

View File

@@ -72,7 +72,7 @@ public abstract class BaseHuskSyncAPI {
public final CompletableFuture<Optional<UserData>> getUserData(@NotNull User user) { public final CompletableFuture<Optional<UserData>> getUserData(@NotNull User user) {
return CompletableFuture.supplyAsync(() -> { return CompletableFuture.supplyAsync(() -> {
if (user instanceof OnlineUser) { if (user instanceof OnlineUser) {
return ((OnlineUser) user).getUserData(plugin.getLoggingAdapter(), plugin.getSettings()).join(); return ((OnlineUser) user).getUserData(plugin).join();
} else { } else {
return plugin.getDatabase().getCurrentUserData(user).join().map(UserDataSnapshot::userData); return plugin.getDatabase().getCurrentUserData(user).join().map(UserDataSnapshot::userData);
} }
@@ -103,7 +103,7 @@ public abstract class BaseHuskSyncAPI {
* @since 2.0 * @since 2.0
*/ */
public final CompletableFuture<Void> saveUserData(@NotNull OnlineUser user) { public final CompletableFuture<Void> saveUserData(@NotNull OnlineUser user) {
return CompletableFuture.runAsync(() -> user.getUserData(plugin.getLoggingAdapter(), plugin.getSettings()) return CompletableFuture.runAsync(() -> user.getUserData(plugin)
.thenAccept(optionalUserData -> optionalUserData.ifPresent( .thenAccept(optionalUserData -> optionalUserData.ifPresent(
userData -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.API).join()))); userData -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.API).join())));
} }

View File

@@ -46,9 +46,10 @@ public class EnderChestCommand extends CommandBase implements TabCompletable {
"/enderchest <player> [version_uuid]").ifPresent(player::sendMessage); "/enderchest <player> [version_uuid]").ifPresent(player::sendMessage);
} }
} else { } else {
// View latest user data // View (and edit) the latest user data
plugin.getDatabase().getCurrentUserData(user).thenAccept(optionalData -> optionalData.ifPresentOrElse( plugin.getDatabase().getCurrentUserData(user).thenAccept(optionalData -> optionalData.ifPresentOrElse(
versionedUserData -> showEnderChestMenu(player, versionedUserData, user, true), versionedUserData -> showEnderChestMenu(player, versionedUserData, user,
player.hasPermission(Permission.COMMAND_ENDER_CHEST_EDIT.node)),
() -> plugin.getLocales().getLocale("error_no_data_to_display") () -> plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(player::sendMessage))); .ifPresent(player::sendMessage)));
} }
@@ -72,7 +73,7 @@ public class EnderChestCommand extends CommandBase implements TabCompletable {
.getLocale("ender_chest_viewer_menu_title", dataOwner.username) .getLocale("ender_chest_viewer_menu_title", dataOwner.username)
.orElse(new MineDown("Ender Chest Viewer"))) .orElse(new MineDown("Ender Chest Viewer")))
.exceptionally(throwable -> { .exceptionally(throwable -> {
plugin.getLoggingAdapter().log(Level.WARNING, "Exception displaying inventory menu to " + player.username, throwable); plugin.log(Level.WARNING, "Exception displaying inventory menu to " + player.username, throwable);
return Optional.empty(); return Optional.empty();
}) })
.thenAccept(dataOnClose -> { .thenAccept(dataOnClose -> {

View File

@@ -63,10 +63,10 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons
newestVersion.ifPresentOrElse( newestVersion.ifPresentOrElse(
newVersion -> player.sendMessage( newVersion -> player.sendMessage(
new MineDown("[HuskSync](#00fb9a bold) [| A new version of HuskSync is available!" new MineDown("[HuskSync](#00fb9a bold) [| A new version of HuskSync is available!"
+ " (v" + newVersion + " (Running: v" + plugin.getPluginVersion() + ")](#00fb9a)")), + " (v" + newVersion + " (Running: v" + plugin.getPluginVersion() + ")](#00fb9a)")),
() -> player.sendMessage( () -> player.sendMessage(
new MineDown("[HuskSync](#00fb9a bold) [| HuskSync is up-to-date." new MineDown("[HuskSync](#00fb9a bold) [| HuskSync is up-to-date."
+ " (Running: v" + plugin.getPluginVersion() + ")](#00fb9a)")))); + " (Running: v" + plugin.getPluginVersion() + ")](#00fb9a)"))));
} }
case "about", "info" -> sendAboutMenu(player); case "about", "info" -> sendAboutMenu(player);
case "reload" -> { case "reload" -> {
@@ -88,25 +88,25 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons
@Override @Override
public void onConsoleExecute(@NotNull String[] args) { public void onConsoleExecute(@NotNull String[] args) {
if (args.length < 1) { if (args.length < 1) {
plugin.getLoggingAdapter().log(Level.INFO, "Console usage: \"husksync <update/about/reload/migrate>\""); plugin.log(Level.INFO, "Console usage: \"husksync <update/about/reload/migrate>\"");
return; return;
} }
switch (args[0].toLowerCase()) { switch (args[0].toLowerCase()) {
case "update", "version" -> plugin.getLatestVersionIfOutdated().thenAccept(newestVersion -> case "update", "version" -> plugin.getLatestVersionIfOutdated().thenAccept(newestVersion ->
newestVersion.ifPresentOrElse(newVersion -> plugin.getLoggingAdapter().log(Level.WARNING, newestVersion.ifPresentOrElse(newVersion -> plugin.log(Level.WARNING,
"An update is available for HuskSync, v" + newVersion "An update is available for HuskSync, v" + newVersion
+ " (Running v" + plugin.getPluginVersion() + ")"), + " (Running v" + plugin.getPluginVersion() + ")"),
() -> plugin.getLoggingAdapter().log(Level.INFO, () -> plugin.log(Level.INFO,
"HuskSync is up to date" + "HuskSync is up to date" +
" (Running v" + plugin.getPluginVersion() + ")"))); " (Running v" + plugin.getPluginVersion() + ")")));
case "about", "info" -> aboutMenu.toString().lines().forEach(plugin.getLoggingAdapter()::info); case "about", "info" -> aboutMenu.toString().lines().forEach(line -> plugin.log(Level.INFO, line));
case "reload" -> { case "reload" -> {
plugin.reload(); plugin.reload();
plugin.getLoggingAdapter().log(Level.INFO, "Reloaded config & message files."); plugin.log(Level.INFO, "Reloaded config & message files.");
} }
case "migrate" -> { case "migrate" -> {
if (args.length < 2) { if (args.length < 2) {
plugin.getLoggingAdapter().log(Level.INFO, plugin.log(Level.INFO,
"Please choose a migrator, then run \"husksync migrate <migrator>\""); "Please choose a migrator, then run \"husksync migrate <migrator>\"");
logMigratorsList(); logMigratorsList();
return; return;
@@ -115,39 +115,39 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons
availableMigrator.getIdentifier().equalsIgnoreCase(args[1])).findFirst(); availableMigrator.getIdentifier().equalsIgnoreCase(args[1])).findFirst();
selectedMigrator.ifPresentOrElse(migrator -> { selectedMigrator.ifPresentOrElse(migrator -> {
if (args.length < 3) { if (args.length < 3) {
plugin.getLoggingAdapter().log(Level.INFO, migrator.getHelpMenu()); plugin.log(Level.INFO, migrator.getHelpMenu());
return; return;
} }
switch (args[2]) { switch (args[2]) {
case "start" -> migrator.start().thenAccept(succeeded -> { case "start" -> migrator.start().thenAccept(succeeded -> {
if (succeeded) { if (succeeded) {
plugin.getLoggingAdapter().log(Level.INFO, "Migration completed successfully!"); plugin.log(Level.INFO, "Migration completed successfully!");
} else { } else {
plugin.getLoggingAdapter().log(Level.WARNING, "Migration failed!"); plugin.log(Level.WARNING, "Migration failed!");
} }
}); });
case "set" -> migrator.handleConfigurationCommand(Arrays.copyOfRange(args, 3, args.length)); case "set" -> migrator.handleConfigurationCommand(Arrays.copyOfRange(args, 3, args.length));
default -> plugin.getLoggingAdapter().log(Level.INFO, default -> plugin.log(Level.INFO,
"Invalid syntax. Console usage: \"husksync migrate " + args[1] + " <start/set>"); "Invalid syntax. Console usage: \"husksync migrate " + args[1] + " <start/set>");
} }
}, () -> { }, () -> {
plugin.getLoggingAdapter().log(Level.INFO, plugin.log(Level.INFO,
"Please specify a valid migrator.\n" + "Please specify a valid migrator.\n" +
"If a migrator is not available, please verify that you meet the prerequisites to use it."); "If a migrator is not available, please verify that you meet the prerequisites to use it.");
logMigratorsList(); logMigratorsList();
}); });
} }
default -> plugin.getLoggingAdapter().log(Level.INFO, default -> plugin.log(Level.INFO,
"Invalid syntax. Console usage: \"husksync <update/about/reload/migrate>\""); "Invalid syntax. Console usage: \"husksync <update/about/reload/migrate>\"");
} }
} }
private void logMigratorsList() { private void logMigratorsList() {
plugin.getLoggingAdapter().log(Level.INFO, plugin.log(Level.INFO,
"List of available migrators:\nMigrator ID / Migrator Name:\n" + "List of available migrators:\nMigrator ID / Migrator Name:\n" +
plugin.getAvailableMigrators().stream() plugin.getAvailableMigrators().stream()
.map(migrator -> migrator.getIdentifier() + " - " + migrator.getName()) .map(migrator -> migrator.getIdentifier() + " - " + migrator.getName())
.collect(Collectors.joining("\n"))); .collect(Collectors.joining("\n")));
} }
@Override @Override

View File

@@ -46,9 +46,10 @@ public class InventoryCommand extends CommandBase implements TabCompletable {
"/inventory <player> [version_uuid]").ifPresent(player::sendMessage); "/inventory <player> [version_uuid]").ifPresent(player::sendMessage);
} }
} else { } else {
// View latest user data // View (and edit) the latest user data
plugin.getDatabase().getCurrentUserData(user).thenAccept(optionalData -> optionalData.ifPresentOrElse( plugin.getDatabase().getCurrentUserData(user).thenAccept(optionalData -> optionalData.ifPresentOrElse(
versionedUserData -> showInventoryMenu(player, versionedUserData, user, true), versionedUserData -> showInventoryMenu(player, versionedUserData, user,
player.hasPermission(Permission.COMMAND_INVENTORY_EDIT.node)),
() -> plugin.getLocales().getLocale("error_no_data_to_display") () -> plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(player::sendMessage))); .ifPresent(player::sendMessage)));
} }
@@ -72,7 +73,7 @@ public class InventoryCommand extends CommandBase implements TabCompletable {
.getLocale("inventory_viewer_menu_title", dataOwner.username) .getLocale("inventory_viewer_menu_title", dataOwner.username)
.orElse(new MineDown("Inventory Viewer"))) .orElse(new MineDown("Inventory Viewer")))
.exceptionally(throwable -> { .exceptionally(throwable -> {
plugin.getLoggingAdapter().log(Level.WARNING, "Exception displaying inventory menu to " + player.username, throwable); plugin.log(Level.WARNING, "Exception displaying inventory menu to " + player.username, throwable);
return Optional.empty(); return Optional.empty();
}) })
.thenAccept(dataOnClose -> { .thenAccept(dataOnClose -> {

View File

@@ -278,7 +278,7 @@ public class UserDataCommand extends CommandBase implements TabCompletable {
.split("-")[0], user.username, result) .split("-")[0], user.username, result)
.ifPresent(player::sendMessage); .ifPresent(player::sendMessage);
} catch (IOException e) { } catch (IOException e) {
plugin.getLoggingAdapter().log(Level.SEVERE, "Failed to dump user data", e); plugin.log(Level.SEVERE, "Failed to dump user data", e);
} }
}, () -> plugin.getLocales().getLocale("error_invalid_version_uuid") }, () -> plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(player::sendMessage))), .ifPresent(player::sendMessage))),

View File

@@ -121,6 +121,21 @@ public class Locales {
return value.toString(); return value.toString();
} }
/**
* Truncates a String to a specified length, and appends an ellipsis if it is longer than the specified length
*
* @param string The string to truncate
* @param length The maximum length of the string
* @return The truncated string
*/
@NotNull
public static String truncate(@NotNull String string, int length) {
if (string.length() > length) {
return string.substring(0, length) + "";
}
return string;
}
/** /**
* Returns the base list options to use for a paginated chat list * Returns the base list options to use for a paginated chat list
* *

View File

@@ -5,9 +5,10 @@ import net.william278.annotaml.YamlFile;
import net.william278.annotaml.YamlKey; import net.william278.annotaml.YamlKey;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
/** /**
* Plugin settings, read from config.yml * Plugin settings, read from config.yml
@@ -19,8 +20,7 @@ import java.util.Optional;
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┣╸ Information: https://william278.net/project/husksync ┣╸ Information: https://william278.net/project/husksync
┗╸ Documentation: https://william278.net/docs/husksync""", ┗╸ Documentation: https://william278.net/docs/husksync""",
versionField = "config_version", versionNumber = 3)
versionField = "config_version", versionNumber = 2)
public class Settings { public class Settings {
// Top-level settings // Top-level settings
@@ -47,7 +47,7 @@ public class Settings {
@YamlKey("database.credentials.database") @YamlKey("database.credentials.database")
public String mySqlDatabase = "HuskSync"; public String mySqlDatabase = "HuskSync";
@YamlKey("database.mysql.credentials.username") @YamlKey("database.credentials.username")
public String mySqlUsername = "root"; public String mySqlUsername = "root";
@YamlKey("database.credentials.password") @YamlKey("database.credentials.password")
@@ -77,8 +77,7 @@ public class Settings {
@NotNull @NotNull
public String getTableName(@NotNull TableName tableName) { public String getTableName(@NotNull TableName tableName) {
return Optional.ofNullable(tableNames.get(tableName.name().toLowerCase())) return tableNames.getOrDefault(tableName.name().toLowerCase(), tableName.defaultName);
.orElse(tableName.defaultName);
} }
@@ -111,6 +110,9 @@ public class Settings {
@YamlKey("synchronization.compress_data") @YamlKey("synchronization.compress_data")
public boolean compressData = true; public boolean compressData = true;
@YamlKey("synchronization.notification_display_slot")
public NotificationDisplaySlot notificationDisplaySlot = NotificationDisplaySlot.ACTION_BAR;
@YamlKey("synchronization.save_dead_player_inventories") @YamlKey("synchronization.save_dead_player_inventories")
public boolean saveDeadPlayerInventories = true; public boolean saveDeadPlayerInventories = true;
@@ -120,9 +122,24 @@ public class Settings {
@YamlKey("synchronization.features") @YamlKey("synchronization.features")
public Map<String, Boolean> synchronizationFeatures = SynchronizationFeature.getDefaults(); public Map<String, Boolean> synchronizationFeatures = SynchronizationFeature.getDefaults();
@YamlKey("synchronization.blacklisted_commands_while_locked")
public List<String> blacklistedCommandsWhileLocked = new ArrayList<>();
public boolean getSynchronizationFeature(@NotNull SynchronizationFeature feature) { public boolean getSynchronizationFeature(@NotNull SynchronizationFeature feature) {
return Optional.ofNullable(synchronizationFeatures.get(feature.name().toLowerCase())) return synchronizationFeatures.getOrDefault(feature.name().toLowerCase(), feature.enabledByDefault);
.orElse(feature.enabledByDefault); }
@YamlKey("synchronization.event_priorities")
public Map<String, String> synchronizationEventPriorities = EventType.getDefaults();
@NotNull
public EventPriority getEventPriority(@NotNull Settings.EventType eventType) {
try {
return EventPriority.valueOf(synchronizationEventPriorities.get(eventType.name().toLowerCase()));
} catch (IllegalArgumentException e) {
e.printStackTrace();
return EventPriority.NORMAL;
}
} }
@@ -139,11 +156,13 @@ public class Settings {
this.defaultName = defaultName; this.defaultName = defaultName;
} }
@NotNull
private Map.Entry<String, String> toEntry() { private Map.Entry<String, String> toEntry() {
return Map.entry(name().toLowerCase(), defaultName); return Map.entry(name().toLowerCase(), defaultName);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@NotNull
private static Map<String, String> getDefaults() { private static Map<String, String> getDefaults() {
return Map.ofEntries(Arrays.stream(values()) return Map.ofEntries(Arrays.stream(values())
.map(TableName::toEntry) .map(TableName::toEntry)
@@ -151,11 +170,32 @@ public class Settings {
} }
} }
/**
* Determines the slot a system notification should be displayed in
*/
public enum NotificationDisplaySlot {
/**
* Displays the notification in the action bar
*/
ACTION_BAR,
/**
* Displays the notification in the chat
*/
CHAT,
/**
* Displays the notification in an advancement toast
*/
TOAST,
/**
* Does not display the notification
*/
NONE
}
/** /**
* Represents enabled synchronisation features * Represents enabled synchronisation features
*/ */
public enum SynchronizationFeature { public enum SynchronizationFeature {
INVENTORIES(true), INVENTORIES(true),
ENDER_CHESTS(true), ENDER_CHESTS(true),
HEALTH(true), HEALTH(true),
@@ -167,6 +207,7 @@ public class Settings {
GAME_MODE(true), GAME_MODE(true),
STATISTICS(true), STATISTICS(true),
PERSISTENT_DATA_CONTAINER(false), PERSISTENT_DATA_CONTAINER(false),
LOCKED_MAPS(false),
LOCATION(false); LOCATION(false);
private final boolean enabledByDefault; private final boolean enabledByDefault;
@@ -175,11 +216,13 @@ public class Settings {
this.enabledByDefault = enabledByDefault; this.enabledByDefault = enabledByDefault;
} }
@NotNull
private Map.Entry<String, Boolean> toEntry() { private Map.Entry<String, Boolean> toEntry() {
return Map.entry(name().toLowerCase(), enabledByDefault); return Map.entry(name().toLowerCase(), enabledByDefault);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@NotNull
private static Map<String, Boolean> getDefaults() { private static Map<String, Boolean> getDefaults() {
return Map.ofEntries(Arrays.stream(values()) return Map.ofEntries(Arrays.stream(values())
.map(SynchronizationFeature::toEntry) .map(SynchronizationFeature::toEntry)
@@ -187,4 +230,51 @@ public class Settings {
} }
} }
/**
* Represents events that HuskSync listens to, with a configurable priority listener
*/
public enum EventType {
JOIN_LISTENER(EventPriority.LOWEST),
QUIT_LISTENER(EventPriority.LOWEST),
DEATH_LISTENER(EventPriority.NORMAL);
private final EventPriority defaultPriority;
EventType(@NotNull EventPriority defaultPriority) {
this.defaultPriority = defaultPriority;
}
@NotNull
private Map.Entry<String, String> toEntry() {
return Map.entry(name().toLowerCase(), defaultPriority.name());
}
@SuppressWarnings("unchecked")
@NotNull
private static Map<String, String> getDefaults() {
return Map.ofEntries(Arrays.stream(values())
.map(EventType::toEntry)
.toArray(Map.Entry[]::new));
}
}
/**
* Represents priorities for events that HuskSync listens to
*/
public enum EventPriority {
/**
* Listens and processes the event execution last
*/
HIGHEST,
/**
* Listens in between {@link #HIGHEST} and {@link #LOWEST} priority marked
*/
NORMAL,
/**
* Listens and processes the event execution first
*/
LOWEST
}
} }

View File

@@ -1,5 +1,6 @@
package net.william278.husksync.data; package net.william278.husksync.data;
import net.william278.husksync.config.Locales;
import net.william278.husksync.player.OnlineUser; import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.api.BaseHuskSyncAPI; import net.william278.husksync.api.BaseHuskSyncAPI;
import net.william278.husksync.player.User; import net.william278.husksync.player.User;
@@ -100,4 +101,9 @@ public enum DataSaveCause {
return UNKNOWN; return UNKNOWN;
} }
@NotNull
public String getDisplayName() {
return Locales.truncate(name().toLowerCase(), 10);
}
} }

View File

@@ -34,9 +34,9 @@ public class PersistentDataContainerData {
return Optional.empty(); return Optional.empty();
} }
public Optional<BukkitPersistentDataTagType> getTagType(@NotNull final String tagType) { public Optional<PersistentDataTagType> getTagType(@NotNull final String tagType) {
if (persistentDataMap.containsKey(tagType)) { if (persistentDataMap.containsKey(tagType)) {
return BukkitPersistentDataTagType.getDataType(persistentDataMap.get(tagType).type); return PersistentDataTagType.getDataType(persistentDataMap.get(tagType).type);
} }
return Optional.empty(); return Optional.empty();
} }

View File

@@ -19,16 +19,17 @@ public class PersistentDataTag<T> {
*/ */
public T value; public T value;
public PersistentDataTag(@NotNull BukkitPersistentDataTagType type, @NotNull T value) { public PersistentDataTag(@NotNull PersistentDataTagType type, @NotNull T value) {
this.type = type.name(); this.type = type.name();
this.value = value; this.value = value;
} }
@SuppressWarnings("unused")
private PersistentDataTag() { private PersistentDataTag() {
} }
public Optional<BukkitPersistentDataTagType> getType() { public Optional<PersistentDataTagType> getType() {
return BukkitPersistentDataTagType.getDataType(type); return PersistentDataTagType.getDataType(type);
} }
} }

View File

@@ -7,7 +7,7 @@ import java.util.Optional;
/** /**
* Represents the type of a {@link PersistentDataTag} * Represents the type of a {@link PersistentDataTag}
*/ */
public enum BukkitPersistentDataTagType { public enum PersistentDataTagType {
BYTE, BYTE,
SHORT, SHORT,
@@ -23,8 +23,8 @@ public enum BukkitPersistentDataTagType {
TAG_CONTAINER; TAG_CONTAINER;
public static Optional<BukkitPersistentDataTagType> getDataType(@NotNull String typeName) { public static Optional<PersistentDataTagType> getDataType(@NotNull String typeName) {
for (BukkitPersistentDataTagType type : values()) { for (PersistentDataTagType type : values()) {
if (type.name().equalsIgnoreCase(typeName)) { if (type.name().equalsIgnoreCase(typeName)) {
return Optional.of(type); return Optional.of(type);
} }

View File

@@ -11,7 +11,7 @@ import java.util.Map;
public class StatisticsData { public class StatisticsData {
/** /**
* Map of untyped statistic names to their values * Map of generic statistic names to their values
*/ */
@SerializedName("untyped_statistics") @SerializedName("untyped_statistics")
public Map<String, Integer> untypedStatistics; public Map<String, Integer> untypedStatistics;

View File

@@ -1,14 +1,12 @@
package net.william278.husksync.database; package net.william278.husksync.database;
import net.william278.husksync.data.DataAdapter; import net.william278.husksync.HuskSync;
import net.william278.husksync.config.Settings;
import net.william278.husksync.data.DataSaveCause; import net.william278.husksync.data.DataSaveCause;
import net.william278.husksync.data.UserData; import net.william278.husksync.data.UserData;
import net.william278.husksync.data.UserDataSnapshot; import net.william278.husksync.data.UserDataSnapshot;
import net.william278.husksync.event.EventCannon;
import net.william278.husksync.migrator.Migrator; import net.william278.husksync.migrator.Migrator;
import net.william278.husksync.player.User; import net.william278.husksync.player.User;
import net.william278.husksync.util.Logger;
import net.william278.husksync.util.ResourceReader;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.IOException; import java.io.IOException;
@@ -26,78 +24,10 @@ import java.util.concurrent.CompletableFuture;
*/ */
public abstract class Database { public abstract class Database {
/** protected final HuskSync plugin;
* Name of the table that stores player information
*/
protected final String playerTableName;
/** protected Database(@NotNull HuskSync plugin) {
* Name of the table that stores data this.plugin = plugin;
*/
protected final String dataTableName;
/**
* The maximum number of user records to store in the database at once per user
*/
protected final int maxUserDataRecords;
/**
* {@link DataAdapter} implementation used for adapting {@link UserData} to and from JSON
*/
private final DataAdapter dataAdapter;
/**
* Returns the {@link DataAdapter} used to adapt {@link UserData} to and from JSON
*
* @return instance of the {@link DataAdapter} implementation
*/
protected DataAdapter getDataAdapter() {
return dataAdapter;
}
/**
* {@link EventCannon} implementation used for firing events
*/
private final EventCannon eventCannon;
/**
* Returns the {@link EventCannon} used to fire events
*
* @return instance of the {@link EventCannon} implementation
*/
protected EventCannon getEventCannon() {
return eventCannon;
}
/**
* Logger instance used for database error logging
*/
private final Logger logger;
/**
* Returns the {@link Logger} used to log database errors
*
* @return the {@link Logger} instance
*/
protected Logger getLogger() {
return logger;
}
/**
* The {@link ResourceReader} used to read internal resource files by name
*/
private final ResourceReader resourceReader;
protected Database(@NotNull String playerTableName, @NotNull String dataTableName, final int maxUserDataRecords,
@NotNull ResourceReader resourceReader, @NotNull DataAdapter dataAdapter,
@NotNull EventCannon eventCannon, @NotNull Logger logger) {
this.playerTableName = playerTableName;
this.dataTableName = dataTableName;
this.maxUserDataRecords = maxUserDataRecords;
this.resourceReader = resourceReader;
this.dataAdapter = dataAdapter;
this.eventCannon = eventCannon;
this.logger = logger;
} }
/** /**
@@ -109,7 +39,7 @@ public abstract class Database {
*/ */
@SuppressWarnings("SameParameterValue") @SuppressWarnings("SameParameterValue")
protected final String[] getSchemaStatements(@NotNull String schemaFileName) throws IOException { protected final String[] getSchemaStatements(@NotNull String schemaFileName) throws IOException {
return formatStatementTables(new String(Objects.requireNonNull(resourceReader.getResource(schemaFileName)) return formatStatementTables(new String(Objects.requireNonNull(plugin.getResource(schemaFileName))
.readAllBytes(), StandardCharsets.UTF_8)).split(";"); .readAllBytes(), StandardCharsets.UTF_8)).split(";");
} }
@@ -120,8 +50,8 @@ public abstract class Database {
* @return the formatted statement, with table placeholders replaced with the correct names * @return the formatted statement, with table placeholders replaced with the correct names
*/ */
protected final String formatStatementTables(@NotNull String sql) { protected final String formatStatementTables(@NotNull String sql) {
return sql.replaceAll("%users_table%", playerTableName) return sql.replaceAll("%users_table%", plugin.getSettings().getTableName(Settings.TableName.USERS))
.replaceAll("%user_data_table%", dataTableName); .replaceAll("%user_data_table%", plugin.getSettings().getTableName(Settings.TableName.USER_DATA));
} }
/** /**

View File

@@ -1,13 +1,11 @@
package net.william278.husksync.database; package net.william278.husksync.database;
import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.HikariDataSource;
import net.william278.husksync.HuskSync;
import net.william278.husksync.config.Settings; import net.william278.husksync.config.Settings;
import net.william278.husksync.data.*; import net.william278.husksync.data.*;
import net.william278.husksync.event.DataSaveEvent; import net.william278.husksync.event.DataSaveEvent;
import net.william278.husksync.event.EventCannon;
import net.william278.husksync.player.User; import net.william278.husksync.player.User;
import net.william278.husksync.util.Logger;
import net.william278.husksync.util.ResourceReader;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
@@ -51,12 +49,9 @@ public class MySqlDatabase extends Database {
*/ */
private HikariDataSource connectionPool; private HikariDataSource connectionPool;
public MySqlDatabase(@NotNull Settings settings, @NotNull ResourceReader resourceReader, @NotNull Logger logger, public MySqlDatabase(@NotNull HuskSync plugin) {
@NotNull DataAdapter dataAdapter, @NotNull EventCannon eventCannon) { super(plugin);
super(settings.getTableName(Settings.TableName.USERS), final Settings settings = plugin.getSettings();
settings.getTableName(Settings.TableName.USER_DATA),
Math.max(1, Math.min(20, settings.maxUserDataSnapshots)),
resourceReader, dataAdapter, eventCannon, logger);
this.mySqlHost = settings.mySqlHost; this.mySqlHost = settings.mySqlHost;
this.mySqlPort = settings.mySqlPort; this.mySqlPort = settings.mySqlPort;
this.mySqlDatabaseName = settings.mySqlDatabase; this.mySqlDatabaseName = settings.mySqlDatabase;
@@ -111,10 +106,10 @@ public class MySqlDatabase extends Database {
} }
return true; return true;
} catch (SQLException | IOException e) { } catch (SQLException | IOException e) {
getLogger().log(Level.SEVERE, "Failed to perform database setup: " + e.getMessage()); plugin.log(Level.SEVERE, "Failed to perform database setup: " + e.getMessage());
} }
} catch (Exception e) { } catch (Exception e) {
getLogger().log(Level.SEVERE, "An unhandled exception occurred during database setup!", e); plugin.log(Level.SEVERE, "An unhandled exception occurred during database setup!", e);
} }
return false; return false;
} }
@@ -135,9 +130,9 @@ public class MySqlDatabase extends Database {
statement.setString(2, existingUser.uuid.toString()); statement.setString(2, existingUser.uuid.toString());
statement.executeUpdate(); statement.executeUpdate();
} }
getLogger().log(Level.INFO, "Updated " + user.username + "'s name in the database (" + existingUser.username + " -> " + user.username + ")"); plugin.log(Level.INFO, "Updated " + user.username + "'s name in the database (" + existingUser.username + " -> " + user.username + ")");
} catch (SQLException e) { } catch (SQLException e) {
getLogger().log(Level.SEVERE, "Failed to update a user's name on the database", e); plugin.log(Level.SEVERE, "Failed to update a user's name on the database", e);
} }
} }
}, },
@@ -153,7 +148,7 @@ public class MySqlDatabase extends Database {
statement.executeUpdate(); statement.executeUpdate();
} }
} catch (SQLException e) { } catch (SQLException e) {
getLogger().log(Level.SEVERE, "Failed to insert a user into the database", e); plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
} }
})); }));
} }
@@ -176,7 +171,7 @@ public class MySqlDatabase extends Database {
} }
} }
} catch (SQLException e) { } catch (SQLException e) {
getLogger().log(Level.SEVERE, "Failed to fetch a user from uuid from the database", e); plugin.log(Level.SEVERE, "Failed to fetch a user from uuid from the database", e);
} }
return Optional.empty(); return Optional.empty();
}); });
@@ -199,7 +194,7 @@ public class MySqlDatabase extends Database {
} }
} }
} catch (SQLException e) { } catch (SQLException e) {
getLogger().log(Level.SEVERE, "Failed to fetch a user by name from the database", e); plugin.log(Level.SEVERE, "Failed to fetch a user by name from the database", e);
} }
return Optional.empty(); return Optional.empty();
}); });
@@ -226,11 +221,11 @@ public class MySqlDatabase extends Database {
Date.from(resultSet.getTimestamp("timestamp").toInstant()), Date.from(resultSet.getTimestamp("timestamp").toInstant()),
DataSaveCause.getCauseByName(resultSet.getString("save_cause")), DataSaveCause.getCauseByName(resultSet.getString("save_cause")),
resultSet.getBoolean("pinned"), resultSet.getBoolean("pinned"),
getDataAdapter().fromBytes(dataByteArray))); plugin.getDataAdapter().fromBytes(dataByteArray)));
} }
} }
} catch (SQLException | DataAdaptionException e) { } catch (SQLException | DataAdaptionException e) {
getLogger().log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e); plugin.log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
} }
return Optional.empty(); return Optional.empty();
}); });
@@ -257,13 +252,13 @@ public class MySqlDatabase extends Database {
Date.from(resultSet.getTimestamp("timestamp").toInstant()), Date.from(resultSet.getTimestamp("timestamp").toInstant()),
DataSaveCause.getCauseByName(resultSet.getString("save_cause")), DataSaveCause.getCauseByName(resultSet.getString("save_cause")),
resultSet.getBoolean("pinned"), resultSet.getBoolean("pinned"),
getDataAdapter().fromBytes(dataByteArray)); plugin.getDataAdapter().fromBytes(dataByteArray));
retrievedData.add(data); retrievedData.add(data);
} }
return retrievedData; return retrievedData;
} }
} catch (SQLException | DataAdaptionException e) { } catch (SQLException | DataAdaptionException e) {
getLogger().log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e); plugin.log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
} }
return retrievedData; return retrievedData;
}); });
@@ -291,11 +286,11 @@ public class MySqlDatabase extends Database {
Date.from(resultSet.getTimestamp("timestamp").toInstant()), Date.from(resultSet.getTimestamp("timestamp").toInstant()),
DataSaveCause.getCauseByName(resultSet.getString("save_cause")), DataSaveCause.getCauseByName(resultSet.getString("save_cause")),
resultSet.getBoolean("pinned"), resultSet.getBoolean("pinned"),
getDataAdapter().fromBytes(dataByteArray))); plugin.getDataAdapter().fromBytes(dataByteArray)));
} }
} }
} catch (SQLException | DataAdaptionException e) { } catch (SQLException | DataAdaptionException e) {
getLogger().log(Level.SEVERE, "Failed to fetch specific user data by UUID from the database", e); plugin.log(Level.SEVERE, "Failed to fetch specific user data by UUID from the database", e);
} }
return Optional.empty(); return Optional.empty();
}); });
@@ -306,7 +301,7 @@ public class MySqlDatabase extends Database {
return CompletableFuture.runAsync(() -> { return CompletableFuture.runAsync(() -> {
final List<UserDataSnapshot> unpinnedUserData = getUserData(user).join().stream() final List<UserDataSnapshot> unpinnedUserData = getUserData(user).join().stream()
.filter(dataSnapshot -> !dataSnapshot.pinned()).toList(); .filter(dataSnapshot -> !dataSnapshot.pinned()).toList();
if (unpinnedUserData.size() > maxUserDataRecords) { if (unpinnedUserData.size() > plugin.getSettings().maxUserDataSnapshots) {
try (Connection connection = getConnection()) { try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
DELETE FROM `%user_data_table%` DELETE FROM `%user_data_table%`
@@ -314,12 +309,12 @@ public class MySqlDatabase extends Database {
AND `pinned` IS FALSE AND `pinned` IS FALSE
ORDER BY `timestamp` ASC ORDER BY `timestamp` ASC
LIMIT %entry_count%;""".replace("%entry_count%", LIMIT %entry_count%;""".replace("%entry_count%",
Integer.toString(unpinnedUserData.size() - maxUserDataRecords))))) { Integer.toString(unpinnedUserData.size() - plugin.getSettings().maxUserDataSnapshots))))) {
statement.setString(1, user.uuid.toString()); statement.setString(1, user.uuid.toString());
statement.executeUpdate(); statement.executeUpdate();
} }
} catch (SQLException e) { } catch (SQLException e) {
getLogger().log(Level.SEVERE, "Failed to prune user data from the database", e); plugin.log(Level.SEVERE, "Failed to prune user data from the database", e);
} }
} }
}); });
@@ -338,7 +333,7 @@ public class MySqlDatabase extends Database {
return statement.executeUpdate() > 0; return statement.executeUpdate() > 0;
} }
} catch (SQLException e) { } catch (SQLException e) {
getLogger().log(Level.SEVERE, "Failed to delete specific user data from the database", e); plugin.log(Level.SEVERE, "Failed to delete specific user data from the database", e);
} }
return false; return false;
}); });
@@ -348,7 +343,7 @@ public class MySqlDatabase extends Database {
public CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData, public CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData,
@NotNull DataSaveCause saveCause) { @NotNull DataSaveCause saveCause) {
return CompletableFuture.runAsync(() -> { return CompletableFuture.runAsync(() -> {
final DataSaveEvent dataSaveEvent = (DataSaveEvent) getEventCannon().fireDataSaveEvent(user, final DataSaveEvent dataSaveEvent = (DataSaveEvent) plugin.getEventCannon().fireDataSaveEvent(user,
userData, saveCause).join(); userData, saveCause).join();
if (!dataSaveEvent.isCancelled()) { if (!dataSaveEvent.isCancelled()) {
final UserData finalData = dataSaveEvent.getUserData(); final UserData finalData = dataSaveEvent.getUserData();
@@ -360,11 +355,11 @@ public class MySqlDatabase extends Database {
statement.setString(1, user.uuid.toString()); statement.setString(1, user.uuid.toString());
statement.setString(2, saveCause.name()); statement.setString(2, saveCause.name());
statement.setBlob(3, new ByteArrayInputStream( statement.setBlob(3, new ByteArrayInputStream(
getDataAdapter().toBytes(finalData))); plugin.getDataAdapter().toBytes(finalData)));
statement.executeUpdate(); statement.executeUpdate();
} }
} catch (SQLException | DataAdaptionException e) { } catch (SQLException | DataAdaptionException e) {
getLogger().log(Level.SEVERE, "Failed to set user data in the database", e); plugin.log(Level.SEVERE, "Failed to set user data in the database", e);
} }
} }
}).thenRun(() -> rotateUserData(user).join()); }).thenRun(() -> rotateUserData(user).join());
@@ -384,7 +379,7 @@ public class MySqlDatabase extends Database {
statement.executeUpdate(); statement.executeUpdate();
} }
} catch (SQLException e) { } catch (SQLException e) {
getLogger().log(Level.SEVERE, "Failed to pin user data in the database", e); plugin.log(Level.SEVERE, "Failed to pin user data in the database", e);
} }
}); });
} }
@@ -403,7 +398,7 @@ public class MySqlDatabase extends Database {
statement.executeUpdate(); statement.executeUpdate();
} }
} catch (SQLException e) { } catch (SQLException e) {
getLogger().log(Level.SEVERE, "Failed to unpin user data in the database", e); plugin.log(Level.SEVERE, "Failed to unpin user data in the database", e);
} }
}); });
} }
@@ -416,7 +411,7 @@ public class MySqlDatabase extends Database {
statement.executeUpdate(formatStatementTables("DELETE FROM `%user_data_table%`;")); statement.executeUpdate(formatStatementTables("DELETE FROM `%user_data_table%`;"));
} }
} catch (SQLException e) { } catch (SQLException e) {
getLogger().log(Level.SEVERE, "Failed to wipe the database", e); plugin.log(Level.SEVERE, "Failed to wipe the database", e);
} }
}); });
} }

View File

@@ -10,8 +10,8 @@ import com.djrapitops.plan.extension.icon.Family;
import com.djrapitops.plan.extension.icon.Icon; import com.djrapitops.plan.extension.icon.Icon;
import com.djrapitops.plan.extension.table.Table; import com.djrapitops.plan.extension.table.Table;
import com.djrapitops.plan.extension.table.TableColumnFormat; import com.djrapitops.plan.extension.table.TableColumnFormat;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.UserDataSnapshot; import net.william278.husksync.data.UserDataSnapshot;
import net.william278.husksync.database.Database;
import net.william278.husksync.player.User; import net.william278.husksync.player.User;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -43,14 +43,14 @@ import java.util.regex.Pattern;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class PlanDataExtension implements DataExtension { public class PlanDataExtension implements DataExtension {
private Database database; private HuskSync plugin;
private static final String UNKNOWN_STRING = "N/A"; private static final String UNKNOWN_STRING = "N/A";
private static final String PINNED_HTML_STRING = "&#128205;&nbsp;"; private static final String PINNED_HTML_STRING = "&#128205;&nbsp;";
protected PlanDataExtension(@NotNull Database database) { protected PlanDataExtension(@NotNull HuskSync plugin) {
this.database = database; this.plugin = plugin;
} }
protected PlanDataExtension() { protected PlanDataExtension() {
@@ -66,9 +66,9 @@ public class PlanDataExtension implements DataExtension {
private CompletableFuture<Optional<UserDataSnapshot>> getCurrentUserData(@NotNull UUID uuid) { private CompletableFuture<Optional<UserDataSnapshot>> getCurrentUserData(@NotNull UUID uuid) {
return CompletableFuture.supplyAsync(() -> { return CompletableFuture.supplyAsync(() -> {
final Optional<User> optionalUser = database.getUser(uuid).join(); final Optional<User> optionalUser = plugin.getDatabase().getUser(uuid).join();
if (optionalUser.isPresent()) { if (optionalUser.isPresent()) {
return database.getCurrentUserData(optionalUser.get()).join(); return plugin.getDatabase().getCurrentUserData(optionalUser.get()).join();
} }
return Optional.empty(); return Optional.empty();
}); });
@@ -208,8 +208,8 @@ public class PlanDataExtension implements DataExtension {
.columnTwo("ID", new Icon(Family.SOLID, "bolt", Color.NONE)) .columnTwo("ID", new Icon(Family.SOLID, "bolt", Color.NONE))
.columnThree("Cause", new Icon(Family.SOLID, "flag", Color.NONE)) .columnThree("Cause", new Icon(Family.SOLID, "flag", Color.NONE))
.columnFour("Pinned", new Icon(Family.SOLID, "thumbtack", Color.NONE)); .columnFour("Pinned", new Icon(Family.SOLID, "thumbtack", Color.NONE));
database.getUser(playerUUID).join().ifPresent(user -> plugin.getDatabase().getUser(playerUUID).join().ifPresent(user ->
database.getUserData(user).join().forEach(versionedUserData -> dataSnapshotsTable.addRow( plugin.getDatabase().getUserData(user).join().forEach(versionedUserData -> dataSnapshotsTable.addRow(
versionedUserData.versionTimestamp().getTime(), versionedUserData.versionTimestamp().getTime(),
versionedUserData.versionUUID().toString().split("-")[0], versionedUserData.versionUUID().toString().split("-")[0],
versionedUserData.cause().name().toLowerCase().replaceAll("_", " "), versionedUserData.cause().name().toLowerCase().replaceAll("_", " "),

View File

@@ -2,20 +2,17 @@ package net.william278.husksync.hook;
import com.djrapitops.plan.capability.CapabilityService; import com.djrapitops.plan.capability.CapabilityService;
import com.djrapitops.plan.extension.ExtensionService; import com.djrapitops.plan.extension.ExtensionService;
import net.william278.husksync.database.Database; import net.william278.husksync.HuskSync;
import net.william278.husksync.util.Logger;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.logging.Level; import java.util.logging.Level;
public class PlanHook { public class PlanHook {
private final Database database; private final HuskSync plugin;
private final Logger logger;
public PlanHook(@NotNull Database database, @NotNull Logger logger) { public PlanHook(@NotNull HuskSync plugin) {
this.database = database; this.plugin = plugin;
this.logger = logger;
} }
public void hookIntoPlan() { public void hookIntoPlan() {
@@ -33,13 +30,9 @@ public class PlanHook {
private void registerDataExtension() { private void registerDataExtension() {
try { try {
ExtensionService.getInstance().register(new PlanDataExtension(database)); ExtensionService.getInstance().register(new PlanDataExtension(plugin));
} catch (IllegalStateException planIsNotEnabled) { } catch (IllegalStateException | IllegalArgumentException e) {
logger.log(Level.SEVERE, "Plan extension hook failed to register. Plan is not enabled.", planIsNotEnabled); plugin.log(Level.WARNING, "Failed to register Plan data extension: " + e.getMessage(), e);
// Plan is not enabled, handle exception
} catch (IllegalArgumentException dataExtensionImplementationIsInvalid) {
logger.log(Level.SEVERE, "Plan extension hook failed to register. Data hook implementation is invalid.", dataExtensionImplementationIsInvalid);
// The DataExtension implementation has an implementation error, handle exception
} }
} }

View File

@@ -1,5 +1,6 @@
package net.william278.husksync.listener; package net.william278.husksync.listener;
import de.themoep.minedown.adventure.MineDown;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSaveCause; import net.william278.husksync.data.DataSaveCause;
import net.william278.husksync.data.ItemData; import net.william278.husksync.data.ItemData;
@@ -51,13 +52,17 @@ public abstract class EventListener {
* @param user The {@link OnlineUser} to handle * @param user The {@link OnlineUser} to handle
*/ */
protected final void handlePlayerJoin(@NotNull OnlineUser user) { protected final void handlePlayerJoin(@NotNull OnlineUser user) {
if (user.isNpc()) {
return;
}
lockedPlayers.add(user.uuid); lockedPlayers.add(user.uuid);
CompletableFuture.runAsync(() -> { CompletableFuture.runAsync(() -> {
try { try {
// Hold reading data for the network latency threshold, to ensure the source server has set the redis key // Hold reading data for the network latency threshold, to ensure the source server has set the redis key
Thread.sleep(Math.max(0, plugin.getSettings().networkLatencyMilliseconds)); Thread.sleep(Math.max(0, plugin.getSettings().networkLatencyMilliseconds));
} catch (InterruptedException e) { } catch (InterruptedException e) {
plugin.getLoggingAdapter().log(Level.SEVERE, "An exception occurred handling a player join", e); plugin.log(Level.SEVERE, "An exception occurred handling a player join", e);
} finally { } finally {
plugin.getRedisManager().getUserServerSwitch(user).thenAccept(changingServers -> { plugin.getRedisManager().getUserServerSwitch(user).thenAccept(changingServers -> {
if (!changingServers) { if (!changingServers) {
@@ -83,8 +88,7 @@ public abstract class EventListener {
} }
plugin.getRedisManager().getUserData(user).thenAccept(redisUserData -> plugin.getRedisManager().getUserData(user).thenAccept(redisUserData ->
redisUserData.ifPresent(redisData -> { redisUserData.ifPresent(redisData -> {
user.setData(redisData, plugin.getSettings(), plugin.getEventCannon(), user.setData(redisData, plugin)
plugin.getLoggingAdapter(), plugin.getMinecraftVersion())
.thenAccept(succeeded -> handleSynchronisationCompletion(user, succeeded)).join(); .thenAccept(succeeded -> handleSynchronisationCompletion(user, succeeded)).join();
executor.shutdown(); executor.shutdown();
})).join(); })).join();
@@ -106,8 +110,7 @@ public abstract class EventListener {
private CompletableFuture<Boolean> setUserFromDatabase(@NotNull OnlineUser user) { private CompletableFuture<Boolean> setUserFromDatabase(@NotNull OnlineUser user) {
return plugin.getDatabase().getCurrentUserData(user).thenApply(databaseUserData -> { return plugin.getDatabase().getCurrentUserData(user).thenApply(databaseUserData -> {
if (databaseUserData.isPresent()) { if (databaseUserData.isPresent()) {
return user.setData(databaseUserData.get().userData(), plugin.getSettings(), plugin.getEventCannon(), return user.setData(databaseUserData.get().userData(), plugin).join();
plugin.getLoggingAdapter(), plugin.getMinecraftVersion()).join();
} }
return true; return true;
}); });
@@ -121,9 +124,17 @@ public abstract class EventListener {
*/ */
private void handleSynchronisationCompletion(@NotNull OnlineUser user, boolean succeeded) { private void handleSynchronisationCompletion(@NotNull OnlineUser user, boolean succeeded) {
if (succeeded) { if (succeeded) {
plugin.getLocales().getLocale("synchronisation_complete").ifPresent(user::sendActionBar); switch (plugin.getSettings().notificationDisplaySlot) {
lockedPlayers.remove(user.uuid); case CHAT -> plugin.getLocales().getLocale("synchronisation_complete")
.ifPresent(user::sendMessage);
case ACTION_BAR -> plugin.getLocales().getLocale("synchronisation_complete")
.ifPresent(user::sendActionBar);
case TOAST -> plugin.getLocales().getLocale("synchronisation_complete")
.ifPresent(locale -> user.sendToast(locale, new MineDown(""),
"minecraft:bell", "TASK"));
}
plugin.getDatabase().ensureUser(user).join(); plugin.getDatabase().ensureUser(user).join();
lockedPlayers.remove(user.uuid);
plugin.getEventCannon().fireSyncCompleteEvent(user); plugin.getEventCannon().fireSyncCompleteEvent(user);
} else { } else {
plugin.getLocales().getLocale("synchronisation_failed") plugin.getLocales().getLocale("synchronisation_failed")
@@ -143,19 +154,19 @@ public abstract class EventListener {
return; return;
} }
// Don't sync players awaiting synchronization // Don't sync players awaiting synchronization
if (lockedPlayers.contains(user.uuid)) { if (lockedPlayers.contains(user.uuid) || user.isNpc()) {
return; return;
} }
// Handle asynchronous disconnection // Handle asynchronous disconnection
lockedPlayers.add(user.uuid); lockedPlayers.add(user.uuid);
CompletableFuture.runAsync(() -> plugin.getRedisManager().setUserServerSwitch(user) CompletableFuture.runAsync(() -> plugin.getRedisManager().setUserServerSwitch(user)
.thenRun(() -> user.getUserData(plugin.getLoggingAdapter(), plugin.getSettings()).thenAccept( .thenRun(() -> user.getUserData(plugin).thenAccept(
optionalUserData -> optionalUserData.ifPresent(userData -> plugin.getRedisManager() optionalUserData -> optionalUserData.ifPresent(userData -> plugin.getRedisManager()
.setUserData(user, userData).thenRun(() -> plugin.getDatabase() .setUserData(user, userData).thenRun(() -> plugin.getDatabase()
.setUserData(user, userData, DataSaveCause.DISCONNECT))))) .setUserData(user, userData, DataSaveCause.DISCONNECT)))))
.thenRun(() -> lockedPlayers.remove(user.uuid)).exceptionally(throwable -> { .exceptionally(throwable -> {
plugin.getLoggingAdapter().log(Level.SEVERE, plugin.log(Level.SEVERE,
"An exception occurred handling a player disconnection"); "An exception occurred handling a player disconnection");
throwable.printStackTrace(); throwable.printStackTrace();
return null; return null;
@@ -171,8 +182,11 @@ public abstract class EventListener {
if (disabling || !plugin.getSettings().saveOnWorldSave) { if (disabling || !plugin.getSettings().saveOnWorldSave) {
return; return;
} }
usersInWorld.forEach(user -> user.getUserData(plugin.getLoggingAdapter(), plugin.getSettings()).join().ifPresent( usersInWorld.stream()
userData -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.WORLD_SAVE).join())); .filter(user -> !lockedPlayers.contains(user.uuid) && !user.isNpc())
.forEach(user -> user.getUserData(plugin)
.thenAccept(data -> data.ifPresent(userData -> plugin.getDatabase()
.setUserData(user, userData, DataSaveCause.WORLD_SAVE))));
} }
/** /**
@@ -182,11 +196,11 @@ public abstract class EventListener {
* @param drops The items that this user would have dropped * @param drops The items that this user would have dropped
*/ */
protected void saveOnPlayerDeath(@NotNull OnlineUser user, @NotNull ItemData drops) { protected void saveOnPlayerDeath(@NotNull OnlineUser user, @NotNull ItemData drops) {
if (disabling || !plugin.getSettings().saveOnDeath) { if (disabling || !plugin.getSettings().saveOnDeath || lockedPlayers.contains(user.uuid) || user.isNpc()) {
return; return;
} }
user.getUserData(plugin.getLoggingAdapter(), plugin.getSettings()) user.getUserData(plugin)
.thenAccept(data -> data.ifPresent(userData -> { .thenAccept(data -> data.ifPresent(userData -> {
userData.getInventory().orElse(ItemData.empty()).serializedItems = drops.serializedItems; userData.getInventory().orElse(ItemData.empty()).serializedItems = drops.serializedItems;
plugin.getDatabase().setUserData(user, userData, DataSaveCause.DEATH); plugin.getDatabase().setUserData(user, userData, DataSaveCause.DEATH);
@@ -196,11 +210,11 @@ public abstract class EventListener {
/** /**
* Determine whether a player event should be cancelled * Determine whether a player event should be cancelled
* *
* @param user {@link OnlineUser} performing the event * @param userUuid The UUID of the user to check
* @return Whether the event should be cancelled * @return Whether the event should be cancelled
*/ */
protected final boolean cancelPlayerEvent(@NotNull OnlineUser user) { protected final boolean cancelPlayerEvent(@NotNull UUID userUuid) {
return disabling || lockedPlayers.contains(user.uuid); return disabling || lockedPlayers.contains(userUuid);
} }
/** /**
@@ -209,12 +223,23 @@ public abstract class EventListener {
public final void handlePluginDisable() { public final void handlePluginDisable() {
disabling = true; disabling = true;
plugin.getOnlineUsers().stream().filter(user -> !lockedPlayers.contains(user.uuid)).forEach( // Save data for all online users
user -> user.getUserData(plugin.getLoggingAdapter(), plugin.getSettings()).join().ifPresent( plugin.getOnlineUsers().stream()
userData -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.SERVER_SHUTDOWN).join())); .filter(user -> !lockedPlayers.contains(user.uuid) && !user.isNpc())
.forEach(user -> {
lockedPlayers.add(user.uuid);
user.getUserData(plugin).join()
.ifPresent(userData -> plugin.getDatabase()
.setUserData(user, userData, DataSaveCause.SERVER_SHUTDOWN).join());
});
// Close outstanding connections
plugin.getDatabase().close(); plugin.getDatabase().close();
plugin.getRedisManager().close(); plugin.getRedisManager().close();
} }
public final Set<UUID> getLockedPlayers() {
return this.lockedPlayers;
}
} }

View File

@@ -2,11 +2,10 @@ package net.william278.husksync.player;
import de.themoep.minedown.adventure.MineDown; import de.themoep.minedown.adventure.MineDown;
import net.william278.desertwell.Version; import net.william278.desertwell.Version;
import net.william278.husksync.HuskSync;
import net.william278.husksync.config.Settings; import net.william278.husksync.config.Settings;
import net.william278.husksync.data.*; import net.william278.husksync.data.*;
import net.william278.husksync.event.EventCannon;
import net.william278.husksync.event.PreSyncEvent; import net.william278.husksync.event.PreSyncEvent;
import net.william278.husksync.util.Logger;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.ArrayList; import java.util.ArrayList;
@@ -198,6 +197,17 @@ public abstract class OnlineUser extends User {
*/ */
public abstract void sendActionBar(@NotNull MineDown mineDown); public abstract void sendActionBar(@NotNull MineDown mineDown);
/**
* Dispatch a toast message to this player
*
* @param title the title of the toast
* @param description the description of the toast
* @param iconMaterial the namespace-keyed material to use as an icon of the toast
* @param backgroundType the background ("ToastType") of the toast
*/
public abstract void sendToast(@NotNull MineDown title, @NotNull MineDown description,
@NotNull String iconMaterial, @NotNull String backgroundType);
/** /**
* Returns if the player has the permission node * Returns if the player has the permission node
* *
@@ -232,37 +242,32 @@ public abstract class OnlineUser extends User {
* This will only set data that is enabled as per the enabled settings in the config file. * This will only set data that is enabled as per the enabled settings in the config file.
* Data present in the {@link UserData} object, but not enabled to be set in the config, will be ignored. * Data present in the {@link UserData} object, but not enabled to be set in the config, will be ignored.
* *
* @param data The {@link UserData} to set to the player * @param plugin The plugin instance
* @param settings The plugin {@link Settings} to determine which data to set
* @param eventCannon The {@link EventCannon} to fire the synchronisation events
* @param logger The {@link Logger} for debug and error logging
* @param serverMinecraftVersion The server's Minecraft version, for validating the format of the {@link UserData}
* @return a future returning a boolean when complete; if the sync was successful, the future will return {@code true}. * @return a future returning a boolean when complete; if the sync was successful, the future will return {@code true}.
*/ */
public final CompletableFuture<Boolean> setData(@NotNull UserData data, @NotNull Settings settings, public final CompletableFuture<Boolean> setData(@NotNull UserData data, @NotNull HuskSync plugin) {
@NotNull EventCannon eventCannon, @NotNull Logger logger,
@NotNull Version serverMinecraftVersion) {
return CompletableFuture.supplyAsync(() -> { return CompletableFuture.supplyAsync(() -> {
// Prevent synchronising user data from newer versions of Minecraft // Prevent synchronising user data from newer versions of Minecraft
if (Version.fromMinecraftVersionString(data.getMinecraftVersion()).compareTo(serverMinecraftVersion) > 0) { if (Version.fromMinecraftVersionString(data.getMinecraftVersion()).compareTo(plugin.getMinecraftVersion()) > 0) {
logger.log(Level.SEVERE, "Cannot set data for " + username + plugin.log(Level.SEVERE, "Cannot set data for " + username +
" because the Minecraft version of their user data (" + data.getMinecraftVersion() + " because the Minecraft version of their user data (" + data.getMinecraftVersion() +
") is newer than the server's Minecraft version (" + serverMinecraftVersion + ")."); ") is newer than the server's Minecraft version (" + plugin.getMinecraftVersion() + ").");
return false; return false;
} }
// Prevent synchronising user data from newer versions of the plugin // Prevent synchronising user data from newer versions of the plugin
if (data.getFormatVersion() > UserData.CURRENT_FORMAT_VERSION) { if (data.getFormatVersion() > UserData.CURRENT_FORMAT_VERSION) {
logger.log(Level.SEVERE, "Cannot set data for " + username + plugin.log(Level.SEVERE, "Cannot set data for " + username +
" because the format version of their user data (v" + data.getFormatVersion() + " because the format version of their user data (v" + data.getFormatVersion() +
") is newer than the current format version (v" + UserData.CURRENT_FORMAT_VERSION + ")."); ") is newer than the current format version (v" + UserData.CURRENT_FORMAT_VERSION + ").");
return false; return false;
} }
// Fire the PreSyncEvent // Fire the PreSyncEvent
final PreSyncEvent preSyncEvent = (PreSyncEvent) eventCannon.firePreSyncEvent(this, data).join(); final PreSyncEvent preSyncEvent = (PreSyncEvent) plugin.getEventCannon().firePreSyncEvent(this, data).join();
final UserData finalData = preSyncEvent.getUserData(); final UserData finalData = preSyncEvent.getUserData();
final List<CompletableFuture<Void>> dataSetOperations = new ArrayList<>() {{ final List<CompletableFuture<Void>> dataSetOperations = new ArrayList<>() {{
if (!isOffline() && !preSyncEvent.isCancelled()) { if (!isOffline() && !preSyncEvent.isCancelled()) {
final Settings settings = plugin.getSettings();
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.INVENTORIES)) { if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.INVENTORIES)) {
finalData.getInventory().ifPresent(itemData -> add(setInventory(itemData))); finalData.getInventory().ifPresent(itemData -> add(setInventory(itemData)));
} }
@@ -292,7 +297,7 @@ public abstract class OnlineUser extends User {
return CompletableFuture.allOf(dataSetOperations.toArray(new CompletableFuture[0])).thenApply(unused -> true) return CompletableFuture.allOf(dataSetOperations.toArray(new CompletableFuture[0])).thenApply(unused -> true)
.exceptionally(exception -> { .exceptionally(exception -> {
// Handle synchronisation exceptions // Handle synchronisation exceptions
logger.log(Level.SEVERE, "Failed to set data for player " + username + " (" + exception.getMessage() + ")"); plugin.log(Level.SEVERE, "Failed to set data for player " + username + " (" + exception.getMessage() + ")");
exception.printStackTrace(); exception.printStackTrace();
return false; return false;
}).join(); }).join();
@@ -311,52 +316,65 @@ public abstract class OnlineUser extends User {
* <p> * <p>
* If the user data could not be returned due to an exception, the optional will return empty * If the user data could not be returned due to an exception, the optional will return empty
* *
* @param logger The logger to use for handling exceptions * @param plugin The plugin instance
* @return the player's current {@link UserData} in an optional; empty if an exception occurs
*/ */
public final CompletableFuture<Optional<UserData>> getUserData(@NotNull Logger logger, @NotNull Settings settings) { public final CompletableFuture<Optional<UserData>> getUserData(@NotNull HuskSync plugin) {
return CompletableFuture.supplyAsync(() -> { return CompletableFuture.supplyAsync(() -> {
final UserDataBuilder builder = UserData.builder(getMinecraftVersion()); final UserDataBuilder builder = UserData.builder(getMinecraftVersion());
final List<CompletableFuture<Void>> dataGetOperations = new ArrayList<>() {{ final List<CompletableFuture<Void>> dataGetOperations = new ArrayList<>() {{
if (!isOffline()) { if (!isOffline()) {
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.INVENTORIES)) { final Settings settings = plugin.getSettings();
if (isDead() && settings.saveDeadPlayerInventories) { if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.INVENTORIES)) {
add(CompletableFuture.runAsync(() -> builder.setInventory(ItemData.empty()))); if (isDead() && settings.saveDeadPlayerInventories) {
} else { plugin.debug("Player " + username + " is dead, so their inventory will be set to empty.");
add(getInventory().thenAccept(builder::setInventory)); add(CompletableFuture.runAsync(() -> builder.setInventory(ItemData.empty())));
} } else {
} add(getInventory().thenAccept(builder::setInventory));
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.ENDER_CHESTS)) {
add(getEnderChest().thenAccept(builder::setEnderChest));
}
add(getStatus().thenAccept(builder::setStatus));
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.POTION_EFFECTS)) {
add(getPotionEffects().thenAccept(builder::setPotionEffects));
}
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.ADVANCEMENTS)) {
add(getAdvancements().thenAccept(builder::setAdvancements));
}
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.STATISTICS)) {
add(getStatistics().thenAccept(builder::setStatistics));
}
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.LOCATION)) {
add(getLocation().thenAccept(builder::setLocation));
}
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.PERSISTENT_DATA_CONTAINER)) {
add(getPersistentDataContainer().thenAccept(builder::setPersistentDataContainer));
}
} }
}}; }
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.ENDER_CHESTS)) {
add(getEnderChest().thenAccept(builder::setEnderChest));
}
add(getStatus().thenAccept(builder::setStatus));
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.POTION_EFFECTS)) {
add(getPotionEffects().thenAccept(builder::setPotionEffects));
}
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.ADVANCEMENTS)) {
add(getAdvancements().thenAccept(builder::setAdvancements));
}
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.STATISTICS)) {
add(getStatistics().thenAccept(builder::setStatistics));
}
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.LOCATION)) {
add(getLocation().thenAccept(builder::setLocation));
}
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.PERSISTENT_DATA_CONTAINER)) {
add(getPersistentDataContainer().thenAccept(builder::setPersistentDataContainer));
}
}
}};
// Apply operations in parallel, join when complete // Apply operations in parallel, join when complete
CompletableFuture.allOf(dataGetOperations.toArray(new CompletableFuture[0])).join(); CompletableFuture.allOf(dataGetOperations.toArray(new CompletableFuture[0])).join();
return Optional.of(builder.build()); return Optional.of(builder.build());
}) }).exceptionally(exception -> {
.exceptionally(exception -> { plugin.log(Level.SEVERE, "Failed to get user data from online player " + username + " (" + exception.getMessage() + ")");
logger.log(Level.SEVERE, "Failed to get user data from online player " + username + " (" + exception.getMessage() + ")"); exception.printStackTrace();
exception.printStackTrace(); return Optional.empty();
return Optional.empty(); });
});
} }
/**
* Get if the player is locked
*
* @return the player's locked status
*/
public abstract boolean isLocked();
/**
* Get if the player is a NPC
*
* @return if the player is a NPC with metadata
*/
public abstract boolean isNpc();
} }

View File

@@ -1,5 +1,6 @@
package net.william278.husksync.redis; package net.william278.husksync.redis;
import de.themoep.minedown.adventure.MineDown;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import net.william278.husksync.data.UserData; import net.william278.husksync.data.UserData;
import net.william278.husksync.player.User; import net.william278.husksync.player.User;
@@ -18,7 +19,7 @@ import java.util.concurrent.CompletableFuture;
/** /**
* Manages the connection to the Redis server, handling the caching of user data * Manages the connection to the Redis server, handling the caching of user data
*/ */
public class RedisManager { public class RedisManager extends JedisPubSub {
protected static final String KEY_NAMESPACE = "husksync:"; protected static final String KEY_NAMESPACE = "husksync:";
protected static String clusterId = ""; protected static String clusterId = "";
@@ -52,21 +53,19 @@ public class RedisManager {
* *
* @return a future returning void when complete * @return a future returning void when complete
*/ */
public CompletableFuture<Boolean> initialize() { public boolean initialize() {
return CompletableFuture.supplyAsync(() -> { if (redisPassword.isBlank()) {
if (redisPassword.isBlank()) { jedisPool = new JedisPool(jedisPoolConfig, redisHost, redisPort, 0, redisUseSsl);
jedisPool = new JedisPool(jedisPoolConfig, redisHost, redisPort, 0, redisUseSsl); } else {
} else { jedisPool = new JedisPool(jedisPoolConfig, redisHost, redisPort, 0, redisPassword, redisUseSsl);
jedisPool = new JedisPool(jedisPoolConfig, redisHost, redisPort, 0, redisPassword, redisUseSsl); }
} try {
try { jedisPool.getResource().ping();
jedisPool.getResource().ping(); } catch (JedisException e) {
} catch (JedisException e) { return false;
return false; }
} CompletableFuture.runAsync(this::subscribe);
CompletableFuture.runAsync(this::subscribe); return true;
return true;
});
} }
private void subscribe() { private void subscribe() {
@@ -74,33 +73,42 @@ public class RedisManager {
new Jedis(redisHost, redisPort, DefaultJedisClientConfig.builder() new Jedis(redisHost, redisPort, DefaultJedisClientConfig.builder()
.password(redisPassword).timeoutMillis(0).ssl(redisUseSsl).build())) { .password(redisPassword).timeoutMillis(0).ssl(redisUseSsl).build())) {
subscriber.connect(); subscriber.connect();
subscriber.subscribe(new JedisPubSub() { subscriber.subscribe(this, Arrays.stream(RedisMessageType.values())
@Override .map(RedisMessageType::getMessageChannel)
public void onMessage(@NotNull String channel, @NotNull String message) { .toArray(String[]::new));
RedisMessageType.getTypeFromChannel(channel).ifPresent(messageType -> {
if (messageType == RedisMessageType.UPDATE_USER_DATA) {
final RedisMessage redisMessage = RedisMessage.fromJson(message);
plugin.getOnlineUser(redisMessage.targetUserUuid).ifPresent(user -> {
final UserData userData = plugin.getDataAdapter().fromBytes(redisMessage.data);
user.setData(userData, plugin.getSettings(), plugin.getEventCannon(),
plugin.getLoggingAdapter(), plugin.getMinecraftVersion()).thenAccept(succeeded -> {
if (succeeded) {
plugin.getLocales().getLocale("data_update_complete")
.ifPresent(user::sendActionBar);
plugin.getEventCannon().fireSyncCompleteEvent(user);
} else {
plugin.getLocales().getLocale("data_update_failed")
.ifPresent(user::sendMessage);
}
});
});
}
});
}
}, Arrays.stream(RedisMessageType.values()).map(RedisMessageType::getMessageChannel).toArray(String[]::new));
} }
} }
@Override
public void onMessage(@NotNull String channel, @NotNull String message) {
final RedisMessageType messageType = RedisMessageType.getTypeFromChannel(channel).orElse(null);
if (messageType != RedisMessageType.UPDATE_USER_DATA) {
return;
}
final RedisMessage redisMessage = RedisMessage.fromJson(message);
plugin.getOnlineUser(redisMessage.targetUserUuid).ifPresent(user -> {
final UserData userData = plugin.getDataAdapter().fromBytes(redisMessage.data);
user.setData(userData, plugin).thenAccept(succeeded -> {
if (succeeded) {
switch (plugin.getSettings().notificationDisplaySlot) {
case CHAT -> plugin.getLocales().getLocale("data_update_complete")
.ifPresent(user::sendMessage);
case ACTION_BAR -> plugin.getLocales().getLocale("data_update_complete")
.ifPresent(user::sendActionBar);
case TOAST -> plugin.getLocales().getLocale("data_update_complete")
.ifPresent(locale -> user.sendToast(locale, new MineDown(""),
"minecraft:bell", "TASK"));
}
plugin.getEventCannon().fireSyncCompleteEvent(user);
} else {
plugin.getLocales().getLocale("data_update_failed")
.ifPresent(user::sendMessage);
}
});
});
}
protected void sendMessage(@NotNull String channel, @NotNull String message) { protected void sendMessage(@NotNull String channel, @NotNull String message) {
try (Jedis jedis = jedisPool.getResource()) { try (Jedis jedis = jedisPool.getResource()) {
jedis.publish(channel, message); jedis.publish(channel, message);
@@ -131,7 +139,7 @@ public class RedisManager {
plugin.getDataAdapter().toBytes(userData)); plugin.getDataAdapter().toBytes(userData));
// Debug logging // Debug logging
plugin.getLoggingAdapter().debug("[" + user.username + "] Set " + RedisKeyType.DATA_UPDATE.name() plugin.debug("[" + user.username + "] Set " + RedisKeyType.DATA_UPDATE.name()
+ " key to redis at: " + + " key to redis at: " +
new SimpleDateFormat("mm:ss.SSS").format(new Date())); new SimpleDateFormat("mm:ss.SSS").format(new Date()));
} }
@@ -147,7 +155,7 @@ public class RedisManager {
try (Jedis jedis = jedisPool.getResource()) { try (Jedis jedis = jedisPool.getResource()) {
jedis.setex(getKey(RedisKeyType.SERVER_SWITCH, user.uuid), jedis.setex(getKey(RedisKeyType.SERVER_SWITCH, user.uuid),
RedisKeyType.SERVER_SWITCH.timeToLive, new byte[0]); RedisKeyType.SERVER_SWITCH.timeToLive, new byte[0]);
plugin.getLoggingAdapter().debug("[" + user.username + "] Set " + RedisKeyType.SERVER_SWITCH.name() plugin.debug("[" + user.username + "] Set " + RedisKeyType.SERVER_SWITCH.name()
+ " key to redis at: " + + " key to redis at: " +
new SimpleDateFormat("mm:ss.SSS").format(new Date())); new SimpleDateFormat("mm:ss.SSS").format(new Date()));
} catch (Exception e) { } catch (Exception e) {
@@ -168,12 +176,12 @@ public class RedisManager {
final byte[] key = getKey(RedisKeyType.DATA_UPDATE, user.uuid); final byte[] key = getKey(RedisKeyType.DATA_UPDATE, user.uuid);
final byte[] dataByteArray = jedis.get(key); final byte[] dataByteArray = jedis.get(key);
if (dataByteArray == null) { if (dataByteArray == null) {
plugin.getLoggingAdapter().debug("[" + user.username + "] Could not read " + plugin.debug("[" + user.username + "] Could not read " +
RedisKeyType.DATA_UPDATE.name() + " key from redis at: " + RedisKeyType.DATA_UPDATE.name() + " key from redis at: " +
new SimpleDateFormat("mm:ss.SSS").format(new Date())); new SimpleDateFormat("mm:ss.SSS").format(new Date()));
return Optional.empty(); return Optional.empty();
} }
plugin.getLoggingAdapter().debug("[" + user.username + "] Successfully read " plugin.debug("[" + user.username + "] Successfully read "
+ RedisKeyType.DATA_UPDATE.name() + " key from redis at: " + + RedisKeyType.DATA_UPDATE.name() + " key from redis at: " +
new SimpleDateFormat("mm:ss.SSS").format(new Date())); new SimpleDateFormat("mm:ss.SSS").format(new Date()));
@@ -195,12 +203,12 @@ public class RedisManager {
final byte[] key = getKey(RedisKeyType.SERVER_SWITCH, user.uuid); final byte[] key = getKey(RedisKeyType.SERVER_SWITCH, user.uuid);
final byte[] readData = jedis.get(key); final byte[] readData = jedis.get(key);
if (readData == null) { if (readData == null) {
plugin.getLoggingAdapter().debug("[" + user.username + "] Could not read " + plugin.debug("[" + user.username + "] Could not read " +
RedisKeyType.SERVER_SWITCH.name() + " key from redis at: " + RedisKeyType.SERVER_SWITCH.name() + " key from redis at: " +
new SimpleDateFormat("mm:ss.SSS").format(new Date())); new SimpleDateFormat("mm:ss.SSS").format(new Date()));
return false; return false;
} }
plugin.getLoggingAdapter().debug("[" + user.username + "] Successfully read " plugin.debug("[" + user.username + "] Successfully read "
+ RedisKeyType.SERVER_SWITCH.name() + " key from redis at: " + + RedisKeyType.SERVER_SWITCH.name() + " key from redis at: " +
new SimpleDateFormat("mm:ss.SSS").format(new Date())); new SimpleDateFormat("mm:ss.SSS").format(new Date()));

View File

@@ -97,7 +97,7 @@ public class DataDumper {
return "(Failed to upload to logs site, got: " + connection.getResponseCode() + ")"; return "(Failed to upload to logs site, got: " + connection.getResponseCode() + ")";
} }
} catch (Exception e) { } catch (Exception e) {
plugin.getLoggingAdapter().log(Level.SEVERE, "Failed to upload data to logs site", e); plugin.log(Level.SEVERE, "Failed to upload data to logs site", e);
} }
return "(Failed to upload to logs site)"; return "(Failed to upload to logs site)";
} }

View File

@@ -32,7 +32,7 @@ public class DataSnapshotList {
.format(snapshot.versionTimestamp()), .format(snapshot.versionTimestamp()),
snapshot.versionUUID().toString().split("-")[0], snapshot.versionUUID().toString().split("-")[0],
snapshot.versionUUID().toString(), snapshot.versionUUID().toString(),
snapshot.cause().name().toLowerCase().replaceAll("_", " "), snapshot.cause().getDisplayName(),
dataOwner.username, dataOwner.username,
snapshot.pinned() ? "" : " ") snapshot.pinned() ? "" : " ")
.orElse("" + snapshot.versionUUID())).toList(), .orElse("" + snapshot.versionUUID())).toList(),

View File

@@ -1,34 +0,0 @@
package net.william278.husksync.util;
import org.jetbrains.annotations.NotNull;
import java.util.logging.Level;
/**
* An abstract, cross-platform representation of a logger
*/
public abstract class Logger {
private boolean debug;
public abstract void log(@NotNull Level level, @NotNull String message, @NotNull Throwable e);
public abstract void log(@NotNull Level level, @NotNull String message);
public abstract void info(@NotNull String message);
public abstract void severe(@NotNull String message);
public final void debug(@NotNull String message) {
if (debug) {
log(Level.INFO, "[DEBUG] " + message);
}
}
public abstract void config(@NotNull String message);
public final void showDebugLogs(boolean debug) {
this.debug = debug;
}
}

View File

@@ -1,20 +0,0 @@
package net.william278.husksync.util;
import org.jetbrains.annotations.Nullable;
import java.io.InputStream;
/**
* Abstract representation of a reader that reads internal resource files by name
*/
public interface ResourceReader {
/**
* Gets the resource with given filename and reads it as an {@link InputStream}
*
* @param fileName Name of the resource file to read
* @return The resource, read as an {@link InputStream}; or {@code null} if the resource was not found
*/
@Nullable InputStream getResource(String fileName);
}

View File

@@ -0,0 +1,115 @@
package net.william278.husksync;
import net.william278.desertwell.Version;
import net.william278.husksync.config.Locales;
import net.william278.husksync.config.Settings;
import net.william278.husksync.data.DataAdapter;
import net.william278.husksync.database.Database;
import net.william278.husksync.event.EventCannon;
import net.william278.husksync.migrator.Migrator;
import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.redis.RedisManager;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.InputStream;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
public class DummyHuskSync implements HuskSync {
@Override
@NotNull
public Set<OnlineUser> getOnlineUsers() {
return Set.of();
}
@Override
@NotNull
public Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid) {
return Optional.empty();
}
@Override
@NotNull
public Database getDatabase() {
throw new UnsupportedOperationException();
}
@Override
@NotNull
public RedisManager getRedisManager() {
throw new UnsupportedOperationException();
}
@Override
@NotNull
public DataAdapter getDataAdapter() {
throw new UnsupportedOperationException();
}
@Override
@NotNull
public EventCannon getEventCannon() {
throw new UnsupportedOperationException();
}
@Override
@NotNull
public List<Migrator> getAvailableMigrators() {
return List.of();
}
@Override
@NotNull
public Settings getSettings() {
return new Settings();
}
@Override
@NotNull
public Locales getLocales() {
return new Locales();
}
@Override
public InputStream getResource(@NotNull String name) {
throw new UnsupportedOperationException();
}
@Override
public void log(@NotNull Level level, @NotNull String message, @NotNull Throwable... throwable) {
System.out.println(message);
}
@Override
@NotNull
public Version getPluginVersion() {
return Version.fromString("1.0.0");
}
@Override
@NotNull
public File getDataFolder() {
return new File(".");
}
@Override
@NotNull
public Version getMinecraftVersion() {
return Version.fromString("1.16.2");
}
@Override
public CompletableFuture<Boolean> reload() {
return CompletableFuture.supplyAsync(() -> true);
}
@Override
public Set<UUID> getLockedPlayers() {
return Set.of();
}
}

View File

@@ -1,7 +1,6 @@
package net.william278.husksync.data; package net.william278.husksync.data;
import net.william278.husksync.config.Settings; import net.william278.husksync.DummyHuskSync;
import net.william278.husksync.logger.DummyLogger;
import net.william278.husksync.player.DummyPlayer; import net.william278.husksync.player.DummyPlayer;
import net.william278.husksync.player.OnlineUser; import net.william278.husksync.player.OnlineUser;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
@@ -19,7 +18,7 @@ public class DataAdaptionTests {
@Test @Test
public void testJsonDataAdapter() { public void testJsonDataAdapter() {
final OnlineUser dummyUser = DummyPlayer.create(); final OnlineUser dummyUser = DummyPlayer.create();
dummyUser.getUserData(new DummyLogger(), new Settings()).join().ifPresent(dummyUserData -> { dummyUser.getUserData(new DummyHuskSync()).join().ifPresent(dummyUserData -> {
final DataAdapter dataAdapter = new JsonDataAdapter(); final DataAdapter dataAdapter = new JsonDataAdapter();
final byte[] data = dataAdapter.toBytes(dummyUserData); final byte[] data = dataAdapter.toBytes(dummyUserData);
final UserData deserializedUserData = dataAdapter.fromBytes(data); final UserData deserializedUserData = dataAdapter.fromBytes(data);
@@ -37,7 +36,7 @@ public class DataAdaptionTests {
final OnlineUser dummyUser = DummyPlayer.create(); final OnlineUser dummyUser = DummyPlayer.create();
final String expectedJson = "{\"status\":{\"health\":20.0,\"max_health\":20.0,\"health_scale\":0.0,\"hunger\":20,\"saturation\":5.0,\"saturation_exhaustion\":5.0,\"selected_item_slot\":1,\"total_experience\":100,\"experience_level\":1,\"experience_progress\":1.0,\"game_mode\":\"SURVIVAL\",\"is_flying\":false},\"inventory\":{\"serialized_items\":\"\"},\"ender_chest\":{\"serialized_items\":\"\"},\"potion_effects\":{\"serialized_potion_effects\":\"\"},\"advancements\":[],\"statistics\":{\"untyped_statistics\":{},\"block_statistics\":{},\"item_statistics\":{},\"entity_statistics\":{}},\"minecraft_version\":\"1.19\",\"format_version\":3}"; final String expectedJson = "{\"status\":{\"health\":20.0,\"max_health\":20.0,\"health_scale\":0.0,\"hunger\":20,\"saturation\":5.0,\"saturation_exhaustion\":5.0,\"selected_item_slot\":1,\"total_experience\":100,\"experience_level\":1,\"experience_progress\":1.0,\"game_mode\":\"SURVIVAL\",\"is_flying\":false},\"inventory\":{\"serialized_items\":\"\"},\"ender_chest\":{\"serialized_items\":\"\"},\"potion_effects\":{\"serialized_potion_effects\":\"\"},\"advancements\":[],\"statistics\":{\"untyped_statistics\":{},\"block_statistics\":{},\"item_statistics\":{},\"entity_statistics\":{}},\"minecraft_version\":\"1.19\",\"format_version\":3}";
AtomicReference<String> json = new AtomicReference<>(); AtomicReference<String> json = new AtomicReference<>();
dummyUser.getUserData(new DummyLogger(), new Settings()).join().ifPresent(dummyUserData -> { dummyUser.getUserData(new DummyHuskSync()).join().ifPresent(dummyUserData -> {
final DataAdapter dataAdapter = new JsonDataAdapter(); final DataAdapter dataAdapter = new JsonDataAdapter();
final byte[] data = dataAdapter.toBytes(dummyUserData); final byte[] data = dataAdapter.toBytes(dummyUserData);
json.set(new String(data, StandardCharsets.UTF_8)); json.set(new String(data, StandardCharsets.UTF_8));
@@ -48,7 +47,7 @@ public class DataAdaptionTests {
@Test @Test
public void testCompressedDataAdapter() { public void testCompressedDataAdapter() {
final OnlineUser dummyUser = DummyPlayer.create(); final OnlineUser dummyUser = DummyPlayer.create();
dummyUser.getUserData(new DummyLogger(), new Settings()).join().ifPresent(dummyUserData -> { dummyUser.getUserData(new DummyHuskSync()).join().ifPresent(dummyUserData -> {
final DataAdapter dataAdapter = new CompressedDataAdapter(); final DataAdapter dataAdapter = new CompressedDataAdapter();
final byte[] data = dataAdapter.toBytes(dummyUserData); final byte[] data = dataAdapter.toBytes(dummyUserData);
final UserData deserializedUserData = dataAdapter.fromBytes(data); final UserData deserializedUserData = dataAdapter.fromBytes(data);
@@ -63,13 +62,13 @@ public class DataAdaptionTests {
private String getTestSerializedPersistentDataContainer() { private String getTestSerializedPersistentDataContainer() {
final HashMap<String, PersistentDataTag<?>> persistentDataTest = new HashMap<>(); final HashMap<String, PersistentDataTag<?>> persistentDataTest = new HashMap<>();
persistentDataTest.put("husksync:byte_test", new PersistentDataTag<>(BukkitPersistentDataTagType.BYTE, 0x01)); persistentDataTest.put("husksync:byte_test", new PersistentDataTag<>(PersistentDataTagType.BYTE, 0x01));
persistentDataTest.put("husksync:double_test", new PersistentDataTag<>(BukkitPersistentDataTagType.DOUBLE, 2d)); persistentDataTest.put("husksync:double_test", new PersistentDataTag<>(PersistentDataTagType.DOUBLE, 2d));
persistentDataTest.put("husksync:string_test", new PersistentDataTag<>(BukkitPersistentDataTagType.STRING, "test")); persistentDataTest.put("husksync:string_test", new PersistentDataTag<>(PersistentDataTagType.STRING, "test"));
persistentDataTest.put("husksync:int_test", new PersistentDataTag<>(BukkitPersistentDataTagType.INTEGER, 3)); persistentDataTest.put("husksync:int_test", new PersistentDataTag<>(PersistentDataTagType.INTEGER, 3));
persistentDataTest.put("husksync:long_test", new PersistentDataTag<>(BukkitPersistentDataTagType.LONG, 4L)); persistentDataTest.put("husksync:long_test", new PersistentDataTag<>(PersistentDataTagType.LONG, 4L));
persistentDataTest.put("husksync:float_test", new PersistentDataTag<>(BukkitPersistentDataTagType.FLOAT, 5f)); persistentDataTest.put("husksync:float_test", new PersistentDataTag<>(PersistentDataTagType.FLOAT, 5f));
persistentDataTest.put("husksync:short_test", new PersistentDataTag<>(BukkitPersistentDataTagType.SHORT, 6)); persistentDataTest.put("husksync:short_test", new PersistentDataTag<>(PersistentDataTagType.SHORT, 6));
final PersistentDataContainerData persistentDataContainerData = new PersistentDataContainerData(persistentDataTest); final PersistentDataContainerData persistentDataContainerData = new PersistentDataContainerData(persistentDataTest);
final DataAdapter dataAdapter = new JsonDataAdapter(); final DataAdapter dataAdapter = new JsonDataAdapter();

View File

@@ -1,38 +0,0 @@
package net.william278.husksync.logger;
import net.william278.husksync.util.Logger;
import org.jetbrains.annotations.NotNull;
import java.util.logging.Level;
public class DummyLogger extends Logger {
public DummyLogger() {
}
@Override
public void log(@NotNull Level level, @NotNull String message, @NotNull Throwable e) {
System.out.println(level.getName() + ": " + message);
e.printStackTrace();
}
@Override
public void log(@NotNull Level level, @NotNull String message) {
System.out.println(level.getName() + ": " + message);
}
@Override
public void info(@NotNull String message) {
System.out.println(Level.INFO.getName() + ": " + message);
}
@Override
public void severe(@NotNull String message) {
System.out.println(Level.SEVERE.getName() + ": " + message);
}
@Override
public void config(@NotNull String message) {
System.out.println(Level.CONFIG.getName() + ": " + message);
}
}

View File

@@ -142,6 +142,12 @@ public class DummyPlayer extends OnlineUser {
// do nothing // do nothing
} }
@Override
public void sendToast(@NotNull MineDown title, @NotNull MineDown description,
@NotNull String iconMaterial, @NotNull String backgroundType) {
// do nothing
}
@Override @Override
public boolean hasPermission(@NotNull String node) { public boolean hasPermission(@NotNull String node) {
return true; return true;
@@ -160,4 +166,14 @@ public class DummyPlayer extends OnlineUser {
return false; return false;
} }
@Override
public boolean isLocked() {
return false;
}
@Override
public boolean isNpc() {
return false;
}
} }

View File

@@ -3,10 +3,10 @@ org.gradle.jvmargs='-Dfile.encoding=UTF-8'
org.gradle.daemon=true org.gradle.daemon=true
javaVersion=16 javaVersion=16
plugin_version=2.1.1 plugin_version=2.2.3
plugin_archive=husksync plugin_archive=husksync
jedis_version=4.2.3 jedis_version=4.3.1
mysql_driver_version=8.0.30 mysql_driver_version=8.0.31
snappy_version=1.1.8.4 snappy_version=1.1.8.4
commons_text_version=1.9 commons_text_version=1.10.0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

BIN
images/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB