permission(@NonNull String permission);
+ /**
+ * Sets the permission node and its default value. The usage of the default value is platform dependant
+ * and may or may not be used. For example, it may be registered to an underlying server.
+ *
+ * Extensions may instead listen for {@link GeyserRegisterPermissionsEvent} to register permissions,
+ * especially if the same permission is required by multiple commands. Also see this event for TriState meanings.
+ *
+ * @param permission the permission node
+ * @param defaultValue the node's default value
+ * @return this builder
+ * @deprecated this method is experimental and may be removed in the future
+ */
+ @Deprecated
+ Builder permission(@NonNull String permission, @NonNull TriState defaultValue);
+
/**
* Sets the aliases.
*
* @param aliases the aliases
- * @return the builder
+ * @return this builder
*/
Builder aliases(@NonNull List aliases);
@@ -168,46 +191,62 @@ public interface Command {
* Sets if this command is designed to be used only by server operators.
*
* @param suggestedOpOnly if this command is designed to be used only by server operators
- * @return the builder
+ * @return this builder
+ * @deprecated this method is not guaranteed to produce meaningful or expected results
*/
+ @Deprecated(forRemoval = true)
Builder suggestedOpOnly(boolean suggestedOpOnly);
/**
* Sets if this command is executable on console.
*
* @param executableOnConsole if this command is executable on console
- * @return the builder
+ * @return this builder
+ * @deprecated use {@link #isPlayerOnly()} instead (inverted)
*/
+ @Deprecated(forRemoval = true)
Builder executableOnConsole(boolean executableOnConsole);
+ /**
+ * Sets if this command can only be executed by players.
+ *
+ * @param playerOnly if this command is player only
+ * @return this builder
+ */
+ Builder playerOnly(boolean playerOnly);
+
+ /**
+ * Sets if this command can only be executed by bedrock players.
+ *
+ * @param bedrockOnly if this command is bedrock only
+ * @return this builder
+ */
+ Builder bedrockOnly(boolean bedrockOnly);
+
/**
* Sets the subcommands.
*
* @param subCommands the subcommands
- * @return the builder
+ * @return this builder
+ * @deprecated this method has no effect
*/
- Builder subCommands(@NonNull List subCommands);
-
- /**
- * Sets if this command is bedrock only.
- *
- * @param bedrockOnly if this command is bedrock only
- * @return the builder
- */
- Builder bedrockOnly(boolean bedrockOnly);
+ @Deprecated(forRemoval = true)
+ default Builder subCommands(@NonNull List subCommands) {
+ return this;
+ }
/**
* Sets the {@link CommandExecutor} for this command.
*
* @param executor the command executor
- * @return the builder
+ * @return this builder
*/
Builder executor(@NonNull CommandExecutor executor);
/**
* Builds the command.
*
- * @return the command
+ * @return a new command from this builder
*/
@NonNull
Command build();
diff --git a/api/src/main/java/org/geysermc/geyser/api/command/CommandSource.java b/api/src/main/java/org/geysermc/geyser/api/command/CommandSource.java
index 45276e2c4..c1453f579 100644
--- a/api/src/main/java/org/geysermc/geyser/api/command/CommandSource.java
+++ b/api/src/main/java/org/geysermc/geyser/api/command/CommandSource.java
@@ -26,6 +26,10 @@
package org.geysermc.geyser.api.command;
import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.geysermc.geyser.api.connection.GeyserConnection;
+
+import java.util.UUID;
/**
* Represents an instance capable of sending commands.
@@ -64,6 +68,17 @@ public interface CommandSource {
*/
boolean isConsole();
+ /**
+ * @return a Java UUID if this source represents a player, otherwise null
+ */
+ @Nullable UUID playerUuid();
+
+ /**
+ * @return a GeyserConnection if this source represents a Bedrock player that is connected
+ * to this Geyser instance, otherwise null
+ */
+ @Nullable GeyserConnection connection();
+
/**
* Returns the locale of the command source.
*
diff --git a/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java b/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java
index 9bda4f903..0a580f975 100644
--- a/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java
+++ b/api/src/main/java/org/geysermc/geyser/api/connection/GeyserConnection.java
@@ -60,6 +60,16 @@ public interface GeyserConnection extends Connection, CommandSource {
*/
@NonNull EntityData entities();
+ /**
+ * Returns the current ping of the connection.
+ */
+ int ping();
+
+ /**
+ * Closes the currently open form on the client.
+ */
+ void closeForm();
+
/**
* @param javaId the Java entity ID to look up.
* @return a {@link GeyserEntity} if present in this connection's entity tracker.
diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/EntityData.java b/api/src/main/java/org/geysermc/geyser/api/entity/EntityData.java
index 90b3fc821..48c717089 100644
--- a/api/src/main/java/org/geysermc/geyser/api/entity/EntityData.java
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/EntityData.java
@@ -81,4 +81,10 @@ public interface EntityData {
* @return whether the movement is locked
*/
boolean isMovementLocked();
+
+ /**
+ * Sends a request to the Java server to switch the items in the main and offhand.
+ * There is no guarantee of the server accepting the request.
+ */
+ void switchHands();
}
diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCommandsEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCommandsEvent.java
index 994373752..d136202bd 100644
--- a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCommandsEvent.java
+++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCommandsEvent.java
@@ -50,7 +50,7 @@ public interface GeyserDefineCommandsEvent extends Event {
/**
* Gets all the registered built-in {@link Command}s.
*
- * @return all the registered built-in commands
+ * @return all the registered built-in commands as an unmodifiable map
*/
@NonNull
Map commands();
diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserRegisterPermissionCheckersEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserRegisterPermissionCheckersEvent.java
new file mode 100644
index 000000000..43ebc2c50
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserRegisterPermissionCheckersEvent.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.api.event.lifecycle;
+
+import org.geysermc.event.Event;
+import org.geysermc.event.PostOrder;
+import org.geysermc.geyser.api.permission.PermissionChecker;
+
+/**
+ * Fired by any permission manager implementations that wish to add support for custom permission checking.
+ * This event is not guaranteed to be fired - it is currently only fired on Geyser-Standalone and ViaProxy.
+ *
+ * Subscribing to this event with an earlier {@link PostOrder} and registering a {@link PermissionChecker}
+ * will result in that checker having a higher priority than others.
+ */
+public interface GeyserRegisterPermissionCheckersEvent extends Event {
+
+ void register(PermissionChecker checker);
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserRegisterPermissionsEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserRegisterPermissionsEvent.java
new file mode 100644
index 000000000..4f06c4e5f
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserRegisterPermissionsEvent.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.api.event.lifecycle;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.geysermc.event.Event;
+import org.geysermc.geyser.api.util.TriState;
+
+/**
+ * Fired by anything that wishes to gather permission nodes and defaults.
+ *
+ * This event is not guaranteed to be fired, as certain Geyser platforms do not have a native permission system.
+ * It can be expected to fire on Geyser-Spigot, Geyser-NeoForge, Geyser-Standalone, and Geyser-ViaProxy
+ * It may be fired by a 3rd party regardless of the platform.
+ */
+public interface GeyserRegisterPermissionsEvent extends Event {
+
+ /**
+ * Registers a permission node and its default value with the firer.
+ * {@link TriState#TRUE} corresponds to all players having the permission by default.
+ * {@link TriState#NOT_SET} corresponds to only server operators having the permission by default (if such a concept exists on the platform).
+ * {@link TriState#FALSE} corresponds to no players having the permission by default.
+ *
+ * @param permission the permission node to register
+ * @param defaultValue the default value of the node
+ */
+ void register(@NonNull String permission, @NonNull TriState defaultValue);
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/extension/Extension.java b/api/src/main/java/org/geysermc/geyser/api/extension/Extension.java
index 993bdee44..1eacfea9a 100644
--- a/api/src/main/java/org/geysermc/geyser/api/extension/Extension.java
+++ b/api/src/main/java/org/geysermc/geyser/api/extension/Extension.java
@@ -107,6 +107,15 @@ public interface Extension extends EventRegistrar {
return this.extensionLoader().description(this);
}
+ /**
+ * @return the root command that all of this extension's commands will stem from.
+ * By default, this is the extension's id.
+ */
+ @NonNull
+ default String rootCommand() {
+ return this.description().id();
+ }
+
/**
* Gets the extension's logger
*
diff --git a/api/src/main/java/org/geysermc/geyser/api/extension/ExtensionDescription.java b/api/src/main/java/org/geysermc/geyser/api/extension/ExtensionDescription.java
index 2df3ee815..25daf450f 100644
--- a/api/src/main/java/org/geysermc/geyser/api/extension/ExtensionDescription.java
+++ b/api/src/main/java/org/geysermc/geyser/api/extension/ExtensionDescription.java
@@ -59,33 +59,46 @@ public interface ExtensionDescription {
String main();
/**
- * Gets the extension's major api version
+ * Represents the human api version that the extension requires.
+ * See the Geyser version outline)
+ * for more details on the Geyser API version.
*
- * @return the extension's major api version
+ * @return the extension's requested human api version
+ */
+ int humanApiVersion();
+
+ /**
+ * Represents the major api version that the extension requires.
+ * See the Geyser version outline)
+ * for more details on the Geyser API version.
+ *
+ * @return the extension's requested major api version
*/
int majorApiVersion();
/**
- * Gets the extension's minor api version
+ * Represents the minor api version that the extension requires.
+ * See the Geyser version outline)
+ * for more details on the Geyser API version.
*
- * @return the extension's minor api version
+ * @return the extension's requested minor api version
*/
int minorApiVersion();
/**
- * Gets the extension's patch api version
- *
- * @return the extension's patch api version
+ * No longer in use. Geyser is now using an adaption of the romantic versioning scheme.
+ * See here for details.
*/
- int patchApiVersion();
+ @Deprecated(forRemoval = true)
+ default int patchApiVersion() {
+ return minorApiVersion();
+ }
/**
- * Gets the extension's api version.
- *
- * @return the extension's api version
+ * Returns the extension's requested Geyser Api version.
*/
default String apiVersion() {
- return majorApiVersion() + "." + minorApiVersion() + "." + patchApiVersion();
+ return humanApiVersion() + "." + majorApiVersion() + "." + minorApiVersion();
}
/**
diff --git a/api/src/main/java/org/geysermc/geyser/api/permission/PermissionChecker.java b/api/src/main/java/org/geysermc/geyser/api/permission/PermissionChecker.java
new file mode 100644
index 000000000..c0d4af2f4
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/permission/PermissionChecker.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.api.permission;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.geysermc.geyser.api.command.CommandSource;
+import org.geysermc.geyser.api.util.TriState;
+
+/**
+ * Something capable of checking if a {@link CommandSource} has a permission
+ */
+@FunctionalInterface
+public interface PermissionChecker {
+
+ /**
+ * Checks if the given source has a permission
+ *
+ * @param source the {@link CommandSource} whose permissions should be queried
+ * @param permission the permission node to check
+ * @return a {@link TriState} as the value of the node. {@link TriState#NOT_SET} generally means that the permission
+ * node itself was not found, and the source does not have such permission.
+ * {@link TriState#TRUE} and {@link TriState#FALSE} represent explicitly set values.
+ */
+ @NonNull
+ TriState hasPermission(@NonNull CommandSource source, @NonNull String permission);
+}
diff --git a/bootstrap/bungeecord/build.gradle.kts b/bootstrap/bungeecord/build.gradle.kts
index 910e50723..1564b7f75 100644
--- a/bootstrap/bungeecord/build.gradle.kts
+++ b/bootstrap/bungeecord/build.gradle.kts
@@ -1,5 +1,12 @@
+plugins {
+ id("geyser.platform-conventions")
+ id("geyser.modrinth-uploading-conventions")
+}
+
dependencies {
api(projects.core)
+
+ implementation(libs.cloud.bungee)
implementation(libs.adventure.text.serializer.bungeecord)
compileOnlyApi(libs.bungeecord.proxy)
}
@@ -8,13 +15,15 @@ platformRelocate("net.md_5.bungee.jni")
platformRelocate("com.fasterxml.jackson")
platformRelocate("io.netty.channel.kqueue") // This is not used because relocating breaks natives, but we must include it or else we get ClassDefNotFound
platformRelocate("net.kyori")
+platformRelocate("org.incendo")
+platformRelocate("io.leangen.geantyref") // provided by cloud, should also be relocated
platformRelocate("org.yaml") // Broken as of 1.20
// These dependencies are already present on the platform
provided(libs.bungeecord.proxy)
-application {
- mainClass.set("org.geysermc.geyser.platform.bungeecord.GeyserBungeeMain")
+tasks.withType {
+ manifest.attributes["Main-Class"] = "org.geysermc.geyser.platform.bungeecord.GeyserBungeeMain"
}
tasks.withType {
diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePlugin.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePlugin.java
index cd6b59f64..7adfd488f 100644
--- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePlugin.java
+++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePlugin.java
@@ -27,6 +27,7 @@ package org.geysermc.geyser.platform.bungeecord;
import io.netty.channel.Channel;
import net.md_5.bungee.BungeeCord;
+import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.config.ListenerInfo;
import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.protocol.ProtocolConstants;
@@ -34,17 +35,20 @@ import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.GeyserBootstrap;
import org.geysermc.geyser.GeyserImpl;
-import org.geysermc.geyser.api.command.Command;
-import org.geysermc.geyser.api.extension.Extension;
import org.geysermc.geyser.api.util.PlatformType;
-import org.geysermc.geyser.command.GeyserCommandManager;
+import org.geysermc.geyser.command.CommandRegistry;
+import org.geysermc.geyser.command.CommandSourceConverter;
+import org.geysermc.geyser.command.GeyserCommandSource;
import org.geysermc.geyser.configuration.GeyserConfiguration;
import org.geysermc.geyser.dump.BootstrapDumpInfo;
import org.geysermc.geyser.ping.GeyserLegacyPingPassthrough;
import org.geysermc.geyser.ping.IGeyserPingPassthrough;
-import org.geysermc.geyser.platform.bungeecord.command.GeyserBungeeCommandExecutor;
+import org.geysermc.geyser.platform.bungeecord.command.BungeeCommandSource;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.util.FileUtils;
+import org.incendo.cloud.CommandManager;
+import org.incendo.cloud.bungee.BungeeCommandManager;
+import org.incendo.cloud.execution.ExecutionCoordinator;
import java.io.File;
import java.io.IOException;
@@ -54,19 +58,17 @@ import java.net.SocketAddress;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
-import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap {
- private GeyserCommandManager geyserCommandManager;
+ private CommandRegistry commandRegistry;
private GeyserBungeeConfiguration geyserConfig;
private GeyserBungeeInjector geyserInjector;
private final GeyserBungeeLogger geyserLogger = new GeyserBungeeLogger(getLogger());
private IGeyserPingPassthrough geyserBungeePingPassthrough;
-
private GeyserImpl geyser;
@Override
@@ -99,10 +101,31 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap {
GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger);
this.geyser = GeyserImpl.load(PlatformType.BUNGEECORD, this);
this.geyserInjector = new GeyserBungeeInjector(this);
+
+ // Registration of listeners occurs only once
+ this.getProxy().getPluginManager().registerListener(this, new GeyserBungeeUpdateListener());
}
@Override
public void onEnable() {
+ if (geyser == null) {
+ return; // Config did not load properly!
+ }
+
+ // After Geyser initialize for parity with other platforms.
+ var sourceConverter = new CommandSourceConverter<>(
+ CommandSender.class,
+ id -> getProxy().getPlayer(id),
+ () -> getProxy().getConsole(),
+ BungeeCommandSource::new
+ );
+ CommandManager cloud = new BungeeCommandManager<>(
+ this,
+ ExecutionCoordinator.simpleCoordinator(),
+ sourceConverter
+ );
+ this.commandRegistry = new CommandRegistry(geyser, cloud, false); // applying root permission would be a breaking change because we can't register permission defaults
+
// Big hack - Bungee does not provide us an event to listen to, so schedule a repeating
// task that waits for a field to be filled which is set after the plugin enable
// process is complete
@@ -142,11 +165,6 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap {
}
this.geyserLogger.setDebug(geyserConfig.isDebugMode());
GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger);
- } else {
- // For consistency with other platforms - create command manager before GeyserImpl#start()
- // This ensures the command events are called before the item/block ones are
- this.geyserCommandManager = new GeyserCommandManager(geyser);
- this.geyserCommandManager.init();
}
// Force-disable query if enabled, or else Geyser won't enable
@@ -181,16 +199,6 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap {
}
this.geyserInjector.initializeLocalChannel(this);
-
- this.getProxy().getPluginManager().registerCommand(this, new GeyserBungeeCommandExecutor("geyser", this.geyser, this.geyserCommandManager.getCommands()));
- for (Map.Entry> entry : this.geyserCommandManager.extensionCommands().entrySet()) {
- Map commands = entry.getValue();
- if (commands.isEmpty()) {
- continue;
- }
-
- this.getProxy().getPluginManager().registerCommand(this, new GeyserBungeeCommandExecutor(entry.getKey().description().id(), this.geyser, commands));
- }
}
@Override
@@ -226,8 +234,8 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap {
}
@Override
- public GeyserCommandManager getGeyserCommandManager() {
- return this.geyserCommandManager;
+ public CommandRegistry getCommandRegistry() {
+ return this.commandRegistry;
}
@Override
diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeUpdateListener.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeUpdateListener.java
index c68839b20..0a89b5421 100644
--- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeUpdateListener.java
+++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeUpdateListener.java
@@ -29,8 +29,8 @@ import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.event.EventHandler;
-import org.geysermc.geyser.Constants;
import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.Permissions;
import org.geysermc.geyser.platform.bungeecord.command.BungeeCommandSource;
import org.geysermc.geyser.util.VersionCheckUtils;
@@ -40,7 +40,7 @@ public final class GeyserBungeeUpdateListener implements Listener {
public void onPlayerJoin(final PostLoginEvent event) {
if (GeyserImpl.getInstance().getConfig().isNotifyOnNewBedrockUpdate()) {
final ProxiedPlayer player = event.getPlayer();
- if (player.hasPermission(Constants.UPDATE_PERMISSION)) {
+ if (player.hasPermission(Permissions.CHECK_UPDATE)) {
VersionCheckUtils.checkForGeyserUpdate(() -> new BungeeCommandSource(player));
}
}
diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/BungeeCommandSource.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/BungeeCommandSource.java
index e3099f170..10ccc5bac 100644
--- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/BungeeCommandSource.java
+++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/BungeeCommandSource.java
@@ -27,19 +27,22 @@ package org.geysermc.geyser.platform.bungeecord.command;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer;
+import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.command.GeyserCommandSource;
import org.geysermc.geyser.text.GeyserLocale;
import java.util.Locale;
+import java.util.UUID;
public class BungeeCommandSource implements GeyserCommandSource {
- private final net.md_5.bungee.api.CommandSender handle;
+ private final CommandSender handle;
- public BungeeCommandSource(net.md_5.bungee.api.CommandSender handle) {
+ public BungeeCommandSource(CommandSender handle) {
this.handle = handle;
// Ensure even Java players' languages are loaded
GeyserLocale.loadGeyserLocale(this.locale());
@@ -72,12 +75,20 @@ public class BungeeCommandSource implements GeyserCommandSource {
return !(handle instanceof ProxiedPlayer);
}
+ @Override
+ public @Nullable UUID playerUuid() {
+ if (handle instanceof ProxiedPlayer player) {
+ return player.getUniqueId();
+ }
+ return null;
+ }
+
@Override
public String locale() {
if (handle instanceof ProxiedPlayer player) {
Locale locale = player.getLocale();
if (locale != null) {
- // Locale can be null early on in the conneciton
+ // Locale can be null early on in the connection
return GeyserLocale.formatLocale(locale.getLanguage() + "_" + locale.getCountry());
}
}
@@ -86,6 +97,12 @@ public class BungeeCommandSource implements GeyserCommandSource {
@Override
public boolean hasPermission(String permission) {
- return handle.hasPermission(permission);
+ // Handle blank permissions ourselves, as bungeecord only handles empty ones
+ return permission.isBlank() || handle.hasPermission(permission);
+ }
+
+ @Override
+ public Object handle() {
+ return handle;
}
}
diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/GeyserBungeeCommandExecutor.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/GeyserBungeeCommandExecutor.java
deleted file mode 100644
index 2d02c9950..000000000
--- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/command/GeyserBungeeCommandExecutor.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- *
- * @author GeyserMC
- * @link https://github.com/GeyserMC/Geyser
- */
-
-package org.geysermc.geyser.platform.bungeecord.command;
-
-import net.md_5.bungee.api.ChatColor;
-import net.md_5.bungee.api.CommandSender;
-import net.md_5.bungee.api.plugin.Command;
-import net.md_5.bungee.api.plugin.TabExecutor;
-import org.geysermc.geyser.GeyserImpl;
-import org.geysermc.geyser.command.GeyserCommand;
-import org.geysermc.geyser.command.GeyserCommandExecutor;
-import org.geysermc.geyser.session.GeyserSession;
-import org.geysermc.geyser.text.GeyserLocale;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Map;
-
-public class GeyserBungeeCommandExecutor extends Command implements TabExecutor {
- private final GeyserCommandExecutor commandExecutor;
-
- public GeyserBungeeCommandExecutor(String name, GeyserImpl geyser, Map commands) {
- super(name);
-
- this.commandExecutor = new GeyserCommandExecutor(geyser, commands);
- }
-
- @Override
- public void execute(CommandSender sender, String[] args) {
- BungeeCommandSource commandSender = new BungeeCommandSource(sender);
- GeyserSession session = this.commandExecutor.getGeyserSession(commandSender);
-
- if (args.length > 0) {
- GeyserCommand command = this.commandExecutor.getCommand(args[0]);
- if (command != null) {
- if (!sender.hasPermission(command.permission())) {
- String message = GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", commandSender.locale());
-
- commandSender.sendMessage(ChatColor.RED + message);
- return;
- }
- if (command.isBedrockOnly() && session == null) {
- String message = GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.bedrock_only", commandSender.locale());
-
- commandSender.sendMessage(ChatColor.RED + message);
- return;
- }
- command.execute(session, commandSender, args.length > 1 ? Arrays.copyOfRange(args, 1, args.length) : new String[0]);
- } else {
- String message = GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.not_found", commandSender.locale());
- commandSender.sendMessage(ChatColor.RED + message);
- }
- } else {
- this.commandExecutor.getCommand("help").execute(session, commandSender, new String[0]);
- }
- }
-
- @Override
- public Iterable onTabComplete(CommandSender sender, String[] args) {
- if (args.length == 1) {
- return commandExecutor.tabComplete(new BungeeCommandSource(sender));
- } else {
- return Collections.emptyList();
- }
- }
-}
diff --git a/bootstrap/mod/build.gradle.kts b/bootstrap/mod/build.gradle.kts
index 57f11b2c7..c43f123ec 100644
--- a/bootstrap/mod/build.gradle.kts
+++ b/bootstrap/mod/build.gradle.kts
@@ -1,3 +1,7 @@
+plugins {
+ id("geyser.modded-conventions")
+}
+
architectury {
common("neoforge", "fabric")
}
diff --git a/bootstrap/mod/fabric/build.gradle.kts b/bootstrap/mod/fabric/build.gradle.kts
index 0d083fcf7..56bec322e 100644
--- a/bootstrap/mod/fabric/build.gradle.kts
+++ b/bootstrap/mod/fabric/build.gradle.kts
@@ -1,5 +1,6 @@
plugins {
- application
+ id("geyser.modded-conventions")
+ id("geyser.modrinth-uploading-conventions")
}
architectury {
@@ -25,10 +26,7 @@ dependencies {
shadow(libs.protocol.connection) { isTransitive = false }
shadow(libs.protocol.common) { isTransitive = false }
shadow(libs.protocol.codec) { isTransitive = false }
- shadow(libs.minecraftauth) { isTransitive = false }
shadow(libs.raknet) { isTransitive = false }
-
- // Consequences of shading + relocating mcauthlib: shadow/relocate mcpl!
shadow(libs.mcprotocollib) { isTransitive = false }
// Since we also relocate cloudburst protocol: shade erosion common
@@ -38,13 +36,12 @@ dependencies {
shadow(projects.api) { isTransitive = false }
shadow(projects.common) { isTransitive = false }
- // Permissions
- modImplementation(libs.fabric.permissions)
- include(libs.fabric.permissions)
+ modImplementation(libs.cloud.fabric)
+ include(libs.cloud.fabric)
}
-application {
- mainClass.set("org.geysermc.geyser.platform.fabric.GeyserFabricMain")
+tasks.withType {
+ manifest.attributes["Main-Class"] = "org.geysermc.geyser.platform.fabric.GeyserFabricMain"
}
relocate("org.cloudburstmc.netty")
@@ -67,4 +64,4 @@ modrinth {
dependencies {
required.project("fabric-api")
}
-}
\ No newline at end of file
+}
diff --git a/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricBootstrap.java b/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricBootstrap.java
index c363ade8f..149246d59 100644
--- a/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricBootstrap.java
+++ b/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricBootstrap.java
@@ -25,7 +25,6 @@
package org.geysermc.geyser.platform.fabric;
-import me.lucko.fabric.api.permissions.v0.Permissions;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
@@ -34,9 +33,16 @@ import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.world.entity.player.Player;
-import org.checkerframework.checker.nullness.qual.NonNull;
+import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.command.CommandRegistry;
+import org.geysermc.geyser.command.CommandSourceConverter;
+import org.geysermc.geyser.command.GeyserCommandSource;
import org.geysermc.geyser.platform.mod.GeyserModBootstrap;
import org.geysermc.geyser.platform.mod.GeyserModUpdateListener;
+import org.geysermc.geyser.platform.mod.command.ModCommandSource;
+import org.incendo.cloud.CommandManager;
+import org.incendo.cloud.execution.ExecutionCoordinator;
+import org.incendo.cloud.fabric.FabricServerCommandManager;
public class GeyserFabricBootstrap extends GeyserModBootstrap implements ModInitializer {
@@ -70,20 +76,23 @@ public class GeyserFabricBootstrap extends GeyserModBootstrap implements ModInit
ServerPlayConnectionEvents.JOIN.register((handler, $, $$) -> GeyserModUpdateListener.onPlayReady(handler.getPlayer()));
this.onGeyserInitialize();
+
+ var sourceConverter = CommandSourceConverter.layered(
+ CommandSourceStack.class,
+ id -> getServer().getPlayerList().getPlayer(id),
+ Player::createCommandSourceStack,
+ () -> getServer().createCommandSourceStack(), // NPE if method reference is used, since server is not available yet
+ ModCommandSource::new
+ );
+ CommandManager cloud = new FabricServerCommandManager<>(
+ ExecutionCoordinator.simpleCoordinator(),
+ sourceConverter
+ );
+ this.setCommandRegistry(new CommandRegistry(GeyserImpl.getInstance(), cloud, false)); // applying root permission would be a breaking change because we can't register permission defaults
}
@Override
public boolean isServer() {
return FabricLoader.getInstance().getEnvironmentType().equals(EnvType.SERVER);
}
-
- @Override
- public boolean hasPermission(@NonNull Player source, @NonNull String permissionNode) {
- return Permissions.check(source, permissionNode);
- }
-
- @Override
- public boolean hasPermission(@NonNull CommandSourceStack source, @NonNull String permissionNode, int permissionLevel) {
- return Permissions.check(source, permissionNode, permissionLevel);
- }
}
diff --git a/bootstrap/mod/neoforge/build.gradle.kts b/bootstrap/mod/neoforge/build.gradle.kts
index e0e7c2dfa..4ab005b4f 100644
--- a/bootstrap/mod/neoforge/build.gradle.kts
+++ b/bootstrap/mod/neoforge/build.gradle.kts
@@ -1,16 +1,18 @@
plugins {
- application
+ id("geyser.modded-conventions")
+ id("geyser.modrinth-uploading-conventions")
}
-// This is provided by "org.cloudburstmc.math.mutable" too, so yeet.
-// NeoForge's class loader is *really* annoying.
-provided("org.cloudburstmc.math", "api")
-
architectury {
platformSetupLoomIde()
neoForge()
}
+// This is provided by "org.cloudburstmc.math.mutable" too, so yeet.
+// NeoForge's class loader is *really* annoying.
+provided("org.cloudburstmc.math", "api")
+provided("com.google.errorprone", "error_prone_annotations")
+
val includeTransitive: Configuration = configurations.getByName("includeTransitive")
dependencies {
@@ -37,10 +39,13 @@ dependencies {
// Include all transitive deps of core via JiJ
includeTransitive(projects.core)
+
+ modImplementation(libs.cloud.neoforge)
+ include(libs.cloud.neoforge)
}
-application {
- mainClass.set("org.geysermc.geyser.platform.forge.GeyserNeoForgeMain")
+tasks.withType {
+ manifest.attributes["Main-Class"] = "org.geysermc.geyser.platform.neoforge.GeyserNeoForgeMain"
}
tasks {
@@ -56,4 +61,4 @@ tasks {
modrinth {
loaders.add("neoforge")
uploadFile.set(tasks.getByPath("remapModrinthJar"))
-}
\ No newline at end of file
+}
diff --git a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeBootstrap.java b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeBootstrap.java
index b97e42389..ad56eda39 100644
--- a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeBootstrap.java
+++ b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeBootstrap.java
@@ -27,6 +27,7 @@ package org.geysermc.geyser.platform.neoforge;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.world.entity.player.Player;
+import net.neoforged.bus.api.EventPriority;
import net.neoforged.fml.ModContainer;
import net.neoforged.fml.common.Mod;
import net.neoforged.fml.loading.FMLLoader;
@@ -35,15 +36,22 @@ import net.neoforged.neoforge.event.GameShuttingDownEvent;
import net.neoforged.neoforge.event.entity.player.PlayerEvent;
import net.neoforged.neoforge.event.server.ServerStartedEvent;
import net.neoforged.neoforge.event.server.ServerStoppingEvent;
-import org.checkerframework.checker.nullness.qual.NonNull;
+import net.neoforged.neoforge.server.permission.events.PermissionGatherEvent;
+import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent;
+import org.geysermc.geyser.command.CommandSourceConverter;
+import org.geysermc.geyser.command.GeyserCommandSource;
import org.geysermc.geyser.platform.mod.GeyserModBootstrap;
import org.geysermc.geyser.platform.mod.GeyserModUpdateListener;
+import org.geysermc.geyser.platform.mod.command.ModCommandSource;
+import org.incendo.cloud.CommandManager;
+import org.incendo.cloud.execution.ExecutionCoordinator;
+import org.incendo.cloud.neoforge.NeoForgeServerCommandManager;
+
+import java.util.Objects;
@Mod(ModConstants.MOD_ID)
public class GeyserNeoForgeBootstrap extends GeyserModBootstrap {
- private final GeyserNeoForgePermissionHandler permissionHandler = new GeyserNeoForgePermissionHandler();
-
public GeyserNeoForgeBootstrap(ModContainer container) {
super(new GeyserNeoForgePlatform(container));
@@ -56,9 +64,26 @@ public class GeyserNeoForgeBootstrap extends GeyserModBootstrap {
NeoForge.EVENT_BUS.addListener(this::onServerStopping);
NeoForge.EVENT_BUS.addListener(this::onPlayerJoin);
- NeoForge.EVENT_BUS.addListener(this.permissionHandler::onPermissionGather);
+
+ NeoForge.EVENT_BUS.addListener(EventPriority.HIGHEST, this::onPermissionGather);
this.onGeyserInitialize();
+
+ var sourceConverter = CommandSourceConverter.layered(
+ CommandSourceStack.class,
+ id -> getServer().getPlayerList().getPlayer(id),
+ Player::createCommandSourceStack,
+ () -> getServer().createCommandSourceStack(),
+ ModCommandSource::new
+ );
+ CommandManager cloud = new NeoForgeServerCommandManager<>(
+ ExecutionCoordinator.simpleCoordinator(),
+ sourceConverter
+ );
+ GeyserNeoForgeCommandRegistry registry = new GeyserNeoForgeCommandRegistry(getGeyser(), cloud);
+ this.setCommandRegistry(registry);
+ // An auxiliary listener for registering undefined permissions belonging to commands. See javadocs for more info.
+ NeoForge.EVENT_BUS.addListener(EventPriority.LOWEST, registry::onPermissionGatherForUndefined);
}
private void onServerStarted(ServerStartedEvent event) {
@@ -87,13 +112,17 @@ public class GeyserNeoForgeBootstrap extends GeyserModBootstrap {
return FMLLoader.getDist().isDedicatedServer();
}
- @Override
- public boolean hasPermission(@NonNull Player source, @NonNull String permissionNode) {
- return this.permissionHandler.hasPermission(source, permissionNode);
- }
+ private void onPermissionGather(PermissionGatherEvent.Nodes event) {
+ getGeyser().eventBus().fire(
+ (GeyserRegisterPermissionsEvent) (permission, defaultValue) -> {
+ Objects.requireNonNull(permission, "permission");
+ Objects.requireNonNull(defaultValue, "permission default for " + permission);
- @Override
- public boolean hasPermission(@NonNull CommandSourceStack source, @NonNull String permissionNode, int permissionLevel) {
- return this.permissionHandler.hasPermission(source, permissionNode, permissionLevel);
+ if (permission.isBlank()) {
+ return;
+ }
+ PermissionUtils.register(permission, defaultValue, event);
+ }
+ );
}
}
diff --git a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeCommandRegistry.java b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeCommandRegistry.java
new file mode 100644
index 000000000..a8854d5d9
--- /dev/null
+++ b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeCommandRegistry.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.platform.neoforge;
+
+import net.neoforged.neoforge.server.permission.events.PermissionGatherEvent;
+import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent;
+import org.geysermc.geyser.api.util.TriState;
+import org.geysermc.geyser.command.CommandRegistry;
+import org.geysermc.geyser.command.GeyserCommand;
+import org.geysermc.geyser.command.GeyserCommandSource;
+import org.incendo.cloud.CommandManager;
+import org.incendo.cloud.neoforge.PermissionNotRegisteredException;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class GeyserNeoForgeCommandRegistry extends CommandRegistry {
+
+ /**
+ * Permissions with an undefined permission default. Use Set to not register the same fallback more than once.
+ * NeoForge requires that all permissions are registered, and cloud-neoforge follows that.
+ * This is unlike most platforms, on which we wouldn't register a permission if no default was provided.
+ */
+ private final Set undefinedPermissions = new HashSet<>();
+
+ public GeyserNeoForgeCommandRegistry(GeyserImpl geyser, CommandManager cloud) {
+ super(geyser, cloud);
+ }
+
+ @Override
+ protected void register(GeyserCommand command, Map commands) {
+ super.register(command, commands);
+
+ // FIRST STAGE: Collect all permissions that may have undefined defaults.
+ if (!command.permission().isBlank() && command.permissionDefault() == null) {
+ // Permission requirement exists but no default value specified.
+ undefinedPermissions.add(command.permission());
+ }
+ }
+
+ @Override
+ protected void onRegisterPermissions(GeyserRegisterPermissionsEvent event) {
+ super.onRegisterPermissions(event);
+
+ // SECOND STAGE
+ // Now that we are aware of all commands, we can eliminate some incorrect assumptions.
+ // Example: two commands may have the same permission, but only of them defines a permission default.
+ undefinedPermissions.removeAll(permissionDefaults.keySet());
+ }
+
+ /**
+ * Registers permissions with possibly undefined defaults.
+ * Should be subscribed late to allow extensions and mods to register a desired permission default first.
+ */
+ void onPermissionGatherForUndefined(PermissionGatherEvent.Nodes event) {
+ // THIRD STAGE
+ for (String permission : undefinedPermissions) {
+ if (PermissionUtils.register(permission, TriState.NOT_SET, event)) {
+ // The permission was not already registered
+ geyser.getLogger().debug("Registered permission " + permission + " with fallback default value of NOT_SET");
+ }
+ }
+ }
+
+ @Override
+ public boolean hasPermission(GeyserCommandSource source, String permission) {
+ // NeoForgeServerCommandManager will throw this exception if the permission is not registered to the server.
+ // We can't realistically ensure that every permission is registered (calls by API users), so we catch this.
+ // This works for our calls, but not for cloud's internal usage. For that case, see above.
+ try {
+ return super.hasPermission(source, permission);
+ } catch (PermissionNotRegisteredException e) {
+ return false;
+ }
+ }
+}
diff --git a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgePermissionHandler.java b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgePermissionHandler.java
deleted file mode 100644
index 0a5f8f052..000000000
--- a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgePermissionHandler.java
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- *
- * @author GeyserMC
- * @link https://github.com/GeyserMC/Geyser
- */
-
-package org.geysermc.geyser.platform.neoforge;
-
-import net.minecraft.commands.CommandSourceStack;
-import net.minecraft.server.level.ServerPlayer;
-import net.minecraft.world.entity.player.Player;
-import net.neoforged.neoforge.server.permission.PermissionAPI;
-import net.neoforged.neoforge.server.permission.events.PermissionGatherEvent;
-import net.neoforged.neoforge.server.permission.nodes.PermissionDynamicContextKey;
-import net.neoforged.neoforge.server.permission.nodes.PermissionNode;
-import net.neoforged.neoforge.server.permission.nodes.PermissionType;
-import net.neoforged.neoforge.server.permission.nodes.PermissionTypes;
-import org.checkerframework.checker.nullness.qual.NonNull;
-import org.geysermc.geyser.Constants;
-import org.geysermc.geyser.GeyserImpl;
-import org.geysermc.geyser.api.command.Command;
-import org.geysermc.geyser.command.GeyserCommandManager;
-
-import java.lang.reflect.Constructor;
-import java.util.HashMap;
-import java.util.Map;
-
-public class GeyserNeoForgePermissionHandler {
-
- private static final Constructor> PERMISSION_NODE_CONSTRUCTOR;
-
- static {
- try {
- @SuppressWarnings("rawtypes")
- Constructor constructor = PermissionNode.class.getDeclaredConstructor(
- String.class,
- PermissionType.class,
- PermissionNode.PermissionResolver.class,
- PermissionDynamicContextKey[].class
- );
- constructor.setAccessible(true);
- PERMISSION_NODE_CONSTRUCTOR = constructor;
- } catch (NoSuchMethodException e) {
- throw new RuntimeException("Unable to construct PermissionNode!", e);
- }
- }
-
- private final Map> permissionNodes = new HashMap<>();
-
- public void onPermissionGather(PermissionGatherEvent.Nodes event) {
- this.registerNode(Constants.UPDATE_PERMISSION, event);
-
- GeyserCommandManager commandManager = GeyserImpl.getInstance().commandManager();
- for (Map.Entry entry : commandManager.commands().entrySet()) {
- Command command = entry.getValue();
-
- // Don't register aliases
- if (!command.name().equals(entry.getKey())) {
- continue;
- }
-
- this.registerNode(command.permission(), event);
- }
-
- for (Map commands : commandManager.extensionCommands().values()) {
- for (Map.Entry entry : commands.entrySet()) {
- Command command = entry.getValue();
-
- // Don't register aliases
- if (!command.name().equals(entry.getKey())) {
- continue;
- }
-
- this.registerNode(command.permission(), event);
- }
- }
- }
-
- public boolean hasPermission(@NonNull Player source, @NonNull String permissionNode) {
- PermissionNode node = this.permissionNodes.get(permissionNode);
- if (node == null) {
- GeyserImpl.getInstance().getLogger().warning("Unable to find permission node " + permissionNode);
- return false;
- }
-
- return PermissionAPI.getPermission((ServerPlayer) source, node);
- }
-
- public boolean hasPermission(@NonNull CommandSourceStack source, @NonNull String permissionNode, int permissionLevel) {
- if (!source.isPlayer()) {
- return true;
- }
- assert source.getPlayer() != null;
- boolean permission = this.hasPermission(source.getPlayer(), permissionNode);
- if (!permission) {
- return source.getPlayer().hasPermissions(permissionLevel);
- }
-
- return true;
- }
-
- private void registerNode(String node, PermissionGatherEvent.Nodes event) {
- PermissionNode permissionNode = this.createNode(node);
-
- // NeoForge likes to crash if you try and register a duplicate node
- if (!event.getNodes().contains(permissionNode)) {
- event.addNodes(permissionNode);
- this.permissionNodes.put(node, permissionNode);
- }
- }
-
- @SuppressWarnings("unchecked")
- private PermissionNode createNode(String node) {
- // The typical constructors in PermissionNode require a
- // mod id, which means our permission nodes end up becoming
- // geyser_neoforge. instead of just . We work around
- // this by using reflection to access the constructor that
- // doesn't require a mod id or ResourceLocation.
- try {
- return (PermissionNode) PERMISSION_NODE_CONSTRUCTOR.newInstance(
- node,
- PermissionTypes.BOOLEAN,
- (PermissionNode.PermissionResolver) (player, playerUUID, context) -> false,
- new PermissionDynamicContextKey[0]
- );
- } catch (Exception e) {
- throw new RuntimeException("Unable to create permission node " + node, e);
- }
- }
-}
diff --git a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/PermissionUtils.java b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/PermissionUtils.java
new file mode 100644
index 000000000..c57dc9a6c
--- /dev/null
+++ b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/PermissionUtils.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2024 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.platform.neoforge;
+
+import net.neoforged.neoforge.server.permission.events.PermissionGatherEvent;
+import net.neoforged.neoforge.server.permission.nodes.PermissionNode;
+import net.neoforged.neoforge.server.permission.nodes.PermissionTypes;
+import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent;
+import org.geysermc.geyser.api.util.TriState;
+import org.geysermc.geyser.platform.neoforge.mixin.PermissionNodeMixin;
+
+/**
+ * Common logic for handling the more complicated way we have to register permission on NeoForge
+ */
+public class PermissionUtils {
+
+ private PermissionUtils() {
+ //no
+ }
+
+ /**
+ * Registers the given permission and its default value to the event. If the permission has the same name as one
+ * that has already been registered to the event, it will not be registered. In other words, it will not override.
+ *
+ * @param permission the permission to register
+ * @param permissionDefault the permission's default value. See {@link GeyserRegisterPermissionsEvent#register(String, TriState)} for TriState meanings.
+ * @param event the registration event
+ * @return true if the permission was registered
+ */
+ public static boolean register(String permission, TriState permissionDefault, PermissionGatherEvent.Nodes event) {
+ // NeoForge likes to crash if you try and register a duplicate node
+ if (event.getNodes().stream().noneMatch(n -> n.getNodeName().equals(permission))) {
+ PermissionNode node = createNode(permission, permissionDefault);
+ event.addNodes(node);
+ return true;
+ }
+ return false;
+ }
+
+ private static PermissionNode createNode(String node, TriState permissionDefault) {
+ return PermissionNodeMixin.geyser$construct(
+ node,
+ PermissionTypes.BOOLEAN,
+ (player, playerUUID, context) -> switch (permissionDefault) {
+ case TRUE -> true;
+ case FALSE -> false;
+ case NOT_SET -> {
+ if (player != null) {
+ yield player.createCommandSourceStack().hasPermission(player.server.getOperatorUserPermissionLevel());
+ }
+ yield false; // NeoForge javadocs say player is null in the case of an offline player.
+ }
+ }
+ );
+ }
+}
diff --git a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/mixin/PermissionNodeMixin.java b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/mixin/PermissionNodeMixin.java
new file mode 100644
index 000000000..a43acd58a
--- /dev/null
+++ b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/mixin/PermissionNodeMixin.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.platform.neoforge.mixin;
+
+import net.neoforged.neoforge.server.permission.nodes.PermissionDynamicContextKey;
+import net.neoforged.neoforge.server.permission.nodes.PermissionNode;
+import net.neoforged.neoforge.server.permission.nodes.PermissionType;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Invoker;
+
+@Mixin(value = PermissionNode.class, remap = false) // this is API - do not remap
+public interface PermissionNodeMixin {
+
+ /**
+ * Invokes the matching private constructor in {@link PermissionNode}.
+ *
+ * The typical constructors in PermissionNode require a mod id, which means our permission nodes
+ * would end up becoming {@code geyser_neoforge.} instead of just {@code }.
+ */
+ @SuppressWarnings("rawtypes") // the varargs
+ @Invoker("")
+ static PermissionNode geyser$construct(String nodeName, PermissionType type, PermissionNode.PermissionResolver defaultResolver, PermissionDynamicContextKey... dynamics) {
+ throw new IllegalStateException();
+ }
+}
diff --git a/bootstrap/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/bootstrap/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml
index fa01bb6ec..56b7d68e1 100644
--- a/bootstrap/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml
+++ b/bootstrap/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml
@@ -11,6 +11,8 @@ authors="GeyserMC"
description="${description}"
[[mixins]]
config = "geyser.mixins.json"
+[[mixins]]
+config = "geyser_neoforge.mixins.json"
[[dependencies.geyser_neoforge]]
modId="neoforge"
type="required"
diff --git a/bootstrap/mod/neoforge/src/main/resources/geyser_neoforge.mixins.json b/bootstrap/mod/neoforge/src/main/resources/geyser_neoforge.mixins.json
new file mode 100644
index 000000000..f1653051c
--- /dev/null
+++ b/bootstrap/mod/neoforge/src/main/resources/geyser_neoforge.mixins.json
@@ -0,0 +1,12 @@
+{
+ "required": true,
+ "minVersion": "0.8",
+ "package": "org.geysermc.geyser.platform.neoforge.mixin",
+ "compatibilityLevel": "JAVA_17",
+ "mixins": [
+ "PermissionNodeMixin"
+ ],
+ "injectors": {
+ "defaultRequire": 1
+ }
+}
diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModBootstrap.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModBootstrap.java
index d7373f0a9..69d6dc9a4 100644
--- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModBootstrap.java
+++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModBootstrap.java
@@ -25,30 +25,21 @@
package org.geysermc.geyser.platform.mod;
-import com.mojang.brigadier.arguments.StringArgumentType;
-import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
-import net.minecraft.commands.CommandSourceStack;
-import net.minecraft.commands.Commands;
import net.minecraft.server.MinecraftServer;
-import net.minecraft.world.entity.player.Player;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.GeyserBootstrap;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.GeyserLogger;
-import org.geysermc.geyser.api.command.Command;
-import org.geysermc.geyser.api.extension.Extension;
-import org.geysermc.geyser.command.GeyserCommand;
-import org.geysermc.geyser.command.GeyserCommandManager;
+import org.geysermc.geyser.command.CommandRegistry;
import org.geysermc.geyser.configuration.GeyserConfiguration;
import org.geysermc.geyser.dump.BootstrapDumpInfo;
import org.geysermc.geyser.level.WorldManager;
import org.geysermc.geyser.ping.GeyserLegacyPingPassthrough;
import org.geysermc.geyser.ping.IGeyserPingPassthrough;
-import org.geysermc.geyser.platform.mod.command.GeyserModCommandExecutor;
import org.geysermc.geyser.platform.mod.platform.GeyserModPlatform;
import org.geysermc.geyser.platform.mod.world.GeyserModWorldManager;
import org.geysermc.geyser.text.GeyserLocale;
@@ -59,7 +50,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.net.SocketAddress;
import java.nio.file.Path;
-import java.util.Map;
import java.util.UUID;
@RequiredArgsConstructor
@@ -70,13 +60,15 @@ public abstract class GeyserModBootstrap implements GeyserBootstrap {
private final GeyserModPlatform platform;
+ @Getter
private GeyserImpl geyser;
private Path dataFolder;
- @Setter
+ @Setter @Getter
private MinecraftServer server;
- private GeyserCommandManager geyserCommandManager;
+ @Setter
+ private CommandRegistry commandRegistry;
private GeyserModConfiguration geyserConfig;
private GeyserModInjector geyserInjector;
private final GeyserModLogger geyserLogger = new GeyserModLogger();
@@ -94,13 +86,14 @@ public abstract class GeyserModBootstrap implements GeyserBootstrap {
this.geyserLogger.setDebug(geyserConfig.isDebugMode());
GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger);
this.geyser = GeyserImpl.load(this.platform.platformType(), this);
-
- // Create command manager here, since the permission handler on neo needs it
- this.geyserCommandManager = new GeyserCommandManager(geyser);
- this.geyserCommandManager.init();
}
public void onGeyserEnable() {
+ // "Disabling" a mod isn't possible; so if we fail to initialize we need to manually stop here
+ if (geyser == null) {
+ return;
+ }
+
if (GeyserImpl.getInstance().isReloading()) {
if (!loadConfig()) {
return;
@@ -130,50 +123,6 @@ public abstract class GeyserModBootstrap implements GeyserBootstrap {
if (isServer()) {
this.geyserInjector.initializeLocalChannel(this);
}
-
- // Start command building
- // Set just "geyser" as the help command
- GeyserModCommandExecutor helpExecutor = new GeyserModCommandExecutor(geyser,
- (GeyserCommand) geyser.commandManager().getCommands().get("help"));
- LiteralArgumentBuilder builder = Commands.literal("geyser").executes(helpExecutor);
-
- // Register all subcommands as valid
- for (Map.Entry command : geyser.commandManager().getCommands().entrySet()) {
- GeyserModCommandExecutor executor = new GeyserModCommandExecutor(geyser, (GeyserCommand) command.getValue());
- builder.then(Commands.literal(command.getKey())
- .executes(executor)
- // Could also test for Bedrock but depending on when this is called it may backfire
- .requires(executor::testPermission)
- // Allows parsing of arguments; e.g. for /geyser dump logs or the connectiontest command
- .then(Commands.argument("args", StringArgumentType.greedyString())
- .executes(context -> executor.runWithArgs(context, StringArgumentType.getString(context, "args")))
- .requires(executor::testPermission)));
- }
- server.getCommands().getDispatcher().register(builder);
-
- // Register extension commands
- for (Map.Entry> extensionMapEntry : geyser.commandManager().extensionCommands().entrySet()) {
- Map extensionCommands = extensionMapEntry.getValue();
- if (extensionCommands.isEmpty()) {
- continue;
- }
-
- // Register help command for just "/"
- GeyserModCommandExecutor extensionHelpExecutor = new GeyserModCommandExecutor(geyser,
- (GeyserCommand) extensionCommands.get("help"));
- LiteralArgumentBuilder extCmdBuilder = Commands.literal(extensionMapEntry.getKey().description().id()).executes(extensionHelpExecutor);
-
- for (Map.Entry command : extensionCommands.entrySet()) {
- GeyserModCommandExecutor executor = new GeyserModCommandExecutor(geyser, (GeyserCommand) command.getValue());
- extCmdBuilder.then(Commands.literal(command.getKey())
- .executes(executor)
- .requires(executor::testPermission)
- .then(Commands.argument("args", StringArgumentType.greedyString())
- .executes(context -> executor.runWithArgs(context, StringArgumentType.getString(context, "args")))
- .requires(executor::testPermission)));
- }
- server.getCommands().getDispatcher().register(extCmdBuilder);
- }
}
@Override
@@ -206,8 +155,8 @@ public abstract class GeyserModBootstrap implements GeyserBootstrap {
}
@Override
- public GeyserCommandManager getGeyserCommandManager() {
- return geyserCommandManager;
+ public CommandRegistry getCommandRegistry() {
+ return commandRegistry;
}
@Override
@@ -235,6 +184,7 @@ public abstract class GeyserModBootstrap implements GeyserBootstrap {
return this.server.getServerVersion();
}
+ @SuppressWarnings("ConstantConditions") // Certain IDEA installations think that ip cannot be null
@NonNull
@Override
public String getServerBindAddress() {
@@ -270,10 +220,6 @@ public abstract class GeyserModBootstrap implements GeyserBootstrap {
return this.platform.resolveResource(resource);
}
- public abstract boolean hasPermission(@NonNull Player source, @NonNull String permissionNode);
-
- public abstract boolean hasPermission(@NonNull CommandSourceStack source, @NonNull String permissionNode, int permissionLevel);
-
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private boolean loadConfig() {
try {
diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModUpdateListener.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModUpdateListener.java
index 11ca0bc4f..6a724155f 100644
--- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModUpdateListener.java
+++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModUpdateListener.java
@@ -25,17 +25,18 @@
package org.geysermc.geyser.platform.mod;
-import net.minecraft.commands.CommandSourceStack;
import net.minecraft.world.entity.player.Player;
-import org.geysermc.geyser.Constants;
-import org.geysermc.geyser.platform.mod.command.ModCommandSender;
+import org.geysermc.geyser.Permissions;
+import org.geysermc.geyser.platform.mod.command.ModCommandSource;
import org.geysermc.geyser.util.VersionCheckUtils;
public final class GeyserModUpdateListener {
public static void onPlayReady(Player player) {
- CommandSourceStack stack = player.createCommandSourceStack();
- if (GeyserModBootstrap.getInstance().hasPermission(stack, Constants.UPDATE_PERMISSION, 2)) {
- VersionCheckUtils.checkForGeyserUpdate(() -> new ModCommandSender(stack));
+ // Should be creating this in the supplier, but we need it for the permission check.
+ // Not a big deal currently because ModCommandSource doesn't load locale, so don't need to try to wait for it.
+ ModCommandSource source = new ModCommandSource(player.createCommandSourceStack());
+ if (source.hasPermission(Permissions.CHECK_UPDATE)) {
+ VersionCheckUtils.checkForGeyserUpdate(() -> source);
}
}
diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/GeyserModCommandExecutor.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/GeyserModCommandExecutor.java
deleted file mode 100644
index 694dc732e..000000000
--- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/GeyserModCommandExecutor.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- *
- * @author GeyserMC
- * @link https://github.com/GeyserMC/Geyser
- */
-
-package org.geysermc.geyser.platform.mod.command;
-
-import com.mojang.brigadier.Command;
-import com.mojang.brigadier.context.CommandContext;
-import net.minecraft.commands.CommandSourceStack;
-import org.geysermc.geyser.GeyserImpl;
-import org.geysermc.geyser.command.GeyserCommand;
-import org.geysermc.geyser.command.GeyserCommandExecutor;
-import org.geysermc.geyser.platform.mod.GeyserModBootstrap;
-import org.geysermc.geyser.session.GeyserSession;
-import org.geysermc.geyser.text.ChatColor;
-import org.geysermc.geyser.text.GeyserLocale;
-
-import java.util.Collections;
-
-public class GeyserModCommandExecutor extends GeyserCommandExecutor implements Command {
- private final GeyserCommand command;
-
- public GeyserModCommandExecutor(GeyserImpl geyser, GeyserCommand command) {
- super(geyser, Collections.singletonMap(command.name(), command));
- this.command = command;
- }
-
- public boolean testPermission(CommandSourceStack source) {
- return GeyserModBootstrap.getInstance().hasPermission(source, command.permission(), command.isSuggestedOpOnly() ? 2 : 0);
- }
-
- @Override
- public int run(CommandContext context) {
- return runWithArgs(context, "");
- }
-
- public int runWithArgs(CommandContext context, String args) {
- CommandSourceStack source = context.getSource();
- ModCommandSender sender = new ModCommandSender(source);
- GeyserSession session = getGeyserSession(sender);
- if (!testPermission(source)) {
- sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", sender.locale()));
- return 0;
- }
-
- if (command.isBedrockOnly() && session == null) {
- sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.bedrock_only", sender.locale()));
- return 0;
- }
-
- command.execute(session, sender, args.split(" "));
- return 0;
- }
-}
diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/ModCommandSender.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/ModCommandSource.java
similarity index 77%
rename from bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/ModCommandSender.java
rename to bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/ModCommandSource.java
index 5bebfae93..af1f368b3 100644
--- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/ModCommandSender.java
+++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/ModCommandSource.java
@@ -31,19 +31,21 @@ import net.minecraft.core.RegistryAccess;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.command.GeyserCommandSource;
-import org.geysermc.geyser.platform.mod.GeyserModBootstrap;
import org.geysermc.geyser.text.ChatColor;
import java.util.Objects;
+import java.util.UUID;
-public class ModCommandSender implements GeyserCommandSource {
+public class ModCommandSource implements GeyserCommandSource {
private final CommandSourceStack source;
- public ModCommandSender(CommandSourceStack source) {
+ public ModCommandSource(CommandSourceStack source) {
this.source = source;
+ // todo find locale?
}
@Override
@@ -75,8 +77,24 @@ public class ModCommandSender implements GeyserCommandSource {
return !(source.getEntity() instanceof ServerPlayer);
}
+ @Override
+ public @Nullable UUID playerUuid() {
+ if (source.getEntity() instanceof ServerPlayer player) {
+ return player.getUUID();
+ }
+ return null;
+ }
+
@Override
public boolean hasPermission(String permission) {
- return GeyserModBootstrap.getInstance().hasPermission(source, permission, source.getServer().getOperatorUserPermissionLevel());
+ // Unlike other bootstraps; we delegate to cloud here too:
+ // On NeoForge; we'd have to keep track of all PermissionNodes - cloud already does that
+ // For Fabric, we won't need to include the Fabric Permissions API anymore - cloud already does that too :p
+ return GeyserImpl.getInstance().commandRegistry().hasPermission(this, permission);
+ }
+
+ @Override
+ public Object handle() {
+ return source;
}
}
diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/world/GeyserModWorldManager.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/world/GeyserModWorldManager.java
index db1768737..89452eba3 100644
--- a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/world/GeyserModWorldManager.java
+++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/world/GeyserModWorldManager.java
@@ -48,7 +48,6 @@ import org.checkerframework.checker.nullness.qual.NonNull;
import org.cloudburstmc.math.vector.Vector3i;
import org.geysermc.geyser.level.GeyserWorldManager;
import org.geysermc.geyser.network.GameProtocol;
-import org.geysermc.geyser.platform.mod.GeyserModBootstrap;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.util.MinecraftKey;
import org.geysermc.mcprotocollib.protocol.data.game.Holder;
@@ -111,12 +110,6 @@ public class GeyserModWorldManager extends GeyserWorldManager {
return SharedConstants.getCurrentVersion().getProtocolVersion() == GameProtocol.getJavaProtocolVersion();
}
- @Override
- public boolean hasPermission(GeyserSession session, String permission) {
- ServerPlayer player = getPlayer(session);
- return GeyserModBootstrap.getInstance().hasPermission(player, permission);
- }
-
@Override
public GameMode getDefaultGameMode(GeyserSession session) {
return GameMode.byId(server.getDefaultGameType().getId());
diff --git a/bootstrap/spigot/build.gradle.kts b/bootstrap/spigot/build.gradle.kts
index fcb85f100..feabfdd7a 100644
--- a/bootstrap/spigot/build.gradle.kts
+++ b/bootstrap/spigot/build.gradle.kts
@@ -1,3 +1,8 @@
+plugins {
+ id("geyser.platform-conventions")
+ id("geyser.modrinth-uploading-conventions")
+}
+
dependencies {
api(projects.core)
api(libs.erosion.bukkit.common) {
@@ -17,12 +22,12 @@ dependencies {
classifier("all") // otherwise the unshaded jar is used without the shaded NMS implementations
})
+ implementation(libs.cloud.paper)
implementation(libs.commodore)
implementation(libs.adventure.text.serializer.bungeecord)
compileOnly(libs.folia.api)
- compileOnly(libs.paper.mojangapi)
compileOnlyApi(libs.viaversion)
}
@@ -33,13 +38,15 @@ platformRelocate("com.fasterxml.jackson")
platformRelocate("net.kyori", "net.kyori.adventure.text.logger.slf4j.ComponentLogger")
platformRelocate("org.objectweb.asm")
platformRelocate("me.lucko.commodore")
+platformRelocate("org.incendo")
+platformRelocate("io.leangen.geantyref") // provided by cloud, should also be relocated
platformRelocate("org.yaml") // Broken as of 1.20
// These dependencies are already present on the platform
provided(libs.viaversion)
-application {
- mainClass.set("org.geysermc.geyser.platform.spigot.GeyserSpigotMain")
+tasks.withType {
+ manifest.attributes["Main-Class"] = "org.geysermc.geyser.platform.spigot.GeyserSpigotMain"
}
tasks.withType {
@@ -79,5 +86,7 @@ tasks.withType {
modrinth {
uploadFile.set(tasks.getByPath("shadowJar"))
+ gameVersions.addAll("1.16.5", "1.17", "1.17.1", "1.18", "1.18.1", "1.18.2", "1.19",
+ "1.19.1", "1.19.2", "1.19.3", "1.19.4", "1.20", "1.20.1", "1.20.2", "1.20.3", "1.20.4", "1.20.5", "1.20.6")
loaders.addAll("spigot", "paper")
}
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java
index 2d13155f2..a2d5c992b 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPlugin.java
@@ -30,37 +30,34 @@ import com.viaversion.viaversion.api.data.MappingData;
import com.viaversion.viaversion.api.protocol.ProtocolPathEntry;
import com.viaversion.viaversion.api.protocol.version.ProtocolVersion;
import io.netty.buffer.ByteBuf;
-import me.lucko.commodore.CommodoreProvider;
import org.bukkit.Bukkit;
import org.bukkit.block.data.BlockData;
-import org.bukkit.command.CommandMap;
-import org.bukkit.command.PluginCommand;
+import org.bukkit.command.CommandSender;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.server.ServerLoadEvent;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionDefault;
-import org.bukkit.plugin.Plugin;
+import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.checkerframework.checker.nullness.qual.NonNull;
-import org.geysermc.geyser.Constants;
import org.geysermc.geyser.GeyserBootstrap;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.adapters.paper.PaperAdapters;
import org.geysermc.geyser.adapters.spigot.SpigotAdapters;
-import org.geysermc.geyser.api.command.Command;
-import org.geysermc.geyser.api.extension.Extension;
+import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent;
import org.geysermc.geyser.api.util.PlatformType;
-import org.geysermc.geyser.command.GeyserCommandManager;
+import org.geysermc.geyser.command.CommandRegistry;
+import org.geysermc.geyser.command.CommandSourceConverter;
+import org.geysermc.geyser.command.GeyserCommandSource;
import org.geysermc.geyser.configuration.GeyserConfiguration;
import org.geysermc.geyser.dump.BootstrapDumpInfo;
import org.geysermc.geyser.level.WorldManager;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.ping.GeyserLegacyPingPassthrough;
import org.geysermc.geyser.ping.IGeyserPingPassthrough;
-import org.geysermc.geyser.platform.spigot.command.GeyserBrigadierSupport;
-import org.geysermc.geyser.platform.spigot.command.GeyserSpigotCommandExecutor;
-import org.geysermc.geyser.platform.spigot.command.GeyserSpigotCommandManager;
+import org.geysermc.geyser.platform.spigot.command.SpigotCommandRegistry;
+import org.geysermc.geyser.platform.spigot.command.SpigotCommandSource;
import org.geysermc.geyser.platform.spigot.world.GeyserPistonListener;
import org.geysermc.geyser.platform.spigot.world.GeyserSpigotBlockPlaceListener;
import org.geysermc.geyser.platform.spigot.world.manager.GeyserSpigotLegacyNativeWorldManager;
@@ -68,21 +65,21 @@ import org.geysermc.geyser.platform.spigot.world.manager.GeyserSpigotNativeWorld
import org.geysermc.geyser.platform.spigot.world.manager.GeyserSpigotWorldManager;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.util.FileUtils;
+import org.incendo.cloud.bukkit.BukkitCommandManager;
+import org.incendo.cloud.execution.ExecutionCoordinator;
+import org.incendo.cloud.paper.LegacyPaperCommandManager;
import java.io.File;
import java.io.IOException;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
import java.net.SocketAddress;
import java.nio.file.Path;
import java.util.List;
-import java.util.Map;
import java.util.Objects;
import java.util.UUID;
public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
- private GeyserSpigotCommandManager geyserCommandManager;
+ private CommandRegistry commandRegistry;
private GeyserSpigotConfiguration geyserConfig;
private GeyserSpigotInjector geyserInjector;
private final GeyserSpigotLogger geyserLogger = GeyserPaperLogger.supported() ?
@@ -120,7 +117,6 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_server.message", "1.13.2"));
geyserLogger.error("");
geyserLogger.error("*********************************************");
- Bukkit.getPluginManager().disablePlugin(this);
return;
}
@@ -134,7 +130,6 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_server_type.message", "Paper"));
geyserLogger.error("");
geyserLogger.error("*********************************************");
- Bukkit.getPluginManager().disablePlugin(this);
return;
}
}
@@ -147,10 +142,25 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
geyserLogger.error("This version of Spigot is using an outdated version of netty. Please use Paper instead!");
geyserLogger.error("");
geyserLogger.error("*********************************************");
- Bukkit.getPluginManager().disablePlugin(this);
return;
}
+ try {
+ // Check spigot config for BungeeCord mode
+ if (Bukkit.getServer().spigot().getConfig().getBoolean("settings.bungeecord")) {
+ warnInvalidProxySetups("BungeeCord");
+ return;
+ }
+
+ // Now: Check for velocity mode - deliberately after checking bungeecord because this is a paper only option
+ if (Bukkit.getServer().spigot().getPaperConfig().getBoolean("proxies.velocity.enabled")) {
+ warnInvalidProxySetups("Velocity");
+ return;
+ }
+ } catch (NoSuchMethodError e) {
+ // no-op
+ }
+
if (!loadConfig()) {
return;
}
@@ -165,31 +175,43 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
@Override
public void onEnable() {
- this.geyserCommandManager = new GeyserSpigotCommandManager(geyser);
- this.geyserCommandManager.init();
-
- // Because Bukkit locks its command map upon startup, we need to
- // add our plugin commands in onEnable, but populating the executor
- // can happen at any time (later in #onGeyserEnable())
- CommandMap commandMap = GeyserSpigotCommandManager.getCommandMap();
- for (Extension extension : this.geyserCommandManager.extensionCommands().keySet()) {
- // Thanks again, Bukkit
- try {
- Constructor constructor = PluginCommand.class.getDeclaredConstructor(String.class, Plugin.class);
- constructor.setAccessible(true);
-
- PluginCommand pluginCommand = constructor.newInstance(extension.description().id(), this);
- pluginCommand.setDescription("The main command for the " + extension.name() + " Geyser extension!");
-
- commandMap.register(extension.description().id(), "geyserext", pluginCommand);
- } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) {
- this.geyserLogger.error("Failed to construct PluginCommand for extension " + extension.name(), ex);
- }
+ // Disabling the plugin in onLoad() is not supported; we need to manually stop here and disable ourselves
+ if (geyser == null) {
+ Bukkit.getPluginManager().disablePlugin(this);
+ return;
}
+ // Register commands after Geyser initialization, but before the server starts.
+ var sourceConverter = new CommandSourceConverter<>(
+ CommandSender.class,
+ Bukkit::getPlayer,
+ Bukkit::getConsoleSender,
+ SpigotCommandSource::new
+ );
+ LegacyPaperCommandManager cloud;
+ try {
+ // LegacyPaperCommandManager works for spigot too, see https://cloud.incendo.org/minecraft/paper
+ cloud = new LegacyPaperCommandManager<>(
+ this,
+ ExecutionCoordinator.simpleCoordinator(),
+ sourceConverter
+ );
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ try {
+ // Commodore brigadier on Spigot/Paper 1.13 - 1.18.2
+ // Paper-only brigadier on 1.19+
+ cloud.registerBrigadier();
+ } catch (BukkitCommandManager.BrigadierInitializationException e) {
+ geyserLogger.debug("Failed to initialize Brigadier support: " + e.getMessage());
+ }
+
+ this.commandRegistry = new SpigotCommandRegistry(geyser, cloud);
+
// Needs to be an anonymous inner class otherwise Bukkit complains about missing classes
Bukkit.getPluginManager().registerEvents(new Listener() {
-
@EventHandler
public void onServerLoaded(ServerLoadEvent event) {
if (event.getType() == ServerLoadEvent.LoadType.RELOAD) {
@@ -227,7 +249,7 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
}
geyserLogger.debug("Spigot ping passthrough type: " + (this.geyserSpigotPingPassthrough == null ? null : this.geyserSpigotPingPassthrough.getClass()));
- // Don't need to re-create the world manager/re-register commands/reinject when reloading
+ // Don't need to re-create the world manager/reinject when reloading
if (GeyserImpl.getInstance().isReloading()) {
return;
}
@@ -282,79 +304,40 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
geyserLogger.debug("Using default world manager.");
}
- PluginCommand geyserCommand = this.getCommand("geyser");
- Objects.requireNonNull(geyserCommand, "base command cannot be null");
- geyserCommand.setExecutor(new GeyserSpigotCommandExecutor(geyser, geyserCommandManager.getCommands()));
-
- for (Map.Entry> entry : this.geyserCommandManager.extensionCommands().entrySet()) {
- Map commands = entry.getValue();
- if (commands.isEmpty()) {
- continue;
- }
-
- PluginCommand command = this.getCommand(entry.getKey().description().id());
- if (command == null) {
- continue;
- }
-
- command.setExecutor(new GeyserSpigotCommandExecutor(this.geyser, commands));
- }
-
// Register permissions so they appear in, for example, LuckPerms' UI
- // Re-registering permissions throws an error
- for (Map.Entry entry : geyserCommandManager.commands().entrySet()) {
- Command command = entry.getValue();
- if (command.aliases().contains(entry.getKey())) {
- // Don't register aliases
- continue;
+ // Re-registering permissions without removing it throws an error
+ PluginManager pluginManager = Bukkit.getPluginManager();
+ geyser.eventBus().fire((GeyserRegisterPermissionsEvent) (permission, def) -> {
+ Objects.requireNonNull(permission, "permission");
+ Objects.requireNonNull(def, "permission default for " + permission);
+
+ if (permission.isBlank()) {
+ return;
+ }
+ PermissionDefault permissionDefault = switch (def) {
+ case TRUE -> PermissionDefault.TRUE;
+ case FALSE -> PermissionDefault.FALSE;
+ case NOT_SET -> PermissionDefault.OP;
+ };
+
+ Permission existingPermission = pluginManager.getPermission(permission);
+ if (existingPermission != null) {
+ geyserLogger.debug("permission " + permission + " with default " +
+ existingPermission.getDefault() + " is being overridden by " + permissionDefault);
+
+ pluginManager.removePermission(permission);
}
- Bukkit.getPluginManager().addPermission(new Permission(command.permission(),
- GeyserLocale.getLocaleStringLog(command.description()),
- command.isSuggestedOpOnly() ? PermissionDefault.OP : PermissionDefault.TRUE));
- }
-
- // Register permissions for extension commands
- for (Map.Entry> commandEntry : this.geyserCommandManager.extensionCommands().entrySet()) {
- for (Map.Entry entry : commandEntry.getValue().entrySet()) {
- Command command = entry.getValue();
- if (command.aliases().contains(entry.getKey())) {
- // Don't register aliases
- continue;
- }
-
- if (command.permission().isBlank()) {
- continue;
- }
-
- // Avoid registering the same permission twice, e.g. for the extension help commands
- if (Bukkit.getPluginManager().getPermission(command.permission()) != null) {
- GeyserImpl.getInstance().getLogger().debug("Skipping permission " + command.permission() + " as it is already registered");
- continue;
- }
-
- Bukkit.getPluginManager().addPermission(new Permission(command.permission(),
- GeyserLocale.getLocaleStringLog(command.description()),
- command.isSuggestedOpOnly() ? PermissionDefault.OP : PermissionDefault.TRUE));
- }
- }
-
- Bukkit.getPluginManager().addPermission(new Permission(Constants.UPDATE_PERMISSION,
- "Whether update notifications can be seen", PermissionDefault.OP));
+ pluginManager.addPermission(new Permission(permission, permissionDefault));
+ });
// Events cannot be unregistered - re-registering results in duplicate firings
GeyserSpigotBlockPlaceListener blockPlaceListener = new GeyserSpigotBlockPlaceListener(geyser, this.geyserWorldManager);
- Bukkit.getServer().getPluginManager().registerEvents(blockPlaceListener, this);
+ pluginManager.registerEvents(blockPlaceListener, this);
- Bukkit.getServer().getPluginManager().registerEvents(new GeyserPistonListener(geyser, this.geyserWorldManager), this);
+ pluginManager.registerEvents(new GeyserPistonListener(geyser, this.geyserWorldManager), this);
- Bukkit.getServer().getPluginManager().registerEvents(new GeyserSpigotUpdateListener(), this);
-
- boolean brigadierSupported = CommodoreProvider.isSupported();
- geyserLogger.debug("Brigadier supported? " + brigadierSupported);
- if (brigadierSupported) {
- GeyserBrigadierSupport.loadBrigadier(this, geyserCommand);
- }
+ pluginManager.registerEvents(new GeyserSpigotUpdateListener(), this);
}
@Override
@@ -390,8 +373,8 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
}
@Override
- public GeyserCommandManager getGeyserCommandManager() {
- return this.geyserCommandManager;
+ public CommandRegistry getCommandRegistry() {
+ return this.commandRegistry;
}
@Override
@@ -494,4 +477,13 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
return true;
}
+
+ private void warnInvalidProxySetups(String platform) {
+ geyserLogger.error("*********************************************");
+ geyserLogger.error("");
+ geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_proxy_backend", platform));
+ geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.bootstrap.setup_guide", "https://geysermc.org/wiki/geyser/setup/"));
+ geyserLogger.error("");
+ geyserLogger.error("*********************************************");
+ }
}
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotUpdateListener.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotUpdateListener.java
index 5e3c4def8..8a8a43460 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotUpdateListener.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotUpdateListener.java
@@ -29,8 +29,8 @@ import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
-import org.geysermc.geyser.Constants;
import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.Permissions;
import org.geysermc.geyser.platform.spigot.command.SpigotCommandSource;
import org.geysermc.geyser.util.VersionCheckUtils;
@@ -40,7 +40,7 @@ public final class GeyserSpigotUpdateListener implements Listener {
public void onPlayerJoin(final PlayerJoinEvent event) {
if (GeyserImpl.getInstance().getConfig().isNotifyOnNewBedrockUpdate()) {
final Player player = event.getPlayer();
- if (player.hasPermission(Constants.UPDATE_PERMISSION)) {
+ if (player.hasPermission(Permissions.CHECK_UPDATE)) {
VersionCheckUtils.checkForGeyserUpdate(() -> new SpigotCommandSource(player));
}
}
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserBrigadierSupport.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserBrigadierSupport.java
deleted file mode 100644
index 61900174c..000000000
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserBrigadierSupport.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- *
- * @author GeyserMC
- * @link https://github.com/GeyserMC/Geyser
- */
-
-package org.geysermc.geyser.platform.spigot.command;
-
-import com.mojang.brigadier.builder.LiteralArgumentBuilder;
-import me.lucko.commodore.Commodore;
-import me.lucko.commodore.CommodoreProvider;
-import org.bukkit.Bukkit;
-import org.bukkit.command.PluginCommand;
-import org.geysermc.geyser.platform.spigot.GeyserSpigotPlugin;
-
-/**
- * Needs to be a separate class so pre-1.13 loads correctly.
- */
-public final class GeyserBrigadierSupport {
-
- public static void loadBrigadier(GeyserSpigotPlugin plugin, PluginCommand pluginCommand) {
- // Enable command completions if supported
- // This is beneficial because this is sent over the network and Bedrock can see it
- Commodore commodore = CommodoreProvider.getCommodore(plugin);
- LiteralArgumentBuilder> builder = LiteralArgumentBuilder.literal("geyser");
- for (String command : plugin.getGeyserCommandManager().getCommands().keySet()) {
- builder.then(LiteralArgumentBuilder.literal(command));
- }
- commodore.register(pluginCommand, builder);
-
- try {
- Class.forName("com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent");
- Bukkit.getServer().getPluginManager().registerEvents(new GeyserPaperCommandListener(), plugin);
- plugin.getGeyserLogger().debug("Successfully registered AsyncPlayerSendCommandsEvent listener.");
- } catch (ClassNotFoundException e) {
- plugin.getGeyserLogger().debug("Not registering AsyncPlayerSendCommandsEvent listener.");
- }
- }
-
- private GeyserBrigadierSupport() {
- }
-}
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserPaperCommandListener.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserPaperCommandListener.java
deleted file mode 100644
index dcec045ab..000000000
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserPaperCommandListener.java
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- *
- * @author GeyserMC
- * @link https://github.com/GeyserMC/Geyser
- */
-
-package org.geysermc.geyser.platform.spigot.command;
-
-import com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent;
-import com.mojang.brigadier.tree.CommandNode;
-import org.bukkit.entity.Player;
-import org.bukkit.event.EventHandler;
-import org.bukkit.event.Listener;
-import org.geysermc.geyser.GeyserImpl;
-import org.geysermc.geyser.api.command.Command;
-
-import java.net.InetSocketAddress;
-import java.util.Iterator;
-import java.util.Map;
-
-public final class GeyserPaperCommandListener implements Listener {
-
- @SuppressWarnings("UnstableApiUsage")
- @EventHandler
- public void onCommandSend(AsyncPlayerSendCommandsEvent> event) {
- // Documentation says to check (event.isAsynchronous() || !event.hasFiredAsync()), but as of Paper 1.18.2
- // event.hasFiredAsync is never true
- if (event.isAsynchronous()) {
- CommandNode> geyserBrigadier = event.getCommandNode().getChild("geyser");
- if (geyserBrigadier != null) {
- Player player = event.getPlayer();
- boolean isJavaPlayer = isProbablyJavaPlayer(player);
- Map commands = GeyserImpl.getInstance().commandManager().getCommands();
- Iterator extends CommandNode>> it = geyserBrigadier.getChildren().iterator();
-
- while (it.hasNext()) {
- CommandNode> subnode = it.next();
- Command command = commands.get(subnode.getName());
- if (command != null) {
- if ((command.isBedrockOnly() && isJavaPlayer) || !player.hasPermission(command.permission())) {
- // Remove this from the node as we don't have permission to use it
- it.remove();
- }
- }
- }
- }
- }
- }
-
- /**
- * This early on, there is a rare chance that Geyser has yet to process the connection. We'll try to minimize that
- * chance, though.
- */
- private boolean isProbablyJavaPlayer(Player player) {
- if (GeyserImpl.getInstance().connectionByUuid(player.getUniqueId()) != null) {
- // For sure this is a Bedrock player
- return false;
- }
-
- if (GeyserImpl.getInstance().getConfig().isUseDirectConnection()) {
- InetSocketAddress address = player.getAddress();
- if (address != null) {
- return address.getPort() != 0;
- }
- }
- return true;
- }
-}
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserSpigotCommandExecutor.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserSpigotCommandExecutor.java
deleted file mode 100644
index 6780bde17..000000000
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserSpigotCommandExecutor.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- *
- * @author GeyserMC
- * @link https://github.com/GeyserMC/Geyser
- */
-
-package org.geysermc.geyser.platform.spigot.command;
-
-import org.bukkit.ChatColor;
-import org.bukkit.command.Command;
-import org.bukkit.command.CommandSender;
-import org.bukkit.command.TabExecutor;
-import org.checkerframework.checker.nullness.qual.NonNull;
-import org.geysermc.geyser.GeyserImpl;
-import org.geysermc.geyser.command.GeyserCommand;
-import org.geysermc.geyser.command.GeyserCommandExecutor;
-import org.geysermc.geyser.session.GeyserSession;
-import org.geysermc.geyser.text.GeyserLocale;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-public class GeyserSpigotCommandExecutor extends GeyserCommandExecutor implements TabExecutor {
-
- public GeyserSpigotCommandExecutor(GeyserImpl geyser, Map commands) {
- super(geyser, commands);
- }
-
- @Override
- public boolean onCommand(@NonNull CommandSender sender, @NonNull Command command, @NonNull String label, String[] args) {
- SpigotCommandSource commandSender = new SpigotCommandSource(sender);
- GeyserSession session = getGeyserSession(commandSender);
-
- if (args.length > 0) {
- GeyserCommand geyserCommand = getCommand(args[0]);
- if (geyserCommand != null) {
- if (!sender.hasPermission(geyserCommand.permission())) {
- String message = GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", commandSender.locale());
-
- commandSender.sendMessage(ChatColor.RED + message);
- return true;
- }
- if (geyserCommand.isBedrockOnly() && session == null) {
- sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.bedrock_only", commandSender.locale()));
- return true;
- }
- geyserCommand.execute(session, commandSender, args.length > 1 ? Arrays.copyOfRange(args, 1, args.length) : new String[0]);
- return true;
- } else {
- String message = GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.not_found", commandSender.locale());
- commandSender.sendMessage(ChatColor.RED + message);
- }
- } else {
- getCommand("help").execute(session, commandSender, new String[0]);
- return true;
- }
- return true;
- }
-
- @Override
- public List onTabComplete(@NonNull CommandSender sender, @NonNull Command command, @NonNull String label, String[] args) {
- if (args.length == 1) {
- return tabComplete(new SpigotCommandSource(sender));
- }
- return Collections.emptyList();
- }
-}
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserSpigotCommandManager.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandRegistry.java
similarity index 61%
rename from bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserSpigotCommandManager.java
rename to bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandRegistry.java
index 655d3be23..39496d2c6 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserSpigotCommandManager.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandRegistry.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
+ * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -29,16 +29,21 @@ import org.bukkit.Bukkit;
import org.bukkit.Server;
import org.bukkit.command.Command;
import org.bukkit.command.CommandMap;
+import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.GeyserImpl;
-import org.geysermc.geyser.command.GeyserCommandManager;
+import org.geysermc.geyser.command.CommandRegistry;
+import org.geysermc.geyser.command.GeyserCommandSource;
+import org.incendo.cloud.CommandManager;
import java.lang.reflect.Field;
-public class GeyserSpigotCommandManager extends GeyserCommandManager {
+public class SpigotCommandRegistry extends CommandRegistry {
- private static final CommandMap COMMAND_MAP;
+ private final CommandMap commandMap;
+
+ public SpigotCommandRegistry(GeyserImpl geyser, CommandManager cloud) {
+ super(geyser, cloud);
- static {
CommandMap commandMap = null;
try {
// Paper-only
@@ -49,24 +54,28 @@ public class GeyserSpigotCommandManager extends GeyserCommandManager {
Field cmdMapField = Bukkit.getServer().getClass().getDeclaredField("commandMap");
cmdMapField.setAccessible(true);
commandMap = (CommandMap) cmdMapField.get(Bukkit.getServer());
- } catch (NoSuchFieldException | IllegalAccessException ex) {
- ex.printStackTrace();
+ } catch (Exception ex) {
+ geyser.getLogger().error("Failed to get Spigot's CommandMap", ex);
}
}
- COMMAND_MAP = commandMap;
- }
-
- public GeyserSpigotCommandManager(GeyserImpl geyser) {
- super(geyser);
+ this.commandMap = commandMap;
}
+ @NonNull
@Override
- public String description(String command) {
- Command cmd = COMMAND_MAP.getCommand(command.replace("/", ""));
- return cmd != null ? cmd.getDescription() : "";
- }
+ public String description(@NonNull String command, @NonNull String locale) {
+ // check if the command is /geyser or an extension command so that we can localize the description
+ String description = super.description(command, locale);
+ if (!description.isBlank()) {
+ return description;
+ }
- public static CommandMap getCommandMap() {
- return COMMAND_MAP;
+ if (commandMap != null) {
+ Command cmd = commandMap.getCommand(command);
+ if (cmd != null) {
+ return cmd.getDescription();
+ }
+ }
+ return "";
}
}
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandSource.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandSource.java
index 365e9ad17..c1fb837c2 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandSource.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/SpigotCommandSource.java
@@ -27,17 +27,21 @@ package org.geysermc.geyser.platform.spigot.command;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer;
+import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.entity.Player;
import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.command.GeyserCommandSource;
import org.geysermc.geyser.platform.spigot.PaperAdventure;
import org.geysermc.geyser.text.GeyserLocale;
-public class SpigotCommandSource implements GeyserCommandSource {
- private final org.bukkit.command.CommandSender handle;
+import java.util.UUID;
- public SpigotCommandSource(org.bukkit.command.CommandSender handle) {
+public class SpigotCommandSource implements GeyserCommandSource {
+ private final CommandSender handle;
+
+ public SpigotCommandSource(CommandSender handle) {
this.handle = handle;
// Ensure even Java players' languages are loaded
GeyserLocale.loadGeyserLocale(locale());
@@ -65,11 +69,24 @@ public class SpigotCommandSource implements GeyserCommandSource {
handle.spigot().sendMessage(BungeeComponentSerializer.get().serialize(message));
}
+ @Override
+ public Object handle() {
+ return handle;
+ }
+
@Override
public boolean isConsole() {
return handle instanceof ConsoleCommandSender;
}
+ @Override
+ public @Nullable UUID playerUuid() {
+ if (handle instanceof Player player) {
+ return player.getUniqueId();
+ }
+ return null;
+ }
+
@SuppressWarnings("deprecation")
@Override
public String locale() {
@@ -83,6 +100,7 @@ public class SpigotCommandSource implements GeyserCommandSource {
@Override
public boolean hasPermission(String permission) {
- return handle.hasPermission(permission);
+ // Don't trust Spigot to handle blank permissions
+ return permission.isBlank() || handle.hasPermission(permission);
}
}
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotWorldManager.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotWorldManager.java
index 73356c4e7..6588a22a3 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotWorldManager.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotWorldManager.java
@@ -128,15 +128,6 @@ public class GeyserSpigotWorldManager extends WorldManager {
return GameMode.byId(Bukkit.getDefaultGameMode().ordinal());
}
- @Override
- public boolean hasPermission(GeyserSession session, String permission) {
- Player player = Bukkit.getPlayer(session.javaUuid());
- if (player != null) {
- return player.hasPermission(permission);
- }
- return false;
- }
-
@Override
public @NonNull CompletableFuture<@Nullable DataComponents> getPickItemComponents(GeyserSession session, int x, int y, int z, boolean addNbtData) {
Player bukkitPlayer;
diff --git a/bootstrap/spigot/src/main/resources/plugin.yml b/bootstrap/spigot/src/main/resources/plugin.yml
index 6e81ccdb6..14e98f577 100644
--- a/bootstrap/spigot/src/main/resources/plugin.yml
+++ b/bootstrap/spigot/src/main/resources/plugin.yml
@@ -6,11 +6,3 @@ version: ${version}
softdepend: ["ViaVersion", "floodgate"]
api-version: 1.13
folia-supported: true
-commands:
- geyser:
- description: The main command for Geyser.
- usage: /geyser
- permission: geyser.command
-permissions:
- geyser.command:
- default: true
diff --git a/bootstrap/standalone/build.gradle.kts b/bootstrap/standalone/build.gradle.kts
index eaf895108..b210693c1 100644
--- a/bootstrap/standalone/build.gradle.kts
+++ b/bootstrap/standalone/build.gradle.kts
@@ -1,5 +1,10 @@
import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer
+plugins {
+ application
+ id("geyser.platform-conventions")
+}
+
val terminalConsoleVersion = "1.2.0"
val jlineVersion = "3.21.0"
diff --git a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java
index f289fa2ba..87fbbf0aa 100644
--- a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java
+++ b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneBootstrap.java
@@ -42,7 +42,8 @@ import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.GeyserBootstrap;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.util.PlatformType;
-import org.geysermc.geyser.command.GeyserCommandManager;
+import org.geysermc.geyser.command.CommandRegistry;
+import org.geysermc.geyser.command.standalone.StandaloneCloudCommandManager;
import org.geysermc.geyser.configuration.GeyserConfiguration;
import org.geysermc.geyser.configuration.GeyserJacksonConfiguration;
import org.geysermc.geyser.dump.BootstrapDumpInfo;
@@ -69,7 +70,8 @@ import java.util.stream.Collectors;
public class GeyserStandaloneBootstrap implements GeyserBootstrap {
- private GeyserCommandManager geyserCommandManager;
+ private StandaloneCloudCommandManager cloud;
+ private CommandRegistry commandRegistry;
private GeyserStandaloneConfiguration geyserConfig;
private final GeyserStandaloneLogger geyserLogger = new GeyserStandaloneLogger();
private IGeyserPingPassthrough geyserPingPassthrough;
@@ -222,13 +224,24 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap {
geyser = GeyserImpl.load(PlatformType.STANDALONE, this);
- geyserCommandManager = new GeyserCommandManager(geyser);
- geyserCommandManager.init();
+ boolean reloading = geyser.isReloading();
+ if (!reloading) {
+ // Currently there would be no significant benefit of re-initializing commands. Also, we would have to unsubscribe CommandRegistry.
+ // Fire GeyserDefineCommandsEvent after PreInitEvent, before PostInitEvent, for consistency with other bootstraps.
+ cloud = new StandaloneCloudCommandManager(geyser);
+ commandRegistry = new CommandRegistry(geyser, cloud);
+ }
GeyserImpl.start();
+ if (!reloading) {
+ // Event must be fired after CommandRegistry has subscribed its listener.
+ // Also, the subscription for the Permissions class is created when Geyser is initialized.
+ cloud.fireRegisterPermissionsEvent();
+ }
+
if (gui != null) {
- gui.enableCommands(geyser.getScheduledThread(), geyserCommandManager);
+ gui.enableCommands(geyser.getScheduledThread(), commandRegistry);
}
geyserPingPassthrough = GeyserLegacyPingPassthrough.init(geyser);
@@ -255,8 +268,6 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap {
@Override
public void onGeyserDisable() {
- // We can re-register commands on standalone, so why not
- GeyserImpl.getInstance().commandManager().getCommands().clear();
geyser.disable();
}
@@ -277,8 +288,8 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap {
}
@Override
- public GeyserCommandManager getGeyserCommandManager() {
- return geyserCommandManager;
+ public CommandRegistry getCommandRegistry() {
+ return commandRegistry;
}
@Override
diff --git a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneLogger.java b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneLogger.java
index 3a34920ce..21e6a5e82 100644
--- a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneLogger.java
+++ b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/GeyserStandaloneLogger.java
@@ -44,7 +44,9 @@ public class GeyserStandaloneLogger extends SimpleTerminalConsole implements Gey
@Override
protected void runCommand(String line) {
- GeyserImpl.getInstance().commandManager().runCommand(this, line);
+ // don't block the terminal!
+ GeyserImpl geyser = GeyserImpl.getInstance();
+ geyser.getScheduledThread().execute(() -> geyser.commandRegistry().runCommand(this, line));
}
@Override
diff --git a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/GeyserStandaloneGUI.java b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/GeyserStandaloneGUI.java
index b82d8cc94..4cbd178af 100644
--- a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/GeyserStandaloneGUI.java
+++ b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/GeyserStandaloneGUI.java
@@ -28,7 +28,7 @@ package org.geysermc.geyser.platform.standalone.gui;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.GeyserLogger;
-import org.geysermc.geyser.command.GeyserCommandManager;
+import org.geysermc.geyser.command.CommandRegistry;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.GeyserLocale;
@@ -271,15 +271,14 @@ public class GeyserStandaloneGUI {
}
/**
- * Enable the command input box.
+ * Enables the command input box.
*
- * @param executor the executor for running commands off the GUI thread
- * @param commandManager the command manager to delegate commands to
+ * @param executor the executor that commands will be run on
+ * @param registry the command registry containing all current commands
*/
- public void enableCommands(ScheduledExecutorService executor, GeyserCommandManager commandManager) {
+ public void enableCommands(ScheduledExecutorService executor, CommandRegistry registry) {
// we don't want to block the GUI thread with the command execution
- // todo: once cloud is used, an AsynchronousCommandExecutionCoordinator can be used to avoid this scheduler
- commandListener.handler = cmd -> executor.schedule(() -> commandManager.runCommand(logger, cmd), 0, TimeUnit.SECONDS);
+ commandListener.dispatcher = cmd -> executor.execute(() -> registry.runCommand(logger, cmd));
commandInput.setEnabled(true);
commandInput.requestFocusInWindow();
}
@@ -344,13 +343,14 @@ public class GeyserStandaloneGUI {
private class CommandListener implements ActionListener {
- private Consumer handler;
+ private Consumer dispatcher;
@Override
public void actionPerformed(ActionEvent e) {
- String command = commandInput.getText();
+ // the headless variant of Standalone strips trailing whitespace for us - we need to manually
+ String command = commandInput.getText().stripTrailing();
appendConsole(command + "\n"); // show what was run in the console
- handler.accept(command); // run the command
+ dispatcher.accept(command); // run the command
commandInput.setText(""); // clear the input
}
}
diff --git a/bootstrap/velocity/build.gradle.kts b/bootstrap/velocity/build.gradle.kts
index 4daad9784..05035e271 100644
--- a/bootstrap/velocity/build.gradle.kts
+++ b/bootstrap/velocity/build.gradle.kts
@@ -1,14 +1,22 @@
+plugins {
+ id("geyser.platform-conventions")
+ id("geyser.modrinth-uploading-conventions")
+}
+
dependencies {
annotationProcessor(libs.velocity.api)
api(projects.core)
compileOnlyApi(libs.velocity.api)
+ api(libs.cloud.velocity)
}
platformRelocate("com.fasterxml.jackson")
platformRelocate("it.unimi.dsi.fastutil")
platformRelocate("net.kyori.adventure.text.serializer.gson.legacyimpl")
platformRelocate("org.yaml")
+platformRelocate("org.incendo")
+platformRelocate("io.leangen.geantyref") // provided by cloud, should also be relocated
exclude("com.google.*:*")
@@ -38,8 +46,8 @@ exclude("net.kyori:adventure-nbt:*")
// These dependencies are already present on the platform
provided(libs.velocity.api)
-application {
- mainClass.set("org.geysermc.geyser.platform.velocity.GeyserVelocityMain")
+tasks.withType {
+ manifest.attributes["Main-Class"] = "org.geysermc.geyser.platform.velocity.GeyserVelocityMain"
}
tasks.withType {
@@ -74,4 +82,4 @@ tasks.withType {
modrinth {
uploadFile.set(tasks.getByPath("shadowJar"))
loaders.addAll("velocity")
-}
\ No newline at end of file
+}
diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPlugin.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPlugin.java
index 539bdadbf..8fa47f569 100644
--- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPlugin.java
+++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPlugin.java
@@ -26,7 +26,7 @@
package org.geysermc.geyser.platform.velocity;
import com.google.inject.Inject;
-import com.velocitypowered.api.command.CommandManager;
+import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ListenerBoundEvent;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
@@ -34,24 +34,28 @@ import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
import com.velocitypowered.api.network.ListenerType;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.plugin.Plugin;
+import com.velocitypowered.api.plugin.PluginContainer;
import com.velocitypowered.api.proxy.ProxyServer;
import lombok.Getter;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.GeyserBootstrap;
import org.geysermc.geyser.GeyserImpl;
-import org.geysermc.geyser.api.command.Command;
-import org.geysermc.geyser.api.extension.Extension;
import org.geysermc.geyser.api.util.PlatformType;
-import org.geysermc.geyser.command.GeyserCommandManager;
+import org.geysermc.geyser.command.CommandRegistry;
+import org.geysermc.geyser.command.CommandSourceConverter;
+import org.geysermc.geyser.command.GeyserCommandSource;
import org.geysermc.geyser.configuration.GeyserConfiguration;
import org.geysermc.geyser.dump.BootstrapDumpInfo;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.ping.GeyserLegacyPingPassthrough;
import org.geysermc.geyser.ping.IGeyserPingPassthrough;
-import org.geysermc.geyser.platform.velocity.command.GeyserVelocityCommandExecutor;
+import org.geysermc.geyser.platform.velocity.command.VelocityCommandSource;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.util.FileUtils;
+import org.incendo.cloud.CommandManager;
+import org.incendo.cloud.execution.ExecutionCoordinator;
+import org.incendo.cloud.velocity.VelocityCommandManager;
import org.slf4j.Logger;
import java.io.File;
@@ -59,29 +63,28 @@ import java.io.IOException;
import java.net.SocketAddress;
import java.nio.file.Path;
import java.nio.file.Paths;
-import java.util.Map;
import java.util.UUID;
@Plugin(id = "geyser", name = GeyserImpl.NAME + "-Velocity", version = GeyserImpl.VERSION, url = "https://geysermc.org", authors = "GeyserMC")
public class GeyserVelocityPlugin implements GeyserBootstrap {
private final ProxyServer proxyServer;
- private final CommandManager commandManager;
+ private final PluginContainer container;
private final GeyserVelocityLogger geyserLogger;
- private GeyserCommandManager geyserCommandManager;
private GeyserVelocityConfiguration geyserConfig;
private GeyserVelocityInjector geyserInjector;
private IGeyserPingPassthrough geyserPingPassthrough;
+ private CommandRegistry commandRegistry;
private GeyserImpl geyser;
@Getter
private final Path configFolder = Paths.get("plugins/" + GeyserImpl.NAME + "-Velocity/");
@Inject
- public GeyserVelocityPlugin(ProxyServer server, Logger logger, CommandManager manager) {
- this.geyserLogger = new GeyserVelocityLogger(logger);
+ public GeyserVelocityPlugin(ProxyServer server, PluginContainer container, Logger logger) {
this.proxyServer = server;
- this.commandManager = manager;
+ this.container = container;
+ this.geyserLogger = new GeyserVelocityLogger(logger);
}
@Override
@@ -106,19 +109,36 @@ public class GeyserVelocityPlugin implements GeyserBootstrap {
this.geyser = GeyserImpl.load(PlatformType.VELOCITY, this);
this.geyserInjector = new GeyserVelocityInjector(proxyServer);
+
+ // We need to register commands here, rather than in onGeyserEnable which is invoked during the appropriate ListenerBoundEvent.
+ // Reason: players can connect after a listener is bound, and a player join locks registration to the cloud CommandManager.
+ var sourceConverter = new CommandSourceConverter<>(
+ CommandSource.class,
+ id -> proxyServer.getPlayer(id).orElse(null),
+ proxyServer::getConsoleCommandSource,
+ VelocityCommandSource::new
+ );
+ CommandManager cloud = new VelocityCommandManager<>(
+ container,
+ proxyServer,
+ ExecutionCoordinator.simpleCoordinator(),
+ sourceConverter
+ );
+ this.commandRegistry = new CommandRegistry(geyser, cloud, false); // applying root permission would be a breaking change because we can't register permission defaults
}
@Override
public void onGeyserEnable() {
+ // If e.g. the config failed to load, GeyserImpl was not loaded and we cannot start
+ if (geyser == null) {
+ return;
+ }
if (GeyserImpl.getInstance().isReloading()) {
if (!loadConfig()) {
return;
}
this.geyserLogger.setDebug(geyserConfig.isDebugMode());
GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger);
- } else {
- this.geyserCommandManager = new GeyserCommandManager(geyser);
- this.geyserCommandManager.init();
}
GeyserImpl.start();
@@ -129,22 +149,10 @@ public class GeyserVelocityPlugin implements GeyserBootstrap {
this.geyserPingPassthrough = new GeyserVelocityPingPassthrough(proxyServer);
}
- // No need to re-register commands when reloading
- if (GeyserImpl.getInstance().isReloading()) {
- return;
+ // No need to re-register events
+ if (!GeyserImpl.getInstance().isReloading()) {
+ proxyServer.getEventManager().register(this, new GeyserVelocityUpdateListener());
}
-
- this.commandManager.register("geyser", new GeyserVelocityCommandExecutor(geyser, geyserCommandManager.getCommands()));
- for (Map.Entry> entry : this.geyserCommandManager.extensionCommands().entrySet()) {
- Map commands = entry.getValue();
- if (commands.isEmpty()) {
- continue;
- }
-
- this.commandManager.register(entry.getKey().description().id(), new GeyserVelocityCommandExecutor(this.geyser, commands));
- }
-
- proxyServer.getEventManager().register(this, new GeyserVelocityUpdateListener());
}
@Override
@@ -175,8 +183,8 @@ public class GeyserVelocityPlugin implements GeyserBootstrap {
}
@Override
- public GeyserCommandManager getGeyserCommandManager() {
- return this.geyserCommandManager;
+ public CommandRegistry getCommandRegistry() {
+ return this.commandRegistry;
}
@Override
diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityUpdateListener.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityUpdateListener.java
index 31e584612..c1c88b70d 100644
--- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityUpdateListener.java
+++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityUpdateListener.java
@@ -28,8 +28,8 @@ package org.geysermc.geyser.platform.velocity;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.PostLoginEvent;
import com.velocitypowered.api.proxy.Player;
-import org.geysermc.geyser.Constants;
import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.Permissions;
import org.geysermc.geyser.platform.velocity.command.VelocityCommandSource;
import org.geysermc.geyser.util.VersionCheckUtils;
@@ -39,7 +39,7 @@ public final class GeyserVelocityUpdateListener {
public void onPlayerJoin(PostLoginEvent event) {
if (GeyserImpl.getInstance().getConfig().isNotifyOnNewBedrockUpdate()) {
final Player player = event.getPlayer();
- if (player.hasPermission(Constants.UPDATE_PERMISSION)) {
+ if (player.hasPermission(Permissions.CHECK_UPDATE)) {
VersionCheckUtils.checkForGeyserUpdate(() -> new VelocityCommandSource(player));
}
}
diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/GeyserVelocityCommandExecutor.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/GeyserVelocityCommandExecutor.java
deleted file mode 100644
index c89c35b06..000000000
--- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/GeyserVelocityCommandExecutor.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- *
- * @author GeyserMC
- * @link https://github.com/GeyserMC/Geyser
- */
-
-package org.geysermc.geyser.platform.velocity.command;
-
-import com.velocitypowered.api.command.SimpleCommand;
-import org.geysermc.geyser.GeyserImpl;
-import org.geysermc.geyser.api.command.Command;
-import org.geysermc.geyser.command.GeyserCommand;
-import org.geysermc.geyser.command.GeyserCommandExecutor;
-import org.geysermc.geyser.command.GeyserCommandSource;
-import org.geysermc.geyser.session.GeyserSession;
-import org.geysermc.geyser.text.ChatColor;
-import org.geysermc.geyser.text.GeyserLocale;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-public class GeyserVelocityCommandExecutor extends GeyserCommandExecutor implements SimpleCommand {
-
- public GeyserVelocityCommandExecutor(GeyserImpl geyser, Map commands) {
- super(geyser, commands);
- }
-
- @Override
- public void execute(Invocation invocation) {
- GeyserCommandSource sender = new VelocityCommandSource(invocation.source());
- GeyserSession session = getGeyserSession(sender);
-
- if (invocation.arguments().length > 0) {
- GeyserCommand command = getCommand(invocation.arguments()[0]);
- if (command != null) {
- if (!invocation.source().hasPermission(getCommand(invocation.arguments()[0]).permission())) {
- sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", sender.locale()));
- return;
- }
- if (command.isBedrockOnly() && session == null) {
- sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.bedrock_only", sender.locale()));
- return;
- }
- command.execute(session, sender, invocation.arguments().length > 1 ? Arrays.copyOfRange(invocation.arguments(), 1, invocation.arguments().length) : new String[0]);
- } else {
- String message = GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.not_found", sender.locale());
- sender.sendMessage(ChatColor.RED + message);
- }
- } else {
- getCommand("help").execute(session, sender, new String[0]);
- }
- }
-
- @Override
- public List suggest(Invocation invocation) {
- // Velocity seems to do the splitting a bit differently. This results in the same behaviour in bungeecord/spigot.
- if (invocation.arguments().length == 0 || invocation.arguments().length == 1) {
- return tabComplete(new VelocityCommandSource(invocation.source()));
- }
- return Collections.emptyList();
- }
-}
diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/VelocityCommandSource.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/VelocityCommandSource.java
index 403e4cb20..2240f9988 100644
--- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/VelocityCommandSource.java
+++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/command/VelocityCommandSource.java
@@ -31,10 +31,12 @@ import com.velocitypowered.api.proxy.Player;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.command.GeyserCommandSource;
import org.geysermc.geyser.text.GeyserLocale;
import java.util.Locale;
+import java.util.UUID;
public class VelocityCommandSource implements GeyserCommandSource {
@@ -72,6 +74,14 @@ public class VelocityCommandSource implements GeyserCommandSource {
return handle instanceof ConsoleCommandSource;
}
+ @Override
+ public @Nullable UUID playerUuid() {
+ if (handle instanceof Player player) {
+ return player.getUniqueId();
+ }
+ return null;
+ }
+
@Override
public String locale() {
if (handle instanceof Player) {
@@ -83,6 +93,12 @@ public class VelocityCommandSource implements GeyserCommandSource {
@Override
public boolean hasPermission(String permission) {
- return handle.hasPermission(permission);
+ // Handle blank permissions ourselves, as velocity only handles empty ones
+ return permission.isBlank() || handle.hasPermission(permission);
+ }
+
+ @Override
+ public Object handle() {
+ return handle;
}
}
diff --git a/bootstrap/viaproxy/build.gradle.kts b/bootstrap/viaproxy/build.gradle.kts
index 6eadc790f..c13862a27 100644
--- a/bootstrap/viaproxy/build.gradle.kts
+++ b/bootstrap/viaproxy/build.gradle.kts
@@ -1,3 +1,7 @@
+plugins {
+ id("geyser.platform-conventions")
+}
+
dependencies {
api(projects.core)
@@ -8,12 +12,14 @@ platformRelocate("net.kyori")
platformRelocate("org.yaml")
platformRelocate("it.unimi.dsi.fastutil")
platformRelocate("org.cloudburstmc.netty")
+platformRelocate("org.incendo")
+platformRelocate("io.leangen.geantyref") // provided by cloud, should also be relocated
// These dependencies are already present on the platform
provided(libs.viaproxy)
-application {
- mainClass.set("org.geysermc.geyser.platform.viaproxy.GeyserViaProxyMain")
+tasks.withType {
+ manifest.attributes["Main-Class"] = "org.geysermc.geyser.platform.viaproxy.GeyserViaProxyMain"
}
tasks.withType {
diff --git a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyPlugin.java b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyPlugin.java
index bdc80335a..b5e614468 100644
--- a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyPlugin.java
+++ b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyPlugin.java
@@ -34,13 +34,15 @@ import net.raphimc.viaproxy.plugins.events.ProxyStartEvent;
import net.raphimc.viaproxy.plugins.events.ProxyStopEvent;
import net.raphimc.viaproxy.plugins.events.ShouldVerifyOnlineModeEvent;
import org.apache.logging.log4j.LogManager;
+import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.GeyserBootstrap;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.GeyserLogger;
import org.geysermc.geyser.api.event.EventRegistrar;
import org.geysermc.geyser.api.network.AuthType;
import org.geysermc.geyser.api.util.PlatformType;
-import org.geysermc.geyser.command.GeyserCommandManager;
+import org.geysermc.geyser.command.CommandRegistry;
+import org.geysermc.geyser.command.standalone.StandaloneCloudCommandManager;
import org.geysermc.geyser.configuration.GeyserConfiguration;
import org.geysermc.geyser.dump.BootstrapDumpInfo;
import org.geysermc.geyser.ping.GeyserLegacyPingPassthrough;
@@ -50,7 +52,6 @@ import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.util.FileUtils;
import org.geysermc.geyser.util.LoopbackUtil;
-import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException;
@@ -66,7 +67,8 @@ public class GeyserViaProxyPlugin extends ViaProxyPlugin implements GeyserBootst
private final GeyserViaProxyLogger logger = new GeyserViaProxyLogger(LogManager.getLogger("Geyser"));
private GeyserViaProxyConfiguration config;
private GeyserImpl geyser;
- private GeyserCommandManager commandManager;
+ private StandaloneCloudCommandManager cloud;
+ private CommandRegistry commandRegistry;
private IGeyserPingPassthrough pingPassthrough;
@Override
@@ -87,7 +89,9 @@ public class GeyserViaProxyPlugin extends ViaProxyPlugin implements GeyserBootst
@EventHandler
private void onConsoleCommand(final ConsoleCommandEvent event) {
final String command = event.getCommand().startsWith("/") ? event.getCommand().substring(1) : event.getCommand();
- if (this.getGeyserCommandManager().runCommand(this.getGeyserLogger(), command + " " + String.join(" ", event.getArgs()))) {
+ CommandRegistry registry = this.getCommandRegistry();
+ if (registry.rootCommands().contains(command)) {
+ registry.runCommand(this.getGeyserLogger(), command + " " + String.join(" ", event.getArgs()));
event.setCancelled(true);
}
}
@@ -128,21 +132,36 @@ public class GeyserViaProxyPlugin extends ViaProxyPlugin implements GeyserBootst
@Override
public void onGeyserEnable() {
- if (GeyserImpl.getInstance().isReloading()) {
+ // If e.g. the config failed to load, GeyserImpl was not loaded and we cannot start
+ if (geyser == null) {
+ return;
+ }
+ boolean reloading = geyser.isReloading();
+ if (reloading) {
if (!this.loadConfig()) {
return;
}
+ } else {
+ // Only initialized once - documented in the Geyser-Standalone bootstrap
+ this.cloud = new StandaloneCloudCommandManager(geyser);
+ this.commandRegistry = new CommandRegistry(geyser, cloud);
}
- this.commandManager = new GeyserCommandManager(this.geyser);
- this.commandManager.init();
-
GeyserImpl.start();
+ if (!reloading) {
+ // Event must be fired after CommandRegistry has subscribed its listener.
+ // Also, the subscription for the Permissions class is created when Geyser is initialized (by GeyserImpl#start)
+ this.cloud.fireRegisterPermissionsEvent();
+ }
+
if (ViaProxy.getConfig().getTargetVersion() != null && ViaProxy.getConfig().getTargetVersion().newerThanOrEqualTo(LegacyProtocolVersion.b1_8tob1_8_1)) {
// Only initialize the ping passthrough if the protocol version is above beta 1.7.3, as that's when the status protocol was added
this.pingPassthrough = GeyserLegacyPingPassthrough.init(this.geyser);
}
+ if (this.config.getRemote().authType() == AuthType.FLOODGATE) {
+ ViaProxy.getConfig().setPassthroughBungeecordPlayerInfo(true);
+ }
}
@Override
@@ -166,8 +185,8 @@ public class GeyserViaProxyPlugin extends ViaProxyPlugin implements GeyserBootst
}
@Override
- public GeyserCommandManager getGeyserCommandManager() {
- return this.commandManager;
+ public CommandRegistry getCommandRegistry() {
+ return this.commandRegistry;
}
@Override
@@ -185,7 +204,7 @@ public class GeyserViaProxyPlugin extends ViaProxyPlugin implements GeyserBootst
return new GeyserViaProxyDumpInfo();
}
- @NotNull
+ @NonNull
@Override
public String getServerBindAddress() {
if (ViaProxy.getConfig().getBindAddress() instanceof InetSocketAddress socketAddress) {
@@ -209,6 +228,7 @@ public class GeyserViaProxyPlugin extends ViaProxyPlugin implements GeyserBootst
return false;
}
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
private boolean loadConfig() {
try {
final File configFile = FileUtils.fileOrCopiedFromResource(new File(ROOT_FOLDER, "config.yml"), "config.yml", s -> s.replaceAll("generateduuid", UUID.randomUUID().toString()), this);
diff --git a/bootstrap/viaproxy/src/main/resources/viaproxy.yml b/bootstrap/viaproxy/src/main/resources/viaproxy.yml
index 66fbdb932..89fc612cd 100644
--- a/bootstrap/viaproxy/src/main/resources/viaproxy.yml
+++ b/bootstrap/viaproxy/src/main/resources/viaproxy.yml
@@ -2,4 +2,4 @@ name: "${name}-ViaProxy"
version: "${version}"
author: "${author}"
main: "org.geysermc.geyser.platform.viaproxy.GeyserViaProxyPlugin"
-min-version: "3.2.1"
+min-version: "3.3.2"
diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts
index 190386667..b87490880 100644
--- a/build-logic/build.gradle.kts
+++ b/build-logic/build.gradle.kts
@@ -12,9 +12,14 @@ repositories {
}
dependencies {
+ // This is for the LibsAccessor.kt hack
// this is OK as long as the same version catalog is used in the main build and build-logic
// see https://github.com/gradle/gradle/issues/15383#issuecomment-779893192
implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
+
+ // This is for applying plugins, and using the version from the libs.versions.toml
+ // Unfortunately they still need to be applied by their string name in the convention scripts.
+ implementation(libs.lombok)
implementation(libs.indra)
implementation(libs.shadow)
implementation(libs.architectury.plugin)
diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts
index 63bde189b..bd4560d11 100644
--- a/build-logic/settings.gradle.kts
+++ b/build-logic/settings.gradle.kts
@@ -8,4 +8,4 @@ dependencyResolutionManagement {
}
}
-rootProject.name = "build-logic"
\ No newline at end of file
+rootProject.name = "build-logic"
diff --git a/build-logic/src/main/kotlin/extensions.kt b/build-logic/src/main/kotlin/extensions.kt
index 41e11344b..1b81f6601 100644
--- a/build-logic/src/main/kotlin/extensions.kt
+++ b/build-logic/src/main/kotlin/extensions.kt
@@ -118,3 +118,12 @@ open class DownloadFilesTask : DefaultTask() {
private fun calcExclusion(section: String, bit: Int, excludedOn: Int): String =
if (excludedOn and bit > 0) section else ""
+fun projectVersion(project: Project): String =
+ project.version.toString().replace("SNAPSHOT", "b" + buildNumber())
+
+fun versionName(project: Project): String =
+ "Geyser-" + project.name.replaceFirstChar { it.uppercase() } + "-" + projectVersion(project)
+
+fun buildNumber(): Int =
+ (System.getenv("BUILD_NUMBER"))?.let { Integer.parseInt(it) } ?: -1
+
diff --git a/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts b/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts
index 950c0184b..093f0a8c0 100644
--- a/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts
+++ b/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts
@@ -3,9 +3,10 @@ plugins {
id("net.kyori.indra")
}
-dependencies {
- compileOnly("org.checkerframework", "checker-qual", "3.19.0")
-}
+val rootProperties: Map = project.rootProject.properties
+group = rootProperties["group"] as String + "." + rootProperties["id"] as String
+version = rootProperties["version"] as String
+description = rootProperties["description"] as String
indra {
github("GeyserMC", "Geyser") {
@@ -20,18 +21,52 @@ indra {
}
}
-tasks {
- processResources {
- // Spigot, BungeeCord, Velocity, Fabric, ViaProxy, NeoForge
- filesMatching(listOf("plugin.yml", "bungee.yml", "velocity-plugin.json", "fabric.mod.json", "viaproxy.yml", "META-INF/neoforge.mods.toml")) {
- expand(
- "id" to "geyser",
- "name" to "Geyser",
- "version" to project.version,
- "description" to project.description,
- "url" to "https://geysermc.org",
- "author" to "GeyserMC"
- )
- }
+dependencies {
+ compileOnly("org.checkerframework", "checker-qual", libs.checker.qual.get().version)
+}
+
+repositories {
+ // mavenLocal()
+
+ mavenCentral()
+
+ // Floodgate, Cumulus etc.
+ maven("https://repo.opencollab.dev/main")
+
+ // Paper, Velocity
+ maven("https://repo.papermc.io/repository/maven-public")
+
+ // Spigot
+ maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots") {
+ mavenContent { snapshotsOnly() }
}
-}
\ No newline at end of file
+
+ // BungeeCord
+ maven("https://oss.sonatype.org/content/repositories/snapshots") {
+ mavenContent { snapshotsOnly() }
+ }
+
+ // NeoForge
+ maven("https://maven.neoforged.net/releases") {
+ mavenContent { releasesOnly() }
+ }
+
+ // Minecraft
+ maven("https://libraries.minecraft.net") {
+ name = "minecraft"
+ mavenContent { releasesOnly() }
+ }
+
+ // ViaVersion
+ maven("https://repo.viaversion.com") {
+ name = "viaversion"
+ }
+
+ // Jitpack for e.g. MCPL
+ maven("https://jitpack.io") {
+ content { includeGroupByRegex("com\\.github\\..*") }
+ }
+
+ // For Adventure snapshots
+ maven("https://s01.oss.sonatype.org/content/repositories/snapshots/")
+}
diff --git a/build-logic/src/main/kotlin/geyser.build-logic.gradle.kts b/build-logic/src/main/kotlin/geyser.build-logic.gradle.kts
deleted file mode 100644
index e69de29bb..000000000
diff --git a/build-logic/src/main/kotlin/geyser.modded-conventions.gradle.kts b/build-logic/src/main/kotlin/geyser.modded-conventions.gradle.kts
index 7952bcf14..779d6446a 100644
--- a/build-logic/src/main/kotlin/geyser.modded-conventions.gradle.kts
+++ b/build-logic/src/main/kotlin/geyser.modded-conventions.gradle.kts
@@ -2,10 +2,9 @@
import net.fabricmc.loom.task.RemapJarTask
import org.gradle.kotlin.dsl.dependencies
-import org.gradle.kotlin.dsl.maven
plugins {
- id("geyser.publish-conventions")
+ id("geyser.platform-conventions")
id("architectury-plugin")
id("dev.architectury.loom")
}
@@ -37,6 +36,10 @@ provided("io.netty", "netty-resolver-dns")
provided("io.netty", "netty-resolver-dns-native-macos")
provided("org.ow2.asm", "asm")
+// cloud-fabric/cloud-neoforge jij's all cloud depends already
+provided("org.incendo", ".*")
+provided("io.leangen.geantyref", "geantyref")
+
architectury {
minecraft = libs.minecraft.get().version as String
}
@@ -82,7 +85,7 @@ tasks {
register("remapModrinthJar", RemapJarTask::class) {
dependsOn(shadowJar)
inputFile.set(shadowJar.get().archiveFile)
- archiveVersion.set(project.version.toString() + "+build." + System.getenv("BUILD_NUMBER"))
+ archiveVersion.set(versionName(project))
archiveClassifier.set("")
}
}
@@ -112,12 +115,3 @@ dependencies {
minecraft(libs.minecraft)
mappings(loom.officialMojangMappings())
}
-
-repositories {
- // mavenLocal()
- maven("https://repo.opencollab.dev/main")
- maven("https://jitpack.io")
- maven("https://oss.sonatype.org/content/repositories/snapshots/")
- maven("https://s01.oss.sonatype.org/content/repositories/snapshots/")
- maven("https://maven.neoforged.net/releases")
-}
\ No newline at end of file
diff --git a/build-logic/src/main/kotlin/geyser.modrinth-uploading-conventions.gradle.kts b/build-logic/src/main/kotlin/geyser.modrinth-uploading-conventions.gradle.kts
index d710ae1a2..d2e207fa4 100644
--- a/build-logic/src/main/kotlin/geyser.modrinth-uploading-conventions.gradle.kts
+++ b/build-logic/src/main/kotlin/geyser.modrinth-uploading-conventions.gradle.kts
@@ -8,11 +8,12 @@ tasks.modrinth.get().dependsOn(tasks.modrinthSyncBody)
modrinth {
token.set(System.getenv("MODRINTH_TOKEN") ?: "") // Even though this is the default value, apparently this prevents GitHub Actions caching the token?
projectId.set("geyser")
- versionNumber.set(project.version as String + "-" + System.getenv("BUILD_NUMBER"))
+ versionName.set(versionName(project))
+ versionNumber.set(projectVersion(project))
versionType.set("beta")
changelog.set(System.getenv("CHANGELOG") ?: "")
- gameVersions.add(libs.minecraft.get().version as String)
+ gameVersions.addAll("1.21", libs.minecraft.get().version as String)
failSilently.set(true)
syncBodyFrom.set(rootProject.file("README.md").readText())
-}
\ No newline at end of file
+}
diff --git a/build-logic/src/main/kotlin/geyser.platform-conventions.gradle.kts b/build-logic/src/main/kotlin/geyser.platform-conventions.gradle.kts
index 81d224906..7a342783b 100644
--- a/build-logic/src/main/kotlin/geyser.platform-conventions.gradle.kts
+++ b/build-logic/src/main/kotlin/geyser.platform-conventions.gradle.kts
@@ -1,4 +1,20 @@
plugins {
- application
id("geyser.publish-conventions")
-}
\ No newline at end of file
+ id("io.freefair.lombok")
+}
+
+tasks {
+ processResources {
+ // Spigot, BungeeCord, Velocity, Fabric, ViaProxy, NeoForge
+ filesMatching(listOf("plugin.yml", "bungee.yml", "velocity-plugin.json", "fabric.mod.json", "viaproxy.yml", "META-INF/neoforge.mods.toml")) {
+ expand(
+ "id" to "geyser",
+ "name" to "Geyser",
+ "version" to project.version,
+ "description" to project.description,
+ "url" to "https://geysermc.org",
+ "author" to "GeyserMC"
+ )
+ }
+ }
+}
diff --git a/build.gradle.kts b/build.gradle.kts
index dfbf9837f..7f700a2f6 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,55 +1,5 @@
plugins {
- `java-library`
// Ensure AP works in eclipse (no effect on other IDEs)
eclipse
- id("geyser.build-logic")
- alias(libs.plugins.lombok) apply false
+ id("geyser.base-conventions")
}
-
-allprojects {
- group = properties["group"] as String + "." + properties["id"] as String
- version = properties["version"] as String
- description = properties["description"] as String
-}
-
-val basePlatforms = setOf(
- projects.bungeecord,
- projects.spigot,
- projects.standalone,
- projects.velocity,
- projects.viaproxy
-).map { it.dependencyProject }
-
-val moddedPlatforms = setOf(
- projects.fabric,
- projects.neoforge,
- projects.mod
-).map { it.dependencyProject }
-
-val modrinthPlatforms = setOf(
- projects.bungeecord,
- projects.fabric,
- projects.neoforge,
- projects.spigot,
- projects.velocity
-).map { it.dependencyProject }
-
-subprojects {
- apply {
- plugin("java-library")
- plugin("io.freefair.lombok")
- plugin("geyser.build-logic")
- }
-
- when (this) {
- in basePlatforms -> plugins.apply("geyser.platform-conventions")
- in moddedPlatforms -> plugins.apply("geyser.modded-conventions")
- else -> plugins.apply("geyser.base-conventions")
- }
-
- // Not combined with platform-conventions as that also contains
- // platforms which we cant publish to modrinth
- if (modrinthPlatforms.contains(this)) {
- plugins.apply("geyser.modrinth-uploading-conventions")
- }
-}
\ No newline at end of file
diff --git a/common/build.gradle.kts b/common/build.gradle.kts
index efba08c8d..166ffe9f5 100644
--- a/common/build.gradle.kts
+++ b/common/build.gradle.kts
@@ -1,5 +1,6 @@
plugins {
id("geyser.publish-conventions")
+ id("io.freefair.lombok")
}
dependencies {
diff --git a/common/src/main/java/org/geysermc/floodgate/util/DeviceOs.java b/common/src/main/java/org/geysermc/floodgate/util/DeviceOs.java
index 406204759..1a92f9c40 100644
--- a/common/src/main/java/org/geysermc/floodgate/util/DeviceOs.java
+++ b/common/src/main/java/org/geysermc/floodgate/util/DeviceOs.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
+ * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -39,15 +39,19 @@ public enum DeviceOs {
OSX("macOS"),
AMAZON("Amazon"),
GEARVR("Gear VR"),
- HOLOLENS("Hololens"),
+ @Deprecated HOLOLENS("Hololens"),
UWP("Windows"),
WIN32("Windows x86"),
DEDICATED("Dedicated"),
- TVOS("Apple TV"),
- PS4("PS4"),
+ @Deprecated TVOS("Apple TV"),
+ /**
+ * This is for all PlayStation platforms not just PS4
+ */
+ PS4("PlayStation"),
NX("Switch"),
- XBOX("Xbox One"),
- WINDOWS_PHONE("Windows Phone");
+ XBOX("Xbox"),
+ @Deprecated WINDOWS_PHONE("Windows Phone"),
+ LINUX("Linux");
private static final DeviceOs[] VALUES = values();
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index b8ae8d757..b0ea5fdf6 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -3,6 +3,7 @@ plugins {
idea
alias(libs.plugins.blossom)
id("geyser.publish-conventions")
+ id("io.freefair.lombok")
}
dependencies {
@@ -51,6 +52,9 @@ dependencies {
// Adventure text serialization
api(libs.bundles.adventure)
+ // command library
+ api(libs.cloud.core)
+
api(libs.erosion.common) {
isTransitive = false
}
@@ -101,9 +105,6 @@ sourceSets {
}
}
-fun buildNumber(): Int =
- (System.getenv("BUILD_NUMBER"))?.let { Integer.parseInt(it) } ?: -1
-
fun isDevBuild(branch: String, repository: String): Boolean {
return branch != "master" || repository.equals("https://github.com/GeyserMC/Geyser", ignoreCase = true).not()
}
@@ -137,7 +138,7 @@ inner class GitInfo {
buildNumber = buildNumber()
isDev = isDevBuild(branch, repository)
- val projectVersion = if (isDev) project.version else project.version.toString().replace("SNAPSHOT", "b${buildNumber}")
+ val projectVersion = if (isDev) project.version else projectVersion(project)
version = "$projectVersion ($gitVersion)"
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/Constants.java b/core/src/main/java/org/geysermc/geyser/Constants.java
index 534cb30ad..7f00075d8 100644
--- a/core/src/main/java/org/geysermc/geyser/Constants.java
+++ b/core/src/main/java/org/geysermc/geyser/Constants.java
@@ -35,9 +35,7 @@ public final class Constants {
public static final String NEWS_PROJECT_NAME = "geyser";
public static final String FLOODGATE_DOWNLOAD_LOCATION = "https://geysermc.org/download#floodgate";
-
public static final String GEYSER_DOWNLOAD_LOCATION = "https://geysermc.org/download";
- public static final String UPDATE_PERMISSION = "geyser.update";
@Deprecated
static final String SAVED_REFRESH_TOKEN_FILE = "saved-refresh-tokens.json";
diff --git a/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java b/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java
index a9414d9d0..3063fa4f6 100644
--- a/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java
+++ b/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java
@@ -27,7 +27,7 @@ package org.geysermc.geyser;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
-import org.geysermc.geyser.command.GeyserCommandManager;
+import org.geysermc.geyser.command.CommandRegistry;
import org.geysermc.geyser.configuration.GeyserConfiguration;
import org.geysermc.geyser.dump.BootstrapDumpInfo;
import org.geysermc.geyser.level.GeyserWorldManager;
@@ -82,11 +82,11 @@ public interface GeyserBootstrap {
GeyserLogger getGeyserLogger();
/**
- * Returns the current CommandManager
+ * Returns the current CommandRegistry
*
- * @return The current CommandManager
+ * @return The current CommandRegistry
*/
- GeyserCommandManager getGeyserCommandManager();
+ CommandRegistry getCommandRegistry();
/**
* Returns the current PingPassthrough manager
diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java
index 8f88f5b6a..bc6108abf 100644
--- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java
+++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java
@@ -62,13 +62,14 @@ import org.geysermc.geyser.api.event.lifecycle.GeyserPostInitializeEvent;
import org.geysermc.geyser.api.event.lifecycle.GeyserPostReloadEvent;
import org.geysermc.geyser.api.event.lifecycle.GeyserPreInitializeEvent;
import org.geysermc.geyser.api.event.lifecycle.GeyserPreReloadEvent;
+import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent;
import org.geysermc.geyser.api.event.lifecycle.GeyserShutdownEvent;
import org.geysermc.geyser.api.network.AuthType;
import org.geysermc.geyser.api.network.BedrockListener;
import org.geysermc.geyser.api.network.RemoteServer;
import org.geysermc.geyser.api.util.MinecraftVersion;
import org.geysermc.geyser.api.util.PlatformType;
-import org.geysermc.geyser.command.GeyserCommandManager;
+import org.geysermc.geyser.command.CommandRegistry;
import org.geysermc.geyser.configuration.GeyserConfiguration;
import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.erosion.UnixSocketClientListener;
@@ -128,7 +129,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Getter
-public class GeyserImpl implements GeyserApi {
+public class GeyserImpl implements GeyserApi, EventRegistrar {
public static final ObjectMapper JSON_MAPPER = new ObjectMapper()
.enable(JsonParser.Feature.IGNORE_UNDEFINED)
.enable(JsonParser.Feature.ALLOW_COMMENTS)
@@ -155,12 +156,6 @@ public class GeyserImpl implements GeyserApi {
private final SessionManager sessionManager = new SessionManager();
- /**
- * This is used in GeyserConnect to stop the bedrock server binding to a port
- */
- @Setter
- private static boolean shouldStartListener = true;
-
private FloodgateCipher cipher;
private FloodgateSkinUploader skinUploader;
private NewsHandler newsHandler;
@@ -231,9 +226,7 @@ public class GeyserImpl implements GeyserApi {
logger.info(GeyserLocale.getLocaleStringLog("geyser.core.load", NAME, VERSION));
logger.info("");
if (IS_DEV) {
- // TODO cloud use language string
- //logger.info(GeyserLocale.getLocaleStringLog("geyser.core.dev_build", "https://discord.gg/geysermc"));
- logger.info("You are running a development build of Geyser! Please report any bugs you find on our Discord server: %s".formatted("https://discord.gg/geysermc"));
+ logger.info(GeyserLocale.getLocaleStringLog("geyser.core.dev_build", "https://discord.gg/geysermc"));
logger.info("");
}
logger.info("******************************************");
@@ -266,6 +259,9 @@ public class GeyserImpl implements GeyserApi {
CompletableFuture.runAsync(AssetUtils::downloadAndRunClientJarTasks);
});
+ // Register our general permissions when possible
+ eventBus.subscribe(this, GeyserRegisterPermissionsEvent.class, Permissions::register);
+
startInstance();
GeyserConfiguration config = bootstrap.getGeyserConfig();
@@ -367,22 +363,6 @@ public class GeyserImpl implements GeyserApi {
}
}
- String broadcastPort = System.getProperty("geyserBroadcastPort", "");
- if (!broadcastPort.isEmpty()) {
- int parsedPort;
- try {
- parsedPort = Integer.parseInt(broadcastPort);
- if (parsedPort < 1 || parsedPort > 65535) {
- throw new NumberFormatException("The broadcast port must be between 1 and 65535 inclusive!");
- }
- } catch (NumberFormatException e) {
- logger.error(String.format("Invalid broadcast port: %s! Defaulting to configured port.", broadcastPort + " (" + e.getMessage() + ")"));
- parsedPort = config.getBedrock().port();
- }
- config.getBedrock().setBroadcastPort(parsedPort);
- logger.info("Broadcast port set from system property: " + parsedPort);
- }
-
if (platformType != PlatformType.VIAPROXY) {
boolean floodgatePresent = bootstrap.testFloodgatePluginPresent();
if (config.getRemote().authType() == AuthType.FLOODGATE && !floodgatePresent) {
@@ -397,6 +377,26 @@ public class GeyserImpl implements GeyserApi {
}
}
+ // Now that the Bedrock port may have been changed, also check the broadcast port (configurable on all platforms)
+ String broadcastPort = System.getProperty("geyserBroadcastPort", "");
+ if (!broadcastPort.isEmpty()) {
+ try {
+ int parsedPort = Integer.parseInt(broadcastPort);
+ if (parsedPort < 1 || parsedPort > 65535) {
+ throw new NumberFormatException("The broadcast port must be between 1 and 65535 inclusive!");
+ }
+ config.getBedrock().setBroadcastPort(parsedPort);
+ logger.info("Broadcast port set from system property: " + parsedPort);
+ } catch (NumberFormatException e) {
+ logger.error(String.format("Invalid broadcast port from system property: %s! Defaulting to configured port.", broadcastPort + " (" + e.getMessage() + ")"));
+ }
+ }
+
+ // It's set to 0 only if no system property or manual config value was set
+ if (config.getBedrock().broadcastPort() == 0) {
+ config.getBedrock().setBroadcastPort(config.getBedrock().port());
+ }
+
String remoteAddress = config.getRemote().address();
// Filters whether it is not an IP address or localhost, because otherwise it is not possible to find out an SRV entry.
if (!remoteAddress.matches(IP_REGEX) && !remoteAddress.equalsIgnoreCase("localhost")) {
@@ -433,24 +433,27 @@ public class GeyserImpl implements GeyserApi {
bedrockThreadCount = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
}
- if (shouldStartListener) {
- this.geyserServer = new GeyserServer(this, bedrockThreadCount);
- this.geyserServer.bind(new InetSocketAddress(config.getBedrock().address(), config.getBedrock().port()))
- .whenComplete((avoid, throwable) -> {
- if (throwable == null) {
- logger.info(GeyserLocale.getLocaleStringLog("geyser.core.start", config.getBedrock().address(),
- String.valueOf(config.getBedrock().port())));
+ this.geyserServer = new GeyserServer(this, bedrockThreadCount);
+ this.geyserServer.bind(new InetSocketAddress(config.getBedrock().address(), config.getBedrock().port()))
+ .whenComplete((avoid, throwable) -> {
+ String address = config.getBedrock().address();
+ String port = String.valueOf(config.getBedrock().port()); // otherwise we get commas
+
+ if (throwable == null) {
+ if ("0.0.0.0".equals(address)) {
+ // basically just hide it in the log because some people get confused and try to change it
+ logger.info(GeyserLocale.getLocaleStringLog("geyser.core.start.ip_suppressed", port));
} else {
- String address = config.getBedrock().address();
- int port = config.getBedrock().port();
- logger.severe(GeyserLocale.getLocaleStringLog("geyser.core.fail", address, String.valueOf(port)));
- if (!"0.0.0.0".equals(address)) {
- logger.info(Component.text("Suggestion: try setting `address` under `bedrock` in the Geyser config back to 0.0.0.0", NamedTextColor.GREEN));
- logger.info(Component.text("Then, restart this server.", NamedTextColor.GREEN));
- }
+ logger.info(GeyserLocale.getLocaleStringLog("geyser.core.start", address, port));
}
- }).join();
- }
+ } else {
+ logger.severe(GeyserLocale.getLocaleStringLog("geyser.core.fail", address, port));
+ if (!"0.0.0.0".equals(address)) {
+ logger.info(Component.text("Suggestion: try setting `address` under `bedrock` in the Geyser config back to 0.0.0.0", NamedTextColor.GREEN));
+ logger.info(Component.text("Then, restart this server.", NamedTextColor.GREEN));
+ }
+ }
+ }).join();
if (config.getRemote().authType() == AuthType.FLOODGATE) {
try {
@@ -730,7 +733,6 @@ public class GeyserImpl implements GeyserApi {
if (isEnabled) {
this.disable();
}
- this.commandManager().getCommands().clear();
// Disable extensions, fire the shutdown event
this.eventBus.fire(new GeyserShutdownEvent(this.extensionManager, this.eventBus));
@@ -768,9 +770,12 @@ public class GeyserImpl implements GeyserApi {
return this.extensionManager;
}
+ /**
+ * @return the current CommandRegistry in use. The instance may change over the lifecycle of the Geyser runtime.
+ */
@NonNull
- public GeyserCommandManager commandManager() {
- return this.bootstrap.getGeyserCommandManager();
+ public CommandRegistry commandRegistry() {
+ return this.bootstrap.getCommandRegistry();
}
@Override
diff --git a/core/src/main/java/org/geysermc/geyser/GeyserLogger.java b/core/src/main/java/org/geysermc/geyser/GeyserLogger.java
index aa79e3630..f408de29c 100644
--- a/core/src/main/java/org/geysermc/geyser/GeyserLogger.java
+++ b/core/src/main/java/org/geysermc/geyser/GeyserLogger.java
@@ -30,6 +30,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.command.GeyserCommandSource;
+import java.util.UUID;
public interface GeyserLogger extends GeyserCommandSource {
@@ -129,6 +130,11 @@ public interface GeyserLogger extends GeyserCommandSource {
return true;
}
+ @Override
+ default @Nullable UUID playerUuid() {
+ return null;
+ }
+
@Override
default boolean hasPermission(String permission) {
return true;
diff --git a/core/src/main/java/org/geysermc/geyser/Permissions.java b/core/src/main/java/org/geysermc/geyser/Permissions.java
new file mode 100644
index 000000000..b65a5af7a
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/Permissions.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2024 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser;
+
+import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent;
+import org.geysermc.geyser.api.util.TriState;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Permissions related to Geyser
+ */
+public final class Permissions {
+ private static final Map PERMISSIONS = new HashMap<>();
+
+ public static final String CHECK_UPDATE = register("geyser.update");
+ public static final String SERVER_SETTINGS = register("geyser.settings.server");
+ public static final String SETTINGS_GAMERULES = register("geyser.settings.gamerules");
+
+ private Permissions() {
+ //no
+ }
+
+ private static String register(String permission) {
+ return register(permission, TriState.NOT_SET);
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private static String register(String permission, TriState permissionDefault) {
+ PERMISSIONS.put(permission, permissionDefault);
+ return permission;
+ }
+
+ public static void register(GeyserRegisterPermissionsEvent event) {
+ for (Map.Entry permission : PERMISSIONS.entrySet()) {
+ event.register(permission.getKey(), permission.getValue());
+ }
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/command/CommandRegistry.java b/core/src/main/java/org/geysermc/geyser/command/CommandRegistry.java
new file mode 100644
index 000000000..54681abea
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/command/CommandRegistry.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.command;
+
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.api.command.Command;
+import org.geysermc.geyser.api.event.EventRegistrar;
+import org.geysermc.geyser.api.event.lifecycle.GeyserDefineCommandsEvent;
+import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent;
+import org.geysermc.geyser.api.extension.Extension;
+import org.geysermc.geyser.api.util.PlatformType;
+import org.geysermc.geyser.api.util.TriState;
+import org.geysermc.geyser.command.defaults.AdvancedTooltipsCommand;
+import org.geysermc.geyser.command.defaults.AdvancementsCommand;
+import org.geysermc.geyser.command.defaults.ConnectionTestCommand;
+import org.geysermc.geyser.command.defaults.DumpCommand;
+import org.geysermc.geyser.command.defaults.ExtensionsCommand;
+import org.geysermc.geyser.command.defaults.HelpCommand;
+import org.geysermc.geyser.command.defaults.ListCommand;
+import org.geysermc.geyser.command.defaults.OffhandCommand;
+import org.geysermc.geyser.command.defaults.PingCommand;
+import org.geysermc.geyser.command.defaults.ReloadCommand;
+import org.geysermc.geyser.command.defaults.SettingsCommand;
+import org.geysermc.geyser.command.defaults.StatisticsCommand;
+import org.geysermc.geyser.command.defaults.StopCommand;
+import org.geysermc.geyser.command.defaults.VersionCommand;
+import org.geysermc.geyser.event.type.GeyserDefineCommandsEventImpl;
+import org.geysermc.geyser.extension.command.GeyserExtensionCommand;
+import org.geysermc.geyser.text.GeyserLocale;
+import org.incendo.cloud.Command.Builder;
+import org.incendo.cloud.CommandManager;
+import org.incendo.cloud.execution.ExecutionCoordinator;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.geysermc.geyser.command.GeyserCommand.DEFAULT_ROOT_COMMAND;
+
+/**
+ * Registers all built-in and extension commands to the given Cloud CommandManager.
+ *
+ * Fires {@link GeyserDefineCommandsEvent} upon construction.
+ *
+ * Subscribes to {@link GeyserRegisterPermissionsEvent} upon construction.
+ * A new instance of this class (that registers the same permissions) shouldn't be created until the previous
+ * instance is unsubscribed from the event.
+ */
+public class CommandRegistry implements EventRegistrar {
+
+ private static final String GEYSER_ROOT_PERMISSION = "geyser.command";
+
+ protected final GeyserImpl geyser;
+ private final CommandManager cloud;
+ private final boolean applyRootPermission;
+
+ /**
+ * Map of Geyser subcommands to their Commands
+ */
+ private final Map commands = new Object2ObjectOpenHashMap<>(13);
+
+ /**
+ * Map of Extensions to maps of their subcommands
+ */
+ private final Map> extensionCommands = new Object2ObjectOpenHashMap<>(0);
+
+ /**
+ * Map of root commands (that are for extensions) to Extensions
+ */
+ private final Map extensionRootCommands = new Object2ObjectOpenHashMap<>(0);
+
+ /**
+ * Map containing only permissions that have been registered with a default value
+ */
+ protected final Map permissionDefaults = new Object2ObjectOpenHashMap<>(13);
+
+ /**
+ * Creates a new CommandRegistry. Does apply a root permission. If undesired, use the other constructor.
+ */
+ public CommandRegistry(GeyserImpl geyser, CommandManager cloud) {
+ this(geyser, cloud, true);
+ }
+
+ /**
+ * Creates a new CommandRegistry
+ *
+ * @param geyser the Geyser instance
+ * @param cloud the cloud command manager to register commands to
+ * @param applyRootPermission true if this registry should apply a permission to Geyser and Extension root commands.
+ * This currently exists because we want to retain the root command permission for Spigot,
+ * but don't want to add it yet to platforms like Velocity where we cannot natively
+ * specify a permission default. Doing so will break setups as players would suddenly not
+ * have the required permission to execute any Geyser commands.
+ */
+ public CommandRegistry(GeyserImpl geyser, CommandManager cloud, boolean applyRootPermission) {
+ this.geyser = geyser;
+ this.cloud = cloud;
+ this.applyRootPermission = applyRootPermission;
+
+ // register our custom exception handlers
+ ExceptionHandlers.register(cloud);
+
+ // begin command registration
+ HelpCommand help = new HelpCommand(DEFAULT_ROOT_COMMAND, "help", "geyser.commands.help.desc", "geyser.command.help", this.commands);
+ registerBuiltInCommand(help);
+ buildRootCommand(GEYSER_ROOT_PERMISSION, help); // build root and delegate to help
+
+ registerBuiltInCommand(new ListCommand(geyser, "list", "geyser.commands.list.desc", "geyser.command.list"));
+ registerBuiltInCommand(new ReloadCommand(geyser, "reload", "geyser.commands.reload.desc", "geyser.command.reload"));
+ registerBuiltInCommand(new OffhandCommand("offhand", "geyser.commands.offhand.desc", "geyser.command.offhand"));
+ registerBuiltInCommand(new DumpCommand(geyser, "dump", "geyser.commands.dump.desc", "geyser.command.dump"));
+ registerBuiltInCommand(new VersionCommand(geyser, "version", "geyser.commands.version.desc", "geyser.command.version"));
+ registerBuiltInCommand(new SettingsCommand("settings", "geyser.commands.settings.desc", "geyser.command.settings"));
+ registerBuiltInCommand(new StatisticsCommand("statistics", "geyser.commands.statistics.desc", "geyser.command.statistics"));
+ registerBuiltInCommand(new AdvancementsCommand("advancements", "geyser.commands.advancements.desc", "geyser.command.advancements"));
+ registerBuiltInCommand(new AdvancedTooltipsCommand("tooltips", "geyser.commands.advancedtooltips.desc", "geyser.command.tooltips"));
+ registerBuiltInCommand(new ConnectionTestCommand(geyser, "connectiontest", "geyser.commands.connectiontest.desc", "geyser.command.connectiontest"));
+ registerBuiltInCommand(new PingCommand("ping", "geyser.commands.ping.desc", "geyser.command.ping"));
+ if (this.geyser.getPlatformType() == PlatformType.STANDALONE) {
+ registerBuiltInCommand(new StopCommand(geyser, "stop", "geyser.commands.stop.desc", "geyser.command.stop"));
+ }
+
+ if (!this.geyser.extensionManager().extensions().isEmpty()) {
+ registerBuiltInCommand(new ExtensionsCommand(this.geyser, "extensions", "geyser.commands.extensions.desc", "geyser.command.extensions"));
+ }
+
+ GeyserDefineCommandsEvent defineCommandsEvent = new GeyserDefineCommandsEventImpl(this.commands) {
+
+ @Override
+ public void register(@NonNull Command command) {
+ if (!(command instanceof GeyserExtensionCommand extensionCommand)) {
+ throw new IllegalArgumentException("Expected GeyserExtensionCommand as part of command registration but got " + command + "! Did you use the Command builder properly?");
+ }
+
+ registerExtensionCommand(extensionCommand.extension(), extensionCommand);
+ }
+ };
+ this.geyser.eventBus().fire(defineCommandsEvent);
+
+ // Stuff that needs to be done on a per-extension basis
+ for (Map.Entry> entry : this.extensionCommands.entrySet()) {
+ Extension extension = entry.getKey();
+
+ // Register this extension's root command
+ extensionRootCommands.put(extension.rootCommand(), extension);
+
+ // Register help commands for all extensions with commands
+ String id = extension.description().id();
+ HelpCommand extensionHelp = new HelpCommand(
+ extension.rootCommand(),
+ "help",
+ "geyser.commands.exthelp.desc",
+ "geyser.command.exthelp." + id,
+ entry.getValue()); // commands it provides help for
+
+ registerExtensionCommand(extension, extensionHelp);
+ buildRootCommand("geyser.extension." + id + ".command", extensionHelp);
+ }
+
+ // Wait for the right moment (depends on the platform) to register permissions.
+ geyser.eventBus().subscribe(this, GeyserRegisterPermissionsEvent.class, this::onRegisterPermissions);
+ }
+
+ /**
+ * @return an immutable view of the root commands registered to this command registry
+ */
+ @NonNull
+ public Collection rootCommands() {
+ return cloud.rootCommands();
+ }
+
+ /**
+ * For internal Geyser commands
+ */
+ private void registerBuiltInCommand(GeyserCommand command) {
+ register(command, this.commands);
+ }
+
+ private void registerExtensionCommand(@NonNull Extension extension, @NonNull GeyserCommand command) {
+ register(command, this.extensionCommands.computeIfAbsent(extension, e -> new HashMap<>()));
+ }
+
+ protected void register(GeyserCommand command, Map commands) {
+ String root = command.rootCommand();
+ String name = command.name();
+ if (commands.containsKey(name)) {
+ throw new IllegalArgumentException("Command with root=%s, name=%s already registered".formatted(root, name));
+ }
+
+ command.register(cloud);
+ commands.put(name, command);
+ geyser.getLogger().debug(GeyserLocale.getLocaleStringLog("geyser.commands.registered", root + " " + name));
+
+ for (String alias : command.aliases()) {
+ commands.put(alias, command);
+ }
+
+ String permission = command.permission();
+ TriState defaultValue = command.permissionDefault();
+ if (!permission.isBlank() && defaultValue != null) {
+
+ TriState existingDefault = permissionDefaults.get(permission);
+ // Extensions might be using the same permission for two different commands
+ if (existingDefault != null && existingDefault != defaultValue) {
+ geyser.getLogger().debug("Overriding permission default %s:%s with %s".formatted(permission, existingDefault, defaultValue));
+ }
+
+ permissionDefaults.put(permission, defaultValue);
+ }
+ }
+
+ /**
+ * Registers a root command to cloud that delegates to the given help command.
+ * The name of this root command is the root of the given help command.
+ *
+ * @param permission the permission of the root command. currently, it may or may not be
+ * applied depending on the platform. see below.
+ * @param help the help command to delegate to
+ */
+ private void buildRootCommand(String permission, HelpCommand help) {
+ Builder builder = cloud.commandBuilder(help.rootCommand());
+
+ if (applyRootPermission) {
+ builder = builder.permission(permission);
+ permissionDefaults.put(permission, TriState.TRUE);
+ }
+
+ cloud.command(builder.handler(context -> {
+ GeyserCommandSource source = context.sender();
+ if (!source.hasPermission(help.permission())) {
+ // delegate if possible - otherwise we have nothing else to offer the user.
+ source.sendLocaleString(ExceptionHandlers.PERMISSION_FAIL_LANG_KEY);
+ return;
+ }
+ help.execute(source);
+ }));
+ }
+
+ protected void onRegisterPermissions(GeyserRegisterPermissionsEvent event) {
+ for (Map.Entry permission : permissionDefaults.entrySet()) {
+ event.register(permission.getKey(), permission.getValue());
+ }
+ }
+
+ public boolean hasPermission(GeyserCommandSource source, String permission) {
+ // Handle blank permissions ourselves, as cloud only handles empty ones
+ return permission.isBlank() || cloud.hasPermission(source, permission);
+ }
+
+ /**
+ * Returns the description of the given command
+ *
+ * @param command the root command node
+ * @param locale the ideal locale that the description should be in
+ * @return a description if found, otherwise an empty string. The locale is not guaranteed.
+ */
+ @NonNull
+ public String description(@NonNull String command, @NonNull String locale) {
+ if (command.equals(DEFAULT_ROOT_COMMAND)) {
+ return GeyserLocale.getPlayerLocaleString("geyser.command.root.geyser", locale);
+ }
+
+ Extension extension = extensionRootCommands.get(command);
+ if (extension != null) {
+ return GeyserLocale.getPlayerLocaleString("geyser.command.root.extension", locale, extension.name());
+ }
+ return "";
+ }
+
+ /**
+ * Dispatches a command into cloud and handles any thrown exceptions.
+ * This method may or may not be blocking, depending on the {@link ExecutionCoordinator} in use by cloud.
+ */
+ public void runCommand(@NonNull GeyserCommandSource source, @NonNull String command) {
+ cloud.commandExecutor().executeCommand(source, command);
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/command/CommandSourceConverter.java b/core/src/main/java/org/geysermc/geyser/command/CommandSourceConverter.java
new file mode 100644
index 000000000..1fa5871e0
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/command/CommandSourceConverter.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.command;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.GeyserLogger;
+import org.geysermc.geyser.session.GeyserSession;
+import org.incendo.cloud.SenderMapper;
+
+import java.util.UUID;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Converts {@link GeyserCommandSource}s to the server's command sender type (and back) in a lenient manner.
+ *
+ * @param senderType class of the server command sender type
+ * @param playerLookup function for looking up a player command sender by UUID
+ * @param consoleProvider supplier of the console command sender
+ * @param commandSourceLookup supplier of the platform implementation of the {@link GeyserCommandSource}
+ * @param server command sender type
+ */
+public record CommandSourceConverter(Class senderType,
+ Function playerLookup,
+ Supplier consoleProvider,
+ Function commandSourceLookup
+) implements SenderMapper {
+
+ /**
+ * Creates a new CommandSourceConverter for a server platform
+ * in which the player type is not a command sender type, and must be mapped.
+ *
+ * @param senderType class of the command sender type
+ * @param playerLookup function for looking up a player by UUID
+ * @param senderLookup function for converting a player to a command sender
+ * @param consoleProvider supplier of the console command sender
+ * @param commandSourceLookup supplier of the platform implementation of {@link GeyserCommandSource}
+ * @return a new CommandSourceConverter
+ * @param server player type
+ * @param server command sender type
+ */
+ public static
CommandSourceConverter layered(Class senderType,
+ Function playerLookup,
+ Function senderLookup,
+ Supplier consoleProvider,
+ Function commandSourceLookup) {
+ Function lookup = uuid -> {
+ P player = playerLookup.apply(uuid);
+ if (player == null) {
+ return null;
+ }
+ return senderLookup.apply(player);
+ };
+ return new CommandSourceConverter<>(senderType, lookup, consoleProvider, commandSourceLookup);
+ }
+
+ @Override
+ public @NonNull GeyserCommandSource map(@NonNull S base) {
+ return commandSourceLookup.apply(base);
+ }
+
+ @Override
+ public @NonNull S reverse(GeyserCommandSource source) throws IllegalArgumentException {
+ Object handle = source.handle();
+ if (senderType.isInstance(handle)) {
+ return senderType.cast(handle); // one of the server platform implementations
+ }
+
+ if (source.isConsole()) {
+ return consoleProvider.get(); // one of the loggers
+ }
+
+ if (!(source instanceof GeyserSession)) {
+ GeyserLogger logger = GeyserImpl.getInstance().getLogger();
+ if (logger.isDebug()) {
+ logger.debug("Falling back to UUID for command sender lookup for a command source that is not a GeyserSession: " + source);
+ Thread.dumpStack();
+ }
+ }
+
+ // Ideally lookup should only be necessary for GeyserSession
+ UUID uuid = source.playerUuid();
+ if (uuid != null) {
+ return playerLookup.apply(uuid);
+ }
+
+ throw new IllegalArgumentException("failed to find sender for name=%s, uuid=%s".formatted(source.name(), source.playerUuid()));
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/command/ExceptionHandlers.java b/core/src/main/java/org/geysermc/geyser/command/ExceptionHandlers.java
new file mode 100644
index 000000000..45657a596
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/command/ExceptionHandlers.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.command;
+
+import io.leangen.geantyref.GenericTypeReflector;
+import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.GeyserLogger;
+import org.geysermc.geyser.text.ChatColor;
+import org.geysermc.geyser.text.GeyserLocale;
+import org.geysermc.geyser.text.MinecraftLocale;
+import org.incendo.cloud.CommandManager;
+import org.incendo.cloud.exception.ArgumentParseException;
+import org.incendo.cloud.exception.CommandExecutionException;
+import org.incendo.cloud.exception.InvalidCommandSenderException;
+import org.incendo.cloud.exception.InvalidSyntaxException;
+import org.incendo.cloud.exception.NoPermissionException;
+import org.incendo.cloud.exception.NoSuchCommandException;
+import org.incendo.cloud.exception.handling.ExceptionController;
+
+import java.lang.reflect.Type;
+import java.util.function.BiConsumer;
+
+/**
+ * Geyser's exception handlers for command execution with Cloud.
+ * Overrides Cloud's defaults so that messages can be customized to our liking: localization, etc.
+ */
+final class ExceptionHandlers {
+
+ final static String PERMISSION_FAIL_LANG_KEY = "geyser.command.permission_fail";
+
+ private final ExceptionController controller;
+
+ private ExceptionHandlers(ExceptionController controller) {
+ this.controller = controller;
+ }
+
+ /**
+ * Clears the existing handlers that are registered to the given command manager, and repopulates them.
+ *
+ * @param manager the manager whose exception handlers will be modified
+ */
+ static void register(CommandManager manager) {
+ new ExceptionHandlers(manager.exceptionController()).register();
+ }
+
+ private void register() {
+ // Yeet the default exception handlers that cloud provides so that we can perform localization.
+ controller.clearHandlers();
+
+ registerExceptionHandler(InvalidSyntaxException.class,
+ (src, e) -> src.sendLocaleString("geyser.command.invalid_syntax", e.correctSyntax()));
+
+ registerExceptionHandler(InvalidCommandSenderException.class, (src, e) -> {
+ // We currently don't use cloud sender type requirements anywhere.
+ // This can be implemented better in the future if necessary.
+ Type type = e.requiredSenderTypes().iterator().next(); // just grab the first
+ String typeString = GenericTypeReflector.getTypeName(type);
+ src.sendLocaleString("geyser.command.invalid_sender", e.commandSender().getClass().getSimpleName(), typeString);
+ });
+
+ registerExceptionHandler(NoPermissionException.class, ExceptionHandlers::handleNoPermission);
+
+ registerExceptionHandler(NoSuchCommandException.class,
+ (src, e) -> src.sendLocaleString("geyser.command.not_found"));
+
+ registerExceptionHandler(ArgumentParseException.class,
+ (src, e) -> src.sendLocaleString("geyser.command.invalid_argument", e.getCause().getMessage()));
+
+ registerExceptionHandler(CommandExecutionException.class,
+ (src, e) -> handleUnexpectedThrowable(src, e.getCause()));
+
+ registerExceptionHandler(Throwable.class,
+ (src, e) -> handleUnexpectedThrowable(src, e.getCause()));
+ }
+
+ private void registerExceptionHandler(Class type, BiConsumer handler) {
+ controller.registerHandler(type, context -> handler.accept(context.context().sender(), context.exception()));
+ }
+
+ private static void handleNoPermission(GeyserCommandSource source, NoPermissionException exception) {
+ // custom handling if the source can't use the command because of additional requirements
+ if (exception.permissionResult() instanceof GeyserPermission.Result result) {
+ if (result.meta() == GeyserPermission.Result.Meta.NOT_BEDROCK) {
+ source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.command.bedrock_only", source.locale()));
+ return;
+ }
+ if (result.meta() == GeyserPermission.Result.Meta.NOT_PLAYER) {
+ source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.command.player_only", source.locale()));
+ return;
+ }
+ } else {
+ GeyserLogger logger = GeyserImpl.getInstance().getLogger();
+ if (logger.isDebug()) {
+ logger.debug("Expected a GeyserPermission.Result for %s but instead got %s from %s".formatted(exception.currentChain(), exception.permissionResult(), exception.missingPermission()));
+ }
+ }
+
+ // Result.NO_PERMISSION or generic permission failure
+ source.sendLocaleString(PERMISSION_FAIL_LANG_KEY);
+ }
+
+ private static void handleUnexpectedThrowable(GeyserCommandSource source, Throwable throwable) {
+ source.sendMessage(MinecraftLocale.getLocaleString("command.failed", source.locale())); // java edition translation key
+ GeyserImpl.getInstance().getLogger().error("Exception while executing command handler", throwable);
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/command/GeyserCommand.java b/core/src/main/java/org/geysermc/geyser/command/GeyserCommand.java
index 47d57e73f..3cc05ca0c 100644
--- a/core/src/main/java/org/geysermc/geyser/command/GeyserCommand.java
+++ b/core/src/main/java/org/geysermc/geyser/command/GeyserCommand.java
@@ -25,65 +25,187 @@
package org.geysermc.geyser.command;
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
-import lombok.experimental.Accessors;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
-import org.geysermc.geyser.api.command.Command;
-import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent;
+import org.geysermc.geyser.api.util.TriState;
+import org.geysermc.geyser.text.GeyserLocale;
+import org.incendo.cloud.Command;
+import org.incendo.cloud.CommandManager;
+import org.incendo.cloud.context.CommandContext;
+import org.incendo.cloud.description.CommandDescription;
+import org.jetbrains.annotations.Contract;
import java.util.Collections;
import java.util.List;
-@Accessors(fluent = true)
-@Getter
-@RequiredArgsConstructor
-public abstract class GeyserCommand implements Command {
+public abstract class GeyserCommand implements org.geysermc.geyser.api.command.Command {
+ public static final String DEFAULT_ROOT_COMMAND = "geyser";
+
+ /**
+ * The second literal of the command. Note: the first literal is {@link #rootCommand()}.
+ */
+ @NonNull
+ private final String name;
- protected final String name;
/**
* The description of the command - will attempt to be translated.
*/
- protected final String description;
- protected final String permission;
-
- private List aliases = Collections.emptyList();
-
- public abstract void execute(@Nullable GeyserSession session, GeyserCommandSource sender, String[] args);
+ @NonNull
+ private final String description;
/**
- * If false, hides the command from being shown on the Geyser Standalone GUI.
- *
- * @return true if the command can be run on the server console
- */
- @Override
- public boolean isExecutableOnConsole() {
- return true;
- }
-
- /**
- * Used in the GUI to know what subcommands can be run
- *
- * @return a list of all possible subcommands, or empty if none.
+ * The permission node required to run the command, or blank if not required.
*/
@NonNull
- @Override
- public List subCommands() {
- return Collections.emptyList();
+ private final String permission;
+
+ /**
+ * The default value of the permission node.
+ * A null value indicates that the permission node should not be registered whatsoever.
+ * See {@link GeyserRegisterPermissionsEvent#register(String, TriState)} for TriState meanings.
+ */
+ @Nullable
+ private final TriState permissionDefault;
+
+ /**
+ * True if this command can be executed by players
+ */
+ private final boolean playerOnly;
+
+ /**
+ * True if this command can only be run by bedrock players
+ */
+ private final boolean bedrockOnly;
+
+ /**
+ * The aliases of the command {@link #name}. This should not be modified after construction.
+ */
+ protected List aliases = Collections.emptyList();
+
+ public GeyserCommand(@NonNull String name, @NonNull String description,
+ @NonNull String permission, @Nullable TriState permissionDefault,
+ boolean playerOnly, boolean bedrockOnly) {
+
+ if (name.isBlank()) {
+ throw new IllegalArgumentException("Command cannot be null or blank!");
+ }
+ if (permission.isBlank()) {
+ // Cloud treats empty permissions as available to everyone, but not blank permissions.
+ // When registering commands, we must convert ALL whitespace permissions into empty ones,
+ // because we cannot override permission checks that Cloud itself performs
+ permission = "";
+ permissionDefault = null;
+ }
+
+ this.name = name;
+ this.description = description;
+ this.permission = permission;
+ this.permissionDefault = permissionDefault;
+
+ if (bedrockOnly && !playerOnly) {
+ throw new IllegalArgumentException("Command cannot be bedrockOnly if it is not playerOnly");
+ }
+
+ this.playerOnly = playerOnly;
+ this.bedrockOnly = bedrockOnly;
}
- public void setAliases(List aliases) {
- this.aliases = aliases;
+ public GeyserCommand(@NonNull String name, @NonNull String description, @NonNull String permission, @Nullable TriState permissionDefault) {
+ this(name, description, permission, permissionDefault, false, false);
+ }
+
+ @NonNull
+ @Override
+ public final String name() {
+ return name;
+ }
+
+ @NonNull
+ @Override
+ public final String description() {
+ return description;
+ }
+
+ @NonNull
+ @Override
+ public final String permission() {
+ return permission;
+ }
+
+ @Nullable
+ public final TriState permissionDefault() {
+ return permissionDefault;
+ }
+
+ @Override
+ public final boolean isPlayerOnly() {
+ return playerOnly;
+ }
+
+ @Override
+ public final boolean isBedrockOnly() {
+ return bedrockOnly;
+ }
+
+ @NonNull
+ @Override
+ public final List aliases() {
+ return Collections.unmodifiableList(aliases);
}
/**
- * Used for permission defaults on server implementations.
- *
- * @return if this command is designated to be used only by server operators.
+ * @return the first (literal) argument of this command, which comes before {@link #name()}.
*/
- @Override
- public boolean isSuggestedOpOnly() {
- return false;
+ public String rootCommand() {
+ return DEFAULT_ROOT_COMMAND;
}
-}
\ No newline at end of file
+
+ /**
+ * Returns a {@link org.incendo.cloud.permission.Permission} that handles {@link #isBedrockOnly()}, {@link #isPlayerOnly()}, and {@link #permission()}.
+ *
+ * @param manager the manager to be used for permission node checking
+ * @return a permission that will properly restrict usage of this command
+ */
+ public final GeyserPermission commandPermission(CommandManager manager) {
+ return new GeyserPermission(bedrockOnly, playerOnly, permission, manager);
+ }
+
+ /**
+ * Creates a new command builder with {@link #rootCommand()}, {@link #name()}, and {@link #aliases()} built on it.
+ * A permission predicate that takes into account {@link #permission()}, {@link #isBedrockOnly()}, and {@link #isPlayerOnly()}
+ * is applied. The Applicable from {@link #meta()} is also applied to the builder.
+ */
+ @Contract(value = "_ -> new", pure = true)
+ public final Command.Builder baseBuilder(CommandManager manager) {
+ return manager.commandBuilder(rootCommand())
+ .literal(name, aliases.toArray(new String[0]))
+ .permission(commandPermission(manager))
+ .apply(meta());
+ }
+
+ /**
+ * @return an Applicable that applies this command's description
+ */
+ protected Command.Builder.Applicable meta() {
+ return builder -> builder
+ .commandDescription(CommandDescription.commandDescription(GeyserLocale.getLocaleStringLog(description))); // used in cloud-bukkit impl
+ }
+
+ /**
+ * Registers this command to the given command manager.
+ * This method may be overridden to register more than one command.
+ *
+ * The default implementation is that {@link #baseBuilder(CommandManager)} with {@link #execute(CommandContext)}
+ * applied as the handler is registered to the manager.
+ */
+ public void register(CommandManager manager) {
+ manager.command(baseBuilder(manager).handler(this::execute));
+ }
+
+ /**
+ * Executes this command
+ * @param context the context with which this command should be executed
+ */
+ public abstract void execute(CommandContext context);
+}
diff --git a/core/src/main/java/org/geysermc/geyser/command/GeyserCommandExecutor.java b/core/src/main/java/org/geysermc/geyser/command/GeyserCommandExecutor.java
deleted file mode 100644
index 37d2ef4fb..000000000
--- a/core/src/main/java/org/geysermc/geyser/command/GeyserCommandExecutor.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- *
- * @author GeyserMC
- * @link https://github.com/GeyserMC/Geyser
- */
-
-package org.geysermc.geyser.command;
-
-import lombok.AllArgsConstructor;
-import org.checkerframework.checker.nullness.qual.Nullable;
-import org.geysermc.geyser.GeyserImpl;
-import org.geysermc.geyser.api.command.Command;
-import org.geysermc.geyser.session.GeyserSession;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Represents helper functions for listening to {@code /geyser} or {@code /geyserext} commands.
- */
-@AllArgsConstructor
-public class GeyserCommandExecutor {
-
- protected final GeyserImpl geyser;
- private final Map commands;
-
- public GeyserCommand getCommand(String label) {
- return (GeyserCommand) commands.get(label);
- }
-
- @Nullable
- public GeyserSession getGeyserSession(GeyserCommandSource sender) {
- if (sender.isConsole()) {
- return null;
- }
-
- for (GeyserSession session : geyser.getSessionManager().getSessions().values()) {
- if (sender.name().equals(session.getPlayerEntity().getUsername())) {
- return session;
- }
- }
- return null;
- }
-
- /**
- * Determine which subcommands to suggest in the tab complete for the main /geyser command by a given command sender.
- *
- * @param sender The command sender to receive the tab complete suggestions.
- * If the command sender is a bedrock player, an empty list will be returned as bedrock players do not get command argument suggestions.
- * If the command sender is not a bedrock player, bedrock commands will not be shown.
- * If the command sender does not have the permission for a given command, the command will not be shown.
- * @return A list of command names to include in the tab complete
- */
- public List tabComplete(GeyserCommandSource sender) {
- if (getGeyserSession(sender) != null) {
- // Bedrock doesn't get tab completions or argument suggestions
- return Collections.emptyList();
- }
-
- List availableCommands = new ArrayList<>();
-
- // Only show commands they have permission to use
- for (Map.Entry entry : commands.entrySet()) {
- Command geyserCommand = entry.getValue();
- if (sender.hasPermission(geyserCommand.permission())) {
- if (geyserCommand.isBedrockOnly()) {
- // Don't show commands the JE player can't run
- continue;
- }
-
- availableCommands.add(entry.getKey());
- }
- }
-
- return availableCommands;
- }
-}
diff --git a/core/src/main/java/org/geysermc/geyser/command/GeyserCommandManager.java b/core/src/main/java/org/geysermc/geyser/command/GeyserCommandManager.java
deleted file mode 100644
index 72ed22381..000000000
--- a/core/src/main/java/org/geysermc/geyser/command/GeyserCommandManager.java
+++ /dev/null
@@ -1,330 +0,0 @@
-/*
- * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- *
- * @author GeyserMC
- * @link https://github.com/GeyserMC/Geyser
- */
-
-package org.geysermc.geyser.command;
-
-import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
-import lombok.Getter;
-import lombok.RequiredArgsConstructor;
-import org.checkerframework.checker.nullness.qual.NonNull;
-import org.checkerframework.checker.nullness.qual.Nullable;
-import org.geysermc.geyser.api.util.PlatformType;
-import org.geysermc.geyser.GeyserImpl;
-import org.geysermc.geyser.api.command.Command;
-import org.geysermc.geyser.api.command.CommandExecutor;
-import org.geysermc.geyser.api.command.CommandSource;
-import org.geysermc.geyser.api.event.lifecycle.GeyserDefineCommandsEvent;
-import org.geysermc.geyser.api.extension.Extension;
-import org.geysermc.geyser.command.defaults.AdvancedTooltipsCommand;
-import org.geysermc.geyser.command.defaults.AdvancementsCommand;
-import org.geysermc.geyser.command.defaults.ConnectionTestCommand;
-import org.geysermc.geyser.command.defaults.DumpCommand;
-import org.geysermc.geyser.command.defaults.ExtensionsCommand;
-import org.geysermc.geyser.command.defaults.HelpCommand;
-import org.geysermc.geyser.command.defaults.ListCommand;
-import org.geysermc.geyser.command.defaults.OffhandCommand;
-import org.geysermc.geyser.command.defaults.ReloadCommand;
-import org.geysermc.geyser.command.defaults.SettingsCommand;
-import org.geysermc.geyser.command.defaults.StatisticsCommand;
-import org.geysermc.geyser.command.defaults.StopCommand;
-import org.geysermc.geyser.command.defaults.VersionCommand;
-import org.geysermc.geyser.event.type.GeyserDefineCommandsEventImpl;
-import org.geysermc.geyser.extension.command.GeyserExtensionCommand;
-import org.geysermc.geyser.session.GeyserSession;
-import org.geysermc.geyser.text.GeyserLocale;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-
-@RequiredArgsConstructor
-public class GeyserCommandManager {
-
- @Getter
- private final Map commands = new Object2ObjectOpenHashMap<>(12);
- private final Map> extensionCommands = new Object2ObjectOpenHashMap<>(0);
-
- private final GeyserImpl geyser;
-
- public void init() {
- registerBuiltInCommand(new HelpCommand(geyser, "help", "geyser.commands.help.desc", "geyser.command.help", "geyser", this.commands));
- registerBuiltInCommand(new ListCommand(geyser, "list", "geyser.commands.list.desc", "geyser.command.list"));
- registerBuiltInCommand(new ReloadCommand(geyser, "reload", "geyser.commands.reload.desc", "geyser.command.reload"));
- registerBuiltInCommand(new OffhandCommand(geyser, "offhand", "geyser.commands.offhand.desc", "geyser.command.offhand"));
- registerBuiltInCommand(new DumpCommand(geyser, "dump", "geyser.commands.dump.desc", "geyser.command.dump"));
- registerBuiltInCommand(new VersionCommand(geyser, "version", "geyser.commands.version.desc", "geyser.command.version"));
- registerBuiltInCommand(new SettingsCommand(geyser, "settings", "geyser.commands.settings.desc", "geyser.command.settings"));
- registerBuiltInCommand(new StatisticsCommand(geyser, "statistics", "geyser.commands.statistics.desc", "geyser.command.statistics"));
- registerBuiltInCommand(new AdvancementsCommand("advancements", "geyser.commands.advancements.desc", "geyser.command.advancements"));
- registerBuiltInCommand(new AdvancedTooltipsCommand("tooltips", "geyser.commands.advancedtooltips.desc", "geyser.command.tooltips"));
- registerBuiltInCommand(new ConnectionTestCommand(geyser, "connectiontest", "geyser.commands.connectiontest.desc", "geyser.command.connectiontest"));
- if (this.geyser.getPlatformType() == PlatformType.STANDALONE) {
- registerBuiltInCommand(new StopCommand(geyser, "stop", "geyser.commands.stop.desc", "geyser.command.stop"));
- }
-
- if (!this.geyser.extensionManager().extensions().isEmpty()) {
- registerBuiltInCommand(new ExtensionsCommand(this.geyser, "extensions", "geyser.commands.extensions.desc", "geyser.command.extensions"));
- }
-
- GeyserDefineCommandsEvent defineCommandsEvent = new GeyserDefineCommandsEventImpl(this.commands) {
-
- @Override
- public void register(@NonNull Command command) {
- if (!(command instanceof GeyserExtensionCommand extensionCommand)) {
- throw new IllegalArgumentException("Expected GeyserExtensionCommand as part of command registration but got " + command + "! Did you use the Command builder properly?");
- }
-
- registerExtensionCommand(extensionCommand.extension(), extensionCommand);
- }
- };
-
- this.geyser.eventBus().fire(defineCommandsEvent);
-
- // Register help commands for all extensions with commands
- for (Map.Entry> entry : this.extensionCommands.entrySet()) {
- String id = entry.getKey().description().id();
- registerExtensionCommand(entry.getKey(), new HelpCommand(this.geyser, "help", "geyser.commands.exthelp.desc", "geyser.command.exthelp." + id, id, entry.getValue()));
- }
- }
-
- /**
- * For internal Geyser commands
- */
- public void registerBuiltInCommand(GeyserCommand command) {
- register(command, this.commands);
- }
-
- public void registerExtensionCommand(@NonNull Extension extension, @NonNull Command command) {
- register(command, this.extensionCommands.computeIfAbsent(extension, e -> new HashMap<>()));
- }
-
- private void register(Command command, Map commands) {
- commands.put(command.name(), command);
- geyser.getLogger().debug(GeyserLocale.getLocaleStringLog("geyser.commands.registered", command.name()));
-
- if (command.aliases().isEmpty()) {
- return;
- }
-
- for (String alias : command.aliases()) {
- commands.put(alias, command);
- }
- }
-
- @NonNull
- public Map commands() {
- return Collections.unmodifiableMap(this.commands);
- }
-
- @NonNull
- public Map> extensionCommands() {
- return Collections.unmodifiableMap(this.extensionCommands);
- }
-
- public boolean runCommand(GeyserCommandSource sender, String command) {
- Extension extension = null;
- for (Extension loopedExtension : this.extensionCommands.keySet()) {
- if (command.startsWith(loopedExtension.description().id() + " ")) {
- extension = loopedExtension;
- break;
- }
- }
-
- if (!command.startsWith("geyser ") && extension == null) {
- return false;
- }
-
- command = command.trim().replace(extension != null ? extension.description().id() + " " : "geyser ", "");
- String label;
- String[] args;
-
- if (!command.contains(" ")) {
- label = command.toLowerCase(Locale.ROOT);
- args = new String[0];
- } else {
- label = command.substring(0, command.indexOf(" ")).toLowerCase(Locale.ROOT);
- String argLine = command.substring(command.indexOf(" ") + 1);
- args = argLine.contains(" ") ? argLine.split(" ") : new String[] { argLine };
- }
-
- Command cmd = (extension != null ? this.extensionCommands.getOrDefault(extension, Collections.emptyMap()) : this.commands).get(label);
- if (cmd == null) {
- sender.sendMessage(GeyserLocale.getLocaleStringLog("geyser.commands.invalid"));
- return false;
- }
-
- if (cmd instanceof GeyserCommand) {
- if (sender instanceof GeyserSession) {
- ((GeyserCommand) cmd).execute((GeyserSession) sender, sender, args);
- } else {
- if (!cmd.isBedrockOnly()) {
- ((GeyserCommand) cmd).execute(null, sender, args);
- } else {
- geyser.getLogger().error(GeyserLocale.getLocaleStringLog("geyser.bootstrap.command.bedrock_only"));
- }
- }
- }
-
- return true;
- }
-
- /**
- * Returns the description of the given command
- *
- * @param command Command to get the description for
- * @return Command description
- */
- public String description(String command) {
- return "";
- }
-
- @RequiredArgsConstructor
- public static class CommandBuilder implements Command.Builder {
- private final Extension extension;
- private Class extends T> sourceType;
- private String name;
- private String description = "";
- private String permission = "";
- private List aliases;
- private boolean suggestedOpOnly = false;
- private boolean executableOnConsole = true;
- private List subCommands;
- private boolean bedrockOnly;
- private CommandExecutor executor;
-
- @Override
- public Command.Builder source(@NonNull Class extends T> sourceType) {
- this.sourceType = sourceType;
- return this;
- }
-
- public CommandBuilder name(@NonNull String name) {
- this.name = name;
- return this;
- }
-
- public CommandBuilder description(@NonNull String description) {
- this.description = description;
- return this;
- }
-
- public CommandBuilder permission(@NonNull String permission) {
- this.permission = permission;
- return this;
- }
-
- public CommandBuilder aliases(@NonNull List aliases) {
- this.aliases = aliases;
- return this;
- }
-
- @Override
- public Command.Builder suggestedOpOnly(boolean suggestedOpOnly) {
- this.suggestedOpOnly = suggestedOpOnly;
- return this;
- }
-
- public CommandBuilder executableOnConsole(boolean executableOnConsole) {
- this.executableOnConsole = executableOnConsole;
- return this;
- }
-
- public CommandBuilder subCommands(@NonNull List subCommands) {
- this.subCommands = subCommands;
- return this;
- }
-
- public CommandBuilder bedrockOnly(boolean bedrockOnly) {
- this.bedrockOnly = bedrockOnly;
- return this;
- }
-
- public CommandBuilder executor(@NonNull CommandExecutor executor) {
- this.executor = executor;
- return this;
- }
-
- @NonNull
- public GeyserExtensionCommand build() {
- if (this.name == null || this.name.isBlank()) {
- throw new IllegalArgumentException("Command cannot be null or blank!");
- }
-
- if (this.sourceType == null) {
- throw new IllegalArgumentException("Source type was not defined for command " + this.name + " in extension " + this.extension.name());
- }
-
- return new GeyserExtensionCommand(this.extension, this.name, this.description, this.permission) {
-
- @SuppressWarnings("unchecked")
- @Override
- public void execute(@Nullable GeyserSession session, GeyserCommandSource sender, String[] args) {
- Class extends T> sourceType = CommandBuilder.this.sourceType;
- CommandExecutor executor = CommandBuilder.this.executor;
- if (sourceType.isInstance(session)) {
- executor.execute((T) session, this, args);
- return;
- }
-
- if (sourceType.isInstance(sender)) {
- executor.execute((T) sender, this, args);
- return;
- }
-
- GeyserImpl.getInstance().getLogger().debug("Ignoring command " + this.name + " due to no suitable sender.");
- }
-
- @NonNull
- @Override
- public List aliases() {
- return CommandBuilder.this.aliases == null ? Collections.emptyList() : CommandBuilder.this.aliases;
- }
-
- @Override
- public boolean isSuggestedOpOnly() {
- return CommandBuilder.this.suggestedOpOnly;
- }
-
- @NonNull
- @Override
- public List subCommands() {
- return CommandBuilder.this.subCommands == null ? Collections.emptyList() : CommandBuilder.this.subCommands;
- }
-
- @Override
- public boolean isBedrockOnly() {
- return CommandBuilder.this.bedrockOnly;
- }
-
- @Override
- public boolean isExecutableOnConsole() {
- return CommandBuilder.this.executableOnConsole;
- }
- };
- }
- }
-}
diff --git a/core/src/main/java/org/geysermc/geyser/command/GeyserCommandSource.java b/core/src/main/java/org/geysermc/geyser/command/GeyserCommandSource.java
index 88d148b11..c14767496 100644
--- a/core/src/main/java/org/geysermc/geyser/command/GeyserCommandSource.java
+++ b/core/src/main/java/org/geysermc/geyser/command/GeyserCommandSource.java
@@ -25,11 +25,16 @@
package org.geysermc.geyser.command;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.command.CommandSource;
+import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.GeyserLocale;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+import java.util.UUID;
+
/**
* Implemented on top of any class that can send a command.
* For example, it wraps around Spigot's CommandSender class.
@@ -46,4 +51,29 @@ public interface GeyserCommandSource extends CommandSource {
default void sendMessage(Component message) {
sendMessage(LegacyComponentSerializer.legacySection().serialize(message));
}
+
+ default void sendLocaleString(String key, Object... values) {
+ sendMessage(GeyserLocale.getPlayerLocaleString(key, locale(), values));
+ }
+
+ default void sendLocaleString(String key) {
+ sendMessage(GeyserLocale.getPlayerLocaleString(key, locale()));
+ }
+
+ @Override
+ default @Nullable GeyserSession connection() {
+ UUID uuid = playerUuid();
+ if (uuid == null) {
+ return null;
+ }
+ return GeyserImpl.getInstance().connectionByUuid(uuid);
+ }
+
+ /**
+ * @return the underlying platform handle that this source represents.
+ * If such handle doesn't exist, this itself is returned.
+ */
+ default Object handle() {
+ return this;
+ }
}
diff --git a/core/src/main/java/org/geysermc/geyser/command/GeyserPermission.java b/core/src/main/java/org/geysermc/geyser/command/GeyserPermission.java
new file mode 100644
index 000000000..1ee677e97
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/command/GeyserPermission.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.command;
+
+import lombok.AllArgsConstructor;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.incendo.cloud.CommandManager;
+import org.incendo.cloud.key.CloudKey;
+import org.incendo.cloud.permission.Permission;
+import org.incendo.cloud.permission.PermissionResult;
+import org.incendo.cloud.permission.PredicatePermission;
+
+import static org.geysermc.geyser.command.GeyserPermission.Result.Meta;
+
+@AllArgsConstructor
+public class GeyserPermission implements PredicatePermission {
+
+ /**
+ * True if this permission requires the command source to be a bedrock player
+ */
+ private final boolean bedrockOnly;
+
+ /**
+ * True if this permission requires the command source to be any player
+ */
+ private final boolean playerOnly;
+
+ /**
+ * The permission node that the command source must have
+ */
+ private final String permission;
+
+ /**
+ * The command manager to delegate permission checks to
+ */
+ private final CommandManager manager;
+
+ @Override
+ public @NonNull Result testPermission(@NonNull GeyserCommandSource source) {
+ if (bedrockOnly) {
+ if (source.connection() == null) {
+ return new Result(Meta.NOT_BEDROCK);
+ }
+ // connection is present -> it is a player -> playerOnly is irrelevant
+ } else if (playerOnly) {
+ if (source.isConsole()) {
+ return new Result(Meta.NOT_PLAYER); // must be a player but is console
+ }
+ }
+
+ if (permission.isBlank() || manager.hasPermission(source, permission)) {
+ return new Result(Meta.ALLOWED);
+ }
+ return new Result(Meta.NO_PERMISSION);
+ }
+
+ @Override
+ public @NonNull CloudKey key() {
+ return CloudKey.cloudKey(permission);
+ }
+
+ /**
+ * Basic implementation of cloud's {@link PermissionResult} that delegates to the more informative {@link Meta}.
+ */
+ public final class Result implements PermissionResult {
+
+ private final Meta meta;
+
+ private Result(Meta meta) {
+ this.meta = meta;
+ }
+
+ public Meta meta() {
+ return meta;
+ }
+
+ @Override
+ public boolean allowed() {
+ return meta == Meta.ALLOWED;
+ }
+
+ @Override
+ public @NonNull Permission permission() {
+ return GeyserPermission.this;
+ }
+
+ /**
+ * More detailed explanation of whether the permission check passed.
+ */
+ public enum Meta {
+
+ /**
+ * The source must be a bedrock player, but is not.
+ */
+ NOT_BEDROCK,
+
+ /**
+ * The source must be a player, but is not.
+ */
+ NOT_PLAYER,
+
+ /**
+ * The source does not have a required permission node.
+ */
+ NO_PERMISSION,
+
+ /**
+ * The source meets all requirements.
+ */
+ ALLOWED
+ }
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancedTooltipsCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancedTooltipsCommand.java
index 466515b3f..75b9252da 100644
--- a/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancedTooltipsCommand.java
+++ b/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancedTooltipsCommand.java
@@ -25,33 +25,32 @@
package org.geysermc.geyser.command.defaults;
+import org.geysermc.geyser.api.util.TriState;
import org.geysermc.geyser.command.GeyserCommand;
import org.geysermc.geyser.command.GeyserCommandSource;
import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.text.MinecraftLocale;
+import org.incendo.cloud.context.CommandContext;
+
+import java.util.Objects;
public class AdvancedTooltipsCommand extends GeyserCommand {
+
public AdvancedTooltipsCommand(String name, String description, String permission) {
- super(name, description, permission);
+ super(name, description, permission, TriState.TRUE, true, true);
}
@Override
- public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) {
- if (session != null) {
- String onOrOff = session.isAdvancedTooltips() ? "off" : "on";
- session.setAdvancedTooltips(!session.isAdvancedTooltips());
- session.sendMessage("§l§e" + MinecraftLocale.getLocaleString("debug.prefix", session.locale()) + " §r" + MinecraftLocale.getLocaleString("debug.advanced_tooltips." + onOrOff, session.locale()));
- session.getInventoryTranslator().updateInventory(session, session.getPlayerInventory());
- }
- }
+ public void execute(CommandContext context) {
+ GeyserSession session = Objects.requireNonNull(context.sender().connection());
- @Override
- public boolean isExecutableOnConsole() {
- return false;
- }
-
- @Override
- public boolean isBedrockOnly() {
- return true;
+ String onOrOff = session.isAdvancedTooltips() ? "off" : "on";
+ session.setAdvancedTooltips(!session.isAdvancedTooltips());
+ session.sendMessage(ChatColor.BOLD + ChatColor.YELLOW
+ + MinecraftLocale.getLocaleString("debug.prefix", session.locale())
+ + " " + ChatColor.RESET
+ + MinecraftLocale.getLocaleString("debug.advanced_tooltips." + onOrOff, session.locale()));
+ session.getInventoryTranslator().updateInventory(session, session.getPlayerInventory());
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancementsCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancementsCommand.java
index 28253433f..0cba28f33 100644
--- a/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancementsCommand.java
+++ b/core/src/main/java/org/geysermc/geyser/command/defaults/AdvancementsCommand.java
@@ -25,29 +25,23 @@
package org.geysermc.geyser.command.defaults;
+import org.geysermc.geyser.api.util.TriState;
import org.geysermc.geyser.command.GeyserCommand;
import org.geysermc.geyser.command.GeyserCommandSource;
import org.geysermc.geyser.session.GeyserSession;
+import org.incendo.cloud.context.CommandContext;
+
+import java.util.Objects;
public class AdvancementsCommand extends GeyserCommand {
+
public AdvancementsCommand(String name, String description, String permission) {
- super(name, description, permission);
+ super(name, description, permission, TriState.TRUE, true, true);
}
@Override
- public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) {
- if (session != null) {
- session.getAdvancementsCache().buildAndShowMenuForm();
- }
- }
-
- @Override
- public boolean isExecutableOnConsole() {
- return false;
- }
-
- @Override
- public boolean isBedrockOnly() {
- return true;
+ public void execute(CommandContext context) {
+ GeyserSession session = Objects.requireNonNull(context.sender().connection());
+ session.getAdvancementsCache().buildAndShowMenuForm();
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/ConnectionTestCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/ConnectionTestCommand.java
index 981c97595..d2066dba1 100644
--- a/core/src/main/java/org/geysermc/geyser/command/defaults/ConnectionTestCommand.java
+++ b/core/src/main/java/org/geysermc/geyser/command/defaults/ConnectionTestCommand.java
@@ -26,90 +26,82 @@
package org.geysermc.geyser.command.defaults;
import com.fasterxml.jackson.databind.JsonNode;
-import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.GeyserImpl;
-import org.geysermc.geyser.api.util.PlatformType;
+import org.geysermc.geyser.api.util.TriState;
import org.geysermc.geyser.command.GeyserCommand;
import org.geysermc.geyser.command.GeyserCommandSource;
import org.geysermc.geyser.configuration.GeyserConfiguration;
-import org.geysermc.geyser.session.GeyserSession;
-import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.util.LoopbackUtil;
import org.geysermc.geyser.util.WebUtils;
+import org.incendo.cloud.CommandManager;
+import org.incendo.cloud.context.CommandContext;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
+import static org.incendo.cloud.parser.standard.IntegerParser.integerParser;
+import static org.incendo.cloud.parser.standard.StringParser.stringParser;
+
public class ConnectionTestCommand extends GeyserCommand {
+
/*
* The MOTD is temporarily changed during the connection test.
* This allows us to check if we are pinging the correct Geyser instance
*/
public static String CONNECTION_TEST_MOTD = null;
- private final GeyserImpl geyser;
+ private static final String ADDRESS = "address";
+ private static final String PORT = "port";
+ private final GeyserImpl geyser;
private final Random random = new Random();
public ConnectionTestCommand(GeyserImpl geyser, String name, String description, String permission) {
- super(name, description, permission);
+ super(name, description, permission, TriState.NOT_SET);
this.geyser = geyser;
}
@Override
- public void execute(@Nullable GeyserSession session, GeyserCommandSource sender, String[] args) {
- // Only allow the console to create dumps on Geyser Standalone
- if (!sender.isConsole() && geyser.getPlatformType() == PlatformType.STANDALONE) {
- sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", sender.locale()));
- return;
- }
+ public void register(CommandManager manager) {
+ manager.command(baseBuilder(manager)
+ .required(ADDRESS, stringParser())
+ .optional(PORT, integerParser(0, 65535))
+ .handler(this::execute));
+ }
- if (args.length == 0) {
- sender.sendMessage("Provide the server IP and port you are trying to test Bedrock connections for. Example: `test.geysermc.org:19132`");
- return;
- }
+ @Override
+ public void execute(CommandContext context) {
+ GeyserCommandSource source = context.sender();
+ String ipArgument = context.get(ADDRESS);
+ Integer portArgument = context.getOrDefault(PORT, null); // null if port was not specified
// Replace "<" and ">" symbols if they are present to avoid the common issue of people including them
- String[] fullAddress = args[0].replace("<", "").replace(">", "").split(":", 2);
-
- // Still allow people to not supply a port and fallback to 19132
- int port;
- if (fullAddress.length == 2) {
- try {
- port = Integer.parseInt(fullAddress[1]);
- } catch (NumberFormatException e) {
- // can occur if e.g. "/geyser connectiontest : is ran
- sender.sendMessage("Not a valid port! Specify a valid numeric port.");
- return;
- }
- } else {
- port = geyser.getConfig().getBedrock().broadcastPort();
- }
- String ip = fullAddress[0];
+ final String ip = ipArgument.replace("<", "").replace(">", "");
+ final int port = portArgument != null ? portArgument : geyser.getConfig().getBedrock().broadcastPort(); // default bedrock port
// Issue: people commonly checking placeholders
if (ip.equals("ip")) {
- sender.sendMessage(ip + " is not a valid IP, and instead a placeholder. Please specify the IP to check.");
+ source.sendMessage(ip + " is not a valid IP, and instead a placeholder. Please specify the IP to check.");
return;
}
// Issue: checking 0.0.0.0 won't work
if (ip.equals("0.0.0.0")) {
- sender.sendMessage("Please specify the IP that you would connect with. 0.0.0.0 in the config tells Geyser to the listen on the server's IPv4.");
+ source.sendMessage("Please specify the IP that you would connect with. 0.0.0.0 in the config tells Geyser to the listen on the server's IPv4.");
return;
}
// Issue: people testing local ip
if (ip.equals("localhost") || ip.startsWith("127.") || ip.startsWith("10.") || ip.startsWith("192.168.")) {
- sender.sendMessage("This tool checks if connections from other networks are possible, so you cannot check a local IP.");
+ source.sendMessage("This tool checks if connections from other networks are possible, so you cannot check a local IP.");
return;
}
// Issue: port out of bounds
if (port <= 0 || port >= 65535) {
- sender.sendMessage("The port you specified is invalid! Please specify a valid port.");
+ source.sendMessage("The port you specified is invalid! Please specify a valid port.");
return;
}
@@ -118,37 +110,37 @@ public class ConnectionTestCommand extends GeyserCommand {
// Issue: do the ports not line up? We only check this if players don't override the broadcast port - if they do, they (hopefully) know what they're doing
if (config.getBedrock().broadcastPort() == config.getBedrock().port()) {
if (port != config.getBedrock().port()) {
- if (fullAddress.length == 2) {
- sender.sendMessage("The port you are testing with (" + port + ") is not the same as you set in your Geyser configuration ("
+ if (portArgument != null) {
+ source.sendMessage("The port you are testing with (" + port + ") is not the same as you set in your Geyser configuration ("
+ config.getBedrock().port() + ")");
- sender.sendMessage("Re-run the command with the port in the config, or change the `bedrock` `port` in the config.");
+ source.sendMessage("Re-run the command with the port in the config, or change the `bedrock` `port` in the config.");
if (config.getBedrock().isCloneRemotePort()) {
- sender.sendMessage("You have `clone-remote-port` enabled. This option ignores the `bedrock` `port` in the config, and uses the Java server port instead.");
+ source.sendMessage("You have `clone-remote-port` enabled. This option ignores the `bedrock` `port` in the config, and uses the Java server port instead.");
}
} else {
- sender.sendMessage("You did not specify the port to check (add it with \":\"), " +
+ source.sendMessage("You did not specify the port to check (add it with \":\"), " +
"and the default port 19132 does not match the port in your Geyser configuration ("
+ config.getBedrock().port() + ")!");
- sender.sendMessage("Re-run the command with that port, or change the port in the config under `bedrock` `port`.");
+ source.sendMessage("Re-run the command with that port, or change the port in the config under `bedrock` `port`.");
}
}
} else {
if (config.getBedrock().broadcastPort() != port) {
- sender.sendMessage("The port you are testing with (" + port + ") is not the same as the broadcast port set in your Geyser configuration ("
+ source.sendMessage("The port you are testing with (" + port + ") is not the same as the broadcast port set in your Geyser configuration ("
+ config.getBedrock().broadcastPort() + "). ");
- sender.sendMessage("You ONLY need to change the broadcast port if clients connects with a port different from the port Geyser is running on.");
- sender.sendMessage("Re-run the command with the port in the config, or change the `bedrock` `broadcast-port` in the config.");
+ source.sendMessage("You ONLY need to change the broadcast port if clients connects with a port different from the port Geyser is running on.");
+ source.sendMessage("Re-run the command with the port in the config, or change the `bedrock` `broadcast-port` in the config.");
}
}
// Issue: is the `bedrock` `address` in the config different?
if (!config.getBedrock().address().equals("0.0.0.0")) {
- sender.sendMessage("The address specified in `bedrock` `address` is not \"0.0.0.0\" - this may cause issues unless this is deliberate and intentional.");
+ source.sendMessage("The address specified in `bedrock` `address` is not \"0.0.0.0\" - this may cause issues unless this is deliberate and intentional.");
}
// Issue: did someone turn on enable-proxy-protocol, and they didn't mean it?
if (config.getBedrock().isEnableProxyProtocol()) {
- sender.sendMessage("You have the `enable-proxy-protocol` setting enabled. " +
+ source.sendMessage("You have the `enable-proxy-protocol` setting enabled. " +
"Unless you're deliberately using additional software that REQUIRES this setting, you may not need it enabled.");
}
@@ -157,14 +149,14 @@ public class ConnectionTestCommand extends GeyserCommand {
// Issue: SRV record?
String[] record = WebUtils.findSrvRecord(geyser, ip);
if (record != null && !ip.equals(record[3]) && !record[2].equals(String.valueOf(port))) {
- sender.sendMessage("Bedrock Edition does not support SRV records. Try connecting to your server using the address " + record[3] + " and the port " + record[2]
+ source.sendMessage("Bedrock Edition does not support SRV records. Try connecting to your server using the address " + record[3] + " and the port " + record[2]
+ ". If that fails, re-run this command with that address and port.");
return;
}
// Issue: does Loopback need applying?
if (LoopbackUtil.needsLoopback(GeyserImpl.getInstance().getLogger())) {
- sender.sendMessage("Loopback is not applied on this computer! You will have issues connecting from the same computer. " +
+ source.sendMessage("Loopback is not applied on this computer! You will have issues connecting from the same computer. " +
"See here for steps on how to resolve: " + "https://wiki.geysermc.org/geyser/fixing-unable-to-connect-to-world/#using-geyser-on-the-same-computer");
}
@@ -178,7 +170,7 @@ public class ConnectionTestCommand extends GeyserCommand {
String connectionTestMotd = "Geyser Connection Test " + randomStr;
CONNECTION_TEST_MOTD = connectionTestMotd;
- sender.sendMessage("Testing server connection to " + ip + " with port: " + port + " now. Please wait...");
+ source.sendMessage("Testing server connection to " + ip + " with port: " + port + " now. Please wait...");
JsonNode output;
try {
String hostname = URLEncoder.encode(ip, StandardCharsets.UTF_8);
@@ -200,31 +192,31 @@ public class ConnectionTestCommand extends GeyserCommand {
JsonNode pong = ping.get("pong");
String remoteMotd = pong.get("motd").asText();
if (!connectionTestMotd.equals(remoteMotd)) {
- sender.sendMessage("The MOTD did not match when we pinged the server (we got '" + remoteMotd + "'). " +
+ source.sendMessage("The MOTD did not match when we pinged the server (we got '" + remoteMotd + "'). " +
"Did you supply the correct IP and port of your server?");
- sendLinks(sender);
+ sendLinks(source);
return;
}
if (ping.get("tcpFirst").asBoolean()) {
- sender.sendMessage("Your server hardware likely has some sort of firewall preventing people from joining easily. See https://geysermc.link/ovh-firewall for more information.");
- sendLinks(sender);
+ source.sendMessage("Your server hardware likely has some sort of firewall preventing people from joining easily. See https://geysermc.link/ovh-firewall for more information.");
+ sendLinks(source);
return;
}
- sender.sendMessage("Your server is likely online and working as of " + when + "!");
- sendLinks(sender);
+ source.sendMessage("Your server is likely online and working as of " + when + "!");
+ sendLinks(source);
return;
}
- sender.sendMessage("Your server is likely unreachable from outside the network!");
+ source.sendMessage("Your server is likely unreachable from outside the network!");
JsonNode message = output.get("message");
if (message != null && !message.asText().isEmpty()) {
- sender.sendMessage("Got the error message: " + message.asText());
+ source.sendMessage("Got the error message: " + message.asText());
}
- sendLinks(sender);
+ sendLinks(source);
} catch (Exception e) {
- sender.sendMessage("An error occurred while trying to check your connection! Check the console for more information.");
+ source.sendMessage("An error occurred while trying to check your connection! Check the console for more information.");
geyser.getLogger().error("Error while trying to check your connection!", e);
}
});
@@ -235,9 +227,4 @@ public class ConnectionTestCommand extends GeyserCommand {
"https://wiki.geysermc.org/geyser/setup/");
sender.sendMessage("If that does not work, see " + "https://wiki.geysermc.org/geyser/fixing-unable-to-connect-to-world/" + ", or contact us on Discord: " + "https://discord.gg/geysermc");
}
-
- @Override
- public boolean isSuggestedOpOnly() {
- return true;
- }
}
diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/DumpCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/DumpCommand.java
index b3fee375f..fc46f0108 100644
--- a/core/src/main/java/org/geysermc/geyser/command/defaults/DumpCommand.java
+++ b/core/src/main/java/org/geysermc/geyser/command/defaults/DumpCommand.java
@@ -29,42 +29,70 @@ import com.fasterxml.jackson.core.util.DefaultIndenter;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
-import org.checkerframework.checker.nullness.qual.NonNull;
-import org.geysermc.geyser.api.util.PlatformType;
import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.api.util.TriState;
import org.geysermc.geyser.command.GeyserCommand;
import org.geysermc.geyser.command.GeyserCommandSource;
import org.geysermc.geyser.dump.DumpInfo;
-import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.AsteriskSerializer;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.util.WebUtils;
+import org.incendo.cloud.CommandManager;
+import org.incendo.cloud.context.CommandContext;
+import org.incendo.cloud.suggestion.SuggestionProvider;
import java.io.FileOutputStream;
import java.io.IOException;
-import java.util.Arrays;
+import java.util.ArrayList;
import java.util.List;
+import static org.incendo.cloud.parser.standard.StringArrayParser.stringArrayParser;
+
public class DumpCommand extends GeyserCommand {
+ private static final String ARGUMENTS = "args";
+ private static final Iterable SUGGESTIONS = List.of("full", "offline", "logs");
+
private final GeyserImpl geyser;
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final String DUMP_URL = "https://dump.geysermc.org/";
public DumpCommand(GeyserImpl geyser, String name, String description, String permission) {
- super(name, description, permission);
-
+ super(name, description, permission, TriState.NOT_SET);
this.geyser = geyser;
}
@Override
- public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) {
- // Only allow the console to create dumps on Geyser Standalone
- if (!sender.isConsole() && geyser.getPlatformType() == PlatformType.STANDALONE) {
- sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", sender.locale()));
- return;
- }
+ public void register(CommandManager manager) {
+ manager.command(baseBuilder(manager)
+ .optional(ARGUMENTS, stringArrayParser(), SuggestionProvider.blockingStrings((ctx, input) -> {
+ // parse suggestions here
+ List inputs = new ArrayList<>();
+ while (input.hasRemainingInput()) {
+ inputs.add(input.readStringSkipWhitespace());
+ }
+
+ if (inputs.size() <= 2) {
+ return SUGGESTIONS; // only `geyser dump` was typed (2 literals)
+ }
+
+ // the rest of the input after `geyser dump` is for this argument
+ inputs = inputs.subList(2, inputs.size());
+
+ // don't suggest any words they have already typed
+ List suggestions = new ArrayList<>();
+ SUGGESTIONS.forEach(suggestions::add);
+ suggestions.removeAll(inputs);
+ return suggestions;
+ }))
+ .handler(this::execute));
+ }
+
+ @Override
+ public void execute(CommandContext context) {
+ GeyserCommandSource source = context.sender();
+ String[] args = context.getOrDefault(ARGUMENTS, new String[0]);
boolean showSensitive = false;
boolean offlineDump = false;
@@ -75,25 +103,28 @@ public class DumpCommand extends GeyserCommand {
case "full" -> showSensitive = true;
case "offline" -> offlineDump = true;
case "logs" -> addLog = true;
+ default -> context.sender().sendMessage("Invalid geyser dump option " + arg + "! Fallback to no arguments.");
}
}
}
AsteriskSerializer.showSensitive = showSensitive;
- sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.collecting", sender.locale()));
+ source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.collecting", source.locale()));
String dumpData;
try {
+ DumpInfo dump = new DumpInfo(geyser, addLog);
+
if (offlineDump) {
DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter();
// Make arrays easier to read
prettyPrinter.indentArraysWith(new DefaultIndenter(" ", "\n"));
- dumpData = MAPPER.writer(prettyPrinter).writeValueAsString(new DumpInfo(addLog));
+ dumpData = MAPPER.writer(prettyPrinter).writeValueAsString(dump);
} else {
- dumpData = MAPPER.writeValueAsString(new DumpInfo(addLog));
+ dumpData = MAPPER.writeValueAsString(dump);
}
} catch (IOException e) {
- sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.collect_error", sender.locale()));
+ source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.collect_error", source.locale()));
geyser.getLogger().error(GeyserLocale.getLocaleStringLog("geyser.commands.dump.collect_error_short"), e);
return;
}
@@ -101,21 +132,21 @@ public class DumpCommand extends GeyserCommand {
String uploadedDumpUrl;
if (offlineDump) {
- sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.writing", sender.locale()));
+ source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.writing", source.locale()));
try {
FileOutputStream outputStream = new FileOutputStream(GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("dump.json").toFile());
outputStream.write(dumpData.getBytes());
outputStream.close();
} catch (IOException e) {
- sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.write_error", sender.locale()));
+ source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.write_error", source.locale()));
geyser.getLogger().error(GeyserLocale.getLocaleStringLog("geyser.commands.dump.write_error_short"), e);
return;
}
uploadedDumpUrl = "dump.json";
} else {
- sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.uploading", sender.locale()));
+ source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.uploading", source.locale()));
String response;
JsonNode responseNode;
@@ -123,33 +154,22 @@ public class DumpCommand extends GeyserCommand {
response = WebUtils.post(DUMP_URL + "documents", dumpData);
responseNode = MAPPER.readTree(response);
} catch (IOException e) {
- sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.upload_error", sender.locale()));
+ source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.upload_error", source.locale()));
geyser.getLogger().error(GeyserLocale.getLocaleStringLog("geyser.commands.dump.upload_error_short"), e);
return;
}
if (!responseNode.has("key")) {
- sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.upload_error_short", sender.locale()) + ": " + (responseNode.has("message") ? responseNode.get("message").asText() : response));
+ source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.dump.upload_error_short", source.locale()) + ": " + (responseNode.has("message") ? responseNode.get("message").asText() : response));
return;
}
uploadedDumpUrl = DUMP_URL + responseNode.get("key").asText();
}
- sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.message", sender.locale()) + " " + ChatColor.DARK_AQUA + uploadedDumpUrl);
- if (!sender.isConsole()) {
- geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.commands.dump.created", sender.name(), uploadedDumpUrl));
+ source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.dump.message", source.locale()) + " " + ChatColor.DARK_AQUA + uploadedDumpUrl);
+ if (!source.isConsole()) {
+ geyser.getLogger().info(GeyserLocale.getLocaleStringLog("geyser.commands.dump.created", source.name(), uploadedDumpUrl));
}
}
-
- @NonNull
- @Override
- public List subCommands() {
- return Arrays.asList("offline", "full", "logs");
- }
-
- @Override
- public boolean isSuggestedOpOnly() {
- return true;
- }
}
diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/ExtensionsCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/ExtensionsCommand.java
index df33437d9..24881f2ca 100644
--- a/core/src/main/java/org/geysermc/geyser/command/defaults/ExtensionsCommand.java
+++ b/core/src/main/java/org/geysermc/geyser/command/defaults/ExtensionsCommand.java
@@ -25,14 +25,14 @@
package org.geysermc.geyser.command.defaults;
-import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.extension.Extension;
+import org.geysermc.geyser.api.util.TriState;
import org.geysermc.geyser.command.GeyserCommand;
import org.geysermc.geyser.command.GeyserCommandSource;
-import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.text.GeyserLocale;
+import org.incendo.cloud.context.CommandContext;
import java.util.Comparator;
import java.util.List;
@@ -41,22 +41,23 @@ public class ExtensionsCommand extends GeyserCommand {
private final GeyserImpl geyser;
public ExtensionsCommand(GeyserImpl geyser, String name, String description, String permission) {
- super(name, description, permission);
-
+ super(name, description, permission, TriState.TRUE);
this.geyser = geyser;
}
@Override
- public void execute(@Nullable GeyserSession session, GeyserCommandSource sender, String[] args) {
+ public void execute(CommandContext context) {
+ GeyserCommandSource source = context.sender();
+
// TODO: Pagination
int page = 1;
int maxPage = 1;
- String header = GeyserLocale.getPlayerLocaleString("geyser.commands.extensions.header", sender.locale(), page, maxPage);
- sender.sendMessage(header);
+ String header = GeyserLocale.getPlayerLocaleString("geyser.commands.extensions.header", source.locale(), page, maxPage);
+ source.sendMessage(header);
this.geyser.extensionManager().extensions().stream().sorted(Comparator.comparing(Extension::name)).forEach(extension -> {
String extensionName = (extension.isEnabled() ? ChatColor.GREEN : ChatColor.RED) + extension.name();
- sender.sendMessage("- " + extensionName + ChatColor.RESET + " v" + extension.description().version() + formatAuthors(extension.description().authors()));
+ source.sendMessage("- " + extensionName + ChatColor.RESET + " v" + extension.description().version() + formatAuthors(extension.description().authors()));
});
}
diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/HelpCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/HelpCommand.java
index c9671b089..9911863ab 100644
--- a/core/src/main/java/org/geysermc/geyser/command/defaults/HelpCommand.java
+++ b/core/src/main/java/org/geysermc/geyser/command/defaults/HelpCommand.java
@@ -25,61 +25,59 @@
package org.geysermc.geyser.command.defaults;
-import org.geysermc.geyser.api.util.PlatformType;
-import org.geysermc.geyser.GeyserImpl;
+import com.google.common.base.Predicates;
import org.geysermc.geyser.api.command.Command;
+import org.geysermc.geyser.api.util.TriState;
import org.geysermc.geyser.command.GeyserCommand;
import org.geysermc.geyser.command.GeyserCommandSource;
-import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.text.GeyserLocale;
+import org.incendo.cloud.context.CommandContext;
+import java.util.Collection;
import java.util.Collections;
+import java.util.Comparator;
import java.util.Map;
public class HelpCommand extends GeyserCommand {
- private final GeyserImpl geyser;
- private final String baseCommand;
- private final Map commands;
+ private final String rootCommand;
+ private final Collection commands;
- public HelpCommand(GeyserImpl geyser, String name, String description, String permission,
- String baseCommand, Map commands) {
- super(name, description, permission);
- this.geyser = geyser;
- this.baseCommand = baseCommand;
- this.commands = commands;
-
- this.setAliases(Collections.singletonList("?"));
+ public HelpCommand(String rootCommand, String name, String description, String permission, Map commands) {
+ super(name, description, permission, TriState.TRUE);
+ this.rootCommand = rootCommand;
+ this.commands = commands.values();
+ this.aliases = Collections.singletonList("?");
}
- /**
- * Sends the help menu to a command sender. Will not show certain commands depending on the command sender and session.
- *
- * @param session The Geyser session of the command sender, if it is a bedrock player. If null, bedrock-only commands will be hidden.
- * @param sender The CommandSender to send the help message to.
- * @param args Not used.
- */
@Override
- public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) {
+ public String rootCommand() {
+ return rootCommand;
+ }
+
+ @Override
+ public void execute(CommandContext context) {
+ execute(context.sender());
+ }
+
+ public void execute(GeyserCommandSource source) {
+ boolean bedrockPlayer = source.connection() != null;
+
+ // todo: pagination
int page = 1;
int maxPage = 1;
- String translationKey = this.baseCommand.equals("geyser") ? "geyser.commands.help.header" : "geyser.commands.extensions.header";
- String header = GeyserLocale.getPlayerLocaleString(translationKey, sender.locale(), page, maxPage);
- sender.sendMessage(header);
+ String translationKey = this.rootCommand.equals(DEFAULT_ROOT_COMMAND) ? "geyser.commands.help.header" : "geyser.commands.extensions.header";
+ String header = GeyserLocale.getPlayerLocaleString(translationKey, source.locale(), page, maxPage);
+ source.sendMessage(header);
- this.commands.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(entry -> {
- Command cmd = entry.getValue();
-
- // Standalone hack-in since it doesn't have a concept of permissions
- if (geyser.getPlatformType() == PlatformType.STANDALONE || sender.hasPermission(cmd.permission())) {
- // Only list commands the player can actually run
- if (cmd.isBedrockOnly() && session == null) {
- return;
- }
-
- sender.sendMessage(ChatColor.YELLOW + "/" + baseCommand + " " + entry.getKey() + ChatColor.WHITE + ": " +
- GeyserLocale.getPlayerLocaleString(cmd.description(), sender.locale()));
- }
- });
+ this.commands.stream()
+ .distinct() // remove aliases
+ .filter(bedrockPlayer ? Predicates.alwaysTrue() : cmd -> !cmd.isBedrockOnly()) // remove bedrock only commands if not a bedrock player
+ .filter(cmd -> source.hasPermission(cmd.permission()))
+ .sorted(Comparator.comparing(Command::name))
+ .forEachOrdered(cmd -> {
+ String description = GeyserLocale.getPlayerLocaleString(cmd.description(), source.locale());
+ source.sendMessage(ChatColor.YELLOW + "/" + rootCommand + " " + cmd.name() + ChatColor.WHITE + ": " + description);
+ });
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/ListCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/ListCommand.java
index 90446fbb6..5a76ab902 100644
--- a/core/src/main/java/org/geysermc/geyser/command/defaults/ListCommand.java
+++ b/core/src/main/java/org/geysermc/geyser/command/defaults/ListCommand.java
@@ -26,10 +26,12 @@
package org.geysermc.geyser.command.defaults;
import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.api.util.TriState;
import org.geysermc.geyser.command.GeyserCommand;
import org.geysermc.geyser.command.GeyserCommandSource;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.GeyserLocale;
+import org.incendo.cloud.context.CommandContext;
import java.util.stream.Collectors;
@@ -38,22 +40,18 @@ public class ListCommand extends GeyserCommand {
private final GeyserImpl geyser;
public ListCommand(GeyserImpl geyser, String name, String description, String permission) {
- super(name, description, permission);
-
+ super(name, description, permission, TriState.NOT_SET);
this.geyser = geyser;
}
@Override
- public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) {
- String message = GeyserLocale.getPlayerLocaleString("geyser.commands.list.message", sender.locale(),
- geyser.getSessionManager().size(),
- geyser.getSessionManager().getAllSessions().stream().map(GeyserSession::bedrockUsername).collect(Collectors.joining(" ")));
+ public void execute(CommandContext context) {
+ GeyserCommandSource source = context.sender();
- sender.sendMessage(message);
- }
+ String message = GeyserLocale.getPlayerLocaleString("geyser.commands.list.message", source.locale(),
+ geyser.getSessionManager().size(),
+ geyser.getSessionManager().getAllSessions().stream().map(GeyserSession::bedrockUsername).collect(Collectors.joining(" ")));
- @Override
- public boolean isSuggestedOpOnly() {
- return true;
+ source.sendMessage(message);
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/OffhandCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/OffhandCommand.java
index 6188e6924..5f9061618 100644
--- a/core/src/main/java/org/geysermc/geyser/command/defaults/OffhandCommand.java
+++ b/core/src/main/java/org/geysermc/geyser/command/defaults/OffhandCommand.java
@@ -25,33 +25,23 @@
package org.geysermc.geyser.command.defaults;
-import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.api.util.TriState;
import org.geysermc.geyser.command.GeyserCommand;
import org.geysermc.geyser.command.GeyserCommandSource;
import org.geysermc.geyser.session.GeyserSession;
+import org.incendo.cloud.context.CommandContext;
+
+import java.util.Objects;
public class OffhandCommand extends GeyserCommand {
- public OffhandCommand(GeyserImpl geyser, String name, String description, String permission) {
- super(name, description, permission);
+ public OffhandCommand(String name, String description, String permission) {
+ super(name, description, permission, TriState.TRUE, true, true);
}
@Override
- public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) {
- if (session == null) {
- return;
- }
-
+ public void execute(CommandContext context) {
+ GeyserSession session = Objects.requireNonNull(context.sender().connection());
session.requestOffhandSwap();
}
-
- @Override
- public boolean isExecutableOnConsole() {
- return false;
- }
-
- @Override
- public boolean isBedrockOnly() {
- return true;
- }
}
diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/PingCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/PingCommand.java
new file mode 100644
index 000000000..f39be0528
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/command/defaults/PingCommand.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.command.defaults;
+
+import org.geysermc.geyser.api.util.TriState;
+import org.geysermc.geyser.command.GeyserCommand;
+import org.geysermc.geyser.command.GeyserCommandSource;
+import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.text.GeyserLocale;
+import org.incendo.cloud.context.CommandContext;
+
+import java.util.Objects;
+
+public class PingCommand extends GeyserCommand {
+
+ public PingCommand(String name, String description, String permission) {
+ super(name, description, permission, TriState.TRUE, true, true);
+ }
+
+ @Override
+ public void execute(CommandContext context) {
+ GeyserSession session = Objects.requireNonNull(context.sender().connection());
+ session.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.ping.message", session.locale(), session.ping()));
+ }
+}
+
diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/ReloadCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/ReloadCommand.java
index 987860238..e54b83ddf 100644
--- a/core/src/main/java/org/geysermc/geyser/command/defaults/ReloadCommand.java
+++ b/core/src/main/java/org/geysermc/geyser/command/defaults/ReloadCommand.java
@@ -25,12 +25,12 @@
package org.geysermc.geyser.command.defaults;
-import org.geysermc.geyser.api.util.PlatformType;
import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.api.util.TriState;
import org.geysermc.geyser.command.GeyserCommand;
import org.geysermc.geyser.command.GeyserCommandSource;
-import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.GeyserLocale;
+import org.incendo.cloud.context.CommandContext;
import java.util.concurrent.TimeUnit;
@@ -39,27 +39,17 @@ public class ReloadCommand extends GeyserCommand {
private final GeyserImpl geyser;
public ReloadCommand(GeyserImpl geyser, String name, String description, String permission) {
- super(name, description, permission);
+ super(name, description, permission, TriState.NOT_SET);
this.geyser = geyser;
}
@Override
- public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) {
- if (!sender.isConsole() && geyser.getPlatformType() == PlatformType.STANDALONE) {
- return;
- }
-
- String message = GeyserLocale.getPlayerLocaleString("geyser.commands.reload.message", sender.locale());
-
- sender.sendMessage(message);
+ public void execute(CommandContext context) {
+ GeyserCommandSource source = context.sender();
+ source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.reload.message", source.locale()));
geyser.getSessionManager().disconnectAll("geyser.commands.reload.kick");
//FIXME Without the tiny wait, players do not get kicked - same happens when Geyser tries to disconnect all sessions on shutdown
geyser.getScheduledThread().schedule(geyser::reloadGeyser, 10, TimeUnit.MILLISECONDS);
}
-
- @Override
- public boolean isSuggestedOpOnly() {
- return true;
- }
}
diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/SettingsCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/SettingsCommand.java
index 7828cf1d2..a5734a69f 100644
--- a/core/src/main/java/org/geysermc/geyser/command/defaults/SettingsCommand.java
+++ b/core/src/main/java/org/geysermc/geyser/command/defaults/SettingsCommand.java
@@ -25,31 +25,24 @@
package org.geysermc.geyser.command.defaults;
-import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.api.util.TriState;
import org.geysermc.geyser.command.GeyserCommand;
import org.geysermc.geyser.command.GeyserCommandSource;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.util.SettingsUtils;
+import org.incendo.cloud.context.CommandContext;
+
+import java.util.Objects;
public class SettingsCommand extends GeyserCommand {
- public SettingsCommand(GeyserImpl geyser, String name, String description, String permission) {
- super(name, description, permission);
+
+ public SettingsCommand(String name, String description, String permission) {
+ super(name, description, permission, TriState.TRUE, true, true);
}
@Override
- public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) {
- if (session != null) {
- session.sendForm(SettingsUtils.buildForm(session));
- }
- }
-
- @Override
- public boolean isExecutableOnConsole() {
- return false;
- }
-
- @Override
- public boolean isBedrockOnly() {
- return true;
+ public void execute(CommandContext context) {
+ GeyserSession session = Objects.requireNonNull(context.sender().connection());
+ session.sendForm(SettingsUtils.buildForm(session));
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/StatisticsCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/StatisticsCommand.java
index 5952ea00d..eebb9170c 100644
--- a/core/src/main/java/org/geysermc/geyser/command/defaults/StatisticsCommand.java
+++ b/core/src/main/java/org/geysermc/geyser/command/defaults/StatisticsCommand.java
@@ -25,35 +25,28 @@
package org.geysermc.geyser.command.defaults;
-import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.api.util.TriState;
import org.geysermc.geyser.command.GeyserCommand;
import org.geysermc.geyser.command.GeyserCommandSource;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.mcprotocollib.protocol.data.game.ClientCommand;
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.ServerboundClientCommandPacket;
+import org.incendo.cloud.context.CommandContext;
+
+import java.util.Objects;
public class StatisticsCommand extends GeyserCommand {
- public StatisticsCommand(GeyserImpl geyser, String name, String description, String permission) {
- super(name, description, permission);
+ public StatisticsCommand(String name, String description, String permission) {
+ super(name, description, permission, TriState.TRUE, true, true);
}
@Override
- public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) {
- if (session == null) return;
+ public void execute(CommandContext context) {
+ GeyserSession session = Objects.requireNonNull(context.sender().connection());
session.setWaitingForStatistics(true);
- ServerboundClientCommandPacket ServerboundClientCommandPacket = new ServerboundClientCommandPacket(ClientCommand.STATS);
- session.sendDownstreamGamePacket(ServerboundClientCommandPacket);
- }
-
- @Override
- public boolean isExecutableOnConsole() {
- return false;
- }
-
- @Override
- public boolean isBedrockOnly() {
- return true;
+ ServerboundClientCommandPacket packet = new ServerboundClientCommandPacket(ClientCommand.STATS);
+ session.sendDownstreamGamePacket(packet);
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/StopCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/StopCommand.java
index 1cd3050c9..f6dc1610a 100644
--- a/core/src/main/java/org/geysermc/geyser/command/defaults/StopCommand.java
+++ b/core/src/main/java/org/geysermc/geyser/command/defaults/StopCommand.java
@@ -25,12 +25,11 @@
package org.geysermc.geyser.command.defaults;
-import org.geysermc.geyser.api.util.PlatformType;
import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.api.util.TriState;
import org.geysermc.geyser.command.GeyserCommand;
import org.geysermc.geyser.command.GeyserCommandSource;
-import org.geysermc.geyser.session.GeyserSession;
-import org.geysermc.geyser.text.GeyserLocale;
+import org.incendo.cloud.context.CommandContext;
import java.util.Collections;
@@ -39,24 +38,13 @@ public class StopCommand extends GeyserCommand {
private final GeyserImpl geyser;
public StopCommand(GeyserImpl geyser, String name, String description, String permission) {
- super(name, description, permission);
+ super(name, description, permission, TriState.NOT_SET);
this.geyser = geyser;
-
- this.setAliases(Collections.singletonList("shutdown"));
+ this.aliases = Collections.singletonList("shutdown");
}
@Override
- public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) {
- if (!sender.isConsole() && geyser.getPlatformType() == PlatformType.STANDALONE) {
- sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", sender.locale()));
- return;
- }
-
+ public void execute(CommandContext context) {
geyser.getBootstrap().onGeyserShutdown();
}
-
- @Override
- public boolean isSuggestedOpOnly() {
- return true;
- }
}
\ No newline at end of file
diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/VersionCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/VersionCommand.java
index c6852d577..8d34c1bf0 100644
--- a/core/src/main/java/org/geysermc/geyser/command/defaults/VersionCommand.java
+++ b/core/src/main/java/org/geysermc/geyser/command/defaults/VersionCommand.java
@@ -29,13 +29,14 @@ import com.fasterxml.jackson.databind.JsonNode;
import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.util.PlatformType;
+import org.geysermc.geyser.api.util.TriState;
import org.geysermc.geyser.command.GeyserCommand;
import org.geysermc.geyser.command.GeyserCommandSource;
import org.geysermc.geyser.network.GameProtocol;
-import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.util.WebUtils;
+import org.incendo.cloud.context.CommandContext;
import java.io.IOException;
import java.util.List;
@@ -45,13 +46,14 @@ public class VersionCommand extends GeyserCommand {
private final GeyserImpl geyser;
public VersionCommand(GeyserImpl geyser, String name, String description, String permission) {
- super(name, description, permission);
-
+ super(name, description, permission, TriState.NOT_SET);
this.geyser = geyser;
}
@Override
- public void execute(GeyserSession session, GeyserCommandSource sender, String[] args) {
+ public void execute(CommandContext context) {
+ GeyserCommandSource source = context.sender();
+
String bedrockVersions;
List supportedCodecs = GameProtocol.SUPPORTED_BEDROCK_CODECS;
if (supportedCodecs.size() > 1) {
@@ -67,45 +69,37 @@ public class VersionCommand extends GeyserCommand {
javaVersions = supportedJavaVersions.get(0);
}
- sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.version", sender.locale(),
+ source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.version", source.locale(),
GeyserImpl.NAME, GeyserImpl.VERSION, javaVersions, bedrockVersions));
// Disable update checking in dev mode and for players in Geyser Standalone
- if (!GeyserImpl.getInstance().isProductionEnvironment() || (!sender.isConsole() && geyser.getPlatformType() == PlatformType.STANDALONE)) {
+ if (!GeyserImpl.getInstance().isProductionEnvironment() || (!source.isConsole() && geyser.getPlatformType() == PlatformType.STANDALONE)) {
return;
}
if (GeyserImpl.IS_DEV) {
- // TODO cloud use language string
- sender.sendMessage("You are running a development build of Geyser! Please report any bugs you find on our Discord server: %s"
- .formatted("https://discord.gg/geysermc"));
- //sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.core.dev_build", sender.locale(), "https://discord.gg/geysermc"));
+ source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.core.dev_build", source.locale(), "https://discord.gg/geysermc"));
return;
}
- sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.checking", sender.locale()));
+ source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.checking", source.locale()));
try {
int buildNumber = this.geyser.buildNumber();
JsonNode response = WebUtils.getJson("https://download.geysermc.org/v2/projects/geyser/versions/latest/builds/latest");
int latestBuildNumber = response.get("build").asInt();
if (latestBuildNumber == buildNumber) {
- sender.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.no_updates", sender.locale()));
+ source.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.commands.version.no_updates", source.locale()));
return;
}
- sender.sendMessage(GeyserLocale.getPlayerLocaleString(
+ source.sendMessage(GeyserLocale.getPlayerLocaleString(
"geyser.commands.version.outdated",
- sender.locale(), (latestBuildNumber - buildNumber), "https://geysermc.org/download"
+ source.locale(), (latestBuildNumber - buildNumber), "https://geysermc.org/download"
));
} catch (IOException e) {
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.commands.version.failed"), e);
- sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.version.failed", sender.locale()));
+ source.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.commands.version.failed", source.locale()));
}
}
-
- @Override
- public boolean isSuggestedOpOnly() {
- return true;
- }
}
diff --git a/core/src/main/java/org/geysermc/geyser/command/standalone/PermissionConfiguration.java b/core/src/main/java/org/geysermc/geyser/command/standalone/PermissionConfiguration.java
new file mode 100644
index 000000000..edacd49ff
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/command/standalone/PermissionConfiguration.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.command.standalone;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Getter;
+
+import java.util.Collections;
+import java.util.Set;
+
+@Getter
+@JsonIgnoreProperties(ignoreUnknown = true)
+@SuppressWarnings("FieldMayBeFinal") // Jackson requires that the fields are not final
+public class PermissionConfiguration {
+
+ @JsonProperty("default-permissions")
+ private Set defaultPermissions = Collections.emptySet();
+}
diff --git a/core/src/main/java/org/geysermc/geyser/command/standalone/StandaloneCloudCommandManager.java b/core/src/main/java/org/geysermc/geyser/command/standalone/StandaloneCloudCommandManager.java
new file mode 100644
index 000000000..99c53f319
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/command/standalone/StandaloneCloudCommandManager.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.command.standalone;
+
+import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionCheckersEvent;
+import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent;
+import org.geysermc.geyser.api.permission.PermissionChecker;
+import org.geysermc.geyser.api.util.TriState;
+import org.geysermc.geyser.command.CommandRegistry;
+import org.geysermc.geyser.command.GeyserCommandSource;
+import org.geysermc.geyser.util.FileUtils;
+import org.incendo.cloud.CommandManager;
+import org.incendo.cloud.execution.ExecutionCoordinator;
+import org.incendo.cloud.internal.CommandRegistrationHandler;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+public class StandaloneCloudCommandManager extends CommandManager {
+
+ private final GeyserImpl geyser;
+
+ /**
+ * The checkers we use to test if a command source has a permission
+ */
+ private final List permissionCheckers = new ArrayList<>();
+
+ /**
+ * Any permissions that all connections have
+ */
+ private final Set basePermissions = new ObjectOpenHashSet<>();
+
+ public StandaloneCloudCommandManager(GeyserImpl geyser) {
+ super(ExecutionCoordinator.simpleCoordinator(), CommandRegistrationHandler.nullCommandRegistrationHandler());
+ // simpleCoordinator: execute commands immediately on the calling thread.
+ // nullCommandRegistrationHandler: cloud is not responsible for handling our CommandRegistry, which is fairly decoupled.
+ this.geyser = geyser;
+
+ // allow any extensions to customize permissions
+ geyser.getEventBus().fire((GeyserRegisterPermissionCheckersEvent) permissionCheckers::add);
+
+ // must still implement a basic permission system
+ try {
+ File permissionsFile = geyser.getBootstrap().getConfigFolder().resolve("permissions.yml").toFile();
+ FileUtils.fileOrCopiedFromResource(permissionsFile, "permissions.yml", geyser.getBootstrap());
+ PermissionConfiguration config = FileUtils.loadConfig(permissionsFile, PermissionConfiguration.class);
+ basePermissions.addAll(config.getDefaultPermissions());
+ } catch (Exception e) {
+ geyser.getLogger().error("Failed to load permissions.yml - proceeding without it", e);
+ }
+ }
+
+ /**
+ * Fire a {@link GeyserRegisterPermissionsEvent} to determine any additions or removals to the base list of
+ * permissions. This should be called after any event listeners have been registered, such as that of {@link CommandRegistry}.
+ */
+ public void fireRegisterPermissionsEvent() {
+ geyser.getEventBus().fire((GeyserRegisterPermissionsEvent) (permission, def) -> {
+ Objects.requireNonNull(permission, "permission");
+ Objects.requireNonNull(def, "permission default for " + permission);
+
+ if (permission.isBlank()) {
+ return;
+ }
+ if (def == TriState.TRUE) {
+ basePermissions.add(permission);
+ }
+ });
+ }
+
+ @Override
+ public boolean hasPermission(@NonNull GeyserCommandSource sender, @NonNull String permission) {
+ // Note: the two GeyserCommandSources on Geyser-Standalone are GeyserLogger and GeyserSession
+ // GeyserLogger#hasPermission always returns true
+ // GeyserSession#hasPermission delegates to this method,
+ // which is why this method doesn't just call GeyserCommandSource#hasPermission
+ if (sender.isConsole()) {
+ return true;
+ }
+
+ // An empty or blank permission is treated as a lack of permission requirement
+ if (permission.isBlank()) {
+ return true;
+ }
+
+ for (PermissionChecker checker : permissionCheckers) {
+ Boolean result = checker.hasPermission(sender, permission).toBoolean();
+ if (result != null) {
+ return result;
+ }
+ // undefined - try the next checker to see if it has a defined value
+ }
+ // fallback to our list of default permissions
+ // note that a PermissionChecker may in fact override any values set here by returning FALSE
+ return basePermissions.contains(permission);
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java b/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java
index 6989dc10a..515e1a629 100644
--- a/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java
+++ b/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java
@@ -81,7 +81,7 @@ public class DumpInfo {
private final FlagsInfo flagsInfo;
private final List extensionInfo;
- public DumpInfo(boolean addLog) {
+ public DumpInfo(GeyserImpl geyser, boolean addLog) {
this.versionInfo = new VersionInfo();
this.cpuCount = Runtime.getRuntime().availableProcessors();
@@ -91,7 +91,7 @@ public class DumpInfo {
this.gitInfo = new GitInfo(GeyserImpl.BUILD_NUMBER, GeyserImpl.COMMIT.substring(0, 7), GeyserImpl.COMMIT, GeyserImpl.BRANCH, GeyserImpl.REPOSITORY);
- this.config = GeyserImpl.getInstance().getConfig();
+ this.config = geyser.getConfig();
this.floodgate = new Floodgate();
String md5Hash = "unknown";
@@ -107,7 +107,7 @@ public class DumpInfo {
//noinspection UnstableApiUsage
sha256Hash = byteSource.hash(Hashing.sha256()).toString();
} catch (Exception e) {
- if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
+ if (this.config.isDebugMode()) {
e.printStackTrace();
}
}
@@ -116,18 +116,22 @@ public class DumpInfo {
this.ramInfo = new RamInfo();
if (addLog) {
- this.logsInfo = new LogsInfo();
+ this.logsInfo = new LogsInfo(geyser);
}
this.userPlatforms = new Object2IntOpenHashMap<>();
- for (GeyserSession session : GeyserImpl.getInstance().getSessionManager().getAllSessions()) {
+ for (GeyserSession session : geyser.getSessionManager().getAllSessions()) {
DeviceOs device = session.getClientData().getDeviceOs();
userPlatforms.put(device, userPlatforms.getOrDefault(device, 0) + 1);
}
- this.connectionAttempts = GeyserImpl.getInstance().getGeyserServer().getConnectionAttempts();
+ if (geyser.getGeyserServer() != null) {
+ this.connectionAttempts = geyser.getGeyserServer().getConnectionAttempts();
+ } else {
+ this.connectionAttempts = 0; // Fallback if Geyser failed to fully startup
+ }
- this.bootstrapInfo = GeyserImpl.getInstance().getBootstrap().getDumpInfo();
+ this.bootstrapInfo = geyser.getBootstrap().getDumpInfo();
this.flagsInfo = new FlagsInfo();
@@ -244,10 +248,10 @@ public class DumpInfo {
public static class LogsInfo {
private String link;
- public LogsInfo() {
+ public LogsInfo(GeyserImpl geyser) {
try {
Map fields = new HashMap<>();
- fields.put("content", FileUtils.readAllLines(GeyserImpl.getInstance().getBootstrap().getLogsPath()).collect(Collectors.joining("\n")));
+ fields.put("content", FileUtils.readAllLines(geyser.getBootstrap().getLogsPath()).collect(Collectors.joining("\n")));
JsonNode logData = GeyserImpl.JSON_MAPPER.readTree(WebUtils.postForm("https://api.mclo.gs/1/log", fields));
diff --git a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java
index 9063c7421..5932ecf41 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/EntityDefinitions.java
@@ -888,7 +888,7 @@ public final class EntityDefinitions {
.type(EntityType.PIG)
.heightAndWidth(0.9f)
.addTranslator(MetadataType.BOOLEAN, (pigEntity, entityMetadata) -> pigEntity.setFlag(EntityFlag.SADDLED, ((BooleanEntityMetadata) entityMetadata).getPrimitiveValue()))
- .addTranslator(null) // Boost time
+ .addTranslator(MetadataType.INT, PigEntity::setBoost)
.build();
POLAR_BEAR = EntityDefinition.inherited(PolarBearEntity::new, ageableEntityBase)
.type(EntityType.POLAR_BEAR)
@@ -914,7 +914,7 @@ public final class EntityDefinitions {
STRIDER = EntityDefinition.inherited(StriderEntity::new, ageableEntityBase)
.type(EntityType.STRIDER)
.height(1.7f).width(0.9f)
- .addTranslator(null) // Boost time
+ .addTranslator(MetadataType.INT, StriderEntity::setBoost)
.addTranslator(MetadataType.BOOLEAN, StriderEntity::setCold)
.addTranslator(MetadataType.BOOLEAN, StriderEntity::setSaddled)
.build();
@@ -955,7 +955,7 @@ public final class EntityDefinitions {
.type(EntityType.CAMEL)
.height(2.375f).width(1.7f)
.addTranslator(MetadataType.BOOLEAN, CamelEntity::setDashing)
- .addTranslator(null) // Last pose change tick
+ .addTranslator(MetadataType.LONG, CamelEntity::setLastPoseTick)
.build();
HORSE = EntityDefinition.inherited(HorseEntity::new, abstractHorseEntityBase)
.type(EntityType.HORSE)
diff --git a/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.java b/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.java
index c9ef7a2dd..6f8f2525f 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.java
@@ -96,4 +96,9 @@ public class GeyserEntityData implements EntityData {
public boolean isMovementLocked() {
return !movementLockOwners.isEmpty();
}
+
+ @Override
+ public void switchHands() {
+ session.requestOffhandSwap();
+ }
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/attribute/GeyserAttributeType.java b/core/src/main/java/org/geysermc/geyser/entity/attribute/GeyserAttributeType.java
index 3b543a943..1e050c840 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/attribute/GeyserAttributeType.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/attribute/GeyserAttributeType.java
@@ -54,7 +54,7 @@ public enum GeyserAttributeType {
// Bedrock Attributes
ABSORPTION(null, "minecraft:absorption", 0f, 1024f, 0f),
- EXHAUSTION(null, "minecraft:player.exhaustion", 0f, 5f, 0f),
+ EXHAUSTION(null, "minecraft:player.exhaustion", 0f, 20f, 0f),
EXPERIENCE(null, "minecraft:player.experience", 0f, 1f, 0f),
EXPERIENCE_LEVEL(null, "minecraft:player.level", 0f, 24791.00f, 0f),
HEALTH(null, "minecraft:health", 0f, 1024f, 20f),
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java
index 518c2bf78..626ceca5c 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/LivingEntity.java
@@ -46,6 +46,7 @@ import org.cloudburstmc.protocol.bedrock.packet.MobEquipmentPacket;
import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket;
import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.entity.attribute.GeyserAttributeType;
+import org.geysermc.geyser.entity.vehicle.ClientVehicle;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.registry.type.ItemMapping;
@@ -77,6 +78,7 @@ public class LivingEntity extends Entity {
protected ItemData chestplate = ItemData.AIR;
protected ItemData leggings = ItemData.AIR;
protected ItemData boots = ItemData.AIR;
+ protected ItemData body = ItemData.AIR;
protected ItemData hand = ItemData.AIR;
protected ItemData offhand = ItemData.AIR;
@@ -115,6 +117,10 @@ public class LivingEntity extends Entity {
this.chestplate = ItemTranslator.translateToBedrock(session, stack);
}
+ public void setBody(ItemStack stack) {
+ this.body = ItemTranslator.translateToBedrock(session, stack);
+ }
+
public void setLeggings(ItemStack stack) {
this.leggings = ItemTranslator.translateToBedrock(session, stack);
}
@@ -302,6 +308,36 @@ public class LivingEntity extends Entity {
return super.interact(hand);
}
+ @Override
+ public void moveRelative(double relX, double relY, double relZ, float yaw, float pitch, float headYaw, boolean isOnGround) {
+ if (this instanceof ClientVehicle clientVehicle) {
+ if (clientVehicle.isClientControlled()) {
+ return;
+ }
+ clientVehicle.getVehicleComponent().moveRelative(relX, relY, relZ);
+ }
+
+ super.moveRelative(relX, relY, relZ, yaw, pitch, headYaw, isOnGround);
+ }
+
+ @Override
+ public boolean setBoundingBoxHeight(float height) {
+ if (valid && this instanceof ClientVehicle clientVehicle) {
+ clientVehicle.getVehicleComponent().setHeight(height);
+ }
+
+ return super.setBoundingBoxHeight(height);
+ }
+
+ @Override
+ public void setBoundingBoxWidth(float width) {
+ if (valid && this instanceof ClientVehicle clientVehicle) {
+ clientVehicle.getVehicleComponent().setWidth(width);
+ }
+
+ super.setBoundingBoxWidth(width);
+ }
+
/**
* Checks to see if a nametag interaction would go through.
*/
@@ -336,6 +372,7 @@ public class LivingEntity extends Entity {
armorEquipmentPacket.setChestplate(chestplate);
armorEquipmentPacket.setLeggings(leggings);
armorEquipmentPacket.setBoots(boots);
+ armorEquipmentPacket.setBody(body);
session.sendUpstreamPacket(armorEquipmentPacket);
}
@@ -414,9 +451,25 @@ public class LivingEntity extends Entity {
this.maxHealth = Math.max((float) AttributeUtils.calculateValue(javaAttribute), 1f);
newAttributes.add(createHealthAttribute());
}
+ case GENERIC_MOVEMENT_SPEED -> {
+ AttributeData attributeData = calculateAttribute(javaAttribute, GeyserAttributeType.MOVEMENT_SPEED);
+ newAttributes.add(attributeData);
+ if (this instanceof ClientVehicle clientVehicle) {
+ clientVehicle.getVehicleComponent().setMoveSpeed(attributeData.getValue());
+ }
+ }
+ case GENERIC_STEP_HEIGHT -> {
+ if (this instanceof ClientVehicle clientVehicle) {
+ clientVehicle.getVehicleComponent().setStepHeight((float) AttributeUtils.calculateValue(javaAttribute));
+ }
+ }
+ case GENERIC_GRAVITY -> {
+ if (this instanceof ClientVehicle clientVehicle) {
+ clientVehicle.getVehicleComponent().setGravity(AttributeUtils.calculateValue(javaAttribute));
+ }
+ }
case GENERIC_ATTACK_DAMAGE -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.ATTACK_DAMAGE));
case GENERIC_FLYING_SPEED -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.FLYING_SPEED));
- case GENERIC_MOVEMENT_SPEED -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.MOVEMENT_SPEED));
case GENERIC_FOLLOW_RANGE -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.FOLLOW_RANGE));
case GENERIC_KNOCKBACK_RESISTANCE -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.KNOCKBACK_RESISTANCE));
case GENERIC_JUMP_STRENGTH -> newAttributes.add(calculateAttribute(javaAttribute, GeyserAttributeType.HORSE_JUMP_STRENGTH));
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PigEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PigEntity.java
index 446e3e109..2ec23d673 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PigEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/PigEntity.java
@@ -27,20 +27,30 @@ package org.geysermc.geyser.entity.type.living.animal;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
+import org.cloudburstmc.math.vector.Vector2f;
import org.cloudburstmc.math.vector.Vector3f;
+import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.geysermc.geyser.entity.EntityDefinition;
+import org.geysermc.geyser.entity.type.Tickable;
+import org.geysermc.geyser.entity.type.player.PlayerEntity;
+import org.geysermc.geyser.entity.vehicle.BoostableVehicleComponent;
+import org.geysermc.geyser.entity.vehicle.ClientVehicle;
+import org.geysermc.geyser.entity.vehicle.VehicleComponent;
import org.geysermc.geyser.inventory.GeyserItemStack;
+import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.tags.ItemTag;
import org.geysermc.geyser.util.EntityUtils;
import org.geysermc.geyser.util.InteractionResult;
import org.geysermc.geyser.util.InteractiveTag;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand;
import java.util.UUID;
-public class PigEntity extends AnimalEntity {
+public class PigEntity extends AnimalEntity implements Tickable, ClientVehicle {
+ private final BoostableVehicleComponent vehicleComponent = new BoostableVehicleComponent<>(this, 1.0f);
public PigEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
@@ -84,4 +94,55 @@ public class PigEntity extends AnimalEntity {
}
}
}
+
+ public void setBoost(IntEntityMetadata entityMetadata) {
+ vehicleComponent.startBoost(entityMetadata.getPrimitiveValue());
+ }
+
+ @Override
+ public void tick() {
+ PlayerEntity player = getPlayerPassenger();
+ if (player == null) {
+ return;
+ }
+
+ if (player == session.getPlayerEntity()) {
+ if (session.getPlayerInventory().isHolding(Items.CARROT_ON_A_STICK)) {
+ vehicleComponent.tickBoost();
+ }
+ } else { // getHand() for session player seems to always return air
+ ItemDefinition itemDefinition = session.getItemMappings().getStoredItems().carrotOnAStick().getBedrockDefinition();
+ if (player.getHand().getDefinition() == itemDefinition || player.getOffhand().getDefinition() == itemDefinition) {
+ vehicleComponent.tickBoost();
+ }
+ }
+ }
+
+ @Override
+ public VehicleComponent> getVehicleComponent() {
+ return vehicleComponent;
+ }
+
+ @Override
+ public Vector2f getAdjustedInput(Vector2f input) {
+ return Vector2f.UNIT_Y;
+ }
+
+ @Override
+ public float getVehicleSpeed() {
+ return vehicleComponent.getMoveSpeed() * 0.225f * vehicleComponent.getBoostMultiplier();
+ }
+
+ private @Nullable PlayerEntity getPlayerPassenger() {
+ if (getFlag(EntityFlag.SADDLED) && !passengers.isEmpty() && passengers.get(0) instanceof PlayerEntity playerEntity) {
+ return playerEntity;
+ }
+
+ return null;
+ }
+
+ @Override
+ public boolean isClientControlled() {
+ return getPlayerPassenger() == session.getPlayerEntity() && session.getPlayerInventory().isHolding(Items.CARROT_ON_A_STICK);
+ }
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/StriderEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/StriderEntity.java
index 0291f75d9..e06af2786 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/StriderEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/StriderEntity.java
@@ -27,23 +27,33 @@ package org.geysermc.geyser.entity.type.living.animal;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
+import org.cloudburstmc.math.vector.Vector2f;
import org.cloudburstmc.math.vector.Vector3f;
+import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.entity.type.Entity;
+import org.geysermc.geyser.entity.type.Tickable;
+import org.geysermc.geyser.entity.type.player.PlayerEntity;
+import org.geysermc.geyser.entity.vehicle.BoostableVehicleComponent;
+import org.geysermc.geyser.entity.vehicle.ClientVehicle;
+import org.geysermc.geyser.entity.vehicle.VehicleComponent;
import org.geysermc.geyser.inventory.GeyserItemStack;
+import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.tags.ItemTag;
import org.geysermc.geyser.util.EntityUtils;
import org.geysermc.geyser.util.InteractionResult;
import org.geysermc.geyser.util.InteractiveTag;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand;
import java.util.UUID;
-public class StriderEntity extends AnimalEntity {
+public class StriderEntity extends AnimalEntity implements Tickable, ClientVehicle {
+ private final BoostableVehicleComponent vehicleComponent = new BoostableVehicleComponent<>(this, 1.0f);
private boolean isCold = false;
public StriderEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
@@ -131,4 +141,60 @@ public class StriderEntity extends AnimalEntity {
}
}
}
+
+ public void setBoost(IntEntityMetadata entityMetadata) {
+ vehicleComponent.startBoost(entityMetadata.getPrimitiveValue());
+ }
+
+ @Override
+ public void tick() {
+ PlayerEntity player = getPlayerPassenger();
+ if (player == null) {
+ return;
+ }
+
+ if (player == session.getPlayerEntity()) {
+ if (session.getPlayerInventory().isHolding(Items.WARPED_FUNGUS_ON_A_STICK)) {
+ vehicleComponent.tickBoost();
+ }
+ } else { // getHand() for session player seems to always return air
+ ItemDefinition itemDefinition = session.getItemMappings().getStoredItems().warpedFungusOnAStick().getBedrockDefinition();
+ if (player.getHand().getDefinition() == itemDefinition || player.getOffhand().getDefinition() == itemDefinition) {
+ vehicleComponent.tickBoost();
+ }
+ }
+ }
+
+ @Override
+ public VehicleComponent> getVehicleComponent() {
+ return vehicleComponent;
+ }
+
+ @Override
+ public Vector2f getAdjustedInput(Vector2f input) {
+ return Vector2f.UNIT_Y;
+ }
+
+ @Override
+ public float getVehicleSpeed() {
+ return vehicleComponent.getMoveSpeed() * (isCold ? 0.35f : 0.55f) * vehicleComponent.getBoostMultiplier();
+ }
+
+ private @Nullable PlayerEntity getPlayerPassenger() {
+ if (getFlag(EntityFlag.SADDLED) && !passengers.isEmpty() && passengers.get(0) instanceof PlayerEntity playerEntity) {
+ return playerEntity;
+ }
+
+ return null;
+ }
+
+ @Override
+ public boolean isClientControlled() {
+ return getPlayerPassenger() == session.getPlayerEntity() && session.getPlayerInventory().isHolding(Items.WARPED_FUNGUS_ON_A_STICK);
+ }
+
+ @Override
+ public boolean canWalkOnLava() {
+ return true;
+ }
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/CamelEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/CamelEntity.java
index ee3b2be70..3c0bf1a70 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/CamelEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/living/animal/horse/CamelEntity.java
@@ -25,26 +25,36 @@
package org.geysermc.geyser.entity.type.living.animal.horse;
+import org.cloudburstmc.math.vector.Vector2f;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector3f;
+import org.cloudburstmc.protocol.bedrock.data.AttributeData;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType;
import org.cloudburstmc.protocol.bedrock.packet.EntityEventPacket;
import org.geysermc.geyser.entity.EntityDefinition;
+import org.geysermc.geyser.entity.attribute.GeyserAttributeType;
+import org.geysermc.geyser.entity.vehicle.CamelVehicleComponent;
+import org.geysermc.geyser.entity.vehicle.ClientVehicle;
+import org.geysermc.geyser.entity.vehicle.VehicleComponent;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.tags.ItemTag;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.Attribute;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.LongEntityMetadata;
import java.util.UUID;
-public class CamelEntity extends AbstractHorseEntity {
-
+public class CamelEntity extends AbstractHorseEntity implements ClientVehicle {
public static final float SITTING_HEIGHT_DIFFERENCE = 1.43F;
+ private final CamelVehicleComponent vehicleComponent = new CamelVehicleComponent(this);
+
public CamelEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
@@ -111,5 +121,58 @@ public class CamelEntity extends AbstractHorseEntity {
}
public void setDashing(BooleanEntityMetadata entityMetadata) {
+ // Java sends true to show dash animation and start the dash cooldown,
+ // false ends the dash animation, not the cooldown.
+ // Bedrock shows dash animation if HAS_DASH_COOLDOWN is set and the camel is above ground
+ if (entityMetadata.getPrimitiveValue()) {
+ setFlag(EntityFlag.HAS_DASH_COOLDOWN, true);
+ vehicleComponent.startDashCooldown();
+ } else if (!isClientControlled()) { // Don't remove dash cooldown prematurely if client is controlling
+ setFlag(EntityFlag.HAS_DASH_COOLDOWN, false);
+ }
+ }
+
+ public void setLastPoseTick(LongEntityMetadata entityMetadata) {
+ // Tick is based on world time. If negative, the camel is sitting.
+ // Must be compared to world time to know if the camel is fully standing/sitting or transitioning.
+ vehicleComponent.setLastPoseTick(entityMetadata.getPrimitiveValue());
+ }
+
+ @Override
+ protected AttributeData calculateAttribute(Attribute javaAttribute, GeyserAttributeType type) {
+ AttributeData attributeData = super.calculateAttribute(javaAttribute, type);
+ if (javaAttribute.getType() == AttributeType.Builtin.GENERIC_JUMP_STRENGTH) {
+ vehicleComponent.setHorseJumpStrength(attributeData.getValue());
+ }
+ return attributeData;
+ }
+
+ @Override
+ public VehicleComponent> getVehicleComponent() {
+ return vehicleComponent;
+ }
+
+ @Override
+ public Vector2f getAdjustedInput(Vector2f input) {
+ return input.mul(0.5f, input.getY() < 0 ? 0.25f : 1.0f);
+ }
+
+ @Override
+ public boolean isClientControlled() {
+ return getFlag(EntityFlag.SADDLED) && !passengers.isEmpty() && passengers.get(0) == session.getPlayerEntity();
+ }
+
+ @Override
+ public float getVehicleSpeed() {
+ float moveSpeed = vehicleComponent.getMoveSpeed();
+ if (!getFlag(EntityFlag.HAS_DASH_COOLDOWN) && session.getPlayerEntity().getFlag(EntityFlag.SPRINTING)) {
+ return moveSpeed + 0.1f;
+ }
+ return moveSpeed;
+ }
+
+ @Override
+ public boolean canClimb() {
+ return false;
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java
index 475b37d48..e3214879a 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java
@@ -43,7 +43,11 @@ import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityLinkData;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
-import org.cloudburstmc.protocol.bedrock.packet.*;
+import org.cloudburstmc.protocol.bedrock.packet.AddPlayerPacket;
+import org.cloudburstmc.protocol.bedrock.packet.MovePlayerPacket;
+import org.cloudburstmc.protocol.bedrock.packet.SetEntityDataPacket;
+import org.cloudburstmc.protocol.bedrock.packet.SetEntityLinkPacket;
+import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket;
import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity;
import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.entity.EntityDefinitions;
@@ -286,7 +290,13 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
@Override
public void setPosition(Vector3f position) {
- super.setPosition(position.add(0, definition.offset(), 0));
+ if (this.bedPosition != null) {
+ // As of Bedrock 1.21.22 and Fabric 1.21.1
+ // Messes with Bedrock if we send this to the client itself, though.
+ super.setPosition(position.up(0.2f));
+ } else {
+ super.setPosition(position.add(0, definition.offset(), 0));
+ }
}
@Override
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java
index dc0545cee..f427b001a 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/SessionPlayerEntity.java
@@ -29,6 +29,7 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import lombok.Getter;
import lombok.Setter;
import org.checkerframework.checker.nullness.qual.Nullable;
+import org.cloudburstmc.math.vector.Vector2f;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.data.AttributeData;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
@@ -42,6 +43,7 @@ import org.geysermc.geyser.level.BedrockDimension;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.util.AttributeUtils;
import org.geysermc.geyser.util.DimensionUtils;
+import org.geysermc.geyser.util.MathUtils;
import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.Attribute;
import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.GlobalPos;
@@ -74,6 +76,16 @@ public class SessionPlayerEntity extends PlayerEntity {
*/
@Getter
private boolean isRidingInFront;
+ /**
+ * Used when emulating client-side vehicles
+ */
+ @Getter
+ private Vector2f vehicleInput = Vector2f.ZERO;
+ /**
+ * Used when emulating client-side vehicles
+ */
+ @Getter
+ private int vehicleJumpStrength;
private int lastAirSupply = getMaxAir();
@@ -128,7 +140,7 @@ public class SessionPlayerEntity extends PlayerEntity {
if (valid) { // Don't update during session init
session.getCollisionManager().updatePlayerBoundingBox(position);
}
- super.setPosition(position);
+ this.position = position.add(0, definition.offset(), 0);
}
/**
@@ -315,13 +327,24 @@ public class SessionPlayerEntity extends PlayerEntity {
this.setAirSupply(getMaxAir());
}
+ public void setVehicleInput(Vector2f vehicleInput) {
+ this.vehicleInput = Vector2f.from(
+ MathUtils.clamp(vehicleInput.getX(), -1.0f, 1.0f),
+ MathUtils.clamp(vehicleInput.getY(), -1.0f, 1.0f)
+ );
+ }
+
+ public void setVehicleJumpStrength(int vehicleJumpStrength) {
+ this.vehicleJumpStrength = MathUtils.constrain(vehicleJumpStrength, 0, 100);
+ }
+
private boolean isBelowVoidFloor() {
return position.getY() < voidFloorPosition();
}
public int voidFloorPosition() {
// The void floor is offset about 40 blocks below the bottom of the world
- BedrockDimension bedrockDimension = session.getChunkCache().getBedrockDimension();
+ BedrockDimension bedrockDimension = session.getBedrockDimension();
return bedrockDimension.minY() - 40;
}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/vehicle/BoostableVehicleComponent.java b/core/src/main/java/org/geysermc/geyser/entity/vehicle/BoostableVehicleComponent.java
new file mode 100644
index 000000000..41224012d
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/entity/vehicle/BoostableVehicleComponent.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.entity.vehicle;
+
+import org.cloudburstmc.math.TrigMath;
+import org.geysermc.geyser.entity.type.LivingEntity;
+
+public class BoostableVehicleComponent extends VehicleComponent {
+ private int boostLength;
+ private int boostTicks = 1;
+
+ public BoostableVehicleComponent(T vehicle, float stepHeight) {
+ super(vehicle, stepHeight);
+ }
+
+ public void startBoost(int boostLength) {
+ this.boostLength = boostLength;
+ this.boostTicks = 1;
+ }
+
+ public float getBoostMultiplier() {
+ if (isBoosting()) {
+ return 1.0f + 1.15f * TrigMath.sin((float) boostTicks / (float) boostLength * TrigMath.PI);
+ }
+ return 1.0f;
+ }
+
+ public boolean isBoosting() {
+ return boostTicks <= boostLength;
+ }
+
+ public void tickBoost() {
+ if (isBoosting()) {
+ boostTicks++;
+ }
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/vehicle/CamelVehicleComponent.java b/core/src/main/java/org/geysermc/geyser/entity/vehicle/CamelVehicleComponent.java
new file mode 100644
index 000000000..7d022ed7c
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/entity/vehicle/CamelVehicleComponent.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.entity.vehicle;
+
+import lombok.Setter;
+import org.cloudburstmc.math.vector.Vector2f;
+import org.cloudburstmc.math.vector.Vector3f;
+import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
+import org.geysermc.geyser.entity.type.living.animal.horse.CamelEntity;
+import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect;
+
+public class CamelVehicleComponent extends VehicleComponent {
+ private static final int STANDING_TICKS = 52;
+ private static final int DASH_TICKS = 55;
+
+ @Setter
+ private float horseJumpStrength = 0.42f; // Not sent by vanilla Java server when spawned
+
+ @Setter
+ private long lastPoseTick;
+
+ private int dashTick;
+ private int effectJumpBoost;
+
+ public CamelVehicleComponent(CamelEntity vehicle) {
+ super(vehicle, 1.5f);
+ }
+
+ public void startDashCooldown() {
+ // tickVehicle is only called while the vehicle is mounted. Use session ticks to keep
+ // track of time instead of counting down
+ this.dashTick = vehicle.getSession().getTicks() + DASH_TICKS;
+ }
+
+ @Override
+ public void tickVehicle() {
+ if (this.dashTick != 0) {
+ if (vehicle.getSession().getTicks() > this.dashTick) {
+ vehicle.setFlag(EntityFlag.HAS_DASH_COOLDOWN, false);
+ this.dashTick = 0;
+ } else {
+ vehicle.setFlag(EntityFlag.HAS_DASH_COOLDOWN, true);
+ }
+ }
+
+ vehicle.setFlag(EntityFlag.CAN_DASH, vehicle.getFlag(EntityFlag.SADDLED) && !isStationary());
+ vehicle.updateBedrockMetadata();
+ super.tickVehicle();
+ }
+
+ @Override
+ public void onDismount() {
+ // Prevent camel from getting stuck in dash animation
+ vehicle.setFlag(EntityFlag.HAS_DASH_COOLDOWN, false);
+ vehicle.updateBedrockMetadata();
+ super.onDismount();
+ }
+
+ @Override
+ protected boolean travel(VehicleContext ctx, float speed) {
+ if (vehicle.isOnGround() && isStationary()) {
+ vehicle.setMotion(vehicle.getMotion().mul(0, 1, 0));
+ }
+
+ return super.travel(ctx, speed);
+ }
+
+ @Override
+ protected Vector3f getInputVelocity(VehicleContext ctx, float speed) {
+ if (isStationary()) {
+ return Vector3f.ZERO;
+ }
+
+ SessionPlayerEntity player = vehicle.getSession().getPlayerEntity();
+ Vector3f inputVelocity = super.getInputVelocity(ctx, speed);
+ float jumpStrength = player.getVehicleJumpStrength();
+
+ if (jumpStrength > 0) {
+ player.setVehicleJumpStrength(0);
+
+ if (jumpStrength >= 90) {
+ jumpStrength = 1.0f;
+ } else {
+ jumpStrength = 0.4f + 0.4f * jumpStrength / 90.0f;
+ }
+
+ return inputVelocity.add(Vector3f.createDirectionDeg(0, -player.getYaw())
+ .mul(22.2222f * jumpStrength * this.moveSpeed * getVelocityMultiplier(ctx))
+ .up(1.4285f * jumpStrength * (this.horseJumpStrength * getJumpVelocityMultiplier(ctx) + (this.effectJumpBoost * 0.1f))));
+ }
+
+ return inputVelocity;
+ }
+
+ @Override
+ protected Vector2f getVehicleRotation() {
+ if (isStationary()) {
+ return Vector2f.from(vehicle.getYaw(), vehicle.getPitch());
+ }
+ return super.getVehicleRotation();
+ }
+
+ /**
+ * Checks if the camel is sitting
+ * or transitioning to standing pose.
+ */
+ private boolean isStationary() {
+ // Java checks if sitting using lastPoseTick
+ return this.lastPoseTick < 0 || vehicle.getSession().getWorldTicks() < this.lastPoseTick + STANDING_TICKS;
+ }
+
+ @Override
+ public void setEffect(Effect effect, int effectAmplifier) {
+ if (effect == Effect.JUMP_BOOST) {
+ effectJumpBoost = effectAmplifier + 1;
+ } else {
+ super.setEffect(effect, effectAmplifier);
+ }
+ }
+
+ @Override
+ public void removeEffect(Effect effect) {
+ if (effect == Effect.JUMP_BOOST) {
+ effectJumpBoost = 0;
+ } else {
+ super.removeEffect(effect);
+ }
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/vehicle/ClientVehicle.java b/core/src/main/java/org/geysermc/geyser/entity/vehicle/ClientVehicle.java
new file mode 100644
index 000000000..e6aaf1daa
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/entity/vehicle/ClientVehicle.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.entity.vehicle;
+
+import org.cloudburstmc.math.vector.Vector2f;
+
+public interface ClientVehicle {
+ VehicleComponent> getVehicleComponent();
+
+ Vector2f getAdjustedInput(Vector2f input);
+
+ float getVehicleSpeed();
+
+ boolean isClientControlled();
+
+ default boolean canWalkOnLava() {
+ return false;
+ }
+
+ default boolean canClimb() {
+ return true;
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/vehicle/VehicleComponent.java b/core/src/main/java/org/geysermc/geyser/entity/vehicle/VehicleComponent.java
new file mode 100644
index 000000000..db703a3cb
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/entity/vehicle/VehicleComponent.java
@@ -0,0 +1,964 @@
+/*
+ * Copyright (c) 2019-2023 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.entity.vehicle;
+
+import it.unimi.dsi.fastutil.objects.ObjectDoublePair;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.cloudburstmc.math.TrigMath;
+import org.cloudburstmc.math.vector.Vector2f;
+import org.cloudburstmc.math.vector.Vector3d;
+import org.cloudburstmc.math.vector.Vector3f;
+import org.cloudburstmc.math.vector.Vector3i;
+import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
+import org.cloudburstmc.protocol.bedrock.packet.MoveEntityDeltaPacket;
+import org.geysermc.erosion.util.BlockPositionIterator;
+import org.geysermc.geyser.entity.type.LivingEntity;
+import org.geysermc.geyser.level.block.BlockStateValues;
+import org.geysermc.geyser.level.block.Blocks;
+import org.geysermc.geyser.level.block.Fluid;
+import org.geysermc.geyser.level.block.property.Properties;
+import org.geysermc.geyser.level.block.type.BedBlock;
+import org.geysermc.geyser.level.block.type.Block;
+import org.geysermc.geyser.level.block.type.BlockState;
+import org.geysermc.geyser.level.block.type.TrapDoorBlock;
+import org.geysermc.geyser.level.physics.BoundingBox;
+import org.geysermc.geyser.level.physics.CollisionManager;
+import org.geysermc.geyser.level.physics.Direction;
+import org.geysermc.geyser.session.cache.tags.BlockTag;
+import org.geysermc.geyser.translator.collision.BlockCollision;
+import org.geysermc.geyser.translator.collision.SolidCollision;
+import org.geysermc.geyser.util.BlockUtils;
+import org.geysermc.geyser.util.MathUtils;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.attribute.AttributeType;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType;
+import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.level.ServerboundMoveVehiclePacket;
+
+public class VehicleComponent {
+ private static final ObjectDoublePair EMPTY_FLUID_PAIR = ObjectDoublePair.of(Fluid.EMPTY, 0.0);
+ private static final float MAX_LOGICAL_FLUID_HEIGHT = 8.0f / BlockStateValues.NUM_FLUID_LEVELS;
+ private static final float BASE_SLIPPERINESS_CUBED = 0.6f * 0.6f * 0.6f;
+ private static final float MIN_VELOCITY = 0.003f;
+
+ protected final T vehicle;
+ protected final BoundingBox boundingBox;
+
+ protected float stepHeight;
+ protected float moveSpeed;
+ protected double gravity;
+ protected int effectLevitation;
+ protected boolean effectSlowFalling;
+ protected boolean effectWeaving;
+
+ public VehicleComponent(T vehicle, float stepHeight) {
+ this.vehicle = vehicle;
+ this.stepHeight = stepHeight;
+ this.moveSpeed = (float) AttributeType.Builtin.GENERIC_MOVEMENT_SPEED.getDef();
+ this.gravity = AttributeType.Builtin.GENERIC_GRAVITY.getDef();
+
+ double width = vehicle.getBoundingBoxWidth();
+ double height = vehicle.getBoundingBoxHeight();
+ this.boundingBox = new BoundingBox(
+ vehicle.getPosition().getX(),
+ vehicle.getPosition().getY() + height / 2,
+ vehicle.getPosition().getZ(),
+ width, height, width
+ );
+ }
+
+ public void setWidth(float width) {
+ boundingBox.setSizeX(width);
+ boundingBox.setSizeZ(width);
+ }
+
+ public void setHeight(float height) {
+ boundingBox.translate(0, (height - boundingBox.getSizeY()) / 2, 0);
+ boundingBox.setSizeY(height);
+ }
+
+ public void moveAbsolute(double x, double y, double z) {
+ boundingBox.setMiddleX(x);
+ boundingBox.setMiddleY(y + boundingBox.getSizeY() / 2);
+ boundingBox.setMiddleZ(z);
+ }
+
+ public void moveRelative(double x, double y, double z) {
+ boundingBox.translate(x, y, z);
+ }
+
+ public void moveRelative(Vector3d vec) {
+ boundingBox.translate(vec);
+ }
+
+ public BoundingBox getBoundingBox() {
+ return this.boundingBox;
+ }
+
+ public void setEffect(Effect effect, int effectAmplifier) {
+ switch (effect) {
+ case LEVITATION -> effectLevitation = effectAmplifier + 1;
+ case SLOW_FALLING -> effectSlowFalling = true;
+ case WEAVING -> effectWeaving = true;
+ }
+ }
+
+ public void removeEffect(Effect effect) {
+ switch (effect) {
+ case LEVITATION -> effectLevitation = 0;
+ case SLOW_FALLING -> effectSlowFalling = false;
+ case WEAVING -> effectWeaving = false;
+ }
+ }
+
+ public void setMoveSpeed(float moveSpeed) {
+ this.moveSpeed = moveSpeed;
+ }
+
+ public float getMoveSpeed() {
+ return moveSpeed;
+ }
+
+ public void setStepHeight(float stepHeight) {
+ this.stepHeight = MathUtils.clamp(stepHeight, 1.0f, 10.0f);
+ }
+
+ public void setGravity(double gravity) {
+ this.gravity = MathUtils.constrain(gravity, -1.0, 1.0);
+ }
+
+ public Vector3d correctMovement(Vector3d movement) {
+ return vehicle.getSession().getCollisionManager().correctMovement(
+ movement, boundingBox, vehicle.isOnGround(), this.stepHeight, true, vehicle.canWalkOnLava()
+ );
+ }
+
+ public void onMount() {
+ vehicle.getSession().getPlayerEntity().setVehicleInput(Vector2f.ZERO);
+ vehicle.getSession().getPlayerEntity().setVehicleJumpStrength(0);
+ }
+
+ public void onDismount() {
+ //
+ }
+
+ /**
+ * Called every session tick while the player is mounted on the vehicle.
+ */
+ public void tickVehicle() {
+ if (!vehicle.isClientControlled()) {
+ return;
+ }
+
+ VehicleContext ctx = new VehicleContext();
+ ctx.loadSurroundingBlocks();
+
+ ObjectDoublePair fluidHeight = updateFluidMovement(ctx);
+ switch (fluidHeight.left()) {
+ case WATER -> waterMovement(ctx);
+ case LAVA -> {
+ if (vehicle.canWalkOnLava() && ctx.centerBlock().is(Blocks.LAVA)) {
+ landMovement(ctx);
+ } else {
+ lavaMovement(ctx, fluidHeight.rightDouble());
+ }
+ }
+ case EMPTY -> landMovement(ctx);
+ }
+ }
+
+ /**
+ * Adds velocity of all colliding fluids to the vehicle, and returns the height of the fluid to use for movement.
+ *
+ * @param ctx context
+ * @return type and height of fluid to use for movement
+ */
+ protected ObjectDoublePair updateFluidMovement(VehicleContext ctx) {
+ BoundingBox box = boundingBox.clone();
+ box.expand(-0.001);
+
+ Vector3d min = box.getMin();
+ Vector3d max = box.getMax();
+
+ BlockPositionIterator iter = BlockPositionIterator.fromMinMax(min.getFloorX(), min.getFloorY(), min.getFloorZ(), max.getFloorX(), max.getFloorY(), max.getFloorZ());
+
+ double waterHeight = getFluidHeightAndApplyMovement(ctx, iter, Fluid.WATER, 0.014, min.getY());
+ double lavaHeight = getFluidHeightAndApplyMovement(ctx, iter, Fluid.LAVA, vehicle.getSession().getDimensionType().ultrawarm() ? 0.007 : 0.007 / 3, min.getY());
+
+ // Apply upward motion if the vehicle is a Strider, and it is submerged in lava
+ if (lavaHeight > 0 && vehicle.getDefinition().entityType() == EntityType.STRIDER) {
+ Vector3i blockPos = ctx.centerPos().toInt();
+ if (!CollisionManager.FLUID_COLLISION.isBelow(blockPos.getY(), boundingBox) || ctx.getBlock(blockPos.up()).is(Blocks.LAVA)) {
+ vehicle.setMotion(vehicle.getMotion().mul(0.5f).add(0, 0.05f, 0));
+ } else {
+ vehicle.setOnGround(true);
+ }
+ }
+
+ // Water movement has priority over lava movement
+ if (waterHeight > 0) {
+ return ObjectDoublePair.of(Fluid.WATER, waterHeight);
+ }
+
+ if (lavaHeight > 0) {
+ return ObjectDoublePair.of(Fluid.LAVA, lavaHeight);
+ }
+
+ return EMPTY_FLUID_PAIR;
+ }
+
+ /**
+ * Calculates how deep the vehicle is in a fluid, and applies its velocity.
+ *
+ * @param ctx context
+ * @param iter iterator of colliding blocks
+ * @param fluid type of fluid
+ * @param speed multiplier for fluid motion
+ * @param minY minY of the bounding box used to check for fluid collision; not exactly the same as the vehicle's bounding box
+ * @return height of fluid compared to minY
+ */
+ protected double getFluidHeightAndApplyMovement(VehicleContext ctx, BlockPositionIterator iter, Fluid fluid, double speed, double minY) {
+ Vector3d totalVelocity = Vector3d.ZERO;
+ double maxFluidHeight = 0;
+ int fluidBlocks = 0;
+
+ for (iter.reset(); iter.hasNext(); iter.next()) {
+ int blockId = ctx.getBlockId(iter);
+ if (BlockStateValues.getFluid(blockId) != fluid) {
+ continue;
+ }
+
+ Vector3i blockPos = Vector3i.from(iter.getX(), iter.getY(), iter.getZ());
+ float worldFluidHeight = getWorldFluidHeight(fluid, blockId);
+
+ double vehicleFluidHeight = blockPos.getY() + worldFluidHeight - minY;
+ if (vehicleFluidHeight < 0) {
+ // Vehicle is not submerged in this fluid block
+ continue;
+ }
+
+ // flowBlocked is only used when determining if a falling fluid should drag the vehicle downwards.
+ // If this block is not a falling fluid, set to true to avoid unnecessary checks.
+ boolean flowBlocked = worldFluidHeight != 1;
+
+ Vector3d velocity = Vector3d.ZERO;
+ for (Direction direction : Direction.HORIZONTAL) {
+ Vector3i adjacentBlockPos = blockPos.add(direction.getUnitVector());
+ int adjacentBlockId = ctx.getBlockId(adjacentBlockPos);
+ Fluid adjacentFluid = BlockStateValues.getFluid(adjacentBlockId);
+
+ float fluidHeightDiff = 0;
+ if (adjacentFluid == fluid) {
+ fluidHeightDiff = getLogicalFluidHeight(fluid, blockId) - getLogicalFluidHeight(fluid, adjacentBlockId);
+ } else if (adjacentFluid == Fluid.EMPTY) {
+ // If the adjacent block is not a fluid and does not have collision,
+ // check if there is a fluid under it
+ BlockCollision adjacentBlockCollision = BlockUtils.getCollision(adjacentBlockId);
+ if (adjacentBlockCollision == null) {
+ float adjacentFluidHeight = getLogicalFluidHeight(fluid, ctx.getBlockId(adjacentBlockPos.add(Direction.DOWN.getUnitVector())));
+ if (adjacentFluidHeight != -1) { // Only care about same type of fluid
+ fluidHeightDiff = getLogicalFluidHeight(fluid, blockId) - (adjacentFluidHeight - MAX_LOGICAL_FLUID_HEIGHT);
+ }
+ } else if (!flowBlocked) {
+ // No need to check if flow is already blocked from another direction, or if this isn't a falling fluid.
+ flowBlocked = isFlowBlocked(fluid, adjacentBlockId);
+ }
+ }
+
+ if (fluidHeightDiff != 0) {
+ velocity = velocity.add(direction.getUnitVector().toDouble().mul(fluidHeightDiff));
+ }
+ }
+
+ if (worldFluidHeight == 1) { // If falling fluid
+ // If flow is not blocked, check if it is blocked for the fluid above
+ if (!flowBlocked) {
+ Vector3i blockPosUp = blockPos.up();
+ for (Direction direction : Direction.HORIZONTAL) {
+ flowBlocked = isFlowBlocked(fluid, ctx.getBlockId(blockPosUp.add(direction.getUnitVector())));
+ if (flowBlocked) {
+ break;
+ }
+ }
+ }
+
+ if (flowBlocked) {
+ velocity = javaNormalize(velocity).add(0.0, -6.0, 0.0);
+ }
+ }
+
+ velocity = javaNormalize(velocity);
+
+ maxFluidHeight = Math.max(vehicleFluidHeight, maxFluidHeight);
+ if (maxFluidHeight < 0.4) {
+ velocity = velocity.mul(maxFluidHeight);
+ }
+
+ totalVelocity = totalVelocity.add(velocity);
+ fluidBlocks++;
+ }
+
+ if (!totalVelocity.equals(Vector3d.ZERO)) {
+ Vector3f motion = vehicle.getMotion();
+
+ totalVelocity = javaNormalize(totalVelocity.mul(1.0 / fluidBlocks));
+ totalVelocity = totalVelocity.mul(speed);
+
+ if (totalVelocity.length() < 0.0045 && Math.abs(motion.getX()) < MIN_VELOCITY && Math.abs(motion.getZ()) < MIN_VELOCITY) {
+ totalVelocity = javaNormalize(totalVelocity).mul(0.0045);
+ }
+
+ vehicle.setMotion(motion.add(totalVelocity.toFloat()));
+ }
+
+ return maxFluidHeight;
+ }
+
+ /**
+ * Java edition returns the zero vector if the length of the input vector is less than 0.0001
+ */
+ protected Vector3d javaNormalize(Vector3d vec) {
+ double len = vec.length();
+ return len < 1.0E-4 ? Vector3d.ZERO : Vector3d.from(vec.getX() / len, vec.getY() / len, vec.getZ() / len);
+ }
+
+ protected float getWorldFluidHeight(Fluid fluidType, int blockId) {
+ return (float) switch (fluidType) {
+ case WATER -> BlockStateValues.getWaterHeight(blockId);
+ case LAVA -> BlockStateValues.getLavaHeight(blockId);
+ case EMPTY -> -1;
+ };
+ }
+
+ protected float getLogicalFluidHeight(Fluid fluidType, int blockId) {
+ return Math.min(getWorldFluidHeight(fluidType, blockId), MAX_LOGICAL_FLUID_HEIGHT);
+ }
+
+ protected boolean isFlowBlocked(Fluid fluid, int adjacentBlockId) {
+ if (BlockState.of(adjacentBlockId).is(Blocks.ICE)) {
+ return false;
+ }
+
+ if (BlockStateValues.getFluid(adjacentBlockId) == fluid) {
+ return false;
+ }
+
+ // TODO: supposed to check if the opposite face of the block touching the fluid is solid, instead of SolidCollision
+ return BlockUtils.getCollision(adjacentBlockId) instanceof SolidCollision;
+ }
+
+ protected void waterMovement(VehicleContext ctx) {
+ double gravity = getGravity();
+ float drag = vehicle.getFlag(EntityFlag.SPRINTING) ? 0.9f : 0.8f; // 0.8f: getBaseMovementSpeedMultiplier
+ double originalY = ctx.centerPos().getY();
+ boolean falling = vehicle.getMotion().getY() <= 0;
+
+ // NOT IMPLEMENTED: depth strider and dolphins grace
+
+ boolean horizontalCollision = travel(ctx, 0.02f);
+
+ if (horizontalCollision && isClimbing(ctx)) {
+ vehicle.setMotion(Vector3f.from(vehicle.getMotion().getX(), 0.2f, vehicle.getMotion().getZ()));
+ }
+
+ vehicle.setMotion(vehicle.getMotion().mul(drag, 0.8f, drag));
+ vehicle.setMotion(getFluidGravity(gravity, falling));
+
+ if (horizontalCollision && shouldApplyFluidJumpBoost(ctx, originalY)) {
+ vehicle.setMotion(Vector3f.from(vehicle.getMotion().getX(), 0.3f, vehicle.getMotion().getZ()));
+ }
+ }
+
+ protected void lavaMovement(VehicleContext ctx, double lavaHeight) {
+ double gravity = getGravity();
+ double originalY = ctx.centerPos().getY();
+ boolean falling = vehicle.getMotion().getY() <= 0;
+
+ boolean horizontalCollision = travel(ctx, 0.02f);
+
+ if (lavaHeight <= (boundingBox.getSizeY() * 0.85 < 0.4 ? 0.0 : 0.4)) { // Swim height
+ vehicle.setMotion(vehicle.getMotion().mul(0.5f, 0.8f, 0.5f));
+ vehicle.setMotion(getFluidGravity(gravity, falling));
+ } else {
+ vehicle.setMotion(vehicle.getMotion().mul(0.5f));
+ }
+
+ vehicle.setMotion(vehicle.getMotion().down((float) (gravity / 4.0)));
+
+ if (horizontalCollision && shouldApplyFluidJumpBoost(ctx, originalY)) {
+ vehicle.setMotion(Vector3f.from(vehicle.getMotion().getX(), 0.3f, vehicle.getMotion().getZ()));
+ }
+ }
+
+ protected void landMovement(VehicleContext ctx) {
+ double gravity = getGravity();
+ float slipperiness = BlockStateValues.getSlipperiness(getVelocityBlock(ctx));
+ float drag = vehicle.isOnGround() ? 0.91f * slipperiness : 0.91f;
+ float speed = vehicle.getVehicleSpeed() * (vehicle.isOnGround() ? BASE_SLIPPERINESS_CUBED / (slipperiness * slipperiness * slipperiness) : 0.1f);
+
+ boolean horizontalCollision = travel(ctx, speed);
+
+ if (isClimbing(ctx)) {
+ Vector3f motion = vehicle.getMotion();
+ vehicle.setMotion(
+ Vector3f.from(
+ MathUtils.clamp(motion.getX(), -0.15f, 0.15f),
+ horizontalCollision ? 0.2f : Math.max(motion.getY(), -0.15f),
+ MathUtils.clamp(motion.getZ(), -0.15f, 0.15f)
+ )
+ );
+ // NOT IMPLEMENTED: climbing in powdered snow
+ }
+
+ if (effectLevitation > 0) {
+ vehicle.setMotion(vehicle.getMotion().up((0.05f * effectLevitation - vehicle.getMotion().getY()) * 0.2f));
+ } else {
+ vehicle.setMotion(vehicle.getMotion().down((float) gravity));
+ // NOT IMPLEMENTED: slow fall when in unloaded chunk
+ }
+
+ vehicle.setMotion(vehicle.getMotion().mul(drag, 0.98f, drag));
+ }
+
+ protected boolean shouldApplyFluidJumpBoost(VehicleContext ctx, double originalY) {
+ BoundingBox box = boundingBox.clone();
+ box.translate(vehicle.getMotion().toDouble().up(0.6f - ctx.centerPos().getY() + originalY));
+ box.expand(-1.0E-7);
+
+ BlockPositionIterator iter = vehicle.getSession().getCollisionManager().collidableBlocksIterator(box);
+ for (iter.reset(); iter.hasNext(); iter.next()) {
+ int blockId = ctx.getBlockId(iter);
+
+ // Also check for fluids
+ BlockCollision blockCollision = BlockUtils.getCollision(blockId);
+ if (blockCollision == null && BlockStateValues.getFluid(blockId) != Fluid.EMPTY) {
+ blockCollision = CollisionManager.SOLID_COLLISION;
+ }
+
+ if (blockCollision != null && blockCollision.checkIntersection(iter.getX(), iter.getY(), iter.getZ(), box)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ protected Vector3f getFluidGravity(double gravity, boolean falling) {
+ Vector3f motion = vehicle.getMotion();
+ if (gravity != 0 && !vehicle.getFlag(EntityFlag.SPRINTING)) {
+ float newY = (float) (motion.getY() - gravity / 16);
+ if (falling && Math.abs(motion.getY() - 0.005f) >= MIN_VELOCITY && Math.abs(newY) < MIN_VELOCITY) {
+ newY = -MIN_VELOCITY;
+ }
+ return Vector3f.from(motion.getX(), newY, motion.getZ());
+ }
+ return motion;
+ }
+
+ /**
+ * Check if any blocks the vehicle is colliding with should multiply movement. (Cobweb, powder snow, berry bush)
+ *
+ * This is different from the speed factor of a block the vehicle is standing on, such as soul sand.
+ *
+ * @param ctx context
+ * @return the multiplier
+ */
+ protected @Nullable Vector3f getBlockMovementMultiplier(VehicleContext ctx) {
+ BoundingBox box = boundingBox.clone();
+ box.expand(-1.0E-7);
+
+ Vector3i min = box.getMin().toInt();
+ Vector3i max = box.getMax().toInt();
+
+ // Iterate xyz backwards
+ // Minecraft iterates forwards but only the last multiplier affects movement
+ for (int x = max.getX(); x >= min.getX(); x--) {
+ for (int y = max.getY(); y >= min.getY(); y--) {
+ for (int z = max.getZ(); z >= min.getZ(); z--) {
+ Block block = ctx.getBlock(x, y, z).block();
+ Vector3f multiplier = null;
+
+ if (block == Blocks.COBWEB) {
+ if (effectWeaving) {
+ multiplier = Vector3f.from(0.5, 0.25, 0.5);
+ } else {
+ multiplier = Vector3f.from(0.25, 0.05f, 0.25);
+ }
+ } else if (block == Blocks.POWDER_SNOW) {
+ multiplier = Vector3f.from(0.9f, 1.5, 0.9f);
+ } else if (block == Blocks.SWEET_BERRY_BUSH) {
+ multiplier = Vector3f.from(0.8f, 0.75, 0.8f);
+ }
+
+ if (multiplier != null) {
+ return multiplier;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ protected void applyBlockCollisionEffects(VehicleContext ctx) {
+ BoundingBox box = boundingBox.clone();
+ box.expand(-1.0E-7);
+
+ Vector3i min = box.getMin().toInt();
+ Vector3i max = box.getMax().toInt();
+
+ BlockPositionIterator iter = BlockPositionIterator.fromMinMax(min.getX(), min.getY(), min.getZ(), max.getX(), max.getY(), max.getZ());
+ for (iter.reset(); iter.hasNext(); iter.next()) {
+ BlockState blockState = ctx.getBlock(iter);
+
+ if (blockState.is(Blocks.HONEY_BLOCK)) {
+ onHoneyBlockCollision();
+ } else if (blockState.is(Blocks.BUBBLE_COLUMN)) {
+ onBubbleColumnCollision(blockState.getValue(Properties.DRAG));
+ }
+ }
+ }
+
+ protected void onHoneyBlockCollision() {
+ if (vehicle.isOnGround() || vehicle.getMotion().getY() >= -0.08f) {
+ return;
+ }
+
+ // NOT IMPLEMENTED: don't slide if inside the honey block
+ Vector3f motion = vehicle.getMotion();
+ float mul = motion.getY() < -0.13f ? -0.05f / motion.getY() : 1;
+ vehicle.setMotion(Vector3f.from(motion.getX() * mul, -0.05f, motion.getZ() * mul));
+ }
+
+ protected void onBubbleColumnCollision(boolean drag) {
+ Vector3f motion = vehicle.getMotion();
+ vehicle.setMotion(Vector3f.from(
+ motion.getX(),
+ drag ? Math.max(-0.3f, motion.getY() - 0.03f) : Math.min(0.7f, motion.getY() + 0.06f),
+ motion.getZ()
+ ));
+ }
+
+ /**
+ * Calculates the next position of the vehicle while checking for collision and adjusting velocity.
+ *
+ * @return true if there was a horizontal collision
+ */
+ protected boolean travel(VehicleContext ctx, float speed) {
+ Vector3f motion = vehicle.getMotion();
+
+ // Java only does this client side
+ motion = motion.mul(0.98f);
+
+ motion = Vector3f.from(
+ Math.abs(motion.getX()) < MIN_VELOCITY ? 0 : motion.getX(),
+ Math.abs(motion.getY()) < MIN_VELOCITY ? 0 : motion.getY(),
+ Math.abs(motion.getZ()) < MIN_VELOCITY ? 0 : motion.getZ()
+ );
+
+ // !isImmobile
+ if (vehicle.isAlive()) {
+ motion = motion.add(getInputVelocity(ctx, speed));
+ }
+
+ Vector3f movementMultiplier = getBlockMovementMultiplier(ctx);
+ if (movementMultiplier != null) {
+ motion = motion.mul(movementMultiplier);
+ }
+
+ // Check world border before blocks
+ Vector3d correctedMovement = vehicle.getSession().getWorldBorder().correctMovement(boundingBox, motion.toDouble());
+ correctedMovement = vehicle.getSession().getCollisionManager().correctMovement(
+ correctedMovement, boundingBox, vehicle.isOnGround(), this.stepHeight, true, vehicle.canWalkOnLava()
+ );
+
+ boundingBox.translate(correctedMovement);
+ ctx.loadSurroundingBlocks(); // Context must be reloaded after vehicle is moved
+
+ // Non-zero values indicate a collision on that axis
+ Vector3d moveDiff = motion.toDouble().sub(correctedMovement);
+
+ vehicle.setOnGround(moveDiff.getY() != 0 && motion.getY() < 0);
+ boolean horizontalCollision = moveDiff.getX() != 0 || moveDiff.getZ() != 0;
+
+ boolean bounced = false;
+ if (vehicle.isOnGround()) {
+ Block landingBlock = getLandingBlock(ctx).block();
+
+ if (landingBlock == Blocks.SLIME_BLOCK) {
+ motion = Vector3f.from(motion.getX(), -motion.getY(), motion.getZ());
+ bounced = true;
+
+ // Slow horizontal movement
+ float absY = Math.abs(motion.getY());
+ if (absY < 0.1f) {
+ float mul = 0.4f + absY * 0.2f;
+ motion = motion.mul(mul, 1.0f, mul);
+ }
+ } else if (landingBlock instanceof BedBlock) {
+ motion = Vector3f.from(motion.getX(), -motion.getY() * 0.66f, motion.getZ());
+ bounced = true;
+ }
+ }
+
+ // Set motion to 0 if a movement multiplier was used, else set to 0 on each axis with a collision
+ if (movementMultiplier != null) {
+ motion = Vector3f.ZERO;
+ } else {
+ motion = motion.mul(
+ moveDiff.getX() == 0 ? 1 : 0,
+ moveDiff.getY() == 0 || bounced ? 1 : 0,
+ moveDiff.getZ() == 0 ? 1 : 0
+ );
+ }
+
+ // Send the new position to the bedrock client and java server
+ moveVehicle(ctx.centerPos());
+ vehicle.setMotion(motion);
+
+ applyBlockCollisionEffects(ctx);
+
+ float velocityMultiplier = getVelocityMultiplier(ctx);
+ vehicle.setMotion(vehicle.getMotion().mul(velocityMultiplier, 1.0f, velocityMultiplier));
+
+ return horizontalCollision;
+ }
+
+ protected boolean isClimbing(VehicleContext ctx) {
+ if (!vehicle.canClimb()) {
+ return false;
+ }
+
+ BlockState blockState = ctx.centerBlock();
+ if (vehicle.getSession().getTagCache().is(BlockTag.CLIMBABLE, blockState.block())) {
+ return true;
+ }
+
+ // Check if the vehicle is in an open trapdoor with a ladder of the same direction under it
+ if (blockState.block() instanceof TrapDoorBlock && blockState.getValue(Properties.OPEN)) {
+ BlockState ladderState = ctx.getBlock(ctx.centerPos().toInt().down());
+ return ladderState.is(Blocks.LADDER) &&
+ ladderState.getValue(Properties.HORIZONTAL_FACING) == blockState.getValue(Properties.HORIZONTAL_FACING);
+ }
+
+ return false;
+ }
+
+ /**
+ * Translates the player's input into velocity.
+ *
+ * @param ctx context
+ * @param speed multiplier for input
+ * @return velocity
+ */
+ protected Vector3f getInputVelocity(VehicleContext ctx, float speed) {
+ Vector2f input = vehicle.getSession().getPlayerEntity().getVehicleInput();
+ input = input.mul(0.98f);
+ input = vehicle.getAdjustedInput(input);
+ input = normalizeInput(input);
+ input = input.mul(speed);
+
+ // Match player rotation
+ float yaw = vehicle.getSession().getPlayerEntity().getYaw();
+ float sin = TrigMath.sin(yaw * TrigMath.DEG_TO_RAD);
+ float cos = TrigMath.cos(yaw * TrigMath.DEG_TO_RAD);
+ return Vector3f.from(input.getX() * cos - input.getY() * sin, 0, input.getY() * cos + input.getX() * sin);
+ }
+
+ protected Vector2f normalizeInput(Vector2f input) {
+ float lenSquared = input.lengthSquared();
+ if (lenSquared < 1.0E-7) {
+ return Vector2f.ZERO;
+ } else if (lenSquared > 1.0) {
+ return input.normalize();
+ }
+ return input;
+ }
+
+ /**
+ * Gets the rotation to use for the vehicle. This is based on the player's head rotation.
+ */
+ protected Vector2f getVehicleRotation() {
+ LivingEntity player = vehicle.getSession().getPlayerEntity();
+ return Vector2f.from(player.getYaw(), player.getPitch() * 0.5f);
+ }
+
+ /**
+ * Sets the new position for the vehicle and sends packets to both the java server and bedrock client.
+ *
+ * This also updates the session's last vehicle move timestamp.
+ * @param javaPos the new java position of the vehicle
+ */
+ protected void moveVehicle(Vector3d javaPos) {
+ Vector3f bedrockPos = javaPos.toFloat();
+ Vector2f rotation = getVehicleRotation();
+
+ MoveEntityDeltaPacket moveEntityDeltaPacket = new MoveEntityDeltaPacket();
+ moveEntityDeltaPacket.setRuntimeEntityId(vehicle.getGeyserId());
+
+ if (vehicle.isOnGround()) {
+ moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.ON_GROUND);
+ }
+
+ if (vehicle.getPosition().getX() != bedrockPos.getX()) {
+ moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_X);
+ moveEntityDeltaPacket.setX(bedrockPos.getX());
+ }
+ if (vehicle.getPosition().getY() != bedrockPos.getY()) {
+ moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Y);
+ moveEntityDeltaPacket.setY(bedrockPos.getY());
+ }
+ if (vehicle.getPosition().getZ() != bedrockPos.getZ()) {
+ moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_Z);
+ moveEntityDeltaPacket.setZ(bedrockPos.getZ());
+ }
+ vehicle.setPosition(bedrockPos);
+
+ if (vehicle.getYaw() != rotation.getX()) {
+ moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_YAW);
+ moveEntityDeltaPacket.setYaw(rotation.getX());
+ vehicle.setYaw(rotation.getX());
+ }
+ if (vehicle.getPitch() != rotation.getY()) {
+ moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_PITCH);
+ moveEntityDeltaPacket.setPitch(rotation.getY());
+ vehicle.setPitch(rotation.getY());
+ }
+ if (vehicle.getHeadYaw() != rotation.getX()) { // Same as yaw
+ moveEntityDeltaPacket.getFlags().add(MoveEntityDeltaPacket.Flag.HAS_HEAD_YAW);
+ moveEntityDeltaPacket.setHeadYaw(rotation.getX());
+ vehicle.setHeadYaw(rotation.getX());
+ }
+
+ if (!moveEntityDeltaPacket.getFlags().isEmpty()) {
+ vehicle.getSession().sendUpstreamPacket(moveEntityDeltaPacket);
+ }
+
+ ServerboundMoveVehiclePacket moveVehiclePacket = new ServerboundMoveVehiclePacket(javaPos.getX(), javaPos.getY(), javaPos.getZ(), rotation.getX(), rotation.getY());
+ vehicle.getSession().sendDownstreamPacket(moveVehiclePacket);
+ vehicle.getSession().setLastVehicleMoveTimestamp(System.currentTimeMillis());
+ }
+
+ protected double getGravity() {
+ if (!vehicle.getFlag(EntityFlag.HAS_GRAVITY)) {
+ return 0;
+ }
+
+ if (vehicle.getMotion().getY() <= 0 && effectSlowFalling) {
+ return Math.min(0.01, this.gravity);
+ }
+
+ return this.gravity;
+ }
+
+ /**
+ * Finds the position of the main block supporting the vehicle.
+ * Used when determining slipperiness, speed, etc.
+ *
+ * Should use {@link VehicleContext#supportingBlockPos()}, instead of calling this directly.
+ *
+ * @param ctx context
+ * @return position of the main block supporting this entity
+ */
+ private @Nullable Vector3i getSupportingBlockPos(VehicleContext ctx) {
+ Vector3i result = null;
+
+ if (vehicle.isOnGround()) {
+ BoundingBox box = boundingBox.clone();
+ box.extend(0, -1.0E-6, 0); // Extend slightly down
+
+ Vector3i min = box.getMin().toInt();
+ Vector3i max = box.getMax().toInt();
+
+ // Use minY as maxY
+ BlockPositionIterator iter = BlockPositionIterator.fromMinMax(min.getX(), min.getY(), min.getZ(), max.getX(), min.getY(), max.getZ());
+
+ double minDistance = Double.MAX_VALUE;
+ for (iter.reset(); iter.hasNext(); iter.next()) {
+ Vector3i blockPos = Vector3i.from(iter.getX(), iter.getY(), iter.getZ());
+ int blockId = ctx.getBlockId(iter);
+
+ BlockCollision blockCollision;
+ if (vehicle.canWalkOnLava()) {
+ blockCollision = vehicle.getSession().getCollisionManager().getCollisionLavaWalking(blockId, blockPos.getY(), boundingBox);
+ } else {
+ blockCollision = BlockUtils.getCollision(blockId);
+ }
+
+ if (blockCollision != null && blockCollision.checkIntersection(blockPos, box)) {
+ double distance = ctx.centerPos().distanceSquared(blockPos.toDouble().add(0.5f, 0.5f, 0.5f));
+ if (distance <= minDistance) {
+ minDistance = distance;
+ result = blockPos;
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns the block that is x amount of blocks under the main supporting block.
+ */
+ protected BlockState getBlockUnderSupport(VehicleContext ctx, float dist) {
+ Vector3i supportingBlockPos = ctx.supportingBlockPos();
+
+ Vector3i blockPos;
+ if (supportingBlockPos != null) {
+ blockPos = Vector3i.from(supportingBlockPos.getX(), Math.floor(ctx.centerPos().getY() - dist), supportingBlockPos.getZ());
+ } else {
+ blockPos = ctx.centerPos().sub(0, dist, 0).toInt();
+ }
+
+ return ctx.getBlock(blockPos);
+ }
+
+ /**
+ * The block to use when determining if the vehicle should bounce after landing. Currently just slime and bed blocks.
+ */
+ protected BlockState getLandingBlock(VehicleContext ctx) {
+ return getBlockUnderSupport(ctx, 0.2f);
+ }
+
+ /**
+ * The block to use when calculating slipperiness and speed. If on a slab, this will be the block under the slab.
+ */
+ protected BlockState getVelocityBlock(VehicleContext ctx) {
+ return getBlockUnderSupport(ctx, 0.500001f);
+ }
+
+ protected float getVelocityMultiplier(VehicleContext ctx) {
+ Block block = ctx.centerBlock().block();
+ if (block == Blocks.WATER || block == Blocks.BUBBLE_COLUMN) {
+ return 1.0f;
+ }
+
+ if (block == Blocks.SOUL_SAND || block == Blocks.HONEY_BLOCK) {
+ return 0.4f;
+ }
+
+ block = getVelocityBlock(ctx).block();
+ if (block == Blocks.SOUL_SAND || block == Blocks.HONEY_BLOCK) {
+ return 0.4f;
+ }
+
+ return 1.0f;
+ }
+
+ protected float getJumpVelocityMultiplier(VehicleContext ctx) {
+ Block block = ctx.centerBlock().block();
+ if (block == Blocks.HONEY_BLOCK) {
+ return 0.5f;
+ }
+
+ block = getVelocityBlock(ctx).block();
+ if (block == Blocks.HONEY_BLOCK) {
+ return 0.5f;
+ }
+
+ return 1.0f;
+ }
+
+ protected class VehicleContext {
+ private Vector3d centerPos;
+ private Vector3d cachePos;
+ private BlockState centerBlock;
+ private Vector3i supportingBlockPos;
+ private BlockPositionIterator blockIter;
+ private int[] blocks;
+
+ /**
+ * Cache frequently used data and blocks used in movement calculations.
+ *
+ * Can be called multiple times, and must be called at least once before using the VehicleContext.
+ */
+ protected void loadSurroundingBlocks() {
+ this.centerPos = boundingBox.getBottomCenter();
+
+ // Reuse block cache if vehicle moved less than 1 block
+ if (this.cachePos == null || this.cachePos.distanceSquared(this.centerPos) > 1) {
+ BoundingBox box = boundingBox.clone();
+ box.expand(2);
+
+ Vector3i min = box.getMin().toInt();
+ Vector3i max = box.getMax().toInt();
+ this.blockIter = BlockPositionIterator.fromMinMax(min.getX(), min.getY(), min.getZ(), max.getX(), max.getY(), max.getZ());
+ this.blocks = vehicle.getSession().getGeyser().getWorldManager().getBlocksAt(vehicle.getSession(), this.blockIter);
+
+ this.cachePos = this.centerPos;
+ }
+
+ this.centerBlock = getBlock(this.centerPos.toInt());
+ this.supportingBlockPos = null;
+ }
+
+ protected Vector3d centerPos() {
+ return this.centerPos;
+ }
+
+ protected BlockState centerBlock() {
+ return this.centerBlock;
+ }
+
+ protected Vector3i supportingBlockPos() {
+ if (this.supportingBlockPos == null) {
+ this.supportingBlockPos = getSupportingBlockPos(this);
+ }
+
+ return this.supportingBlockPos;
+ }
+
+ protected int getBlockId(int x, int y, int z) {
+ int index = this.blockIter.getIndex(x, y, z);
+ if (index == -1) {
+ vehicle.getSession().getGeyser().getLogger().debug("[client-vehicle] Block cache miss");
+ return vehicle.getSession().getGeyser().getWorldManager().getBlockAt(vehicle.getSession(), x, y, z);
+ }
+
+ return blocks[index];
+ }
+
+ protected int getBlockId(Vector3i pos) {
+ return getBlockId(pos.getX(), pos.getY(), pos.getZ());
+ }
+
+ protected int getBlockId(BlockPositionIterator iter) {
+ return getBlockId(iter.getX(), iter.getY(), iter.getZ());
+ }
+
+ protected BlockState getBlock(int x, int y, int z) {
+ return BlockState.of(getBlockId(x, y, z));
+ }
+
+ protected BlockState getBlock(Vector3i pos) {
+ return BlockState.of(getBlockId(pos.getX(), pos.getY(), pos.getZ()));
+ }
+
+ protected BlockState getBlock(BlockPositionIterator iter) {
+ return BlockState.of(getBlockId(iter.getX(), iter.getY(), iter.getZ()));
+ }
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/erosion/ErosionCancellationException.java b/core/src/main/java/org/geysermc/geyser/erosion/ErosionCancellationException.java
new file mode 100644
index 000000000..ae283895b
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/erosion/ErosionCancellationException.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2024 GeyserMC. http://geysermc.org
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ *
+ * @author GeyserMC
+ * @link https://github.com/GeyserMC/Geyser
+ */
+
+package org.geysermc.geyser.erosion;
+
+import java.io.Serial;
+import java.util.concurrent.CancellationException;
+
+public class ErosionCancellationException extends CancellationException {
+ @Serial
+ private static final long serialVersionUID = 1L;
+}
diff --git a/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundHandshakePacketHandler.java b/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundHandshakePacketHandler.java
index 0b4f03643..88a7e0cd3 100644
--- a/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundHandshakePacketHandler.java
+++ b/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundHandshakePacketHandler.java
@@ -42,7 +42,6 @@ public final class GeyserboundHandshakePacketHandler extends AbstractGeyserbound
public void handleHandshake(GeyserboundHandshakePacket packet) {
boolean useTcp = packet.getTransportType().getSocketAddress() == null;
GeyserboundPacketHandlerImpl handler = new GeyserboundPacketHandlerImpl(session, useTcp ? new GeyserErosionPacketSender(session) : new NettyPacketSender<>());
- session.setErosionHandler(handler);
if (!useTcp) {
if (session.getGeyser().getErosionUnixListener() == null) {
session.disconnect("Erosion configurations using Unix socket handling are not supported on this hardware!");
@@ -52,6 +51,7 @@ public final class GeyserboundHandshakePacketHandler extends AbstractGeyserbound
} else {
handler.onConnect();
}
+ session.setErosionHandler(handler);
session.ensureInEventLoop(() -> session.getChunkCache().clear());
}
diff --git a/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundPacketHandlerImpl.java b/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundPacketHandlerImpl.java
index c8cbe384b..7202db449 100644
--- a/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundPacketHandlerImpl.java
+++ b/core/src/main/java/org/geysermc/geyser/erosion/GeyserboundPacketHandlerImpl.java
@@ -171,10 +171,10 @@ public final class GeyserboundPacketHandlerImpl extends AbstractGeyserboundPacke
@Override
public void handleHandshake(GeyserboundHandshakePacket packet) {
- this.close();
var handler = new GeyserboundHandshakePacketHandler(this.session);
session.setErosionHandler(handler);
handler.handleHandshake(packet);
+ this.close();
}
@Override
@@ -198,6 +198,17 @@ public final class GeyserboundPacketHandlerImpl extends AbstractGeyserboundPacke
public void close() {
this.packetSender.close();
+
+ if (pendingLookup != null) {
+ pendingLookup.completeExceptionally(new ErosionCancellationException());
+ }
+ if (pendingBatchLookup != null) {
+ pendingBatchLookup.completeExceptionally(new ErosionCancellationException());
+ }
+ if (pickBlockLookup != null) {
+ pickBlockLookup.completeExceptionally(new ErosionCancellationException());
+ }
+ asyncPendingLookups.forEach(($, future) -> future.completeExceptionally(new ErosionCancellationException()));
}
public int getNextTransactionId() {
diff --git a/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineCommandsEventImpl.java b/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineCommandsEventImpl.java
index e07a62d8a..4a6efbbd4 100644
--- a/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineCommandsEventImpl.java
+++ b/core/src/main/java/org/geysermc/geyser/event/type/GeyserDefineCommandsEventImpl.java
@@ -35,12 +35,12 @@ import java.util.Map;
public abstract class GeyserDefineCommandsEventImpl implements GeyserDefineCommandsEvent {
private final Map commands;
- public GeyserDefineCommandsEventImpl(Map commands) {
- this.commands = commands;
+ public GeyserDefineCommandsEventImpl(Map commands) {
+ this.commands = Collections.unmodifiableMap(commands);
}
@Override
public @NonNull Map commands() {
- return Collections.unmodifiableMap(this.commands);
+ return this.commands;
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionDescription.java b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionDescription.java
index 239ffc450..a84f12813 100644
--- a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionDescription.java
+++ b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionDescription.java
@@ -43,9 +43,9 @@ import java.util.regex.Pattern;
public record GeyserExtensionDescription(@NonNull String id,
@NonNull String name,
@NonNull String main,
+ int humanApiVersion,
int majorApiVersion,
int minorApiVersion,
- int patchApiVersion,
@NonNull String version,
@NonNull List authors) implements ExtensionDescription {
@@ -82,9 +82,9 @@ public record GeyserExtensionDescription(@NonNull String id,
throw new InvalidDescriptionException(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_format", name, apiVersion));
}
String[] api = apiVersion.split("\\.");
- int majorApi = Integer.parseUnsignedInt(api[0]);
- int minorApi = Integer.parseUnsignedInt(api[1]);
- int patchApi = Integer.parseUnsignedInt(api[2]);
+ int humanApi = Integer.parseUnsignedInt(api[0]);
+ int majorApi = Integer.parseUnsignedInt(api[1]);
+ int minorApi = Integer.parseUnsignedInt(api[2]);
List authors = new ArrayList<>();
if (source.author != null) {
@@ -94,7 +94,7 @@ public record GeyserExtensionDescription(@NonNull String id,
authors.addAll(source.authors);
}
- return new GeyserExtensionDescription(id, name, main, majorApi, minorApi, patchApi, version, authors);
+ return new GeyserExtensionDescription(id, name, main, humanApi, majorApi, minorApi, version, authors);
}
@NonNull
diff --git a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java
index 2f0ff1580..a56e00671 100644
--- a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java
+++ b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java
@@ -29,10 +29,15 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import lombok.RequiredArgsConstructor;
import org.checkerframework.checker.nullness.qual.NonNull;
-import org.geysermc.api.Geyser;
+import org.geysermc.api.util.ApiVersion;
import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.api.GeyserApi;
import org.geysermc.geyser.api.event.ExtensionEventBus;
-import org.geysermc.geyser.api.extension.*;
+import org.geysermc.geyser.api.extension.Extension;
+import org.geysermc.geyser.api.extension.ExtensionDescription;
+import org.geysermc.geyser.api.extension.ExtensionLoader;
+import org.geysermc.geyser.api.extension.ExtensionLogger;
+import org.geysermc.geyser.api.extension.ExtensionManager;
import org.geysermc.geyser.api.extension.exception.InvalidDescriptionException;
import org.geysermc.geyser.api.extension.exception.InvalidExtensionException;
import org.geysermc.geyser.extension.event.GeyserExtensionEventBus;
@@ -40,7 +45,12 @@ import org.geysermc.geyser.text.GeyserLocale;
import java.io.IOException;
import java.io.Reader;
-import java.nio.file.*;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
@@ -176,16 +186,22 @@ public class GeyserExtensionLoader extends ExtensionLoader {
return;
}
- // Completely different API version
- if (description.majorApiVersion() != Geyser.api().majorApiVersion()) {
- GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_version", name, description.apiVersion()));
- return;
- }
+ // Check whether an extensions' requested api version is compatible
+ ApiVersion.Compatibility compatibility = GeyserApi.api().geyserApiVersion().supportsRequestedVersion(
+ description.humanApiVersion(),
+ description.majorApiVersion(),
+ description.minorApiVersion()
+ );
- // If the extension requires new API features, being backwards compatible
- if (description.minorApiVersion() > Geyser.api().minorApiVersion()) {
- GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_version", name, description.apiVersion()));
- return;
+ if (compatibility != ApiVersion.Compatibility.COMPATIBLE) {
+ // Workaround for the switch to the Geyser API version instead of the Base API version in extensions
+ if (compatibility == ApiVersion.Compatibility.HUMAN_DIFFER && description.humanApiVersion() == 1) {
+ GeyserImpl.getInstance().getLogger().warning("The extension %s requested the Base API version %s, which is deprecated in favor of specifying the Geyser API version. Please update the extension, or contact its developer."
+ .formatted(name, description.apiVersion()));
+ } else {
+ GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_version", name, description.apiVersion()));
+ return;
+ }
}
GeyserExtensionContainer container = this.loadExtension(path, description);
diff --git a/core/src/main/java/org/geysermc/geyser/extension/command/GeyserExtensionCommand.java b/core/src/main/java/org/geysermc/geyser/extension/command/GeyserExtensionCommand.java
index 4a7830c90..0b22a8b8e 100644
--- a/core/src/main/java/org/geysermc/geyser/extension/command/GeyserExtensionCommand.java
+++ b/core/src/main/java/org/geysermc/geyser/extension/command/GeyserExtensionCommand.java
@@ -25,19 +25,208 @@
package org.geysermc.geyser.extension.command;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.geysermc.geyser.api.command.Command;
+import org.geysermc.geyser.api.command.CommandExecutor;
+import org.geysermc.geyser.api.command.CommandSource;
+import org.geysermc.geyser.api.connection.GeyserConnection;
import org.geysermc.geyser.api.extension.Extension;
+import org.geysermc.geyser.api.util.TriState;
import org.geysermc.geyser.command.GeyserCommand;
+import org.geysermc.geyser.command.GeyserCommandSource;
+import org.geysermc.geyser.session.GeyserSession;
+import org.incendo.cloud.CommandManager;
+import org.incendo.cloud.context.CommandContext;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import static org.incendo.cloud.parser.standard.StringParser.greedyStringParser;
public abstract class GeyserExtensionCommand extends GeyserCommand {
+
private final Extension extension;
+ private final String rootCommand;
- public GeyserExtensionCommand(Extension extension, String name, String description, String permission) {
- super(name, description, permission);
+ public GeyserExtensionCommand(@NonNull Extension extension, @NonNull String name, @NonNull String description,
+ @NonNull String permission, @Nullable TriState permissionDefault,
+ boolean playerOnly, boolean bedrockOnly) {
+ super(name, description, permission, permissionDefault, playerOnly, bedrockOnly);
this.extension = extension;
+ this.rootCommand = Objects.requireNonNull(extension.rootCommand());
+
+ if (this.rootCommand.isBlank()) {
+ throw new IllegalStateException("rootCommand of extension " + extension.name() + " may not be blank");
+ }
}
- public Extension extension() {
+ public final Extension extension() {
return this.extension;
}
+
+ @Override
+ public final String rootCommand() {
+ return this.rootCommand;
+ }
+
+ public static class Builder implements Command.Builder {
+ @NonNull private final Extension extension;
+ @Nullable private Class extends T> sourceType;
+ @Nullable private String name;
+ @NonNull private String description = "";
+ @NonNull private String permission = "";
+ @Nullable private TriState permissionDefault;
+ @Nullable private List aliases;
+ private boolean suggestedOpOnly = false; // deprecated for removal
+ private boolean playerOnly = false;
+ private boolean bedrockOnly = false;
+ @Nullable private CommandExecutor executor;
+
+ public Builder(@NonNull Extension extension) {
+ this.extension = Objects.requireNonNull(extension);
+ }
+
+ @Override
+ public Command.Builder source(@NonNull Class extends T> sourceType) {
+ this.sourceType = Objects.requireNonNull(sourceType, "command source type");
+ return this;
+ }
+
+ @Override
+ public Builder name(@NonNull String name) {
+ this.name = Objects.requireNonNull(name, "command name");
+ return this;
+ }
+
+ @Override
+ public Builder description(@NonNull String description) {
+ this.description = Objects.requireNonNull(description, "command description");
+ return this;
+ }
+
+ @Override
+ public Builder permission(@NonNull String permission) {
+ this.permission = Objects.requireNonNull(permission, "command permission");
+ return this;
+ }
+
+ @Override
+ public Builder permission(@NonNull String permission, @NonNull TriState defaultValue) {
+ this.permission = Objects.requireNonNull(permission, "command permission");
+ this.permissionDefault = Objects.requireNonNull(defaultValue, "command permission defaultValue");
+ return this;
+ }
+
+ @Override
+ public Builder aliases(@NonNull List aliases) {
+ this.aliases = Objects.requireNonNull(aliases, "command aliases");
+ return this;
+ }
+
+ @SuppressWarnings("removal") // this is our doing
+ @Override
+ public Builder suggestedOpOnly(boolean suggestedOpOnly) {
+ this.suggestedOpOnly = suggestedOpOnly;
+ if (suggestedOpOnly) {
+ // the most amount of legacy/deprecated behaviour I'm willing to support
+ this.permissionDefault = TriState.NOT_SET;
+ }
+ return this;
+ }
+
+ @SuppressWarnings("removal") // this is our doing
+ @Override
+ public Builder executableOnConsole(boolean executableOnConsole) {
+ this.playerOnly = !executableOnConsole;
+ return this;
+ }
+
+ @Override
+ public Command.Builder playerOnly(boolean playerOnly) {
+ this.playerOnly = playerOnly;
+ return this;
+ }
+
+ @Override
+ public Builder bedrockOnly(boolean bedrockOnly) {
+ this.bedrockOnly = bedrockOnly;
+ return this;
+ }
+
+ @Override
+ public Builder executor(@NonNull CommandExecutor executor) {
+ this.executor = Objects.requireNonNull(executor, "command executor");
+ return this;
+ }
+
+ @NonNull
+ @Override
+ public GeyserExtensionCommand build() {
+ // These are captured in the anonymous lambda below and shouldn't change even if the builder does
+ final Class extends T> sourceType = this.sourceType;
+ final boolean suggestedOpOnly = this.suggestedOpOnly;
+ final CommandExecutor executor = this.executor;
+
+ if (name == null) {
+ throw new IllegalArgumentException("name was not provided for a command in extension " + extension.name());
+ }
+ if (sourceType == null) {
+ throw new IllegalArgumentException("Source type was not defined for command " + name + " in extension " + extension.name());
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException("Command executor was not defined for command " + name + " in extension " + extension.name());
+ }
+
+ // if the source type is a GeyserConnection then it is inherently bedrockOnly
+ final boolean bedrockOnly = this.bedrockOnly || GeyserConnection.class.isAssignableFrom(sourceType);
+ // a similar check would exist for executableOnConsole, but there is not a logger type exposed in the api
+
+ GeyserExtensionCommand command = new GeyserExtensionCommand(extension, name, description, permission, permissionDefault, playerOnly, bedrockOnly) {
+
+ @Override
+ public void register(CommandManager manager) {
+ manager.command(baseBuilder(manager)
+ .optional("args", greedyStringParser())
+ .handler(this::execute));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void execute(CommandContext context) {
+ GeyserCommandSource source = context.sender();
+ String[] args = context.getOrDefault("args", "").split(" ");
+
+ if (sourceType.isInstance(source)) {
+ executor.execute((T) source, this, args);
+ return;
+ }
+
+ @Nullable GeyserSession session = source.connection();
+ if (sourceType.isInstance(session)) {
+ executor.execute((T) session, this, args);
+ return;
+ }
+
+ // currently, the only subclass of CommandSource exposed in the api is GeyserConnection.
+ // when this command was registered, we enabled bedrockOnly if the sourceType was a GeyserConnection.
+ // as a result, the permission checker should handle that case and this method shouldn't even be reached.
+ source.sendMessage("You must be a " + sourceType.getSimpleName() + " to run this command.");
+ }
+
+ @SuppressWarnings("removal") // this is our doing
+ @Override
+ public boolean isSuggestedOpOnly() {
+ return suggestedOpOnly;
+ }
+ };
+
+ if (aliases != null) {
+ command.aliases = new ArrayList<>(aliases);
+ }
+ return command;
+ }
+ }
}
diff --git a/core/src/main/java/org/geysermc/geyser/impl/camera/CameraDefinitions.java b/core/src/main/java/org/geysermc/geyser/impl/camera/CameraDefinitions.java
index 80564bdf3..1cf6a794e 100644
--- a/core/src/main/java/org/geysermc/geyser/impl/camera/CameraDefinitions.java
+++ b/core/src/main/java/org/geysermc/geyser/impl/camera/CameraDefinitions.java
@@ -43,13 +43,13 @@ public class CameraDefinitions {
static {
CAMERA_PRESETS = List.of(
- new CameraPreset(CameraPerspective.FIRST_PERSON.id(), "", null, null, null, null, OptionalBoolean.empty()),
- new CameraPreset(CameraPerspective.FREE.id(), "", null, null, null, null, OptionalBoolean.empty()),
- new CameraPreset(CameraPerspective.THIRD_PERSON.id(), "", null, null, null, null, OptionalBoolean.empty()),
- new CameraPreset(CameraPerspective.THIRD_PERSON_FRONT.id(), "", null, null, null, null, OptionalBoolean.empty()),
- new CameraPreset("geyser:free_audio", "minecraft:free", null, null, null, CameraAudioListener.PLAYER, OptionalBoolean.of(false)),
- new CameraPreset("geyser:free_effects", "minecraft:free", null, null, null, CameraAudioListener.CAMERA, OptionalBoolean.of(true)),
- new CameraPreset("geyser:free_audio_effects", "minecraft:free", null, null, null, CameraAudioListener.PLAYER, OptionalBoolean.of(true)));
+ new CameraPreset(CameraPerspective.FIRST_PERSON.id(), "", null, null, null, null, null, null, OptionalBoolean.empty(), null, OptionalBoolean.empty(), null),
+ new CameraPreset(CameraPerspective.FREE.id(), "", null, null, null, null, null, null, OptionalBoolean.empty(), null, OptionalBoolean.empty(), null),
+ new CameraPreset(CameraPerspective.THIRD_PERSON.id(), "", null, null, null, null, null, null, OptionalBoolean.empty(), null, OptionalBoolean.empty(), null),
+ new CameraPreset(CameraPerspective.THIRD_PERSON_FRONT.id(), "", null, null, null, null, null, null, OptionalBoolean.empty(), null, OptionalBoolean.empty(), null),
+ new CameraPreset("geyser:free_audio", "minecraft:free", null, null, null, null, null, CameraAudioListener.PLAYER, OptionalBoolean.empty(), null, OptionalBoolean.of(false), null),
+ new CameraPreset("geyser:free_effects", "minecraft:free", null, null, null, null, null, CameraAudioListener.CAMERA, OptionalBoolean.empty(), null, OptionalBoolean.of(true), null),
+ new CameraPreset("geyser:free_audio_effects", "minecraft:free", null, null, null, null, null, CameraAudioListener.PLAYER, OptionalBoolean.empty(), null, OptionalBoolean.of(true), null));
SimpleDefinitionRegistry.Builder builder = SimpleDefinitionRegistry.builder();
for (int i = 0; i < CAMERA_PRESETS.size(); i++) {
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java b/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java
index c3756d663..3ea9cd112 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/PlayerInventory.java
@@ -62,6 +62,16 @@ public class PlayerInventory extends Inventory {
cursor = newCursor;
}
+ /**
+ * Checks if the player is holding the specified item in either hand
+ *
+ * @param item The item to look for
+ * @return If the player is holding the item in either hand
+ */
+ public boolean isHolding(@NonNull Item item) {
+ return getItemInHand().asItem() == item || getOffhand().asItem() == item;
+ }
+
public GeyserItemStack getItemInHand(@NonNull Hand hand) {
return hand == Hand.OFF_HAND ? getOffhand() : getItemInHand();
}
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java b/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java
index 53b02ef88..9d6f4d3e3 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/click/ClickPlan.java
@@ -162,6 +162,27 @@ public final class ClickPlan {
finished = true;
}
+ public Inventory getInventory() {
+ return inventory;
+ }
+
+ /**
+ * Test if the item stacks with another item in the specified slot.
+ * This will check the simulated inventory without copying.
+ */
+ public boolean canStack(int slot, GeyserItemStack item) {
+ GeyserItemStack slotItem = simulatedItems.getOrDefault(slot, inventory.getItem(slot));
+ return InventoryUtils.canStack(slotItem, item);
+ }
+
+ /**
+ * Test if the specified slot is empty.
+ * This will check the simulated inventory without copying.
+ */
+ public boolean isEmpty(int slot) {
+ return simulatedItems.getOrDefault(slot, inventory.getItem(slot)).isEmpty();
+ }
+
public GeyserItemStack getItem(int slot) {
return simulatedItems.computeIfAbsent(slot, k -> inventory.getItem(slot).copy());
}
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java b/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java
index 475a3e588..a8a711cc2 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/item/StoredItemMappings.java
@@ -43,6 +43,7 @@ public class StoredItemMappings {
private final ItemMapping banner;
private final ItemMapping barrier;
private final ItemMapping bow;
+ private final ItemMapping carrotOnAStick;
private final ItemMapping compass;
private final ItemMapping crossbow;
private final ItemMapping egg;
@@ -52,6 +53,7 @@ public class StoredItemMappings {
private final ItemMapping shield;
private final ItemMapping totem;
private final ItemMapping upgradeTemplate;
+ private final ItemMapping warpedFungusOnAStick;
private final ItemMapping wheat;
private final ItemMapping writableBook;
private final ItemMapping writtenBook;
@@ -60,6 +62,7 @@ public class StoredItemMappings {
this.banner = load(itemMappings, Items.WHITE_BANNER); // As of 1.17.10, all banners have the same Bedrock ID
this.barrier = load(itemMappings, Items.BARRIER);
this.bow = load(itemMappings, Items.BOW);
+ this.carrotOnAStick = load(itemMappings, Items.CARROT_ON_A_STICK);
this.compass = load(itemMappings, Items.COMPASS);
this.crossbow = load(itemMappings, Items.CROSSBOW);
this.egg = load(itemMappings, Items.EGG);
@@ -69,6 +72,7 @@ public class StoredItemMappings {
this.shield = load(itemMappings, Items.SHIELD);
this.totem = load(itemMappings, Items.TOTEM_OF_UNDYING);
this.upgradeTemplate = load(itemMappings, Items.NETHERITE_UPGRADE_SMITHING_TEMPLATE);
+ this.warpedFungusOnAStick = load(itemMappings, Items.WARPED_FUNGUS_ON_A_STICK);
this.wheat = load(itemMappings, Items.WHEAT);
this.writableBook = load(itemMappings, Items.WRITABLE_BOOK);
this.writtenBook = load(itemMappings, Items.WRITTEN_BOOK);
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/AnvilInventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/AnvilInventoryUpdater.java
index 7afd31cc9..2e0c75708 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/updater/AnvilInventoryUpdater.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/AnvilInventoryUpdater.java
@@ -32,6 +32,8 @@ import net.kyori.adventure.text.Component;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtMapBuilder;
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId;
+import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType;
+import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket;
import org.geysermc.geyser.GeyserImpl;
@@ -78,6 +80,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater {
slotPacket.setContainerId(ContainerId.UI);
slotPacket.setSlot(bedrockSlot);
slotPacket.setItem(inventory.getItem(i).getItemData(session));
+ slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
session.sendUpstreamPacket(slotPacket);
}
}
@@ -98,6 +101,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater {
slotPacket.setContainerId(ContainerId.UI);
slotPacket.setSlot(translator.javaSlotToBedrock(javaSlot));
slotPacket.setItem(inventory.getItem(javaSlot).getItemData(session));
+ slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
session.sendUpstreamPacket(slotPacket);
} else if (lastTargetSlot != javaSlot) {
// Update the previous target slot to remove repair cost changes
@@ -105,6 +109,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater {
slotPacket.setContainerId(ContainerId.UI);
slotPacket.setSlot(translator.javaSlotToBedrock(lastTargetSlot));
slotPacket.setItem(inventory.getItem(lastTargetSlot).getItemData(session));
+ slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
session.sendUpstreamPacket(slotPacket);
}
@@ -168,6 +173,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater {
slotPacket.setContainerId(ContainerId.UI);
slotPacket.setSlot(translator.javaSlotToBedrock(slot));
slotPacket.setItem(itemData);
+ slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
session.sendUpstreamPacket(slotPacket);
}
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/ChestInventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/ChestInventoryUpdater.java
index 5d6214871..9f3d00c57 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/updater/ChestInventoryUpdater.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/ChestInventoryUpdater.java
@@ -25,6 +25,8 @@
package org.geysermc.geyser.inventory.updater;
+import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType;
+import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
import org.cloudburstmc.protocol.bedrock.packet.InventoryContentPacket;
import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket;
@@ -61,6 +63,7 @@ public class ChestInventoryUpdater extends InventoryUpdater {
InventoryContentPacket contentPacket = new InventoryContentPacket();
contentPacket.setContainerId(inventory.getBedrockId());
contentPacket.setContents(bedrockItems);
+ contentPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
session.sendUpstreamPacket(contentPacket);
}
@@ -73,6 +76,7 @@ public class ChestInventoryUpdater extends InventoryUpdater {
slotPacket.setContainerId(inventory.getBedrockId());
slotPacket.setSlot(translator.javaSlotToBedrock(javaSlot));
slotPacket.setItem(inventory.getItem(javaSlot).getItemData(session));
+ slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
session.sendUpstreamPacket(slotPacket);
return true;
}
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/ContainerInventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/ContainerInventoryUpdater.java
index c9f313f2a..3d372c083 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/updater/ContainerInventoryUpdater.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/ContainerInventoryUpdater.java
@@ -25,6 +25,8 @@
package org.geysermc.geyser.inventory.updater;
+import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType;
+import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
import org.cloudburstmc.protocol.bedrock.packet.InventoryContentPacket;
import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket;
@@ -49,6 +51,7 @@ public class ContainerInventoryUpdater extends InventoryUpdater {
InventoryContentPacket contentPacket = new InventoryContentPacket();
contentPacket.setContainerId(inventory.getBedrockId());
contentPacket.setContents(Arrays.asList(bedrockItems));
+ contentPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
session.sendUpstreamPacket(contentPacket);
}
@@ -61,6 +64,7 @@ public class ContainerInventoryUpdater extends InventoryUpdater {
slotPacket.setContainerId(inventory.getBedrockId());
slotPacket.setSlot(translator.javaSlotToBedrock(javaSlot));
slotPacket.setItem(inventory.getItem(javaSlot).getItemData(session));
+ slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
session.sendUpstreamPacket(slotPacket);
return true;
}
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/CrafterInventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/CrafterInventoryUpdater.java
index 4474d420c..315b84c6d 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/updater/CrafterInventoryUpdater.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/CrafterInventoryUpdater.java
@@ -26,6 +26,8 @@
package org.geysermc.geyser.inventory.updater;
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId;
+import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType;
+import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
import org.cloudburstmc.protocol.bedrock.packet.InventoryContentPacket;
import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket;
@@ -56,6 +58,7 @@ public class CrafterInventoryUpdater extends InventoryUpdater {
contentPacket = new InventoryContentPacket();
contentPacket.setContainerId(inventory.getBedrockId());
contentPacket.setContents(Arrays.asList(bedrockItems));
+ contentPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
session.sendUpstreamPacket(contentPacket);
// inventory and hotbar
@@ -67,6 +70,7 @@ public class CrafterInventoryUpdater extends InventoryUpdater {
contentPacket = new InventoryContentPacket();
contentPacket.setContainerId(ContainerId.INVENTORY);
contentPacket.setContents(Arrays.asList(bedrockItems));
+ contentPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
session.sendUpstreamPacket(contentPacket);
// Crafter result - it doesn't come after the grid, as explained elsewhere.
@@ -88,6 +92,7 @@ public class CrafterInventoryUpdater extends InventoryUpdater {
packet.setContainerId(containerId);
packet.setSlot(translator.javaSlotToBedrock(javaSlot));
packet.setItem(inventory.getItem(javaSlot).getItemData(session));
+ packet.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
session.sendUpstreamPacket(packet);
return true;
}
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/HorseInventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/HorseInventoryUpdater.java
index 7441e66d0..1a46fc02a 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/updater/HorseInventoryUpdater.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/HorseInventoryUpdater.java
@@ -25,6 +25,8 @@
package org.geysermc.geyser.inventory.updater;
+import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType;
+import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
import org.cloudburstmc.protocol.bedrock.packet.InventoryContentPacket;
import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket;
@@ -49,6 +51,7 @@ public class HorseInventoryUpdater extends InventoryUpdater {
InventoryContentPacket contentPacket = new InventoryContentPacket();
contentPacket.setContainerId(inventory.getBedrockId());
contentPacket.setContents(Arrays.asList(bedrockItems));
+ contentPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
session.sendUpstreamPacket(contentPacket);
}
@@ -61,6 +64,7 @@ public class HorseInventoryUpdater extends InventoryUpdater {
slotPacket.setContainerId(4); // Horse GUI?
slotPacket.setSlot(translator.javaSlotToBedrock(javaSlot));
slotPacket.setItem(inventory.getItem(javaSlot).getItemData(session));
+ slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
session.sendUpstreamPacket(slotPacket);
return true;
}
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/InventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/InventoryUpdater.java
index 68ee334ba..b7ef4720f 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/updater/InventoryUpdater.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/InventoryUpdater.java
@@ -26,6 +26,8 @@
package org.geysermc.geyser.inventory.updater;
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId;
+import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType;
+import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
import org.cloudburstmc.protocol.bedrock.packet.InventoryContentPacket;
import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket;
@@ -45,6 +47,7 @@ public class InventoryUpdater {
InventoryContentPacket contentPacket = new InventoryContentPacket();
contentPacket.setContainerId(ContainerId.INVENTORY);
contentPacket.setContents(Arrays.asList(bedrockItems));
+ contentPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
session.sendUpstreamPacket(contentPacket);
}
@@ -54,6 +57,7 @@ public class InventoryUpdater {
slotPacket.setContainerId(ContainerId.INVENTORY);
slotPacket.setSlot(translator.javaSlotToBedrock(javaSlot));
slotPacket.setItem(inventory.getItem(javaSlot).getItemData(session));
+ slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
session.sendUpstreamPacket(slotPacket);
return true;
}
diff --git a/core/src/main/java/org/geysermc/geyser/inventory/updater/UIInventoryUpdater.java b/core/src/main/java/org/geysermc/geyser/inventory/updater/UIInventoryUpdater.java
index a23385b53..f4f40d6ce 100644
--- a/core/src/main/java/org/geysermc/geyser/inventory/updater/UIInventoryUpdater.java
+++ b/core/src/main/java/org/geysermc/geyser/inventory/updater/UIInventoryUpdater.java
@@ -26,6 +26,8 @@
package org.geysermc.geyser.inventory.updater;
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId;
+import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType;
+import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName;
import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket;
import org.geysermc.geyser.inventory.Inventory;
import org.geysermc.geyser.session.GeyserSession;
@@ -46,6 +48,7 @@ public class UIInventoryUpdater extends InventoryUpdater {
slotPacket.setContainerId(ContainerId.UI);
slotPacket.setSlot(bedrockSlot);
slotPacket.setItem(inventory.getItem(i).getItemData(session));
+ slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
session.sendUpstreamPacket(slotPacket);
}
}
@@ -59,6 +62,7 @@ public class UIInventoryUpdater extends InventoryUpdater {
slotPacket.setContainerId(ContainerId.UI);
slotPacket.setSlot(translator.javaSlotToBedrock(javaSlot));
slotPacket.setItem(inventory.getItem(javaSlot).getItemData(session));
+ slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
session.sendUpstreamPacket(slotPacket);
return true;
}
diff --git a/core/src/main/java/org/geysermc/geyser/item/enchantment/Enchantment.java b/core/src/main/java/org/geysermc/geyser/item/enchantment/Enchantment.java
index 3c0caa60c..301f69a5f 100644
--- a/core/src/main/java/org/geysermc/geyser/item/enchantment/Enchantment.java
+++ b/core/src/main/java/org/geysermc/geyser/item/enchantment/Enchantment.java
@@ -25,8 +25,7 @@
package org.geysermc.geyser.item.enchantment;
-import java.util.List;
-import java.util.function.Function;
+import it.unimi.dsi.fastutil.ints.IntArrays;
import net.kyori.adventure.key.Key;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.nbt.NbtMap;
@@ -35,11 +34,14 @@ import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.session.cache.registry.RegistryEntryContext;
import org.geysermc.geyser.translator.text.MessageTranslator;
+import org.geysermc.geyser.util.MinecraftKey;
+import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Set;
-import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet;
+import java.util.function.ToIntFunction;
/**
* @param description only populated if {@link #bedrockEnchantment()} is not null.
@@ -69,7 +71,7 @@ public record Enchantment(String identifier,
// TODO - description is a component. So if a hardcoded literal string is given, this will display normally on Java,
// but Geyser will attempt to lookup the literal string as translation - and will fail, displaying an empty string as enchantment name.
- String description = bedrockEnchantment == null ? MessageTranslator.deserializeDescription(data) : null;
+ String description = bedrockEnchantment == null ? MessageTranslator.deserializeDescription(context.session(), data) : null;
return new Enchantment(context.id().asString(), effects, supportedItems, maxLevel,
description, anvilCost, exclusiveSet, bedrockEnchantment);
@@ -86,21 +88,21 @@ public record Enchantment(String identifier,
}
// TODO holder set util?
- private static HolderSet readHolderSet(@Nullable Object holderSet, Function keyIdMapping) {
+ private static HolderSet readHolderSet(@Nullable Object holderSet, ToIntFunction keyIdMapping) {
if (holderSet == null) {
- return new HolderSet(new int[]{});
+ return new HolderSet(IntArrays.EMPTY_ARRAY);
}
if (holderSet instanceof String stringTag) {
// Tag
if (stringTag.startsWith("#")) {
- return new HolderSet(Key.key(stringTag.substring(1))); // Remove '#' at beginning that indicates tag
+ return new HolderSet(MinecraftKey.key(stringTag.substring(1))); // Remove '#' at beginning that indicates tag
} else {
- return new HolderSet(new int[]{keyIdMapping.apply(Key.key(stringTag))});
+ return new HolderSet(new int[]{keyIdMapping.applyAsInt(MinecraftKey.key(stringTag))});
}
} else if (holderSet instanceof List> list) {
// Assume the list is a list of strings
- return new HolderSet(list.stream().map(o -> (String) o).map(Key::key).map(keyIdMapping).mapToInt(Integer::intValue).toArray());
+ return new HolderSet(list.stream().map(o -> (String) o).map(Key::key).mapToInt(keyIdMapping).toArray());
}
throw new IllegalArgumentException("Holder set must either be a tag, a string ID or a list of string IDs");
}
diff --git a/core/src/main/java/org/geysermc/geyser/level/GeyserAdvancement.java b/core/src/main/java/org/geysermc/geyser/level/GeyserAdvancement.java
index 7d48b90af..7dad1639b 100644
--- a/core/src/main/java/org/geysermc/geyser/level/GeyserAdvancement.java
+++ b/core/src/main/java/org/geysermc/geyser/level/GeyserAdvancement.java
@@ -82,11 +82,15 @@ public class GeyserAdvancement {
this.rootId = this.advancement.getId();
} else {
// Go through our cache, and descend until we find the root ID
- GeyserAdvancement advancement = advancementsCache.getStoredAdvancements().get(this.advancement.getParentId());
- if (advancement.getParentId() == null) {
- this.rootId = advancement.getId();
+ GeyserAdvancement parent = advancementsCache.getStoredAdvancements().get(this.advancement.getParentId());
+ if (parent == null) {
+ // Parent doesn't exist, is invalid, or couldn't be found for another reason
+ // So assuming there is no parent and this is the root
+ this.rootId = this.advancement.getId();
+ } else if (parent.getParentId() == null) {
+ this.rootId = parent.getId();
} else {
- this.rootId = advancement.getRootId(advancementsCache);
+ this.rootId = parent.getRootId(advancementsCache);
}
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/level/GeyserWorldManager.java b/core/src/main/java/org/geysermc/geyser/level/GeyserWorldManager.java
index 9faa7424c..befcfa4b7 100644
--- a/core/src/main/java/org/geysermc/geyser/level/GeyserWorldManager.java
+++ b/core/src/main/java/org/geysermc/geyser/level/GeyserWorldManager.java
@@ -35,6 +35,7 @@ import org.geysermc.erosion.packet.backendbound.BackendboundBatchBlockRequestPac
import org.geysermc.erosion.packet.backendbound.BackendboundBlockRequestPacket;
import org.geysermc.erosion.packet.backendbound.BackendboundPickBlockPacket;
import org.geysermc.erosion.util.BlockPositionIterator;
+import org.geysermc.geyser.erosion.ErosionCancellationException;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents;
@@ -49,6 +50,8 @@ public class GeyserWorldManager extends WorldManager {
var erosionHandler = session.getErosionHandler().getAsActive();
if (erosionHandler == null) {
return session.getChunkCache().getBlockAt(x, y, z);
+ } else if (session.isClosed()) {
+ throw new ErosionCancellationException();
}
CompletableFuture future = new CompletableFuture<>(); // Boxes
erosionHandler.setPendingLookup(future);
@@ -61,6 +64,8 @@ public class GeyserWorldManager extends WorldManager {
var erosionHandler = session.getErosionHandler().getAsActive();
if (erosionHandler == null) {
return super.getBlockAtAsync(session, x, y, z);
+ } else if (session.isClosed()) {
+ return CompletableFuture.failedFuture(new ErosionCancellationException());
}
CompletableFuture future = new CompletableFuture<>(); // Boxes
int transactionId = erosionHandler.getNextTransactionId();
@@ -74,6 +79,8 @@ public class GeyserWorldManager extends WorldManager {
var erosionHandler = session.getErosionHandler().getAsActive();
if (erosionHandler == null) {
return super.getBlocksAt(session, iter);
+ } else if (session.isClosed()) {
+ throw new ErosionCancellationException();
}
CompletableFuture future = new CompletableFuture<>();
erosionHandler.setPendingBatchLookup(future);
@@ -118,17 +125,14 @@ public class GeyserWorldManager extends WorldManager {
return GameMode.SURVIVAL;
}
- @Override
- public boolean hasPermission(GeyserSession session, String permission) {
- return false;
- }
-
@NonNull
@Override
public CompletableFuture<@Nullable DataComponents> getPickItemComponents(GeyserSession session, int x, int y, int z, boolean addNbtData) {
var erosionHandler = session.getErosionHandler().getAsActive();
if (erosionHandler == null) {
return super.getPickItemComponents(session, x, y, z, addNbtData);
+ } else if (session.isClosed()) {
+ return CompletableFuture.failedFuture(new ErosionCancellationException());
}
CompletableFuture