9
0
mirror of https://github.com/WiIIiam278/HuskSync.git synced 2025-12-26 18:19:10 +00:00

Compare commits

...

30 Commits
3.6 ... 3.6.3

Author SHA1 Message Date
William
8105ac27fc deps: bump Uniform to 1.1.8
Fixes startup NPE fetching usage text
2024-06-19 12:49:55 +01:00
William
44f251a948 deps: bump Uniform to 1.1.7
Adds usage text to bukkit & legacy Paper commands
2024-06-19 12:45:50 +01:00
William
463e707d27 deps: bump Uniform to 1.1.6 2024-06-19 12:23:40 +01:00
William
2d85910744 deps: bump Uniform to 1.1.5 2024-06-18 23:47:33 +01:00
William
268b279fdf feat: add the ability to disable HuskSync commands 2024-06-18 13:26:21 +01:00
William
a8ca3314d8 refactor: minor userdata dump refactor 2024-06-18 13:20:24 +01:00
William
2bdd3dae37 fix: enable game mode syncing by default
not sure why this is off by default
2024-06-18 13:11:29 +01:00
William
e29564c4ad deps: bump Uniform to 1.1.4
Fixes namespaced-backed commands being missing
2024-06-18 13:02:58 +01:00
William
6b8bb23af9 fix: cleanup leftover todo 2024-06-18 12:34:56 +01:00
William
91bbe05851 fix: fix various Fabric issues
Adjusted a mixin
Fixed Uniform being relocated causing a ClassNotFound exception (it's a JiJ mod now)
2024-06-18 12:31:58 +01:00
William
8ed6869aad docs: update maven README badge 2024-06-18 01:03:08 +01:00
dependabot[bot]
7efdf0d329 build(deps): bump urllib3 from 2.0.7 to 2.2.2 in /test (#324)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.0.7 to 2.2.2.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.0.7...2.2.2)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-18 01:01:26 +01:00
William
49c32e3f98 build: adjust repos order 2024-06-18 00:59:56 +01:00
William
f0574527b9 build: bump gradle to 8.8, uniform to 1.1.3 2024-06-18 00:46:29 +01:00
William
ad510a8fca deps: bump uniform to 1.1.2 2024-06-17 23:07:17 +01:00
William
303b287705 deps: bump uniform to 1.1.1 2024-06-17 22:44:04 +01:00
William
549508b9c1 fix: shadow fabric, not implement! 2024-06-17 22:21:48 +01:00
William
6c8a577701 fix: suppress IncompatibleClassChangeError on paper
Paper plugins don't get run through bytecode fixups by Spigot's Commodore. Spigot changed InventoryView to an interface recently, which causes this to be thrown.
2024-06-17 22:14:42 +01:00
William
862177bec7 build: bump to 3.6.1 2024-06-17 21:43:33 +01:00
William
dbed4d83a2 deps: bump NBT-API to 2.13.1-SNAPSHOT
Fixes 1.21 support on Paper
2024-06-17 21:28:35 +01:00
William
aa2090d97a docs: remove brigadier tab completion 2024-06-17 21:19:32 +01:00
William
b168ede7c5 fix: locked maps in shulker boxes not saving, close #322 2024-06-17 21:18:05 +01:00
William
0e706d36c4 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
2024-06-17 21:07:09 +01:00
William
69d68de5c0 build: adjust Fabric build to append MC version 2024-06-15 18:20:30 +01:00
William
3d5395e5ae refactor: Remove debug print statements 2024-06-15 18:16:56 +01:00
William
332c71f041 fix/fabric: fix first item slot not syncing 2024-06-15 14:16:03 +01:00
William
b9fbcd72dd fix/fabric: slightly adjust item applying 2024-06-15 13:55:38 +01:00
William
68897e6265 fix/fabric: fix way game mode is changed 2024-06-15 13:51:29 +01:00
William
04606a7c9a docs: improve setup instructions
Improve Mongo instructions & add advice for Pterodactyl self-hosts
2024-06-15 13:46:04 +01:00
Stampede
6286bbe2ad fix: mongo breaking due to mixed use of UUIDs and strings (#321)
All UUIDs are now read and written as actual UUID objects, which was before causing errors due to a mixed use of UUID objects and string representations.
2024-06-15 13:41:52 +01:00
52 changed files with 789 additions and 1273 deletions

View File

@@ -5,7 +5,7 @@
<img src="https://img.shields.io/github/actions/workflow/status/WiIIiam278/HuskSync/ci.yml?branch=master&logo=github"/> <img src="https://img.shields.io/github/actions/workflow/status/WiIIiam278/HuskSync/ci.yml?branch=master&logo=github"/>
</a> </a>
<a href="https://repo.william278.net/#/releases/net/william278/husksync/"> <a href="https://repo.william278.net/#/releases/net/william278/husksync/">
<img src="https://repo.william278.net/api/badge/latest/releases/net/william278/husksync?color=00fb9a&name=Maven&prefix=v" /> <img src="https://repo.william278.net/api/badge/latest/releases/net/william278/husksync/husksync-common?color=00fb9a&name=Maven&prefix=v" />
</a> </a>
<a href="https://discord.gg/tVYhJfyDWG"> <a href="https://discord.gg/tVYhJfyDWG">
<img src="https://img.shields.io/discord/818135932103557162.svg?label=&logo=discord&logoColor=fff&color=7389D8&labelColor=6A7EC2" /> <img src="https://img.shields.io/discord/818135932103557162.svg?label=&logo=discord&logoColor=fff&color=7389D8&labelColor=6A7EC2" />

View File

@@ -3,6 +3,7 @@ import org.apache.tools.ant.filters.ReplaceTokens
plugins { plugins {
id 'com.github.johnrengelman.shadow' version '8.1.1' id 'com.github.johnrengelman.shadow' version '8.1.1'
id 'org.cadixdev.licenser' version '0.6.1' apply false id 'org.cadixdev.licenser' version '0.6.1' apply false
id 'fabric-loom' version '1.7-SNAPSHOT' apply false
id 'org.ajoberstar.grgit' version '5.2.2' id 'org.ajoberstar.grgit' version '5.2.2'
id 'maven-publish' id 'maven-publish'
id 'java' id 'java'
@@ -69,6 +70,7 @@ allprojects {
repositories { repositories {
mavenLocal() mavenLocal()
mavenCentral() mavenCentral()
maven { url 'https://repo.william278.net/releases/' }
maven { url 'https://oss.sonatype.org/content/repositories/snapshots' } maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' } maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' }
maven { url "https://repo.dmulloy2.net/repository/public/" } maven { url "https://repo.dmulloy2.net/repository/public/" }
@@ -78,7 +80,6 @@ allprojects {
maven { url 'https://jitpack.io' } maven { url 'https://jitpack.io' }
maven { url 'https://mvn-repo.arim.space/lesser-gpl3/' } maven { url 'https://mvn-repo.arim.space/lesser-gpl3/' }
maven { url 'https://libraries.minecraft.net/' } maven { url 'https://libraries.minecraft.net/' }
maven { url 'https://repo.william278.net/releases/' }
} }
dependencies { dependencies {
@@ -106,6 +107,10 @@ allprojects {
} }
subprojects { subprojects {
if (['fabric'].contains(project.name)) {
apply plugin: 'fabric-loom'
}
version rootProject.version version rootProject.version
archivesBaseName = "${rootProject.name}-${project.name.capitalize()}" archivesBaseName = "${rootProject.name}-${project.name.capitalize()}"
@@ -169,8 +174,8 @@ subprojects {
mavenJavaFabric(MavenPublication) { mavenJavaFabric(MavenPublication) {
groupId = 'net.william278.husksync' groupId = 'net.william278.husksync'
artifactId = 'husksync-fabric' artifactId = 'husksync-fabric'
version = "$rootProject.version" version = "$rootProject.version+${fabric_minecraft_version}"
artifact shadowJar artifact remapJar
artifact sourcesJar artifact sourcesJar
artifact javadocJar artifact javadocJar
} }

View File

@@ -1,16 +1,16 @@
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.8'
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'
implementation 'de.tr7zw:item-nbt-api:2.13.0' implementation 'de.tr7zw:item-nbt-api:2.13.1-SNAPSHOT'
compileOnly 'org.spigotmc:spigot-api:1.17.1-R0.1-SNAPSHOT' compileOnly 'org.spigotmc:spigot-api:1.17.1-R0.1-SNAPSHOT'
compileOnly 'com.github.retrooper.packetevents:spigot:2.3.0' compileOnly 'com.github.retrooper.packetevents:spigot:2.3.0'
@@ -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'

View File

@@ -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() {

View File

@@ -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
);
}
}
}

View File

@@ -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());
}
}
}

View File

@@ -161,11 +161,15 @@ public abstract class BukkitData implements Data {
} }
private void clearInventoryCraftingSlots(@NotNull Player player) { private void clearInventoryCraftingSlots(@NotNull Player player) {
final org.bukkit.inventory.Inventory inventory = player.getOpenInventory().getTopInventory(); try {
if (inventory.getType() == InventoryType.CRAFTING) { final org.bukkit.inventory.Inventory inventory = player.getOpenInventory().getTopInventory();
for (int slot = 0; slot < 5; slot++) { if (inventory.getType() == InventoryType.CRAFTING) {
inventory.setItem(slot, null); for (int slot = 0; slot < 5; slot++) {
inventory.setItem(slot, null);
}
} }
} catch (Throwable e) {
// Ignore any exceptions
} }
} }

View File

@@ -31,8 +31,10 @@ import net.william278.mapdataapi.MapData;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.block.ShulkerBox;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BlockStateMeta;
import org.bukkit.inventory.meta.MapMeta; import org.bukkit.inventory.meta.MapMeta;
import org.bukkit.map.*; import org.bukkit.map.*;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
@@ -94,6 +96,9 @@ public interface BukkitMapPersister {
} }
if (item.getType() == Material.FILLED_MAP && item.hasItemMeta()) { if (item.getType() == Material.FILLED_MAP && item.hasItemMeta()) {
items[i] = function.apply(item); items[i] = function.apply(item);
} else if (item.getItemMeta() instanceof BlockStateMeta b && b.getBlockState() instanceof ShulkerBox box) {
forEachMap(box.getInventory().getContents(), function);
b.setBlockState(box);
} }
} }
return items; return items;
@@ -150,8 +155,8 @@ public interface BukkitMapPersister {
Optional<String> world = Optional.empty(); Optional<String> world = Optional.empty();
for (String worldUid : mapIds.getKeys()) { for (String worldUid : mapIds.getKeys()) {
world = getPlugin().getServer().getWorlds().stream() world = getPlugin().getServer().getWorlds().stream()
.map(w -> w.getUID().toString()).filter(u -> u.equals(worldUid)) .map(w -> w.getUID().toString()).filter(u -> u.equals(worldUid))
.findFirst(); .findFirst();
if (world.isPresent()) { if (world.isPresent()) {
break; break;
} }
@@ -174,7 +179,7 @@ public interface BukkitMapPersister {
try { try {
getPlugin().debug("Deserializing map data from NBT and generating view..."); getPlugin().debug("Deserializing map data from NBT and generating view...");
canvasData = MapData.fromByteArray(Objects.requireNonNull(mapData.getByteArray(MAP_PIXEL_DATA_KEY), canvasData = MapData.fromByteArray(Objects.requireNonNull(mapData.getByteArray(MAP_PIXEL_DATA_KEY),
"Map pixel data is null")); "Map pixel data is null"));
} catch (Throwable e) { } catch (Throwable e) {
getPlugin().log(Level.WARNING, "Failed to deserialize map data from NBT", e); getPlugin().log(Level.WARNING, "Failed to deserialize map data from NBT", e);
return; return;
@@ -190,8 +195,8 @@ public interface BukkitMapPersister {
// Set the map view ID in NBT // Set the map view ID in NBT
NBT.modify(map, editable -> { NBT.modify(map, editable -> {
Objects.requireNonNull(editable.getCompound(MAP_VIEW_ID_MAPPINGS_KEY), Objects.requireNonNull(editable.getCompound(MAP_VIEW_ID_MAPPINGS_KEY),
"Map view ID mappings compound is null") "Map view ID mappings compound is null")
.setInteger(worldUid, view.getId()); .setInteger(worldUid, view.getId());
}); });
getPlugin().debug(String.format("Generated view (#%s) and updated map (UID: %s)", view.getId(), worldUid)); getPlugin().debug(String.format("Generated view (#%s) and updated map (UID: %s)", view.getId(), worldUid));
}); });
@@ -321,29 +326,29 @@ public interface BukkitMapPersister {
@NotNull @NotNull
private static MapCursor createBannerCursor(@NotNull MapBanner banner) { private static MapCursor createBannerCursor(@NotNull MapBanner banner) {
return new MapCursor( return new MapCursor(
(byte) banner.getPosition().getX(), (byte) banner.getPosition().getX(),
(byte) banner.getPosition().getZ(), (byte) banner.getPosition().getZ(),
(byte) 8, // Always rotate banners upright (byte) 8, // Always rotate banners upright
switch (banner.getColor().toLowerCase(Locale.ENGLISH)) { switch (banner.getColor().toLowerCase(Locale.ENGLISH)) {
case "white" -> MapCursor.Type.BANNER_WHITE; case "white" -> MapCursor.Type.BANNER_WHITE;
case "orange" -> MapCursor.Type.BANNER_ORANGE; case "orange" -> MapCursor.Type.BANNER_ORANGE;
case "magenta" -> MapCursor.Type.BANNER_MAGENTA; case "magenta" -> MapCursor.Type.BANNER_MAGENTA;
case "light_blue" -> MapCursor.Type.BANNER_LIGHT_BLUE; case "light_blue" -> MapCursor.Type.BANNER_LIGHT_BLUE;
case "yellow" -> MapCursor.Type.BANNER_YELLOW; case "yellow" -> MapCursor.Type.BANNER_YELLOW;
case "lime" -> MapCursor.Type.BANNER_LIME; case "lime" -> MapCursor.Type.BANNER_LIME;
case "pink" -> MapCursor.Type.BANNER_PINK; case "pink" -> MapCursor.Type.BANNER_PINK;
case "gray" -> MapCursor.Type.BANNER_GRAY; case "gray" -> MapCursor.Type.BANNER_GRAY;
case "light_gray" -> MapCursor.Type.BANNER_LIGHT_GRAY; case "light_gray" -> MapCursor.Type.BANNER_LIGHT_GRAY;
case "cyan" -> MapCursor.Type.BANNER_CYAN; case "cyan" -> MapCursor.Type.BANNER_CYAN;
case "purple" -> MapCursor.Type.BANNER_PURPLE; case "purple" -> MapCursor.Type.BANNER_PURPLE;
case "blue" -> MapCursor.Type.BANNER_BLUE; case "blue" -> MapCursor.Type.BANNER_BLUE;
case "brown" -> MapCursor.Type.BANNER_BROWN; case "brown" -> MapCursor.Type.BANNER_BROWN;
case "green" -> MapCursor.Type.BANNER_GREEN; case "green" -> MapCursor.Type.BANNER_GREEN;
case "red" -> MapCursor.Type.BANNER_RED; case "red" -> MapCursor.Type.BANNER_RED;
default -> MapCursor.Type.BANNER_BLACK; default -> MapCursor.Type.BANNER_BLACK;
}, },
true, true,
banner.getText().isEmpty() ? null : banner.getText() banner.getText().isEmpty() ? null : banner.getText()
); );
} }
@@ -425,11 +430,11 @@ public interface BukkitMapPersister {
final String type = cursor.getType().name().toLowerCase(Locale.ENGLISH); final String type = cursor.getType().name().toLowerCase(Locale.ENGLISH);
if (type.startsWith(BANNER_PREFIX)) { if (type.startsWith(BANNER_PREFIX)) {
banners.add(new MapBanner( banners.add(new MapBanner(
type.replaceAll(BANNER_PREFIX, ""), type.replaceAll(BANNER_PREFIX, ""),
cursor.getCaption() == null ? "" : cursor.getCaption(), cursor.getCaption() == null ? "" : cursor.getCaption(),
cursor.getX(), cursor.getX(),
mapView.getWorld() != null ? mapView.getWorld().getSeaLevel() : 128, mapView.getWorld() != null ? mapView.getWorld().getSeaLevel() : 128,
cursor.getY() cursor.getY()
)); ));
} }
} }

View File

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

View File

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

View File

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

View File

@@ -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;
}
}
}
}

View File

@@ -16,6 +16,8 @@ dependencies {
exclude module: 'slf4j-api' exclude module: 'slf4j-api'
} }
compileOnly 'net.william278.uniform:uniform-common:1.1.8'
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'

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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)
); );
}); });

View File

@@ -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);
}

View File

@@ -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());
switch (subCommand) {
case "about" -> executor.sendMessage(aboutMenu.toComponent());
case "status" -> {
getPlugin().getLocales().getLocale("system_status_header").ifPresent(executor::sendMessage);
executor.sendMessage(Component.join(
JoinConfiguration.newlines(),
Arrays.stream(StatusLine.values()).map(s -> s.get(plugin)).toList()
));
}
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 private void about(@NotNull BaseCommand<?> c, @NotNull CommandContext<?> ctx) {
private void handleMigrationCommand(@NotNull String[] args) { user(c, ctx).getAudience().sendMessage(aboutMenu.toComponent());
if (args.length < 2) { }
plugin.log(Level.INFO,
"Please choose a migrator, then run \"husksync migrate <migrator>\"");
this.logMigratorList();
return;
}
final Optional<Migrator> selectedMigrator = plugin.getAvailableMigrators().stream() @NotNull
.filter(available -> available.getIdentifier().equalsIgnoreCase(args[1])) private CommandProvider status() {
.findFirst(); return (sub) -> sub.setDefaultExecutor((ctx) -> {
selectedMigrator.ifPresentOrElse(migrator -> { final CommandUser user = user(sub, ctx);
if (args.length < 3) { plugin.getLocales().getLocale("system_status_header").ifPresent(user::sendMessage);
plugin.log(Level.INFO, migrator.getHelpMenu()); user.sendMessage(Component.join(
JoinConfiguration.newlines(),
Arrays.stream(StatusLine.values()).map(s -> s.get(plugin)).toList()
));
});
}
@NotNull
private CommandProvider reload() {
return (sub) -> sub.setDefaultExecutor((ctx) -> {
final CommandUser user = user(sub, ctx);
try {
plugin.loadSettings();
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);
}
});
}
@NotNull
private CommandProvider update() {
return (sub) -> sub.setDefaultExecutor((ctx) -> updateChecker.check().thenAccept(checked -> {
final CommandUser user = user(sub, ctx);
if (checked.isUpToDate()) {
plugin.getLocales().getLocale("up_to_date", plugin.getPluginVersion().toString())
.ifPresent(user::sendMessage);
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
@Override
public List<String> suggest(@NotNull CommandUser user, @NotNull String[] args) {
return switch (args.length) {
case 0, 1 -> SUB_COMMANDS.keySet().stream().sorted().toList();
default -> null;
}; };
} }
@NotNull
private <S> ArgumentElement<S, Migrator> migrator() {
return new ArgumentElement<>("migrator", reader -> {
final String id = reader.readString();
final Migrator migrator = plugin.getAvailableMigrators().stream()
.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"));
} }
} }

View File

@@ -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)
); );
}); });

View File

@@ -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;
};
}
} }

View File

@@ -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();
}
});
}
}

View File

@@ -0,0 +1,129 @@
/*
* 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))
.filter(command -> !plugin.getSettings().isCommandDisabled(command))
.toArray(PluginCommand[]::new);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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,83 +29,32 @@ 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
@@ -224,7 +174,8 @@ public class UserDataCommand extends Command implements TabProvider {
} }
// Dump a snapshot // Dump a snapshot
private void dumpSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version, boolean webDump) { private void dumpSnapshot(@NotNull CommandUser executor, @NotNull User user, @NotNull UUID version,
@NotNull DumpType type) {
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")
@@ -237,22 +188,99 @@ 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); (type == DumpType.WEB ? 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);
}, 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
}
} }

View File

@@ -25,6 +25,7 @@ import de.exlll.configlib.Configuration;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import net.william278.husksync.command.PluginCommand;
import net.william278.husksync.data.DataSnapshot; import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.data.Identifier; import net.william278.husksync.data.Identifier;
import net.william278.husksync.database.Database; import net.william278.husksync.database.Database;
@@ -69,15 +70,15 @@ 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;
@Comment("Whether to cancel game event packets directly when handling locked players if ProtocolLib or PacketEvents is installed") @Comment("Whether to cancel game event packets directly when handling locked players if ProtocolLib or PacketEvents is installed")
private boolean cancelPackets = true; private boolean cancelPackets = true;
@Comment("Add HuskSync commands to this list to prevent them from being registered (e.g. ['userdata'])")
@Getter(AccessLevel.NONE)
private List<String> disabledCommands = Lists.newArrayList();
// Database settings // Database settings
@Comment("Database settings") @Comment("Database settings")
@@ -185,7 +186,7 @@ public class Settings {
} }
// Synchronization settings // Synchronization settings
@Comment("Redis settings") @Comment("Data syncing settings")
private SynchronizationSettings synchronization = new SynchronizationSettings(); private SynchronizationSettings synchronization = new SynchronizationSettings();
@Getter @Getter
@@ -297,4 +298,10 @@ public class Settings {
} }
} }
public boolean isCommandDisabled(@NotNull PluginCommand command) {
return disabledCommands.stream().map(c -> c.startsWith("/") ? c.substring(1) : c)
.anyMatch(c -> c.equalsIgnoreCase(command.getName()) || command.getAliases().contains(c));
}
} }

View File

@@ -45,7 +45,7 @@ public class Identifier {
public static final Identifier ADVANCEMENTS = huskSync("advancements", true); public static final Identifier ADVANCEMENTS = huskSync("advancements", true);
public static final Identifier STATISTICS = huskSync("statistics", true); public static final Identifier STATISTICS = huskSync("statistics", true);
public static final Identifier POTION_EFFECTS = huskSync("potion_effects", true); public static final Identifier POTION_EFFECTS = huskSync("potion_effects", true);
public static final Identifier GAME_MODE = huskSync("game_mode", false); public static final Identifier GAME_MODE = huskSync("game_mode", true);
public static final Identifier FLIGHT_STATUS = huskSync("flight_status", true, public static final Identifier FLIGHT_STATUS = huskSync("flight_status", true,
Dependency.optional("game_mode") Dependency.optional("game_mode")
); );

View File

@@ -107,7 +107,7 @@ public class MongoDbDatabase extends Database {
if (!existingUser.getUsername().equals(user.getUsername())) { if (!existingUser.getUsername().equals(user.getUsername())) {
// Update a user's name if it has changed in the database // Update a user's name if it has changed in the database
try { try {
Document filter = new Document("uuid", existingUser.getUuid().toString()); Document filter = new Document("uuid", existingUser.getUuid());
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first(); Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
if (doc == null) { if (doc == null) {
throw new MongoException("User document returned null!"); throw new MongoException("User document returned null!");
@@ -123,7 +123,7 @@ public class MongoDbDatabase extends Database {
() -> { () -> {
// Insert new player data into the database // Insert new player data into the database
try { try {
Document doc = new Document("uuid", user.getUuid().toString()).append("username", user.getUsername()); Document doc = new Document("uuid", user.getUuid()).append("username", user.getUsername());
mongoCollectionHelper.insertDocument(usersTable, doc); mongoCollectionHelper.insertDocument(usersTable, doc);
} catch (MongoException e) { } catch (MongoException e) {
plugin.log(Level.SEVERE, "Failed to insert a user into the database", e); plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
@@ -148,8 +148,7 @@ public class MongoDbDatabase extends Database {
Document filter = new Document("uuid", uuid); Document filter = new Document("uuid", uuid);
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first(); Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
if (doc != null) { if (doc != null) {
return Optional.of(new User(UUID.fromString(doc.getString("uuid")), return Optional.of(new User(uuid, doc.getString("username")));
doc.getString("username")));
} }
return Optional.empty(); return Optional.empty();
} catch (MongoException e) { } catch (MongoException e) {
@@ -171,7 +170,7 @@ public class MongoDbDatabase extends Database {
Document filter = new Document("username", username); Document filter = new Document("username", username);
Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first(); Document doc = mongoCollectionHelper.getCollection(usersTable).find(filter).first();
if (doc != null) { if (doc != null) {
return Optional.of(new User(UUID.fromString(doc.getString("uuid")), return Optional.of(new User(doc.get("uuid", UUID.class),
doc.getString("username"))); doc.getString("username")));
} }
return Optional.empty(); return Optional.empty();
@@ -191,12 +190,12 @@ public class MongoDbDatabase extends Database {
@Override @Override
public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) { public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) {
try { try {
Document filter = new Document("player_uuid", user.getUuid().toString()); Document filter = new Document("player_uuid", user.getUuid());
Document sort = new Document("timestamp", -1); // -1 = Descending Document sort = new Document("timestamp", -1); // -1 = Descending
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort); FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort);
Document doc = iterable.first(); Document doc = iterable.first();
if (doc != null) { if (doc != null) {
final UUID versionUuid = UUID.fromString(doc.getString("version_uuid")); final UUID versionUuid = doc.get("version_uuid", UUID.class);
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(Instant.ofEpochMilli((long) doc.get("timestamp")), TimeZone.getDefault().toZoneId()); final OffsetDateTime timestamp = OffsetDateTime.ofInstant(Instant.ofEpochMilli((long) doc.get("timestamp")), TimeZone.getDefault().toZoneId());
final Binary bin = doc.get("data", Binary.class); final Binary bin = doc.get("data", Binary.class);
final byte[] dataByteArray = bin.getData(); final byte[] dataByteArray = bin.getData();
@@ -221,11 +220,11 @@ public class MongoDbDatabase extends Database {
public List<DataSnapshot.Packed> getAllSnapshots(@NotNull User user) { public List<DataSnapshot.Packed> getAllSnapshots(@NotNull User user) {
try { try {
final List<DataSnapshot.Packed> retrievedData = Lists.newArrayList(); final List<DataSnapshot.Packed> retrievedData = Lists.newArrayList();
Document filter = new Document("player_uuid", user.getUuid().toString()); Document filter = new Document("player_uuid", user.getUuid());
Document sort = new Document("timestamp", -1); // -1 = Descending Document sort = new Document("timestamp", -1); // -1 = Descending
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort); FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort);
for (Document doc : iterable) { for (Document doc : iterable) {
final UUID versionUuid = UUID.fromString(doc.getString("version_uuid")); final UUID versionUuid = doc.get("version_uuid", UUID.class);
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(Instant.ofEpochMilli((long) doc.get("timestamp")), TimeZone.getDefault().toZoneId()); final OffsetDateTime timestamp = OffsetDateTime.ofInstant(Instant.ofEpochMilli((long) doc.get("timestamp")), TimeZone.getDefault().toZoneId());
final Binary bin = doc.get("data", Binary.class); final Binary bin = doc.get("data", Binary.class);
final byte[] dataByteArray = bin.getData(); final byte[] dataByteArray = bin.getData();
@@ -249,7 +248,7 @@ public class MongoDbDatabase extends Database {
@Override @Override
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) { public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
try { try {
Document filter = new Document("player_uuid", user.getUuid().toString()).append("version_uuid", versionUuid.toString()); Document filter = new Document("player_uuid", user.getUuid()).append("version_uuid", versionUuid);
Document sort = new Document("timestamp", -1); // -1 = Descending Document sort = new Document("timestamp", -1); // -1 = Descending
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort); FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable).find(filter).sort(sort);
Document doc = iterable.first(); Document doc = iterable.first();
@@ -281,7 +280,7 @@ public class MongoDbDatabase extends Database {
final int maxSnapshots = plugin.getSettings().getSynchronization().getMaxUserDataSnapshots(); final int maxSnapshots = plugin.getSettings().getSynchronization().getMaxUserDataSnapshots();
if (unpinnedUserData.size() > maxSnapshots) { if (unpinnedUserData.size() > maxSnapshots) {
Document filter = new Document("player_uuid", user.getUuid().toString()).append("pinned", false); Document filter = new Document("player_uuid", user.getUuid()).append("pinned", false);
Document sort = new Document("timestamp", 1); // 1 = Ascending Document sort = new Document("timestamp", 1); // 1 = Ascending
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable) FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable)
.find(filter) .find(filter)
@@ -307,7 +306,7 @@ public class MongoDbDatabase extends Database {
@Override @Override
public boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid) { public boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
try { try {
Document filter = new Document("player_uuid", user.getUuid().toString()).append("version_uuid", versionUuid.toString()); Document filter = new Document("player_uuid", user.getUuid()).append("version_uuid", versionUuid);
Document doc = mongoCollectionHelper.getCollection(userDataTable).find(filter).first(); Document doc = mongoCollectionHelper.getCollection(userDataTable).find(filter).first();
if (doc == null) { if (doc == null) {
return false; return false;
@@ -332,7 +331,7 @@ public class MongoDbDatabase extends Database {
@Override @Override
protected void rotateLatestSnapshot(@NotNull User user, @NotNull OffsetDateTime within) { protected void rotateLatestSnapshot(@NotNull User user, @NotNull OffsetDateTime within) {
try { try {
Document filter = new Document("player_uuid", user.getUuid().toString()).append("pinned", false); Document filter = new Document("player_uuid", user.getUuid()).append("pinned", false);
Document sort = new Document("timestamp", 1); // 1 = Ascending Document sort = new Document("timestamp", 1); // 1 = Ascending
FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable) FindIterable<Document> iterable = mongoCollectionHelper.getCollection(userDataTable)
.find(filter) .find(filter)
@@ -362,8 +361,8 @@ public class MongoDbDatabase extends Database {
@Override @Override
protected void createSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) { protected void createSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
try { try {
Document doc = new Document("player_uuid", user.getUuid().toString()) Document doc = new Document("player_uuid", user.getUuid())
.append("version_uuid", data.getId().toString()) .append("version_uuid", data.getId())
.append("timestamp", data.getTimestamp().toInstant().toEpochMilli()) .append("timestamp", data.getTimestamp().toInstant().toEpochMilli())
.append("save_cause", data.getSaveCause().name()) .append("save_cause", data.getSaveCause().name())
.append("pinned", data.isPinned()) .append("pinned", data.isPinned())
@@ -384,7 +383,7 @@ public class MongoDbDatabase extends Database {
@Override @Override
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) { public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
try { try {
Document doc = new Document("player_uuid", user.getUuid().toString()).append("version_uuid", data.getId().toString()); Document doc = new Document("player_uuid", user.getUuid()).append("version_uuid", data.getId());
Bson updates = Updates.combine( Bson updates = Updates.combine(
Updates.set("save_cause", data.getSaveCause().name()), Updates.set("save_cause", data.getSaveCause().name()),
Updates.set("pinned", data.isPinned()), Updates.set("pinned", data.isPinned()),

View File

@@ -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();

View File

@@ -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;
} }
} }

View File

@@ -53,7 +53,7 @@ Add the repository to your `pom.xml` as per below. You can alternatively specify
</repository> </repository>
</repositories> </repositories>
``` ```
Add the dependency to your `pom.xml` as per below. Replace `VERSION` with the latest version of HuskSync (without the v): ![Latest version](https://img.shields.io/github/v/tag/WiIIiam278/HuskSync?color=%23282828&label=%20&style=flat-square) Add the dependency to your `pom.xml` as per below. Replace `VERSION` with the latest version of HuskSync (without the v): ![Latest version](https://img.shields.io/github/v/tag/WiIIiam278/HuskSync?color=%23282828&label=%20&style=flat-square). Note for Fabric you must append the target Minecraft version to the version number (e.g. `3.6.1+1.20.1`).
```xml ```xml
<dependency> <dependency>
<groupId>net.william278.husksync</groupId> <groupId>net.william278.husksync</groupId>
@@ -76,7 +76,7 @@ allprojects {
} }
} }
``` ```
Add the dependency as per below. Replace `VERSION` with the latest version of HuskSync (without the v): ![Latest version](https://img.shields.io/github/v/tag/WiIIiam278/HuskSync?color=%23282828&label=%20&style=flat-square) Add the dependency as per below. Replace `VERSION` with the latest version of HuskSync (without the v): ![Latest version](https://img.shields.io/github/v/tag/WiIIiam278/HuskSync?color=%23282828&label=%20&style=flat-square). Note for Fabric you must append the target Minecraft version to the version number (e.g. `3.6.1+1.20.1`).
```groovy ```groovy
dependencies { dependencies {

View File

@@ -28,13 +28,13 @@ check_for_updates: true
cluster_id: '' cluster_id: ''
# Enable development debug logging # Enable development debug logging
debug_logging: true debug_logging: true
# Whether to provide modern, rich TAB suggestions for commands (if available)
brigadier_tab_completion: false
# Whether to enable the Player Analytics hook. # Whether to enable the Player Analytics hook.
# Docs: https://william278.net/docs/husksync/plan-hook # Docs: https://william278.net/docs/husksync/plan-hook
enable_plan_hook: true enable_plan_hook: true
# Whether to cancel game event packets directly when handling locked players if ProtocolLib or PacketEvents is installed # Whether to cancel game event packets directly when handling locked players if ProtocolLib or PacketEvents is installed
cancel_packets: true cancel_packets: true
# Add HuskSync commands to this list to prevent them from being registered (e.g. ['userdata'])
disabled_commands: []
# Database settings # Database settings
database: database:
# Type of database to use (MYSQL, MARIADB, POSTGRES, MONGO) # Type of database to use (MYSQL, MARIADB, POSTGRES, MONGO)
@@ -78,7 +78,7 @@ redis:
# List of host:port pairs # List of host:port pairs
nodes: [] nodes: []
password: '' password: ''
# Redis settings # Data syncing settings
synchronization: synchronization:
# The data synchronization mode to use (LOCKSTEP or DELAY). LOCKSTEP is recommended for most networks. # The data synchronization mode to use (LOCKSTEP or DELAY). LOCKSTEP is recommended for most networks.
# Docs: https://william278.net/docs/husksync/sync-modes # Docs: https://william278.net/docs/husksync/sync-modes

View File

@@ -31,22 +31,29 @@ This will walk you through installing HuskSync on your network of Spigot or Fabr
- Unless you want to have multiple clusters of servers within your network, each with separate user data, you should not change the value of `cluster_id`. - Unless you want to have multiple clusters of servers within your network, each with separate user data, you should not change the value of `cluster_id`.
<details> <details>
<summary>Important &mdash; MongoDB Users</summary> <summary>MongoDB users &mdash; additional instructions</summary>
- Navigate to the HuskSync config file on each server (`~/plugins/HuskSync/config.yml`) - Navigate to the HuskSync config file on each server (`~/plugins/HuskSync/config.yml`)
- Set `type` in the `database` section to `MONGO` - Set `type` in the `database` section to `MONGO`
- Under `credentials` in the `database` section, enter the credentials of your MongoDB Database. You shouldn't touch the `connection_pool` properties. - Under `credentials` in the `database` section, enter the credentials of your MongoDB Database. You shouldn't touch the `connection_pool` properties.
<details> - Under `parameters` in the `mongo_settings` section, ensure the specified `&authSource=` matches the database you are using (default is `HuskSync`).
<summary>Additional configuration for MongoDB Atlas users</summary> #### Additional setup for MongoDB Atlas
- Navigate to the HuskSync config file on each server (`~/plugins/HuskSync/config.yml`)
- Set `using_atlas` in the `mongo_settings` section to `true`. - Set `using_atlas` in the `mongo_settings` section to `true`.
- Remove `&authSource=HuskSync` from `parameters` in the `mongo_settings`. - Remove `&authSource=HuskSync` from `parameters` in the `mongo_settings`.
(The `port` setting in `credentials` is disregarded when using Atlas.) (The `port` setting in `credentials` is disregarded when using Atlas.)
</details> </details>
<details>
<summary>Pterodactyl self-hosts &mdash; Redis setup instructions</summary>
If you are hosting your Redis server on the same node as your servers, you need to use `172.18.0.1` as your host (or equivalent if you changed your network settings), and bind it in the Redis config `nano /etc/redis/redis.conf`.
You will also need to uncomment the `requirepass` directive and set a password to allow outside connections, or disable `protected-mode`. Once a password is set and Redis is restarted `systemctl restart redis`, you will also need to update the password in your pterodactyl `.env` (`nano /var/www/pterodactyl/.env`) and refresh the cache `cd /var/www/pterodactyl && php artisan config:clear`.
You may also need to allow connections from your firewall depending on your distribution.
</details> </details>
### 4. Set server names in server.yml files ### 4. Set server names in server.yml files

View File

@@ -38,7 +38,7 @@ Although it's a common request, HuskSync doesn't synchronize economy data for a
I strongly recommend making use of economy plugins that have cross-server economy balance synchronization built-in, of which there are a multitude of options available. Please see our [[FAQs]] section for more details on this decision. I strongly recommend making use of economy plugins that have cross-server economy balance synchronization built-in, of which there are a multitude of options available. Please see our [[FAQs]] section for more details on this decision.
## Toggling Sync Features ## Toggling Sync Features
All synchronization features, except location and locked map synchronising, are enabled by default. To toggle a feature, navigate to the `features:` section in the `synchronization:` part of your `config.yml` file, and change the option to `true`/`false` respectively. All synchronization features, except location and locked map synchronizing, are enabled by default. To toggle a feature, navigate to the `features:` section in the `synchronization:` part of your `config.yml` file, and change the option to `true`/`false` respectively.
<details> <details>
<summary>Example in config.yml</summary> <summary>Example in config.yml</summary>

View File

@@ -1,5 +1,5 @@
plugins { plugins {
id 'fabric-loom' version '1.6-SNAPSHOT' id 'fabric-loom' version '1.7-SNAPSHOT'
} }
apply plugin: 'fabric-loom' apply plugin: 'fabric-loom'
@@ -18,9 +18,9 @@ dependencies {
modImplementation include("net.kyori:adventure-platform-fabric:${adventure_platform_fabric_version}") modImplementation include("net.kyori:adventure-platform-fabric:${adventure_platform_fabric_version}")
modImplementation include("me.lucko:fabric-permissions-api:${fabric_permissions_api_version}") modImplementation include("me.lucko:fabric-permissions-api:${fabric_permissions_api_version}")
modImplementation include("eu.pb4:sgui:${sgui_version}") modImplementation include("eu.pb4:sgui:${sgui_version}")
modImplementation include('net.william278.uniform:uniform-fabric:1.1.8+1.20.1')
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('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")

View File

@@ -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 -> {
@@ -157,7 +157,7 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
registerSerializer(Identifier.INVENTORY, new FabricSerializer.Inventory(this)); registerSerializer(Identifier.INVENTORY, new FabricSerializer.Inventory(this));
registerSerializer(Identifier.ENDER_CHEST, new FabricSerializer.EnderChest(this)); registerSerializer(Identifier.ENDER_CHEST, new FabricSerializer.EnderChest(this));
registerSerializer(Identifier.ADVANCEMENTS, new FabricSerializer.Advancements(this)); registerSerializer(Identifier.ADVANCEMENTS, new FabricSerializer.Advancements(this));
registerSerializer(Identifier.STATISTICS, new Serializer.Json<>(this, FabricData.Statistics.class)); // TODO APPLY registerSerializer(Identifier.STATISTICS, new Serializer.Json<>(this, FabricData.Statistics.class));
registerSerializer(Identifier.POTION_EFFECTS, new FabricSerializer.PotionEffects(this)); registerSerializer(Identifier.POTION_EFFECTS, new FabricSerializer.PotionEffects(this));
registerSerializer(Identifier.GAME_MODE, new Serializer.Json<>(this, FabricData.GameMode.class)); registerSerializer(Identifier.GAME_MODE, new Serializer.Json<>(this, FabricData.GameMode.class));
registerSerializer(Identifier.FLIGHT_STATUS, new Serializer.Json<>(this, FabricData.FlightStatus.class)); registerSerializer(Identifier.FLIGHT_STATUS, new Serializer.Json<>(this, FabricData.FlightStatus.class));
@@ -202,9 +202,7 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync,
}); });
// Register API // Register API
initialize("api", (plugin) -> { initialize("api", (plugin) -> FabricHuskSyncAPI.register(this));
FabricHuskSyncAPI.register(this);
});
// Check for updates // Check for updates
this.checkForUpdates(); this.checkForUpdates();
@@ -232,12 +230,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 +245,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(mod.getMetadata().getId());
} }
@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 +292,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]);

View File

@@ -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();
}
}
}

View File

@@ -153,29 +153,23 @@ public abstract class FabricData implements Data {
@Override @Override
public int getSlotCount() { public int getSlotCount() {
return INVENTORY_SLOT_COUNT; return getContents().length;
} }
@Override @Override
public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException { public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException {
final ServerPlayerEntity player = user.getPlayer(); final ServerPlayerEntity player = user.getPlayer();
this.clearInventoryCraftingSlots(player); player.playerScreenHandler.clearCraftingSlots();
player.currentScreenHandler.setCursorStack(ItemStack.EMPTY); player.currentScreenHandler.setCursorStack(ItemStack.EMPTY);
final ItemStack[] items = getContents(); final ItemStack[] items = getContents();
for (int slot = 0; slot < player.getInventory().size(); slot++) { for (int slot = 0; slot < player.getInventory().size(); slot++) {
player.getInventory().setStack( player.getInventory().setStack(slot, items[slot] == null ? ItemStack.EMPTY : items[slot]);
slot, items[slot] == null ? ItemStack.EMPTY : items[slot]
);
} }
player.getInventory().selectedSlot = heldItemSlot; player.getInventory().selectedSlot = heldItemSlot;
player.playerScreenHandler.sendContentUpdates(); player.playerScreenHandler.sendContentUpdates();
player.getInventory().updateItems(); player.getInventory().updateItems();
} }
private void clearInventoryCraftingSlots(@NotNull ServerPlayerEntity player) {
player.playerScreenHandler.clearCraftingSlots();
}
} }
public static class EnderChest extends FabricData.Items implements Data.Items.EnderChest { public static class EnderChest extends FabricData.Items implements Data.Items.EnderChest {
@@ -321,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);
@@ -485,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;
} }
@@ -494,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);
} }
}); });
}); });
@@ -510,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);
} }
}); });
@@ -593,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
)); ));
@@ -602,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());
} }
@@ -769,7 +763,7 @@ public abstract class FabricData implements Data {
@Override @Override
public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException { public void apply(@NotNull FabricUser user, @NotNull FabricHuskSync plugin) throws IllegalStateException {
user.getPlayer().interactionManager.changeGameMode(net.minecraft.world.GameMode.byName(gameMode)); user.getPlayer().changeGameMode(net.minecraft.world.GameMode.byName(gameMode));
} }
} }

View File

@@ -58,7 +58,7 @@ public abstract class FabricSerializer {
} }
public static class Inventory extends FabricSerializer implements Serializer<FabricData.Items.Inventory>, public static class Inventory extends FabricSerializer implements Serializer<FabricData.Items.Inventory>,
ItemDeserializer { ItemDeserializer {
public Inventory(@NotNull HuskSync plugin) { public Inventory(@NotNull HuskSync plugin) {
super(plugin); super(plugin);
@@ -66,7 +66,7 @@ public abstract class FabricSerializer {
@Override @Override
public FabricData.Items.Inventory deserialize(@NotNull String serialized, @NotNull Version dataMcVersion) public FabricData.Items.Inventory deserialize(@NotNull String serialized, @NotNull Version dataMcVersion)
throws DeserializationException { throws DeserializationException {
// Read item NBT from string // Read item NBT from string
final FabricHuskSync plugin = (FabricHuskSync) getPlugin(); final FabricHuskSync plugin = (FabricHuskSync) getPlugin();
final NbtCompound root; final NbtCompound root;
@@ -79,8 +79,8 @@ public abstract class FabricSerializer {
// Deserialize the inventory data // Deserialize the inventory data
final NbtCompound items = root.contains(ITEMS_TAG) ? root.getCompound(ITEMS_TAG) : null; final NbtCompound items = root.contains(ITEMS_TAG) ? root.getCompound(ITEMS_TAG) : null;
return FabricData.Items.Inventory.from( return FabricData.Items.Inventory.from(
items != null ? getItems(items, dataMcVersion, plugin) : new ItemStack[INVENTORY_SLOT_COUNT], items != null ? getItems(items, dataMcVersion, plugin) : new ItemStack[INVENTORY_SLOT_COUNT],
root.contains(HELD_ITEM_SLOT_TAG) ? root.getInt(HELD_ITEM_SLOT_TAG) : 0 root.contains(HELD_ITEM_SLOT_TAG) ? root.getInt(HELD_ITEM_SLOT_TAG) : 0
); );
} }
@@ -105,7 +105,7 @@ public abstract class FabricSerializer {
} }
public static class EnderChest extends FabricSerializer implements Serializer<FabricData.Items.EnderChest>, public static class EnderChest extends FabricSerializer implements Serializer<FabricData.Items.EnderChest>,
ItemDeserializer { ItemDeserializer {
public EnderChest(@NotNull HuskSync plugin) { public EnderChest(@NotNull HuskSync plugin) {
super(plugin); super(plugin);
@@ -113,7 +113,7 @@ public abstract class FabricSerializer {
@Override @Override
public FabricData.Items.EnderChest deserialize(@NotNull String serialized, @NotNull Version dataMcVersion) public FabricData.Items.EnderChest deserialize(@NotNull String serialized, @NotNull Version dataMcVersion)
throws DeserializationException { throws DeserializationException {
final FabricHuskSync plugin = (FabricHuskSync) getPlugin(); final FabricHuskSync plugin = (FabricHuskSync) getPlugin();
try { try {
final NbtCompound items = StringNbtReader.parse(serialized); final NbtCompound items = StringNbtReader.parse(serialized);
@@ -150,6 +150,7 @@ public abstract class FabricSerializer {
int VERSION1_20_2 = 3578; // Future int VERSION1_20_2 = 3578; // Future
int VERSION1_20_4 = 3700; // Future int VERSION1_20_4 = 3700; // Future
int VERSION1_20_5 = 3837; // Future int VERSION1_20_5 = 3837; // Future
int VERSION1_21 = 3953; // Future
@NotNull @NotNull
default ItemStack[] getItems(@NotNull NbtCompound tag, @NotNull Version mcVersion, @NotNull FabricHuskSync plugin) { default ItemStack[] getItems(@NotNull NbtCompound tag, @NotNull Version mcVersion, @NotNull FabricHuskSync plugin) {
@@ -158,15 +159,14 @@ public abstract class FabricSerializer {
return upgradeItemStacks(tag, mcVersion, plugin); return upgradeItemStacks(tag, mcVersion, plugin);
} }
final int size = tag.getInt("size"); final ItemStack[] contents = new ItemStack[tag.getInt("size")];
final NbtList items = tag.getList("items", NbtElement.COMPOUND_TYPE); final NbtList itemList = tag.getList("items", NbtElement.COMPOUND_TYPE);
final ItemStack[] itemStacks = new ItemStack[size]; itemList.forEach(element -> {
for (int i = 0; i < size; i++) { final NbtCompound compound = (NbtCompound) element;
final NbtCompound compound = items.getCompound(i); contents[compound.getInt("Slot")] = ItemStack.fromNbt(compound);
final int slot = compound.getInt("Slot"); });
itemStacks[slot] = ItemStack.fromNbt(compound); plugin.debug(Arrays.toString(contents));
} return contents;
return itemStacks;
} catch (Throwable e) { } catch (Throwable e) {
throw new Serializer.DeserializationException("Failed to read item NBT string (%s)".formatted(tag), e); throw new Serializer.DeserializationException("Failed to read item NBT string (%s)".formatted(tag), e);
} }
@@ -216,8 +216,8 @@ public abstract class FabricSerializer {
private NbtCompound upgradeItemData(@NotNull NbtCompound tag, @NotNull Version mcVersion, private NbtCompound upgradeItemData(@NotNull NbtCompound tag, @NotNull Version mcVersion,
@NotNull FabricHuskSync plugin) { @NotNull FabricHuskSync plugin) {
return (NbtCompound) plugin.getMinecraftServer().getDataFixer().update( return (NbtCompound) plugin.getMinecraftServer().getDataFixer().update(
TypeReferences.ITEM_STACK, new Dynamic<Object>((DynamicOps) NbtOps.INSTANCE, tag), TypeReferences.ITEM_STACK, new Dynamic<Object>((DynamicOps) NbtOps.INSTANCE, tag),
getDataVersion(mcVersion), getDataVersion(plugin.getMinecraftVersion()) getDataVersion(mcVersion), getDataVersion(plugin.getMinecraftVersion())
).getValue(); ).getValue();
} }
@@ -232,6 +232,7 @@ public abstract class FabricSerializer {
case "1.20.2" -> VERSION1_20_2; // Future case "1.20.2" -> VERSION1_20_2; // Future
case "1.20.4" -> VERSION1_20_4; // Future case "1.20.4" -> VERSION1_20_4; // Future
case "1.20.5", "1.20.6" -> VERSION1_20_5; // Future case "1.20.5", "1.20.6" -> VERSION1_20_5; // Future
case "1.21" -> VERSION1_21; // Future
default -> VERSION1_20_1; // Current supported ver default -> VERSION1_20_1; // Current supported ver
}; };
} }
@@ -250,7 +251,7 @@ public abstract class FabricSerializer {
@Override @Override
public FabricData.PotionEffects deserialize(@NotNull String serialized) throws DeserializationException { public FabricData.PotionEffects deserialize(@NotNull String serialized) throws DeserializationException {
return FabricData.PotionEffects.adapt( return FabricData.PotionEffects.adapt(
plugin.getGson().fromJson(serialized, TYPE.getType()) plugin.getGson().fromJson(serialized, TYPE.getType())
); );
} }
@@ -274,7 +275,7 @@ public abstract class FabricSerializer {
@Override @Override
public FabricData.Advancements deserialize(@NotNull String serialized) throws DeserializationException { public FabricData.Advancements deserialize(@NotNull String serialized) throws DeserializationException {
return FabricData.Advancements.from( return FabricData.Advancements.from(
plugin.getGson().fromJson(serialized, TYPE.getType()) plugin.getGson().fromJson(serialized, TYPE.getType())
); );
} }

View File

@@ -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;
@@ -68,7 +69,7 @@ public class FabricEventListener extends EventListener implements LockedHandler
WorldSaveCallback.EVENT.register(this::handleWorldSave); WorldSaveCallback.EVENT.register(this::handleWorldSave);
PlayerDeathDropsCallback.EVENT.register(this::handlePlayerDeathDrops); PlayerDeathDropsCallback.EVENT.register(this::handlePlayerDeathDrops);
// TODO: Events of extra things to cancel if the player has not been set yet // Locked events handling
ItemPickupCallback.EVENT.register(this::handleItemPickup); ItemPickupCallback.EVENT.register(this::handleItemPickup);
ItemDropCallback.EVENT.register(this::handleItemDrop); ItemDropCallback.EVENT.register(this::handleItemDrop);
UseBlockCallback.EVENT.register(this::handleBlockInteract); UseBlockCallback.EVENT.register(this::handleBlockInteract);
@@ -82,22 +83,26 @@ 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));
} }
private void handleWorldSave(@NotNull ServerWorld world) { private void handleWorldSave(@NotNull ServerWorld world) {
saveOnWorldSave(world.getPlayers().stream() this.saveOnWorldSave(
.map(player -> (OnlineUser) FabricUser.adapt(player, plugin)).collect(Collectors.toList())); world.getPlayers().stream().map(player -> (OnlineUser) FabricUser.adapt(player, plugin)).toList()
);
} }
private void handlePlayerDeathDrops(@NotNull ServerPlayerEntity player, @Nullable ItemStack @NotNull [] toKeep, private void handlePlayerDeathDrops(@NotNull ServerPlayerEntity player, @Nullable ItemStack @NotNull [] toKeep,
@Nullable ItemStack @NotNull [] toDrop) { @Nullable ItemStack @NotNull [] toDrop) {
final SaveOnDeathSettings settings = plugin.getSettings().getSynchronization().getSaveOnDeath(); final SaveOnDeathSettings settings = plugin.getSettings().getSynchronization().getSaveOnDeath();
saveOnPlayerDeath( this.saveOnPlayerDeath(
FabricUser.adapt(player, plugin), FabricUser.adapt(player, plugin),
FabricData.Items.ItemArray.adapt( FabricData.Items.ItemArray.adapt(
settings.getItemsToSave() == SaveOnDeathSettings.DeathItemsMode.DROPS ? toDrop : toKeep settings.getItemsToSave() == SaveOnDeathSettings.DeathItemsMode.DROPS ? toDrop : toKeep

View File

@@ -20,22 +20,26 @@
package net.william278.husksync.mixins; package net.william278.husksync.mixins;
import net.minecraft.entity.ItemEntity; import net.minecraft.entity.ItemEntity;
import net.minecraft.entity.player.PlayerInventory; import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.ItemStack; import net.minecraft.item.ItemStack;
import net.minecraft.util.ActionResult; import net.minecraft.util.ActionResult;
import net.william278.husksync.event.ItemPickupCallback; import net.william278.husksync.event.ItemPickupCallback;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ItemEntity.class) @Mixin(ItemEntity.class)
public class ItemEntityMixin { public class ItemEntityMixin {
@Redirect(at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/player/PlayerInventory;insertStack(Lnet/minecraft/item/ItemStack;)Z"), @Inject(method = "onPlayerCollision", at = @At("HEAD"), cancellable = true)
method = "onPlayerCollision") private void onPlayerPickupItem(PlayerEntity player, CallbackInfo ci) {
public boolean onPlayerCollision(PlayerInventory inventory, ItemStack stack) { final ItemStack stack = ((ItemEntity) (Object) this).getStack();
ActionResult result = ItemPickupCallback.EVENT.invoker().interact(inventory.player, stack); final ActionResult result = ItemPickupCallback.EVENT.invoker().interact(player, stack);
return (result != ActionResult.FAIL && inventory.insertStack(stack));
if (result == ActionResult.FAIL) {
ci.cancel();
}
} }
} }

View File

@@ -21,6 +21,7 @@ package net.william278.husksync.mixins;
import net.minecraft.entity.ItemEntity; import net.minecraft.entity.ItemEntity;
import net.minecraft.item.ItemStack; import net.minecraft.item.ItemStack;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.ActionResult; import net.minecraft.util.ActionResult;
import net.william278.husksync.event.ItemDropCallback; import net.william278.husksync.event.ItemDropCallback;
@@ -35,8 +36,8 @@ public class ServerPlayerEntityMixin {
@Inject(method = "dropItem", at = @At("HEAD"), cancellable = true) @Inject(method = "dropItem", at = @At("HEAD"), cancellable = true)
private void onPlayerDropItem(ItemStack stack, boolean dropAtFeet, boolean saveThrower, private void onPlayerDropItem(ItemStack stack, boolean dropAtFeet, boolean saveThrower,
final CallbackInfoReturnable<ItemEntity> ci) { final CallbackInfoReturnable<ItemEntity> ci) {
ServerPlayerEntity player = (ServerPlayerEntity) (Object) this; final ServerPlayerEntity player = (ServerPlayerEntity) (Object) this;
ActionResult result = ItemDropCallback.EVENT.invoker().interact(player, stack); final ActionResult result = ItemDropCallback.EVENT.invoker().interact(player, stack);
if (result == ActionResult.FAIL) { if (result == ActionResult.FAIL) {
ci.cancel(); ci.cancel();

View File

@@ -19,9 +19,12 @@
package net.william278.husksync.mixins; package net.william278.husksync.mixins;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.world.ServerWorld; import net.minecraft.server.world.ServerWorld;
import net.william278.husksync.event.WorldSaveCallback; import net.william278.husksync.event.WorldSaveCallback;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@@ -29,8 +32,15 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ServerWorld.class) @Mixin(ServerWorld.class)
public class ServerWorldMixin { public class ServerWorldMixin {
@Final
@Shadow
private MinecraftServer server;
@Inject(method = "saveLevel", at = @At("HEAD")) @Inject(method = "saveLevel", at = @At("HEAD"))
public void saveLevel(CallbackInfo ci) { public void saveLevel(CallbackInfo ci) {
if (server.isStopping() || server.isStopped()) {
return;
}
WorldSaveCallback.EVENT.invoker().save((ServerWorld) (Object) this); WorldSaveCallback.EVENT.invoker().save((ServerWorld) (Object) this);
} }

View File

@@ -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

View File

@@ -3,7 +3,7 @@ org.gradle.jvmargs='-Dfile.encoding=UTF-8'
org.gradle.daemon=true org.gradle.daemon=true
javaVersion=17 javaVersion=17
plugin_version=3.6 plugin_version=3.6.3
plugin_archive=husksync plugin_archive=husksync
plugin_description=A modern, cross-server player data synchronization system plugin_description=A modern, cross-server player data synchronization system

Binary file not shown.

View File

@@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

22
gradlew vendored
View File

@@ -83,7 +83,8 @@ done
# This is normally unused # This is normally unused
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@@ -130,10 +131,13 @@ location of your Java installation."
fi fi
else else
JAVACMD=java JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
@@ -141,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045 # shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
@@ -149,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #( '' | soft) :;; #(
*) *)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045 # shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac esac
@@ -198,11 +202,11 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command; # Collect all arguments for the java command:
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# shell script including quotes and variable substitutions, so put them in # and any embedded shellness will be escaped.
# double quotes to make sure that they get re-expanded; and # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# * put everything else in single quotes, so that it's not re-expanded. # treated as '${Hostname}' itself on the command line.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \

20
gradlew.bat vendored
View File

@@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail
@@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail

View File

@@ -1,7 +1,13 @@
plugins {
id 'xyz.jpenilla.run-paper' version '2.3.0'
}
dependencies { dependencies {
implementation project(':bukkit') implementation project(':bukkit')
compileOnly project(':common') compileOnly project(':common')
implementation 'net.william278.uniform:uniform-paper:1.1.8'
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 +30,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 +40,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'
@@ -41,3 +47,13 @@ shadowJar {
minimize() minimize()
} }
tasks {
runServer {
minecraftVersion('1.20.4')
downloadPlugins {
url('https://download.luckperms.net/1549/bukkit/loader/LuckPerms-Bukkit-5.4.134.jar')
}
}
}

View File

@@ -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);
}
} }

View File

@@ -4,4 +4,4 @@ colorama==0.4.6
idna==3.7 idna==3.7
requests==2.32.0 requests==2.32.0
tqdm==4.66.3 tqdm==4.66.3
urllib3==2.0.7 urllib3==2.2.2

View File

@@ -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):