mirror of
https://github.com/WiIIiam278/HuskSync.git
synced 2026-01-03 22:16:17 +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:
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user