refactor: use Uniform for native command support (#323)

* refactor: use Uniform for commands

* refactor: remove commodore

* fix: update Uniform, fix commands

* fix: bump uniform, fix commands on fabric

* feat: use new Uniform command permission system

* test: target 1.21
feat/data-edit-commands
William 5 months ago committed by GitHub
parent 69d68de5c0
commit 0e706d36c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,12 +1,12 @@
dependencies { dependencies {
implementation project(path: ':common') implementation project(path: ':common')
implementation 'org.bstats:bstats-bukkit:3.0.2' implementation 'net.william278.uniform:uniform-bukkit:1.1'
implementation 'net.william278:mpdbdataconverter:1.0.1' implementation 'net.william278:mpdbdataconverter:1.0.1'
implementation 'net.william278:hsldataconverter:1.0' implementation 'net.william278:hsldataconverter:1.0'
implementation 'net.william278:mapdataapi:1.0.3' implementation 'net.william278:mapdataapi:1.0.3'
implementation 'net.william278:andjam:1.0.2' implementation 'net.william278:andjam:1.0.2'
implementation 'me.lucko:commodore:2.2' implementation 'org.bstats:bstats-bukkit:3.0.2'
implementation 'net.kyori:adventure-platform-bukkit:4.3.3' implementation 'net.kyori:adventure-platform-bukkit:4.3.3'
implementation 'dev.triumphteam:triumph-gui:3.1.10' implementation 'dev.triumphteam:triumph-gui:3.1.10'
implementation 'space.arim.morepaperlib:morepaperlib:0.4.4' implementation 'space.arim.morepaperlib:morepaperlib:0.4.4'
@ -42,6 +42,7 @@ shadowJar {
relocate 'org.intellij', 'net.william278.husksync.libraries' relocate 'org.intellij', 'net.william278.husksync.libraries'
relocate 'com.zaxxer', 'net.william278.husksync.libraries' relocate 'com.zaxxer', 'net.william278.husksync.libraries'
relocate 'de.exlll', 'net.william278.husksync.libraries' relocate 'de.exlll', 'net.william278.husksync.libraries'
relocate 'net.william278.uniform', 'net.william278.husksync.libraries.uniform'
relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell' relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell'
relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown' relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown'
relocate 'net.william278.mapdataapi', 'net.william278.husksync.libraries.mapdataapi' relocate 'net.william278.mapdataapi', 'net.william278.husksync.libraries.mapdataapi'
@ -51,7 +52,6 @@ shadowJar {
relocate 'org.json', 'net.william278.husksync.libraries.json' relocate 'org.json', 'net.william278.husksync.libraries.json'
relocate 'net.querz', 'net.william278.husksync.libraries.nbtparser' relocate 'net.querz', 'net.william278.husksync.libraries.nbtparser'
relocate 'net.roxeez', 'net.william278.husksync.libraries' relocate 'net.roxeez', 'net.william278.husksync.libraries'
relocate 'me.lucko.commodore', 'net.william278.husksync.libraries.commodore'
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats' relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
relocate 'dev.triumphteam.gui', 'net.william278.husksync.libraries.triumphgui' relocate 'dev.triumphteam.gui', 'net.william278.husksync.libraries.triumphgui'
relocate 'space.arim.morepaperlib', 'net.william278.husksync.libraries.paperlib' relocate 'space.arim.morepaperlib', 'net.william278.husksync.libraries.paperlib'

@ -34,7 +34,7 @@ import net.william278.husksync.adapter.DataAdapter;
import net.william278.husksync.adapter.GsonAdapter; import net.william278.husksync.adapter.GsonAdapter;
import net.william278.husksync.adapter.SnappyGsonAdapter; import net.william278.husksync.adapter.SnappyGsonAdapter;
import net.william278.husksync.api.BukkitHuskSyncAPI; import net.william278.husksync.api.BukkitHuskSyncAPI;
import net.william278.husksync.command.BukkitCommand; import net.william278.husksync.command.PluginCommand;
import net.william278.husksync.config.Locales; import net.william278.husksync.config.Locales;
import net.william278.husksync.config.Server; import net.william278.husksync.config.Server;
import net.william278.husksync.config.Settings; import net.william278.husksync.config.Settings;
@ -57,6 +57,8 @@ import net.william278.husksync.util.BukkitLegacyConverter;
import net.william278.husksync.util.BukkitMapPersister; import net.william278.husksync.util.BukkitMapPersister;
import net.william278.husksync.util.BukkitTask; import net.william278.husksync.util.BukkitTask;
import net.william278.husksync.util.LegacyConverter; import net.william278.husksync.util.LegacyConverter;
import net.william278.uniform.Uniform;
import net.william278.uniform.bukkit.BukkitUniform;
import org.bstats.bukkit.Metrics; import org.bstats.bukkit.Metrics;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.map.MapView; import org.bukkit.map.MapView;
@ -64,7 +66,6 @@ import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import space.arim.morepaperlib.MorePaperLib; import space.arim.morepaperlib.MorePaperLib;
import space.arim.morepaperlib.commands.CommandRegistration;
import space.arim.morepaperlib.scheduling.AsynchronousScheduler; import space.arim.morepaperlib.scheduling.AsynchronousScheduler;
import space.arim.morepaperlib.scheduling.AttachedScheduler; import space.arim.morepaperlib.scheduling.AttachedScheduler;
import space.arim.morepaperlib.scheduling.GracefulScheduling; import space.arim.morepaperlib.scheduling.GracefulScheduling;
@ -135,6 +136,10 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
@Override @Override
public void onEnable() { public void onEnable() {
this.audiences = BukkitAudiences.create(this); this.audiences = BukkitAudiences.create(this);
// Register commands
initialize("commands", (plugin) -> getUniform().register(PluginCommand.Type.create(this)));
// Prepare data adapter // Prepare data adapter
initialize("data adapter", (plugin) -> { initialize("data adapter", (plugin) -> {
if (settings.getSynchronization().isCompressData()) { if (settings.getSynchronization().isCompressData()) {
@ -196,9 +201,6 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
// Register events // Register events
initialize("events", (plugin) -> eventListener.onEnable()); initialize("events", (plugin) -> eventListener.onEnable());
// Register commands
initialize("commands", (plugin) -> BukkitCommand.Type.registerCommands(this));
// Register plugin hooks // Register plugin hooks
initialize("hooks", (plugin) -> { initialize("hooks", (plugin) -> {
if (isDependencyLoaded("Plan") && getSettings().isEnablePlanHook()) { if (isDependencyLoaded("Plan") && getSettings().isEnablePlanHook()) {
@ -264,6 +266,12 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
this.dataSyncer = dataSyncer; this.dataSyncer = dataSyncer;
} }
@Override
@NotNull
public Uniform getUniform() {
return BukkitUniform.getInstance(this);
}
@NotNull @NotNull
@Override @Override
public Map<Identifier, Data> getPlayerCustomDataStore(@NotNull OnlineUser user) { public Map<Identifier, Data> getPlayerCustomDataStore(@NotNull OnlineUser user) {
@ -352,11 +360,6 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
return getScheduler().entitySpecificScheduler(((BukkitUser) user).getPlayer()); return getScheduler().entitySpecificScheduler(((BukkitUser) user).getPlayer());
} }
@NotNull
public CommandRegistration getCommandRegistrar() {
return paperLib.commandRegistration();
}
@Override @Override
@NotNull @NotNull
public Path getConfigDirectory() { public Path getConfigDirectory() {

@ -1,60 +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 me.lucko.commodore.CommodoreProvider;
import me.lucko.commodore.file.CommodoreFileReader;
import net.william278.husksync.BukkitHuskSync;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Level;
public class BrigadierUtil {
/**
* Uses commodore to register command completions.
*
* @param plugin instance of the registering Bukkit plugin
* @param bukkitCommand the Bukkit PluginCommand to register completions for
* @param command the {@link Command} to register completions for
*/
protected static void registerCommodore(@NotNull BukkitHuskSync plugin,
@NotNull org.bukkit.command.Command bukkitCommand,
@NotNull Command command) {
final InputStream commodoreFile = plugin.getResource(
"commodore/" + bukkitCommand.getName() + ".commodore"
);
if (commodoreFile == null) {
return;
}
try {
CommodoreProvider.getCommodore(plugin).register(bukkitCommand,
CommodoreFileReader.INSTANCE.parse(commodoreFile),
player -> player.hasPermission(command.getPermission()));
} catch (IOException e) {
plugin.log(Level.SEVERE, String.format(
"Failed to read command commodore completions for %s", bukkitCommand.getName()), e
);
}
}
}

@ -1,164 +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 me.lucko.commodore.CommodoreProvider;
import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.user.BukkitUser;
import net.william278.husksync.user.CommandUser;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionDefault;
import org.bukkit.plugin.PluginManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Function;
public class BukkitCommand extends org.bukkit.command.Command {
private final BukkitHuskSync plugin;
private final Command command;
public BukkitCommand(@NotNull Command command, @NotNull BukkitHuskSync plugin) {
super(command.getName(), command.getDescription(), command.getUsage(), command.getAliases());
this.command = command;
this.plugin = plugin;
}
@Override
public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, @NotNull String[] args) {
this.command.onExecuted(sender instanceof Player p ? BukkitUser.adapt(p, plugin) : plugin.getConsole(), args);
return true;
}
@NotNull
@Override
public List<String> tabComplete(@NotNull CommandSender sender, @NotNull String alias,
@NotNull String[] args) throws IllegalArgumentException {
if (!(this.command instanceof TabProvider provider)) {
return List.of();
}
final CommandUser user = sender instanceof Player p ? BukkitUser.adapt(p, plugin) : plugin.getConsole();
if (getPermission() == null || user.hasPermission(getPermission())) {
return provider.getSuggestions(user, args);
}
return List.of();
}
public void register() {
// Register with bukkit
plugin.getCommandRegistrar().getServerCommandMap().register("husksync", this);
// Register permissions
BukkitCommand.addPermission(
plugin,
command.getPermission(),
command.getUsage(),
BukkitCommand.getPermissionDefault(command.isOperatorCommand())
);
final List<Permission> childNodes = command.getAdditionalPermissions()
.entrySet().stream()
.map((entry) -> BukkitCommand.addPermission(
plugin,
entry.getKey(),
"",
BukkitCommand.getPermissionDefault(entry.getValue()))
)
.filter(Objects::nonNull)
.toList();
if (!childNodes.isEmpty()) {
BukkitCommand.addPermission(
plugin,
command.getPermission("*"),
command.getUsage(),
PermissionDefault.FALSE,
childNodes.toArray(new Permission[0])
);
}
// Register commodore TAB completion
if (CommodoreProvider.isSupported() && plugin.getSettings().isBrigadierTabCompletion()) {
BrigadierUtil.registerCommodore(plugin, this, command);
}
}
@Nullable
protected static Permission addPermission(@NotNull BukkitHuskSync plugin, @NotNull String node,
@NotNull String description, @NotNull PermissionDefault permissionDefault,
@NotNull Permission... children) {
final Map<String, Boolean> childNodes = Arrays.stream(children)
.map(Permission::getName)
.collect(HashMap::new, (map, child) -> map.put(child, true), HashMap::putAll);
final PluginManager manager = plugin.getServer().getPluginManager();
if (manager.getPermission(node) != null) {
return null;
}
Permission permission;
if (description.isEmpty()) {
permission = new Permission(node, permissionDefault, childNodes);
} else {
permission = new Permission(node, description, permissionDefault, childNodes);
}
manager.addPermission(permission);
return permission;
}
@NotNull
protected static PermissionDefault getPermissionDefault(boolean isOperatorCommand) {
return isOperatorCommand ? PermissionDefault.OP : PermissionDefault.TRUE;
}
/**
* Commands available on the Bukkit HuskSync implementation
*/
public enum Type {
HUSKSYNC_COMMAND(HuskSyncCommand::new),
USERDATA_COMMAND(UserDataCommand::new),
INVENTORY_COMMAND(InventoryCommand::new),
ENDER_CHEST_COMMAND(EnderChestCommand::new);
public final Function<BukkitHuskSync, Command> commandSupplier;
Type(@NotNull Function<BukkitHuskSync, Command> supplier) {
this.commandSupplier = supplier;
}
@NotNull
public Command createCommand(@NotNull BukkitHuskSync plugin) {
return commandSupplier.apply(plugin);
}
public static void registerCommands(@NotNull BukkitHuskSync plugin) {
Arrays.stream(values())
.map((type) -> type.createCommand(plugin))
.forEach((command) -> new BukkitCommand(command, plugin).register());
}
}
}

@ -1,3 +0,0 @@
inventory {
name brigadier:string single_word;
}

@ -1,6 +0,0 @@
husksync {
update;
about;
status;
reload;
}

@ -1,3 +0,0 @@
enderchest {
name brigadier:string single_word;
}

@ -1,35 +0,0 @@
userdata {
view {
name brigadier:string single_word {
version brigadier:string single_word;
}
}
list {
name brigadier:string single_word {
page brigadier:integer;
}
}
delete {
name brigadier:string single_word {
version brigadier:string single_word;
}
}
restore {
name brigadier:string single_word {
version brigadier:string single_word;
}
}
pin {
name brigadier:string single_word {
version brigadier:string single_word;
}
}
dump {
name brigadier:string single_word {
version brigadier:string single_word {
web;
file;
}
}
}
}

@ -16,6 +16,8 @@ dependencies {
exclude module: 'slf4j-api' exclude module: 'slf4j-api'
} }
compileOnly 'net.william278.uniform:uniform-common:1.1'
compileOnly 'com.mojang:brigadier:1.1.8'
compileOnly 'org.projectlombok:lombok:1.18.32' compileOnly 'org.projectlombok:lombok:1.18.32'
compileOnly 'org.jetbrains:annotations:24.1.0' compileOnly 'org.jetbrains:annotations:24.1.0'
compileOnly 'net.kyori:adventure-api:4.17.0' compileOnly 'net.kyori:adventure-api:4.17.0'

@ -41,6 +41,7 @@ import net.william278.husksync.user.ConsoleUser;
import net.william278.husksync.user.OnlineUser; import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.util.LegacyConverter; import net.william278.husksync.util.LegacyConverter;
import net.william278.husksync.util.Task; import net.william278.husksync.util.Task;
import net.william278.uniform.Uniform;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.InputStream; import java.io.InputStream;
@ -111,6 +112,14 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
*/ */
void setDataSyncer(@NotNull DataSyncer dataSyncer); void setDataSyncer(@NotNull DataSyncer dataSyncer);
/**
* Get the uniform command provider
*
* @return the command provider
*/
@NotNull
Uniform getUniform();
/** /**
* Returns a list of available data {@link Migrator}s * Returns a list of available data {@link Migrator}s
* *
@ -256,10 +265,10 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
@NotNull @NotNull
default UpdateChecker getUpdateChecker() { default UpdateChecker getUpdateChecker() {
return UpdateChecker.builder() return UpdateChecker.builder()
.currentVersion(getPluginVersion()) .currentVersion(getPluginVersion())
.endpoint(UpdateChecker.Endpoint.SPIGOT) .endpoint(UpdateChecker.Endpoint.SPIGOT)
.resource(Integer.toString(SPIGOT_RESOURCE_ID)) .resource(Integer.toString(SPIGOT_RESOURCE_ID))
.build(); .build();
} }
default void checkForUpdates() { default void checkForUpdates() {
@ -267,8 +276,8 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
getUpdateChecker().check().thenAccept(checked -> { getUpdateChecker().check().thenAccept(checked -> {
if (!checked.isUpToDate()) { if (!checked.isUpToDate()) {
log(Level.WARNING, String.format( log(Level.WARNING, String.format(
"A new version of HuskSync is available: v%s (running v%s)", "A new version of HuskSync is available: v%s (running v%s)",
checked.getLatestVersion(), getPluginVersion()) checked.getLatestVersion(), getPluginVersion())
); );
} }
}); });
@ -311,15 +320,15 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
final class FailedToLoadException extends IllegalStateException { final class FailedToLoadException extends IllegalStateException {
private static final String FORMAT = """ private static final String FORMAT = """
HuskSync has failed to load! The plugin will not be enabled and no data will be synchronized. 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): Please make sure the plugin has been setup correctly (https://william278.net/docs/husksync/setup):
1) Make sure you've entered your MySQL, MariaDB or MongoDB database details correctly in config.yml 1) Make sure you've entered your MySQL, MariaDB or MongoDB database details correctly in config.yml
2) Make sure your Redis server details are also correct 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-file) 3) Make sure your config is up-to-date (https://william278.net/docs/husksync/config-file)
4) Check the error below for more details 4) Check the error below for more details
Caused by: %s"""; Caused by: %s""";
FailedToLoadException(@NotNull String message, @NotNull Throwable cause) { FailedToLoadException(@NotNull String message, @NotNull Throwable cause) {
super(String.format(FORMAT, message), cause); super(String.format(FORMAT, message), cause);

@ -1,94 +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 com.google.common.collect.Maps;
import net.william278.husksync.HuskSync;
import net.william278.husksync.user.CommandUser;
import org.jetbrains.annotations.NotNull;
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 = Maps.newHashMap();
}
@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
protected String[] removeFirstArg(@NotNull String[] args) {
if (args.length <= 1) {
return new String[0];
}
String[] newArgs = new String[args.length - 1];
System.arraycopy(args, 1, newArgs, 0, args.length - 1);
return newArgs;
}
@NotNull
public final String getRawUsage() {
return usage;
}
@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;
}
}

@ -37,7 +37,7 @@ import java.util.Optional;
public class EnderChestCommand extends ItemsCommand { public class EnderChestCommand extends ItemsCommand {
public EnderChestCommand(@NotNull HuskSync plugin) { public EnderChestCommand(@NotNull HuskSync plugin) {
super(plugin, List.of("enderchest", "echest", "openechest")); super("enderchest", List.of("echest", "openechest"), plugin);
} }
@Override @Override
@ -46,29 +46,29 @@ public class EnderChestCommand extends ItemsCommand {
final Optional<Data.Items.EnderChest> optionalEnderChest = snapshot.getEnderChest(); final Optional<Data.Items.EnderChest> optionalEnderChest = snapshot.getEnderChest();
if (optionalEnderChest.isEmpty()) { if (optionalEnderChest.isEmpty()) {
plugin.getLocales().getLocale("error_no_data_to_display") plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage); .ifPresent(viewer::sendMessage);
return; return;
} }
// Display opening message // Display opening message
plugin.getLocales().getLocale("ender_chest_viewer_opened", user.getUsername(), plugin.getLocales().getLocale("ender_chest_viewer_opened", user.getUsername(),
snapshot.getTimestamp().format(DateTimeFormatter snapshot.getTimestamp().format(DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT))) .ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
.ifPresent(viewer::sendMessage); .ifPresent(viewer::sendMessage);
// Show GUI // Show GUI
final Data.Items.EnderChest enderChest = optionalEnderChest.get(); final Data.Items.EnderChest enderChest = optionalEnderChest.get();
viewer.showGui( viewer.showGui(
enderChest, enderChest,
plugin.getLocales().getLocale("ender_chest_viewer_menu_title", user.getUsername()) plugin.getLocales().getLocale("ender_chest_viewer_menu_title", user.getUsername())
.orElse(new MineDown(String.format("%s's Ender Chest", user.getUsername()))), .orElse(new MineDown(String.format("%s's Ender Chest", user.getUsername()))),
allowEdit, allowEdit,
enderChest.getSlotCount(), enderChest.getSlotCount(),
(itemsOnClose) -> { (itemsOnClose) -> {
if (allowEdit && !enderChest.equals(itemsOnClose)) { if (allowEdit && !enderChest.equals(itemsOnClose)) {
plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user)); plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user));
}
} }
}
); );
} }
@ -78,7 +78,7 @@ public class EnderChestCommand extends ItemsCommand {
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder); final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
if (latestData.isEmpty()) { if (latestData.isEmpty()) {
plugin.getLocales().getLocale("error_no_data_to_display") plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage); .ifPresent(viewer::sendMessage);
return; return;
} }
@ -88,7 +88,7 @@ public class EnderChestCommand extends ItemsCommand {
data.getEnderChest().ifPresent(enderChest -> enderChest.setContents(items)); data.getEnderChest().ifPresent(enderChest -> enderChest.setContents(items));
data.setSaveCause(DataSnapshot.SaveCause.ENDERCHEST_COMMAND); data.setSaveCause(DataSnapshot.SaveCause.ENDERCHEST_COMMAND);
data.setPinned( data.setPinned(
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.ENDERCHEST_COMMAND) plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.ENDERCHEST_COMMAND)
); );
}); });

@ -1,29 +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.user.CommandUser;
import org.jetbrains.annotations.NotNull;
public interface Executable {
void onExecuted(@NotNull CommandUser executor, @NotNull String[] args);
}

@ -19,6 +19,8 @@
package net.william278.husksync.command; package net.william278.husksync.command;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import de.themoep.minedown.adventure.MineDown; import de.themoep.minedown.adventure.MineDown;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.JoinConfiguration; import net.kyori.adventure.text.JoinConfiguration;
@ -31,229 +33,219 @@ import net.william278.husksync.HuskSync;
import net.william278.husksync.database.Database; import net.william278.husksync.database.Database;
import net.william278.husksync.migrator.Migrator; import net.william278.husksync.migrator.Migrator;
import net.william278.husksync.user.CommandUser; import net.william278.husksync.user.CommandUser;
import net.william278.husksync.user.OnlineUser; import net.william278.uniform.BaseCommand;
import net.william278.uniform.CommandProvider;
import net.william278.uniform.Permission;
import net.william278.uniform.element.ArgumentElement;
import org.apache.commons.text.WordUtils; import org.apache.commons.text.WordUtils;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*; import java.util.Arrays;
import java.util.List;
import java.util.function.Function; import java.util.function.Function;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class HuskSyncCommand extends Command implements TabProvider { public class HuskSyncCommand extends PluginCommand {
private static final Map<String, Boolean> SUB_COMMANDS = Map.of(
"about", false,
"status", true,
"reload", true,
"migrate", true,
"update", true
);
private final UpdateChecker updateChecker; private final UpdateChecker updateChecker;
private final AboutMenu aboutMenu; private final AboutMenu aboutMenu;
public HuskSyncCommand(@NotNull HuskSync plugin) { public HuskSyncCommand(@NotNull HuskSync plugin) {
super("husksync", List.of(), "[" + String.join("|", SUB_COMMANDS.keySet()) + "]", plugin); super("husksync", List.of(), Permission.Default.TRUE, plugin);
addAdditionalPermissions(SUB_COMMANDS);
this.updateChecker = plugin.getUpdateChecker(); this.updateChecker = plugin.getUpdateChecker();
this.aboutMenu = AboutMenu.builder() this.aboutMenu = AboutMenu.builder()
.title(Component.text("HuskSync")) .title(Component.text("HuskSync"))
.description(Component.text("A modern, cross-server player data synchronization system")) .description(Component.text("A modern, cross-server player data synchronization system"))
.version(plugin.getPluginVersion()) .version(plugin.getPluginVersion())
.credits("Author", .credits("Author",
AboutMenu.Credit.of("William278").description("Click to visit website").url("https://william278.net")) AboutMenu.Credit.of("William278").description("Click to visit website").url("https://william278.net"))
.credits("Contributors", .credits("Contributors",
AboutMenu.Credit.of("HarvelsX").description("Code"), AboutMenu.Credit.of("HarvelsX").description("Code"),
AboutMenu.Credit.of("HookWoods").description("Code"), AboutMenu.Credit.of("HookWoods").description("Code"),
AboutMenu.Credit.of("Preva1l").description("Code"), AboutMenu.Credit.of("Preva1l").description("Code"),
AboutMenu.Credit.of("hanbings").description("Code (Fabric porting)"), AboutMenu.Credit.of("hanbings").description("Code (Fabric porting)"),
AboutMenu.Credit.of("Stampede2011").description("Code (Fabric mixins)")) AboutMenu.Credit.of("Stampede2011").description("Code (Fabric mixins)"))
.credits("Translators", .credits("Translators",
AboutMenu.Credit.of("Namiu").description("Japanese (ja-jp)"), AboutMenu.Credit.of("Namiu").description("Japanese (ja-jp)"),
AboutMenu.Credit.of("anchelthe").description("Spanish (es-es)"), AboutMenu.Credit.of("anchelthe").description("Spanish (es-es)"),
AboutMenu.Credit.of("Melonzio").description("Spanish (es-es)"), AboutMenu.Credit.of("Melonzio").description("Spanish (es-es)"),
AboutMenu.Credit.of("Ceddix").description("German (de-de)"), AboutMenu.Credit.of("Ceddix").description("German (de-de)"),
AboutMenu.Credit.of("Pukejoy_1").description("Bulgarian (bg-bg)"), AboutMenu.Credit.of("Pukejoy_1").description("Bulgarian (bg-bg)"),
AboutMenu.Credit.of("mateusneresrb").description("Brazilian Portuguese (pt-br)"), AboutMenu.Credit.of("mateusneresrb").description("Brazilian Portuguese (pt-br)"),
AboutMenu.Credit.of("小蔡").description("Traditional Chinese (zh-tw)"), AboutMenu.Credit.of("小蔡").description("Traditional Chinese (zh-tw)"),
AboutMenu.Credit.of("Ghost-chu").description("Simplified Chinese (zh-cn)"), AboutMenu.Credit.of("Ghost-chu").description("Simplified Chinese (zh-cn)"),
AboutMenu.Credit.of("DJelly4K").description("Simplified Chinese (zh-cn)"), AboutMenu.Credit.of("DJelly4K").description("Simplified Chinese (zh-cn)"),
AboutMenu.Credit.of("Thourgard").description("Ukrainian (uk-ua)"), AboutMenu.Credit.of("Thourgard").description("Ukrainian (uk-ua)"),
AboutMenu.Credit.of("xF3d3").description("Italian (it-it)"), AboutMenu.Credit.of("xF3d3").description("Italian (it-it)"),
AboutMenu.Credit.of("cada3141").description("Korean (ko-kr)"), AboutMenu.Credit.of("cada3141").description("Korean (ko-kr)"),
AboutMenu.Credit.of("Wirayuda5620").description("Indonesian (id-id)"), AboutMenu.Credit.of("Wirayuda5620").description("Indonesian (id-id)"),
AboutMenu.Credit.of("WinTone01").description("Turkish (tr-tr)"), AboutMenu.Credit.of("WinTone01").description("Turkish (tr-tr)"),
AboutMenu.Credit.of("IbanEtchep").description("French (fr-fr)")) AboutMenu.Credit.of("IbanEtchep").description("French (fr-fr)"))
.buttons( .buttons(
AboutMenu.Link.of("https://william278.net/docs/husksync").text("Documentation").icon("⛏"), AboutMenu.Link.of("https://william278.net/docs/husksync").text("Documentation").icon("⛏"),
AboutMenu.Link.of("https://github.com/WiIIiam278/HuskSync/issues").text("Issues").icon("❌").color(TextColor.color(0xff9f0f)), AboutMenu.Link.of("https://github.com/WiIIiam278/HuskSync/issues").text("Issues").icon("❌").color(TextColor.color(0xff9f0f)),
AboutMenu.Link.of("https://discord.gg/tVYhJfyDWG").text("Discord").icon("⭐").color(TextColor.color(0x6773f5))) AboutMenu.Link.of("https://discord.gg/tVYhJfyDWG").text("Discord").icon("⭐").color(TextColor.color(0x6773f5)))
.build(); .build();
} }
@Override @Override
public void execute(@NotNull CommandUser executor, @NotNull String[] args) { public void provide(@NotNull BaseCommand<?> command) {
final String subCommand = parseStringArg(args, 0).orElse("about").toLowerCase(Locale.ENGLISH); command.setDefaultExecutor((ctx) -> about(command, ctx));
if (SUB_COMMANDS.containsKey(subCommand) && !executor.hasPermission(getPermission(subCommand))) { command.addSubCommand("about", (sub) -> sub.setDefaultExecutor((ctx) -> about(command, ctx)));
plugin.getLocales().getLocale("error_no_permission") command.addSubCommand("status", needsOp("status"), status());
.ifPresent(executor::sendMessage); command.addSubCommand("reload", needsOp("reload"), reload());
return; command.addSubCommand("update", needsOp("update"), update());
} command.addSubCommand("migrate", migrate());
}
private void about(@NotNull BaseCommand<?> c, @NotNull CommandContext<?> ctx) {
user(c, ctx).getAudience().sendMessage(aboutMenu.toComponent());
}
@NotNull
private CommandProvider status() {
return (sub) -> sub.setDefaultExecutor((ctx) -> {
final CommandUser user = user(sub, ctx);
plugin.getLocales().getLocale("system_status_header").ifPresent(user::sendMessage);
user.sendMessage(Component.join(
JoinConfiguration.newlines(),
Arrays.stream(StatusLine.values()).map(s -> s.get(plugin)).toList()
));
});
}
switch (subCommand) { @NotNull
case "about" -> executor.sendMessage(aboutMenu.toComponent()); private CommandProvider reload() {
case "status" -> { return (sub) -> sub.setDefaultExecutor((ctx) -> {
getPlugin().getLocales().getLocale("system_status_header").ifPresent(executor::sendMessage); final CommandUser user = user(sub, ctx);
executor.sendMessage(Component.join( try {
JoinConfiguration.newlines(), plugin.loadSettings();
Arrays.stream(StatusLine.values()).map(s -> s.get(plugin)).toList() plugin.loadLocales();
plugin.loadServer();
plugin.getLocales().getLocale("reload_complete").ifPresent(user::sendMessage);
} catch (Throwable e) {
user.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 "reload" -> { });
try {
plugin.loadSettings();
plugin.loadLocales();
plugin.loadServer();
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 (executor instanceof OnlineUser) {
plugin.getLocales().getLocale("error_console_command_only")
.ifPresent(executor::sendMessage);
return;
}
this.handleMigrationCommand(args);
}
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);
}
} }
// Handle a migration console command input @NotNull
private void handleMigrationCommand(@NotNull String[] args) { private CommandProvider update() {
if (args.length < 2) { return (sub) -> sub.setDefaultExecutor((ctx) -> updateChecker.check().thenAccept(checked -> {
plugin.log(Level.INFO, final CommandUser user = user(sub, ctx);
"Please choose a migrator, then run \"husksync migrate <migrator>\""); if (checked.isUpToDate()) {
this.logMigratorList(); plugin.getLocales().getLocale("up_to_date", plugin.getPluginVersion().toString())
return; .ifPresent(user::sendMessage);
}
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; return;
} }
switch (args[2]) { plugin.getLocales().getLocale("update_available", checked.getLatestVersion().toString(),
case "start" -> migrator.start().thenAccept(succeeded -> { plugin.getPluginVersion().toString()).ifPresent(user::sendMessage);
}));
}
@NotNull
private CommandProvider migrate() {
return (sub) -> {
sub.setCondition((ctx) -> sub.getUser(ctx).isConsole());
sub.setDefaultExecutor((ctx) -> {
plugin.log(Level.INFO, "Please choose a migrator, then run \"husksync migrate <migrator>\"");
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"))
));
});
sub.addSubCommand("start", (start) -> start.addSyntax((cmd) -> {
final Migrator migrator = cmd.getArgument("migrator", Migrator.class);
migrator.start().thenAccept(succeeded -> {
if (succeeded) { if (succeeded) {
plugin.log(Level.INFO, "Migration completed successfully!"); plugin.log(Level.INFO, "Migration completed successfully!");
} else { } else {
plugin.log(Level.WARNING, "Migration failed!"); plugin.log(Level.WARNING, "Migration failed!");
} }
}); });
case "set" -> migrator.handleConfigurationCommand(Arrays.copyOfRange(args, 3, args.length)); }, migrator()));
default -> plugin.log(Level.INFO, String.format( sub.addSubCommand("set", (set) -> set.addSyntax((cmd) -> {
"Invalid syntax. Console usage: \"husksync migrate %s <start/set>", args[1] final Migrator migrator = cmd.getArgument("migrator", Migrator.class);
)); final String[] args = cmd.getArgument("args", String.class).split(" ");
} migrator.handleConfigurationCommand(Arrays.copyOfRange(args, 3, args.length));
}, () -> { }, migrator(), BaseCommand.greedyString("args")));
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 @NotNull
@Override private <S> ArgumentElement<S, Migrator> migrator() {
public List<String> suggest(@NotNull CommandUser user, @NotNull String[] args) { return new ArgumentElement<>("migrator", reader -> {
return switch (args.length) { final String id = reader.readString();
case 0, 1 -> SUB_COMMANDS.keySet().stream().sorted().toList(); final Migrator migrator = plugin.getAvailableMigrators().stream()
default -> null; .filter(m -> m.getIdentifier().equalsIgnoreCase(id)).findFirst().orElse(null);
}; if (migrator == null) {
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader);
}
return migrator;
}, (context, builder) -> {
for (Migrator material : plugin.getAvailableMigrators()) {
builder.suggest(material.getIdentifier());
}
return builder.buildFuture();
});
} }
private enum StatusLine { private enum StatusLine {
PLUGIN_VERSION(plugin -> Component.text("v" + plugin.getPluginVersion().toStringWithoutMetadata()) PLUGIN_VERSION(plugin -> Component.text("v" + plugin.getPluginVersion().toStringWithoutMetadata())
.appendSpace().append(plugin.getPluginVersion().getMetadata().isBlank() ? Component.empty() .appendSpace().append(plugin.getPluginVersion().getMetadata().isBlank() ? Component.empty()
: Component.text("(build " + plugin.getPluginVersion().getMetadata() + ")"))), : Component.text("(build " + plugin.getPluginVersion().getMetadata() + ")"))),
PLATFORM_TYPE(plugin -> Component.text(WordUtils.capitalizeFully(plugin.getPlatformType()))), PLATFORM_TYPE(plugin -> Component.text(WordUtils.capitalizeFully(plugin.getPlatformType()))),
LANGUAGE(plugin -> Component.text(plugin.getSettings().getLanguage())), LANGUAGE(plugin -> Component.text(plugin.getSettings().getLanguage())),
MINECRAFT_VERSION(plugin -> Component.text(plugin.getMinecraftVersion().toString())), MINECRAFT_VERSION(plugin -> Component.text(plugin.getMinecraftVersion().toString())),
JAVA_VERSION(plugin -> Component.text(System.getProperty("java.version"))), JAVA_VERSION(plugin -> Component.text(System.getProperty("java.version"))),
JAVA_VENDOR(plugin -> Component.text(System.getProperty("java.vendor"))), JAVA_VENDOR(plugin -> Component.text(System.getProperty("java.vendor"))),
SYNC_MODE(plugin -> Component.text(WordUtils.capitalizeFully( SYNC_MODE(plugin -> Component.text(WordUtils.capitalizeFully(
plugin.getSettings().getSynchronization().getMode().toString() plugin.getSettings().getSynchronization().getMode().toString()
))), ))),
DELAY_LATENCY(plugin -> Component.text( DELAY_LATENCY(plugin -> Component.text(
plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds() + "ms" plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds() + "ms"
)), )),
SERVER_NAME(plugin -> Component.text(plugin.getServerName())), SERVER_NAME(plugin -> Component.text(plugin.getServerName())),
CLUSTER_ID(plugin -> Component.text(plugin.getSettings().getClusterId().isBlank() ? "None" : plugin.getSettings().getClusterId())), CLUSTER_ID(plugin -> Component.text(plugin.getSettings().getClusterId().isBlank() ? "None" : plugin.getSettings().getClusterId())),
DATABASE_TYPE(plugin -> DATABASE_TYPE(plugin ->
Component.text(plugin.getSettings().getDatabase().getType().getDisplayName() + Component.text(plugin.getSettings().getDatabase().getType().getDisplayName() +
(plugin.getSettings().getDatabase().getType() == Database.Type.MONGO ? (plugin.getSettings().getDatabase().getType() == Database.Type.MONGO ?
(plugin.getSettings().getDatabase().getMongoSettings().isUsingAtlas() ? " Atlas" : "") : "")) (plugin.getSettings().getDatabase().getMongoSettings().isUsingAtlas() ? " Atlas" : "") : ""))
), ),
IS_DATABASE_LOCAL(plugin -> getLocalhostBoolean(plugin.getSettings().getDatabase().getCredentials().getHost())), IS_DATABASE_LOCAL(plugin -> getLocalhostBoolean(plugin.getSettings().getDatabase().getCredentials().getHost())),
USING_REDIS_SENTINEL(plugin -> getBoolean( USING_REDIS_SENTINEL(plugin -> getBoolean(
!plugin.getSettings().getRedis().getSentinel().getMaster().isBlank() !plugin.getSettings().getRedis().getSentinel().getMaster().isBlank()
)), )),
USING_REDIS_PASSWORD(plugin -> getBoolean( USING_REDIS_PASSWORD(plugin -> getBoolean(
!plugin.getSettings().getRedis().getCredentials().getPassword().isBlank() !plugin.getSettings().getRedis().getCredentials().getPassword().isBlank()
)), )),
REDIS_USING_SSL(plugin -> getBoolean( REDIS_USING_SSL(plugin -> getBoolean(
plugin.getSettings().getRedis().getCredentials().isUseSsl() plugin.getSettings().getRedis().getCredentials().isUseSsl()
)), )),
IS_REDIS_LOCAL(plugin -> getLocalhostBoolean( IS_REDIS_LOCAL(plugin -> getLocalhostBoolean(
plugin.getSettings().getRedis().getCredentials().getHost() plugin.getSettings().getRedis().getCredentials().getHost()
)), )),
DATA_TYPES(plugin -> Component.join( DATA_TYPES(plugin -> Component.join(
JoinConfiguration.commas(true), JoinConfiguration.commas(true),
plugin.getRegisteredDataTypes().stream().map(i -> Component.textOfChildren(Component.text(i.toString()) plugin.getRegisteredDataTypes().stream().map(i -> Component.textOfChildren(Component.text(i.toString())
.appendSpace().append(Component.text(i.isEnabled() ? '✔' : '❌'))) .appendSpace().append(Component.text(i.isEnabled() ? '✔' : '❌')))
.color(i.isEnabled() ? NamedTextColor.GREEN : NamedTextColor.RED) .color(i.isEnabled() ? NamedTextColor.GREEN : NamedTextColor.RED)
.hoverEvent(HoverEvent.showText( .hoverEvent(HoverEvent.showText(
Component.text(i.isEnabled() ? "Enabled" : "Disabled") Component.text(i.isEnabled() ? "Enabled" : "Disabled")
.append(Component.newline()) .append(Component.newline())
.append(Component.text("Dependencies: %s".formatted(i.getDependencies() .append(Component.text("Dependencies: %s".formatted(i.getDependencies()
.isEmpty() ? "(None)" : i.getDependencies().stream() .isEmpty() ? "(None)" : i.getDependencies().stream()
.map(d -> "%s (%s)".formatted( .map(d -> "%s (%s)".formatted(
d.getKey().value(), d.isRequired() ? "Required" : "Optional" d.getKey().value(), d.isRequired() ? "Required" : "Optional"
)).collect(Collectors.joining(", "))) )).collect(Collectors.joining(", ")))
).color(NamedTextColor.GRAY)) ).color(NamedTextColor.GRAY))
))).toList() ))).toList()
)); ));
private final Function<HuskSync, Component> supplier; private final Function<HuskSync, Component> supplier;
@ -265,13 +257,13 @@ public class HuskSyncCommand extends Command implements TabProvider {
@NotNull @NotNull
private Component get(@NotNull HuskSync plugin) { private Component get(@NotNull HuskSync plugin) {
return Component return Component
.text("•").appendSpace() .text("•").appendSpace()
.append(Component.text( .append(Component.text(
WordUtils.capitalizeFully(name().replaceAll("_", " ")), WordUtils.capitalizeFully(name().replaceAll("_", " ")),
TextColor.color(0x848484) TextColor.color(0x848484)
)) ))
.append(Component.text(':')).append(Component.space().color(NamedTextColor.WHITE)) .append(Component.text(':')).append(Component.space().color(NamedTextColor.WHITE))
.append(supplier.apply(plugin)); .append(supplier.apply(plugin));
} }
@NotNull @NotNull
@ -282,7 +274,7 @@ public class HuskSyncCommand extends Command implements TabProvider {
@NotNull @NotNull
private static Component getLocalhostBoolean(@NotNull String value) { private static Component getLocalhostBoolean(@NotNull String value) {
return getBoolean(value.equals("127.0.0.1") || value.equals("0.0.0.0") return getBoolean(value.equals("127.0.0.1") || value.equals("0.0.0.0")
|| value.equals("localhost") || value.equals("::1")); || value.equals("localhost") || value.equals("::1"));
} }
} }

@ -37,7 +37,7 @@ import java.util.Optional;
public class InventoryCommand extends ItemsCommand { public class InventoryCommand extends ItemsCommand {
public InventoryCommand(@NotNull HuskSync plugin) { public InventoryCommand(@NotNull HuskSync plugin) {
super(plugin, List.of("inventory", "invsee", "openinv")); super("inventory", List.of("invsee", "openinv"), plugin);
} }
@Override @Override
@ -47,29 +47,29 @@ public class InventoryCommand extends ItemsCommand {
if (optionalInventory.isEmpty()) { if (optionalInventory.isEmpty()) {
viewer.sendMessage(new MineDown("what the FUCK is happening")); viewer.sendMessage(new MineDown("what the FUCK is happening"));
plugin.getLocales().getLocale("error_no_data_to_display") plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage); .ifPresent(viewer::sendMessage);
return; return;
} }
// Display opening message // Display opening message
plugin.getLocales().getLocale("inventory_viewer_opened", user.getUsername(), plugin.getLocales().getLocale("inventory_viewer_opened", user.getUsername(),
snapshot.getTimestamp().format(DateTimeFormatter snapshot.getTimestamp().format(DateTimeFormatter
.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT))) .ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.SHORT)))
.ifPresent(viewer::sendMessage); .ifPresent(viewer::sendMessage);
// Show GUI // Show GUI
final Data.Items.Inventory inventory = optionalInventory.get(); final Data.Items.Inventory inventory = optionalInventory.get();
viewer.showGui( viewer.showGui(
inventory, inventory,
plugin.getLocales().getLocale("inventory_viewer_menu_title", user.getUsername()) plugin.getLocales().getLocale("inventory_viewer_menu_title", user.getUsername())
.orElse(new MineDown(String.format("%s's Inventory", user.getUsername()))), .orElse(new MineDown(String.format("%s's Inventory", user.getUsername()))),
allowEdit, allowEdit,
inventory.getSlotCount(), inventory.getSlotCount(),
(itemsOnClose) -> { (itemsOnClose) -> {
if (allowEdit && !inventory.equals(itemsOnClose)) { if (allowEdit && !inventory.equals(itemsOnClose)) {
plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user)); plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user));
}
} }
}
); );
} }
@ -79,7 +79,7 @@ public class InventoryCommand extends ItemsCommand {
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder); final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
if (latestData.isEmpty()) { if (latestData.isEmpty()) {
plugin.getLocales().getLocale("error_no_data_to_display") plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage); .ifPresent(viewer::sendMessage);
return; return;
} }
@ -89,7 +89,7 @@ public class InventoryCommand extends ItemsCommand {
data.getInventory().ifPresent(inventory -> inventory.setContents(items)); data.getInventory().ifPresent(inventory -> inventory.setContents(items));
data.setSaveCause(DataSnapshot.SaveCause.INVENTORY_COMMAND); data.setSaveCause(DataSnapshot.SaveCause.INVENTORY_COMMAND);
data.setPinned( data.setPinned(
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.INVENTORY_COMMAND) plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.INVENTORY_COMMAND)
); );
}); });

@ -24,102 +24,90 @@ import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.CommandUser; import net.william278.husksync.user.CommandUser;
import net.william278.husksync.user.OnlineUser; import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.user.User; import net.william278.husksync.user.User;
import net.william278.uniform.BaseCommand;
import net.william278.uniform.Permission;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
public abstract class ItemsCommand extends Command implements TabProvider { public abstract class ItemsCommand extends PluginCommand {
protected ItemsCommand(@NotNull HuskSync plugin, @NotNull List<String> aliases) { protected ItemsCommand(@NotNull String name, @NotNull List<String> aliases, @NotNull HuskSync plugin) {
super(aliases.get(0), aliases.subList(1, aliases.size()), "<player> [version_uuid]", plugin); super(name, aliases, Permission.Default.IF_OP, plugin);
setOperatorCommand(true);
addAdditionalPermissions(Map.of("edit", true));
} }
@Override @Override
public void execute(@NotNull CommandUser executor, @NotNull String[] args) { public void provide(@NotNull BaseCommand<?> command) {
if (!(executor instanceof OnlineUser player)) { command.addSyntax((ctx) -> {
plugin.getLocales().getLocale("error_in_game_command_only") final User user = ctx.getArgument("username", User.class);
final UUID version = ctx.getArgument("version", UUID.class);
final CommandUser executor = user(command, ctx);
if (!(executor instanceof OnlineUser online)) {
plugin.getLocales().getLocale("error_in_game_command_only")
.ifPresent(executor::sendMessage); .ifPresent(executor::sendMessage);
return; return;
} }
this.showSnapshotItems(online, user, version);
// Find the user to view the items for }, user("username"), uuid("version"));
final Optional<User> optionalUser = parseStringArg(args, 0) command.addSyntax((ctx) -> {
.flatMap(name -> plugin.getDatabase().getUserByName(name)); final User user = ctx.getArgument("username", User.class);
if (optionalUser.isEmpty()) { final CommandUser executor = user(command, ctx);
plugin.getLocales().getLocale( if (!(executor instanceof OnlineUser online)) {
args.length >= 1 ? "error_invalid_player" : "error_invalid_syntax", getUsage() plugin.getLocales().getLocale("error_in_game_command_only")
).ifPresent(player::sendMessage); .ifPresent(executor::sendMessage);
return; return;
} }
this.showLatestItems(online, user);
// Show the user data }, user("username"));
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 // View (and edit) the latest user data
private void showLatestItems(@NotNull OnlineUser viewer, @NotNull User user) { private void showLatestItems(@NotNull OnlineUser viewer, @NotNull User user) {
plugin.getRedisManager().getUserData(user.getUuid(), user).thenAccept(data -> data plugin.getRedisManager().getUserData(user.getUuid(), user).thenAccept(data -> data
.or(() -> plugin.getDatabase().getLatestSnapshot(user)) .or(() -> plugin.getDatabase().getLatestSnapshot(user))
.or(() -> { .or(() -> {
plugin.getLocales().getLocale("error_no_data_to_display") plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage); .ifPresent(viewer::sendMessage);
return Optional.empty();
})
.flatMap(packed -> {
if (packed.isInvalid()) {
plugin.getLocales().getLocale("error_invalid_data", packed.getInvalidReason(plugin))
.ifPresent(viewer::sendMessage);
return Optional.empty(); return Optional.empty();
}) }
.flatMap(packed -> { return Optional.of(packed.unpack(plugin));
if (packed.isInvalid()) { })
plugin.getLocales().getLocale("error_invalid_data", packed.getInvalidReason(plugin)) .ifPresent(snapshot -> this.showItems(
.ifPresent(viewer::sendMessage); viewer, snapshot, user, viewer.hasPermission(getPermission("edit"))
return Optional.empty(); )));
}
return Optional.of(packed.unpack(plugin));
})
.ifPresent(snapshot -> this.showItems(
viewer, snapshot, user, viewer.hasPermission(getPermission("edit"))
)));
} }
// View a specific version of the user data // View a specific version of the user data
private void showSnapshotItems(@NotNull OnlineUser viewer, @NotNull User user, @NotNull UUID version) { private void showSnapshotItems(@NotNull OnlineUser viewer, @NotNull User user, @NotNull UUID version) {
plugin.getDatabase().getSnapshot(user, version) plugin.getDatabase().getSnapshot(user, version)
.or(() -> { .or(() -> {
plugin.getLocales().getLocale("error_invalid_version_uuid") plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(viewer::sendMessage); .ifPresent(viewer::sendMessage);
return Optional.empty();
})
.flatMap(packed -> {
if (packed.isInvalid()) {
plugin.getLocales().getLocale("error_invalid_data", packed.getInvalidReason(plugin))
.ifPresent(viewer::sendMessage);
return Optional.empty(); return Optional.empty();
}) }
.flatMap(packed -> { return Optional.of(packed.unpack(plugin));
if (packed.isInvalid()) { })
plugin.getLocales().getLocale("error_invalid_data", packed.getInvalidReason(plugin)) .ifPresent(snapshot -> this.showItems(
.ifPresent(viewer::sendMessage); viewer, snapshot, user, false
return Optional.empty(); ));
}
return Optional.of(packed.unpack(plugin));
})
.ifPresent(snapshot -> this.showItems(
viewer, snapshot, user, false
));
} }
// Show a GUI menu with the correct item data from the snapshot // Show a GUI menu with the correct item data from the snapshot
protected abstract void showItems(@NotNull OnlineUser viewer, @NotNull DataSnapshot.Unpacked snapshot, protected abstract void showItems(@NotNull OnlineUser viewer, @NotNull DataSnapshot.Unpacked snapshot,
@NotNull User user, boolean allowEdit); @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;
};
}
} }

@ -1,105 +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 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();
}
});
}
}

@ -0,0 +1,127 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.command;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.william278.husksync.HuskSync;
import net.william278.husksync.user.CommandUser;
import net.william278.husksync.user.User;
import net.william278.uniform.BaseCommand;
import net.william278.uniform.Command;
import net.william278.uniform.Permission;
import net.william278.uniform.element.ArgumentElement;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.function.Function;
public abstract class PluginCommand extends Command {
protected final HuskSync plugin;
protected PluginCommand(@NotNull String name, @NotNull List<String> aliases,
@NotNull Permission.Default permissionDefault, @NotNull HuskSync plugin) {
super(name, aliases, getDescription(plugin, name), new Permission(createPermission(name), permissionDefault));
this.plugin = plugin;
}
private static String getDescription(@NotNull HuskSync plugin, @NotNull String name) {
return plugin.getLocales().getRawLocale("%s_command_description".formatted(name)).orElse("");
}
@NotNull
private static String createPermission(@NotNull String name, @NotNull String... sub) {
return "husksync.command." + name + (sub.length > 0 ? "." + String.join(".", sub) : "");
}
@NotNull
protected String getPermission(@NotNull String... sub) {
return createPermission(this.getName(), sub);
}
@NotNull
@SuppressWarnings("rawtypes")
protected CommandUser user(@NotNull BaseCommand base, @NotNull CommandContext context) {
return adapt(base.getUser(context.getSource()));
}
@NotNull
protected Permission needsOp(@NotNull String... nodes) {
return new Permission(getPermission(nodes), Permission.Default.IF_OP);
}
@NotNull
protected CommandUser adapt(net.william278.uniform.CommandUser user) {
return user.getUuid() == null ? plugin.getConsole() : plugin.getOnlineUser(user.getUuid()).orElseThrow();
}
@NotNull
protected <S> ArgumentElement<S, User> user(@NotNull String name) {
return new ArgumentElement<>(name, reader -> {
final String username = reader.readString();
return plugin.getDatabase().getUserByName(username).orElseThrow(
() -> CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader)
);
}, (context, builder) -> {
plugin.getOnlineUsers().forEach(u -> builder.suggest(u.getUsername()));
return builder.buildFuture();
});
}
@NotNull
protected <S> ArgumentElement<S, UUID> uuid(@NotNull String name) {
return new ArgumentElement<>(name, reader -> {
try {
return UUID.fromString(reader.readString());
} catch (IllegalArgumentException e) {
throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(reader);
}
}, (context, builder) -> builder.buildFuture());
}
public enum Type {
HUSKSYNC_COMMAND(HuskSyncCommand::new),
USERDATA_COMMAND(UserDataCommand::new),
INVENTORY_COMMAND(InventoryCommand::new),
ENDER_CHEST_COMMAND(EnderChestCommand::new);
public final Function<HuskSync, PluginCommand> commandSupplier;
Type(@NotNull Function<HuskSync, PluginCommand> supplier) {
this.commandSupplier = supplier;
}
@NotNull
public PluginCommand supply(@NotNull HuskSync plugin) {
return commandSupplier.apply(plugin);
}
@NotNull
public static PluginCommand[] create(@NotNull HuskSync plugin) {
return Arrays.stream(values()).map(type -> type.supply(plugin)).toArray(PluginCommand[]::new);
}
}
}

@ -1,50 +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.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();
}
}

@ -19,6 +19,7 @@
package net.william278.husksync.command; package net.william278.husksync.command;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSnapshot; import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.redis.RedisKeyType; import net.william278.husksync.redis.RedisKeyType;
@ -28,116 +29,65 @@ import net.william278.husksync.user.User;
import net.william278.husksync.util.DataDumper; import net.william278.husksync.util.DataDumper;
import net.william278.husksync.util.DataSnapshotList; import net.william278.husksync.util.DataSnapshotList;
import net.william278.husksync.util.DataSnapshotOverview; import net.william278.husksync.util.DataSnapshotOverview;
import net.william278.uniform.BaseCommand;
import net.william278.uniform.CommandProvider;
import net.william278.uniform.Permission;
import net.william278.uniform.element.ArgumentElement;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*; import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.UUID;
import java.util.logging.Level; import java.util.logging.Level;
public class UserDataCommand extends Command implements TabProvider { public class UserDataCommand extends PluginCommand {
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 plugin) { public UserDataCommand(@NotNull HuskSync plugin) {
super("userdata", List.of("playerdata"), String.format( super("userdata", List.of("playerdata"), Permission.Default.IF_OP, plugin);
"<%s> [username] [version_uuid]", String.join("/", SUB_COMMANDS.keySet())
), plugin);
setOperatorCommand(true);
addAdditionalPermissions(SUB_COMMANDS);
} }
@Override @Override
public void execute(@NotNull CommandUser executor, @NotNull String[] args) { public void provide(@NotNull BaseCommand<?> command) {
final String subCommand = parseStringArg(args, 0).orElse("view").toLowerCase(Locale.ENGLISH); command.addSubCommand("view", needsOp("view"), view());
final Optional<User> optionalUser = parseStringArg(args, 1) command.addSubCommand("list", needsOp("list"), list());
.flatMap(name -> plugin.getDatabase().getUserByName(name)) command.addSubCommand("delete", needsOp("delete"), delete());
.or(() -> parseStringArg(args, 0).flatMap(name -> plugin.getDatabase().getUserByName(name))) command.addSubCommand("restore", needsOp("restore"), restore());
.or(() -> args.length < 2 && executor instanceof User userExecutor command.addSubCommand("pin", needsOp("pin"), pin());
? Optional.of(userExecutor) : Optional.empty()); command.addSubCommand("dump", needsOp("dump"), dump());
final Optional<UUID> uuid = parseUUIDArg(args, 2).or(() -> parseUUIDArg(args, 1));
if (optionalUser.isEmpty()) {
plugin.getLocales().getLocale("error_invalid_player")
.ifPresent(executor::sendMessage);
return;
}
final User user = optionalUser.get();
switch (subCommand) {
case "view" -> uuid.ifPresentOrElse(
version -> viewSnapshot(executor, user, version),
() -> viewLatestSnapshot(executor, user)
);
case "list" -> listSnapshots(
executor, user, parseIntArg(args, 2).or(() -> parseIntArg(args, 1)).orElse(1)
);
case "delete" -> uuid.ifPresentOrElse(
version -> deleteSnapshot(executor, user, version),
() -> plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata delete <username> <version_uuid>")
.ifPresent(executor::sendMessage)
);
case "restore" -> uuid.ifPresentOrElse(
version -> restoreSnapshot(executor, user, version),
() -> plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata restore <username> <version_uuid>")
.ifPresent(executor::sendMessage)
);
case "pin" -> uuid.ifPresentOrElse(
version -> pinSnapshot(executor, user, version),
() -> plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata pin <username> <version_uuid>")
.ifPresent(executor::sendMessage)
);
case "dump" -> uuid.ifPresentOrElse(
version -> dumpSnapshot(executor, user, version, parseStringArg(args, 3)
.map(arg -> arg.equalsIgnoreCase("web")).orElse(false)),
() -> plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata dump <username> <version_uuid> <web/file>")
.ifPresent(executor::sendMessage)
);
default -> plugin.getLocales().getLocale("error_invalid_syntax", getUsage())
.ifPresent(executor::sendMessage);
}
} }
// Show the latest snapshot // Show the latest snapshot
private void viewLatestSnapshot(@NotNull CommandUser executor, @NotNull User user) { private void viewLatestSnapshot(@NotNull CommandUser executor, @NotNull User user) {
plugin.getDatabase().getLatestSnapshot(user).ifPresentOrElse( plugin.getDatabase().getLatestSnapshot(user).ifPresentOrElse(
data -> { data -> {
if (data.isInvalid()) { if (data.isInvalid()) {
plugin.getLocales().getLocale("error_invalid_data", data.getInvalidReason(plugin)) plugin.getLocales().getLocale("error_invalid_data", data.getInvalidReason(plugin))
.ifPresent(executor::sendMessage); .ifPresent(executor::sendMessage);
return; return;
} }
DataSnapshotOverview.of(data.unpack(plugin), data.getFileSize(plugin), user, plugin) DataSnapshotOverview.of(data.unpack(plugin), data.getFileSize(plugin), user, plugin)
.show(executor); .show(executor);
}, },
() -> plugin.getLocales().getLocale("error_no_data_to_display") () -> plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(executor::sendMessage) .ifPresent(executor::sendMessage)
); );
} }
// Show the specified snapshot // Show the specified snapshot
private void viewSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) { private void viewSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) {
plugin.getDatabase().getSnapshot(user, version).ifPresentOrElse( plugin.getDatabase().getSnapshot(user, version).ifPresentOrElse(
data -> { data -> {
if (data.isInvalid()) { if (data.isInvalid()) {
plugin.getLocales().getLocale("error_invalid_data", data.getInvalidReason(plugin)) plugin.getLocales().getLocale("error_invalid_data", data.getInvalidReason(plugin))
.ifPresent(executor::sendMessage); .ifPresent(executor::sendMessage);
return; return;
} }
DataSnapshotOverview.of(data.unpack(plugin), data.getFileSize(plugin), user, plugin) DataSnapshotOverview.of(data.unpack(plugin), data.getFileSize(plugin), user, plugin)
.show(executor); .show(executor);
}, },
() -> plugin.getLocales().getLocale("error_invalid_version_uuid") () -> plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(executor::sendMessage) .ifPresent(executor::sendMessage)
); );
} }
@ -146,7 +96,7 @@ public class UserDataCommand extends Command implements TabProvider {
final List<DataSnapshot.Packed> dataList = plugin.getDatabase().getAllSnapshots(user); final List<DataSnapshot.Packed> dataList = plugin.getDatabase().getAllSnapshots(user);
if (dataList.isEmpty()) { if (dataList.isEmpty()) {
plugin.getLocales().getLocale("error_no_data_to_display") plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(executor::sendMessage); .ifPresent(executor::sendMessage);
return; return;
} }
DataSnapshotList.create(dataList, user, plugin).displayPage(executor, page); DataSnapshotList.create(dataList, user, plugin).displayPage(executor, page);
@ -156,16 +106,16 @@ public class UserDataCommand extends Command implements TabProvider {
private void deleteSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) { private void deleteSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version) {
if (!plugin.getDatabase().deleteSnapshot(user, version)) { if (!plugin.getDatabase().deleteSnapshot(user, version)) {
plugin.getLocales().getLocale("error_invalid_version_uuid") plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(executor::sendMessage); .ifPresent(executor::sendMessage);
return; return;
} }
plugin.getRedisManager().clearUserData(user); plugin.getRedisManager().clearUserData(user);
plugin.getLocales().getLocale("data_deleted", plugin.getLocales().getLocale("data_deleted",
version.toString().split("-")[0], version.toString().split("-")[0],
version.toString(), version.toString(),
user.getUsername(), user.getUsername(),
user.getUuid().toString()) user.getUuid().toString())
.ifPresent(executor::sendMessage); .ifPresent(executor::sendMessage);
} }
// Restore a snapshot // Restore a snapshot
@ -173,7 +123,7 @@ public class UserDataCommand extends Command implements TabProvider {
final Optional<DataSnapshot.Packed> optionalData = plugin.getDatabase().getSnapshot(user, version); final Optional<DataSnapshot.Packed> optionalData = plugin.getDatabase().getSnapshot(user, version);
if (optionalData.isEmpty()) { if (optionalData.isEmpty()) {
plugin.getLocales().getLocale("error_invalid_version_uuid") plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(executor::sendMessage); .ifPresent(executor::sendMessage);
return; return;
} }
@ -181,14 +131,14 @@ public class UserDataCommand extends Command implements TabProvider {
final DataSnapshot.Packed data = optionalData.get().copy(); final DataSnapshot.Packed data = optionalData.get().copy();
if (data.isInvalid()) { if (data.isInvalid()) {
plugin.getLocales().getLocale("error_invalid_data", data.getInvalidReason(plugin)) plugin.getLocales().getLocale("error_invalid_data", data.getInvalidReason(plugin))
.ifPresent(executor::sendMessage); .ifPresent(executor::sendMessage);
return; return;
} }
data.edit(plugin, (unpacked -> { data.edit(plugin, (unpacked -> {
unpacked.getHealth().ifPresent(status -> status.setHealth(Math.max(1, status.getHealth()))); unpacked.getHealth().ifPresent(status -> status.setHealth(Math.max(1, status.getHealth())));
unpacked.setSaveCause(DataSnapshot.SaveCause.BACKUP_RESTORE); unpacked.setSaveCause(DataSnapshot.SaveCause.BACKUP_RESTORE);
unpacked.setPinned( unpacked.setPinned(
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.BACKUP_RESTORE) plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.BACKUP_RESTORE)
); );
})); }));
@ -198,7 +148,7 @@ public class UserDataCommand extends Command implements TabProvider {
redis.getUserData(u).ifPresent(d -> redis.setUserData(u, s, RedisKeyType.TTL_1_YEAR)); redis.getUserData(u).ifPresent(d -> redis.setUserData(u, s, RedisKeyType.TTL_1_YEAR));
redis.sendUserDataUpdate(u, s); redis.sendUserDataUpdate(u, s);
plugin.getLocales().getLocale("data_restored", u.getUsername(), u.getUuid().toString(), plugin.getLocales().getLocale("data_restored", u.getUsername(), u.getUuid().toString(),
s.getShortId(), s.getId().toString()).ifPresent(executor::sendMessage); s.getShortId(), s.getId().toString()).ifPresent(executor::sendMessage);
}); });
} }
@ -207,7 +157,7 @@ public class UserDataCommand extends Command implements TabProvider {
final Optional<DataSnapshot.Packed> optionalData = plugin.getDatabase().getSnapshot(user, version); final Optional<DataSnapshot.Packed> optionalData = plugin.getDatabase().getSnapshot(user, version);
if (optionalData.isEmpty()) { if (optionalData.isEmpty()) {
plugin.getLocales().getLocale("error_invalid_version_uuid") plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(executor::sendMessage); .ifPresent(executor::sendMessage);
return; return;
} }
@ -219,8 +169,8 @@ public class UserDataCommand extends Command implements TabProvider {
plugin.getDatabase().pinSnapshot(user, data.getId()); plugin.getDatabase().pinSnapshot(user, data.getId());
} }
plugin.getLocales().getLocale(data.isPinned() ? "data_unpinned" : "data_pinned", data.getShortId(), plugin.getLocales().getLocale(data.isPinned() ? "data_unpinned" : "data_pinned", data.getShortId(),
data.getId().toString(), user.getUsername(), user.getUuid().toString()) data.getId().toString(), user.getUsername(), user.getUuid().toString())
.ifPresent(executor::sendMessage); .ifPresent(executor::sendMessage);
} }
// Dump a snapshot // Dump a snapshot
@ -228,7 +178,7 @@ public class UserDataCommand extends Command implements TabProvider {
final Optional<DataSnapshot.Packed> data = plugin.getDatabase().getSnapshot(user, version); final Optional<DataSnapshot.Packed> data = plugin.getDatabase().getSnapshot(user, version);
if (data.isEmpty()) { if (data.isEmpty()) {
plugin.getLocales().getLocale("error_invalid_version_uuid") plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(executor::sendMessage); .ifPresent(executor::sendMessage);
return; return;
} }
@ -237,22 +187,98 @@ public class UserDataCommand extends Command implements TabProvider {
final DataDumper dumper = DataDumper.create(userData, user, plugin); final DataDumper dumper = DataDumper.create(userData, user, plugin);
try { try {
plugin.getLocales().getLocale("data_dumped", userData.getShortId(), user.getUsername(), plugin.getLocales().getLocale("data_dumped", userData.getShortId(), user.getUsername(),
(webDump ? dumper.toWeb() : dumper.toFile())).ifPresent(executor::sendMessage); (webDump ? dumper.toWeb() : dumper.toFile())).ifPresent(executor::sendMessage);
} catch (Throwable e) { } catch (Throwable e) {
plugin.log(Level.SEVERE, "Failed to dump user data", e); plugin.log(Level.SEVERE, "Failed to dump user data", e);
} }
} }
@Nullable @NotNull
@Override private CommandProvider view() {
public List<String> suggest(@NotNull CommandUser executor, @NotNull String[] args) { return (sub) -> {
return switch (args.length) { sub.addSyntax((ctx) -> {
case 0, 1 -> SUB_COMMANDS.keySet().stream().sorted().toList(); final User user = ctx.getArgument("username", User.class);
case 2 -> plugin.getOnlineUsers().stream().map(User::getUsername).toList(); viewLatestSnapshot(user(sub, ctx), user);
case 4 -> parseStringArg(args, 0) }, user("username"));
.map(arg -> arg.equalsIgnoreCase("dump") ? List.of("web", "file") : null) sub.addSyntax((ctx) -> {
.orElse(null); final User user = ctx.getArgument("username", User.class);
default -> null; final UUID version = ctx.getArgument("version", UUID.class);
viewSnapshot(user(sub, ctx), user, version);
}, user("username"), uuid("version"));
}; };
} }
@NotNull
private CommandProvider list() {
return (sub) -> {
sub.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
listSnapshots(user(sub, ctx), user, 1);
}, user("username"));
sub.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
final int page = ctx.getArgument("page", Integer.class);
listSnapshots(user(sub, ctx), user, page);
}, user("username"), BaseCommand.intNum("page", 1));
};
}
@NotNull
private CommandProvider delete() {
return (sub) -> sub.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
final UUID version = ctx.getArgument("version", UUID.class);
deleteSnapshot(user(sub, ctx), user, version);
}, user("username"), uuid("version"));
}
@NotNull
private CommandProvider restore() {
return (sub) -> sub.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
final UUID version = ctx.getArgument("version", UUID.class);
restoreSnapshot(user(sub, ctx), user, version);
}, user("username"), uuid("version"));
}
@NotNull
private CommandProvider pin() {
return (sub) -> sub.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
final UUID version = ctx.getArgument("version", UUID.class);
pinSnapshot(user(sub, ctx), user, version);
}, user("username"), uuid("version"));
}
@NotNull
private CommandProvider dump() {
return (sub) -> sub.addSyntax((ctx) -> {
final User user = ctx.getArgument("username", User.class);
final UUID version = ctx.getArgument("version", UUID.class);
final DumpType type = ctx.getArgument("type", DumpType.class);
dumpSnapshot(user(sub, ctx), user, version, type == DumpType.WEB);
}, user("username"), uuid("version"), dumpType());
}
private <S> ArgumentElement<S, DumpType> dumpType() {
return new ArgumentElement<>("type", reader -> {
final String type = reader.readString();
return switch (type.toLowerCase(Locale.ENGLISH)) {
case "web" -> DumpType.WEB;
case "file" -> DumpType.FILE;
default -> throw CommandSyntaxException.BUILT_IN_EXCEPTIONS
.dispatcherUnknownArgument().createWithContext(reader);
};
}, (context, builder) -> {
builder.suggest("web");
builder.suggest("file");
return builder.buildFuture();
});
}
enum DumpType {
WEB,
FILE
}
} }

@ -69,9 +69,6 @@ public class Settings {
@Comment("Enable development debug logging") @Comment("Enable development debug logging")
private boolean debugLogging = false; private boolean debugLogging = false;
@Comment("Whether to provide modern, rich TAB suggestions for commands (if available)")
private boolean brigadierTabCompletion = false;
@Comment({"Whether to enable the Player Analytics hook.", "Docs: https://william278.net/docs/husksync/plan-hook"}) @Comment({"Whether to enable the Player Analytics hook.", "Docs: https://william278.net/docs/husksync/plan-hook"})
private boolean enablePlanHook = true; private boolean enablePlanHook = true;

@ -24,7 +24,7 @@ import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public interface CommandUser { public interface CommandUser {
@NotNull @NotNull
Audience getAudience(); Audience getAudience();

@ -42,4 +42,6 @@ public final class ConsoleUser implements CommandUser {
public boolean hasPermission(@NotNull String permission) { public boolean hasPermission(@NotNull String permission) {
return true; return true;
} }
} }

@ -20,7 +20,7 @@ dependencies {
modImplementation include("eu.pb4:sgui:${sgui_version}") modImplementation include("eu.pb4:sgui:${sgui_version}")
modCompileOnly "net.fabricmc.fabric-api:fabric-api:${fabric_api_version}" modCompileOnly "net.fabricmc.fabric-api:fabric-api:${fabric_api_version}"
// Runtime dependencies on Bukkit; "include" them on Fabric. (todo: minify JAR?) implementation include('net.william278.uniform:uniform-fabric:1.1+1.20.1')
implementation include('org.apache.commons:commons-pool2:2.12.0') implementation include('org.apache.commons:commons-pool2:2.12.0')
implementation include("redis.clients:jedis:$jedis_version") implementation include("redis.clients:jedis:$jedis_version")
implementation include("com.mysql:mysql-connector-j:$mysql_driver_version") implementation include("com.mysql:mysql-connector-j:$mysql_driver_version")
@ -33,7 +33,7 @@ dependencies {
annotationProcessor 'org.projectlombok:lombok:1.18.32' annotationProcessor 'org.projectlombok:lombok:1.18.32'
shadow project(path: ":common") implementation project(path: ":common")
} }
shadowJar { shadowJar {
@ -54,6 +54,7 @@ shadowJar {
relocate 'org.intellij', 'net.william278.husksync.libraries' relocate 'org.intellij', 'net.william278.husksync.libraries'
relocate 'com.zaxxer', 'net.william278.husksync.libraries' relocate 'com.zaxxer', 'net.william278.husksync.libraries'
relocate 'de.exlll', 'net.william278.husksync.libraries' relocate 'de.exlll', 'net.william278.husksync.libraries'
relocate 'net.william278.uniform', 'net.william278.husksync.libraries.uniform'
relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell' relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell'
relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown' relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown'
relocate 'org.json', 'net.william278.husksync.libraries.json' relocate 'org.json', 'net.william278.husksync.libraries.json'

@ -28,7 +28,6 @@ import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import net.fabricmc.api.DedicatedServerModInitializer; import net.fabricmc.api.DedicatedServerModInitializer;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer; import net.fabricmc.loader.api.ModContainer;
@ -40,8 +39,7 @@ import net.william278.husksync.adapter.DataAdapter;
import net.william278.husksync.adapter.GsonAdapter; import net.william278.husksync.adapter.GsonAdapter;
import net.william278.husksync.adapter.SnappyGsonAdapter; import net.william278.husksync.adapter.SnappyGsonAdapter;
import net.william278.husksync.api.FabricHuskSyncAPI; import net.william278.husksync.api.FabricHuskSyncAPI;
import net.william278.husksync.command.Command; import net.william278.husksync.command.PluginCommand;
import net.william278.husksync.command.FabricCommand;
import net.william278.husksync.config.Locales; import net.william278.husksync.config.Locales;
import net.william278.husksync.config.Server; import net.william278.husksync.config.Server;
import net.william278.husksync.config.Settings; import net.william278.husksync.config.Settings;
@ -62,6 +60,8 @@ import net.william278.husksync.user.FabricUser;
import net.william278.husksync.user.OnlineUser; import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.util.FabricTask; import net.william278.husksync.util.FabricTask;
import net.william278.husksync.util.LegacyConverter; import net.william278.husksync.util.LegacyConverter;
import net.william278.uniform.Uniform;
import net.william278.uniform.fabric.FabricUniform;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -74,22 +74,22 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.*;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors;
@Getter @Getter
@NoArgsConstructor @NoArgsConstructor
public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync, FabricTask.Supplier, public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync, FabricTask.Supplier,
FabricEventDispatcher { FabricEventDispatcher {
private static final String PLATFORM_TYPE_ID = "fabric"; private static final String PLATFORM_TYPE_ID = "fabric";
private final TreeMap<Identifier, Serializer<? extends Data>> serializers = Maps.newTreeMap( private final TreeMap<Identifier, Serializer<? extends Data>> serializers = Maps.newTreeMap(
SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR
); );
private final Map<UUID, Map<Identifier, Data>> playerCustomDataStore = Maps.newConcurrentMap(); private final Map<UUID, Map<Identifier, Data>> playerCustomDataStore = Maps.newConcurrentMap();
private final Map<String, Boolean> permissions = Maps.newHashMap(); private final Map<String, Boolean> permissions = Maps.newHashMap();
private final List<Migrator> availableMigrators = Lists.newArrayList(); private final List<Migrator> availableMigrators = Lists.newArrayList();
private final Set<UUID> lockedPlayers = Sets.newConcurrentHashSet(); private final Set<UUID> lockedPlayers = Sets.newConcurrentHashSet();
private final Map<UUID, FabricUser> playerMap = Maps.newConcurrentMap();
private Logger logger; private Logger logger;
private ModContainer mod; private ModContainer mod;
@ -127,7 +127,7 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
}); });
// Register commands // Register commands
initialize("commands", (plugin) -> this.registerCommands()); initialize("commands", (plugin) -> getUniform().register(PluginCommand.Type.create(this)));
// Load HuskSync after server startup // Load HuskSync after server startup
ServerLifecycleEvents.SERVER_STARTED.register(server -> { ServerLifecycleEvents.SERVER_STARTED.register(server -> {
@ -232,12 +232,6 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
log(Level.INFO, "Successfully disabled HuskSync v" + getPluginVersion()); log(Level.INFO, "Successfully disabled HuskSync v" + getPluginVersion());
} }
private void registerCommands() {
final List<Command> commands = FabricCommand.Type.getCommands(this);
CommandRegistrationCallback.EVENT.register((dispatcher, registry, environment) ->
commands.forEach(command -> new FabricCommand(command, this).register(dispatcher))
);
}
@NotNull @NotNull
@Override @Override
@ -253,31 +247,34 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
@Override @Override
@NotNull @NotNull
public Set<OnlineUser> getOnlineUsers() { public Set<OnlineUser> getOnlineUsers() {
return minecraftServer.getPlayerManager().getPlayerList() return Sets.newHashSet(playerMap.values());
.stream().map(user -> (OnlineUser) FabricUser.adapt(user, this))
.collect(Collectors.toSet());
} }
@Override @Override
@NotNull @NotNull
public Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid) { public Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid) {
return Optional.ofNullable(minecraftServer.getPlayerManager().getPlayer(uuid)) return Optional.ofNullable(playerMap.get(uuid));
.map(user -> FabricUser.adapt(user, this)); }
@Override
@NotNull
public Uniform getUniform() {
return FabricUniform.getInstance();
} }
@Override @Override
@Nullable @Nullable
public InputStream getResource(@NotNull String name) { public InputStream getResource(@NotNull String name) {
return this.mod.findPath(name) return this.mod.findPath(name)
.map(path -> { .map(path -> {
try { try {
return Files.newInputStream(path); return Files.newInputStream(path);
} catch (IOException e) { } catch (IOException e) {
log(Level.WARNING, "Failed to load resource: " + name, e); log(Level.WARNING, "Failed to load resource: " + name, e);
} }
return null; return null;
}) })
.orElse(this.getClass().getClassLoader().getResourceAsStream(name)); .orElse(this.getClass().getClassLoader().getResourceAsStream(name));
} }
@Override @Override
@ -297,11 +294,11 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
@Override @Override
public void log(@NotNull Level level, @NotNull String message, @NotNull Throwable... throwable) { public void log(@NotNull Level level, @NotNull String message, @NotNull Throwable... throwable) {
LoggingEventBuilder logEvent = logger.makeLoggingEventBuilder( LoggingEventBuilder logEvent = logger.makeLoggingEventBuilder(
switch (level.getName()) { switch (level.getName()) {
case "WARNING" -> org.slf4j.event.Level.WARN; case "WARNING" -> org.slf4j.event.Level.WARN;
case "SEVERE" -> org.slf4j.event.Level.ERROR; case "SEVERE" -> org.slf4j.event.Level.ERROR;
default -> org.slf4j.event.Level.INFO; default -> org.slf4j.event.Level.INFO;
} }
); );
if (throwable.length >= 1) { if (throwable.length >= 1) {
logEvent = logEvent.setCause(throwable[0]); logEvent = logEvent.setCause(throwable[0]);

@ -1,153 +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 com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.tree.LiteralCommandNode;
import me.lucko.fabric.api.permissions.v0.PermissionCheckEvent;
import me.lucko.fabric.api.permissions.v0.Permissions;
import net.fabricmc.fabric.api.util.TriState;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.network.ServerPlayerEntity;
import net.william278.husksync.FabricHuskSync;
import net.william278.husksync.HuskSync;
import net.william278.husksync.user.CommandUser;
import net.william278.husksync.user.FabricUser;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Predicate;
import static com.mojang.brigadier.arguments.StringArgumentType.greedyString;
import static net.minecraft.server.command.CommandManager.argument;
import static net.minecraft.server.command.CommandManager.literal;
public class FabricCommand {
private final FabricHuskSync plugin;
private final Command command;
public FabricCommand(@NotNull Command command, @NotNull FabricHuskSync plugin) {
this.command = command;
this.plugin = plugin;
}
public void register(@NotNull CommandDispatcher<ServerCommandSource> dispatcher) {
// Register brigadier command
final Predicate<ServerCommandSource> predicate = Permissions
.require(command.getPermission(), command.isOperatorCommand() ? 3 : 0);
final LiteralArgumentBuilder<ServerCommandSource> builder = literal(command.getName())
.requires(predicate).executes(getBrigadierExecutor());
plugin.getPermissions().put(command.getPermission(), command.isOperatorCommand());
if (!command.getRawUsage().isBlank()) {
builder.then(argument(command.getRawUsage().replaceAll("[<>\\[\\]]", ""), greedyString())
.executes(getBrigadierExecutor())
.suggests(getBrigadierSuggester()));
}
// Register additional permissions
final Map<String, Boolean> permissions = command.getAdditionalPermissions();
permissions.forEach((permission, isOp) -> plugin.getPermissions().put(permission, isOp));
PermissionCheckEvent.EVENT.register((player, node) -> {
if (permissions.containsKey(node) && permissions.get(node) && player.hasPermissionLevel(3)) {
return TriState.TRUE;
}
return TriState.DEFAULT;
});
// Register aliases
final LiteralCommandNode<ServerCommandSource> node = dispatcher.register(builder);
dispatcher.register(literal("husksync:" + command.getName())
.requires(predicate).executes(getBrigadierExecutor()).redirect(node));
command.getAliases().forEach(alias -> dispatcher.register(literal(alias)
.requires(predicate).executes(getBrigadierExecutor()).redirect(node)));
}
private com.mojang.brigadier.Command<ServerCommandSource> getBrigadierExecutor() {
return (context) -> {
command.onExecuted(
resolveExecutor(context.getSource()),
command.removeFirstArg(context.getInput().split(" "))
);
return 1;
};
}
private com.mojang.brigadier.suggestion.SuggestionProvider<ServerCommandSource> getBrigadierSuggester() {
if (!(command instanceof TabProvider provider)) {
return (context, builder) -> com.mojang.brigadier.suggestion.Suggestions.empty();
}
return (context, builder) -> {
final String[] args = command.removeFirstArg(context.getInput().split(" ", -1));
provider.getSuggestions(resolveExecutor(context.getSource()), args).stream()
.map(suggestion -> {
final String completedArgs = String.join(" ", args);
int lastIndex = completedArgs.lastIndexOf(" ");
if (lastIndex == -1) {
return suggestion;
}
return completedArgs.substring(0, lastIndex + 1) + suggestion;
})
.forEach(builder::suggest);
return builder.buildFuture();
};
}
private CommandUser resolveExecutor(@NotNull ServerCommandSource source) {
if (source.getEntity() instanceof ServerPlayerEntity player) {
return FabricUser.adapt(player, plugin);
}
return plugin.getConsole();
}
/**
* Commands available on the Fabric HuskSync implementation.
*/
public enum Type {
HUSKSYNC_COMMAND(HuskSyncCommand::new),
USERDATA_COMMAND(UserDataCommand::new),
INVENTORY_COMMAND(InventoryCommand::new),
ENDER_CHEST_COMMAND(EnderChestCommand::new);
private final Function<HuskSync, Command> supplier;
Type(@NotNull Function<HuskSync, Command> supplier) {
this.supplier = supplier;
}
@NotNull
public Command createCommand(@NotNull HuskSync plugin) {
return supplier.apply(plugin);
}
@NotNull
public static List<Command> getCommands(@NotNull FabricHuskSync plugin) {
return Arrays.stream(values()).map(type -> type.createCommand(plugin)).toList();
}
}
}

@ -315,7 +315,7 @@ public abstract class FabricData implements Data {
// Only save the advancement if criteria has been completed // Only save the advancement if criteria has been completed
if (!awardedCriteria.isEmpty()) { if (!awardedCriteria.isEmpty()) {
advancements.add(Advancement.adapt(advancement.getId().asString(), awardedCriteria)); advancements.add(Advancement.adapt(advancement.getId().toString(), awardedCriteria));
} }
}); });
return new FabricData.Advancements(advancements); return new FabricData.Advancements(advancements);
@ -479,7 +479,7 @@ public abstract class FabricData implements Data {
Registries.STAT_TYPE.getEntrySet().forEach(stat -> { Registries.STAT_TYPE.getEntrySet().forEach(stat -> {
final Registry<?> registry = stat.getValue().getRegistry(); final Registry<?> registry = stat.getValue().getRegistry();
final String registryId = registry.getKey().getValue().value(); final String registryId = registry.getKey().getValue().getPath();
if (registryId.equals("custom_stat")) { if (registryId.equals("custom_stat")) {
return; return;
} }
@ -488,13 +488,13 @@ public abstract class FabricData implements Data {
case ITEM_STAT_TYPE -> items; case ITEM_STAT_TYPE -> items;
case ENTITY_STAT_TYPE -> entities; case ENTITY_STAT_TYPE -> entities;
default -> throw new IllegalStateException("Unexpected value: %s".formatted(registryId)); default -> throw new IllegalStateException("Unexpected value: %s".formatted(registryId));
}).compute(stat.getKey().getValue().asString(), (k, v) -> v == null ? Maps.newHashMap() : v); }).compute(stat.getKey().getValue().toString(), (k, v) -> v == null ? Maps.newHashMap() : v);
registry.getEntrySet().forEach(entry -> { registry.getEntrySet().forEach(entry -> {
@SuppressWarnings({"unchecked", "rawtypes"}) final int value = player.getStatHandler() @SuppressWarnings({"unchecked", "rawtypes"}) final int value = player.getStatHandler()
.getStat((StatType) stat.getValue(), entry.getValue()); .getStat((StatType) stat.getValue(), entry.getValue());
if (value != 0) { if (value != 0) {
map.put(entry.getKey().getValue().asString(), value); map.put(entry.getKey().getValue().toString(), value);
} }
}); });
}); });
@ -504,7 +504,7 @@ public abstract class FabricData implements Data {
Registries.CUSTOM_STAT.getEntrySet().forEach(stat -> { Registries.CUSTOM_STAT.getEntrySet().forEach(stat -> {
final int value = player.getStatHandler().getStat(Stats.CUSTOM.getOrCreateStat(stat.getValue())); final int value = player.getStatHandler().getStat(Stats.CUSTOM.getOrCreateStat(stat.getValue()));
if (value != 0) { if (value != 0) {
generic.put(stat.getKey().getValue().asString(), value); generic.put(stat.getKey().getValue().toString(), value);
} }
}); });
@ -587,7 +587,7 @@ public abstract class FabricData implements Data {
-1 -1
))); )));
attributes.add(new Attribute( attributes.add(new Attribute(
key.asString(), key.toString(),
instance.getBaseValue(), instance.getBaseValue(),
modifiers modifiers
)); ));
@ -596,7 +596,7 @@ public abstract class FabricData implements Data {
} }
public Optional<Attribute> getAttribute(@NotNull EntityAttribute id) { public Optional<Attribute> getAttribute(@NotNull EntityAttribute id) {
return Optional.ofNullable(Registries.ATTRIBUTE.getId(id)).map(Identifier::asString) return Optional.ofNullable(Registries.ATTRIBUTE.getId(id)).map(Identifier::toString)
.flatMap(key -> attributes.stream().filter(attribute -> attribute.name().equals(key)).findFirst()); .flatMap(key -> attributes.stream().filter(attribute -> attribute.name().equals(key)).findFirst());
} }

@ -44,6 +44,7 @@ import net.minecraft.util.hit.BlockHitResult;
import net.minecraft.util.hit.EntityHitResult; import net.minecraft.util.hit.EntityHitResult;
import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.BlockPos;
import net.minecraft.world.World; import net.minecraft.world.World;
import net.william278.husksync.FabricHuskSync;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import net.william278.husksync.config.Settings.SynchronizationSettings.SaveOnDeathSettings; import net.william278.husksync.config.Settings.SynchronizationSettings.SaveOnDeathSettings;
import net.william278.husksync.data.FabricData; import net.william278.husksync.data.FabricData;
@ -82,10 +83,13 @@ public class FabricEventListener extends EventListener implements LockedHandler
private void handlePlayerJoin(@NotNull ServerPlayNetworkHandler handler, @NotNull PacketSender sender, private void handlePlayerJoin(@NotNull ServerPlayNetworkHandler handler, @NotNull PacketSender sender,
@NotNull MinecraftServer server) { @NotNull MinecraftServer server) {
handlePlayerJoin(FabricUser.adapt(handler.player, plugin)); final FabricUser user = FabricUser.adapt(handler.player, plugin);
((FabricHuskSync) plugin).getPlayerMap().put(handler.player.getUuid(), user);
handlePlayerJoin(user);
} }
private void handlePlayerQuit(@NotNull ServerPlayNetworkHandler handler, @NotNull MinecraftServer server) { private void handlePlayerQuit(@NotNull ServerPlayNetworkHandler handler, @NotNull MinecraftServer server) {
((FabricHuskSync) plugin).getPlayerMap().remove(handler.player.getUuid());
handlePlayerQuit(FabricUser.adapt(handler.player, plugin)); handlePlayerQuit(FabricUser.adapt(handler.player, plugin));
} }

@ -72,7 +72,7 @@ public class FabricUser extends OnlineUser implements FabricUserDataHolder {
@Override @Override
public void sendToast(@NotNull MineDown title, @NotNull MineDown description, @NotNull String iconMaterial, public void sendToast(@NotNull MineDown title, @NotNull MineDown description, @NotNull String iconMaterial,
@NotNull String backgroundType) { @NotNull String backgroundType) {
player.sendActionBar(title.toComponent()); // Toasts unimplemented for now getAudience().sendActionBar(title.toComponent()); // Toasts unimplemented for now
} }
@Override @Override

@ -2,6 +2,8 @@ dependencies {
implementation project(':bukkit') implementation project(':bukkit')
compileOnly project(':common') compileOnly project(':common')
implementation 'net.william278.uniform:uniform-paper:1.1'
compileOnly 'io.papermc.paper:paper-api:1.19.4-R0.1-SNAPSHOT' compileOnly 'io.papermc.paper:paper-api:1.19.4-R0.1-SNAPSHOT'
compileOnly 'org.jetbrains:annotations:24.1.0' compileOnly 'org.jetbrains:annotations:24.1.0'
compileOnly 'org.projectlombok:lombok:1.18.32' compileOnly 'org.projectlombok:lombok:1.18.32'
@ -24,6 +26,7 @@ shadowJar {
relocate 'org.intellij', 'net.william278.husksync.libraries' relocate 'org.intellij', 'net.william278.husksync.libraries'
relocate 'com.zaxxer', 'net.william278.husksync.libraries' relocate 'com.zaxxer', 'net.william278.husksync.libraries'
relocate 'de.exlll', 'net.william278.husksync.libraries' relocate 'de.exlll', 'net.william278.husksync.libraries'
relocate 'net.william278.uniform', 'net.william278.husksync.libraries.uniform'
relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell' relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell'
relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown' relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown'
relocate 'net.william278.mapdataapi', 'net.william278.husksync.libraries.mapdataapi' relocate 'net.william278.mapdataapi', 'net.william278.husksync.libraries.mapdataapi'
@ -33,7 +36,6 @@ shadowJar {
relocate 'org.json', 'net.william278.husksync.libraries.json' relocate 'org.json', 'net.william278.husksync.libraries.json'
relocate 'net.querz', 'net.william278.husksync.libraries.nbtparser' relocate 'net.querz', 'net.william278.husksync.libraries.nbtparser'
relocate 'net.roxeez', 'net.william278.husksync.libraries' relocate 'net.roxeez', 'net.william278.husksync.libraries'
relocate 'me.lucko.commodore', 'net.william278.husksync.libraries.commodore'
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats' relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
relocate 'dev.triumphteam.gui', 'net.william278.husksync.libraries.triumphgui' relocate 'dev.triumphteam.gui', 'net.william278.husksync.libraries.triumphgui'
relocate 'space.arim.morepaperlib', 'net.william278.husksync.libraries.paperlib' relocate 'space.arim.morepaperlib', 'net.william278.husksync.libraries.paperlib'

@ -22,6 +22,8 @@ package net.william278.husksync;
import net.kyori.adventure.audience.Audience; import net.kyori.adventure.audience.Audience;
import net.william278.husksync.listener.BukkitEventListener; import net.william278.husksync.listener.BukkitEventListener;
import net.william278.husksync.listener.PaperEventListener; import net.william278.husksync.listener.PaperEventListener;
import net.william278.uniform.Uniform;
import net.william278.uniform.paper.PaperUniform;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -43,4 +45,9 @@ public class PaperHuskSync extends BukkitHuskSync {
return player == null || !player.isOnline() ? Audience.empty() : player; return player == null || !player.isOnline() ? Audience.empty() : player;
} }
@Override
@NotNull
public Uniform getUniform() {
return PaperUniform.getInstance(this);
}
} }

@ -33,7 +33,7 @@ class Parameters:
proxy_plugins = [] proxy_plugins = []
proxy_plugin_folders = [] proxy_plugin_folders = []
just_update_plugins = True just_update_plugins = False
def main(update=False): def main(update=False):

Loading…
Cancel
Save