mirror of
https://github.com/WiIIiam278/HuskSync.git
synced 2025-12-28 02:59:13 +00:00
v3.0: New modular, more compatible data format, new API, better UX (#160)
* Start work on v3 * More work on task scheduling * Add comment to notification display slot * Synchronise branches * Use new HuskHomes-style task system * Bump to 2.3 * Remove HuskSyncInitializationException.java * Optimise database for MariaDB * Update libraries, move some around * Tweak command registration * Remove dummyhusksync * Fixup core synchronisation logic to use new task system * Implement new event dispatch subsystem * Remove last remaining future calls * Remove `Event#fire()` * Refactor startup process * New command subsystem, more initialization improvements, locale fixes * Update docs, tweak command perms * Reduce task number during data setting * add todo * Start work on data format / serialization refactor * More work on Bukkit impl * More serialization work * Fixes to serialization, data preview system * Start legacy conversion skeleton * Handle setting empty inventories * Start on-the-fly legacy conversion work * Add advancement conversion * Rewrite advancement get / apply logic * Start work on locked map persistence * More map persistence work * More work on map serialization * Move around persistence logic * Add testing suite * Fix item synchronisation * Finalize more reliable locked map persistence * Remove deprecated method call * remove sync feature enum * Fix held item slot syncing * Make data types modular and API-extensible * Remove some excessive debugging, minor refactor * Fixup date formatting, improve menu UIs * Finish up legacy data converting * Null safety in item stack serializaiton * Fix relocation of nbtapi, update dumping docs * Add v1/MPDB Migrators back in * Fix pinning/unpinning data not working * Consumer instead of Function for editing data * Show file size in DataSnapshotOverview * Fix getIdentifier always returning empty * Re-add items and inventory GUI commands * Improve config file, fixup data restoration * Add min time between backups (more useful backups!) * More work on backups * Fixup backup rotation frequency * Remove stdout debug print in `#getEventPriority` * Improve sync complete locale logic, fix synchronization spelling * Remove `static` on exception * Use dedicated thread for Redis, properly unsubscribe * Refactor `player` package -> `user` * `PlayerDataHolder` -> `UserDataHolder` * Make `StatisticsMap` public, but `@ApiStatus.Internal` * Suppress unused warnings on `Data` * Add option to disable Plan hook * Decompress legacy data before converting * Decompress bytes in fromBytes * Check permission node before serving TAB suggestions * Actually convert legacy item stack data * Fix syntax errors * Minor method refactor in items command * Fixup case-sensitive parsing in HuskSync command * Start API work * More work on API, fix potion effects * Fix cross-server, config formatting for auto-pinned issue * Fix confusion with UserData command, update docs images * Update commands docs * More docs updating * Fix sync feature enabled/disabled checking logic * Fix `#isCustom()` * Enable persistent_data syncing by default * docs: update Sync-Features config snippet * docs: correct typo in Sync Features * More API work * bukkit: slightly optimized schedulers * More API work, various refactorings * docs: Start new API docs * bump dependencies * Add some basic unit tests * docs: Correct typos * More docs work, annotate DB methods as `@Blocking` * Encapsulate `RedisMessage`, minor optimisations * api: Simplify `#getCurrentData` * api: Simplify `editCurrentData`, using `ThrowingConsumers` for better error handling * docs: More Data Snapshot API documenting * docs: add TOC to Data Snapshot API page * bukkit: Make data types extend BukkitData * Move where custom data is stored, finish up Custom Data API docs * Optimise imports * Fix `data_manager_advancements_preview_remaining` locale * Fix advancement and playtime previews * Fix potion effect deserialization * Make snapshot_backup_frequency default to 4, more error handling/logging * docs: Add ToC to Custom Data API * docs: Minor legacy API tweaks * Remove some unneeded catch logic * Suppress a few warnings * Fix Effect constructor being supplied in wrong order
This commit is contained in:
@@ -19,31 +19,40 @@
|
||||
|
||||
package net.william278.husksync;
|
||||
|
||||
import com.fatboyindustrial.gsonjavatime.Converters;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import net.william278.annotaml.Annotaml;
|
||||
import net.william278.desertwell.util.ThrowingConsumer;
|
||||
import net.william278.desertwell.util.UpdateChecker;
|
||||
import net.william278.desertwell.util.Version;
|
||||
import net.william278.husksync.adapter.DataAdapter;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.DataAdapter;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.Identifier;
|
||||
import net.william278.husksync.data.Serializer;
|
||||
import net.william278.husksync.database.Database;
|
||||
import net.william278.husksync.event.EventCannon;
|
||||
import net.william278.husksync.event.EventDispatcher;
|
||||
import net.william278.husksync.migrator.Migrator;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.redis.RedisManager;
|
||||
import net.william278.husksync.user.ConsoleUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.util.LegacyConverter;
|
||||
import net.william278.husksync.util.Task;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
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.lang.reflect.InvocationTargetException;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Abstract implementation of the HuskSync plugin.
|
||||
*/
|
||||
public interface HuskSync {
|
||||
public interface HuskSync extends Task.Supplier, EventDispatcher {
|
||||
|
||||
int SPIGOT_RESOURCE_ID = 97144;
|
||||
|
||||
@@ -81,21 +90,45 @@ public interface HuskSync {
|
||||
@NotNull
|
||||
RedisManager getRedisManager();
|
||||
|
||||
/**
|
||||
* Returns the data adapter implementation
|
||||
*
|
||||
* @return the {@link DataAdapter} implementation
|
||||
*/
|
||||
@NotNull
|
||||
DataAdapter getDataAdapter();
|
||||
|
||||
/**
|
||||
* Returns the event firing cannon
|
||||
*
|
||||
* @return the {@link EventCannon} implementation
|
||||
* Returns the data serializer for the given {@link Identifier}
|
||||
*/
|
||||
@NotNull
|
||||
EventCannon getEventCannon();
|
||||
<T extends Data> Map<Identifier, Serializer<T>> getSerializers();
|
||||
|
||||
/**
|
||||
* Register a data serializer for the given {@link Identifier}
|
||||
*
|
||||
* @param identifier the {@link Identifier}
|
||||
* @param serializer the {@link Serializer}
|
||||
*/
|
||||
default void registerSerializer(@NotNull Identifier identifier,
|
||||
@NotNull Serializer<? extends Data> serializer) {
|
||||
if (identifier.isCustom()) {
|
||||
log(Level.INFO, String.format("Registered custom data type: %s", identifier));
|
||||
}
|
||||
getSerializers().put(identifier, (Serializer<Data>) serializer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Identifier} for the given key
|
||||
*/
|
||||
default Optional<Identifier> getIdentifier(@NotNull String key) {
|
||||
return getSerializers().keySet().stream().filter(identifier -> identifier.toString().equals(key)).findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the set of registered data types
|
||||
*
|
||||
* @return the set of registered data types
|
||||
*/
|
||||
@NotNull
|
||||
default Set<Identifier> getRegisteredDataTypes() {
|
||||
return getSerializers().keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of available data {@link Migrator}s
|
||||
@@ -105,6 +138,25 @@ public interface HuskSync {
|
||||
@NotNull
|
||||
List<Migrator> getAvailableMigrators();
|
||||
|
||||
@NotNull
|
||||
Map<Identifier, Data> getPlayerCustomDataStore(@NotNull OnlineUser user);
|
||||
|
||||
/**
|
||||
* Initialize a faucet of the plugin.
|
||||
*
|
||||
* @param name the name of the faucet
|
||||
* @param runner a runnable for initializing the faucet
|
||||
*/
|
||||
default void initialize(@NotNull String name, @NotNull ThrowingConsumer<HuskSync> runner) {
|
||||
log(Level.INFO, "Initializing " + name + "...");
|
||||
try {
|
||||
runner.accept(this);
|
||||
} catch (Throwable e) {
|
||||
throw new FailedToLoadException("Failed to initialize " + name, e);
|
||||
}
|
||||
log(Level.INFO, "Successfully initialized " + name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the plugin {@link Settings}
|
||||
*
|
||||
@@ -113,6 +165,8 @@ public interface HuskSync {
|
||||
@NotNull
|
||||
Settings getSettings();
|
||||
|
||||
void setSettings(@NotNull Settings settings);
|
||||
|
||||
/**
|
||||
* Returns the plugin {@link Locales}
|
||||
*
|
||||
@@ -121,6 +175,16 @@ public interface HuskSync {
|
||||
@NotNull
|
||||
Locales getLocales();
|
||||
|
||||
void setLocales(@NotNull Locales locales);
|
||||
|
||||
/**
|
||||
* Returns if a dependency is loaded
|
||||
*
|
||||
* @param name the name of the dependency
|
||||
* @return {@code true} if the dependency is loaded, {@code false} otherwise
|
||||
*/
|
||||
boolean isDependencyLoaded(@NotNull String name);
|
||||
|
||||
/**
|
||||
* Get a resource as an {@link InputStream} from the plugin jar
|
||||
*
|
||||
@@ -129,6 +193,14 @@ public interface HuskSync {
|
||||
*/
|
||||
InputStream getResource(@NotNull String name);
|
||||
|
||||
/**
|
||||
* Returns the plugin data folder
|
||||
*
|
||||
* @return the plugin data folder as a {@link File}
|
||||
*/
|
||||
@NotNull
|
||||
File getDataFolder();
|
||||
|
||||
/**
|
||||
* Log a message to the console
|
||||
*
|
||||
@@ -146,10 +218,18 @@ public interface HuskSync {
|
||||
*/
|
||||
default void debug(@NotNull String message, @NotNull Throwable... throwable) {
|
||||
if (getSettings().doDebugLogging()) {
|
||||
log(Level.INFO, "[DEBUG] " + message, throwable);
|
||||
log(Level.INFO, String.format("[DEBUG] %s", message), throwable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the console user
|
||||
*
|
||||
* @return the {@link ConsoleUser}
|
||||
*/
|
||||
@NotNull
|
||||
ConsoleUser getConsole();
|
||||
|
||||
/**
|
||||
* Returns the plugin version
|
||||
*
|
||||
@@ -158,30 +238,6 @@ public interface HuskSync {
|
||||
@NotNull
|
||||
Version getPluginVersion();
|
||||
|
||||
/**
|
||||
* Returns the plugin data folder
|
||||
*
|
||||
* @return the plugin data folder as a {@link File}
|
||||
*/
|
||||
@NotNull
|
||||
File getDataFolder();
|
||||
|
||||
/**
|
||||
* Returns a future returning the latest plugin {@link Version} if the plugin is out-of-date
|
||||
*
|
||||
* @return a {@link CompletableFuture} returning the latest {@link Version} if the current one is out-of-date
|
||||
*/
|
||||
default CompletableFuture<Optional<Version>> getLatestVersionIfOutdated() {
|
||||
return UpdateChecker.builder()
|
||||
.currentVersion(getPluginVersion())
|
||||
.endpoint(UpdateChecker.Endpoint.SPIGOT)
|
||||
.resource(Integer.toString(SPIGOT_RESOURCE_ID)).build()
|
||||
.check()
|
||||
.thenApply(checked -> checked.isUpToDate()
|
||||
? Optional.empty()
|
||||
: Optional.of(checked.getLatestVersion()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Minecraft version implementation
|
||||
*
|
||||
@@ -191,12 +247,95 @@ public interface HuskSync {
|
||||
Version getMinecraftVersion();
|
||||
|
||||
/**
|
||||
* Reloads the {@link Settings} and {@link Locales} from their respective config files
|
||||
* Returns the platform type
|
||||
*
|
||||
* @return a {@link CompletableFuture} that will be completed when the plugin reload is complete and if it was successful
|
||||
* @return the platform type
|
||||
*/
|
||||
CompletableFuture<Boolean> reload();
|
||||
@NotNull
|
||||
String getPlatformType();
|
||||
|
||||
/**
|
||||
* Returns the legacy data converter, if it exists
|
||||
*
|
||||
* @return the {@link LegacyConverter}
|
||||
*/
|
||||
Optional<LegacyConverter> getLegacyConverter();
|
||||
|
||||
/**
|
||||
* Reloads the {@link Settings} and {@link Locales} from their respective config files.
|
||||
*/
|
||||
default void loadConfigs() {
|
||||
try {
|
||||
// Load settings
|
||||
setSettings(Annotaml.create(new File(getDataFolder(), "config.yml"), Settings.class).get());
|
||||
|
||||
// Load locales from language preset default
|
||||
final Locales languagePresets = Annotaml.create(
|
||||
Locales.class,
|
||||
Objects.requireNonNull(getResource(String.format("locales/%s.yml", getSettings().getLanguage())))
|
||||
).get();
|
||||
setLocales(Annotaml.create(new File(
|
||||
getDataFolder(),
|
||||
String.format("messages_%s.yml", getSettings().getLanguage())
|
||||
), languagePresets).get());
|
||||
} catch (IOException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
|
||||
throw new FailedToLoadException("Failed to load config or message files", e);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default UpdateChecker getUpdateChecker() {
|
||||
return UpdateChecker.builder()
|
||||
.currentVersion(getPluginVersion())
|
||||
.endpoint(UpdateChecker.Endpoint.SPIGOT)
|
||||
.resource(Integer.toString(SPIGOT_RESOURCE_ID))
|
||||
.build();
|
||||
}
|
||||
|
||||
default void checkForUpdates() {
|
||||
if (getSettings().doCheckForUpdates()) {
|
||||
getUpdateChecker().check().thenAccept(checked -> {
|
||||
if (!checked.isUpToDate()) {
|
||||
log(Level.WARNING, String.format(
|
||||
"A new version of HuskSync is available: v%s (running v%s)",
|
||||
checked.getLatestVersion(), getPluginVersion())
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
Set<UUID> getLockedPlayers();
|
||||
|
||||
@NotNull
|
||||
Gson getGson();
|
||||
|
||||
@NotNull
|
||||
default Gson createGson() {
|
||||
return Converters.registerOffsetDateTime(new GsonBuilder()).create();
|
||||
}
|
||||
|
||||
/**
|
||||
* An exception indicating the plugin has been accessed before it has been registered.
|
||||
*/
|
||||
final class FailedToLoadException extends IllegalStateException {
|
||||
|
||||
private static final String FORMAT = """
|
||||
HuskSync has failed to load! The plugin will not be enabled and no data will be synchronized.
|
||||
Please make sure the plugin has been setup correctly (https://william278.net/docs/husksync/setup):
|
||||
|
||||
1) Make sure you've entered your MySQL or MariaDB database details correctly in config.yml
|
||||
2) Make sure your Redis server details are also correct in config.yml
|
||||
3) Make sure your config is up-to-date (https://william278.net/docs/husksync/config-files)
|
||||
4) Check the error below for more details
|
||||
|
||||
Caused by: %s""";
|
||||
|
||||
FailedToLoadException(@NotNull String message, @NotNull Throwable cause) {
|
||||
super(String.format(FORMAT, message), cause);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,14 +17,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
package net.william278.husksync.adapter;
|
||||
|
||||
/**
|
||||
* Indicates an error occurred during {@link UserData} adaptation to and from (compressed) json.
|
||||
*/
|
||||
public class DataAdaptionException extends RuntimeException {
|
||||
protected DataAdaptionException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public interface Adaptable {
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.adapter;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* An adapter that adapts data to and from a portable byte array.
|
||||
*/
|
||||
public interface DataAdapter {
|
||||
|
||||
/**
|
||||
* Converts an {@link Adaptable} to a string.
|
||||
*
|
||||
* @param data The {@link Adaptable} to adapt
|
||||
* @param <A> The type of the {@link Adaptable}
|
||||
* @return The string
|
||||
* @throws AdaptionException If an error occurred during adaptation.
|
||||
*/
|
||||
@NotNull
|
||||
default <A extends Adaptable> String toString(@NotNull A data) throws AdaptionException {
|
||||
return new String(this.toBytes(data), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an {@link Adaptable} to a byte array.
|
||||
*
|
||||
* @param data The {@link Adaptable} to adapt
|
||||
* @param <A> The type of the {@link Adaptable}
|
||||
* @return The byte array
|
||||
* @throws AdaptionException If an error occurred during adaptation.
|
||||
*/
|
||||
<A extends Adaptable> byte[] toBytes(@NotNull A data) throws AdaptionException;
|
||||
|
||||
/**
|
||||
* Converts a JSON string to an {@link Adaptable}.
|
||||
*
|
||||
* @param data The JSON string to adapt.
|
||||
* @param type The class type of the {@link Adaptable} to adapt to.
|
||||
* @param <A> The type of the {@link Adaptable}
|
||||
* @return The {@link Adaptable}
|
||||
* @throws AdaptionException If an error occurred during adaptation.
|
||||
*/
|
||||
@NotNull
|
||||
<A extends Adaptable> A fromJson(@NotNull String data, @NotNull Class<A> type) throws AdaptionException;
|
||||
|
||||
/**
|
||||
* Converts an {@link Adaptable} to a JSON string.
|
||||
*
|
||||
* @param data The {@link Adaptable} to adapt
|
||||
* @param <A> The type of the {@link Adaptable}
|
||||
* @return The JSON string
|
||||
* @throws AdaptionException If an error occurred during adaptation.
|
||||
*/
|
||||
@NotNull
|
||||
<A extends Adaptable> String toJson(@NotNull A data) throws AdaptionException;
|
||||
|
||||
/**
|
||||
* Converts a byte array to an {@link Adaptable}.
|
||||
*
|
||||
* @param data The byte array to adapt.
|
||||
* @param type The class type of the {@link Adaptable} to adapt to.
|
||||
* @param <A> The type of the {@link Adaptable}
|
||||
* @return The {@link Adaptable}
|
||||
* @throws AdaptionException If an error occurred during adaptation.
|
||||
*/
|
||||
<A extends Adaptable> A fromBytes(@NotNull byte[] data, @NotNull Class<A> type) throws AdaptionException;
|
||||
|
||||
/**
|
||||
* Converts a byte array to a string, including decompression if required.
|
||||
*
|
||||
* @param bytes The byte array to convert
|
||||
* @return the string form of the bytes
|
||||
*/
|
||||
@NotNull
|
||||
String bytesToString(byte[] bytes);
|
||||
|
||||
final class AdaptionException extends IllegalStateException {
|
||||
static final String FORMAT = "An exception occurred when adapting serialized/deserialized data: %s";
|
||||
|
||||
public AdaptionException(@NotNull String message, @NotNull Throwable cause) {
|
||||
super(String.format(FORMAT, message), cause);
|
||||
}
|
||||
|
||||
public AdaptionException(@NotNull String message) {
|
||||
super(String.format(FORMAT, message));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.adapter;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class GsonAdapter implements DataAdapter {
|
||||
|
||||
private final HuskSync plugin;
|
||||
|
||||
public GsonAdapter(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <A extends Adaptable> byte[] toBytes(@NotNull A data) throws AdaptionException {
|
||||
return this.toJson(data).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public <A extends Adaptable> String toJson(@NotNull A data) throws AdaptionException {
|
||||
try {
|
||||
return plugin.getGson().toJson(data);
|
||||
} catch (Throwable e) {
|
||||
throw new AdaptionException("Failed to adapt data to JSON via Gson", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public <A extends Adaptable> A fromBytes(@NotNull byte[] data, @NotNull Class<A> type) throws AdaptionException {
|
||||
return this.fromJson(new String(data, StandardCharsets.UTF_8), type);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String bytesToString(byte[] bytes) {
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public <A extends Adaptable> A fromJson(@NotNull String data, @NotNull Class<A> type) throws AdaptionException {
|
||||
try {
|
||||
return plugin.getGson().fromJson(data, type);
|
||||
} catch (Throwable e) {
|
||||
throw new AdaptionException("Failed to adapt data from JSON via Gson", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.adapter;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.xerial.snappy.Snappy;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class SnappyGsonAdapter extends GsonAdapter {
|
||||
|
||||
public SnappyGsonAdapter(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public <A extends Adaptable> byte[] toBytes(@NotNull A data) throws AdaptionException {
|
||||
try {
|
||||
return Snappy.compress(super.toBytes(data));
|
||||
} catch (IOException e) {
|
||||
throw new AdaptionException("Failed to compress data through Snappy", e);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public <A extends Adaptable> A fromBytes(@NotNull byte[] data, @NotNull Class<A> type) throws AdaptionException {
|
||||
try {
|
||||
return super.fromBytes(decompressBytes(data), type);
|
||||
} catch (IOException e) {
|
||||
throw new AdaptionException("Failed to decompress data through Snappy", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String bytesToString(byte[] bytes) {
|
||||
try {
|
||||
return super.bytesToString(decompressBytes(bytes));
|
||||
} catch (IOException e) {
|
||||
throw new AdaptionException("Failed to decompress data through Snappy", e);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] decompressBytes(byte[] bytes) throws IOException {
|
||||
return Snappy.uncompress(bytes);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.api;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.data.UserDataSnapshot;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* The base implementation of the HuskSync API, containing cross-platform API calls.
|
||||
* </p>
|
||||
* This class should not be used directly, but rather through platform-specific extending API classes.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public abstract class BaseHuskSyncAPI {
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Instance of the implementing plugin.
|
||||
*/
|
||||
protected final HuskSync plugin;
|
||||
|
||||
protected BaseHuskSyncAPI(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link User} by the given player's account {@link UUID}, if they exist.
|
||||
*
|
||||
* @param uuid the unique id of the player to get the {@link User} instance for
|
||||
* @return future returning the {@link User} instance for the given player's unique id if they exist, otherwise an empty {@link Optional}
|
||||
* @apiNote The player does not have to be online
|
||||
* @since 2.0
|
||||
*/
|
||||
public final CompletableFuture<Optional<User>> getUser(@NotNull UUID uuid) {
|
||||
return plugin.getDatabase().getUser(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link User} by the given player's username (case-insensitive), if they exist.
|
||||
*
|
||||
* @param username the username of the {@link User} instance for
|
||||
* @return future returning the {@link User} instance for the given player's username if they exist,
|
||||
* otherwise an empty {@link Optional}
|
||||
* @apiNote The player does not have to be online, though their username has to be the username
|
||||
* they had when they last joined the server.
|
||||
* @since 2.0
|
||||
*/
|
||||
public final CompletableFuture<Optional<User>> getUser(@NotNull String username) {
|
||||
return plugin.getDatabase().getUserByName(username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link User}'s current {@link UserData}
|
||||
*
|
||||
* @param user the {@link User} to get the {@link UserData} for
|
||||
* @return future returning the {@link UserData} for the given {@link User} if they exist, otherwise an empty {@link Optional}
|
||||
* @apiNote If the user is not online on the implementing bukkit server,
|
||||
* the {@link UserData} returned will be their last database-saved UserData.
|
||||
* </p>
|
||||
* Because of this, if the user is online on another server on the network,
|
||||
* then the {@link UserData} returned by this method will <i>not necessarily reflective of
|
||||
* their current state</i>
|
||||
* @since 2.0
|
||||
*/
|
||||
public final CompletableFuture<Optional<UserData>> getUserData(@NotNull User user) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
if (user instanceof OnlineUser) {
|
||||
return ((OnlineUser) user).getUserData(plugin).join();
|
||||
} else {
|
||||
return plugin.getDatabase().getCurrentUserData(user).join().map(UserDataSnapshot::userData);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link UserData} to the database for the given {@link User}.
|
||||
* </p>
|
||||
* If the user is online and on the same cluster, their data will be updated in game.
|
||||
*
|
||||
* @param user the {@link User} to set the {@link UserData} for
|
||||
* @param userData the {@link UserData} to set for the given {@link User}
|
||||
* @return future returning void when complete
|
||||
* @since 2.0
|
||||
*/
|
||||
public final CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData) {
|
||||
return CompletableFuture.runAsync(() ->
|
||||
plugin.getDatabase().setUserData(user, userData, DataSaveCause.API)
|
||||
.thenRun(() -> plugin.getRedisManager().sendUserDataUpdate(user, userData).join()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the {@link UserData} of an {@link OnlineUser} to the database
|
||||
*
|
||||
* @param user the {@link OnlineUser} to save the {@link UserData} of
|
||||
* @return future returning void when complete
|
||||
* @since 2.0
|
||||
*/
|
||||
public final CompletableFuture<Void> saveUserData(@NotNull OnlineUser user) {
|
||||
return CompletableFuture.runAsync(() -> user.getUserData(plugin)
|
||||
.thenAccept(optionalUserData -> optionalUserData.ifPresent(
|
||||
userData -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.API).join())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the saved {@link UserDataSnapshot} records for the given {@link User}
|
||||
*
|
||||
* @param user the {@link User} to get the {@link UserDataSnapshot} for
|
||||
* @return future returning a list {@link UserDataSnapshot} for the given {@link User} if they exist,
|
||||
* otherwise an empty {@link Optional}
|
||||
* @apiNote The length of the list of VersionedUserData will correspond to the configured
|
||||
* {@code max_user_data_records} config option
|
||||
* @since 2.0
|
||||
*/
|
||||
public final CompletableFuture<List<UserDataSnapshot>> getSavedUserData(@NotNull User user) {
|
||||
return CompletableFuture.supplyAsync(() -> plugin.getDatabase().getUserData(user).join());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON string representation of the given {@link UserData}
|
||||
*
|
||||
* @param userData the {@link UserData} to get the JSON string representation of
|
||||
* @param prettyPrint whether to pretty print the JSON string
|
||||
* @return the JSON string representation of the given {@link UserData}
|
||||
* @since 2.0
|
||||
*/
|
||||
@NotNull
|
||||
public final String getUserDataJson(@NotNull UserData userData, boolean prettyPrint) {
|
||||
return plugin.getDataAdapter().toJson(userData, prettyPrint);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,458 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.api;
|
||||
|
||||
import net.william278.desertwell.util.ThrowingConsumer;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.Adaptable;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.data.Identifier;
|
||||
import net.william278.husksync.data.Serializer;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* The base implementation of the HuskSync API, containing cross-platform API calls.
|
||||
* </p>
|
||||
* This class should not be used directly, but rather through platform-specific extending API classes.
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public abstract class HuskSyncAPI {
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Instance of the implementing plugin.
|
||||
*/
|
||||
protected final HuskSync plugin;
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Constructor, instantiating the base API class.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
protected HuskSyncAPI(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link User} by their UUID
|
||||
*
|
||||
* @param uuid The UUID of the user to get
|
||||
* @return A future containing the user, or an empty optional if the user doesn't exist
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public CompletableFuture<Optional<User>> getUser(@NotNull UUID uuid) {
|
||||
return plugin.supplyAsync(() -> plugin.getDatabase().getUser(uuid));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link User} by their username
|
||||
*
|
||||
* @param username The username of the user to get
|
||||
* @return A future containing the user, or an empty optional if the user doesn't exist
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public CompletableFuture<Optional<User>> getUser(@NotNull String username) {
|
||||
return plugin.supplyAsync(() -> plugin.getDatabase().getUserByName(username));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new data snapshot of an {@link OnlineUser}'s data.
|
||||
*
|
||||
* @param user The user to create the snapshot of
|
||||
* @return The snapshot of the user's data
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public DataSnapshot.Packed createSnapshot(@NotNull OnlineUser user) {
|
||||
return snapshotBuilder().saveCause(DataSnapshot.SaveCause.API).buildAndPack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link User}'s current data, as a {@link DataSnapshot.Unpacked}
|
||||
* <p>
|
||||
* If the user is online, this will create a new snapshot of their data with the {@code API} data save cause.
|
||||
* </p>
|
||||
* If the user is offline, this will return the latest snapshot of their data if that exists
|
||||
* (an empty optional will be returned otherwise).
|
||||
*
|
||||
* @param user The user to get the data of
|
||||
* @return A future containing the user's current data, or an empty optional if the user has no data
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<Optional<DataSnapshot.Unpacked>> getCurrentData(@NotNull User user) {
|
||||
return plugin.getRedisManager()
|
||||
.getUserData(UUID.randomUUID(), user)
|
||||
.thenApply(data -> data.or(() -> plugin.getDatabase().getLatestSnapshot(user)))
|
||||
.thenApply(data -> data.map(snapshot -> snapshot.unpack(plugin)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a user's current data.
|
||||
* <p>
|
||||
* This will update the user's data in the database (creating a new snapshot) and send a data update,
|
||||
* updating the user if they are online.
|
||||
*
|
||||
* @param user The user to set the data of
|
||||
* @param data The data to set
|
||||
* @since 3.0
|
||||
*/
|
||||
public void setCurrentData(@NotNull User user, @NotNull DataSnapshot data) {
|
||||
plugin.runAsync(() -> {
|
||||
final DataSnapshot.Packed packed = data instanceof DataSnapshot.Unpacked unpacked
|
||||
? unpacked.pack(plugin) : (DataSnapshot.Packed) data;
|
||||
addSnapshot(user, packed);
|
||||
plugin.getRedisManager().sendUserDataUpdate(user, packed);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a user's current data.
|
||||
* <p>
|
||||
* This will update the user's data in the database (creating a new snapshot) and send a data update,
|
||||
* updating the user if they are online.
|
||||
*
|
||||
* @param user The user to edit the data of
|
||||
* @param editor The editor function
|
||||
* @since 3.0
|
||||
*/
|
||||
public void editCurrentData(@NotNull User user, @NotNull ThrowingConsumer<DataSnapshot.Unpacked> editor) {
|
||||
getCurrentData(user).thenAccept(optional -> optional.ifPresent(data -> {
|
||||
editor.accept(data);
|
||||
setCurrentData(user, data);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all saved data snapshots for a user
|
||||
*
|
||||
* @param user The user to get the data snapshots of
|
||||
* @return The user's data snapshots
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<List<DataSnapshot.Unpacked>> getSnapshots(@NotNull User user) {
|
||||
return plugin.supplyAsync(
|
||||
() -> plugin.getDatabase().getAllSnapshots(user).stream()
|
||||
.map(snapshot -> snapshot.unpack(plugin))
|
||||
.toList()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific data snapshot for a user
|
||||
*
|
||||
* @param user The user to get the data snapshot of
|
||||
* @param versionId The version ID of the snapshot to get
|
||||
* @return The user's data snapshot, or an empty optional if the user has no data
|
||||
* @see #getSnapshots(User)
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<List<DataSnapshot.Unpacked>> getSnapshot(@NotNull User user, @NotNull UUID versionId) {
|
||||
return plugin.supplyAsync(
|
||||
() -> plugin.getDatabase().getSnapshot(user, versionId).stream()
|
||||
.map(snapshot -> snapshot.unpack(plugin))
|
||||
.toList()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a data snapshot for a user
|
||||
*
|
||||
* @param user The user to edit the snapshot of
|
||||
* @param versionId The version ID of the snapshot to edit
|
||||
* @param editor The editor function
|
||||
* @since 3.0
|
||||
*/
|
||||
public void editSnapshot(@NotNull User user, @NotNull UUID versionId,
|
||||
@NotNull ThrowingConsumer<DataSnapshot.Unpacked> editor) {
|
||||
plugin.runAsync(() -> plugin.getDatabase().getSnapshot(user, versionId).ifPresent(snapshot -> {
|
||||
final DataSnapshot.Unpacked unpacked = snapshot.unpack(plugin);
|
||||
editor.accept(unpacked);
|
||||
plugin.getDatabase().updateSnapshot(user, unpacked.pack(plugin));
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest data snapshot for a user that has been saved in the database.
|
||||
* <p>
|
||||
* Not to be confused with {@link #getCurrentData(User)}, which will return the current data of a user
|
||||
* if they are online (this method will only return their latest <i>saved</i> snapshot).
|
||||
* </p>
|
||||
*
|
||||
* @param user The user to get the latest data snapshot of
|
||||
* @return The user's latest data snapshot, or an empty optional if the user has no data
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<Optional<DataSnapshot.Unpacked>> getLatestSnapshot(@NotNull User user) {
|
||||
return plugin.supplyAsync(
|
||||
() -> plugin.getDatabase().getLatestSnapshot(user).map(snapshot -> snapshot.unpack(plugin))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the latest data snapshot for a user
|
||||
*
|
||||
* @param user The user to edit the latest snapshot of
|
||||
* @param editor The editor function
|
||||
* @since 3.0
|
||||
*/
|
||||
public void editLatestSnapshot(@NotNull User user, @NotNull ThrowingConsumer<DataSnapshot.Unpacked> editor) {
|
||||
plugin.runAsync(() -> plugin.getDatabase().getLatestSnapshot(user).ifPresent(snapshot -> {
|
||||
final DataSnapshot.Unpacked unpacked = snapshot.unpack(plugin);
|
||||
editor.accept(unpacked);
|
||||
plugin.getDatabase().updateSnapshot(user, unpacked.pack(plugin));
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a data snapshot to the database
|
||||
*
|
||||
* @param user The user to save the data for
|
||||
* @param snapshot The snapshot to save
|
||||
* @since 3.0
|
||||
*/
|
||||
public void addSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot) {
|
||||
plugin.runAsync(() -> plugin.getDatabase().addSnapshot(
|
||||
user, snapshot instanceof DataSnapshot.Unpacked unpacked
|
||||
? unpacked.pack(plugin) : (DataSnapshot.Packed) snapshot
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an <i>existing</i> data snapshot in the database.
|
||||
* Not to be confused with {@link #addSnapshot(User, DataSnapshot)}, which will add a new snapshot if one
|
||||
* snapshot doesn't exist.
|
||||
*
|
||||
* @param user The user to update the snapshot of
|
||||
* @param snapshot The snapshot to update
|
||||
* @since 3.0
|
||||
*/
|
||||
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot) {
|
||||
plugin.runAsync(() -> plugin.getDatabase().updateSnapshot(
|
||||
user, snapshot instanceof DataSnapshot.Unpacked unpacked
|
||||
? unpacked.pack(plugin) : (DataSnapshot.Packed) snapshot
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin a data snapshot, preventing it from being rotated
|
||||
*
|
||||
* @param user The user to pin the snapshot of
|
||||
* @param snapshotVersion The version ID of the snapshot to pin
|
||||
* @since 3.0
|
||||
*/
|
||||
public void pinSnapshot(@NotNull User user, @NotNull UUID snapshotVersion) {
|
||||
plugin.runAsync(() -> plugin.getDatabase().pinSnapshot(user, snapshotVersion));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpin a data snapshot, allowing it to be rotated
|
||||
*
|
||||
* @param user The user to unpin the snapshot of
|
||||
* @param snapshotVersion The version ID of the snapshot to unpin
|
||||
* @since 3.0
|
||||
*/
|
||||
public void unpinSnapshot(@NotNull User user, @NotNull UUID snapshotVersion) {
|
||||
plugin.runAsync(() -> plugin.getDatabase().unpinSnapshot(user, snapshotVersion));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a data snapshot from the database
|
||||
*
|
||||
* @param user The user to delete the snapshot of
|
||||
* @param versionId The version ID of the snapshot to delete
|
||||
* @return A future which will complete with true if the snapshot was deleted, or false if it wasn't
|
||||
* (e.g., if the snapshot didn't exist)
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<Boolean> deleteSnapshot(@NotNull User user, @NotNull UUID versionId) {
|
||||
return plugin.supplyAsync(() -> plugin.getDatabase().deleteSnapshot(user, versionId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a data snapshot from the database
|
||||
*
|
||||
* @param user The user to delete the snapshot of
|
||||
* @param snapshot The snapshot to delete
|
||||
* @return A future which will complete with true if the snapshot was deleted, or false if it wasn't
|
||||
* (e.g., if the snapshot hasn't been saved to the database yet)
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<Boolean> deleteSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot) {
|
||||
return deleteSnapshot(user, snapshot.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new custom data type serializer.
|
||||
* <p>
|
||||
* This allows for custom {@link Data} types to be persisted in {@link DataSnapshot}s. To register
|
||||
* a new data type, you must provide a {@link Serializer} for serializing and deserializing the data type
|
||||
* and invoke this method.
|
||||
* </p>
|
||||
* You'll need to do this on every server you wish to sync data between. On servers where the registered
|
||||
* data type is not present, the data will be ignored and snapshots created on that server will not
|
||||
* contain the data.
|
||||
*
|
||||
* @param identifier The identifier of the data type to register.
|
||||
* Create one using {@code Identifier.from(Key.of("your_plugin_name", "key"))}
|
||||
* @param serializer An implementation of {@link Serializer} for serializing and deserializing the {@link Data}
|
||||
* @param <T> A type extending {@link Data}; this will represent the data being held.
|
||||
*/
|
||||
public <T extends Data> void registerDataSerializer(@NotNull Identifier identifier,
|
||||
@NotNull Serializer<T> serializer) {
|
||||
plugin.registerSerializer(identifier, serializer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link DataSnapshot.Unpacked} from a {@link DataSnapshot.Packed}
|
||||
*
|
||||
* @param unpacked The unpacked snapshot
|
||||
* @return The packed snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public DataSnapshot.Packed packSnapshot(@NotNull DataSnapshot.Unpacked unpacked) {
|
||||
return unpacked.pack(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link DataSnapshot.Unpacked} from a {@link DataSnapshot.Packed}
|
||||
*
|
||||
* @param packed The packed snapshot
|
||||
* @return The unpacked snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public DataSnapshot.Unpacked unpackSnapshot(@NotNull DataSnapshot.Packed packed) {
|
||||
return packed.unpack(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpack, edit, and repack a data snapshot.
|
||||
* </p>
|
||||
* This won't save the snapshot to the database; it'll just edit the data snapshot in place.
|
||||
*
|
||||
* @param packed The packed snapshot
|
||||
* @param editor An editor function for editing the unpacked snapshot
|
||||
* @return The edited packed snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public DataSnapshot.Packed editPackedSnapshot(@NotNull DataSnapshot.Packed packed,
|
||||
@NotNull ThrowingConsumer<DataSnapshot.Unpacked> editor) {
|
||||
final DataSnapshot.Unpacked unpacked = packed.unpack(plugin);
|
||||
editor.accept(unpacked);
|
||||
return unpacked.pack(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the estimated size of a {@link DataSnapshot} in bytes
|
||||
*
|
||||
* @param snapshot The snapshot to get the size of
|
||||
* @return The size of the snapshot in bytes
|
||||
* @since 3.0
|
||||
*/
|
||||
public int getSnapshotFileSize(@NotNull DataSnapshot snapshot) {
|
||||
return (snapshot instanceof DataSnapshot.Packed packed)
|
||||
? packed.getFileSize(plugin)
|
||||
: ((DataSnapshot.Unpacked) snapshot).pack(plugin).getFileSize(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a builder for creating a new data snapshot
|
||||
*
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public DataSnapshot.Builder snapshotBuilder() {
|
||||
return DataSnapshot.builder(plugin).saveCause(DataSnapshot.SaveCause.API);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize a JSON string to an {@link Adaptable}
|
||||
*
|
||||
* @param serialized The serialized JSON string
|
||||
* @param type The type of the element
|
||||
* @param <T> The type of the element
|
||||
* @return The deserialized element
|
||||
* @throws Serializer.DeserializationException If the element could not be deserialized
|
||||
*/
|
||||
@NotNull
|
||||
public <T extends Adaptable> T deserializeData(@NotNull String serialized, Class<T> type)
|
||||
throws Serializer.DeserializationException {
|
||||
return plugin.getDataAdapter().fromJson(serialized, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize an {@link Adaptable} to a JSON string
|
||||
*
|
||||
* @param element The element to serialize
|
||||
* @param <T> The type of the element
|
||||
* @return The serialized JSON string
|
||||
* @throws Serializer.SerializationException If the element could not be serialized
|
||||
*/
|
||||
@NotNull
|
||||
public <T extends Adaptable> String serializeData(@NotNull T element)
|
||||
throws Serializer.SerializationException {
|
||||
return plugin.getDataAdapter().toJson(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Get the plugin instance
|
||||
*
|
||||
* @return The plugin instance
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public HuskSync getPlugin() {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* An exception indicating the plugin has been accessed before it has been registered.
|
||||
*/
|
||||
static final class NotRegisteredException extends IllegalStateException {
|
||||
|
||||
private static final String MESSAGE = """
|
||||
Could not access the HuskSync API as it has not yet been registered. This could be because:
|
||||
1) HuskSync has failed to enable successfully
|
||||
2) Your plugin isn't set to load after HuskSync has
|
||||
(Check if it set as a (soft)depend in plugin.yml or to load: BEFORE in paper-plugin.yml?)
|
||||
3) You are attempting to access HuskSync on plugin construction/before your plugin has enabled.
|
||||
4) You have shaded HuskSync into your plugin jar and need to fix your maven/gradle/build script
|
||||
to only include HuskSync as a dependency and not as a shaded dependency.""";
|
||||
|
||||
NotRegisteredException() {
|
||||
super(MESSAGE);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public abstract class Command extends Node {
|
||||
|
||||
private final String usage;
|
||||
private final Map<String, Boolean> additionalPermissions;
|
||||
|
||||
protected Command(@NotNull String name, @NotNull List<String> aliases, @NotNull String usage,
|
||||
@NotNull HuskSync plugin) {
|
||||
super(name, aliases, plugin);
|
||||
this.usage = usage;
|
||||
this.additionalPermissions = new HashMap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void onExecuted(@NotNull CommandUser executor, @NotNull String[] args) {
|
||||
if (!executor.hasPermission(getPermission())) {
|
||||
plugin.getLocales().getLocale("error_no_permission")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
plugin.runAsync(() -> this.execute(executor, args));
|
||||
}
|
||||
|
||||
public abstract void execute(@NotNull CommandUser executor, @NotNull String[] args);
|
||||
|
||||
@NotNull
|
||||
public final String getRawUsage() {
|
||||
return usage;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public final String getUsage() {
|
||||
return "/" + getName() + " " + getRawUsage();
|
||||
}
|
||||
|
||||
public final void addAdditionalPermissions(@NotNull Map<String, Boolean> permissions) {
|
||||
permissions.forEach((permission, value) -> this.additionalPermissions.put(getPermission(permission), value));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public final Map<String, Boolean> getAdditionalPermissions() {
|
||||
return additionalPermissions;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getDescription() {
|
||||
return plugin.getLocales().getRawLocale(getName() + "_command_description")
|
||||
.orElse(getUsage());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public final HuskSync getPlugin() {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Represents an abstract cross-platform representation for a plugin command
|
||||
*/
|
||||
public abstract class CommandBase {
|
||||
|
||||
/**
|
||||
* The input string to match for this command
|
||||
*/
|
||||
public final String command;
|
||||
|
||||
/**
|
||||
* The permission node required to use this command
|
||||
*/
|
||||
public final String permission;
|
||||
|
||||
/**
|
||||
* Alias input strings for this command
|
||||
*/
|
||||
public final String[] aliases;
|
||||
|
||||
/**
|
||||
* Instance of the implementing plugin
|
||||
*/
|
||||
public final HuskSync plugin;
|
||||
|
||||
|
||||
public CommandBase(@NotNull String command, @NotNull Permission permission, @NotNull HuskSync implementor, String... aliases) {
|
||||
this.command = command;
|
||||
this.permission = permission.node;
|
||||
this.plugin = implementor;
|
||||
this.aliases = aliases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires when the command is executed
|
||||
*
|
||||
* @param player {@link OnlineUser} executing the command
|
||||
* @param args Command arguments
|
||||
*/
|
||||
public abstract void onExecute(@NotNull OnlineUser player, @NotNull String[] args);
|
||||
|
||||
/**
|
||||
* Returns the localised description string of this command
|
||||
*
|
||||
* @return the command description
|
||||
*/
|
||||
public String getDescription() {
|
||||
return plugin.getLocales().getRawLocale(command + "_command_description")
|
||||
.orElse("A HuskSync command");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,112 +21,72 @@ package net.william278.husksync.command;
|
||||
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.data.UserDataBuilder;
|
||||
import net.william278.husksync.data.UserDataSnapshot;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class EnderChestCommand extends CommandBase implements TabCompletable {
|
||||
public class EnderChestCommand extends ItemsCommand {
|
||||
|
||||
public EnderChestCommand(@NotNull HuskSync implementor) {
|
||||
super("enderchest", Permission.COMMAND_ENDER_CHEST, implementor, "echest", "openechest");
|
||||
public EnderChestCommand(@NotNull HuskSync plugin) {
|
||||
super(plugin, List.of("enderchest", "echest", "openechest"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) {
|
||||
if (args.length == 0 || args.length > 2) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax", "/enderchest <player>")
|
||||
.ifPresent(player::sendMessage);
|
||||
protected void showItems(@NotNull OnlineUser viewer, @NotNull DataSnapshot.Unpacked snapshot,
|
||||
@NotNull User user, boolean allowEdit) {
|
||||
final Optional<Data.Items.EnderChest> optionalEnderChest = snapshot.getEnderChest();
|
||||
if (optionalEnderChest.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage);
|
||||
return;
|
||||
}
|
||||
plugin.getDatabase().getUserByName(args[0].toLowerCase(Locale.ENGLISH)).thenAccept(optionalUser ->
|
||||
optionalUser.ifPresentOrElse(user -> {
|
||||
if (args.length == 2) {
|
||||
// View user data by specified UUID
|
||||
try {
|
||||
final UUID versionUuid = UUID.fromString(args[1]);
|
||||
plugin.getDatabase().getUserData(user, versionUuid).thenAccept(data -> data.ifPresentOrElse(
|
||||
userData -> showEnderChestMenu(player, userData, user, false),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(player::sendMessage)));
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/enderchest <player> [version_uuid]").ifPresent(player::sendMessage);
|
||||
}
|
||||
} else {
|
||||
// View (and edit) the latest user data
|
||||
plugin.getDatabase().getCurrentUserData(user).thenAccept(optionalData -> optionalData.ifPresentOrElse(
|
||||
versionedUserData -> showEnderChestMenu(player, versionedUserData, user,
|
||||
player.hasPermission(Permission.COMMAND_ENDER_CHEST_EDIT.node)),
|
||||
() -> plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(player::sendMessage)));
|
||||
|
||||
// Display opening message
|
||||
plugin.getLocales().getLocale("ender_chest_viewer_opened", user.getUsername(),
|
||||
snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("dd/MM/yyyy, HH:mm")))
|
||||
.ifPresent(viewer::sendMessage);
|
||||
|
||||
// Show GUI
|
||||
final Data.Items.EnderChest enderChest = optionalEnderChest.get();
|
||||
viewer.showGui(
|
||||
enderChest,
|
||||
plugin.getLocales().getLocale("ender_chest_viewer_menu_title", user.getUsername())
|
||||
.orElse(new MineDown(String.format("%s's Ender Chest", user.getUsername()))),
|
||||
allowEdit,
|
||||
enderChest.getSlotCount(),
|
||||
(itemsOnClose) -> {
|
||||
if (allowEdit && !enderChest.equals(itemsOnClose)) {
|
||||
plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user));
|
||||
}
|
||||
}, () -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage)));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private void showEnderChestMenu(@NotNull OnlineUser player, @NotNull UserDataSnapshot userDataSnapshot,
|
||||
@NotNull User dataOwner, boolean allowEdit) {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
final UserData data = userDataSnapshot.userData();
|
||||
data.getEnderChest().ifPresent(itemData -> {
|
||||
// Show message
|
||||
plugin.getLocales().getLocale("ender_chest_viewer_opened", dataOwner.username,
|
||||
new SimpleDateFormat("MMM dd yyyy, HH:mm:ss.sss")
|
||||
.format(userDataSnapshot.versionTimestamp()))
|
||||
.ifPresent(player::sendMessage);
|
||||
// Creates a new snapshot with the updated enderChest
|
||||
@SuppressWarnings("DuplicatedCode")
|
||||
private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User user) {
|
||||
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(user);
|
||||
if (latestData.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show inventory menu
|
||||
player.showMenu(itemData, allowEdit, 3, plugin.getLocales()
|
||||
.getLocale("ender_chest_viewer_menu_title", dataOwner.username)
|
||||
.orElse(new MineDown("Ender Chest Viewer")))
|
||||
.exceptionally(throwable -> {
|
||||
plugin.log(Level.WARNING, "Exception displaying inventory menu to " + player.username, throwable);
|
||||
return Optional.empty();
|
||||
})
|
||||
.thenAccept(dataOnClose -> {
|
||||
if (dataOnClose.isEmpty() || !allowEdit) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the updated data
|
||||
final UserDataBuilder builder = UserData.builder(plugin.getMinecraftVersion());
|
||||
data.getStatus().ifPresent(builder::setStatus);
|
||||
data.getAdvancements().ifPresent(builder::setAdvancements);
|
||||
data.getLocation().ifPresent(builder::setLocation);
|
||||
data.getPersistentDataContainer().ifPresent(builder::setPersistentDataContainer);
|
||||
data.getStatistics().ifPresent(builder::setStatistics);
|
||||
data.getPotionEffects().ifPresent(builder::setPotionEffects);
|
||||
data.getInventory().ifPresent(builder::setInventory);
|
||||
builder.setEnderChest(dataOnClose.get());
|
||||
|
||||
// Set the updated data
|
||||
final UserData updatedUserData = builder.build();
|
||||
plugin.getDatabase()
|
||||
.setUserData(dataOwner, updatedUserData, DataSaveCause.INVENTORY_COMMAND)
|
||||
.thenRun(() -> plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData));
|
||||
});
|
||||
});
|
||||
// Create and pack the snapshot with the updated enderChest
|
||||
final DataSnapshot.Packed snapshot = latestData.get().copy();
|
||||
snapshot.edit(plugin, (data) -> {
|
||||
data.setSaveCause(DataSnapshot.SaveCause.ENDERCHEST_COMMAND);
|
||||
data.setPinned(plugin.getSettings().doAutoPin(DataSnapshot.SaveCause.ENDERCHEST_COMMAND));
|
||||
data.getEnderChest().ifPresent(enderChest -> enderChest.setContents(items));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(@NotNull String[] args) {
|
||||
return plugin.getOnlineUsers().stream().map(user -> user.username)
|
||||
.filter(argument -> argument.startsWith(args.length >= 1 ? args[0] : ""))
|
||||
.sorted().collect(Collectors.toList());
|
||||
plugin.getDatabase().addSnapshot(user, snapshot);
|
||||
plugin.getRedisManager().sendUserDataUpdate(user, snapshot);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,18 +19,11 @@
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Interface providing console execution of commands
|
||||
*/
|
||||
public interface ConsoleExecutable {
|
||||
public interface Executable {
|
||||
|
||||
/**
|
||||
* What to do when console executes a command
|
||||
*
|
||||
* @param args command argument strings
|
||||
*/
|
||||
void onConsoleExecute(@NotNull String[] args);
|
||||
void onExecuted(@NotNull CommandUser executor, @NotNull String[] args);
|
||||
|
||||
}
|
||||
@@ -23,26 +23,39 @@ import de.themoep.minedown.adventure.MineDown;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import net.william278.desertwell.about.AboutMenu;
|
||||
import net.william278.desertwell.util.UpdateChecker;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.migrator.Migrator;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class HuskSyncCommand extends CommandBase implements TabCompletable, ConsoleExecutable {
|
||||
public class HuskSyncCommand extends Command implements TabProvider {
|
||||
|
||||
private final String[] SUB_COMMANDS = {"update", "about", "reload", "migrate"};
|
||||
private static final Map<String, Boolean> SUB_COMMANDS = Map.of(
|
||||
"about", false,
|
||||
"reload", true,
|
||||
"migrate", true,
|
||||
"update", true
|
||||
);
|
||||
|
||||
private final UpdateChecker updateChecker;
|
||||
private final AboutMenu aboutMenu;
|
||||
|
||||
public HuskSyncCommand(@NotNull HuskSync implementor) {
|
||||
super("husksync", Permission.COMMAND_HUSKSYNC, implementor);
|
||||
public HuskSyncCommand(@NotNull HuskSync plugin) {
|
||||
super("husksync", List.of(), "[" + String.join("|", SUB_COMMANDS.keySet()) + "]", plugin);
|
||||
addAdditionalPermissions(SUB_COMMANDS);
|
||||
|
||||
this.updateChecker = plugin.getUpdateChecker();
|
||||
this.aboutMenu = AboutMenu.builder()
|
||||
.title(Component.text("HuskSync"))
|
||||
.description(Component.text("A modern, cross-server player data synchronization system"))
|
||||
.version(implementor.getPluginVersion())
|
||||
.version(plugin.getPluginVersion())
|
||||
.credits("Author",
|
||||
AboutMenu.Credit.of("William278").description("Click to visit website").url("https://william278.net"))
|
||||
.credits("Contributors",
|
||||
@@ -68,123 +81,104 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) {
|
||||
if (args.length < 1) {
|
||||
sendAboutMenu(player);
|
||||
public void execute(@NotNull CommandUser executor, @NotNull String[] args) {
|
||||
final String subCommand = parseStringArg(args, 0).orElse("about").toLowerCase(Locale.ENGLISH);
|
||||
if (SUB_COMMANDS.containsKey(subCommand) && !executor.hasPermission(getPermission(subCommand))) {
|
||||
plugin.getLocales().getLocale("error_no_permission")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
switch (args[0].toLowerCase(Locale.ENGLISH)) {
|
||||
case "update", "version" -> {
|
||||
if (!player.hasPermission(Permission.COMMAND_HUSKSYNC_UPDATE.node)) {
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
plugin.getLatestVersionIfOutdated().thenAccept(newestVersion ->
|
||||
newestVersion.ifPresentOrElse(
|
||||
newVersion -> player.sendMessage(
|
||||
new MineDown("[HuskSync](#00fb9a bold) [| A new version of HuskSync is available!"
|
||||
+ " (v" + newVersion + " (Running: v" + plugin.getPluginVersion() + ")](#00fb9a)")),
|
||||
() -> player.sendMessage(
|
||||
new MineDown("[HuskSync](#00fb9a bold) [| HuskSync is up-to-date."
|
||||
+ " (Running: v" + plugin.getPluginVersion() + ")](#00fb9a)"))));
|
||||
}
|
||||
case "about", "info" -> sendAboutMenu(player);
|
||||
case "reload" -> {
|
||||
if (!player.hasPermission(Permission.COMMAND_HUSKSYNC_RELOAD.node)) {
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
plugin.reload();
|
||||
plugin.getLocales().getLocale("reload_complete").ifPresent(player::sendMessage);
|
||||
}
|
||||
case "migrate" ->
|
||||
plugin.getLocales().getLocale("error_console_command_only").ifPresent(player::sendMessage);
|
||||
default -> plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/husksync <update/about/reload>")
|
||||
.ifPresent(player::sendMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConsoleExecute(@NotNull String[] args) {
|
||||
if (args.length < 1) {
|
||||
plugin.log(Level.INFO, "Console usage: \"husksync <update/about/reload/migrate>\"");
|
||||
return;
|
||||
}
|
||||
switch (args[0].toLowerCase(Locale.ENGLISH)) {
|
||||
case "update", "version" -> plugin.getLatestVersionIfOutdated().thenAccept(newestVersion ->
|
||||
newestVersion.ifPresentOrElse(newVersion -> plugin.log(Level.WARNING,
|
||||
"An update is available for HuskSync, v" + newVersion
|
||||
+ " (Running v" + plugin.getPluginVersion() + ")"),
|
||||
() -> plugin.log(Level.INFO,
|
||||
"HuskSync is up to date" +
|
||||
" (Running v" + plugin.getPluginVersion() + ")")));
|
||||
case "about", "info" -> aboutMenu.toString().lines().forEach(line -> plugin.log(Level.INFO, line));
|
||||
switch (subCommand) {
|
||||
case "about" -> executor.sendMessage(aboutMenu.toComponent());
|
||||
case "reload" -> {
|
||||
plugin.reload();
|
||||
plugin.log(Level.INFO, "Reloaded config & message files.");
|
||||
try {
|
||||
plugin.loadConfigs();
|
||||
plugin.getLocales().getLocale("reload_complete").ifPresent(executor::sendMessage);
|
||||
} catch (Throwable e) {
|
||||
executor.sendMessage(new MineDown(
|
||||
"[Error:](#ff3300) [Failed to reload the plugin. Check console for errors.](#ff7e5e)"
|
||||
));
|
||||
plugin.log(Level.SEVERE, "Failed to reload the plugin", e);
|
||||
}
|
||||
}
|
||||
case "migrate" -> {
|
||||
if (args.length < 2) {
|
||||
plugin.log(Level.INFO,
|
||||
"Please choose a migrator, then run \"husksync migrate <migrator>\"");
|
||||
logMigratorsList();
|
||||
if (executor instanceof OnlineUser) {
|
||||
plugin.getLocales().getLocale("error_console_command_only")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
final Optional<Migrator> selectedMigrator = plugin.getAvailableMigrators().stream().filter(availableMigrator ->
|
||||
availableMigrator.getIdentifier().equalsIgnoreCase(args[1])).findFirst();
|
||||
selectedMigrator.ifPresentOrElse(migrator -> {
|
||||
if (args.length < 3) {
|
||||
plugin.log(Level.INFO, migrator.getHelpMenu());
|
||||
return;
|
||||
}
|
||||
switch (args[2]) {
|
||||
case "start" -> migrator.start().thenAccept(succeeded -> {
|
||||
if (succeeded) {
|
||||
plugin.log(Level.INFO, "Migration completed successfully!");
|
||||
} else {
|
||||
plugin.log(Level.WARNING, "Migration failed!");
|
||||
}
|
||||
});
|
||||
case "set" -> migrator.handleConfigurationCommand(Arrays.copyOfRange(args, 3, args.length));
|
||||
default -> plugin.log(Level.INFO,
|
||||
"Invalid syntax. Console usage: \"husksync migrate " + args[1] + " <start/set>");
|
||||
}
|
||||
}, () -> {
|
||||
plugin.log(Level.INFO,
|
||||
"Please specify a valid migrator.\n" +
|
||||
"If a migrator is not available, please verify that you meet the prerequisites to use it.");
|
||||
logMigratorsList();
|
||||
});
|
||||
this.handleMigrationCommand(args);
|
||||
}
|
||||
default -> plugin.log(Level.INFO,
|
||||
"Invalid syntax. Console usage: \"husksync <update/about/reload/migrate>\"");
|
||||
case "update" -> updateChecker.check().thenAccept(checked -> {
|
||||
if (checked.isUpToDate()) {
|
||||
plugin.getLocales().getLocale("up_to_date", plugin.getPluginVersion().toString())
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
plugin.getLocales().getLocale("update_available", checked.getLatestVersion().toString(),
|
||||
plugin.getPluginVersion().toString()).ifPresent(executor::sendMessage);
|
||||
});
|
||||
default -> plugin.getLocales().getLocale("error_invalid_syntax", getUsage())
|
||||
.ifPresent(executor::sendMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private void logMigratorsList() {
|
||||
plugin.log(Level.INFO,
|
||||
"List of available migrators:\nMigrator ID / Migrator Name:\n" +
|
||||
plugin.getAvailableMigrators().stream()
|
||||
.map(migrator -> migrator.getIdentifier() + " - " + migrator.getName())
|
||||
.collect(Collectors.joining("\n")));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(@NotNull String[] args) {
|
||||
if (args.length <= 1) {
|
||||
return Arrays.stream(SUB_COMMANDS)
|
||||
.filter(argument -> argument.startsWith(args.length == 1 ? args[0] : ""))
|
||||
.sorted().collect(Collectors.toList());
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private void sendAboutMenu(@NotNull OnlineUser player) {
|
||||
if (!player.hasPermission(Permission.COMMAND_HUSKSYNC_ABOUT.node)) {
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
// Handle a migration console command input
|
||||
private void handleMigrationCommand(@NotNull String[] args) {
|
||||
if (args.length < 2) {
|
||||
plugin.log(Level.INFO,
|
||||
"Please choose a migrator, then run \"husksync migrate <migrator>\"");
|
||||
this.logMigratorList();
|
||||
return;
|
||||
}
|
||||
player.sendMessage(aboutMenu.toComponent());
|
||||
|
||||
final Optional<Migrator> selectedMigrator = plugin.getAvailableMigrators().stream()
|
||||
.filter(available -> available.getIdentifier().equalsIgnoreCase(args[1]))
|
||||
.findFirst();
|
||||
selectedMigrator.ifPresentOrElse(migrator -> {
|
||||
if (args.length < 3) {
|
||||
plugin.log(Level.INFO, migrator.getHelpMenu());
|
||||
return;
|
||||
}
|
||||
switch (args[2]) {
|
||||
case "start" -> migrator.start().thenAccept(succeeded -> {
|
||||
if (succeeded) {
|
||||
plugin.log(Level.INFO, "Migration completed successfully!");
|
||||
} else {
|
||||
plugin.log(Level.WARNING, "Migration failed!");
|
||||
}
|
||||
});
|
||||
case "set" -> migrator.handleConfigurationCommand(Arrays.copyOfRange(args, 3, args.length));
|
||||
default -> plugin.log(Level.INFO, String.format(
|
||||
"Invalid syntax. Console usage: \"husksync migrate %s <start/set>", args[1]
|
||||
));
|
||||
}
|
||||
}, () -> {
|
||||
plugin.log(Level.INFO,
|
||||
"Please specify a valid migrator.\n" +
|
||||
"If a migrator is not available, please verify that you meet the prerequisites to use it.");
|
||||
this.logMigratorList();
|
||||
});
|
||||
}
|
||||
|
||||
// Log the list of available migrators
|
||||
private void logMigratorList() {
|
||||
plugin.log(Level.INFO, String.format(
|
||||
"List of available migrators:\nMigrator ID / Migrator Name:\n%s",
|
||||
plugin.getAvailableMigrators().stream()
|
||||
.map(migrator -> String.format("%s - %s", migrator.getIdentifier(), migrator.getName()))
|
||||
.collect(Collectors.joining("\n"))
|
||||
));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public List<String> suggest(@NotNull CommandUser user, @NotNull String[] args) {
|
||||
return switch (args.length) {
|
||||
case 0, 1 -> SUB_COMMANDS.keySet().stream().sorted().toList();
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,111 +21,72 @@ package net.william278.husksync.command;
|
||||
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.data.UserDataBuilder;
|
||||
import net.william278.husksync.data.UserDataSnapshot;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class InventoryCommand extends CommandBase implements TabCompletable {
|
||||
public class InventoryCommand extends ItemsCommand {
|
||||
|
||||
public InventoryCommand(@NotNull HuskSync implementor) {
|
||||
super("inventory", Permission.COMMAND_INVENTORY, implementor, "invsee", "openinv");
|
||||
public InventoryCommand(@NotNull HuskSync plugin) {
|
||||
super(plugin, List.of("inventory", "invsee", "openinv"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) {
|
||||
if (args.length == 0 || args.length > 2) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax", "/inventory <player>")
|
||||
.ifPresent(player::sendMessage);
|
||||
protected void showItems(@NotNull OnlineUser viewer, @NotNull DataSnapshot.Unpacked snapshot,
|
||||
@NotNull User user, boolean allowEdit) {
|
||||
final Optional<Data.Items.Inventory> optionalInventory = snapshot.getInventory();
|
||||
if (optionalInventory.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage);
|
||||
return;
|
||||
}
|
||||
plugin.getDatabase().getUserByName(args[0].toLowerCase(Locale.ENGLISH)).thenAccept(optionalUser ->
|
||||
optionalUser.ifPresentOrElse(user -> {
|
||||
if (args.length == 2) {
|
||||
// View user data by specified UUID
|
||||
try {
|
||||
final UUID versionUuid = UUID.fromString(args[1]);
|
||||
plugin.getDatabase().getUserData(user, versionUuid).thenAccept(data -> data.ifPresentOrElse(
|
||||
userData -> showInventoryMenu(player, userData, user, false),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(player::sendMessage)));
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/inventory <player> [version_uuid]").ifPresent(player::sendMessage);
|
||||
}
|
||||
} else {
|
||||
// View (and edit) the latest user data
|
||||
plugin.getDatabase().getCurrentUserData(user).thenAccept(optionalData -> optionalData.ifPresentOrElse(
|
||||
versionedUserData -> showInventoryMenu(player, versionedUserData, user,
|
||||
player.hasPermission(Permission.COMMAND_INVENTORY_EDIT.node)),
|
||||
() -> plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(player::sendMessage)));
|
||||
|
||||
// Display opening message
|
||||
plugin.getLocales().getLocale("inventory_viewer_opened", user.getUsername(),
|
||||
snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("dd/MM/yyyy, HH:mm")))
|
||||
.ifPresent(viewer::sendMessage);
|
||||
|
||||
// Show GUI
|
||||
final Data.Items.Inventory inventory = optionalInventory.get();
|
||||
viewer.showGui(
|
||||
inventory,
|
||||
plugin.getLocales().getLocale("inventory_viewer_menu_title", user.getUsername())
|
||||
.orElse(new MineDown(String.format("%s's Inventory", user.getUsername()))),
|
||||
allowEdit,
|
||||
inventory.getSlotCount(),
|
||||
(itemsOnClose) -> {
|
||||
if (allowEdit && !inventory.equals(itemsOnClose)) {
|
||||
plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user));
|
||||
}
|
||||
}, () -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage)));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private void showInventoryMenu(@NotNull OnlineUser player, @NotNull UserDataSnapshot userDataSnapshot,
|
||||
@NotNull User dataOwner, boolean allowEdit) {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
final UserData data = userDataSnapshot.userData();
|
||||
data.getInventory().ifPresent(itemData -> {
|
||||
// Show message
|
||||
plugin.getLocales().getLocale("inventory_viewer_opened", dataOwner.username,
|
||||
new SimpleDateFormat("MMM dd yyyy, HH:mm:ss.sss")
|
||||
.format(userDataSnapshot.versionTimestamp()))
|
||||
.ifPresent(player::sendMessage);
|
||||
// Creates a new snapshot with the updated inventory
|
||||
@SuppressWarnings("DuplicatedCode")
|
||||
private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User user) {
|
||||
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(user);
|
||||
if (latestData.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show inventory menu
|
||||
player.showMenu(itemData, allowEdit, 5, plugin.getLocales()
|
||||
.getLocale("inventory_viewer_menu_title", dataOwner.username)
|
||||
.orElse(new MineDown("Inventory Viewer")))
|
||||
.exceptionally(throwable -> {
|
||||
plugin.log(Level.WARNING, "Exception displaying inventory menu to " + player.username, throwable);
|
||||
return Optional.empty();
|
||||
})
|
||||
.thenAccept(dataOnClose -> {
|
||||
if (dataOnClose.isEmpty() || !allowEdit) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the updated data
|
||||
final UserDataBuilder builder = UserData.builder(plugin.getMinecraftVersion());
|
||||
data.getStatus().ifPresent(builder::setStatus);
|
||||
data.getAdvancements().ifPresent(builder::setAdvancements);
|
||||
data.getLocation().ifPresent(builder::setLocation);
|
||||
data.getPersistentDataContainer().ifPresent(builder::setPersistentDataContainer);
|
||||
data.getStatistics().ifPresent(builder::setStatistics);
|
||||
data.getPotionEffects().ifPresent(builder::setPotionEffects);
|
||||
data.getEnderChest().ifPresent(builder::setEnderChest);
|
||||
builder.setInventory(dataOnClose.get());
|
||||
|
||||
// Set the updated data
|
||||
final UserData updatedUserData = builder.build();
|
||||
plugin.getDatabase()
|
||||
.setUserData(dataOwner, updatedUserData, DataSaveCause.INVENTORY_COMMAND)
|
||||
.thenRun(() -> plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData));
|
||||
});
|
||||
});
|
||||
// Create and pack the snapshot with the updated inventory
|
||||
final DataSnapshot.Packed snapshot = latestData.get().copy();
|
||||
snapshot.edit(plugin, (data) -> {
|
||||
data.setSaveCause(DataSnapshot.SaveCause.INVENTORY_COMMAND);
|
||||
data.setPinned(plugin.getSettings().doAutoPin(DataSnapshot.SaveCause.INVENTORY_COMMAND));
|
||||
data.getInventory().ifPresent(inventory -> inventory.setContents(items));
|
||||
});
|
||||
plugin.getDatabase().addSnapshot(user, snapshot);
|
||||
plugin.getRedisManager().sendUserDataUpdate(user, snapshot);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(@NotNull String[] args) {
|
||||
return plugin.getOnlineUsers().stream().map(user -> user.username)
|
||||
.filter(argument -> argument.startsWith(args.length >= 1 ? args[0] : ""))
|
||||
.sorted().collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public abstract class ItemsCommand extends Command implements TabProvider {
|
||||
|
||||
protected ItemsCommand(@NotNull HuskSync plugin, @NotNull List<String> aliases) {
|
||||
super(aliases.get(0), aliases.subList(1, aliases.size()), "<player> [version_uuid]", plugin);
|
||||
setOperatorCommand(true);
|
||||
addAdditionalPermissions(Map.of("edit", true));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void execute(@NotNull CommandUser executor, @NotNull String[] args) {
|
||||
if (!(executor instanceof OnlineUser player)) {
|
||||
plugin.getLocales().getLocale("error_in_game_command_only")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the user to view the items for
|
||||
final Optional<User> optionalUser = parseStringArg(args, 0)
|
||||
.flatMap(name -> plugin.getDatabase().getUserByName(name));
|
||||
if (optionalUser.isEmpty()) {
|
||||
plugin.getLocales().getLocale(
|
||||
args.length >= 1 ? "error_invalid_player" : "error_invalid_syntax", getUsage()
|
||||
).ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the user data
|
||||
final User user = optionalUser.get();
|
||||
parseUUIDArg(args, 1).ifPresentOrElse(
|
||||
version -> this.showSnapshotItems(player, user, version),
|
||||
() -> this.showLatestItems(player, user)
|
||||
);
|
||||
}
|
||||
|
||||
// View (and edit) the latest user data
|
||||
private void showLatestItems(@NotNull OnlineUser viewer, @NotNull User user) {
|
||||
plugin.getRedisManager().getUserData(user.getUuid(), user).thenAccept(data -> data
|
||||
.or(() -> plugin.getDatabase().getLatestSnapshot(user))
|
||||
.ifPresentOrElse(
|
||||
snapshot -> this.showItems(
|
||||
viewer, snapshot.unpack(plugin), user,
|
||||
viewer.hasPermission(getPermission("edit"))
|
||||
),
|
||||
() -> plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage)
|
||||
));
|
||||
}
|
||||
|
||||
// View a specific version of the user data
|
||||
private void showSnapshotItems(@NotNull OnlineUser viewer, @NotNull User user, @NotNull UUID version) {
|
||||
plugin.getDatabase().getSnapshot(user, version)
|
||||
.ifPresentOrElse(
|
||||
snapshot -> this.showItems(
|
||||
viewer, snapshot.unpack(plugin), user, false
|
||||
),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(viewer::sendMessage)
|
||||
);
|
||||
}
|
||||
|
||||
// Show a GUI menu with the correct item data from the snapshot
|
||||
protected abstract void showItems(@NotNull OnlineUser viewer, @NotNull DataSnapshot.Unpacked snapshot,
|
||||
@NotNull User user, boolean allowEdit);
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public List<String> suggest(@NotNull CommandUser executor, @NotNull String[] args) {
|
||||
return switch (args.length) {
|
||||
case 0, 1 -> plugin.getOnlineUsers().stream().map(User::getUsername).toList();
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
}
|
||||
105
common/src/main/java/net/william278/husksync/command/Node.java
Normal file
105
common/src/main/java/net/william278/husksync/command/Node.java
Normal file
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.UUID;
|
||||
|
||||
public abstract class Node implements Executable {
|
||||
|
||||
protected static final String PERMISSION_PREFIX = "husksync.command";
|
||||
|
||||
protected final HuskSync plugin;
|
||||
private final String name;
|
||||
private final List<String> aliases;
|
||||
private boolean operatorCommand = false;
|
||||
|
||||
protected Node(@NotNull String name, @NotNull List<String> aliases, @NotNull HuskSync plugin) {
|
||||
if (name.isBlank()) {
|
||||
throw new IllegalArgumentException("Command name cannot be blank");
|
||||
}
|
||||
this.name = name;
|
||||
this.aliases = aliases;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<String> getAliases() {
|
||||
return aliases;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getPermission(@NotNull String... child) {
|
||||
final StringJoiner joiner = new StringJoiner(".")
|
||||
.add(PERMISSION_PREFIX)
|
||||
.add(getName());
|
||||
for (final String node : child) {
|
||||
joiner.add(node);
|
||||
}
|
||||
return joiner.toString().trim();
|
||||
}
|
||||
|
||||
public boolean isOperatorCommand() {
|
||||
return operatorCommand;
|
||||
}
|
||||
|
||||
public void setOperatorCommand(boolean operatorCommand) {
|
||||
this.operatorCommand = operatorCommand;
|
||||
}
|
||||
|
||||
protected Optional<String> parseStringArg(@NotNull String[] args, int index) {
|
||||
if (args.length > index) {
|
||||
return Optional.of(args[index]);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
protected Optional<Integer> parseIntArg(@NotNull String[] args, int index) {
|
||||
return parseStringArg(args, index).flatMap(arg -> {
|
||||
try {
|
||||
return Optional.of(Integer.parseInt(arg));
|
||||
} catch (NumberFormatException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected Optional<UUID> parseUUIDArg(@NotNull String[] args, int index) {
|
||||
return parseStringArg(args, index).flatMap(arg -> {
|
||||
try {
|
||||
return Optional.of(UUID.fromString(arg));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Static plugin permission nodes required to execute commands
|
||||
*/
|
||||
public enum Permission {
|
||||
|
||||
/*
|
||||
* /husksync command permissions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Lets the user use the {@code /husksync} command (subcommand permissions required)
|
||||
*/
|
||||
COMMAND_HUSKSYNC("husksync.command.husksync", DefaultAccess.EVERYONE),
|
||||
/**
|
||||
* Lets the user view plugin info {@code /husksync info}
|
||||
*/
|
||||
COMMAND_HUSKSYNC_ABOUT("husksync.command.husksync.info", DefaultAccess.EVERYONE),
|
||||
/**
|
||||
* Lets the user reload the plugin {@code /husksync reload}
|
||||
*/
|
||||
COMMAND_HUSKSYNC_RELOAD("husksync.command.husksync.reload", DefaultAccess.OPERATORS),
|
||||
/**
|
||||
* Lets the user view the plugin version and check for updates {@code /husksync update}
|
||||
*/
|
||||
COMMAND_HUSKSYNC_UPDATE("husksync.command.husksync.update", DefaultAccess.OPERATORS),
|
||||
|
||||
/*
|
||||
* /userdata command permissions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Lets the user view user data {@code /userdata view/list (player) (version_uuid)}
|
||||
*/
|
||||
COMMAND_USER_DATA("husksync.command.userdata", DefaultAccess.OPERATORS),
|
||||
/**
|
||||
* Lets the user restore and delete user data {@code /userdata restore/delete (player) (version_uuid)}
|
||||
*/
|
||||
COMMAND_USER_DATA_MANAGE("husksync.command.userdata.manage", DefaultAccess.OPERATORS),
|
||||
|
||||
/**
|
||||
* Lets the user dump user data to a file or the web {@code /userdata dump (player) (version_uuid)}
|
||||
*/
|
||||
COMMAND_USER_DATA_DUMP("husksync.command.userdata.dump", DefaultAccess.NOBODY),
|
||||
|
||||
/*
|
||||
* /inventory command permissions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Lets the user use the {@code /inventory (player)} command and view offline players' inventories
|
||||
*/
|
||||
COMMAND_INVENTORY("husksync.command.inventory", DefaultAccess.OPERATORS),
|
||||
/**
|
||||
* Lets the user edit the contents of offline players' inventories
|
||||
*/
|
||||
COMMAND_INVENTORY_EDIT("husksync.command.inventory.edit", DefaultAccess.OPERATORS),
|
||||
|
||||
/*
|
||||
* /enderchest command permissions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Lets the user use the {@code /enderchest (player)} command and view offline players' ender chests
|
||||
*/
|
||||
COMMAND_ENDER_CHEST("husksync.command.enderchest", DefaultAccess.OPERATORS),
|
||||
/**
|
||||
* Lets the user edit the contents of offline players' ender chests
|
||||
*/
|
||||
COMMAND_ENDER_CHEST_EDIT("husksync.command.enderchest.edit", DefaultAccess.OPERATORS);
|
||||
|
||||
|
||||
public final String node;
|
||||
public final DefaultAccess defaultAccess;
|
||||
|
||||
Permission(@NotNull String node, @NotNull DefaultAccess defaultAccess) {
|
||||
this.node = node;
|
||||
this.defaultAccess = defaultAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies who gets what permissions by default
|
||||
*/
|
||||
public enum DefaultAccess {
|
||||
/**
|
||||
* Everyone gets this permission node by default
|
||||
*/
|
||||
EVERYONE,
|
||||
/**
|
||||
* Nobody gets this permission node by default
|
||||
*/
|
||||
NOBODY,
|
||||
/**
|
||||
* Server operators ({@code /op}) get this permission node by default
|
||||
*/
|
||||
OPERATORS
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Interface providing tab completions for a command
|
||||
*/
|
||||
public interface TabCompletable {
|
||||
|
||||
/**
|
||||
* What should be returned when the player or console attempts to TAB-complete a command
|
||||
*
|
||||
* @param args Current command arguments
|
||||
* @return List of String arguments to offer TAB suggestions
|
||||
*/
|
||||
List<String> onTabComplete(@NotNull String[] args);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface TabProvider {
|
||||
|
||||
@Nullable
|
||||
List<String> suggest(@NotNull CommandUser user, @NotNull String[] args);
|
||||
|
||||
@NotNull
|
||||
default List<String> getSuggestions(@NotNull CommandUser user, @NotNull String[] args) {
|
||||
List<String> suggestions = suggest(user, args);
|
||||
if (suggestions == null) {
|
||||
suggestions = List.of();
|
||||
}
|
||||
return filter(suggestions, args);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default List<String> filter(@NotNull List<String> suggestions, @NotNull String[] args) {
|
||||
return suggestions.stream()
|
||||
.filter(suggestion -> args.length == 0 || suggestion.toLowerCase()
|
||||
.startsWith(args[args.length - 1].toLowerCase().trim()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,309 +20,216 @@
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import net.william278.husksync.util.DataDumper;
|
||||
import net.william278.husksync.util.DataSnapshotList;
|
||||
import net.william278.husksync.util.DataSnapshotOverview;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class UserDataCommand extends CommandBase implements TabCompletable {
|
||||
public class UserDataCommand extends Command implements TabProvider {
|
||||
|
||||
private final String[] COMMAND_ARGUMENTS = {"view", "list", "delete", "restore", "pin", "dump"};
|
||||
private static final Map<String, Boolean> SUB_COMMANDS = Map.of(
|
||||
"view", false,
|
||||
"list", false,
|
||||
"delete", true,
|
||||
"restore", true,
|
||||
"pin", true,
|
||||
"dump", true
|
||||
);
|
||||
|
||||
public UserDataCommand(@NotNull HuskSync implementor) {
|
||||
super("userdata", Permission.COMMAND_USER_DATA, implementor, "playerdata");
|
||||
public UserDataCommand(@NotNull HuskSync plugin) {
|
||||
super("userdata", List.of("playerdata"), String.format(
|
||||
"<%s> [username] [version_uuid]", String.join("/", SUB_COMMANDS.keySet())
|
||||
), plugin);
|
||||
setOperatorCommand(true);
|
||||
addAdditionalPermissions(SUB_COMMANDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) {
|
||||
if (args.length < 1) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata <view/list/delete/restore/pin/dump> <username> [version_uuid]")
|
||||
.ifPresent(player::sendMessage);
|
||||
public void execute(@NotNull CommandUser executor, @NotNull String[] args) {
|
||||
final String subCommand = parseStringArg(args, 0).orElse("view").toLowerCase(Locale.ENGLISH);
|
||||
final Optional<User> optionalUser = parseStringArg(args, 1)
|
||||
.flatMap(name -> plugin.getDatabase().getUserByName(name))
|
||||
.or(() -> parseStringArg(args, 0).flatMap(name -> plugin.getDatabase().getUserByName(name)))
|
||||
.or(() -> args.length < 2 && executor instanceof User userExecutor
|
||||
? Optional.of(userExecutor) : Optional.empty());
|
||||
final Optional<UUID> optionalUuid = parseUUIDArg(args, 2).or(() -> parseUUIDArg(args, 1));
|
||||
if (optionalUser.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (args[0].toLowerCase(Locale.ENGLISH)) {
|
||||
case "view" -> {
|
||||
if (args.length < 2) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata view <username> [version_uuid]")
|
||||
.ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
final String username = args[1];
|
||||
if (args.length >= 3) {
|
||||
try {
|
||||
final UUID versionUuid = UUID.fromString(args[2]);
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase()
|
||||
.getUserByName(username.toLowerCase(Locale.ENGLISH))
|
||||
.thenAccept(optionalUser -> optionalUser
|
||||
.ifPresentOrElse(user -> plugin.getDatabase().getUserData(user, versionUuid)
|
||||
.thenAccept(data -> data.ifPresentOrElse(
|
||||
userData -> userData.displayDataOverview(player, user, plugin.getLocales()),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(player::sendMessage))),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage))));
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata view <username> [version_uuid]")
|
||||
.ifPresent(player::sendMessage);
|
||||
}
|
||||
} else {
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase()
|
||||
.getUserByName(username.toLowerCase(Locale.ENGLISH))
|
||||
.thenAccept(optionalUser -> optionalUser
|
||||
.ifPresentOrElse(user -> plugin.getDatabase().getCurrentUserData(user)
|
||||
.thenAccept(latestData -> latestData.ifPresentOrElse(
|
||||
userData -> userData.displayDataOverview(player, user, plugin.getLocales()),
|
||||
() -> plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(player::sendMessage))),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage))));
|
||||
}
|
||||
}
|
||||
final User user = optionalUser.get();
|
||||
switch (subCommand) {
|
||||
case "view" -> optionalUuid.ifPresentOrElse(
|
||||
// Show the specified snapshot
|
||||
version -> plugin.getDatabase().getSnapshot(user, version).ifPresentOrElse(
|
||||
data -> DataSnapshotOverview.of(
|
||||
data.unpack(plugin), data.getFileSize(plugin), user, plugin
|
||||
).show(executor),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage)),
|
||||
|
||||
// Show the latest snapshot
|
||||
() -> plugin.getDatabase().getLatestSnapshot(user).ifPresentOrElse(
|
||||
data -> DataSnapshotOverview.of(
|
||||
data.unpack(plugin), data.getFileSize(plugin), user, plugin
|
||||
).show(executor),
|
||||
() -> plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(executor::sendMessage))
|
||||
);
|
||||
|
||||
case "list" -> {
|
||||
if (!player.hasPermission(Permission.COMMAND_USER_DATA_MANAGE.node)) {
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
// Check if there is data to display
|
||||
final List<DataSnapshot.Packed> dataList = plugin.getDatabase().getAllSnapshots(user);
|
||||
if (dataList.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
if (args.length < 2) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata list <username> [page]")
|
||||
.ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
final String username = args[1];
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase()
|
||||
.getUserByName(username.toLowerCase(Locale.ENGLISH))
|
||||
.thenAccept(optionalUser -> optionalUser.ifPresentOrElse(
|
||||
user -> plugin.getDatabase().getUserData(user).thenAccept(dataList -> {
|
||||
// Check if there is data to display
|
||||
if (dataList.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine page to display
|
||||
int page = 1;
|
||||
if (args.length >= 3) {
|
||||
try {
|
||||
page = Integer.parseInt(args[2]);
|
||||
} catch (NumberFormatException e) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata list <username> [page]")
|
||||
.ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Show the list to the player
|
||||
DataSnapshotList.create(dataList, user, plugin.getLocales())
|
||||
.displayPage(player, page);
|
||||
}),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage))));
|
||||
// Show the list to the player
|
||||
DataSnapshotList.create(dataList, user, plugin).displayPage(
|
||||
executor,
|
||||
parseIntArg(args, 2).or(() -> parseIntArg(args, 1)).orElse(1)
|
||||
);
|
||||
}
|
||||
|
||||
case "delete" -> {
|
||||
if (!player.hasPermission(Permission.COMMAND_USER_DATA_MANAGE.node)) {
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
if (optionalUuid.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata delete <username> <version_uuid>")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete user data by specified UUID
|
||||
if (args.length < 3) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata delete <username> <version_uuid>")
|
||||
.ifPresent(player::sendMessage);
|
||||
final UUID version = optionalUuid.get();
|
||||
if (!plugin.getDatabase().deleteSnapshot(user, version)) {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
final String username = args[1];
|
||||
try {
|
||||
final UUID versionUuid = UUID.fromString(args[2]);
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase()
|
||||
.getUserByName(username.toLowerCase(Locale.ENGLISH))
|
||||
.thenAccept(optionalUser -> optionalUser.ifPresentOrElse(
|
||||
user -> plugin.getDatabase().deleteUserData(user, versionUuid).thenAccept(deleted -> {
|
||||
if (deleted) {
|
||||
plugin.getLocales().getLocale("data_deleted",
|
||||
versionUuid.toString().split("-")[0],
|
||||
versionUuid.toString(),
|
||||
user.username,
|
||||
user.uuid.toString())
|
||||
.ifPresent(player::sendMessage);
|
||||
} else {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(player::sendMessage);
|
||||
}
|
||||
}),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage))));
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata delete <username> <version_uuid>")
|
||||
.ifPresent(player::sendMessage);
|
||||
}
|
||||
|
||||
plugin.getLocales().getLocale("data_deleted",
|
||||
version.toString().split("-")[0],
|
||||
version.toString(),
|
||||
user.getUsername(),
|
||||
user.getUuid().toString())
|
||||
.ifPresent(executor::sendMessage);
|
||||
}
|
||||
|
||||
case "restore" -> {
|
||||
if (!player.hasPermission(Permission.COMMAND_USER_DATA_MANAGE.node)) {
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
if (optionalUuid.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata delete <username> <version_uuid>")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
// Get user data by specified uuid and username
|
||||
if (args.length < 3) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata restore <username> <version_uuid>")
|
||||
.ifPresent(player::sendMessage);
|
||||
|
||||
// Restore user data by specified UUID
|
||||
final Optional<DataSnapshot.Packed> optionalData = plugin.getDatabase().getSnapshot(user, optionalUuid.get());
|
||||
if (optionalData.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
final String username = args[1];
|
||||
try {
|
||||
final UUID versionUuid = UUID.fromString(args[2]);
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase()
|
||||
.getUserByName(username.toLowerCase(Locale.ENGLISH))
|
||||
.thenAccept(optionalUser -> optionalUser.ifPresentOrElse(
|
||||
user -> plugin.getDatabase().getUserData(user, versionUuid).thenAccept(data -> {
|
||||
if (data.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore users with a minimum of one health (prevent restoring players with <=0 health)
|
||||
final UserData userData = data.get().userData();
|
||||
userData.getStatus().ifPresent(status -> status.health = Math.max(1, status.health));
|
||||
// Restore users with a minimum of one health (prevent restoring players with <=0 health)
|
||||
final DataSnapshot.Packed data = optionalData.get().copy();
|
||||
data.edit(plugin, (unpacked -> {
|
||||
unpacked.getHealth().ifPresent(status -> status.setHealth(Math.max(1, status.getHealth())));
|
||||
unpacked.setSaveCause(DataSnapshot.SaveCause.BACKUP_RESTORE);
|
||||
unpacked.setPinned(plugin.getSettings().doAutoPin(DataSnapshot.SaveCause.BACKUP_RESTORE));
|
||||
}));
|
||||
|
||||
// Set the users data and send a message
|
||||
plugin.getDatabase().setUserData(user, userData, DataSaveCause.BACKUP_RESTORE);
|
||||
plugin.getRedisManager().sendUserDataUpdate(user, data.get().userData()).join();
|
||||
plugin.getLocales().getLocale("data_restored",
|
||||
user.username,
|
||||
user.uuid.toString(),
|
||||
versionUuid.toString().split("-")[0],
|
||||
versionUuid.toString())
|
||||
.ifPresent(player::sendMessage);
|
||||
}),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage))));
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata restore <username> <version_uuid>")
|
||||
.ifPresent(player::sendMessage);
|
||||
}
|
||||
// Set the user's data and send a message
|
||||
plugin.getDatabase().addSnapshot(user, data);
|
||||
plugin.getRedisManager().sendUserDataUpdate(user, data);
|
||||
plugin.getLocales().getLocale("data_restored", user.getUsername(), user.getUuid().toString(),
|
||||
data.getShortId(), data.getId().toString()).ifPresent(executor::sendMessage);
|
||||
}
|
||||
|
||||
case "pin" -> {
|
||||
if (!player.hasPermission(Permission.COMMAND_USER_DATA_MANAGE.node)) {
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
if (args.length < 3) {
|
||||
if (optionalUuid.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata pin <username> <version_uuid>")
|
||||
.ifPresent(player::sendMessage);
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
final String username = args[1];
|
||||
try {
|
||||
final UUID versionUuid = UUID.fromString(args[2]);
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase()
|
||||
.getUserByName(username.toLowerCase(Locale.ENGLISH))
|
||||
.thenAccept(optionalUser -> optionalUser.ifPresentOrElse(
|
||||
user -> plugin.getDatabase().getUserData(user, versionUuid).thenAccept(
|
||||
optionalUserData -> optionalUserData.ifPresentOrElse(userData -> {
|
||||
if (userData.pinned()) {
|
||||
plugin.getDatabase().unpinUserData(user, versionUuid).join();
|
||||
plugin.getLocales().getLocale("data_unpinned",
|
||||
versionUuid.toString().split("-")[0],
|
||||
versionUuid.toString(),
|
||||
user.username,
|
||||
user.uuid.toString())
|
||||
.ifPresent(player::sendMessage);
|
||||
} else {
|
||||
plugin.getDatabase().pinUserData(user, versionUuid).join();
|
||||
plugin.getLocales().getLocale("data_pinned",
|
||||
versionUuid.toString().split("-")[0],
|
||||
versionUuid.toString(),
|
||||
user.username,
|
||||
user.uuid.toString())
|
||||
.ifPresent(player::sendMessage);
|
||||
}
|
||||
}, () -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(player::sendMessage))),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage))));
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata pin <username> <version_uuid>")
|
||||
.ifPresent(player::sendMessage);
|
||||
// Check that the data exists
|
||||
final Optional<DataSnapshot.Packed> optionalData = plugin.getDatabase().getSnapshot(user, optionalUuid.get());
|
||||
if (optionalData.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Pin or unpin the data
|
||||
final DataSnapshot.Packed data = optionalData.get();
|
||||
if (data.isPinned()) {
|
||||
plugin.getDatabase().unpinSnapshot(user, data.getId());
|
||||
} else {
|
||||
plugin.getDatabase().pinSnapshot(user, data.getId());
|
||||
}
|
||||
plugin.getLocales().getLocale(data.isPinned() ? "data_unpinned" : "data_pinned", data.getShortId(),
|
||||
data.getId().toString(), user.getUsername(), user.getUuid().toString())
|
||||
.ifPresent(executor::sendMessage);
|
||||
}
|
||||
|
||||
case "dump" -> {
|
||||
if (!player.hasPermission(Permission.COMMAND_USER_DATA_DUMP.node)) {
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
if (args.length < 3) {
|
||||
if (optionalUuid.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata dump <username> <version_uuid>")
|
||||
.ifPresent(player::sendMessage);
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean toWeb = args.length > 3 && args[3].equalsIgnoreCase("web");
|
||||
final String username = args[1];
|
||||
// Determine dump type
|
||||
final boolean webDump = parseStringArg(args, 3)
|
||||
.map(arg -> arg.equalsIgnoreCase("web"))
|
||||
.orElse(false);
|
||||
final Optional<DataSnapshot.Packed> data = plugin.getDatabase().getSnapshot(user, optionalUuid.get());
|
||||
if (data.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Dump the data
|
||||
final DataSnapshot.Packed userData = data.get();
|
||||
final DataDumper dumper = DataDumper.create(userData, user, plugin);
|
||||
try {
|
||||
final UUID versionUuid = UUID.fromString(args[2]);
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase()
|
||||
.getUserByName(username.toLowerCase(Locale.ENGLISH))
|
||||
.thenAccept(optionalUser -> optionalUser.ifPresentOrElse(
|
||||
user -> plugin.getDatabase().getUserData(user, versionUuid).thenAccept(
|
||||
optionalUserData -> optionalUserData.ifPresentOrElse(userData -> {
|
||||
try {
|
||||
final DataDumper dumper = DataDumper.create(userData, user, plugin);
|
||||
final String result = toWeb ? dumper.toWeb() : dumper.toFile();
|
||||
plugin.getLocales().getLocale("data_dumped", versionUuid.toString()
|
||||
.split("-")[0], user.username, result)
|
||||
.ifPresent(player::sendMessage);
|
||||
} catch (IOException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to dump user data", e);
|
||||
}
|
||||
}, () -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(player::sendMessage))),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage))));
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata dump <username> <version_uuid>")
|
||||
.ifPresent(player::sendMessage);
|
||||
plugin.getLocales().getLocale("data_dumped", userData.getShortId(), user.getUsername(),
|
||||
(webDump ? dumper.toWeb() : dumper.toFile())).ifPresent(executor::sendMessage);
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "Failed to dump user data", e);
|
||||
}
|
||||
}
|
||||
|
||||
default -> plugin.getLocales().getLocale("error_invalid_syntax", getUsage())
|
||||
.ifPresent(executor::sendMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public List<String> onTabComplete(@NotNull String[] args) {
|
||||
switch (args.length) {
|
||||
case 0, 1 -> {
|
||||
return Arrays.stream(COMMAND_ARGUMENTS)
|
||||
.filter(argument -> argument.startsWith(args.length == 1 ? args[0] : ""))
|
||||
.sorted().collect(Collectors.toList());
|
||||
}
|
||||
case 2 -> {
|
||||
return plugin.getOnlineUsers().stream().map(user -> user.username)
|
||||
.filter(argument -> argument.startsWith(args[1]))
|
||||
.sorted().collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
public List<String> suggest(@NotNull CommandUser executor, @NotNull String[] args) {
|
||||
return switch (args.length) {
|
||||
case 0, 1 -> SUB_COMMANDS.keySet().stream().sorted().toList();
|
||||
case 2 -> plugin.getOnlineUsers().stream().map(User::getUsername).toList();
|
||||
case 4 -> parseStringArg(args, 0)
|
||||
.map(arg -> arg.equalsIgnoreCase("dump") ? List.of("web", "file") : null)
|
||||
.orElse(null);
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ public class Locales {
|
||||
public Map<String, String> rawLocales = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Returns a raw, un-formatted locale loaded from the locales file
|
||||
* Returns a raw, unformatted locale loaded from the Locales file
|
||||
*
|
||||
* @param localeId String identifier of the locale, corresponding to a key in the file
|
||||
* @return An {@link Optional} containing the locale corresponding to the id, if it exists
|
||||
@@ -60,12 +60,12 @@ public class Locales {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a raw, un-formatted locale loaded from the locales file, with replacements applied
|
||||
* Returns a raw, unformatted locale loaded from the Locales file, with replacements applied
|
||||
* <p>
|
||||
* Note that replacements will not be MineDown-escaped; use {@link #escapeMineDown(String)} to escape replacements
|
||||
*
|
||||
* @param localeId String identifier of the locale, corresponding to a key in the file
|
||||
* @param replacements Ordered array of replacement strings to fill in placeholders with
|
||||
* @param replacements An ordered array of replacement strings to fill in placeholders with
|
||||
* @return An {@link Optional} containing the replacement-applied locale corresponding to the id, if it exists
|
||||
*/
|
||||
public Optional<String> getRawLocale(@NotNull String localeId, @NotNull String... replacements) {
|
||||
@@ -73,7 +73,7 @@ public class Locales {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a MineDown-formatted locale from the locales file
|
||||
* Returns a MineDown-formatted locale from the Locales file
|
||||
*
|
||||
* @param localeId String identifier of the locale, corresponding to a key in the file
|
||||
* @return An {@link Optional} containing the formatted locale corresponding to the id, if it exists
|
||||
@@ -83,12 +83,12 @@ public class Locales {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a MineDown-formatted locale from the locales file, with replacements applied
|
||||
* Returns a MineDown-formatted locale from the Locales file, with replacements applied
|
||||
* <p>
|
||||
* Note that replacements will be MineDown-escaped before application
|
||||
*
|
||||
* @param localeId String identifier of the locale, corresponding to a key in the file
|
||||
* @param replacements Ordered array of replacement strings to fill in placeholders with
|
||||
* @param replacements An ordered array of replacement strings to fill in placeholders with
|
||||
* @return An {@link Optional} containing the replacement-applied, formatted locale corresponding to the id, if it exists
|
||||
*/
|
||||
public Optional<MineDown> getLocale(@NotNull String localeId, @NotNull String... replacements) {
|
||||
@@ -100,7 +100,7 @@ public class Locales {
|
||||
* Apply placeholder replacements to a raw locale
|
||||
*
|
||||
* @param rawLocale The raw, unparsed locale
|
||||
* @param replacements Ordered array of replacement strings to fill in placeholders with
|
||||
* @param replacements An ordered array of replacement strings to fill in placeholders with
|
||||
* @return the raw locale, with inserted placeholders
|
||||
*/
|
||||
@NotNull
|
||||
@@ -189,4 +189,25 @@ public class Locales {
|
||||
public Locales() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the slot a system notification should be displayed in
|
||||
*/
|
||||
public enum NotificationSlot {
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,10 @@ package net.william278.husksync.config;
|
||||
import net.william278.annotaml.YamlComment;
|
||||
import net.william278.annotaml.YamlFile;
|
||||
import net.william278.annotaml.YamlKey;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.data.Identifier;
|
||||
import net.william278.husksync.database.Database;
|
||||
import net.william278.husksync.listener.EventListener;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
@@ -36,30 +39,43 @@ import java.util.*;
|
||||
┃ Developed by William278 ┃
|
||||
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
┣╸ Information: https://william278.net/project/husksync
|
||||
┗╸ Documentation: https://william278.net/docs/husksync""",
|
||||
versionField = "config_version", versionNumber = 4)
|
||||
┣╸ Config Help: https://william278.net/docs/husksync/config-file/
|
||||
┗╸ Documentation: https://william278.net/docs/husksync""")
|
||||
public class Settings {
|
||||
|
||||
// Top-level settings
|
||||
@YamlComment("Locale of the default language file to use. Docs: https://william278.net/docs/huskhomes/translations")
|
||||
@YamlKey("language")
|
||||
private String language = "en-gb";
|
||||
|
||||
@YamlComment("Whether to automatically check for plugin updates on startup")
|
||||
@YamlKey("check_for_updates")
|
||||
private boolean checkForUpdates = true;
|
||||
|
||||
@YamlComment("Specify a common ID for grouping servers running HuskSync. "
|
||||
+ "Don't modify this unless you know what you're doing!")
|
||||
@YamlKey("cluster_id")
|
||||
private String clusterId = "";
|
||||
|
||||
@YamlComment("Enable development debug logging")
|
||||
@YamlKey("debug_logging")
|
||||
private boolean debugLogging = false;
|
||||
|
||||
@YamlComment("Whether to provide modern, rich TAB suggestions for commands (if available)")
|
||||
@YamlKey("brigadier_tab_completion")
|
||||
private boolean brigadierTabCompletion = false;
|
||||
|
||||
@YamlComment("Whether to enable the Player Analytics hook. Docs: https://william278.net/docs/husksync/plan-hook")
|
||||
@YamlKey("enable_plan_hook")
|
||||
private boolean enablePlanHook = true;
|
||||
|
||||
|
||||
// Database settings
|
||||
@YamlComment("Type of database to use (MYSQL, MARIADB)")
|
||||
@YamlKey("database.type")
|
||||
private Database.Type databaseType = Database.Type.MYSQL;
|
||||
|
||||
@YamlComment("Database connection settings")
|
||||
|
||||
@YamlComment("Specify credentials here for your MYSQL or MARIADB database")
|
||||
@YamlKey("database.credentials.host")
|
||||
private String mySqlHost = "localhost";
|
||||
|
||||
@@ -76,9 +92,13 @@ public class Settings {
|
||||
private String mySqlPassword = "pa55w0rd";
|
||||
|
||||
@YamlKey("database.credentials.parameters")
|
||||
private String mySqlConnectionParameters = "?autoReconnect=true&useSSL=false";
|
||||
private String mySqlConnectionParameters = "?autoReconnect=true"
|
||||
+ "&useSSL=false"
|
||||
+ "&useUnicode=true"
|
||||
+ "&characterEncoding=UTF-8";
|
||||
|
||||
@YamlComment("MySQL connection pool properties")
|
||||
@YamlComment("MYSQL / MARIADB database Hikari connection pool properties. "
|
||||
+ "Don't modify this unless you know what you're doing!")
|
||||
@YamlKey("database.connection_pool.maximum_pool_size")
|
||||
private int mySqlConnectionPoolSize = 10;
|
||||
|
||||
@@ -94,12 +114,13 @@ public class Settings {
|
||||
@YamlKey("database.connection_pool.connection_timeout")
|
||||
private long mySqlConnectionPoolTimeout = 5000;
|
||||
|
||||
@YamlComment("Names of tables to use on your database. Don't modify this unless you know what you're doing!")
|
||||
@YamlKey("database.table_names")
|
||||
private Map<String, String> tableNames = TableName.getDefaults();
|
||||
|
||||
|
||||
// Redis settings
|
||||
@YamlComment("Redis connection settings")
|
||||
@YamlComment("Specify the credentials of your Redis database here. Set \"password\" to '' if you don't have one")
|
||||
@YamlKey("redis.credentials.host")
|
||||
private String redisHost = "localhost";
|
||||
|
||||
@@ -114,42 +135,75 @@ public class Settings {
|
||||
|
||||
|
||||
// Synchronization settings
|
||||
@YamlComment("Synchronization settings")
|
||||
@YamlComment("The number of data snapshot backups that should be kept at once per user")
|
||||
@YamlKey("synchronization.max_user_data_snapshots")
|
||||
private int maxUserDataSnapshots = 5;
|
||||
private int maxUserDataSnapshots = 16;
|
||||
|
||||
@YamlComment("Number of hours between new snapshots being saved as backups (Use \"0\" to backup all snapshots)")
|
||||
@YamlKey("synchronization.snapshot_backup_frequency")
|
||||
private int snapshotBackupFrequency = 4;
|
||||
|
||||
@YamlComment("List of save cause IDs for which a snapshot will be automatically pinned (so it won't be rotated)."
|
||||
+ " Docs: https://william278.net/docs/husksync/data-rotation#save-causes")
|
||||
@YamlKey("synchronization.auto_pinned_save_causes")
|
||||
private List<String> autoPinnedSaveCauses = List.of(
|
||||
DataSnapshot.SaveCause.INVENTORY_COMMAND.name(),
|
||||
DataSnapshot.SaveCause.ENDERCHEST_COMMAND.name(),
|
||||
DataSnapshot.SaveCause.BACKUP_RESTORE.name(),
|
||||
DataSnapshot.SaveCause.CONVERTED_FROM_V2.name(),
|
||||
DataSnapshot.SaveCause.LEGACY_MIGRATION.name(),
|
||||
DataSnapshot.SaveCause.MPDB_MIGRATION.name()
|
||||
);
|
||||
|
||||
@YamlComment("Whether to create a snapshot for users on a world when the server saves that world")
|
||||
@YamlKey("synchronization.save_on_world_save")
|
||||
private boolean saveOnWorldSave = true;
|
||||
|
||||
@YamlComment("Whether to create a snapshot for users when they die (containing their death drops)")
|
||||
@YamlKey("synchronization.save_on_death")
|
||||
private boolean saveOnDeath = false;
|
||||
|
||||
@YamlComment("Whether to save empty death drops for users when they die")
|
||||
@YamlKey("synchronization.save_empty_drops_on_death")
|
||||
private boolean saveEmptyDropsOnDeath = true;
|
||||
|
||||
@YamlComment("Whether to use the snappy data compression algorithm. Keep on unless you know what you're doing")
|
||||
@YamlKey("synchronization.compress_data")
|
||||
private boolean compressData = true;
|
||||
|
||||
@YamlComment("Where to display sync notifications (ACTION_BAR, CHAT, TOAST or NONE)")
|
||||
@YamlKey("synchronization.notification_display_slot")
|
||||
private NotificationDisplaySlot notificationDisplaySlot = NotificationDisplaySlot.ACTION_BAR;
|
||||
private Locales.NotificationSlot notificationSlot = Locales.NotificationSlot.ACTION_BAR;
|
||||
|
||||
@YamlKey("synchronization.synchronise_dead_players_changing_server")
|
||||
private boolean synchroniseDeadPlayersChangingServer = true;
|
||||
@YamlComment("(Experimental) Persist Cartography Table locked maps to let them be viewed on any server")
|
||||
@YamlKey("synchronization.persist_locked_maps")
|
||||
private boolean persistLockedMaps = false;
|
||||
|
||||
@YamlComment("Whether dead players who log out and log in to a different server should have their items saved. "
|
||||
+ "You may need to modify this if you're using the keepInventory gamerule.")
|
||||
@YamlKey("synchronization.synchronize_dead_players_changing_server")
|
||||
private boolean synchronizeDeadPlayersChangingServer = true;
|
||||
|
||||
@YamlComment("How long, in milliseconds, this server should wait for a response from the redis server before "
|
||||
+ "pulling data from the database instead (i.e., if the user did not change servers).")
|
||||
@YamlKey("synchronization.network_latency_milliseconds")
|
||||
private int networkLatencyMilliseconds = 500;
|
||||
|
||||
@YamlComment("Which data types to synchronize (Docs: https://william278.net/docs/husksync/sync-features)")
|
||||
@YamlKey("synchronization.features")
|
||||
private Map<String, Boolean> synchronizationFeatures = SynchronizationFeature.getDefaults();
|
||||
private Map<String, Boolean> synchronizationFeatures = Identifier.getConfigMap();
|
||||
|
||||
@YamlComment("Commands which should be blocked before a player has finished syncing (Use * to block all commands)")
|
||||
@YamlKey("synchronization.blacklisted_commands_while_locked")
|
||||
private List<String> blacklistedCommandsWhileLocked = new ArrayList<>(List.of("*"));
|
||||
|
||||
@YamlComment("Event priorities for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts")
|
||||
@YamlKey("synchronization.event_priorities")
|
||||
private Map<String, String> synchronizationEventPriorities = EventType.getDefaults();
|
||||
private Map<String, String> syncEventPriorities = EventListener.ListenerType.getDefaults();
|
||||
|
||||
|
||||
// Zero-args constructor for instantiation via Annotaml
|
||||
@SuppressWarnings("unused")
|
||||
public Settings() {
|
||||
}
|
||||
|
||||
@@ -172,6 +226,13 @@ public class Settings {
|
||||
return debugLogging;
|
||||
}
|
||||
|
||||
public boolean doBrigadierTabCompletion() {
|
||||
return brigadierTabCompletion;
|
||||
}
|
||||
|
||||
public boolean usePlanHook() {
|
||||
return enablePlanHook;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Database.Type getDatabaseType() {
|
||||
@@ -246,7 +307,7 @@ public class Settings {
|
||||
return redisPassword;
|
||||
}
|
||||
|
||||
public boolean isRedisUseSsl() {
|
||||
public boolean redisUseSsl() {
|
||||
return redisUseSsl;
|
||||
}
|
||||
|
||||
@@ -254,6 +315,10 @@ public class Settings {
|
||||
return maxUserDataSnapshots;
|
||||
}
|
||||
|
||||
public int getBackupFrequency() {
|
||||
return snapshotBackupFrequency;
|
||||
}
|
||||
|
||||
public boolean doSaveOnWorldSave() {
|
||||
return saveOnWorldSave;
|
||||
}
|
||||
@@ -270,13 +335,21 @@ public class Settings {
|
||||
return compressData;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public NotificationDisplaySlot getNotificationDisplaySlot() {
|
||||
return notificationDisplaySlot;
|
||||
public boolean doAutoPin(@NotNull DataSnapshot.SaveCause cause) {
|
||||
return autoPinnedSaveCauses.contains(cause.name());
|
||||
}
|
||||
|
||||
public boolean isSynchroniseDeadPlayersChangingServer() {
|
||||
return synchroniseDeadPlayersChangingServer;
|
||||
@NotNull
|
||||
public Locales.NotificationSlot getNotificationDisplaySlot() {
|
||||
return notificationSlot;
|
||||
}
|
||||
|
||||
public boolean doPersistLockedMaps() {
|
||||
return persistLockedMaps;
|
||||
}
|
||||
|
||||
public boolean doSynchronizeDeadPlayersChangingServer() {
|
||||
return synchronizeDeadPlayersChangingServer;
|
||||
}
|
||||
|
||||
public int getNetworkLatencyMilliseconds() {
|
||||
@@ -288,8 +361,8 @@ public class Settings {
|
||||
return synchronizationFeatures;
|
||||
}
|
||||
|
||||
public boolean getSynchronizationFeature(@NotNull SynchronizationFeature feature) {
|
||||
return getSynchronizationFeatures().getOrDefault(feature.name().toLowerCase(Locale.ENGLISH), feature.enabledByDefault);
|
||||
public boolean isSyncFeatureEnabled(@NotNull Identifier id) {
|
||||
return id.isCustom() || getSynchronizationFeatures().getOrDefault(id.getKeyValue(), id.isEnabledByDefault());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -298,12 +371,11 @@ public class Settings {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public EventPriority getEventPriority(@NotNull Settings.EventType eventType) {
|
||||
public EventListener.Priority getEventPriority(@NotNull EventListener.ListenerType type) {
|
||||
try {
|
||||
return EventPriority.valueOf(synchronizationEventPriorities.get(eventType.name().toLowerCase(Locale.ENGLISH)));
|
||||
return EventListener.Priority.valueOf(syncEventPriorities.get(type.name().toLowerCase(Locale.ENGLISH)));
|
||||
} catch (IllegalArgumentException e) {
|
||||
e.printStackTrace();
|
||||
return EventPriority.NORMAL;
|
||||
return EventListener.Priority.NORMAL;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,111 +406,4 @@ 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
|
||||
*/
|
||||
public enum SynchronizationFeature {
|
||||
INVENTORIES(true),
|
||||
ENDER_CHESTS(true),
|
||||
HEALTH(true),
|
||||
MAX_HEALTH(true),
|
||||
HUNGER(true),
|
||||
EXPERIENCE(true),
|
||||
POTION_EFFECTS(true),
|
||||
ADVANCEMENTS(true),
|
||||
GAME_MODE(true),
|
||||
STATISTICS(true),
|
||||
PERSISTENT_DATA_CONTAINER(false),
|
||||
LOCKED_MAPS(false),
|
||||
LOCATION(false);
|
||||
|
||||
private final boolean enabledByDefault;
|
||||
|
||||
SynchronizationFeature(boolean enabledByDefault) {
|
||||
this.enabledByDefault = enabledByDefault;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Map.Entry<String, Boolean> toEntry() {
|
||||
return Map.entry(name().toLowerCase(Locale.ENGLISH), enabledByDefault);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@NotNull
|
||||
private static Map<String, Boolean> getDefaults() {
|
||||
return Map.ofEntries(Arrays.stream(values())
|
||||
.map(SynchronizationFeature::toEntry)
|
||||
.toArray(Map.Entry[]::new));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(Locale.ENGLISH), 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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A mapped piece of advancement data
|
||||
*/
|
||||
public class AdvancementData {
|
||||
|
||||
/**
|
||||
* The advancement namespaced key
|
||||
*/
|
||||
@SerializedName("key")
|
||||
public String key;
|
||||
|
||||
/**
|
||||
* A map of completed advancement criteria to when it was completed
|
||||
*/
|
||||
@SerializedName("completed_criteria")
|
||||
public Map<String, Date> completedCriteria;
|
||||
|
||||
public AdvancementData(@NotNull String key, @NotNull Map<String, Date> awardedCriteria) {
|
||||
this.key = key;
|
||||
this.completedCriteria = awardedCriteria;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected AdvancementData() {
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.xerial.snappy.Snappy;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class CompressedDataAdapter extends JsonDataAdapter {
|
||||
|
||||
@Override
|
||||
public byte[] toBytes(@NotNull UserData data) throws DataAdaptionException {
|
||||
try {
|
||||
return Snappy.compress(super.toBytes(data));
|
||||
} catch (IOException e) {
|
||||
throw new DataAdaptionException("Failed to compress data", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull UserData fromBytes(byte[] data) throws DataAdaptionException {
|
||||
try {
|
||||
return super.fromBytes(Snappy.uncompress(data));
|
||||
} catch (IOException e) {
|
||||
throw new DataAdaptionException("Failed to decompress data", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
368
common/src/main/java/net/william278/husksync/data/Data.java
Normal file
368
common/src/main/java/net/william278/husksync/data/Data.java
Normal file
@@ -0,0 +1,368 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A piece of data, held by a {@link DataHolder}
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public interface Data {
|
||||
|
||||
/**
|
||||
* Apply (set) this data container to the given {@link OnlineUser}
|
||||
*
|
||||
* @param user the user to apply this element to
|
||||
* @param plugin the plugin instance
|
||||
*/
|
||||
void apply(@NotNull UserDataHolder user, @NotNull HuskSync plugin);
|
||||
|
||||
/**
|
||||
* A data container holding data for:
|
||||
* <ul>
|
||||
* <li>Inventories</li>
|
||||
* <li>Ender Chests</li>
|
||||
* </ul>
|
||||
*/
|
||||
interface Items extends Data {
|
||||
|
||||
@NotNull
|
||||
Stack[] getStack();
|
||||
|
||||
default int getSlotCount() {
|
||||
return getStack().length;
|
||||
}
|
||||
|
||||
record Stack(@NotNull String material, int amount, @Nullable String name,
|
||||
@Nullable List<String> lore, @NotNull List<String> enchantments) {
|
||||
|
||||
}
|
||||
|
||||
default boolean isEmpty() {
|
||||
return Arrays.stream(getStack()).allMatch(Objects::isNull) || getStack().length == 0;
|
||||
}
|
||||
|
||||
void clear();
|
||||
|
||||
void setContents(@NotNull Items contents);
|
||||
|
||||
/**
|
||||
* A data container holding data for inventories and selected hotbar slot
|
||||
*/
|
||||
interface Inventory extends Items {
|
||||
|
||||
int getHeldItemSlot();
|
||||
|
||||
void setHeldItemSlot(int heldItemSlot) throws IllegalArgumentException;
|
||||
|
||||
default Optional<Stack> getHelmet() {
|
||||
return Optional.ofNullable(getStack()[39]);
|
||||
}
|
||||
|
||||
default Optional<Stack> getChestplate() {
|
||||
return Optional.ofNullable(getStack()[38]);
|
||||
}
|
||||
|
||||
default Optional<Stack> getLeggings() {
|
||||
return Optional.ofNullable(getStack()[37]);
|
||||
}
|
||||
|
||||
default Optional<Stack> getBoots() {
|
||||
return Optional.ofNullable(getStack()[36]);
|
||||
}
|
||||
|
||||
default Optional<Stack> getOffHand() {
|
||||
return Optional.ofNullable(getStack()[40]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data container holding data for ender chests
|
||||
*/
|
||||
interface EnderChest extends Items {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Data container holding data for potion effects
|
||||
*/
|
||||
interface PotionEffects extends Data {
|
||||
|
||||
@NotNull
|
||||
List<Effect> getActiveEffects();
|
||||
|
||||
/**
|
||||
* Represents a potion effect
|
||||
*
|
||||
* @param type the type of potion effect
|
||||
* @param amplifier the amplifier of the potion effect
|
||||
* @param duration the duration of the potion effect
|
||||
* @param isAmbient whether the potion effect is ambient
|
||||
* @param showParticles whether the potion effect shows particles
|
||||
* @param hasIcon whether the potion effect displays a HUD icon
|
||||
*/
|
||||
record Effect(@SerializedName("type") @NotNull String type,
|
||||
@SerializedName("amplifier") int amplifier,
|
||||
@SerializedName("duration") int duration,
|
||||
@SerializedName("is_ambient") boolean isAmbient,
|
||||
@SerializedName("show_particles") boolean showParticles,
|
||||
@SerializedName("has_icon") boolean hasIcon) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Data container holding data for advancements
|
||||
*/
|
||||
interface Advancements extends Data {
|
||||
|
||||
@NotNull
|
||||
List<Advancement> getCompleted();
|
||||
|
||||
@NotNull
|
||||
default List<Advancement> getCompletedExcludingRecipes() {
|
||||
return getCompleted().stream()
|
||||
.filter(advancement -> !advancement.getKey().startsWith("minecraft:recipe"))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
void setCompleted(@NotNull List<Advancement> completed);
|
||||
|
||||
class Advancement {
|
||||
@SerializedName("key")
|
||||
private String key;
|
||||
|
||||
@SerializedName("completed_criteria")
|
||||
private Map<String, Long> completedCriteria;
|
||||
|
||||
private Advancement(@NotNull String key, @NotNull Map<String, Date> completedCriteria) {
|
||||
this.key = key;
|
||||
this.completedCriteria = adaptDateMap(completedCriteria);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private Advancement() {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static Advancement adapt(@NotNull String key, @NotNull Map<String, Date> completedCriteria) {
|
||||
return new Advancement(key, completedCriteria);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Map<String, Long> adaptDateMap(@NotNull Map<String, Date> dateMap) {
|
||||
return dateMap.entrySet().stream()
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getTime()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Map<String, Date> adaptLongMap(@NotNull Map<String, Long> dateMap) {
|
||||
return dateMap.entrySet().stream()
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> new Date(e.getValue())));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setKey(@NotNull String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public Map<String, Date> getCompletedCriteria() {
|
||||
return adaptLongMap(completedCriteria);
|
||||
}
|
||||
|
||||
public void setCompletedCriteria(Map<String, Date> completedCriteria) {
|
||||
this.completedCriteria = adaptDateMap(completedCriteria);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Data container holding data for the player's location
|
||||
*/
|
||||
interface Location extends Data {
|
||||
double getX();
|
||||
|
||||
void setX(double x);
|
||||
|
||||
double getY();
|
||||
|
||||
void setY(double y);
|
||||
|
||||
double getZ();
|
||||
|
||||
void setZ(double z);
|
||||
|
||||
float getYaw();
|
||||
|
||||
void setYaw(float yaw);
|
||||
|
||||
float getPitch();
|
||||
|
||||
void setPitch(float pitch);
|
||||
|
||||
@NotNull
|
||||
World getWorld();
|
||||
|
||||
void setWorld(@NotNull World world);
|
||||
|
||||
record World(
|
||||
@SerializedName("name") @NotNull String name,
|
||||
@SerializedName("uuid") @NotNull UUID uuid,
|
||||
@SerializedName("environment") @NotNull String environment
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data container holding data for statistics
|
||||
*/
|
||||
interface Statistics extends Data {
|
||||
@NotNull
|
||||
Map<String, Integer> getGenericStatistics();
|
||||
|
||||
@NotNull
|
||||
Map<String, Map<String, Integer>> getBlockStatistics();
|
||||
|
||||
@NotNull
|
||||
Map<String, Map<String, Integer>> getItemStatistics();
|
||||
|
||||
@NotNull
|
||||
Map<String, Map<String, Integer>> getEntityStatistics();
|
||||
}
|
||||
|
||||
/**
|
||||
* Data container holding data for persistent data containers
|
||||
*/
|
||||
interface PersistentData extends Data {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A data container holding data for:
|
||||
* <ul>
|
||||
* <li>Health</li>
|
||||
* <li>Max Health</li>
|
||||
* <li>Health Scale</li>
|
||||
* </ul>
|
||||
*/
|
||||
interface Health extends Data {
|
||||
double getHealth();
|
||||
|
||||
void setHealth(double health);
|
||||
|
||||
double getMaxHealth();
|
||||
|
||||
void setMaxHealth(double maxHealth);
|
||||
|
||||
double getHealthScale();
|
||||
|
||||
void setHealthScale(double healthScale);
|
||||
}
|
||||
|
||||
/**
|
||||
* A data container holding data for:
|
||||
* <ul>
|
||||
*
|
||||
* <li>Food Level</li>
|
||||
* <li>Saturation</li>
|
||||
* <li>Exhaustion</li>
|
||||
* </ul>
|
||||
*/
|
||||
interface Hunger extends Data {
|
||||
|
||||
int getFoodLevel();
|
||||
|
||||
void setFoodLevel(int foodLevel);
|
||||
|
||||
float getSaturation();
|
||||
|
||||
void setSaturation(float saturation);
|
||||
|
||||
float getExhaustion();
|
||||
|
||||
void setExhaustion(float exhaustion);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A data container holding data for:
|
||||
* <ul>
|
||||
* <li>Total experience</li>
|
||||
* <li>Experience level</li>
|
||||
* <li>Experience progress</li>
|
||||
* </ul>
|
||||
*/
|
||||
interface Experience extends Data {
|
||||
|
||||
int getTotalExperience();
|
||||
|
||||
void setTotalExperience(int totalExperience);
|
||||
|
||||
int getExpLevel();
|
||||
|
||||
void setExpLevel(int expLevel);
|
||||
|
||||
float getExpProgress();
|
||||
|
||||
void setExpProgress(float expProgress);
|
||||
}
|
||||
|
||||
/**
|
||||
* A data container holding data for:
|
||||
* <ul>
|
||||
* <li>Game mode</li>
|
||||
* <li>Allow flight</li>
|
||||
* <li>Is flying</li>
|
||||
* </ul>
|
||||
*/
|
||||
interface GameMode extends Data {
|
||||
|
||||
@NotNull
|
||||
String getGameMode();
|
||||
|
||||
void setGameMode(@NotNull String gameMode);
|
||||
|
||||
boolean getAllowFlight();
|
||||
|
||||
void setAllowFlight(boolean allowFlight);
|
||||
|
||||
boolean getIsFlying();
|
||||
|
||||
void setIsFlying(boolean isFlying);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* An adapter that adapts {@link UserData} to and from a portable byte array.
|
||||
*/
|
||||
public interface DataAdapter {
|
||||
|
||||
/**
|
||||
* Converts {@link UserData} to a byte array
|
||||
*
|
||||
* @param data The {@link UserData} to adapt
|
||||
* @return The byte array.
|
||||
* @throws DataAdaptionException If an error occurred during adaptation.
|
||||
*/
|
||||
byte[] toBytes(@NotNull UserData data) throws DataAdaptionException;
|
||||
|
||||
/**
|
||||
* Serializes {@link UserData} to a JSON string.
|
||||
*
|
||||
* @param data The {@link UserData} to serialize
|
||||
* @param pretty Whether to pretty print the JSON.
|
||||
* @return The output json string.
|
||||
* @throws DataAdaptionException If an error occurred during adaptation.
|
||||
*/
|
||||
@NotNull
|
||||
String toJson(@NotNull UserData data, boolean pretty) throws DataAdaptionException;
|
||||
|
||||
/**
|
||||
* Converts a byte array to {@link UserData}.
|
||||
*
|
||||
* @param data The byte array to adapt.
|
||||
* @return The {@link UserData}.
|
||||
* @throws DataAdaptionException If an error occurred during adaptation, such as if the byte array is invalid.
|
||||
*/
|
||||
@NotNull
|
||||
UserData fromBytes(final byte[] data) throws DataAdaptionException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public interface DataHolder {
|
||||
|
||||
@NotNull
|
||||
Map<Identifier, Data> getData();
|
||||
|
||||
default Optional<? extends Data> getData(@NotNull Identifier identifier) {
|
||||
return Optional.ofNullable(getData().get(identifier));
|
||||
}
|
||||
|
||||
default void setData(@NotNull Identifier identifier, @NotNull Data data) {
|
||||
getData().put(identifier, data);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Items.Inventory> getInventory() {
|
||||
return getData(Identifier.INVENTORY).map(Data.Items.Inventory.class::cast);
|
||||
}
|
||||
|
||||
default void setInventory(@NotNull Data.Items.Inventory inventory) {
|
||||
setData(Identifier.INVENTORY, inventory);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Items.EnderChest> getEnderChest() {
|
||||
return getData(Identifier.ENDER_CHEST).map(Data.Items.EnderChest.class::cast);
|
||||
}
|
||||
|
||||
default void setEnderChest(@NotNull Data.Items.EnderChest enderChest) {
|
||||
setData(Identifier.ENDER_CHEST, enderChest);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.PotionEffects> getPotionEffects() {
|
||||
return getData(Identifier.POTION_EFFECTS).map(Data.PotionEffects.class::cast);
|
||||
}
|
||||
|
||||
default void setPotionEffects(@NotNull Data.PotionEffects potionEffects) {
|
||||
setData(Identifier.POTION_EFFECTS, potionEffects);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Advancements> getAdvancements() {
|
||||
return getData(Identifier.ADVANCEMENTS).map(Data.Advancements.class::cast);
|
||||
}
|
||||
|
||||
default void setAdvancements(@NotNull Data.Advancements advancements) {
|
||||
setData(Identifier.ADVANCEMENTS, advancements);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Location> getLocation() {
|
||||
return Optional.ofNullable((Data.Location) getData().get(Identifier.LOCATION));
|
||||
}
|
||||
|
||||
default void setLocation(@NotNull Data.Location location) {
|
||||
getData().put(Identifier.LOCATION, location);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Statistics> getStatistics() {
|
||||
return Optional.ofNullable((Data.Statistics) getData().get(Identifier.STATISTICS));
|
||||
}
|
||||
|
||||
default void setStatistics(@NotNull Data.Statistics statistics) {
|
||||
getData().put(Identifier.STATISTICS, statistics);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Health> getHealth() {
|
||||
return Optional.ofNullable((Data.Health) getData().get(Identifier.HEALTH));
|
||||
}
|
||||
|
||||
default void setHealth(@NotNull Data.Health health) {
|
||||
getData().put(Identifier.HEALTH, health);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Hunger> getHunger() {
|
||||
return Optional.ofNullable((Data.Hunger) getData().get(Identifier.HUNGER));
|
||||
}
|
||||
|
||||
default void setHunger(@NotNull Data.Hunger hunger) {
|
||||
getData().put(Identifier.HUNGER, hunger);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Experience> getExperience() {
|
||||
return Optional.ofNullable((Data.Experience) getData().get(Identifier.EXPERIENCE));
|
||||
}
|
||||
|
||||
default void setExperience(@NotNull Data.Experience experience) {
|
||||
getData().put(Identifier.EXPERIENCE, experience);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.GameMode> getGameMode() {
|
||||
return Optional.ofNullable((Data.GameMode) getData().get(Identifier.GAME_MODE));
|
||||
}
|
||||
|
||||
default void setGameMode(@NotNull Data.GameMode gameMode) {
|
||||
getData().put(Identifier.GAME_MODE, gameMode);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.PersistentData> getPersistentData() {
|
||||
return Optional.ofNullable((Data.PersistentData) getData().get(Identifier.PERSISTENT_DATA));
|
||||
}
|
||||
|
||||
default void setPersistentData(@NotNull Data.PersistentData persistentData) {
|
||||
getData().put(Identifier.PERSISTENT_DATA, persistentData);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import net.william278.husksync.api.BaseHuskSyncAPI;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Identifies the cause of a player data save.
|
||||
*
|
||||
* @implNote This enum is saved in the database.
|
||||
* </p>
|
||||
* Cause names have a max length of 32 characters.
|
||||
*/
|
||||
public enum DataSaveCause {
|
||||
|
||||
/**
|
||||
* Indicates data saved when a player disconnected from the server (either to change servers, or to log off)
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
DISCONNECT,
|
||||
/**
|
||||
* Indicates data saved when the world saved
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
WORLD_SAVE,
|
||||
/**
|
||||
* Indicates data saved when the user died
|
||||
*
|
||||
* @since 2.1
|
||||
*/
|
||||
DEATH,
|
||||
/**
|
||||
* Indicates data saved when the server shut down
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
SERVER_SHUTDOWN,
|
||||
/**
|
||||
* Indicates data was saved by editing inventory contents via the {@code /inventory} command
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
INVENTORY_COMMAND,
|
||||
/**
|
||||
* Indicates data was saved by editing Ender Chest contents via the {@code /enderchest} command
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
ENDERCHEST_COMMAND,
|
||||
/**
|
||||
* Indicates data was saved by restoring it from a previous version
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
BACKUP_RESTORE,
|
||||
/**
|
||||
* Indicates data was saved by an API call
|
||||
*
|
||||
* @see BaseHuskSyncAPI#saveUserData(OnlineUser)
|
||||
* @see BaseHuskSyncAPI#setUserData(User, UserData)
|
||||
* @since 2.0
|
||||
*/
|
||||
API,
|
||||
/**
|
||||
* Indicates data was saved from being imported from MySQLPlayerDataBridge
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
MPDB_MIGRATION,
|
||||
/**
|
||||
* Indicates data was saved from being imported from a legacy version (v1.x)
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
LEGACY_MIGRATION,
|
||||
/**
|
||||
* Indicates data was saved by an unknown cause.
|
||||
* </p>
|
||||
* This should not be used and is only used for error handling purposes.
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
UNKNOWN;
|
||||
|
||||
/**
|
||||
* Returns a {@link DataSaveCause} by name.
|
||||
*
|
||||
* @return the {@link DataSaveCause} or {@link #UNKNOWN} if the name is not valid.
|
||||
*/
|
||||
@NotNull
|
||||
public static DataSaveCause getCauseByName(@NotNull String name) {
|
||||
for (DataSaveCause cause : values()) {
|
||||
if (cause.name().equalsIgnoreCase(name)) {
|
||||
return cause;
|
||||
}
|
||||
}
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getDisplayName() {
|
||||
return Locales.truncate(name().toLowerCase(Locale.ENGLISH), 10);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,819 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.Expose;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import net.william278.desertwell.util.Version;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.Adaptable;
|
||||
import net.william278.husksync.adapter.DataAdapter;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A snapshot of a {@link DataHolder} at a given time.
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
public class DataSnapshot {
|
||||
|
||||
/*
|
||||
* Current version of the snapshot data format.
|
||||
* HuskSync v3.0 uses v4; HuskSync v2.0 uses v3. HuskSync v1.0 uses v1 or v2
|
||||
*/
|
||||
protected static final int CURRENT_FORMAT_VERSION = 4;
|
||||
|
||||
@SerializedName("id")
|
||||
protected UUID id;
|
||||
|
||||
@SerializedName("pinned")
|
||||
protected boolean pinned;
|
||||
|
||||
@SerializedName("timestamp")
|
||||
protected OffsetDateTime timestamp;
|
||||
|
||||
@SerializedName("save_cause")
|
||||
protected SaveCause saveCause;
|
||||
|
||||
@SerializedName("minecraft_version")
|
||||
protected String minecraftVersion;
|
||||
|
||||
@SerializedName("platform_type")
|
||||
protected String platformType;
|
||||
|
||||
@SerializedName("format_version")
|
||||
protected int formatVersion;
|
||||
|
||||
@SerializedName("data")
|
||||
protected Map<String, String> data;
|
||||
|
||||
private DataSnapshot(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull SaveCause saveCause, @NotNull Map<String, String> data,
|
||||
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion) {
|
||||
this.id = id;
|
||||
this.pinned = pinned;
|
||||
this.timestamp = timestamp;
|
||||
this.saveCause = saveCause;
|
||||
this.data = data;
|
||||
this.minecraftVersion = minecraftVersion.toStringWithoutMetadata();
|
||||
this.platformType = platformType;
|
||||
this.formatVersion = formatVersion;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private DataSnapshot() {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
public static DataSnapshot.Builder builder(@NotNull HuskSync plugin) {
|
||||
return new Builder(plugin);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
public static DataSnapshot.Packed deserialize(@NotNull HuskSync plugin, byte[] data) throws IllegalStateException {
|
||||
final DataSnapshot.Packed snapshot = plugin.getDataAdapter().fromBytes(data, DataSnapshot.Packed.class);
|
||||
if (snapshot.getMinecraftVersion().compareTo(plugin.getMinecraftVersion()) > 0) {
|
||||
throw new IllegalStateException(String.format("Cannot set data for user because the Minecraft version of " +
|
||||
"their user data (%s) is newer than the server's Minecraft version (%s)." +
|
||||
"Please ensure each server is running the same version of Minecraft.",
|
||||
snapshot.getMinecraftVersion(), plugin.getMinecraftVersion()));
|
||||
}
|
||||
if (snapshot.getFormatVersion() > CURRENT_FORMAT_VERSION) {
|
||||
throw new IllegalStateException(String.format("Cannot set data for user because the format version of " +
|
||||
"their user data (%s) is newer than the current format version (%s). " +
|
||||
"Please ensure each server is running the latest version of HuskSync.",
|
||||
snapshot.getFormatVersion(), CURRENT_FORMAT_VERSION));
|
||||
}
|
||||
if (snapshot.getFormatVersion() < CURRENT_FORMAT_VERSION) {
|
||||
if (plugin.getLegacyConverter().isPresent()) {
|
||||
return plugin.getLegacyConverter().get().convert(data);
|
||||
}
|
||||
throw new IllegalStateException(String.format(
|
||||
"No legacy converter to convert format version: %s", snapshot.getFormatVersion()
|
||||
));
|
||||
}
|
||||
if (!snapshot.getPlatformType().equalsIgnoreCase(plugin.getPlatformType())) {
|
||||
throw new IllegalStateException(String.format("Cannot set data for user because the platform type of " +
|
||||
"their user data (%s) is different to the server platform type (%s). " +
|
||||
"Please ensure each server is running the same platform type.",
|
||||
snapshot.getPlatformType(), plugin.getPlatformType()));
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the ID of the snapshot
|
||||
*
|
||||
* @return The snapshot ID
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the short display ID of the snapshot
|
||||
*
|
||||
* @return The short display ID
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public String getShortId() {
|
||||
return id.toString().substring(0, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the snapshot is pinned
|
||||
*
|
||||
* @return Whether the snapshot is pinned
|
||||
* @since 3.0
|
||||
*/
|
||||
public boolean isPinned() {
|
||||
return pinned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the snapshot is pinned
|
||||
*
|
||||
* @param pinned Whether the snapshot is pinned
|
||||
* @since 3.0
|
||||
*/
|
||||
public void setPinned(boolean pinned) {
|
||||
this.pinned = pinned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get why the snapshot was created
|
||||
*
|
||||
* @return The {@link SaveCause data save cause} of the snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public SaveCause getSaveCause() {
|
||||
return saveCause;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set why the snapshot was created
|
||||
*
|
||||
* @param saveCause The {@link SaveCause data save cause} of the snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
public void setSaveCause(SaveCause saveCause) {
|
||||
this.saveCause = saveCause;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get when the snapshot was created
|
||||
*
|
||||
* @return The {@link OffsetDateTime timestamp} of the snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public OffsetDateTime getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Minecraft version of the server when the Snapshot was created
|
||||
*
|
||||
* @return The Minecraft version of the server when the Snapshot was created
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Version getMinecraftVersion() {
|
||||
return Version.fromString(minecraftVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the platform type of the server when the Snapshot was created
|
||||
*
|
||||
* @return The platform type of the server when the Snapshot was created (e.g. {@code "bukkit"})
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public String getPlatformType() {
|
||||
return platformType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the format version of the snapshot (indicating the version of HuskSync that created it)
|
||||
* <ul>
|
||||
* <li>1: HuskSync v1.0+</li>
|
||||
* <li>2: HuskSync v1.5+</li>
|
||||
* <li>3: HuskSync v2.0+</li>
|
||||
* <li>4: HuskSync v3.0+</li>
|
||||
* </ul>
|
||||
*
|
||||
* @return The format version of the snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
public int getFormatVersion() {
|
||||
return formatVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* A packed {@link DataSnapshot} that has not been deserialized.
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
public static class Packed extends DataSnapshot implements Adaptable {
|
||||
|
||||
protected Packed(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull SaveCause saveCause, @NotNull Map<String, String> data,
|
||||
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion) {
|
||||
super(id, pinned, timestamp, saveCause, data, minecraftVersion, platformType, formatVersion);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private Packed() {
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public void edit(@NotNull HuskSync plugin, @NotNull Consumer<Unpacked> editor) {
|
||||
final Unpacked data = unpack(plugin);
|
||||
editor.accept(data);
|
||||
this.pinned = data.isPinned();
|
||||
this.saveCause = data.getSaveCause();
|
||||
this.data = data.serializeData(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a copy of this snapshot at the current system timestamp with a new ID
|
||||
*
|
||||
* @return The copied snapshot (with a new ID, with a timestamp of the current system time)
|
||||
*/
|
||||
@NotNull
|
||||
public Packed copy() {
|
||||
return new Packed(
|
||||
UUID.randomUUID(), pinned, OffsetDateTime.now(), saveCause, data,
|
||||
getMinecraftVersion(), platformType, formatVersion
|
||||
);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
public byte[] asBytes(@NotNull HuskSync plugin) throws DataAdapter.AdaptionException {
|
||||
return plugin.getDataAdapter().toBytes(this);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
public String asJson(@NotNull HuskSync plugin) throws DataAdapter.AdaptionException {
|
||||
return plugin.getDataAdapter().toJson(this);
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public int getFileSize(@NotNull HuskSync plugin) {
|
||||
return asBytes(plugin).length;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public DataSnapshot.Unpacked unpack(@NotNull HuskSync plugin) {
|
||||
return new Unpacked(
|
||||
id, pinned, timestamp, saveCause, data,
|
||||
getMinecraftVersion(), platformType, formatVersion, plugin
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* An unpacked {@link DataSnapshot}.
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
public static class Unpacked extends DataSnapshot implements DataHolder {
|
||||
|
||||
@Expose(serialize = false, deserialize = false)
|
||||
private final Map<Identifier, Data> deserialized;
|
||||
|
||||
private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull SaveCause saveCause, @NotNull Map<String, String> data,
|
||||
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion,
|
||||
@NotNull HuskSync plugin) {
|
||||
super(id, pinned, timestamp, saveCause, data, minecraftVersion, platformType, formatVersion);
|
||||
this.deserialized = deserializeData(plugin);
|
||||
}
|
||||
|
||||
private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull SaveCause saveCause, @NotNull Map<Identifier, Data> data,
|
||||
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion) {
|
||||
super(id, pinned, timestamp, saveCause, Map.of(), minecraftVersion, platformType, formatVersion);
|
||||
this.deserialized = data;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
private Map<Identifier, Data> deserializeData(@NotNull HuskSync plugin) {
|
||||
return data.entrySet().stream()
|
||||
.map((entry) -> plugin.getIdentifier(entry.getKey()).map(id -> Map.entry(
|
||||
id, plugin.getSerializers().get(id).deserialize(entry.getValue())
|
||||
)).orElse(null))
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
private Map<String, String> serializeData(@NotNull HuskSync plugin) {
|
||||
return deserialized.entrySet().stream()
|
||||
.map((entry) -> Map.entry(entry.getKey().toString(),
|
||||
Objects.requireNonNull(
|
||||
plugin.getSerializers().get(entry.getKey()),
|
||||
String.format("No serializer found for %s", entry.getKey())
|
||||
).serialize(entry.getValue())))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data the snapshot is holding
|
||||
*
|
||||
* @return The data map
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Map<Identifier, Data> getData() {
|
||||
return deserialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack the {@link DataSnapshot} into a {@link DataSnapshot.Packed packed} snapshot
|
||||
*
|
||||
* @param plugin The HuskSync plugin instance
|
||||
* @return The packed snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
public DataSnapshot.Packed pack(@NotNull HuskSync plugin) {
|
||||
return new DataSnapshot.Packed(
|
||||
id, pinned, timestamp, saveCause, serializeData(plugin),
|
||||
getMinecraftVersion(), platformType, formatVersion
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link DataSnapshot}s.
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public static class Builder {
|
||||
|
||||
private final HuskSync plugin;
|
||||
private SaveCause saveCause;
|
||||
private boolean pinned;
|
||||
private OffsetDateTime timestamp;
|
||||
private final Map<Identifier, Data> data;
|
||||
|
||||
private Builder(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
this.pinned = false;
|
||||
this.data = new HashMap<>();
|
||||
this.timestamp = OffsetDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cause of the data save
|
||||
*
|
||||
* @param saveCause The cause of the data save
|
||||
* @return The builder
|
||||
* @apiNote If the {@link SaveCause data save cause} specified is configured to auto-pin, then the value of
|
||||
* {@link #pinned(boolean)} will be ignored
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder saveCause(@NotNull SaveCause saveCause) {
|
||||
this.saveCause = saveCause;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the data should be pinned
|
||||
*
|
||||
* @param pinned Whether the data should be pinned
|
||||
* @return The builder
|
||||
* @apiNote If the {@link SaveCause data save cause} specified is configured to auto-pin, this will be ignored
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder pinned(boolean pinned) {
|
||||
this.pinned = pinned;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the timestamp of the snapshot.
|
||||
* By default, this is the current server time.
|
||||
* The timestamp passed to this method cannot be in the future.
|
||||
* <p>
|
||||
* Note that this will affect the rotation of data snapshots in the database if unpinned,
|
||||
* as well as the order snapshots appear in the list.
|
||||
*
|
||||
* @param timestamp The timestamp
|
||||
* @return The builder
|
||||
* @throws IllegalArgumentException if the timestamp is in the future
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder timestamp(@NotNull OffsetDateTime timestamp) {
|
||||
if (timestamp.isAfter(OffsetDateTime.now())) {
|
||||
throw new IllegalArgumentException("Data snapshots cannot have a timestamp set in the future");
|
||||
}
|
||||
this.timestamp = timestamp;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the data for a given identifier
|
||||
*
|
||||
* @param identifier The identifier
|
||||
* @param data The data
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder data(@NotNull Identifier identifier, @NotNull Data data) {
|
||||
this.data.put(identifier, data);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a map of data to the snapshot
|
||||
*
|
||||
* @param data The data
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder data(@NotNull Map<Identifier, Data> data) {
|
||||
this.data.putAll(data);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the inventory contents of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.INVENTORY, inventory)}
|
||||
* </p>
|
||||
*
|
||||
* @param inventory The inventory contents
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder inventory(@NotNull Data.Items.Inventory inventory) {
|
||||
return data(Identifier.INVENTORY, inventory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Ender Chest contents of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.ENDER_CHEST, inventory)}
|
||||
* </p>
|
||||
*
|
||||
* @param enderChest The Ender Chest contents
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder enderChest(@NotNull Data.Items.EnderChest enderChest) {
|
||||
return data(Identifier.ENDER_CHEST, enderChest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the potion effects of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.POTION_EFFECTS, potionEffects)}
|
||||
* </p>
|
||||
*
|
||||
* @param potionEffects The potion effects
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder potionEffects(@NotNull Data.PotionEffects potionEffects) {
|
||||
return data(Identifier.POTION_EFFECTS, potionEffects);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the advancements of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.ADVANCEMENTS, advancements)}
|
||||
* </p>
|
||||
*
|
||||
* @param advancements The advancements
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder advancements(@NotNull Data.Advancements advancements) {
|
||||
return data(Identifier.ADVANCEMENTS, advancements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the location of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.LOCATION, location)}
|
||||
* </p>
|
||||
*
|
||||
* @param location The location
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder location(@NotNull Data.Location location) {
|
||||
return data(Identifier.LOCATION, location);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the statistics of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.STATISTICS, statistics)}
|
||||
* </p>
|
||||
*
|
||||
* @param statistics The statistics
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder statistics(@NotNull Data.Statistics statistics) {
|
||||
return data(Identifier.STATISTICS, statistics);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the health of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.HEALTH, health)}
|
||||
* </p>
|
||||
*
|
||||
* @param health The health
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder health(@NotNull Data.Health health) {
|
||||
return data(Identifier.HEALTH, health);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the hunger of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.HUNGER, hunger)}
|
||||
* </p>
|
||||
*
|
||||
* @param hunger The hunger
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder hunger(@NotNull Data.Hunger hunger) {
|
||||
return data(Identifier.HUNGER, hunger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the experience of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.EXPERIENCE, experience)}
|
||||
* </p>
|
||||
*
|
||||
* @param experience The experience
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder experience(@NotNull Data.Experience experience) {
|
||||
return data(Identifier.EXPERIENCE, experience);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the game mode of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.GAME_MODE, gameMode)}
|
||||
* </p>
|
||||
*
|
||||
* @param gameMode The game mode
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder gameMode(@NotNull Data.GameMode gameMode) {
|
||||
return data(Identifier.GAME_MODE, gameMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the persistent data container of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.PERSISTENT_DATA, persistentData)}
|
||||
* </p>
|
||||
*
|
||||
* @param persistentData The persistent data container data
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder persistentData(@NotNull Data.PersistentData persistentData) {
|
||||
return data(Identifier.PERSISTENT_DATA, persistentData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the {@link DataSnapshot}
|
||||
*
|
||||
* @return The {@link DataSnapshot.Unpacked snapshot}
|
||||
* @throws IllegalStateException If no save cause is specified
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public DataSnapshot.Unpacked build() throws IllegalStateException {
|
||||
if (saveCause == null) {
|
||||
throw new IllegalStateException("Cannot build DataSnapshot without a save cause");
|
||||
}
|
||||
return new Unpacked(
|
||||
UUID.randomUUID(),
|
||||
pinned || plugin.getSettings().doAutoPin(saveCause),
|
||||
timestamp,
|
||||
saveCause,
|
||||
data,
|
||||
plugin.getMinecraftVersion(),
|
||||
plugin.getPlatformType(),
|
||||
DataSnapshot.CURRENT_FORMAT_VERSION
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build and pack the {@link DataSnapshot}
|
||||
*
|
||||
* @return The {@link DataSnapshot.Packed snapshot}
|
||||
* @throws IllegalStateException If no save cause is specified
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public DataSnapshot.Packed buildAndPack() throws IllegalStateException {
|
||||
return build().pack(plugin);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the cause of a player data save.
|
||||
*
|
||||
* @implNote This enum is saved in the database.
|
||||
* </p>
|
||||
* Cause names have a max length of 32 characters.
|
||||
*/
|
||||
public enum SaveCause {
|
||||
|
||||
/**
|
||||
* Indicates data saved when a player disconnected from the server (either to change servers, or to log off)
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
DISCONNECT,
|
||||
/**
|
||||
* Indicates data saved when the world saved
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
WORLD_SAVE,
|
||||
/**
|
||||
* Indicates data saved when the user died
|
||||
*
|
||||
* @since 2.1
|
||||
*/
|
||||
DEATH,
|
||||
/**
|
||||
* Indicates data saved when the server shut down
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
SERVER_SHUTDOWN,
|
||||
/**
|
||||
* Indicates data was saved by editing inventory contents via the {@code /inventory} command
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
INVENTORY_COMMAND,
|
||||
/**
|
||||
* Indicates data was saved by editing Ender Chest contents via the {@code /enderchest} command
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
ENDERCHEST_COMMAND,
|
||||
/**
|
||||
* Indicates data was saved by restoring it from a previous version
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
BACKUP_RESTORE,
|
||||
/**
|
||||
* Indicates data was saved by an API call
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
API,
|
||||
/**
|
||||
* Indicates data was saved from being imported from MySQLPlayerDataBridge
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
MPDB_MIGRATION,
|
||||
/**
|
||||
* Indicates data was saved from being imported from a legacy version (v1.x -> v2.x)
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
LEGACY_MIGRATION,
|
||||
/**
|
||||
* Indicates data was saved from being imported from a legacy version (v2.x -> v3.x)
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
CONVERTED_FROM_V2;
|
||||
|
||||
@NotNull
|
||||
public String getDisplayName() {
|
||||
return Locales.truncate(name().toLowerCase(Locale.ENGLISH)
|
||||
.replaceAll("_", " "), 18);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the cause of a player having their data updated.
|
||||
*/
|
||||
public enum UpdateCause {
|
||||
/**
|
||||
* Indicates the data was updated by a synchronization process
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
SYNCHRONIZED("synchronization_complete", "synchronization_failed"),
|
||||
/**
|
||||
* Indicates the data was updated by a user joining the server
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
NEW_USER("user_registration_complete", null),
|
||||
/**
|
||||
* Indicates the data was updated by a data update process (management command, API, etc.)
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
UPDATED("data_update_complete", "data_update_failed");
|
||||
|
||||
private final String completedLocale;
|
||||
private final String failureLocale;
|
||||
|
||||
UpdateCause(@Nullable String completedLocale, @Nullable String failureLocale) {
|
||||
this.completedLocale = completedLocale;
|
||||
this.failureLocale = failureLocale;
|
||||
}
|
||||
|
||||
public Optional<MineDown> getCompletedLocale(@NotNull HuskSync plugin) {
|
||||
if (completedLocale != null) {
|
||||
return plugin.getLocales().getLocale(completedLocale);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public Optional<MineDown> getFailedLocale(@NotNull HuskSync plugin) {
|
||||
if (failureLocale != null) {
|
||||
return plugin.getLocales().getLocale(failureLocale);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import net.kyori.adventure.key.InvalidKeyException;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import org.intellij.lang.annotations.Subst;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Identifiers of different types of {@link Data}s
|
||||
*/
|
||||
public class Identifier {
|
||||
|
||||
public static Identifier INVENTORY = huskSync("inventory", true);
|
||||
public static Identifier ENDER_CHEST = huskSync("ender_chest", true);
|
||||
public static Identifier POTION_EFFECTS = huskSync("potion_effects", true);
|
||||
public static Identifier ADVANCEMENTS = huskSync("advancements", true);
|
||||
public static Identifier LOCATION = huskSync("location", false);
|
||||
public static Identifier STATISTICS = huskSync("statistics", true);
|
||||
public static Identifier HEALTH = huskSync("health", true);
|
||||
public static Identifier HUNGER = huskSync("hunger", true);
|
||||
public static Identifier EXPERIENCE = huskSync("experience", true);
|
||||
public static Identifier GAME_MODE = huskSync("game_mode", true);
|
||||
public static Identifier PERSISTENT_DATA = huskSync("persistent_data", true);
|
||||
|
||||
private final Key key;
|
||||
private final boolean configDefault;
|
||||
|
||||
private Identifier(@NotNull Key key, boolean configDefault) {
|
||||
this.key = key;
|
||||
this.configDefault = configDefault;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an identifier from a {@link Key}
|
||||
*
|
||||
* @param key the key
|
||||
* @return the identifier
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public static Identifier from(@NotNull Key key) {
|
||||
if (key.namespace().equals("husksync")) {
|
||||
throw new IllegalArgumentException("You cannot register a key with \"husksync\" as the namespace!");
|
||||
}
|
||||
return new Identifier(key, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an identifier from a namespace and value
|
||||
*
|
||||
* @param plugin the namespace
|
||||
* @param name the value
|
||||
* @return the identifier
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public static Identifier from(@Subst("plugin") @NotNull String plugin, @Subst("null") @NotNull String name) {
|
||||
return from(Key.key(plugin, name));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Identifier huskSync(@Subst("null") @NotNull String name,
|
||||
boolean configDefault) throws InvalidKeyException {
|
||||
return new Identifier(Key.key("husksync", name), configDefault);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@SuppressWarnings("unused")
|
||||
private static Identifier parse(@NotNull String key) throws InvalidKeyException {
|
||||
return huskSync(key, true);
|
||||
}
|
||||
|
||||
public boolean isEnabledByDefault() {
|
||||
return configDefault;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Map.Entry<String, Boolean> getConfigEntry() {
|
||||
return Map.entry(getKeyValue(), configDefault);
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Get a map of the default config entries for all HuskSync identifiers
|
||||
*
|
||||
* @return a map of all the config entries
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Map<String, Boolean> getConfigMap() {
|
||||
return Map.ofEntries(Stream.of(
|
||||
INVENTORY, ENDER_CHEST, POTION_EFFECTS, ADVANCEMENTS, LOCATION,
|
||||
STATISTICS, HEALTH, HUNGER, EXPERIENCE, GAME_MODE, PERSISTENT_DATA
|
||||
)
|
||||
.map(Identifier::getConfigEntry)
|
||||
.toArray(Map.Entry[]::new));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the namespace of the identifier
|
||||
*
|
||||
* @return the namespace
|
||||
*/
|
||||
@NotNull
|
||||
public String getKeyNamespace() {
|
||||
return key.namespace();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the identifier
|
||||
*
|
||||
* @return the value
|
||||
*/
|
||||
@NotNull
|
||||
public String getKeyValue() {
|
||||
return key.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the identifier is a custom (non-HuskSync) identifier
|
||||
*
|
||||
* @return {@code false} if {@link #getKeyNamespace()} returns "husksync"; {@code true} otherwise
|
||||
*/
|
||||
public boolean isCustom() {
|
||||
return !getKeyNamespace().equals("husksync");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the identifier as a string (the key)
|
||||
*
|
||||
* @return the identifier as a string
|
||||
*/
|
||||
@NotNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return key.asString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the given object is an identifier with the same key as this identifier
|
||||
*
|
||||
* @param obj the object to compare
|
||||
* @return {@code true} if the given object is an identifier with the same key as this identifier
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Identifier other) {
|
||||
return key.equals(other.key);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Stores information about the contents of a player's inventory or Ender Chest.
|
||||
*/
|
||||
public class ItemData {
|
||||
|
||||
/**
|
||||
* A Base-64 string of platform-serialized items
|
||||
*/
|
||||
@SerializedName("serialized_items")
|
||||
public String serializedItems;
|
||||
|
||||
/**
|
||||
* Get an empty item data object, representing an empty inventory or Ender Chest
|
||||
*
|
||||
* @return an empty item data object
|
||||
*/
|
||||
@NotNull
|
||||
public static ItemData empty() {
|
||||
return new ItemData("");
|
||||
}
|
||||
|
||||
public ItemData(@NotNull final String serializedItems) {
|
||||
this.serializedItems = serializedItems;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected ItemData() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the item data is empty
|
||||
*
|
||||
* @return {@code true} if the item data is empty; {@code false} otherwise
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return serializedItems.isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class JsonDataAdapter implements DataAdapter {
|
||||
|
||||
@Override
|
||||
public byte[] toBytes(@NotNull UserData data) throws DataAdaptionException {
|
||||
return toJson(data, false).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toJson(@NotNull UserData data, boolean pretty) throws DataAdaptionException {
|
||||
return (pretty ? new GsonBuilder().setPrettyPrinting() : new GsonBuilder()).create().toJson(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull UserData fromBytes(byte[] data) throws DataAdaptionException {
|
||||
try {
|
||||
return new GsonBuilder().create().fromJson(new String(data, StandardCharsets.UTF_8), UserData.class);
|
||||
} catch (JsonSyntaxException e) {
|
||||
throw new DataAdaptionException("Failed to parse JSON data", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Stores information about a player's location
|
||||
*/
|
||||
public class LocationData {
|
||||
|
||||
/**
|
||||
* Name of the world on the server
|
||||
*/
|
||||
@SerializedName("world_name")
|
||||
public String worldName;
|
||||
/**
|
||||
* Unique id of the world
|
||||
*/
|
||||
@SerializedName("world_uuid")
|
||||
public UUID worldUuid;
|
||||
/**
|
||||
* The environment type of the world (one of "NORMAL", "NETHER", "THE_END")
|
||||
*/
|
||||
@SerializedName("world_environment")
|
||||
public String worldEnvironment;
|
||||
|
||||
/**
|
||||
* The x coordinate of the location
|
||||
*/
|
||||
@SerializedName("x")
|
||||
public double x;
|
||||
/**
|
||||
* The y coordinate of the location
|
||||
*/
|
||||
@SerializedName("y")
|
||||
public double y;
|
||||
/**
|
||||
* The z coordinate of the location
|
||||
*/
|
||||
@SerializedName("z")
|
||||
public double z;
|
||||
|
||||
/**
|
||||
* The location's facing yaw angle
|
||||
*/
|
||||
@SerializedName("yaw")
|
||||
public float yaw;
|
||||
/**
|
||||
* The location's facing pitch angle
|
||||
*/
|
||||
@SerializedName("pitch")
|
||||
public float pitch;
|
||||
|
||||
public LocationData(@NotNull String worldName, @NotNull UUID worldUuid,
|
||||
@NotNull String worldEnvironment,
|
||||
double x, double y, double z,
|
||||
float yaw, float pitch) {
|
||||
this.worldName = worldName;
|
||||
this.worldUuid = worldUuid;
|
||||
this.worldEnvironment = worldEnvironment;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.yaw = yaw;
|
||||
this.pitch = pitch;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected LocationData() {
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Store's a user's persistent data container, holding a map of plugin-set persistent values
|
||||
*/
|
||||
public class PersistentDataContainerData {
|
||||
|
||||
/**
|
||||
* Map of namespaced key strings to a byte array representing the persistent data
|
||||
*/
|
||||
@SerializedName("persistent_data_map")
|
||||
protected Map<String, PersistentDataTag<?>> persistentDataMap;
|
||||
|
||||
public PersistentDataContainerData(@NotNull Map<String, PersistentDataTag<?>> persistentDataMap) {
|
||||
this.persistentDataMap = persistentDataMap;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected PersistentDataContainerData() {
|
||||
}
|
||||
|
||||
public <T> Optional<T> getTagValue(@NotNull String tagName, @NotNull Class<T> tagClass) {
|
||||
if (!persistentDataMap.containsKey(tagName)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// If the tag cannot be cast to the specified class, return an empty optional
|
||||
final boolean canCast = tagClass.isAssignableFrom(persistentDataMap.get(tagName).value.getClass());
|
||||
if (!canCast) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(tagClass.cast(persistentDataMap.get(tagName).value));
|
||||
}
|
||||
|
||||
public Optional<PersistentDataTagType> getTagType(@NotNull String tagType) {
|
||||
if (persistentDataMap.containsKey(tagType)) {
|
||||
return PersistentDataTagType.getDataType(persistentDataMap.get(tagType).type);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public Set<String> getTags() {
|
||||
return persistentDataMap.keySet();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Represents a persistent data tag set by a plugin.
|
||||
*/
|
||||
public class PersistentDataTag<T> {
|
||||
|
||||
/**
|
||||
* The enumerated primitive data type name value of the tag
|
||||
*/
|
||||
protected String type;
|
||||
|
||||
/**
|
||||
* The value of the tag
|
||||
*/
|
||||
public T value;
|
||||
|
||||
public PersistentDataTag(@NotNull PersistentDataTagType type, @NotNull T value) {
|
||||
this.type = type.name();
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private PersistentDataTag() {
|
||||
}
|
||||
|
||||
public Optional<PersistentDataTagType> getType() {
|
||||
return PersistentDataTagType.getDataType(type);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,34 +21,24 @@ package net.william278.husksync.data;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
public interface Serializer<T extends Data> {
|
||||
|
||||
/**
|
||||
* Represents the type of a {@link PersistentDataTag}
|
||||
*/
|
||||
public enum PersistentDataTagType {
|
||||
T deserialize(@NotNull String serialized) throws DeserializationException;
|
||||
|
||||
BYTE,
|
||||
SHORT,
|
||||
INTEGER,
|
||||
LONG,
|
||||
FLOAT,
|
||||
DOUBLE,
|
||||
STRING,
|
||||
BYTE_ARRAY,
|
||||
INTEGER_ARRAY,
|
||||
LONG_ARRAY,
|
||||
TAG_CONTAINER_ARRAY,
|
||||
TAG_CONTAINER;
|
||||
@NotNull
|
||||
String serialize(@NotNull T element) throws SerializationException;
|
||||
|
||||
|
||||
public static Optional<PersistentDataTagType> getDataType(@NotNull String typeName) {
|
||||
for (PersistentDataTagType type : values()) {
|
||||
if (type.name().equalsIgnoreCase(typeName)) {
|
||||
return Optional.of(type);
|
||||
}
|
||||
static final class DeserializationException extends IllegalStateException {
|
||||
DeserializationException(@NotNull String message, @NotNull Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
static final class SerializationException extends IllegalStateException {
|
||||
SerializationException(@NotNull String message, @NotNull Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Stores information about a player's statistics
|
||||
*/
|
||||
public class StatisticsData {
|
||||
|
||||
/**
|
||||
* Map of generic statistic names to their values
|
||||
*/
|
||||
@SerializedName("untyped_statistics")
|
||||
public Map<String, Integer> untypedStatistics;
|
||||
|
||||
/**
|
||||
* Map of block type statistics to a map of material types to values
|
||||
*/
|
||||
@SerializedName("block_statistics")
|
||||
public Map<String, Map<String, Integer>> blockStatistics;
|
||||
|
||||
/**
|
||||
* Map of item type statistics to a map of material types to values
|
||||
*/
|
||||
@SerializedName("item_statistics")
|
||||
public Map<String, Map<String, Integer>> itemStatistics;
|
||||
|
||||
/**
|
||||
* Map of entity type statistics to a map of entity types to values
|
||||
*/
|
||||
@SerializedName("entity_statistics")
|
||||
public Map<String, Map<String, Integer>> entityStatistics;
|
||||
|
||||
public StatisticsData(@NotNull Map<String, Integer> untypedStatistics,
|
||||
@NotNull Map<String, Map<String, Integer>> blockStatistics,
|
||||
@NotNull Map<String, Map<String, Integer>> itemStatistics,
|
||||
@NotNull Map<String, Map<String, Integer>> entityStatistics) {
|
||||
this.untypedStatistics = untypedStatistics;
|
||||
this.blockStatistics = blockStatistics;
|
||||
this.itemStatistics = itemStatistics;
|
||||
this.entityStatistics = entityStatistics;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected StatisticsData() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Stores status information about a player
|
||||
*/
|
||||
public class StatusData {
|
||||
|
||||
/**
|
||||
* The player's health points
|
||||
*/
|
||||
@SerializedName("health")
|
||||
public double health;
|
||||
|
||||
/**
|
||||
* The player's maximum health points
|
||||
*/
|
||||
@SerializedName("max_health")
|
||||
public double maxHealth;
|
||||
|
||||
/**
|
||||
* The player's health scaling factor
|
||||
*/
|
||||
@SerializedName("health_scale")
|
||||
public double healthScale;
|
||||
|
||||
/**
|
||||
* The player's hunger points
|
||||
*/
|
||||
@SerializedName("hunger")
|
||||
public int hunger;
|
||||
|
||||
/**
|
||||
* The player's saturation points
|
||||
*/
|
||||
@SerializedName("saturation")
|
||||
public float saturation;
|
||||
|
||||
/**
|
||||
* The player's saturation exhaustion points
|
||||
*/
|
||||
@SerializedName("saturation_exhaustion")
|
||||
public float saturationExhaustion;
|
||||
|
||||
/**
|
||||
* The player's currently selected item slot
|
||||
*/
|
||||
@SerializedName("selected_item_slot")
|
||||
public int selectedItemSlot;
|
||||
|
||||
/**
|
||||
* The player's total experience points<p>
|
||||
* (not to be confused with <i>experience level</i> - this is the "points" value shown on the death screen)
|
||||
*/
|
||||
@SerializedName("total_experience")
|
||||
public int totalExperience;
|
||||
|
||||
/**
|
||||
* The player's experience level (shown on the exp bar)
|
||||
*/
|
||||
@SerializedName("experience_level")
|
||||
public int expLevel;
|
||||
|
||||
/**
|
||||
* The player's progress to their next experience level
|
||||
*/
|
||||
@SerializedName("experience_progress")
|
||||
public float expProgress;
|
||||
|
||||
/**
|
||||
* The player's game mode string (one of "SURVIVAL", "CREATIVE", "ADVENTURE", "SPECTATOR")
|
||||
*/
|
||||
@SerializedName("game_mode")
|
||||
public String gameMode;
|
||||
|
||||
/**
|
||||
* If the player is currently flying
|
||||
*/
|
||||
@SerializedName("is_flying")
|
||||
public boolean isFlying;
|
||||
|
||||
public StatusData(final double health, final double maxHealth, final double healthScale,
|
||||
final int hunger, final float saturation, final float saturationExhaustion,
|
||||
final int selectedItemSlot, final int totalExperience, final int expLevel,
|
||||
final float expProgress, final String gameMode, final boolean isFlying) {
|
||||
this.health = health;
|
||||
this.maxHealth = maxHealth;
|
||||
this.healthScale = healthScale;
|
||||
this.hunger = hunger;
|
||||
this.saturation = saturation;
|
||||
this.saturationExhaustion = saturationExhaustion;
|
||||
this.selectedItemSlot = selectedItemSlot;
|
||||
this.totalExperience = totalExperience;
|
||||
this.expLevel = expLevel;
|
||||
this.expProgress = expProgress;
|
||||
this.gameMode = gameMode;
|
||||
this.isFlying = isFlying;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected StatusData() {
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import net.william278.husksync.config.Settings;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Flags for setting {@link StatusData}, indicating which elements should be synced
|
||||
*
|
||||
* @deprecated Use the more direct {@link Settings#getSynchronizationFeature(Settings.SynchronizationFeature)} instead
|
||||
*/
|
||||
@Deprecated(since = "2.1")
|
||||
public enum StatusDataFlag {
|
||||
|
||||
SET_HEALTH(Settings.SynchronizationFeature.HEALTH),
|
||||
SET_MAX_HEALTH(Settings.SynchronizationFeature.MAX_HEALTH),
|
||||
SET_HUNGER(Settings.SynchronizationFeature.HUNGER),
|
||||
SET_EXPERIENCE(Settings.SynchronizationFeature.EXPERIENCE),
|
||||
SET_GAME_MODE(Settings.SynchronizationFeature.GAME_MODE),
|
||||
SET_FLYING(Settings.SynchronizationFeature.LOCATION),
|
||||
SET_SELECTED_ITEM_SLOT(Settings.SynchronizationFeature.INVENTORIES);
|
||||
|
||||
private final Settings.SynchronizationFeature feature;
|
||||
|
||||
StatusDataFlag(@NotNull Settings.SynchronizationFeature feature) {
|
||||
this.feature = feature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all status data flags
|
||||
*
|
||||
* @return all status data flags as a list
|
||||
* @deprecated Use {@link Settings#getSynchronizationFeature(Settings.SynchronizationFeature)} instead
|
||||
*/
|
||||
@NotNull
|
||||
@Deprecated(since = "2.1")
|
||||
@SuppressWarnings("unused")
|
||||
public static List<StatusDataFlag> getAll() {
|
||||
return Arrays.stream(StatusDataFlag.values()).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all status data flags that are enabled for setting as per the {@link Settings}
|
||||
*
|
||||
* @param settings the settings to use for determining which flags are enabled
|
||||
* @return all status data flags that are enabled for setting
|
||||
* @deprecated Use {@link Settings#getSynchronizationFeature(Settings.SynchronizationFeature)} instead
|
||||
*/
|
||||
@NotNull
|
||||
@Deprecated(since = "2.1")
|
||||
public static List<StatusDataFlag> getFromSettings(@NotNull Settings settings) {
|
||||
return Arrays.stream(StatusDataFlag.values()).filter(
|
||||
flag -> settings.getSynchronizationFeature(flag.feature)).toList();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,376 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import net.william278.desertwell.util.Version;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Stores data about a user
|
||||
*/
|
||||
public class UserData {
|
||||
|
||||
/**
|
||||
* Indicates the version of the {@link UserData} format being used.
|
||||
* </p>
|
||||
* This value is to be incremented whenever the format changes.
|
||||
*/
|
||||
public static final int CURRENT_FORMAT_VERSION = 3;
|
||||
|
||||
/**
|
||||
* Stores the user's status data, including health, food, etc.
|
||||
*/
|
||||
@SerializedName("status")
|
||||
@Nullable
|
||||
protected StatusData statusData;
|
||||
|
||||
/**
|
||||
* Stores the user's inventory contents
|
||||
*/
|
||||
@SerializedName("inventory")
|
||||
@Nullable
|
||||
protected ItemData inventoryData;
|
||||
|
||||
/**
|
||||
* Stores the user's ender chest contents
|
||||
*/
|
||||
@SerializedName("ender_chest")
|
||||
@Nullable
|
||||
protected ItemData enderChestData;
|
||||
|
||||
/**
|
||||
* Store's the user's potion effects
|
||||
*/
|
||||
@SerializedName("potion_effects")
|
||||
@Nullable
|
||||
protected PotionEffectData potionEffectData;
|
||||
|
||||
/**
|
||||
* Stores the set of this user's advancements
|
||||
*/
|
||||
@SerializedName("advancements")
|
||||
@Nullable
|
||||
protected List<AdvancementData> advancementData;
|
||||
|
||||
/**
|
||||
* Stores the user's set of statistics
|
||||
*/
|
||||
@SerializedName("statistics")
|
||||
@Nullable
|
||||
protected StatisticsData statisticData;
|
||||
|
||||
/**
|
||||
* Store's the user's world location and coordinates
|
||||
*/
|
||||
@SerializedName("location")
|
||||
@Nullable
|
||||
protected LocationData locationData;
|
||||
|
||||
/**
|
||||
* Stores the user's serialized persistent data container, which contains metadata keys applied by other plugins
|
||||
*/
|
||||
@SerializedName("persistent_data_container")
|
||||
@Nullable
|
||||
protected PersistentDataContainerData persistentDataContainerData;
|
||||
|
||||
/**
|
||||
* Stores the version of Minecraft this data was generated in
|
||||
*/
|
||||
@SerializedName("minecraft_version")
|
||||
@NotNull
|
||||
protected String minecraftVersion;
|
||||
|
||||
/**
|
||||
* Stores the version of the data format being used
|
||||
*/
|
||||
@SerializedName("format_version")
|
||||
protected int formatVersion = CURRENT_FORMAT_VERSION;
|
||||
|
||||
/**
|
||||
* Create a new {@link UserData} object with the provided data
|
||||
*
|
||||
* @param statusData the user's status data ({@link StatusData})
|
||||
* @param inventoryData the user's inventory data ({@link ItemData})
|
||||
* @param enderChestData the user's ender chest data ({@link ItemData})
|
||||
* @param potionEffectData the user's potion effect data ({@link PotionEffectData})
|
||||
* @param advancementData the user's advancement data ({@link AdvancementData})
|
||||
* @param statisticData the user's statistic data ({@link StatisticsData})
|
||||
* @param locationData the user's location data ({@link LocationData})
|
||||
* @param persistentDataContainerData the user's persistent data container data ({@link PersistentDataContainerData})
|
||||
* @param minecraftVersion the version of Minecraft this data was generated in (e.g. {@code "1.19.2"})
|
||||
* @deprecated see {@link #builder(String)} or {@link #builder(Version)} to create a {@link UserDataBuilder}, which
|
||||
* you can use to {@link UserDataBuilder#build()} a {@link UserData} instance with
|
||||
*/
|
||||
@Deprecated(since = "2.1")
|
||||
public UserData(@Nullable StatusData statusData, @Nullable ItemData inventoryData,
|
||||
@Nullable ItemData enderChestData, @Nullable PotionEffectData potionEffectData,
|
||||
@Nullable List<AdvancementData> advancementData, @Nullable StatisticsData statisticData,
|
||||
@Nullable LocationData locationData, @Nullable PersistentDataContainerData persistentDataContainerData,
|
||||
@NotNull String minecraftVersion) {
|
||||
this.statusData = statusData;
|
||||
this.inventoryData = inventoryData;
|
||||
this.enderChestData = enderChestData;
|
||||
this.potionEffectData = potionEffectData;
|
||||
this.advancementData = advancementData;
|
||||
this.statisticData = statisticData;
|
||||
this.locationData = locationData;
|
||||
this.persistentDataContainerData = persistentDataContainerData;
|
||||
this.minecraftVersion = minecraftVersion;
|
||||
}
|
||||
|
||||
// Empty constructor to facilitate json serialization
|
||||
@SuppressWarnings("unused")
|
||||
protected UserData() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link StatusData} from this user data
|
||||
*
|
||||
* @return the {@link StatusData} of this user data
|
||||
* @since 2.0
|
||||
* @deprecated Use {@link #getStatus()}, which returns an optional instead
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated(since = "2.1")
|
||||
public StatusData getStatusData() {
|
||||
return statusData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link StatusData} from this user data
|
||||
*
|
||||
* @return an optional containing the {@link StatusData} if it is present in this user data
|
||||
* @since 2.1
|
||||
*/
|
||||
public Optional<StatusData> getStatus() {
|
||||
return Optional.ofNullable(statusData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link ItemData} representing the player's inventory from this user data
|
||||
*
|
||||
* @return the inventory {@link ItemData} of this user data
|
||||
* @since 2.0
|
||||
* @deprecated Use {@link #getInventory()}, which returns an optional instead
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated(since = "2.1")
|
||||
public ItemData getInventoryData() {
|
||||
return inventoryData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link ItemData} representing the player's inventory from this user data
|
||||
*
|
||||
* @return an optional containing the inventory {@link ItemData} if it is present in this user data
|
||||
* @since 2.1
|
||||
*/
|
||||
public Optional<ItemData> getInventory() {
|
||||
return Optional.ofNullable(inventoryData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link ItemData} representing the player's ender chest from this user data
|
||||
*
|
||||
* @return the ender chest {@link ItemData} of this user data
|
||||
* @since 2.0
|
||||
* @deprecated Use {@link #getEnderChest()}, which returns an optional instead
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated(since = "2.1")
|
||||
public ItemData getEnderChestData() {
|
||||
return enderChestData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link ItemData} representing the player's ender chest from this user data
|
||||
*
|
||||
* @return an optional containing the ender chest {@link ItemData} if it is present in this user data
|
||||
* @since 2.1
|
||||
*/
|
||||
public Optional<ItemData> getEnderChest() {
|
||||
return Optional.ofNullable(enderChestData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link PotionEffectData} representing player status effects from this user data
|
||||
*
|
||||
* @return the {@link PotionEffectData} of this user data
|
||||
* @since 2.0
|
||||
* @deprecated Use {@link #getPotionEffects()}, which returns an optional instead
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated(since = "2.1")
|
||||
public PotionEffectData getPotionEffectsData() {
|
||||
return potionEffectData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link PotionEffectData} representing the player's potion effects from this user data
|
||||
*
|
||||
* @return an optional containing {@link PotionEffectData} if it is present in this user data
|
||||
* @since 2.1
|
||||
*/
|
||||
public Optional<PotionEffectData> getPotionEffects() {
|
||||
return Optional.ofNullable(potionEffectData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of {@link AdvancementData} from this user data
|
||||
*
|
||||
* @return the {@link AdvancementData} of this user data
|
||||
* @since 2.0
|
||||
* @deprecated Use {@link #getAdvancements()}, which returns an optional instead
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated(since = "2.1")
|
||||
public List<AdvancementData> getAdvancementData() {
|
||||
return advancementData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of {@link AdvancementData} representing the player's advancements from this user data
|
||||
*
|
||||
* @return an optional containing a {@link List} of {@link AdvancementData} if it is present in this user data
|
||||
* @since 2.1
|
||||
*/
|
||||
public Optional<List<AdvancementData>> getAdvancements() {
|
||||
return Optional.ofNullable(advancementData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link StatisticsData} representing player statistics from this user data
|
||||
*
|
||||
* @return the {@link StatisticsData} of this user data
|
||||
* @since 2.0
|
||||
* @deprecated Use {@link #getStatistics()}, which returns an optional instead
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated(since = "2.1")
|
||||
public StatisticsData getStatisticsData() {
|
||||
return statisticData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets {@link StatisticsData} representing player statistics from this user data
|
||||
*
|
||||
* @return an optional containing player {@link StatisticsData} if it is present in this user data
|
||||
* @since 2.1
|
||||
*/
|
||||
public Optional<StatisticsData> getStatistics() {
|
||||
return Optional.ofNullable(statisticData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link LocationData} representing the player location from this user data
|
||||
*
|
||||
* @return the inventory {@link LocationData} of this user data
|
||||
* @since 2.0
|
||||
* @deprecated Use {@link #getLocation()}, which returns an optional instead
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated(since = "2.1")
|
||||
public LocationData getLocationData() {
|
||||
return locationData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets {@link LocationData} representing the player location from this user data
|
||||
*
|
||||
* @return an optional containing player {@link LocationData} if it is present in this user data
|
||||
* @since 2.1
|
||||
*/
|
||||
public Optional<LocationData> getLocation() {
|
||||
return Optional.ofNullable(locationData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link PersistentDataContainerData} from this user data
|
||||
*
|
||||
* @return the {@link PersistentDataContainerData} of this user data
|
||||
* @since 2.0
|
||||
* @deprecated Use {@link #getPersistentDataContainer()}, which returns an optional instead
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated(since = "2.1")
|
||||
public PersistentDataContainerData getPersistentDataContainerData() {
|
||||
return persistentDataContainerData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets {@link PersistentDataContainerData} from this user data
|
||||
*
|
||||
* @return an optional containing the player's {@link PersistentDataContainerData} if it is present in this user data
|
||||
* @since 2.1
|
||||
*/
|
||||
public Optional<PersistentDataContainerData> getPersistentDataContainer() {
|
||||
return Optional.ofNullable(persistentDataContainerData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version of Minecraft this data was generated in
|
||||
*
|
||||
* @return the version of Minecraft this data was generated in
|
||||
*/
|
||||
@NotNull
|
||||
public String getMinecraftVersion() {
|
||||
return minecraftVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the version of the data format being used
|
||||
*
|
||||
* @return the version of the data format being used
|
||||
*/
|
||||
public int getFormatVersion() {
|
||||
return formatVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new {@link UserDataBuilder} for creating {@link UserData}
|
||||
*
|
||||
* @param minecraftVersion the version of Minecraft this data was generated in (e.g. {@code "1.19.2"})
|
||||
* @return a UserData {@link UserDataBuilder} instance
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public static UserDataBuilder builder(@NotNull String minecraftVersion) {
|
||||
return new UserDataBuilder(minecraftVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new {@link UserDataBuilder} for creating {@link UserData}
|
||||
*
|
||||
* @param minecraftVersion a {@link Version} object, representing the Minecraft version this data was generated in
|
||||
* @return a UserData {@link UserDataBuilder} instance
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public static UserDataBuilder builder(@NotNull Version minecraftVersion) {
|
||||
return builder(minecraftVersion.toStringWithoutMetadata());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A builder utility for creating {@link UserData} instances
|
||||
*
|
||||
* @since 2.1
|
||||
*/
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
public class UserDataBuilder {
|
||||
|
||||
@NotNull
|
||||
private final UserData userData;
|
||||
|
||||
protected UserDataBuilder(@NotNull String minecraftVersion) {
|
||||
this.userData = new UserData();
|
||||
this.userData.minecraftVersion = minecraftVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link StatusData} to this {@link UserData}
|
||||
*
|
||||
* @param status the {@link StatusData} to set
|
||||
* @return this {@link UserDataBuilder}
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public UserDataBuilder setStatus(@NotNull StatusData status) {
|
||||
this.userData.statusData = status;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the inventory {@link ItemData} to this {@link UserData}
|
||||
*
|
||||
* @param inventoryData the inventory {@link ItemData} to set
|
||||
* @return this {@link UserDataBuilder}
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public UserDataBuilder setInventory(@Nullable ItemData inventoryData) {
|
||||
this.userData.inventoryData = inventoryData;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the ender chest {@link ItemData} to this {@link UserData}
|
||||
*
|
||||
* @param enderChestData the ender chest {@link ItemData} to set
|
||||
* @return this {@link UserDataBuilder}
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public UserDataBuilder setEnderChest(@Nullable ItemData enderChestData) {
|
||||
this.userData.enderChestData = enderChestData;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link List} of {@link ItemData} to this {@link UserData}
|
||||
*
|
||||
* @param potionEffectData the {@link List} of {@link ItemData} to set
|
||||
* @return this {@link UserDataBuilder}
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public UserDataBuilder setPotionEffects(@Nullable PotionEffectData potionEffectData) {
|
||||
this.userData.potionEffectData = potionEffectData;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link List} of {@link ItemData} to this {@link UserData}
|
||||
*
|
||||
* @param advancementData the {@link List} of {@link ItemData} to set
|
||||
* @return this {@link UserDataBuilder}
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public UserDataBuilder setAdvancements(@Nullable List<AdvancementData> advancementData) {
|
||||
this.userData.advancementData = advancementData;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link StatisticsData} to this {@link UserData}
|
||||
*
|
||||
* @param statisticData the {@link StatisticsData} to set
|
||||
* @return this {@link UserDataBuilder}
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public UserDataBuilder setStatistics(@Nullable StatisticsData statisticData) {
|
||||
this.userData.statisticData = statisticData;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the {@link LocationData} to this {@link UserData}
|
||||
*
|
||||
* @param locationData the {@link LocationData} to set
|
||||
* @return this {@link UserDataBuilder}
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public UserDataBuilder setLocation(@Nullable LocationData locationData) {
|
||||
this.userData.locationData = locationData;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link PersistentDataContainerData} to this {@link UserData}
|
||||
*
|
||||
* @param persistentDataContainerData the {@link PersistentDataContainerData} to set
|
||||
* @return this {@link UserDataBuilder}
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public UserDataBuilder setPersistentDataContainer(@Nullable PersistentDataContainerData persistentDataContainerData) {
|
||||
this.userData.persistentDataContainerData = persistentDataContainerData;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build and get the {@link UserData} instance
|
||||
*
|
||||
* @return the {@link UserData} instance
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public UserData build() {
|
||||
return this.userData;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import net.william278.desertwell.util.ThrowingConsumer;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A holder of data in the form of {@link Data}s, which can be synced
|
||||
*/
|
||||
public interface UserDataHolder extends DataHolder {
|
||||
|
||||
/**
|
||||
* Get the data that is enabled for syncing in the config
|
||||
*
|
||||
* @return the data that is enabled for syncing
|
||||
* @since 3.0
|
||||
*/
|
||||
@Override
|
||||
@NotNull
|
||||
default Map<Identifier, Data> getData() {
|
||||
return getPlugin().getRegisteredDataTypes().stream()
|
||||
.filter(type -> type.isCustom() || getPlugin().getSettings().isSyncFeatureEnabled(type))
|
||||
.map(id -> Map.entry(id, getData(id)))
|
||||
.filter(data -> data.getValue().isPresent())
|
||||
.collect(HashMap::new, (map, data) -> map.put(data.getKey(), data.getValue().get()), HashMap::putAll);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the data for the given {@link Identifier} to the holder.
|
||||
* <p>
|
||||
* This will be performed synchronously on the main server thread; it will not happen instantly.
|
||||
*
|
||||
* @param identifier the {@link Identifier} to set the data for
|
||||
* @param data the {@link Data} to set
|
||||
* @since 3.0
|
||||
*/
|
||||
@Override
|
||||
default void setData(@NotNull Identifier identifier, @NotNull Data data) {
|
||||
getPlugin().runSync(() -> data.apply(this, getPlugin()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a serialized data snapshot of this data owner
|
||||
*
|
||||
* @param saveCause the cause of the snapshot
|
||||
* @return the snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
default DataSnapshot.Packed createSnapshot(@NotNull DataSnapshot.SaveCause saveCause) {
|
||||
return DataSnapshot.builder(getPlugin()).data(this.getData()).saveCause(saveCause).buildAndPack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize and apply a data snapshot to this data owner
|
||||
* <p>
|
||||
* This method will deserialize the data on the current thread, then synchronously apply it on
|
||||
* the main server thread.
|
||||
* </p>
|
||||
* The {@code runAfter} callback function will be run after the snapshot has been applied.
|
||||
*
|
||||
* @param snapshot the snapshot to apply
|
||||
* @param runAfter the function to run asynchronously after the snapshot has been applied
|
||||
* @since 3.0
|
||||
*/
|
||||
default void applySnapshot(@NotNull DataSnapshot.Packed snapshot, @NotNull ThrowingConsumer<UserDataHolder> runAfter) {
|
||||
final HuskSync plugin = getPlugin();
|
||||
final DataSnapshot.Unpacked unpacked = snapshot.unpack(plugin);
|
||||
plugin.runSync(() -> {
|
||||
unpacked.getData().forEach((type, data) -> {
|
||||
if (plugin.getSettings().isSyncFeatureEnabled(type)) {
|
||||
if (type.isCustom()) {
|
||||
getCustomDataStore().put(type, data);
|
||||
}
|
||||
data.apply(this, plugin);
|
||||
}
|
||||
});
|
||||
plugin.runAsync(() -> runAfter.accept(this));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setInventory(@NotNull Data.Items.Inventory inventory) {
|
||||
this.setData(Identifier.INVENTORY, inventory);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setEnderChest(@NotNull Data.Items.EnderChest enderChest) {
|
||||
this.setData(Identifier.ENDER_CHEST, enderChest);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setPotionEffects(@NotNull Data.PotionEffects potionEffects) {
|
||||
this.setData(Identifier.POTION_EFFECTS, potionEffects);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setAdvancements(@NotNull Data.Advancements advancements) {
|
||||
this.setData(Identifier.ADVANCEMENTS, advancements);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setLocation(@NotNull Data.Location location) {
|
||||
this.setData(Identifier.LOCATION, location);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setStatistics(@NotNull Data.Statistics statistics) {
|
||||
this.setData(Identifier.STATISTICS, statistics);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setHealth(@NotNull Data.Health health) {
|
||||
this.setData(Identifier.HEALTH, health);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setHunger(@NotNull Data.Hunger hunger) {
|
||||
this.setData(Identifier.HUNGER, hunger);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setExperience(@NotNull Data.Experience experience) {
|
||||
this.setData(Identifier.EXPERIENCE, experience);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setGameMode(@NotNull Data.GameMode gameMode) {
|
||||
this.setData(Identifier.GAME_MODE, gameMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setPersistentData(@NotNull Data.PersistentData persistentData) {
|
||||
this.setData(Identifier.PERSISTENT_DATA, persistentData);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
Map<Identifier, Data> getCustomDataStore();
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
HuskSync getPlugin();
|
||||
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import net.william278.husksync.command.Permission;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Represents a uniquely versioned and timestamped snapshot of a user's data, including why it was saved.
|
||||
*
|
||||
* @param versionUUID The unique identifier for this user data version
|
||||
* @param versionTimestamp An epoch milliseconds timestamp of when this data was created
|
||||
* @param userData The {@link UserData} that has been versioned
|
||||
* @param cause The {@link DataSaveCause} that caused this data to be saved
|
||||
*/
|
||||
public record UserDataSnapshot(@NotNull UUID versionUUID, @NotNull Date versionTimestamp,
|
||||
@NotNull DataSaveCause cause, boolean pinned,
|
||||
@NotNull UserData userData) implements Comparable<UserDataSnapshot> {
|
||||
|
||||
/**
|
||||
* Version {@link UserData} into a {@link UserDataSnapshot}, assigning it a random {@link UUID} and the current timestamp {@link Date}
|
||||
* </p>
|
||||
* Note that this method will set {@code cause} to {@link DataSaveCause#API}
|
||||
*
|
||||
* @param userData The {@link UserData} to version
|
||||
* @return A new {@link UserDataSnapshot}
|
||||
* @implNote This isn't used to version data that is going to be set to a database to prevent UUID collisions.<p>
|
||||
* Database implementations should instead use their own UUID generation functions.
|
||||
*/
|
||||
public static UserDataSnapshot create(@NotNull UserData userData) {
|
||||
return new UserDataSnapshot(UUID.randomUUID(), new Date(),
|
||||
DataSaveCause.API, false, userData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a menu in chat to an {@link OnlineUser} about this {@link UserDataSnapshot} for a {@link User dataOwner}
|
||||
*
|
||||
* @param user The {@link OnlineUser} to display the menu to
|
||||
* @param dataOwner The {@link User} whose data this snapshot captures a state of
|
||||
* @param locales The {@link Locales} to use for displaying the menu
|
||||
*/
|
||||
public void displayDataOverview(@NotNull OnlineUser user, @NotNull User dataOwner, @NotNull Locales locales) {
|
||||
// Title message, timestamp, owner and cause.
|
||||
locales.getLocale("data_manager_title", versionUUID().toString().split("-")[0],
|
||||
versionUUID().toString(), dataOwner.username, dataOwner.uuid.toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
locales.getLocale("data_manager_timestamp",
|
||||
new SimpleDateFormat("MMM dd yyyy, HH:mm:ss.sss").format(versionTimestamp()))
|
||||
.ifPresent(user::sendMessage);
|
||||
if (pinned()) {
|
||||
locales.getLocale("data_manager_pinned").ifPresent(user::sendMessage);
|
||||
}
|
||||
locales.getLocale("data_manager_cause", cause().name().toLowerCase(Locale.ENGLISH).replaceAll("_", " "))
|
||||
.ifPresent(user::sendMessage);
|
||||
|
||||
// User status data, if present in the snapshot
|
||||
userData().getStatus()
|
||||
.flatMap(statusData -> locales.getLocale("data_manager_status",
|
||||
Integer.toString((int) statusData.health),
|
||||
Integer.toString((int) statusData.maxHealth),
|
||||
Integer.toString(statusData.hunger),
|
||||
Integer.toString(statusData.expLevel),
|
||||
statusData.gameMode.toLowerCase(Locale.ENGLISH)))
|
||||
.ifPresent(user::sendMessage);
|
||||
|
||||
// Advancement and statistic data, if both are present in the snapshot
|
||||
userData().getAdvancements()
|
||||
.flatMap(advancementData -> userData().getStatistics()
|
||||
.flatMap(statisticsData -> locales.getLocale("data_manager_advancements_statistics",
|
||||
Integer.toString(advancementData.size()),
|
||||
generateAdvancementPreview(advancementData, locales),
|
||||
String.format("%.2f", (((statisticsData.untypedStatistics.getOrDefault(
|
||||
"PLAY_ONE_MINUTE", 0)) / 20d) / 60d) / 60d))))
|
||||
.ifPresent(user::sendMessage);
|
||||
|
||||
if (user.hasPermission(Permission.COMMAND_INVENTORY.node)
|
||||
&& user.hasPermission(Permission.COMMAND_ENDER_CHEST.node)) {
|
||||
locales.getLocale("data_manager_item_buttons", dataOwner.username, versionUUID().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
if (user.hasPermission(Permission.COMMAND_USER_DATA_MANAGE.node)) {
|
||||
locales.getLocale("data_manager_management_buttons", dataOwner.username, versionUUID().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
if (user.hasPermission(Permission.COMMAND_USER_DATA_DUMP.node)) {
|
||||
locales.getLocale("data_manager_system_buttons", dataOwner.username, versionUUID().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String generateAdvancementPreview(@NotNull List<AdvancementData> advancementData, @NotNull Locales locales) {
|
||||
final StringJoiner joiner = new StringJoiner("\n");
|
||||
final List<AdvancementData> advancementsToPreview = advancementData.stream().filter(dataItem ->
|
||||
!dataItem.key.startsWith("minecraft:recipes/")).toList();
|
||||
final int PREVIEW_SIZE = 8;
|
||||
for (int i = 0; i < advancementsToPreview.size(); i++) {
|
||||
joiner.add(advancementsToPreview.get(i).key);
|
||||
if (i >= PREVIEW_SIZE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
final int remainingAdvancements = advancementsToPreview.size() - PREVIEW_SIZE;
|
||||
if (remainingAdvancements > 0) {
|
||||
joiner.add(locales.getRawLocale("data_manager_advancements_preview_remaining",
|
||||
Integer.toString(remainingAdvancements)).orElse("+" + remainingAdvancements + "…"));
|
||||
}
|
||||
return joiner.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare UserData by creation timestamp
|
||||
*
|
||||
* @param other the other UserData to be compared
|
||||
* @return the comparison result; the more recent UserData is greater than the less recent UserData
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(@NotNull UserDataSnapshot other) {
|
||||
return Long.compare(this.versionTimestamp.getTime(), other.versionTimestamp.getTime());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,20 +21,20 @@ package net.william278.husksync.database;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.data.UserDataSnapshot;
|
||||
import net.william278.husksync.migrator.Migrator;
|
||||
import net.william278.husksync.player.User;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.data.DataSnapshot.SaveCause;
|
||||
import net.william278.husksync.data.UserDataHolder;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.Blocking;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* An abstract representation of the plugin database, storing player data.
|
||||
@@ -57,17 +57,19 @@ public abstract class Database {
|
||||
* @throws IOException if the resource could not be read
|
||||
*/
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
@NotNull
|
||||
protected final String[] getSchemaStatements(@NotNull String schemaFileName) throws IOException {
|
||||
return formatStatementTables(new String(Objects.requireNonNull(plugin.getResource(schemaFileName))
|
||||
.readAllBytes(), StandardCharsets.UTF_8)).split(";");
|
||||
}
|
||||
|
||||
/**
|
||||
* Format all table name placeholder strings in a SQL statement
|
||||
* Format all table name placeholder strings in an SQL statement
|
||||
*
|
||||
* @param sql the SQL statement with un-formatted table name placeholders
|
||||
* @param sql the SQL statement with unformatted table name placeholders
|
||||
* @return the formatted statement, with table placeholders replaced with the correct names
|
||||
*/
|
||||
@NotNull
|
||||
protected final String formatStatementTables(@NotNull String sql) {
|
||||
return sql.replaceAll("%users_table%", plugin.getSettings().getTableName(Settings.TableName.USERS))
|
||||
.replaceAll("%user_data_table%", plugin.getSettings().getTableName(Settings.TableName.USER_DATA));
|
||||
@@ -75,57 +77,67 @@ public abstract class Database {
|
||||
|
||||
/**
|
||||
* Initialize the database and ensure tables are present; create tables if they do not exist.
|
||||
*
|
||||
* @throws IllegalStateException if the database could not be initialized
|
||||
*/
|
||||
public abstract void initialize();
|
||||
@Blocking
|
||||
public abstract void initialize() throws IllegalStateException;
|
||||
|
||||
/**
|
||||
* Ensure a {@link User} has an entry in the database and that their username is up-to-date
|
||||
*
|
||||
* @param user The {@link User} to ensure
|
||||
* @return A future returning void when complete
|
||||
*/
|
||||
public abstract CompletableFuture<Void> ensureUser(@NotNull User user);
|
||||
@Blocking
|
||||
public abstract void ensureUser(@NotNull User user);
|
||||
|
||||
/**
|
||||
* Get a player by their Minecraft account {@link UUID}
|
||||
*
|
||||
* @param uuid Minecraft account {@link UUID} of the {@link User} to get
|
||||
* @return A future returning an optional with the {@link User} present if they exist
|
||||
* @return An optional with the {@link User} present if they exist
|
||||
*/
|
||||
public abstract CompletableFuture<Optional<User>> getUser(@NotNull UUID uuid);
|
||||
@Blocking
|
||||
public abstract Optional<User> getUser(@NotNull UUID uuid);
|
||||
|
||||
/**
|
||||
* Get a user by their username (<i>case-insensitive</i>)
|
||||
*
|
||||
* @param username Username of the {@link User} to get (<i>case-insensitive</i>)
|
||||
* @return A future returning an optional with the {@link User} present if they exist
|
||||
* @return An optional with the {@link User} present if they exist
|
||||
*/
|
||||
public abstract CompletableFuture<Optional<User>> getUserByName(@NotNull String username);
|
||||
@Blocking
|
||||
public abstract Optional<User> getUserByName(@NotNull String username);
|
||||
|
||||
|
||||
/**
|
||||
* Get the current uniquely versioned user data for a given user, if it exists.
|
||||
*
|
||||
* @param user the user to get data for
|
||||
* @return an optional containing the {@link UserDataSnapshot}, if it exists, or an empty optional if it does not
|
||||
*/
|
||||
public abstract CompletableFuture<Optional<UserDataSnapshot>> getCurrentUserData(@NotNull User user);
|
||||
|
||||
/**
|
||||
* Get all {@link UserDataSnapshot} entries for a user from the database.
|
||||
* Get the latest data snapshot for a user.
|
||||
*
|
||||
* @param user The user to get data for
|
||||
* @return A future returning a list of a user's {@link UserDataSnapshot} entries
|
||||
* @return an optional containing the {@link DataSnapshot}, if it exists, or an empty optional if it does not
|
||||
*/
|
||||
public abstract CompletableFuture<List<UserDataSnapshot>> getUserData(@NotNull User user);
|
||||
@Blocking
|
||||
public abstract Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user);
|
||||
|
||||
/**
|
||||
* Gets a specific {@link UserDataSnapshot} entry for a user from the database, by its UUID.
|
||||
* Get all {@link DataSnapshot} entries for a user from the database.
|
||||
*
|
||||
* @param user The user to get data for
|
||||
* @return The list of a user's {@link DataSnapshot} entries
|
||||
*/
|
||||
@Blocking
|
||||
@NotNull
|
||||
public abstract List<DataSnapshot.Packed> getAllSnapshots(@NotNull User user);
|
||||
|
||||
/**
|
||||
* Gets a specific {@link DataSnapshot} entry for a user from the database, by its UUID.
|
||||
*
|
||||
* @param user The user to get data for
|
||||
* @param versionUuid The UUID of the {@link UserDataSnapshot} entry to get
|
||||
* @return A future returning an optional containing the {@link UserDataSnapshot}, if it exists, or an empty optional if it does not
|
||||
* @param versionUuid The UUID of the {@link DataSnapshot} entry to get
|
||||
* @return An optional containing the {@link DataSnapshot}, if it exists
|
||||
*/
|
||||
public abstract CompletableFuture<Optional<UserDataSnapshot>> getUserData(@NotNull User user, @NotNull UUID versionUuid);
|
||||
@Blocking
|
||||
public abstract Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid);
|
||||
|
||||
/**
|
||||
* <b>(Internal)</b> Prune user data for a given user to the maximum value as configured.
|
||||
@@ -133,61 +145,131 @@ public abstract class Database {
|
||||
* @param user The user to prune data for
|
||||
* @implNote Data snapshots marked as {@code pinned} are exempt from rotation
|
||||
*/
|
||||
protected abstract void rotateUserData(@NotNull User user);
|
||||
@Blocking
|
||||
protected abstract void rotateSnapshots(@NotNull User user);
|
||||
|
||||
/**
|
||||
* Deletes a specific {@link UserDataSnapshot} entry for a user from the database, by its UUID.
|
||||
* Deletes a specific {@link DataSnapshot} entry for a user from the database, by its UUID.
|
||||
*
|
||||
* @param user The user to get data for
|
||||
* @param versionUuid The UUID of the {@link UserDataSnapshot} entry to delete
|
||||
* @return A future returning void when complete
|
||||
* @param versionUuid The UUID of the {@link DataSnapshot} entry to delete
|
||||
*/
|
||||
public abstract CompletableFuture<Boolean> deleteUserData(@NotNull User user, @NotNull UUID versionUuid);
|
||||
@Blocking
|
||||
public abstract boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid);
|
||||
|
||||
/**
|
||||
* Save user data to the database<p>
|
||||
* Save user data to the database
|
||||
* </p>
|
||||
* This will remove the oldest data for the user if the amount of data exceeds the limit as configured
|
||||
*
|
||||
* @param user The user to add data for
|
||||
* @param userData The {@link UserData} to set. The implementation should version it with a random UUID and the current timestamp during insertion.
|
||||
* @return A future returning void when complete
|
||||
* @see UserDataSnapshot#create(UserData)
|
||||
* @param snapshot The {@link DataSnapshot} to set.
|
||||
* The implementation should version it with a random UUID and the current timestamp during insertion.
|
||||
* @see UserDataHolder#createSnapshot(SaveCause)
|
||||
*/
|
||||
public abstract CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData, @NotNull DataSaveCause dataSaveCause);
|
||||
@Blocking
|
||||
public void addSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed snapshot) {
|
||||
if (snapshot.getSaveCause() != SaveCause.SERVER_SHUTDOWN) {
|
||||
plugin.fireEvent(
|
||||
plugin.getDataSaveEvent(user, snapshot),
|
||||
(event) -> this.addAndRotateSnapshot(user, snapshot)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.addAndRotateSnapshot(user, snapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin a saved {@link UserDataSnapshot} by given version UUID, setting it's {@code pinned} state to {@code true}.
|
||||
* <b>Internal</b> - Save user data to the database. This will:
|
||||
* <ol>
|
||||
* <li>Delete their most recent snapshot, if it was created before the backup frequency time</li>
|
||||
* <li>Create the snapshot</li>
|
||||
* <li>Rotate snapshot backups</li>
|
||||
* </ol>
|
||||
*
|
||||
* @param user The user to pin the data for
|
||||
* @param versionUuid The UUID of the user's {@link UserDataSnapshot} entry to pin
|
||||
* @return A future returning a boolean; {@code true} if the operation completed successfully, {@code false} if it failed
|
||||
* @see UserDataSnapshot#pinned()
|
||||
* @param user The user to add data for
|
||||
* @param snapshot The {@link DataSnapshot} to set.
|
||||
*/
|
||||
public abstract CompletableFuture<Void> pinUserData(@NotNull User user, @NotNull UUID versionUuid);
|
||||
@Blocking
|
||||
private void addAndRotateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed snapshot) {
|
||||
final int backupFrequency = plugin.getSettings().getBackupFrequency();
|
||||
if (!snapshot.isPinned() && backupFrequency > 0) {
|
||||
this.rotateLatestSnapshot(user, snapshot.getTimestamp().minusHours(backupFrequency));
|
||||
}
|
||||
this.createSnapshot(user, snapshot);
|
||||
this.rotateSnapshots(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpin a saved {@link UserDataSnapshot} by given version UUID, setting it's {@code pinned} state to {@code false}.
|
||||
* Deletes the most recent data snapshot by the given {@link User user}
|
||||
* The snapshot must have been created after {@link OffsetDateTime time} and NOT be pinned
|
||||
* Facilities the backup frequency feature, reducing redundant snapshots from being saved longer than needed
|
||||
*
|
||||
* @param user The user to delete a snapshot for
|
||||
* @param within The time to delete a snapshot after
|
||||
*/
|
||||
@Blocking
|
||||
protected abstract void rotateLatestSnapshot(@NotNull User user, @NotNull OffsetDateTime within);
|
||||
|
||||
/**
|
||||
* <b>Internal</b> - Create user data in the database
|
||||
*
|
||||
* @param user The user to add data for
|
||||
* @param data The {@link DataSnapshot} to set.
|
||||
*/
|
||||
@Blocking
|
||||
protected abstract void createSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data);
|
||||
|
||||
/**
|
||||
* Update a saved {@link DataSnapshot} by given version UUID
|
||||
*
|
||||
* @param user The user whose data snapshot
|
||||
* @param snapshot The {@link DataSnapshot} to update
|
||||
*/
|
||||
@Blocking
|
||||
public abstract void updateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed snapshot);
|
||||
|
||||
/**
|
||||
* Unpin a saved {@link DataSnapshot} by given version UUID, setting it's {@code pinned} state to {@code false}.
|
||||
*
|
||||
* @param user The user to unpin the data for
|
||||
* @param versionUuid The UUID of the user's {@link UserDataSnapshot} entry to unpin
|
||||
* @return A future returning a boolean; {@code true} if the operation completed successfully, {@code false} if it failed
|
||||
* @see UserDataSnapshot#pinned()
|
||||
* @param versionUuid The UUID of the user's {@link DataSnapshot} entry to unpin
|
||||
* @see DataSnapshot#isPinned()
|
||||
*/
|
||||
public abstract CompletableFuture<Void> unpinUserData(@NotNull User user, @NotNull UUID versionUuid);
|
||||
@Blocking
|
||||
public final void unpinSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
this.getSnapshot(user, versionUuid).ifPresent(data -> {
|
||||
data.edit(plugin, (snapshot) -> snapshot.setPinned(false));
|
||||
this.updateSnapshot(user, data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wipes <b>all</b> {@link UserData} entries from the database.
|
||||
* <b>This should never be used</b>, except when preparing tables for migration.
|
||||
* Pin a saved {@link DataSnapshot} by given version UUID, setting it's {@code pinned} state to {@code true}.
|
||||
*
|
||||
* @return A future returning void when complete
|
||||
* @see Migrator#start()
|
||||
* @param user The user to pin the data for
|
||||
* @param versionUuid The UUID of the user's {@link DataSnapshot} entry to pin
|
||||
*/
|
||||
public abstract CompletableFuture<Void> wipeDatabase();
|
||||
@Blocking
|
||||
public final void pinSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
this.getSnapshot(user, versionUuid).ifPresent(data -> {
|
||||
data.edit(plugin, (snapshot) -> snapshot.setPinned(true));
|
||||
this.updateSnapshot(user, data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wipes <b>all</b> {@link User} entries from the database.
|
||||
* <b>This should only be used when preparing tables for a data migration.</b>
|
||||
*/
|
||||
@Blocking
|
||||
public abstract void wipeDatabase();
|
||||
|
||||
/**
|
||||
* Close the database connection
|
||||
*/
|
||||
public abstract void close();
|
||||
public abstract void terminate();
|
||||
|
||||
/**
|
||||
* Identifies types of databases
|
||||
|
||||
@@ -21,20 +21,17 @@ package net.william278.husksync.database;
|
||||
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataAdaptionException;
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.data.UserDataSnapshot;
|
||||
import net.william278.husksync.event.DataSaveEvent;
|
||||
import net.william278.husksync.player.User;
|
||||
import net.william278.husksync.adapter.DataAdapter;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.Blocking;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.sql.*;
|
||||
import java.util.Date;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class MySqlDatabase extends Database {
|
||||
@@ -46,8 +43,7 @@ public class MySqlDatabase extends Database {
|
||||
|
||||
public MySqlDatabase(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
this.flavor = plugin.getSettings().getDatabaseType() == Type.MARIADB
|
||||
? "mariadb" : "mysql";
|
||||
this.flavor = plugin.getSettings().getDatabaseType().getProtocol();
|
||||
this.driverClass = plugin.getSettings().getDatabaseType() == Type.MARIADB
|
||||
? "org.mariadb.jdbc.Driver" : "com.mysql.cj.jdbc.Driver";
|
||||
}
|
||||
@@ -58,10 +54,16 @@ public class MySqlDatabase extends Database {
|
||||
* @return The {@link Connection} to the MySQL database
|
||||
* @throws SQLException if the connection fails for some reason
|
||||
*/
|
||||
@Blocking
|
||||
@NotNull
|
||||
private Connection getConnection() throws SQLException {
|
||||
if (dataSource == null) {
|
||||
throw new IllegalStateException("The database has not been initialized");
|
||||
}
|
||||
return dataSource.getConnection();
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public void initialize() throws IllegalStateException {
|
||||
// Initialize the Hikari pooled connection
|
||||
@@ -124,192 +126,175 @@ public class MySqlDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public CompletableFuture<Void> ensureUser(@NotNull User user) {
|
||||
return getUser(user.uuid).thenAccept(optionalUser ->
|
||||
optionalUser.ifPresentOrElse(existingUser -> {
|
||||
if (!existingUser.username.equals(user.username)) {
|
||||
// Update a user's name if it has changed in the database
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
UPDATE `%users_table%`
|
||||
SET `username`=?
|
||||
WHERE `uuid`=?"""))) {
|
||||
public void ensureUser(@NotNull User user) {
|
||||
getUser(user.getUuid()).ifPresentOrElse(
|
||||
existingUser -> {
|
||||
if (!existingUser.getUsername().equals(user.getUsername())) {
|
||||
// Update a user's name if it has changed in the database
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
UPDATE `%users_table%`
|
||||
SET `username`=?
|
||||
WHERE `uuid`=?"""))) {
|
||||
|
||||
statement.setString(1, user.username);
|
||||
statement.setString(2, existingUser.uuid.toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
plugin.log(Level.INFO, "Updated " + user.username + "'s name in the database (" + existingUser.username + " -> " + user.username + ")");
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to update a user's name on the database", e);
|
||||
}
|
||||
statement.setString(1, user.getUsername());
|
||||
statement.setString(2, existingUser.getUuid().toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
},
|
||||
() -> {
|
||||
// Insert new player data into the database
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
INSERT INTO `%users_table%` (`uuid`,`username`)
|
||||
VALUES (?,?);"""))) {
|
||||
plugin.log(Level.INFO, "Updated " + user.getUsername() + "'s name in the database (" + existingUser.getUsername() + " -> " + user.getUsername() + ")");
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to update a user's name on the database", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
() -> {
|
||||
// Insert new player data into the database
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
INSERT INTO `%users_table%` (`uuid`,`username`)
|
||||
VALUES (?,?);"""))) {
|
||||
|
||||
statement.setString(1, user.uuid.toString());
|
||||
statement.setString(2, user.username);
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Optional<User>> getUser(@NotNull UUID uuid) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `uuid`, `username`
|
||||
FROM `%users_table%`
|
||||
WHERE `uuid`=?"""))) {
|
||||
|
||||
statement.setString(1, uuid.toString());
|
||||
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
return Optional.of(new User(UUID.fromString(resultSet.getString("uuid")),
|
||||
resultSet.getString("username")));
|
||||
statement.setString(1, user.getUuid().toString());
|
||||
statement.setString(2, user.getUsername());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user from uuid from the database", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public CompletableFuture<Optional<User>> getUserByName(@NotNull String username) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `uuid`, `username`
|
||||
FROM `%users_table%`
|
||||
WHERE `username`=?"""))) {
|
||||
statement.setString(1, username);
|
||||
public Optional<User> getUser(@NotNull UUID uuid) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `uuid`, `username`
|
||||
FROM `%users_table%`
|
||||
WHERE `uuid`=?"""))) {
|
||||
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
return Optional.of(new User(UUID.fromString(resultSet.getString("uuid")),
|
||||
resultSet.getString("username")));
|
||||
}
|
||||
statement.setString(1, uuid.toString());
|
||||
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
return Optional.of(new User(UUID.fromString(resultSet.getString("uuid")),
|
||||
resultSet.getString("username")));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user by name from the database", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user from uuid from the database", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public CompletableFuture<Optional<UserDataSnapshot>> getCurrentUserData(@NotNull User user) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `version_uuid`, `timestamp`, `save_cause`, `pinned`, `data`
|
||||
FROM `%user_data_table%`
|
||||
WHERE `player_uuid`=?
|
||||
ORDER BY `timestamp` DESC
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, user.uuid.toString());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
final Blob blob = resultSet.getBlob("data");
|
||||
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
|
||||
blob.free();
|
||||
return Optional.of(new UserDataSnapshot(
|
||||
UUID.fromString(resultSet.getString("version_uuid")),
|
||||
Date.from(resultSet.getTimestamp("timestamp").toInstant()),
|
||||
DataSaveCause.getCauseByName(resultSet.getString("save_cause")),
|
||||
resultSet.getBoolean("pinned"),
|
||||
plugin.getDataAdapter().fromBytes(dataByteArray)));
|
||||
}
|
||||
public Optional<User> getUserByName(@NotNull String username) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `uuid`, `username`
|
||||
FROM `%users_table%`
|
||||
WHERE `username`=?"""))) {
|
||||
statement.setString(1, username);
|
||||
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
return Optional.of(new User(UUID.fromString(resultSet.getString("uuid")),
|
||||
resultSet.getString("username")));
|
||||
}
|
||||
} catch (SQLException | DataAdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user by name from the database", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public CompletableFuture<List<UserDataSnapshot>> getUserData(@NotNull User user) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
final List<UserDataSnapshot> retrievedData = new ArrayList<>();
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `version_uuid`, `timestamp`, `save_cause`, `pinned`, `data`
|
||||
FROM `%user_data_table%`
|
||||
WHERE `player_uuid`=?
|
||||
ORDER BY `timestamp` DESC;"""))) {
|
||||
statement.setString(1, user.uuid.toString());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
while (resultSet.next()) {
|
||||
final Blob blob = resultSet.getBlob("data");
|
||||
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
|
||||
blob.free();
|
||||
final UserDataSnapshot data = new UserDataSnapshot(
|
||||
UUID.fromString(resultSet.getString("version_uuid")),
|
||||
Date.from(resultSet.getTimestamp("timestamp").toInstant()),
|
||||
DataSaveCause.getCauseByName(resultSet.getString("save_cause")),
|
||||
resultSet.getBoolean("pinned"),
|
||||
plugin.getDataAdapter().fromBytes(dataByteArray));
|
||||
retrievedData.add(data);
|
||||
}
|
||||
return retrievedData;
|
||||
public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `version_uuid`, `timestamp`, `save_cause`, `pinned`, `data`
|
||||
FROM `%user_data_table%`
|
||||
WHERE `player_uuid`=?
|
||||
ORDER BY `timestamp` DESC
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, user.getUuid().toString());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
final Blob blob = resultSet.getBlob("data");
|
||||
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
|
||||
blob.free();
|
||||
return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray));
|
||||
}
|
||||
} catch (SQLException | DataAdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
|
||||
}
|
||||
return retrievedData;
|
||||
});
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public CompletableFuture<Optional<UserDataSnapshot>> getUserData(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `version_uuid`, `timestamp`, `save_cause`, `pinned`, `data`
|
||||
FROM `%user_data_table%`
|
||||
WHERE `player_uuid`=? AND `version_uuid`=?
|
||||
ORDER BY `timestamp` DESC
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, user.uuid.toString());
|
||||
statement.setString(2, versionUuid.toString());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
final Blob blob = resultSet.getBlob("data");
|
||||
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
|
||||
blob.free();
|
||||
return Optional.of(new UserDataSnapshot(
|
||||
UUID.fromString(resultSet.getString("version_uuid")),
|
||||
Date.from(resultSet.getTimestamp("timestamp").toInstant()),
|
||||
DataSaveCause.getCauseByName(resultSet.getString("save_cause")),
|
||||
resultSet.getBoolean("pinned"),
|
||||
plugin.getDataAdapter().fromBytes(dataByteArray)));
|
||||
}
|
||||
@NotNull
|
||||
public List<DataSnapshot.Packed> getAllSnapshots(@NotNull User user) {
|
||||
final List<DataSnapshot.Packed> retrievedData = new ArrayList<>();
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `version_uuid`, `timestamp`, `save_cause`, `pinned`, `data`
|
||||
FROM `%user_data_table%`
|
||||
WHERE `player_uuid`=?
|
||||
ORDER BY `timestamp` DESC;"""))) {
|
||||
statement.setString(1, user.getUuid().toString());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
while (resultSet.next()) {
|
||||
final Blob blob = resultSet.getBlob("data");
|
||||
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
|
||||
blob.free();
|
||||
retrievedData.add(DataSnapshot.deserialize(plugin, dataByteArray));
|
||||
}
|
||||
} catch (SQLException | DataAdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch specific user data by UUID from the database", e);
|
||||
return retrievedData;
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
|
||||
}
|
||||
return retrievedData;
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
protected void rotateUserData(@NotNull User user) {
|
||||
final List<UserDataSnapshot> unpinnedUserData = getUserData(user).join().stream()
|
||||
.filter(dataSnapshot -> !dataSnapshot.pinned()).toList();
|
||||
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `version_uuid`, `timestamp`, `save_cause`, `pinned`, `data`
|
||||
FROM `%user_data_table%`
|
||||
WHERE `player_uuid`=? AND `version_uuid`=?
|
||||
ORDER BY `timestamp` DESC
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, user.getUuid().toString());
|
||||
statement.setString(2, versionUuid.toString());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
final Blob blob = resultSet.getBlob("data");
|
||||
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
|
||||
blob.free();
|
||||
return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray));
|
||||
}
|
||||
}
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch specific user data by UUID from the database", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
protected void rotateSnapshots(@NotNull User user) {
|
||||
final List<DataSnapshot.Packed> unpinnedUserData = getAllSnapshots(user).stream()
|
||||
.filter(dataSnapshot -> !dataSnapshot.isPinned()).toList();
|
||||
if (unpinnedUserData.size() > plugin.getSettings().getMaxUserDataSnapshots()) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
@@ -319,7 +304,7 @@ public class MySqlDatabase extends Database {
|
||||
ORDER BY `timestamp` ASC
|
||||
LIMIT %entry_count%;""".replace("%entry_count%",
|
||||
Integer.toString(unpinnedUserData.size() - plugin.getSettings().getMaxUserDataSnapshots()))))) {
|
||||
statement.setString(1, user.uuid.toString());
|
||||
statement.setString(1, user.getUuid().toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
@@ -328,105 +313,97 @@ public class MySqlDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public CompletableFuture<Boolean> deleteUserData(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
DELETE FROM `%user_data_table%`
|
||||
WHERE `player_uuid`=? AND `version_uuid`=?
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, user.uuid.toString());
|
||||
statement.setString(2, versionUuid.toString());
|
||||
return statement.executeUpdate() > 0;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to delete specific user data from the database", e);
|
||||
public boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
DELETE FROM `%user_data_table%`
|
||||
WHERE `player_uuid`=? AND `version_uuid`=?
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, user.getUuid().toString());
|
||||
statement.setString(2, versionUuid.toString());
|
||||
return statement.executeUpdate() > 0;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to delete specific user data from the database", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
protected void rotateLatestSnapshot(@NotNull User user, @NotNull OffsetDateTime within) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
DELETE FROM `%user_data_table%`
|
||||
WHERE `player_uuid`=? AND `timestamp`>? AND `pinned` IS FALSE
|
||||
ORDER BY `timestamp` ASC
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, user.getUuid().toString());
|
||||
statement.setTimestamp(2, Timestamp.from(within.toInstant()));
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to delete a user's data from the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
protected void createSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
INSERT INTO `%user_data_table%`
|
||||
(`player_uuid`,`version_uuid`,`timestamp`,`save_cause`,`pinned`,`data`)
|
||||
VALUES (?,?,?,?,?,?);"""))) {
|
||||
statement.setString(1, user.getUuid().toString());
|
||||
statement.setString(2, data.getId().toString());
|
||||
statement.setTimestamp(3, Timestamp.from(data.getTimestamp().toInstant()));
|
||||
statement.setString(4, data.getSaveCause().name());
|
||||
statement.setBoolean(5, data.isPinned());
|
||||
statement.setBlob(6, new ByteArrayInputStream(data.asBytes(plugin)));
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to set user data in the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
UPDATE `%user_data_table%`
|
||||
SET `save_cause`=?,`pinned`=?,`data`=?
|
||||
WHERE `player_uuid`=? AND `version_uuid`=?
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, data.getSaveCause().name());
|
||||
statement.setBoolean(2, data.isPinned());
|
||||
statement.setBlob(3, new ByteArrayInputStream(data.asBytes(plugin)));
|
||||
statement.setString(4, user.getUuid().toString());
|
||||
statement.setString(5, data.getId().toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to pin user data in the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData,
|
||||
@NotNull DataSaveCause saveCause) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
final DataSaveEvent dataSaveEvent = (DataSaveEvent) plugin.getEventCannon().fireDataSaveEvent(user,
|
||||
userData, saveCause).join();
|
||||
if (!dataSaveEvent.isCancelled()) {
|
||||
final UserData finalData = dataSaveEvent.getUserData();
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
INSERT INTO `%user_data_table%`
|
||||
(`player_uuid`,`version_uuid`,`timestamp`,`save_cause`,`data`)
|
||||
VALUES (?,UUID(),NOW(),?,?);"""))) {
|
||||
statement.setString(1, user.uuid.toString());
|
||||
statement.setString(2, saveCause.name());
|
||||
statement.setBlob(3, new ByteArrayInputStream(
|
||||
plugin.getDataAdapter().toBytes(finalData)));
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException | DataAdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to set user data in the database", e);
|
||||
}
|
||||
public void wipeDatabase() {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
statement.executeUpdate(formatStatementTables("DELETE FROM `%user_data_table%`;"));
|
||||
}
|
||||
this.rotateUserData(user);
|
||||
});
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to wipe the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> pinUserData(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
UPDATE `%user_data_table%`
|
||||
SET `pinned`=TRUE
|
||||
WHERE `player_uuid`=? AND `version_uuid`=?
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, user.uuid.toString());
|
||||
statement.setString(2, versionUuid.toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to pin user data in the database", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> unpinUserData(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
UPDATE `%user_data_table%`
|
||||
SET `pinned`=FALSE
|
||||
WHERE `player_uuid`=? AND `version_uuid`=?
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, user.uuid.toString());
|
||||
statement.setString(2, versionUuid.toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to unpin user data in the database", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> wipeDatabase() {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
statement.executeUpdate(formatStatementTables("DELETE FROM `%user_data_table%`;"));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to wipe the database", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
public void terminate() {
|
||||
if (dataSource != null) {
|
||||
if (!dataSource.isClosed()) {
|
||||
dataSource.close();
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
|
||||
package net.william278.husksync.event;
|
||||
|
||||
public interface CancellableEvent extends Event {
|
||||
@SuppressWarnings("unused")
|
||||
public interface Cancellable extends Event {
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
default boolean isCancelled() {
|
||||
return false;
|
||||
}
|
||||
@@ -19,21 +19,34 @@
|
||||
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.player.User;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public interface DataSaveEvent extends CancellableEvent {
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public interface DataSaveEvent extends Cancellable {
|
||||
|
||||
@NotNull
|
||||
UserData getUserData();
|
||||
|
||||
void setUserData(@NotNull UserData userData);
|
||||
|
||||
@NotNull User getUser();
|
||||
User getUser();
|
||||
|
||||
@NotNull
|
||||
DataSaveCause getSaveCause();
|
||||
DataSnapshot.Packed getData();
|
||||
|
||||
default void editData(@NotNull Consumer<DataSnapshot.Unpacked> editor) {
|
||||
getData().edit(getPlugin(), editor);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default DataSnapshot.SaveCause getSaveCause() {
|
||||
return getData().getSaveCause();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
HuskSync getPlugin();
|
||||
|
||||
}
|
||||
|
||||
@@ -19,10 +19,6 @@
|
||||
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public interface Event {
|
||||
|
||||
CompletableFuture<Event> fire();
|
||||
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Used to fire plugin {@link Event}s
|
||||
*/
|
||||
public abstract class EventCannon {
|
||||
|
||||
protected EventCannon() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires a {@link PreSyncEvent}
|
||||
*
|
||||
* @param user The user to fire the event for
|
||||
* @param userData The user data to fire the event with
|
||||
* @return A future that will be completed when the event is fired
|
||||
*/
|
||||
public abstract CompletableFuture<Event> firePreSyncEvent(@NotNull OnlineUser user, @NotNull UserData userData);
|
||||
|
||||
/**
|
||||
* Fires a {@link DataSaveEvent}
|
||||
*
|
||||
* @param user The user to fire the event for
|
||||
* @param userData The user data to fire the event with
|
||||
* @return A future that will be completed when the event is fired
|
||||
*/
|
||||
public abstract CompletableFuture<Event> fireDataSaveEvent(@NotNull User user, @NotNull UserData userData,
|
||||
|
||||
@NotNull DataSaveCause saveCause);
|
||||
|
||||
/**
|
||||
* Fires a {@link SyncCompleteEvent}
|
||||
*
|
||||
* @param user The user to fire the event for
|
||||
*/
|
||||
public abstract void fireSyncCompleteEvent(@NotNull OnlineUser user);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Used to fire plugin {@link Event}s
|
||||
*/
|
||||
public interface EventDispatcher {
|
||||
|
||||
/**
|
||||
* Fire an event synchronously, then run a callback asynchronously.
|
||||
*
|
||||
* @param event The event to fire
|
||||
* @param callback The callback to run after the event has been fired
|
||||
* @param <T> The material of event to fire
|
||||
*/
|
||||
default <T extends Event> void fireEvent(@NotNull T event, @Nullable Consumer<T> callback) {
|
||||
getPlugin().runSync(() -> {
|
||||
if (!fireIsCancelled(event) && callback != null) {
|
||||
getPlugin().runAsync(() -> callback.accept(event));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire an event on this thread, and return whether the event was canceled.
|
||||
*
|
||||
* @param event The event to fire
|
||||
* @param <T> The material of event to fire
|
||||
* @return Whether the event was canceled
|
||||
*/
|
||||
<T extends Event> boolean fireIsCancelled(@NotNull T event);
|
||||
|
||||
@NotNull
|
||||
PreSyncEvent getPreSyncEvent(@NotNull OnlineUser user, @NotNull DataSnapshot.Packed userData);
|
||||
|
||||
@NotNull
|
||||
DataSaveEvent getDataSaveEvent(@NotNull User user, @NotNull DataSnapshot.Packed saveCause);
|
||||
|
||||
@NotNull
|
||||
SyncCompleteEvent getSyncCompleteEvent(@NotNull OnlineUser user);
|
||||
|
||||
@NotNull
|
||||
HuskSync getPlugin();
|
||||
|
||||
}
|
||||
@@ -19,10 +19,12 @@
|
||||
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public interface PlayerEvent extends Event {
|
||||
|
||||
@NotNull
|
||||
OnlineUser getUser();
|
||||
|
||||
}
|
||||
|
||||
@@ -19,14 +19,30 @@
|
||||
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public interface PreSyncEvent extends CancellableEvent {
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public interface PreSyncEvent extends PlayerEvent {
|
||||
|
||||
@NotNull
|
||||
UserData getUserData();
|
||||
DataSnapshot.Packed getData();
|
||||
|
||||
void setUserData(@NotNull UserData userData);
|
||||
default void editData(@NotNull Consumer<DataSnapshot.Unpacked> editor) {
|
||||
getData().edit(getPlugin(), editor);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default DataSnapshot.SaveCause getSaveCause() {
|
||||
return getData().getSaveCause();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
HuskSync getPlugin();
|
||||
|
||||
}
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.hook;
|
||||
|
||||
import com.djrapitops.plan.extension.CallEvents;
|
||||
import com.djrapitops.plan.extension.DataExtension;
|
||||
import com.djrapitops.plan.extension.ElementOrder;
|
||||
import com.djrapitops.plan.extension.FormatType;
|
||||
import com.djrapitops.plan.extension.annotation.*;
|
||||
import com.djrapitops.plan.extension.icon.Color;
|
||||
import com.djrapitops.plan.extension.icon.Family;
|
||||
import com.djrapitops.plan.extension.icon.Icon;
|
||||
import com.djrapitops.plan.extension.table.Table;
|
||||
import com.djrapitops.plan.extension.table.TableColumnFormat;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.UserDataSnapshot;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@TabInfo(
|
||||
tab = "Current Status",
|
||||
iconName = "id-card",
|
||||
iconFamily = Family.SOLID,
|
||||
elementOrder = {ElementOrder.VALUES, ElementOrder.TABLE, ElementOrder.GRAPH}
|
||||
)
|
||||
@TabInfo(
|
||||
tab = "Data Snapshots",
|
||||
iconName = "clipboard-list",
|
||||
iconFamily = Family.SOLID,
|
||||
elementOrder = {ElementOrder.VALUES, ElementOrder.TABLE, ElementOrder.GRAPH}
|
||||
)
|
||||
@TabOrder({"Current Status", "Data Snapshots"})
|
||||
@PluginInfo(
|
||||
name = "HuskSync",
|
||||
iconName = "exchange-alt",
|
||||
iconFamily = Family.SOLID,
|
||||
color = Color.LIGHT_BLUE
|
||||
)
|
||||
@SuppressWarnings("unused")
|
||||
public class PlanDataExtension implements DataExtension {
|
||||
|
||||
private HuskSync plugin;
|
||||
|
||||
private static final String UNKNOWN_STRING = "N/A";
|
||||
|
||||
private static final String PINNED_HTML_STRING = "📍 ";
|
||||
|
||||
protected PlanDataExtension(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
protected PlanDataExtension() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallEvents[] callExtensionMethodsOn() {
|
||||
return new CallEvents[]{
|
||||
CallEvents.PLAYER_JOIN,
|
||||
CallEvents.PLAYER_LEAVE
|
||||
};
|
||||
}
|
||||
|
||||
private CompletableFuture<Optional<UserDataSnapshot>> getCurrentUserData(@NotNull UUID uuid) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
final Optional<User> optionalUser = plugin.getDatabase().getUser(uuid).join();
|
||||
if (optionalUser.isPresent()) {
|
||||
return plugin.getDatabase().getCurrentUserData(optionalUser.get()).join();
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
}
|
||||
|
||||
@BooleanProvider(
|
||||
text = "Has Synced",
|
||||
description = "Whether this user has saved, synchronised data.",
|
||||
iconName = "exchange-alt",
|
||||
iconFamily = Family.SOLID,
|
||||
conditionName = "hasSynced",
|
||||
hidden = true
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public boolean getUserHasSynced(@NotNull UUID uuid) {
|
||||
return getCurrentUserData(uuid).join().isPresent();
|
||||
}
|
||||
|
||||
@Conditional("hasSynced")
|
||||
@NumberProvider(
|
||||
text = "Sync Time",
|
||||
description = "The last time the user had their data synced with the server.",
|
||||
iconName = "clock",
|
||||
iconFamily = Family.SOLID,
|
||||
format = FormatType.DATE_SECOND,
|
||||
priority = 6
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public long getCurrentDataTimestamp(@NotNull UUID uuid) {
|
||||
return getCurrentUserData(uuid).join().map(
|
||||
versionedUserData -> versionedUserData.versionTimestamp().getTime())
|
||||
.orElse(new Date().getTime());
|
||||
}
|
||||
|
||||
@Conditional("hasSynced")
|
||||
@StringProvider(
|
||||
text = "Version ID",
|
||||
description = "ID of the data version that the user is currently using.",
|
||||
iconName = "bolt",
|
||||
iconFamily = Family.SOLID,
|
||||
priority = 5
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public String getCurrentDataId(@NotNull UUID uuid) {
|
||||
return getCurrentUserData(uuid).join()
|
||||
.map(versionedUserData -> versionedUserData.versionUUID().toString()
|
||||
.split(Pattern.quote("-"))[0])
|
||||
.orElse(UNKNOWN_STRING);
|
||||
}
|
||||
|
||||
@Conditional("hasSynced")
|
||||
@StringProvider(
|
||||
text = "Health",
|
||||
description = "The number of health points out of the max health points this player currently has.",
|
||||
iconName = "heart",
|
||||
iconFamily = Family.SOLID,
|
||||
priority = 4
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public String getHealth(@NotNull UUID uuid) {
|
||||
return getCurrentUserData(uuid).join()
|
||||
.flatMap(versionedUserData -> versionedUserData.userData().getStatus())
|
||||
.map(statusData -> (int) statusData.health + "/" + (int) statusData.maxHealth)
|
||||
.orElse(UNKNOWN_STRING);
|
||||
}
|
||||
|
||||
@Conditional("hasSynced")
|
||||
@NumberProvider(
|
||||
text = "Hunger",
|
||||
description = "The number of hunger points this player currently has.",
|
||||
iconName = "drumstick-bite",
|
||||
iconFamily = Family.SOLID,
|
||||
priority = 3
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public long getHunger(@NotNull UUID uuid) {
|
||||
return getCurrentUserData(uuid).join()
|
||||
.flatMap(versionedUserData -> versionedUserData.userData().getStatus())
|
||||
.map(statusData -> (long) statusData.hunger)
|
||||
.orElse(0L);
|
||||
}
|
||||
|
||||
@Conditional("hasSynced")
|
||||
@NumberProvider(
|
||||
text = "Experience Level",
|
||||
description = "The number of experience levels this player currently has.",
|
||||
iconName = "hat-wizard",
|
||||
iconFamily = Family.SOLID,
|
||||
priority = 2
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public long getExperienceLevel(@NotNull UUID uuid) {
|
||||
return getCurrentUserData(uuid).join()
|
||||
.flatMap(versionedUserData -> versionedUserData.userData().getStatus())
|
||||
.map(statusData -> (long) statusData.expLevel)
|
||||
.orElse(0L);
|
||||
}
|
||||
|
||||
@Conditional("hasSynced")
|
||||
@StringProvider(
|
||||
text = "Game Mode",
|
||||
description = "The game mode this player is currently in.",
|
||||
iconName = "gamepad",
|
||||
iconFamily = Family.SOLID,
|
||||
priority = 1
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public String getGameMode(@NotNull UUID uuid) {
|
||||
return getCurrentUserData(uuid).join()
|
||||
.flatMap(versionedUserData -> versionedUserData.userData().getStatus())
|
||||
.map(status -> status.gameMode)
|
||||
.orElse(UNKNOWN_STRING);
|
||||
}
|
||||
|
||||
@Conditional("hasSynced")
|
||||
@NumberProvider(
|
||||
text = "Advancements",
|
||||
description = "The number of advancements & recipes the player has progressed in.",
|
||||
iconName = "award",
|
||||
iconFamily = Family.SOLID
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public long getAdvancementsCompleted(@NotNull UUID playerUUID) {
|
||||
return getCurrentUserData(playerUUID).join()
|
||||
.flatMap(versionedUserData -> versionedUserData.userData().getAdvancements())
|
||||
.map(advancementsData -> (long) advancementsData.size())
|
||||
.orElse(0L);
|
||||
}
|
||||
|
||||
@Conditional("hasSynced")
|
||||
@TableProvider(tableColor = Color.LIGHT_BLUE)
|
||||
@Tab("Data Snapshots")
|
||||
public Table getDataSnapshots(@NotNull UUID playerUUID) {
|
||||
final Table.Factory dataSnapshotsTable = Table.builder()
|
||||
.columnOne("Time", new Icon(Family.SOLID, "clock", Color.NONE))
|
||||
.columnOneFormat(TableColumnFormat.DATE_SECOND)
|
||||
.columnTwo("ID", new Icon(Family.SOLID, "bolt", Color.NONE))
|
||||
.columnThree("Cause", new Icon(Family.SOLID, "flag", Color.NONE))
|
||||
.columnFour("Pinned", new Icon(Family.SOLID, "thumbtack", Color.NONE));
|
||||
plugin.getDatabase().getUser(playerUUID).join().ifPresent(user ->
|
||||
plugin.getDatabase().getUserData(user).join().forEach(versionedUserData -> dataSnapshotsTable.addRow(
|
||||
versionedUserData.versionTimestamp().getTime(),
|
||||
versionedUserData.versionUUID().toString().split("-")[0],
|
||||
versionedUserData.cause().name().toLowerCase(Locale.ENGLISH).replaceAll("_", " "),
|
||||
versionedUserData.pinned() ? PINNED_HTML_STRING + "Pinned" : "Unpinned"
|
||||
)));
|
||||
return dataSnapshotsTable.build();
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,22 @@
|
||||
package net.william278.husksync.hook;
|
||||
|
||||
import com.djrapitops.plan.capability.CapabilityService;
|
||||
import com.djrapitops.plan.extension.ExtensionService;
|
||||
import com.djrapitops.plan.extension.*;
|
||||
import com.djrapitops.plan.extension.annotation.*;
|
||||
import com.djrapitops.plan.extension.icon.Color;
|
||||
import com.djrapitops.plan.extension.icon.Family;
|
||||
import com.djrapitops.plan.extension.icon.Icon;
|
||||
import com.djrapitops.plan.extension.table.Table;
|
||||
import com.djrapitops.plan.extension.table.TableColumnFormat;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.DataHolder;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class PlanHook {
|
||||
@@ -50,6 +62,7 @@ public class PlanHook {
|
||||
private void registerDataExtension() {
|
||||
try {
|
||||
ExtensionService.getInstance().register(new PlanDataExtension(plugin));
|
||||
plugin.log(Level.INFO, "Registered HuskSync Plan data extension");
|
||||
} catch (IllegalStateException | IllegalArgumentException e) {
|
||||
plugin.log(Level.WARNING, "Failed to register Plan data extension: " + e.getMessage(), e);
|
||||
}
|
||||
@@ -64,4 +77,199 @@ public class PlanHook {
|
||||
});
|
||||
}
|
||||
|
||||
@TabInfo(
|
||||
tab = "Current Status",
|
||||
iconName = "id-card",
|
||||
iconFamily = Family.SOLID,
|
||||
elementOrder = {ElementOrder.VALUES, ElementOrder.TABLE, ElementOrder.GRAPH}
|
||||
)
|
||||
@TabInfo(
|
||||
tab = "Data Snapshots",
|
||||
iconName = "clipboard-list",
|
||||
iconFamily = Family.SOLID,
|
||||
elementOrder = {ElementOrder.VALUES, ElementOrder.TABLE, ElementOrder.GRAPH}
|
||||
)
|
||||
@TabOrder({"Current Status", "Data Snapshots"})
|
||||
@PluginInfo(
|
||||
name = "HuskSync",
|
||||
iconName = "exchange-alt",
|
||||
iconFamily = Family.SOLID,
|
||||
color = Color.LIGHT_BLUE
|
||||
)
|
||||
@SuppressWarnings("unused")
|
||||
public static class PlanDataExtension implements DataExtension {
|
||||
|
||||
private HuskSync plugin;
|
||||
|
||||
private static final String UNKNOWN_STRING = "N/A";
|
||||
|
||||
private static final String PINNED_HTML_STRING = "📍 ";
|
||||
|
||||
protected PlanDataExtension(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
protected PlanDataExtension() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public CallEvents[] callExtensionMethodsOn() {
|
||||
return new CallEvents[]{
|
||||
CallEvents.PLAYER_JOIN,
|
||||
CallEvents.PLAYER_LEAVE
|
||||
};
|
||||
}
|
||||
|
||||
// Get the user's latest data snapshot
|
||||
private Optional<DataSnapshot.Unpacked> getLatestSnapshot(@NotNull UUID uuid) {
|
||||
return plugin.getDatabase().getUser(uuid)
|
||||
.flatMap(user -> plugin.getDatabase().getLatestSnapshot(user))
|
||||
.map(snapshot -> snapshot.unpack(plugin));
|
||||
}
|
||||
|
||||
@BooleanProvider(
|
||||
text = "Has Synced",
|
||||
description = "Whether this user has saved, synchronized data.",
|
||||
iconName = "exchange-alt",
|
||||
iconFamily = Family.SOLID,
|
||||
conditionName = "hasSynced",
|
||||
hidden = true
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public boolean getUserHasSynced(@NotNull UUID uuid) {
|
||||
return getLatestSnapshot(uuid).isPresent();
|
||||
}
|
||||
|
||||
@Conditional("hasSynced")
|
||||
@NumberProvider(
|
||||
text = "Sync Time",
|
||||
description = "The last time the user had their data synced with the server.",
|
||||
iconName = "clock",
|
||||
iconFamily = Family.SOLID,
|
||||
format = FormatType.DATE_SECOND,
|
||||
priority = 6
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public long getCurrentDataTimestamp(@NotNull UUID uuid) {
|
||||
return getLatestSnapshot(uuid)
|
||||
.map(DataSnapshot::getTimestamp)
|
||||
.orElse(OffsetDateTime.now())
|
||||
.toEpochSecond();
|
||||
}
|
||||
|
||||
@Conditional("hasSynced")
|
||||
@StringProvider(
|
||||
text = "Version ID",
|
||||
description = "ID of the data version that the user is currently using.",
|
||||
iconName = "bolt",
|
||||
iconFamily = Family.SOLID,
|
||||
priority = 5
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public String getCurrentDataId(@NotNull UUID uuid) {
|
||||
return getLatestSnapshot(uuid)
|
||||
.map(DataSnapshot::getShortId)
|
||||
.orElse(UNKNOWN_STRING);
|
||||
}
|
||||
|
||||
@Conditional("hasSynced")
|
||||
@StringProvider(
|
||||
text = "Health",
|
||||
description = "The number of health points out of the max health points this player currently has.",
|
||||
iconName = "heart",
|
||||
iconFamily = Family.SOLID,
|
||||
priority = 4
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public String getHealth(@NotNull UUID uuid) {
|
||||
return getLatestSnapshot(uuid)
|
||||
.flatMap(DataHolder::getHealth)
|
||||
.map(health -> String.format("%s / %s", health.getHealth(), health.getMaxHealth()))
|
||||
.orElse(UNKNOWN_STRING);
|
||||
}
|
||||
|
||||
@Conditional("hasSynced")
|
||||
@NumberProvider(
|
||||
text = "Hunger",
|
||||
description = "The number of hunger points this player currently has.",
|
||||
iconName = "drumstick-bite",
|
||||
iconFamily = Family.SOLID,
|
||||
priority = 3
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public long getHunger(@NotNull UUID uuid) {
|
||||
return getLatestSnapshot(uuid)
|
||||
.flatMap(DataHolder::getHunger)
|
||||
.map(Data.Hunger::getFoodLevel)
|
||||
.orElse(20);
|
||||
}
|
||||
|
||||
@Conditional("hasSynced")
|
||||
@NumberProvider(
|
||||
text = "Experience Level",
|
||||
description = "The number of experience levels this player currently has.",
|
||||
iconName = "hat-wizard",
|
||||
iconFamily = Family.SOLID,
|
||||
priority = 2
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public long getExperienceLevel(@NotNull UUID uuid) {
|
||||
return getLatestSnapshot(uuid)
|
||||
.flatMap(DataHolder::getExperience)
|
||||
.map(Data.Experience::getExpLevel)
|
||||
.orElse(0);
|
||||
}
|
||||
|
||||
@Conditional("hasSynced")
|
||||
@StringProvider(
|
||||
text = "Game Mode",
|
||||
description = "The game mode this player is currently in.",
|
||||
iconName = "gamepad",
|
||||
iconFamily = Family.SOLID,
|
||||
priority = 1
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public String getGameMode(@NotNull UUID uuid) {
|
||||
return getLatestSnapshot(uuid)
|
||||
.flatMap(DataHolder::getGameMode)
|
||||
.map(Data.GameMode::getGameMode)
|
||||
.orElse(UNKNOWN_STRING);
|
||||
}
|
||||
|
||||
@Conditional("hasSynced")
|
||||
@NumberProvider(
|
||||
text = "Advancements",
|
||||
description = "The number of advancements & recipes the player has progressed in.",
|
||||
iconName = "award",
|
||||
iconFamily = Family.SOLID
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public long getAdvancementsCompleted(@NotNull UUID playerUUID) {
|
||||
return getLatestSnapshot(playerUUID)
|
||||
.flatMap(DataHolder::getAdvancements)
|
||||
.map(Data.Advancements::getCompleted)
|
||||
.stream().count();
|
||||
}
|
||||
|
||||
@Conditional("hasSynced")
|
||||
@TableProvider(tableColor = Color.LIGHT_BLUE)
|
||||
@Tab("Data Snapshots")
|
||||
public Table getDataSnapshots(@NotNull UUID playerUUID) {
|
||||
final Table.Factory dataSnapshotsTable = Table.builder()
|
||||
.columnOne("Time", new Icon(Family.SOLID, "clock", Color.NONE))
|
||||
.columnOneFormat(TableColumnFormat.DATE_SECOND)
|
||||
.columnTwo("ID", new Icon(Family.SOLID, "bolt", Color.NONE))
|
||||
.columnThree("Cause", new Icon(Family.SOLID, "flag", Color.NONE))
|
||||
.columnFour("Pinned", new Icon(Family.SOLID, "thumbtack", Color.NONE));
|
||||
plugin.getDatabase().getUser(playerUUID).ifPresent(user ->
|
||||
plugin.getDatabase().getAllSnapshots(user).forEach(snapshot -> dataSnapshotsTable.addRow(
|
||||
snapshot.getTimestamp().toEpochSecond(),
|
||||
snapshot.getShortId(),
|
||||
snapshot.getSaveCause().getDisplayName(),
|
||||
snapshot.isPinned() ? PINNED_HTML_STRING + "Pinned" : "Unpinned"
|
||||
))
|
||||
);
|
||||
return dataSnapshotsTable.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,22 +19,16 @@
|
||||
|
||||
package net.william278.husksync.listener;
|
||||
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.data.ItemData;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.util.Task;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
@@ -42,13 +36,11 @@ import java.util.logging.Level;
|
||||
*/
|
||||
public abstract class EventListener {
|
||||
|
||||
/**
|
||||
* The plugin instance
|
||||
*/
|
||||
// The plugin instance
|
||||
protected final HuskSync plugin;
|
||||
|
||||
/**
|
||||
* Set of UUIDs of "locked players", for which events will be cancelled.
|
||||
* Set of UUIDs of "locked players", for which events will be canceled.
|
||||
* </p>
|
||||
* Players are locked while their items are being set (on join) or saved (on quit)
|
||||
*/
|
||||
@@ -66,7 +58,7 @@ public abstract class EventListener {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a player joining the server (including players switching from another proxied server)
|
||||
* Handle a player joining the server (including players switching from another server on the network)
|
||||
*
|
||||
* @param user The {@link OnlineUser} to handle
|
||||
*/
|
||||
@@ -74,92 +66,51 @@ public abstract class EventListener {
|
||||
if (user.isNpc()) {
|
||||
return;
|
||||
}
|
||||
lockedPlayers.add(user.getUuid());
|
||||
|
||||
lockedPlayers.add(user.uuid);
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
// 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().getNetworkLatencyMilliseconds()));
|
||||
} catch (InterruptedException e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred handling a player join", e);
|
||||
} finally {
|
||||
plugin.getRedisManager().getUserServerSwitch(user).thenAccept(changingServers -> {
|
||||
if (!changingServers) {
|
||||
// Fetch from the database if the user isn't changing servers
|
||||
setUserFromDatabase(user).thenAccept(succeeded -> handleSynchronisationCompletion(user, succeeded));
|
||||
} else {
|
||||
final int TIME_OUT_MILLISECONDS = 3200;
|
||||
CompletableFuture.runAsync(() -> {
|
||||
final AtomicInteger currentMilliseconds = new AtomicInteger(0);
|
||||
final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
// Set the user as soon as the source server has set the data to redis
|
||||
executor.scheduleAtFixedRate(() -> {
|
||||
if (user.isOffline()) {
|
||||
executor.shutdown();
|
||||
return;
|
||||
}
|
||||
if (disabling || currentMilliseconds.get() > TIME_OUT_MILLISECONDS) {
|
||||
executor.shutdown();
|
||||
setUserFromDatabase(user).thenAccept(
|
||||
succeeded -> handleSynchronisationCompletion(user, succeeded));
|
||||
return;
|
||||
}
|
||||
plugin.getRedisManager().getUserData(user).thenAccept(redisUserData ->
|
||||
redisUserData.ifPresent(redisData -> {
|
||||
user.setData(redisData, plugin)
|
||||
.thenAccept(succeeded -> handleSynchronisationCompletion(user, succeeded)).join();
|
||||
executor.shutdown();
|
||||
})).join();
|
||||
currentMilliseconds.addAndGet(200);
|
||||
}, 0, 200L, TimeUnit.MILLISECONDS);
|
||||
});
|
||||
}
|
||||
});
|
||||
plugin.runAsyncDelayed(() -> {
|
||||
// Fetch from the database if the user isn't changing servers
|
||||
if (!plugin.getRedisManager().getUserServerSwitch(user)) {
|
||||
this.setUserFromDatabase(user);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Set the user as soon as the source server has set the data to redis
|
||||
final long MAX_ATTEMPTS = 16L;
|
||||
final AtomicLong timesRun = new AtomicLong(0L);
|
||||
final AtomicReference<Task.Repeating> task = new AtomicReference<>();
|
||||
final Runnable runnable = () -> {
|
||||
if (user.isOffline()) {
|
||||
task.get().cancel();
|
||||
return;
|
||||
}
|
||||
if (disabling || timesRun.getAndIncrement() > MAX_ATTEMPTS) {
|
||||
task.get().cancel();
|
||||
this.setUserFromDatabase(user);
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getRedisManager().getUserData(user).ifPresent(redisData -> {
|
||||
task.get().cancel();
|
||||
user.applySnapshot(redisData, DataSnapshot.UpdateCause.SYNCHRONIZED);
|
||||
});
|
||||
};
|
||||
task.set(plugin.getRepeatingTask(runnable, 10));
|
||||
task.get().run();
|
||||
|
||||
}, Math.max(0, plugin.getSettings().getNetworkLatencyMilliseconds() / 50L));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a user's data from the database
|
||||
*
|
||||
* @param user The user to set the data for
|
||||
* @return Whether the data was successfully set
|
||||
*/
|
||||
private CompletableFuture<Boolean> setUserFromDatabase(@NotNull OnlineUser user) {
|
||||
return plugin.getDatabase().getCurrentUserData(user).thenApply(databaseUserData -> {
|
||||
if (databaseUserData.isPresent()) {
|
||||
return user.setData(databaseUserData.get().userData(), plugin).join();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a player's synchronization completion
|
||||
*
|
||||
* @param user The {@link OnlineUser} to handle
|
||||
* @param succeeded Whether the synchronization succeeded
|
||||
*/
|
||||
private void handleSynchronisationCompletion(@NotNull OnlineUser user, boolean succeeded) {
|
||||
if (succeeded) {
|
||||
switch (plugin.getSettings().getNotificationDisplaySlot()) {
|
||||
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();
|
||||
lockedPlayers.remove(user.uuid);
|
||||
plugin.getEventCannon().fireSyncCompleteEvent(user);
|
||||
} else {
|
||||
plugin.getLocales().getLocale("synchronisation_failed")
|
||||
.ifPresent(user::sendMessage);
|
||||
plugin.getDatabase().ensureUser(user).join();
|
||||
}
|
||||
private void setUserFromDatabase(@NotNull OnlineUser user) {
|
||||
plugin.getDatabase().getLatestSnapshot(user).ifPresentOrElse(
|
||||
snapshot -> user.applySnapshot(snapshot, DataSnapshot.UpdateCause.SYNCHRONIZED),
|
||||
() -> user.completeSync(true, DataSnapshot.UpdateCause.NEW_USER, plugin)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,28 +119,27 @@ public abstract class EventListener {
|
||||
* @param user The {@link OnlineUser} to handle
|
||||
*/
|
||||
protected final void handlePlayerQuit(@NotNull OnlineUser user) {
|
||||
// Players quitting have their data manually saved by the plugin disable hook
|
||||
// Players quitting have their data manually saved when the plugin is disabled
|
||||
if (disabling) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't sync players awaiting synchronization
|
||||
if (lockedPlayers.contains(user.uuid) || user.isNpc()) {
|
||||
if (lockedPlayers.contains(user.getUuid()) || user.isNpc()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle asynchronous disconnection
|
||||
lockedPlayers.add(user.uuid);
|
||||
CompletableFuture.runAsync(() -> plugin.getRedisManager().setUserServerSwitch(user)
|
||||
.thenRun(() -> user.getUserData(plugin).thenAccept(
|
||||
optionalUserData -> optionalUserData.ifPresent(userData -> plugin.getRedisManager()
|
||||
.setUserData(user, userData).thenRun(() -> plugin.getDatabase()
|
||||
.setUserData(user, userData, DataSaveCause.DISCONNECT)))))
|
||||
.exceptionally(throwable -> {
|
||||
plugin.log(Level.SEVERE,
|
||||
"An exception occurred handling a player disconnection");
|
||||
throwable.printStackTrace();
|
||||
return null;
|
||||
}).join());
|
||||
// Handle disconnection
|
||||
try {
|
||||
lockedPlayers.add(user.getUuid());
|
||||
plugin.getRedisManager().setUserServerSwitch(user).thenRun(() -> {
|
||||
final DataSnapshot.Packed data = user.createSnapshot(DataSnapshot.SaveCause.DISCONNECT);
|
||||
plugin.getRedisManager().setUserData(user, data);
|
||||
plugin.getDatabase().addSnapshot(user, data);
|
||||
});
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred handling a player disconnection", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,10 +152,10 @@ public abstract class EventListener {
|
||||
return;
|
||||
}
|
||||
usersInWorld.stream()
|
||||
.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))));
|
||||
.filter(user -> !lockedPlayers.contains(user.getUuid()) && !user.isNpc())
|
||||
.forEach(user -> plugin.getDatabase().addSnapshot(
|
||||
user, user.createSnapshot(DataSnapshot.SaveCause.WORLD_SAVE)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -214,24 +164,22 @@ public abstract class EventListener {
|
||||
* @param user The user who died
|
||||
* @param drops The items that this user would have dropped
|
||||
*/
|
||||
protected void saveOnPlayerDeath(@NotNull OnlineUser user, @NotNull ItemData drops) {
|
||||
if (disabling || !plugin.getSettings().doSaveOnDeath() || lockedPlayers.contains(user.uuid) || user.isNpc()
|
||||
|| (!plugin.getSettings().doSaveEmptyDropsOnDeath() && drops.isEmpty())) {
|
||||
protected void saveOnPlayerDeath(@NotNull OnlineUser user, @NotNull Data.Items drops) {
|
||||
if (disabling || !plugin.getSettings().doSaveOnDeath() || lockedPlayers.contains(user.getUuid()) || user.isNpc()
|
||||
|| (!plugin.getSettings().doSaveEmptyDropsOnDeath() && drops.isEmpty())) {
|
||||
return;
|
||||
}
|
||||
|
||||
user.getUserData(plugin)
|
||||
.thenAccept(data -> data.ifPresent(userData -> {
|
||||
userData.getInventory().orElse(ItemData.empty()).serializedItems = drops.serializedItems;
|
||||
plugin.getDatabase().setUserData(user, userData, DataSaveCause.DEATH);
|
||||
}));
|
||||
final DataSnapshot.Packed snapshot = user.createSnapshot(DataSnapshot.SaveCause.DEATH);
|
||||
snapshot.edit(plugin, (data -> data.getInventory().ifPresent(inventory -> inventory.setContents(drops))));
|
||||
plugin.getDatabase().addSnapshot(user, snapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a player event should be cancelled
|
||||
* Determine whether a player event should be canceled
|
||||
*
|
||||
* @param userUuid The UUID of the user to check
|
||||
* @return Whether the event should be cancelled
|
||||
* @return Whether the event should be canceled
|
||||
*/
|
||||
protected final boolean cancelPlayerEvent(@NotNull UUID userUuid) {
|
||||
return disabling || lockedPlayers.contains(userUuid);
|
||||
@@ -245,21 +193,65 @@ public abstract class EventListener {
|
||||
|
||||
// Save data for all online users
|
||||
plugin.getOnlineUsers().stream()
|
||||
.filter(user -> !lockedPlayers.contains(user.uuid) && !user.isNpc())
|
||||
.filter(user -> !lockedPlayers.contains(user.getUuid()) && !user.isNpc())
|
||||
.forEach(user -> {
|
||||
lockedPlayers.add(user.uuid);
|
||||
user.getUserData(plugin).join()
|
||||
.ifPresent(userData -> plugin.getDatabase()
|
||||
.setUserData(user, userData, DataSaveCause.SERVER_SHUTDOWN).join());
|
||||
lockedPlayers.add(user.getUuid());
|
||||
plugin.getDatabase().addSnapshot(user, user.createSnapshot(DataSnapshot.SaveCause.SERVER_SHUTDOWN));
|
||||
});
|
||||
|
||||
// Close outstanding connections
|
||||
plugin.getDatabase().close();
|
||||
plugin.getRedisManager().close();
|
||||
plugin.getDatabase().terminate();
|
||||
plugin.getRedisManager().terminate();
|
||||
}
|
||||
|
||||
public final Set<UUID> getLockedPlayers() {
|
||||
return this.lockedPlayers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents priorities for events that HuskSync listens to
|
||||
*/
|
||||
public enum Priority {
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents events that HuskSync listens to, with a configurable priority listener
|
||||
*/
|
||||
public enum ListenerType {
|
||||
JOIN_LISTENER(Priority.LOWEST),
|
||||
QUIT_LISTENER(Priority.LOWEST),
|
||||
DEATH_LISTENER(Priority.NORMAL);
|
||||
|
||||
private final Priority defaultPriority;
|
||||
|
||||
ListenerType(@NotNull EventListener.Priority defaultPriority) {
|
||||
this.defaultPriority = defaultPriority;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Map.Entry<String, String> toEntry() {
|
||||
return Map.entry(name().toLowerCase(), defaultPriority.name());
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@NotNull
|
||||
public static Map<String, String> getDefaults() {
|
||||
return Map.ofEntries(Arrays.stream(values())
|
||||
.map(ListenerType::toEntry)
|
||||
.toArray(Map.Entry[]::new));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,13 +20,12 @@
|
||||
package net.william278.husksync.migrator;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* A migrator that migrates data from other data formats to HuskSync's {@link UserData} format
|
||||
* A migrator that migrates data from other data formats to HuskSync's format
|
||||
*/
|
||||
public abstract class Migrator {
|
||||
|
||||
|
||||
@@ -1,423 +0,0 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.player;
|
||||
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import de.themoep.minedown.adventure.MineDownParser;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.william278.desertwell.util.Version;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.*;
|
||||
import net.william278.husksync.event.PreSyncEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Represents a logged-in {@link User}
|
||||
*/
|
||||
public abstract class OnlineUser extends User {
|
||||
|
||||
public OnlineUser(@NotNull UUID uuid, @NotNull String username) {
|
||||
super(uuid, username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the player's {@link StatusData}
|
||||
*
|
||||
* @return the player's {@link StatusData}
|
||||
*/
|
||||
public abstract CompletableFuture<StatusData> getStatus();
|
||||
|
||||
/**
|
||||
* Set the player's {@link StatusData}
|
||||
*
|
||||
* @param statusData the player's {@link StatusData}
|
||||
* @param statusDataFlags the flags to use for setting the status data
|
||||
* @return a future returning void when complete
|
||||
* @deprecated Use {@link #setStatus(StatusData, Settings)} instead
|
||||
*/
|
||||
@Deprecated(since = "2.1")
|
||||
public final CompletableFuture<Void> setStatus(@NotNull StatusData statusData,
|
||||
@NotNull List<StatusDataFlag> statusDataFlags) {
|
||||
final Settings settings = new Settings();
|
||||
settings.getSynchronizationFeatures().put(Settings.SynchronizationFeature.HEALTH.name().toLowerCase(Locale.ENGLISH), statusDataFlags.contains(StatusDataFlag.SET_HEALTH));
|
||||
settings.getSynchronizationFeatures().put(Settings.SynchronizationFeature.MAX_HEALTH.name().toLowerCase(Locale.ENGLISH), statusDataFlags.contains(StatusDataFlag.SET_MAX_HEALTH));
|
||||
settings.getSynchronizationFeatures().put(Settings.SynchronizationFeature.HUNGER.name().toLowerCase(Locale.ENGLISH), statusDataFlags.contains(StatusDataFlag.SET_HUNGER));
|
||||
settings.getSynchronizationFeatures().put(Settings.SynchronizationFeature.EXPERIENCE.name().toLowerCase(Locale.ENGLISH), statusDataFlags.contains(StatusDataFlag.SET_EXPERIENCE));
|
||||
settings.getSynchronizationFeatures().put(Settings.SynchronizationFeature.INVENTORIES.name().toLowerCase(Locale.ENGLISH), statusDataFlags.contains(StatusDataFlag.SET_SELECTED_ITEM_SLOT));
|
||||
settings.getSynchronizationFeatures().put(Settings.SynchronizationFeature.LOCATION.name().toLowerCase(Locale.ENGLISH), statusDataFlags.contains(StatusDataFlag.SET_GAME_MODE) || statusDataFlags.contains(StatusDataFlag.SET_FLYING));
|
||||
return setStatus(statusData, settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the player's {@link StatusData}
|
||||
*
|
||||
* @param statusData the player's {@link StatusData}
|
||||
* @param settings settings, containing information about which features should be synced
|
||||
* @return a future returning void when complete
|
||||
*/
|
||||
public abstract CompletableFuture<Void> setStatus(@NotNull StatusData statusData, @NotNull Settings settings);
|
||||
|
||||
/**
|
||||
* Get the player's inventory {@link ItemData} contents
|
||||
*
|
||||
* @return The player's inventory {@link ItemData} contents
|
||||
*/
|
||||
public abstract CompletableFuture<ItemData> getInventory();
|
||||
|
||||
/**
|
||||
* Set the player's {@link ItemData}
|
||||
*
|
||||
* @param itemData The player's {@link ItemData}
|
||||
* @return a future returning void when complete
|
||||
*/
|
||||
public abstract CompletableFuture<Void> setInventory(@NotNull ItemData itemData);
|
||||
|
||||
/**
|
||||
* Get the player's ender chest {@link ItemData} contents
|
||||
*
|
||||
* @return The player's ender chest {@link ItemData} contents
|
||||
*/
|
||||
public abstract CompletableFuture<ItemData> getEnderChest();
|
||||
|
||||
/**
|
||||
* Set the player's {@link ItemData}
|
||||
*
|
||||
* @param enderChestData The player's {@link ItemData}
|
||||
* @return a future returning void when complete
|
||||
*/
|
||||
public abstract CompletableFuture<Void> setEnderChest(@NotNull ItemData enderChestData);
|
||||
|
||||
|
||||
/**
|
||||
* Get the player's {@link PotionEffectData}
|
||||
*
|
||||
* @return The player's {@link PotionEffectData}
|
||||
*/
|
||||
public abstract CompletableFuture<PotionEffectData> getPotionEffects();
|
||||
|
||||
/**
|
||||
* Set the player's {@link PotionEffectData}
|
||||
*
|
||||
* @param potionEffectData The player's {@link PotionEffectData}
|
||||
* @return a future returning void when complete
|
||||
*/
|
||||
public abstract CompletableFuture<Void> setPotionEffects(@NotNull PotionEffectData potionEffectData);
|
||||
|
||||
/**
|
||||
* Get the player's set of {@link AdvancementData}
|
||||
*
|
||||
* @return the player's set of {@link AdvancementData}
|
||||
*/
|
||||
public abstract CompletableFuture<List<AdvancementData>> getAdvancements();
|
||||
|
||||
/**
|
||||
* Set the player's {@link AdvancementData}
|
||||
*
|
||||
* @param advancementData List of the player's {@link AdvancementData}
|
||||
* @return a future returning void when complete
|
||||
*/
|
||||
public abstract CompletableFuture<Void> setAdvancements(@NotNull List<AdvancementData> advancementData);
|
||||
|
||||
/**
|
||||
* Get the player's {@link StatisticsData}
|
||||
*
|
||||
* @return The player's {@link StatisticsData}
|
||||
*/
|
||||
public abstract CompletableFuture<StatisticsData> getStatistics();
|
||||
|
||||
/**
|
||||
* Set the player's {@link StatisticsData}
|
||||
*
|
||||
* @param statisticsData The player's {@link StatisticsData}
|
||||
* @return a future returning void when complete
|
||||
*/
|
||||
public abstract CompletableFuture<Void> setStatistics(@NotNull StatisticsData statisticsData);
|
||||
|
||||
/**
|
||||
* Get the player's {@link LocationData}
|
||||
*
|
||||
* @return the player's {@link LocationData}
|
||||
*/
|
||||
public abstract CompletableFuture<LocationData> getLocation();
|
||||
|
||||
/**
|
||||
* Set the player's {@link LocationData}
|
||||
*
|
||||
* @param locationData the player's {@link LocationData}
|
||||
* @return a future returning void when complete
|
||||
*/
|
||||
public abstract CompletableFuture<Void> setLocation(@NotNull LocationData locationData);
|
||||
|
||||
/**
|
||||
* Get the player's {@link PersistentDataContainerData}
|
||||
*
|
||||
* @return The player's {@link PersistentDataContainerData} when fetched
|
||||
*/
|
||||
public abstract CompletableFuture<PersistentDataContainerData> getPersistentDataContainer();
|
||||
|
||||
/**
|
||||
* Set the player's {@link PersistentDataContainerData}
|
||||
*
|
||||
* @param persistentDataContainerData The player's {@link PersistentDataContainerData} to set
|
||||
* @return A future returning void when complete
|
||||
*/
|
||||
public abstract CompletableFuture<Void> setPersistentDataContainer(@NotNull PersistentDataContainerData persistentDataContainerData);
|
||||
|
||||
/**
|
||||
* Indicates if the player has gone offline
|
||||
*
|
||||
* @return {@code true} if the player has left the server; {@code false} otherwise
|
||||
*/
|
||||
public abstract boolean isOffline();
|
||||
|
||||
/**
|
||||
* Returns the implementing Minecraft server version
|
||||
*
|
||||
* @return The Minecraft server version
|
||||
*/
|
||||
@NotNull
|
||||
public abstract Version getMinecraftVersion();
|
||||
|
||||
/**
|
||||
* Get the player's adventure {@link Audience}
|
||||
*
|
||||
* @return the player's {@link Audience}
|
||||
*/
|
||||
@NotNull
|
||||
public abstract Audience getAudience();
|
||||
|
||||
/**
|
||||
* Send a message to this player
|
||||
*
|
||||
* @param component the {@link Component} message to send
|
||||
*/
|
||||
public void sendMessage(@NotNull Component component) {
|
||||
getAudience().sendMessage(component);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a MineDown-formatted message to this player
|
||||
*
|
||||
* @param mineDown the parsed {@link MineDown} to send
|
||||
*/
|
||||
public void sendMessage(@NotNull MineDown mineDown) {
|
||||
sendMessage(mineDown
|
||||
.disable(MineDownParser.Option.SIMPLE_FORMATTING)
|
||||
.replace().toComponent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a MineDown-formatted action bar message to this player
|
||||
*
|
||||
* @param mineDown the parsed {@link MineDown} to send
|
||||
*/
|
||||
public void sendActionBar(@NotNull MineDown mineDown) {
|
||||
getAudience().sendActionBar(mineDown
|
||||
.disable(MineDownParser.Option.SIMPLE_FORMATTING)
|
||||
.replace().toComponent());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @param node The permission node string
|
||||
* @return {@code true} if the player has permission node; {@code false} otherwise
|
||||
*/
|
||||
public abstract boolean hasPermission(@NotNull String node);
|
||||
|
||||
/**
|
||||
* Show a GUI chest menu to the player, containing the given {@link ItemData}
|
||||
*
|
||||
* @param itemData Item data to be shown in the GUI
|
||||
* @param editable If the player should be able to remove, replace and move around the items
|
||||
* @param minimumRows The minimum number of rows to show in the chest menu
|
||||
* @param title The title of the chest menu, as a {@link MineDown} locale
|
||||
* @return A future returning the {@link ItemData} in the chest menu when the player closes it
|
||||
* @since 2.1
|
||||
*/
|
||||
public abstract CompletableFuture<Optional<ItemData>> showMenu(@NotNull ItemData itemData, boolean editable,
|
||||
int minimumRows, @NotNull MineDown title);
|
||||
|
||||
/**
|
||||
* Returns true if the player is dead
|
||||
*
|
||||
* @return true if the player is dead
|
||||
*/
|
||||
public abstract boolean isDead();
|
||||
|
||||
/**
|
||||
* Apply {@link UserData} to a player, updating their inventory, status, statistics, etc. as per the config.
|
||||
* <p>
|
||||
* 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.
|
||||
*
|
||||
* @param plugin The plugin instance
|
||||
* @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 HuskSync plugin) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
// Prevent synchronising user data from newer versions of Minecraft
|
||||
if (Version.fromString(data.getMinecraftVersion()).compareTo(plugin.getMinecraftVersion()) > 0) {
|
||||
plugin.log(Level.SEVERE, "Cannot set data for " + username +
|
||||
" because the Minecraft version of their user data (" + data.getMinecraftVersion() +
|
||||
") is newer than the server's Minecraft version (" + plugin.getMinecraftVersion() + ").");
|
||||
return false;
|
||||
}
|
||||
// Prevent synchronising user data from newer versions of the plugin
|
||||
if (data.getFormatVersion() > UserData.CURRENT_FORMAT_VERSION) {
|
||||
plugin.log(Level.SEVERE, "Cannot set data for " + username +
|
||||
" because the format version of their user data (v" + data.getFormatVersion() +
|
||||
") is newer than the current format version (v" + UserData.CURRENT_FORMAT_VERSION + ").");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fire the PreSyncEvent
|
||||
final PreSyncEvent preSyncEvent = (PreSyncEvent) plugin.getEventCannon().firePreSyncEvent(this, data).join();
|
||||
final UserData finalData = preSyncEvent.getUserData();
|
||||
final List<CompletableFuture<Void>> dataSetOperations = new ArrayList<>() {{
|
||||
if (!isOffline() && !preSyncEvent.isCancelled()) {
|
||||
final Settings settings = plugin.getSettings();
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.INVENTORIES)) {
|
||||
finalData.getInventory().ifPresent(itemData -> add(setInventory(itemData)));
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.ENDER_CHESTS)) {
|
||||
finalData.getEnderChest().ifPresent(itemData -> add(setEnderChest(itemData)));
|
||||
}
|
||||
finalData.getStatus().ifPresent(statusData -> add(setStatus(statusData, settings)));
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.POTION_EFFECTS)) {
|
||||
finalData.getPotionEffects().ifPresent(potionEffectData -> add(setPotionEffects(potionEffectData)));
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.ADVANCEMENTS)) {
|
||||
finalData.getAdvancements().ifPresent(advancementData -> add(setAdvancements(advancementData)));
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.STATISTICS)) {
|
||||
finalData.getStatistics().ifPresent(statisticData -> add(setStatistics(statisticData)));
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.LOCATION)) {
|
||||
finalData.getLocation().ifPresent(locationData -> add(setLocation(locationData)));
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.PERSISTENT_DATA_CONTAINER)) {
|
||||
finalData.getPersistentDataContainer().ifPresent(persistentDataContainerData ->
|
||||
add(setPersistentDataContainer(persistentDataContainerData)));
|
||||
}
|
||||
}
|
||||
}};
|
||||
// Apply operations in parallel, join when complete
|
||||
return CompletableFuture.allOf(dataSetOperations.toArray(new CompletableFuture[0])).thenApply(unused -> true)
|
||||
.exceptionally(exception -> {
|
||||
// Handle synchronisation exceptions
|
||||
plugin.log(Level.SEVERE, "Failed to set data for player " + username + " (" + exception.getMessage() + ")");
|
||||
exception.printStackTrace();
|
||||
return false;
|
||||
}).join();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the player's current {@link UserData} in an {@link Optional}.
|
||||
* <p>
|
||||
* Since v2.1, this method will respect the data synchronisation settings; user data will only be as big as the
|
||||
* enabled synchronisation values set in the config file
|
||||
* <p>
|
||||
* Also note that if the {@code SYNCHRONIZATION_SAVE_DEAD_PLAYER_INVENTORIES} ConfigOption has been set,
|
||||
* the user's inventory will only be returned if the player is alive.
|
||||
* <p>
|
||||
* If the user data could not be returned due to an exception, the optional will return empty
|
||||
*
|
||||
* @param plugin The plugin instance
|
||||
*/
|
||||
public final CompletableFuture<Optional<UserData>> getUserData(@NotNull HuskSync plugin) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
final UserDataBuilder builder = UserData.builder(getMinecraftVersion());
|
||||
final List<CompletableFuture<Void>> dataGetOperations = new ArrayList<>() {{
|
||||
if (!isOffline()) {
|
||||
final Settings settings = plugin.getSettings();
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.INVENTORIES)) {
|
||||
if (isDead() && settings.isSynchroniseDeadPlayersChangingServer()) {
|
||||
plugin.debug("Player " + username + " is dead, so their inventory will be set to empty.");
|
||||
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));
|
||||
}
|
||||
}
|
||||
}};
|
||||
|
||||
// Apply operations in parallel, join when complete
|
||||
CompletableFuture.allOf(dataGetOperations.toArray(new CompletableFuture[0])).join();
|
||||
return Optional.of(builder.build());
|
||||
}).exceptionally(exception -> {
|
||||
plugin.log(Level.SEVERE, "Failed to get user data from online player " + username + " (" + exception.getMessage() + ")");
|
||||
exception.printStackTrace();
|
||||
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();
|
||||
}
|
||||
@@ -28,14 +28,24 @@ public enum RedisKeyType {
|
||||
DATA_UPDATE(10),
|
||||
SERVER_SWITCH(10);
|
||||
|
||||
public final int timeToLive;
|
||||
private final int timeToLive;
|
||||
|
||||
RedisKeyType(int timeToLive) {
|
||||
this.timeToLive = timeToLive;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getKeyPrefix() {
|
||||
return RedisManager.KEY_NAMESPACE.toLowerCase(Locale.ENGLISH) + ":" + RedisManager.clusterId.toLowerCase(Locale.ENGLISH) + ":" + name().toLowerCase(Locale.ENGLISH);
|
||||
public String getKeyPrefix(@NotNull String clusterId) {
|
||||
return String.format(
|
||||
"%s:%s:%s",
|
||||
RedisManager.KEY_NAMESPACE.toLowerCase(Locale.ENGLISH),
|
||||
clusterId.toLowerCase(Locale.ENGLISH),
|
||||
name().toLowerCase(Locale.ENGLISH)
|
||||
);
|
||||
}
|
||||
|
||||
public int getTimeToLive() {
|
||||
return timeToLive;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,21 +19,24 @@
|
||||
|
||||
package net.william278.husksync.redis;
|
||||
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.player.User;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.Blocking;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import redis.clients.jedis.*;
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
import redis.clients.jedis.JedisPoolConfig;
|
||||
import redis.clients.jedis.JedisPubSub;
|
||||
import redis.clients.jedis.exceptions.JedisException;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Manages the connection to the Redis server, handling the caching of user data
|
||||
@@ -41,216 +44,247 @@ import java.util.concurrent.CompletableFuture;
|
||||
public class RedisManager extends JedisPubSub {
|
||||
|
||||
protected static final String KEY_NAMESPACE = "husksync:";
|
||||
protected static String clusterId = "";
|
||||
|
||||
private final HuskSync plugin;
|
||||
private final JedisPoolConfig jedisPoolConfig;
|
||||
private final String redisHost;
|
||||
private final int redisPort;
|
||||
private final String redisPassword;
|
||||
private final boolean redisUseSsl;
|
||||
private final String clusterId;
|
||||
private JedisPool jedisPool;
|
||||
private final Map<UUID, CompletableFuture<Optional<DataSnapshot.Packed>>> pendingRequests;
|
||||
|
||||
public RedisManager(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
clusterId = plugin.getSettings().getClusterId();
|
||||
|
||||
// Set redis credentials
|
||||
this.redisHost = plugin.getSettings().getRedisHost();
|
||||
this.redisPort = plugin.getSettings().getRedisPort();
|
||||
this.redisPassword = plugin.getSettings().getRedisPassword();
|
||||
this.redisUseSsl = plugin.getSettings().isRedisUseSsl();
|
||||
|
||||
// Configure the jedis pool
|
||||
this.jedisPoolConfig = new JedisPoolConfig();
|
||||
this.jedisPoolConfig.setMaxIdle(0);
|
||||
this.jedisPoolConfig.setTestOnBorrow(true);
|
||||
this.jedisPoolConfig.setTestOnReturn(true);
|
||||
this.clusterId = plugin.getSettings().getClusterId();
|
||||
this.pendingRequests = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the redis connection pool
|
||||
*
|
||||
* @return a future returning void when complete
|
||||
*/
|
||||
public boolean initialize() {
|
||||
if (redisPassword.isBlank()) {
|
||||
jedisPool = new JedisPool(jedisPoolConfig, redisHost, redisPort, 0, redisUseSsl);
|
||||
} else {
|
||||
jedisPool = new JedisPool(jedisPoolConfig, redisHost, redisPort, 0, redisPassword, redisUseSsl);
|
||||
}
|
||||
@Blocking
|
||||
public void initialize() throws IllegalStateException {
|
||||
final String password = plugin.getSettings().getRedisPassword();
|
||||
final String host = plugin.getSettings().getRedisHost();
|
||||
final int port = plugin.getSettings().getRedisPort();
|
||||
final boolean useSSL = plugin.getSettings().redisUseSsl();
|
||||
|
||||
// Create the jedis pool
|
||||
final JedisPoolConfig config = new JedisPoolConfig();
|
||||
config.setMaxIdle(0);
|
||||
config.setTestOnBorrow(true);
|
||||
config.setTestOnReturn(true);
|
||||
this.jedisPool = password.isEmpty()
|
||||
? new JedisPool(config, host, port, 0, useSSL)
|
||||
: new JedisPool(config, host, port, 0, password, useSSL);
|
||||
|
||||
// Ping the server to check the connection
|
||||
try {
|
||||
jedisPool.getResource().ping();
|
||||
} catch (JedisException e) {
|
||||
return false;
|
||||
throw new IllegalStateException("Failed to establish connection with the Redis server. "
|
||||
+ "Please check the supplied credentials in the config file", e);
|
||||
}
|
||||
CompletableFuture.runAsync(this::subscribe);
|
||||
return true;
|
||||
|
||||
// Subscribe using a thread (rather than a task)
|
||||
new Thread(this::subscribe, "husksync:redis_subscriber").start();
|
||||
}
|
||||
|
||||
@Blocking
|
||||
private void subscribe() {
|
||||
try (final Jedis subscriber = redisPassword.isBlank() ? new Jedis(redisHost, redisPort, 0, redisUseSsl) :
|
||||
new Jedis(redisHost, redisPort, DefaultJedisClientConfig.builder()
|
||||
.password(redisPassword).timeoutMillis(0).ssl(redisUseSsl).build())) {
|
||||
subscriber.connect();
|
||||
subscriber.subscribe(this, Arrays.stream(RedisMessageType.values())
|
||||
.map(RedisMessageType::getMessageChannel)
|
||||
.toArray(String[]::new));
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
jedis.subscribe(
|
||||
this,
|
||||
Arrays.stream(RedisMessageType.values())
|
||||
.map(type -> type.getMessageChannel(clusterId))
|
||||
.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) {
|
||||
final RedisMessageType messageType = RedisMessageType.getTypeFromChannel(channel, clusterId).orElse(null);
|
||||
if (messageType == null) {
|
||||
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().getNotificationDisplaySlot()) {
|
||||
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);
|
||||
final RedisMessage redisMessage = RedisMessage.fromJson(plugin, message);
|
||||
switch (messageType) {
|
||||
case UPDATE_USER_DATA -> plugin.getOnlineUser(redisMessage.getTargetUuid()).ifPresent(
|
||||
user -> user.applySnapshot(
|
||||
DataSnapshot.deserialize(plugin, redisMessage.getPayload()),
|
||||
DataSnapshot.UpdateCause.UPDATED
|
||||
)
|
||||
);
|
||||
case REQUEST_USER_DATA -> plugin.getOnlineUser(redisMessage.getTargetUuid()).ifPresent(
|
||||
user -> RedisMessage.create(
|
||||
UUID.fromString(new String(redisMessage.getPayload(), StandardCharsets.UTF_8)),
|
||||
user.createSnapshot(DataSnapshot.SaveCause.INVENTORY_COMMAND).asBytes(plugin)
|
||||
).dispatch(plugin, RedisMessageType.RETURN_USER_DATA)
|
||||
);
|
||||
case RETURN_USER_DATA -> {
|
||||
final CompletableFuture<Optional<DataSnapshot.Packed>> future = pendingRequests.get(
|
||||
redisMessage.getTargetUuid()
|
||||
);
|
||||
if (future != null) {
|
||||
future.complete(Optional.of(DataSnapshot.deserialize(plugin, redisMessage.getPayload())));
|
||||
pendingRequests.remove(redisMessage.getTargetUuid());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
protected void sendMessage(@NotNull String channel, @NotNull String message) {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
jedis.publish(channel, message);
|
||||
}
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> sendUserDataUpdate(@NotNull User user, @NotNull UserData userData) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
final RedisMessage redisMessage = new RedisMessage(user.uuid, plugin.getDataAdapter().toBytes(userData));
|
||||
redisMessage.dispatch(this, RedisMessageType.UPDATE_USER_DATA);
|
||||
public void sendUserDataUpdate(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
plugin.runAsync(() -> {
|
||||
final RedisMessage redisMessage = RedisMessage.create(user.getUuid(), data.asBytes(plugin));
|
||||
redisMessage.dispatch(plugin, RedisMessageType.UPDATE_USER_DATA);
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<Optional<DataSnapshot.Packed>> getUserData(@NotNull UUID requestId, @NotNull User user) {
|
||||
return plugin.getOnlineUser(user.getUuid())
|
||||
.map(online -> CompletableFuture.completedFuture(
|
||||
Optional.of(online.createSnapshot(DataSnapshot.SaveCause.API)))
|
||||
)
|
||||
.orElse(this.requestData(requestId, user));
|
||||
}
|
||||
|
||||
private CompletableFuture<Optional<DataSnapshot.Packed>> requestData(@NotNull UUID requestId, @NotNull User user) {
|
||||
final CompletableFuture<Optional<DataSnapshot.Packed>> future = new CompletableFuture<>();
|
||||
pendingRequests.put(requestId, future);
|
||||
plugin.runAsync(() -> {
|
||||
final RedisMessage redisMessage = RedisMessage.create(
|
||||
user.getUuid(),
|
||||
requestId.toString().getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
redisMessage.dispatch(plugin, RedisMessageType.REQUEST_USER_DATA);
|
||||
});
|
||||
return future.orTimeout(
|
||||
plugin.getSettings().getNetworkLatencyMilliseconds(),
|
||||
TimeUnit.MILLISECONDS
|
||||
)
|
||||
.exceptionally(throwable -> {
|
||||
pendingRequests.remove(requestId);
|
||||
return Optional.empty();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a user's data to the Redis server
|
||||
*
|
||||
* @param user the user to set data for
|
||||
* @param userData the user's data to set
|
||||
* @return a future returning void when complete
|
||||
* @param user the user to set data for
|
||||
* @param data the user's data to set
|
||||
*/
|
||||
public CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData) {
|
||||
try {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
// Set the user's data as a compressed byte array of the json using Snappy
|
||||
jedis.setex(getKey(RedisKeyType.DATA_UPDATE, user.uuid),
|
||||
RedisKeyType.DATA_UPDATE.timeToLive,
|
||||
plugin.getDataAdapter().toBytes(userData));
|
||||
|
||||
// Debug logging
|
||||
plugin.debug("[" + user.username + "] Set " + RedisKeyType.DATA_UPDATE.name()
|
||||
+ " key to redis at: " +
|
||||
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> setUserServerSwitch(@NotNull User user) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
public void setUserData(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
plugin.runAsync(() -> {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
jedis.setex(getKey(RedisKeyType.SERVER_SWITCH, user.uuid),
|
||||
RedisKeyType.SERVER_SWITCH.timeToLive, new byte[0]);
|
||||
plugin.debug("[" + user.username + "] Set " + RedisKeyType.SERVER_SWITCH.name()
|
||||
+ " key to redis at: " +
|
||||
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
jedis.setex(
|
||||
getKey(RedisKeyType.DATA_UPDATE, user.getUuid(), clusterId),
|
||||
RedisKeyType.DATA_UPDATE.getTimeToLive(),
|
||||
data.asBytes(plugin)
|
||||
);
|
||||
plugin.debug(String.format("[%s] Set %s key to redis at: %s", user.getUsername(),
|
||||
RedisKeyType.DATA_UPDATE.name(), new SimpleDateFormat("mm:ss.SSS").format(new Date())));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred setting a user's server switch", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a user's server switch to the Redis server
|
||||
*
|
||||
* @param user the user to set the server switch for
|
||||
* @return a future returning void when complete
|
||||
*/
|
||||
public CompletableFuture<Void> setUserServerSwitch(@NotNull User user) {
|
||||
final CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
plugin.runAsync(() -> {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
jedis.setex(
|
||||
getKey(RedisKeyType.SERVER_SWITCH, user.getUuid(), clusterId),
|
||||
RedisKeyType.SERVER_SWITCH.getTimeToLive(), new byte[0]
|
||||
);
|
||||
future.complete(null);
|
||||
plugin.debug(String.format("[%s] Set %s key to redis at: %s", user.getUsername(),
|
||||
RedisKeyType.SERVER_SWITCH.name(), new SimpleDateFormat("mm:ss.SSS").format(new Date())));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred setting a user's server switch", e);
|
||||
}
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a user's data from the Redis server and consume the key if found
|
||||
*
|
||||
* @param user The user to fetch data for
|
||||
* @return The user's data, if it's present on the database. Otherwise, an empty optional.
|
||||
*/
|
||||
public CompletableFuture<Optional<UserData>> getUserData(@NotNull User user) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
final byte[] key = getKey(RedisKeyType.DATA_UPDATE, user.uuid);
|
||||
final byte[] dataByteArray = jedis.get(key);
|
||||
if (dataByteArray == null) {
|
||||
plugin.debug("[" + user.username + "] Could not read " +
|
||||
RedisKeyType.DATA_UPDATE.name() + " key from redis at: " +
|
||||
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
|
||||
return Optional.empty();
|
||||
}
|
||||
plugin.debug("[" + user.username + "] Successfully read "
|
||||
+ RedisKeyType.DATA_UPDATE.name() + " key from redis at: " +
|
||||
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
|
||||
|
||||
// Consume the key (delete from redis)
|
||||
jedis.del(key);
|
||||
|
||||
// Use Snappy to decompress the json
|
||||
return Optional.of(plugin.getDataAdapter().fromBytes(dataByteArray));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
public Optional<DataSnapshot.Packed> getUserData(@NotNull User user) {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
final byte[] key = getKey(RedisKeyType.DATA_UPDATE, user.getUuid(), clusterId);
|
||||
final byte[] dataByteArray = jedis.get(key);
|
||||
if (dataByteArray == null) {
|
||||
plugin.debug("[" + user.getUsername() + "] Could not read " +
|
||||
RedisKeyType.DATA_UPDATE.name() + " key from redis at: " +
|
||||
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
|
||||
return Optional.empty();
|
||||
}
|
||||
});
|
||||
plugin.debug("[" + user.getUsername() + "] Successfully read "
|
||||
+ RedisKeyType.DATA_UPDATE.name() + " key from redis at: " +
|
||||
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
|
||||
|
||||
// Consume the key (delete from redis)
|
||||
jedis.del(key);
|
||||
|
||||
// Use Snappy to decompress the json
|
||||
return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray));
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred fetching a user's data from redis", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public CompletableFuture<Boolean> getUserServerSwitch(@NotNull User user) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
final byte[] key = getKey(RedisKeyType.SERVER_SWITCH, user.uuid);
|
||||
final byte[] readData = jedis.get(key);
|
||||
if (readData == null) {
|
||||
plugin.debug("[" + user.username + "] Could not read " +
|
||||
RedisKeyType.SERVER_SWITCH.name() + " key from redis at: " +
|
||||
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
|
||||
return false;
|
||||
}
|
||||
plugin.debug("[" + user.username + "] Successfully read "
|
||||
+ RedisKeyType.SERVER_SWITCH.name() + " key from redis at: " +
|
||||
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
|
||||
|
||||
// Consume the key (delete from redis)
|
||||
jedis.del(key);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
public boolean getUserServerSwitch(@NotNull User user) {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
final byte[] key = getKey(RedisKeyType.SERVER_SWITCH, user.getUuid(), clusterId);
|
||||
final byte[] readData = jedis.get(key);
|
||||
if (readData == null) {
|
||||
plugin.debug("[" + user.getUsername() + "] Could not read " +
|
||||
RedisKeyType.SERVER_SWITCH.name() + " key from redis at: " +
|
||||
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
|
||||
return false;
|
||||
}
|
||||
});
|
||||
plugin.debug("[" + user.getUsername() + "] Successfully read "
|
||||
+ RedisKeyType.SERVER_SWITCH.name() + " key from redis at: " +
|
||||
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
|
||||
|
||||
// Consume the key (delete from redis)
|
||||
jedis.del(key);
|
||||
return true;
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "An exception occurred fetching a user's server switch from redis", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void close() {
|
||||
public void terminate() {
|
||||
if (jedisPool != null) {
|
||||
if (!jedisPool.isClosed()) {
|
||||
jedisPool.close();
|
||||
}
|
||||
}
|
||||
this.unsubscribe();
|
||||
}
|
||||
|
||||
private static byte[] getKey(@NotNull RedisKeyType keyType, @NotNull UUID uuid) {
|
||||
return (keyType.getKeyPrefix() + ":" + uuid).getBytes(StandardCharsets.UTF_8);
|
||||
private static byte[] getKey(@NotNull RedisKeyType keyType, @NotNull UUID uuid, @NotNull String clusterId) {
|
||||
return String.format("%s:%s", keyType.getKeyPrefix(clusterId), uuid).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,34 +19,62 @@
|
||||
|
||||
package net.william278.husksync.redis;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.Adaptable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class RedisMessage {
|
||||
public class RedisMessage implements Adaptable {
|
||||
|
||||
public UUID targetUserUuid;
|
||||
public byte[] data;
|
||||
@SerializedName("target_uuid")
|
||||
private UUID targetUuid;
|
||||
@SerializedName("payload")
|
||||
private byte[] payload;
|
||||
|
||||
public RedisMessage(@NotNull UUID targetUserUuid, byte[] message) {
|
||||
this.targetUserUuid = targetUserUuid;
|
||||
this.data = message;
|
||||
private RedisMessage(@NotNull UUID targetUuid, byte[] message) {
|
||||
this.setTargetUuid(targetUuid);
|
||||
this.setPayload(message);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public RedisMessage() {
|
||||
}
|
||||
|
||||
public void dispatch(@NotNull RedisManager redisManager, @NotNull RedisMessageType type) {
|
||||
CompletableFuture.runAsync(() -> redisManager.sendMessage(type.getMessageChannel(),
|
||||
new GsonBuilder().create().toJson(this)));
|
||||
@NotNull
|
||||
public static RedisMessage create(@NotNull UUID targetUuid, byte[] message) {
|
||||
return new RedisMessage(targetUuid, message);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static RedisMessage fromJson(@NotNull String json) throws JsonSyntaxException {
|
||||
return new GsonBuilder().create().fromJson(json, RedisMessage.class);
|
||||
public static RedisMessage fromJson(@NotNull HuskSync plugin, @NotNull String json) throws JsonSyntaxException {
|
||||
return plugin.getGson().fromJson(json, RedisMessage.class);
|
||||
}
|
||||
|
||||
public void dispatch(@NotNull HuskSync plugin, @NotNull RedisMessageType type) {
|
||||
plugin.runAsync(() -> plugin.getRedisManager().sendMessage(
|
||||
type.getMessageChannel(plugin.getSettings().getClusterId()),
|
||||
plugin.getGson().toJson(this)
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public UUID getTargetUuid() {
|
||||
return targetUuid;
|
||||
}
|
||||
|
||||
public void setTargetUuid(@NotNull UUID targetUuid) {
|
||||
this.targetUuid = targetUuid;
|
||||
}
|
||||
|
||||
public byte[] getPayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
public void setPayload(byte[] payload) {
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -27,17 +27,24 @@ import java.util.Optional;
|
||||
|
||||
public enum RedisMessageType {
|
||||
|
||||
UPDATE_USER_DATA;
|
||||
UPDATE_USER_DATA,
|
||||
REQUEST_USER_DATA,
|
||||
RETURN_USER_DATA;
|
||||
|
||||
@NotNull
|
||||
public String getMessageChannel() {
|
||||
return RedisManager.KEY_NAMESPACE.toLowerCase(Locale.ENGLISH) + ":" + RedisManager.clusterId.toLowerCase(Locale.ENGLISH)
|
||||
+ ":" + name().toLowerCase(Locale.ENGLISH);
|
||||
public String getMessageChannel(@NotNull String clusterId) {
|
||||
return String.format(
|
||||
"%s:%s:%s",
|
||||
RedisManager.KEY_NAMESPACE.toLowerCase(Locale.ENGLISH),
|
||||
clusterId.toLowerCase(Locale.ENGLISH),
|
||||
name().toLowerCase(Locale.ENGLISH)
|
||||
);
|
||||
}
|
||||
|
||||
public static Optional<RedisMessageType> getTypeFromChannel(@NotNull String messageChannel) {
|
||||
return Arrays.stream(values()).filter(messageType -> messageType.getMessageChannel()
|
||||
.equalsIgnoreCase(messageChannel)).findFirst();
|
||||
public static Optional<RedisMessageType> getTypeFromChannel(@NotNull String channel, @NotNull String clusterId) {
|
||||
return Arrays.stream(values())
|
||||
.filter(messageType -> messageType.getMessageChannel(clusterId).equalsIgnoreCase(channel))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,15 +17,26 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync;
|
||||
package net.william278.husksync.user;
|
||||
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Indicates an exception occurred while initializing the HuskSync plugin
|
||||
*/
|
||||
public class HuskSyncInitializationException extends IllegalStateException {
|
||||
public HuskSyncInitializationException(@NotNull String message) {
|
||||
super(message);
|
||||
public interface CommandUser {
|
||||
|
||||
@NotNull
|
||||
Audience getAudience();
|
||||
|
||||
boolean hasPermission(@NotNull String permission);
|
||||
|
||||
default void sendMessage(@NotNull Component component) {
|
||||
getAudience().sendMessage(component);
|
||||
}
|
||||
|
||||
default void sendMessage(@NotNull MineDown mineDown) {
|
||||
this.sendMessage(mineDown.toComponent());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,25 +17,28 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
package net.william278.husksync.user;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Stores potion effect data
|
||||
*/
|
||||
public class PotionEffectData {
|
||||
public final class ConsoleUser implements CommandUser {
|
||||
|
||||
@SerializedName("serialized_potion_effects")
|
||||
public String serializedPotionEffects;
|
||||
@NotNull
|
||||
private final Audience audience;
|
||||
|
||||
public PotionEffectData(@NotNull final String serializedPotionEffects) {
|
||||
this.serializedPotionEffects = serializedPotionEffects;
|
||||
public ConsoleUser(@NotNull Audience console) {
|
||||
this.audience = console;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected PotionEffectData() {
|
||||
@Override
|
||||
@NotNull
|
||||
public Audience getAudience() {
|
||||
return audience;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(@NotNull String permission) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.user;
|
||||
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import de.themoep.minedown.adventure.MineDownParser;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.data.Identifier;
|
||||
import net.william278.husksync.data.UserDataHolder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Represents a logged-in {@link User}
|
||||
*/
|
||||
public abstract class OnlineUser extends User implements CommandUser, UserDataHolder {
|
||||
|
||||
public OnlineUser(@NotNull UUID uuid, @NotNull String username) {
|
||||
super(uuid, username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the player has gone offline
|
||||
*
|
||||
* @return {@code true} if the player has left the server; {@code false} otherwise
|
||||
*/
|
||||
public abstract boolean isOffline();
|
||||
|
||||
/**
|
||||
* Get the player's adventure {@link Audience}
|
||||
*
|
||||
* @return the player's {@link Audience}
|
||||
*/
|
||||
@NotNull
|
||||
public abstract Audience getAudience();
|
||||
|
||||
/**
|
||||
* Send a message to this player
|
||||
*
|
||||
* @param component the {@link Component} message to send
|
||||
*/
|
||||
public void sendMessage(@NotNull Component component) {
|
||||
getAudience().sendMessage(component);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a MineDown-formatted message to this player
|
||||
*
|
||||
* @param mineDown the parsed {@link MineDown} to send
|
||||
*/
|
||||
public void sendMessage(@NotNull MineDown mineDown) {
|
||||
sendMessage(mineDown
|
||||
.disable(MineDownParser.Option.SIMPLE_FORMATTING)
|
||||
.replace().toComponent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a MineDown-formatted action bar message to this player
|
||||
*
|
||||
* @param mineDown the parsed {@link MineDown} to send
|
||||
*/
|
||||
public void sendActionBar(@NotNull MineDown mineDown) {
|
||||
getAudience().sendActionBar(mineDown
|
||||
.disable(MineDownParser.Option.SIMPLE_FORMATTING)
|
||||
.replace().toComponent());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 hasIcon 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);
|
||||
|
||||
/**
|
||||
* Show a GUI chest menu to the user
|
||||
*
|
||||
* @param items the items to fill the menu with
|
||||
* @param title the title of the menu
|
||||
* @param editable whether the menu is editable (items can be removed or added)
|
||||
* @param size the size of the menu
|
||||
* @param onClose the action to perform when the menu is closed
|
||||
*/
|
||||
public abstract void showGui(@NotNull Data.Items.Items items, @NotNull MineDown title, boolean editable, int size,
|
||||
@NotNull Consumer<Data.Items.Items> onClose);
|
||||
|
||||
/**
|
||||
* Returns if the player has the permission node
|
||||
*
|
||||
* @param node The permission node string
|
||||
* @return {@code true} if the player has permission node; {@code false} otherwise
|
||||
*/
|
||||
public abstract boolean hasPermission(@NotNull String node);
|
||||
|
||||
|
||||
/**
|
||||
* Set a player's status from a {@link DataSnapshot}
|
||||
*
|
||||
* @param snapshot The {@link DataSnapshot} to set the player's status from
|
||||
*/
|
||||
public void applySnapshot(@NotNull DataSnapshot.Packed snapshot, @NotNull DataSnapshot.UpdateCause cause) {
|
||||
getPlugin().fireEvent(getPlugin().getPreSyncEvent(this, snapshot), (event) -> {
|
||||
if (!isOffline()) {
|
||||
UserDataHolder.super.applySnapshot(
|
||||
event.getData(), (owner) -> completeSync(true, cause, getPlugin())
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a player's synchronization completion
|
||||
*
|
||||
* @param succeeded Whether the synchronization succeeded
|
||||
* @param plugin The plugin instance
|
||||
*/
|
||||
public void completeSync(boolean succeeded, @NotNull DataSnapshot.UpdateCause cause, @NotNull HuskSync plugin) {
|
||||
if (succeeded) {
|
||||
switch (plugin.getSettings().getNotificationDisplaySlot()) {
|
||||
case CHAT -> cause.getCompletedLocale(plugin).ifPresent(this::sendMessage);
|
||||
case ACTION_BAR -> cause.getCompletedLocale(plugin).ifPresent(this::sendActionBar);
|
||||
case TOAST -> cause.getCompletedLocale(plugin)
|
||||
.ifPresent(locale -> this.sendToast(
|
||||
locale, new MineDown(""),
|
||||
"minecraft:bell",
|
||||
"TASK"
|
||||
));
|
||||
}
|
||||
plugin.fireEvent(
|
||||
plugin.getSyncCompleteEvent(this),
|
||||
(event) -> plugin.getLockedPlayers().remove(getUuid())
|
||||
);
|
||||
} else {
|
||||
cause.getFailedLocale(plugin).ifPresent(this::sendMessage);
|
||||
}
|
||||
|
||||
// Ensure the user is in the database
|
||||
plugin.getDatabase().ensureUser(this);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Map<Identifier, Data> getCustomDataStore() {
|
||||
return getPlugin().getPlayerCustomDataStore(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
@@ -17,37 +17,48 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.player;
|
||||
package net.william278.husksync.user;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Represents a user who has their data synchronised by HuskSync
|
||||
* Represents a user who has their data synchronized by HuskSync
|
||||
*/
|
||||
public class User {
|
||||
|
||||
/**
|
||||
* The user's unique account ID
|
||||
*/
|
||||
public final UUID uuid;
|
||||
private final UUID uuid;
|
||||
|
||||
/**
|
||||
* The user's username
|
||||
*/
|
||||
public final String username;
|
||||
private final String username;
|
||||
|
||||
public User(@NotNull UUID uuid, @NotNull String username) {
|
||||
this.username = username;
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's unique account ID
|
||||
*/
|
||||
@NotNull
|
||||
public UUID getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's username
|
||||
*/
|
||||
@NotNull
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
if (object instanceof User other) {
|
||||
return this.uuid.equals(other.uuid);
|
||||
return this.getUuid().equals(other.getUuid());
|
||||
}
|
||||
return super.equals(object);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -22,8 +22,8 @@ package net.william278.husksync.util;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.UserDataSnapshot;
|
||||
import net.william278.husksync.player.User;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.*;
|
||||
@@ -31,38 +31,37 @@ import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Utility class for dumping {@link UserDataSnapshot}s to a file or as a paste on the web
|
||||
* Utility class for dumping {@link DataSnapshot}s to a file or as a paste on the web
|
||||
*/
|
||||
public class DataDumper {
|
||||
|
||||
private static final String LOGS_SITE_ENDPOINT = "https://api.mclo.gs/1/log";
|
||||
|
||||
private final HuskSync plugin;
|
||||
private final UserDataSnapshot dataSnapshot;
|
||||
private final DataSnapshot.Packed snapshot;
|
||||
private final User user;
|
||||
|
||||
private DataDumper(@NotNull UserDataSnapshot dataSnapshot,
|
||||
@NotNull User user, @NotNull HuskSync implementor) {
|
||||
this.dataSnapshot = dataSnapshot;
|
||||
private DataDumper(@NotNull DataSnapshot.Packed snapshot, @NotNull User user, @NotNull HuskSync implementor) {
|
||||
this.snapshot = snapshot;
|
||||
this.user = user;
|
||||
this.plugin = implementor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link DataDumper} of the given {@link UserDataSnapshot}
|
||||
* Create a {@link DataDumper} of the given {@link DataSnapshot}
|
||||
*
|
||||
* @param dataSnapshot The {@link UserDataSnapshot} to dump
|
||||
* @param dataSnapshot The {@link DataSnapshot} to dump
|
||||
* @param user The {@link User} whose data is being dumped
|
||||
* @param plugin The implementing {@link HuskSync} plugin
|
||||
* @return A {@link DataDumper} for the given {@link UserDataSnapshot}
|
||||
* @return A {@link DataDumper} for the given {@link DataSnapshot}
|
||||
*/
|
||||
public static DataDumper create(@NotNull UserDataSnapshot dataSnapshot,
|
||||
public static DataDumper create(@NotNull DataSnapshot.Packed dataSnapshot,
|
||||
@NotNull User user, @NotNull HuskSync plugin) {
|
||||
return new DataDumper(dataSnapshot, user, plugin);
|
||||
}
|
||||
@@ -75,7 +74,7 @@ public class DataDumper {
|
||||
@Override
|
||||
@NotNull
|
||||
public String toString() {
|
||||
return plugin.getDataAdapter().toJson(dataSnapshot.userData(), true);
|
||||
return snapshot.asJson(plugin);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -128,7 +127,7 @@ public class DataDumper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump the {@link UserDataSnapshot} to a file and return the file name
|
||||
* Dump the {@link DataSnapshot} to a file and return the file name
|
||||
*
|
||||
* @return the relative path of the file the data was dumped to
|
||||
*/
|
||||
@@ -182,11 +181,11 @@ public class DataDumper {
|
||||
@NotNull
|
||||
private String getFileName() {
|
||||
return new StringJoiner("_")
|
||||
.add(user.username)
|
||||
.add(new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(dataSnapshot.versionTimestamp()))
|
||||
.add(dataSnapshot.cause().name().toLowerCase(Locale.ENGLISH))
|
||||
.add(dataSnapshot.versionUUID().toString().split("-")[0])
|
||||
+ ".json";
|
||||
.add(user.getUsername())
|
||||
.add(snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")))
|
||||
.add(snapshot.getSaveCause().name().toLowerCase(Locale.ENGLISH))
|
||||
.add(snapshot.getShortId())
|
||||
+ ".json";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,19 +19,19 @@
|
||||
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.data.UserDataSnapshot;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import net.william278.paginedown.PaginatedList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Represents a chat-viewable paginated list of {@link UserDataSnapshot}s
|
||||
* Represents a chat-viewable paginated list of {@link net.william278.husksync.data.DataSnapshot}s
|
||||
*/
|
||||
public class DataSnapshotList {
|
||||
|
||||
@@ -41,46 +41,52 @@ public class DataSnapshotList {
|
||||
@NotNull
|
||||
private final PaginatedList paginatedList;
|
||||
|
||||
private DataSnapshotList(@NotNull List<UserDataSnapshot> snapshots, @NotNull User dataOwner,
|
||||
@NotNull Locales locales) {
|
||||
private DataSnapshotList(@NotNull List<DataSnapshot.Packed> snapshots, @NotNull User dataOwner,
|
||||
@NotNull HuskSync plugin) {
|
||||
final AtomicInteger snapshotNumber = new AtomicInteger(1);
|
||||
this.paginatedList = PaginatedList.of(snapshots.stream()
|
||||
.map(snapshot -> locales.getRawLocale("data_list_item",
|
||||
.map(snapshot -> plugin.getLocales()
|
||||
.getRawLocale("data_list_item",
|
||||
getNumberIcon(snapshotNumber.getAndIncrement()),
|
||||
new SimpleDateFormat("MMM dd yyyy, HH:mm:ss.sss")
|
||||
.format(snapshot.versionTimestamp()),
|
||||
snapshot.versionUUID().toString().split("-")[0],
|
||||
snapshot.versionUUID().toString(),
|
||||
snapshot.cause().getDisplayName(),
|
||||
dataOwner.username,
|
||||
snapshot.pinned() ? "※" : " ")
|
||||
.orElse("• " + snapshot.versionUUID())).toList(),
|
||||
locales.getBaseChatList(6)
|
||||
.setHeaderFormat(locales.getRawLocale("data_list_title", dataOwner.username,
|
||||
dataOwner.getUsername(),
|
||||
snapshot.getId().toString(),
|
||||
snapshot.getShortId(),
|
||||
snapshot.isPinned() ? "※" : " ",
|
||||
snapshot.getTimestamp().format(DateTimeFormatter
|
||||
.ofPattern("dd/MM/yyyy, HH:mm")),
|
||||
snapshot.getTimestamp().format(DateTimeFormatter
|
||||
.ofPattern("MMM dd yyyy, HH:mm:ss.SSS")),
|
||||
snapshot.getSaveCause().getDisplayName(),
|
||||
String.format("%.2fKiB", snapshot.getFileSize(plugin) / 1024f))
|
||||
.orElse("• " + snapshot.getId())).toList(),
|
||||
plugin.getLocales().getBaseChatList(6)
|
||||
.setHeaderFormat(plugin.getLocales()
|
||||
.getRawLocale("data_list_title", dataOwner.getUsername(),
|
||||
"%first_item_on_page_index%", "%last_item_on_page_index%", "%total_items%")
|
||||
.orElse(""))
|
||||
.setCommand("/husksync:userdata list " + dataOwner.username)
|
||||
.setCommand("/husksync:userdata list " + dataOwner.getUsername())
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link DataSnapshotList} from a list of {@link UserDataSnapshot}s
|
||||
* Create a new {@link DataSnapshotList} from a list of {@link DataSnapshot}s
|
||||
*
|
||||
* @param snapshots The list of {@link UserDataSnapshot}s to display
|
||||
* @param user The {@link User} who owns the {@link UserDataSnapshot}s
|
||||
* @param locales The {@link Locales} instance
|
||||
* @return A new {@link DataSnapshotList}, to be viewed with {@link #displayPage(OnlineUser, int)}
|
||||
* @param snapshots The list of {@link DataSnapshot}s to display
|
||||
* @param user The {@link User} who owns the {@link DataSnapshot}s
|
||||
* @param plugin The instance of the plugin
|
||||
* @return A new {@link DataSnapshotList}, to be viewed with {@link #displayPage(CommandUser, int)}
|
||||
*/
|
||||
public static DataSnapshotList create(@NotNull List<UserDataSnapshot> snapshots, @NotNull User user,
|
||||
@NotNull Locales locales) {
|
||||
return new DataSnapshotList(snapshots, user, locales);
|
||||
@NotNull
|
||||
public static DataSnapshotList create(@NotNull List<DataSnapshot.Packed> snapshots, @NotNull User user,
|
||||
@NotNull HuskSync plugin) {
|
||||
return new DataSnapshotList(snapshots, user, plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an icon for the given snapshot number, via {@link #CIRCLED_NUMBER_ICONS}
|
||||
* Get an hasIcon for the given snapshot number, via {@link #CIRCLED_NUMBER_ICONS}
|
||||
*
|
||||
* @param number the snapshot number
|
||||
* @return the icon for the given snapshot number
|
||||
* @return the hasIcon for the given snapshot number
|
||||
*/
|
||||
private static String getNumberIcon(int number) {
|
||||
if (number < 1 || number > 20) {
|
||||
@@ -90,12 +96,12 @@ public class DataSnapshotList {
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a page of the list of {@link UserDataSnapshot} to the user
|
||||
* Display a page of the list of {@link DataSnapshot} to the user
|
||||
*
|
||||
* @param onlineUser The online user to display the message to
|
||||
* @param page The page number to display
|
||||
*/
|
||||
public void displayPage(@NotNull OnlineUser onlineUser, int page) {
|
||||
public void displayPage(@NotNull CommandUser onlineUser, int page) {
|
||||
onlineUser.sendMessage(paginatedList.getNearestValidPage(page));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
public class DataSnapshotOverview {
|
||||
|
||||
private final HuskSync plugin;
|
||||
private final User dataOwner;
|
||||
private final DataSnapshot.Unpacked snapshot;
|
||||
private final long snapshotSize;
|
||||
|
||||
private DataSnapshotOverview(@NotNull DataSnapshot.Unpacked snapshot, long snapshotSize,
|
||||
@NotNull User dataOwner, @NotNull HuskSync plugin) {
|
||||
this.snapshot = snapshot;
|
||||
this.snapshotSize = snapshotSize;
|
||||
this.dataOwner = dataOwner;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static DataSnapshotOverview of(@NotNull DataSnapshot.Unpacked snapshot, long snapshotSize,
|
||||
@NotNull User dataOwner, @NotNull HuskSync plugin) {
|
||||
return new DataSnapshotOverview(snapshot, snapshotSize, dataOwner, plugin);
|
||||
}
|
||||
|
||||
public void show(@NotNull CommandUser user) {
|
||||
// Title message, timestamp, owner and cause.
|
||||
final Locales locales = plugin.getLocales();
|
||||
locales.getLocale("data_manager_title", snapshot.getShortId(), snapshot.getId().toString(),
|
||||
dataOwner.getUsername(), dataOwner.getUuid().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
locales.getLocale("data_manager_timestamp",
|
||||
snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("MMM dd yyyy, HH:mm:ss.SSS")),
|
||||
snapshot.getTimestamp().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
if (snapshot.isPinned()) {
|
||||
locales.getLocale("data_manager_pinned")
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
locales.getLocale("data_manager_cause", snapshot.getSaveCause().getDisplayName())
|
||||
.ifPresent(user::sendMessage);
|
||||
|
||||
// User status data, if present in the snapshot
|
||||
final Optional<Data.Health> health = snapshot.getHealth();
|
||||
final Optional<Data.Hunger> food = snapshot.getHunger();
|
||||
final Optional<Data.Experience> experience = snapshot.getExperience();
|
||||
final Optional<Data.GameMode> gameMode = snapshot.getGameMode();
|
||||
if (health.isPresent() && food.isPresent() && experience.isPresent() && gameMode.isPresent()) {
|
||||
locales.getLocale("data_manager_status",
|
||||
Integer.toString((int) health.get().getHealth()),
|
||||
Integer.toString((int) health.get().getMaxHealth()),
|
||||
Integer.toString(food.get().getFoodLevel()),
|
||||
Integer.toString(experience.get().getExpLevel()),
|
||||
gameMode.get().getGameMode().toLowerCase(Locale.ENGLISH))
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
|
||||
// Snapshot size
|
||||
locales.getLocale("data_manager_size", String.format("%.2fKiB", snapshotSize / 1024f))
|
||||
.ifPresent(user::sendMessage);
|
||||
|
||||
// Advancement and statistic data, if both are present in the snapshot
|
||||
snapshot.getAdvancements()
|
||||
.flatMap(advancementData -> snapshot.getStatistics()
|
||||
.flatMap(statisticsData -> locales.getLocale("data_manager_advancements_statistics",
|
||||
Integer.toString(advancementData.getCompletedExcludingRecipes().size()),
|
||||
generateAdvancementPreview(advancementData.getCompletedExcludingRecipes(), locales),
|
||||
String.format("%.2f", (((statisticsData.getGenericStatistics().getOrDefault(
|
||||
"minecraft:play_one_minute", 0)) / 20d) / 60d) / 60d))))
|
||||
.ifPresent(user::sendMessage);
|
||||
|
||||
if (user.hasPermission("husksync.command.inventory.edit")
|
||||
&& user.hasPermission("husksync.command.enderchest.edit")) {
|
||||
locales.getLocale("data_manager_item_buttons", dataOwner.getUsername(), snapshot.getId().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
locales.getLocale("data_manager_management_buttons", dataOwner.getUsername(), snapshot.getId().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
if (user.hasPermission("husksync.command.userdata.dump")) {
|
||||
locales.getLocale("data_manager_system_buttons", dataOwner.getUsername(), snapshot.getId().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String generateAdvancementPreview(@NotNull List<Data.Advancements.Advancement> advancementData, @NotNull Locales locales) {
|
||||
final StringJoiner joiner = new StringJoiner("\n");
|
||||
final int PREVIEW_SIZE = 8;
|
||||
for (int i = 0; i < advancementData.size(); i++) {
|
||||
joiner.add(advancementData.get(i).getKey());
|
||||
if (i >= PREVIEW_SIZE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
final int remaining = advancementData.size() - PREVIEW_SIZE;
|
||||
if (remaining > 0) {
|
||||
joiner.add(locales.getRawLocale("data_manager_advancements_preview_remaining",
|
||||
Integer.toString(remaining))
|
||||
.orElse(String.format("+%s…", remaining)));
|
||||
}
|
||||
return joiner.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,18 +17,22 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.DataAdapter;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Indicates an error occurred during Base-64 serialization and deserialization of data.
|
||||
* </p>
|
||||
* For example, an exception deserializing {@link ItemData} item stack or {@link PotionEffectData} potion effect arrays
|
||||
*/
|
||||
public class DataSerializationException extends RuntimeException {
|
||||
protected DataSerializationException(@NotNull String message, @NotNull Throwable cause) {
|
||||
super(message, cause);
|
||||
public abstract class LegacyConverter {
|
||||
|
||||
protected final HuskSync plugin;
|
||||
|
||||
protected LegacyConverter(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public abstract DataSnapshot.Packed convert(@NotNull byte[] data) throws DataAdapter.AdaptionException;
|
||||
|
||||
}
|
||||
144
common/src/main/java/net/william278/husksync/util/Task.java
Normal file
144
common/src/main/java/net/william278/husksync/util/Task.java
Normal file
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public interface Task extends Runnable {
|
||||
|
||||
abstract class Base implements Task {
|
||||
|
||||
protected final HuskSync plugin;
|
||||
protected final Runnable runnable;
|
||||
protected boolean cancelled = false;
|
||||
|
||||
protected Base(@NotNull HuskSync plugin, @NotNull Runnable runnable) {
|
||||
this.plugin = plugin;
|
||||
this.runnable = runnable;
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
cancelled = true;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public HuskSync getPlugin() {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
abstract class Async extends Base {
|
||||
|
||||
protected long delayTicks;
|
||||
|
||||
protected Async(@NotNull HuskSync plugin, @NotNull Runnable runnable, long delayTicks) {
|
||||
super(plugin, runnable);
|
||||
this.delayTicks = delayTicks;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
abstract class Sync extends Base {
|
||||
|
||||
protected long delayTicks;
|
||||
|
||||
protected Sync(@NotNull HuskSync plugin, @NotNull Runnable runnable, long delayTicks) {
|
||||
super(plugin, runnable);
|
||||
this.delayTicks = delayTicks;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
abstract class Repeating extends Base {
|
||||
|
||||
protected final long repeatingTicks;
|
||||
|
||||
protected Repeating(@NotNull HuskSync plugin, @NotNull Runnable runnable, long repeatingTicks) {
|
||||
super(plugin, runnable);
|
||||
this.repeatingTicks = repeatingTicks;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
interface Supplier {
|
||||
|
||||
@NotNull
|
||||
Task.Sync getSyncTask(@NotNull Runnable runnable, long delayTicks);
|
||||
|
||||
@NotNull
|
||||
Task.Async getAsyncTask(@NotNull Runnable runnable, long delayTicks);
|
||||
|
||||
@NotNull
|
||||
Task.Repeating getRepeatingTask(@NotNull Runnable runnable, long repeatingTicks);
|
||||
|
||||
@NotNull
|
||||
default Task.Sync runSyncDelayed(@NotNull Runnable runnable, long delayTicks) {
|
||||
final Task.Sync task = getSyncTask(runnable, delayTicks);
|
||||
task.run();
|
||||
return task;
|
||||
}
|
||||
|
||||
default Task.Async runAsyncDelayed(@NotNull Runnable runnable, long delayTicks) {
|
||||
final Task.Async task = getAsyncTask(runnable, delayTicks);
|
||||
task.run();
|
||||
return task;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Task.Sync runSync(@NotNull Runnable runnable) {
|
||||
return runSyncDelayed(runnable, 0);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Task.Async runAsync(@NotNull Runnable runnable) {
|
||||
final Task.Async task = getAsyncTask(runnable, 0);
|
||||
task.run();
|
||||
return task;
|
||||
}
|
||||
|
||||
default <T> CompletableFuture<T> supplyAsync(@NotNull java.util.function.Supplier<T> supplier) {
|
||||
final CompletableFuture<T> future = new CompletableFuture<>();
|
||||
runAsync(() -> {
|
||||
try {
|
||||
future.complete(supplier.get());
|
||||
} catch (Throwable throwable) {
|
||||
future.completeExceptionally(throwable);
|
||||
}
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
void cancelTasks();
|
||||
|
||||
@NotNull
|
||||
HuskSync getPlugin();
|
||||
|
||||
}
|
||||
|
||||
@NotNull
|
||||
HuskSync getPlugin();
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user