supportedBedrockVersions();
+
+ /**
+ * Gets the {@link CommandSource} for the console.
+ *
+ * @return the console command source
+ */
+ @NonNull
+ CommandSource consoleCommandSource();
+
/**
* Gets the current {@link GeyserApiBase} instance.
*
diff --git a/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/CameraData.java b/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/CameraData.java
new file mode 100644
index 000000000..f208879d1
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/CameraData.java
@@ -0,0 +1,180 @@
+/*
+ * 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.bedrock.camera;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.geysermc.geyser.api.connection.GeyserConnection;
+
+import java.util.Set;
+import java.util.UUID;
+
+/**
+ * This interface holds all the methods that relate to a client's camera.
+ * Can be accessed through {@link GeyserConnection#camera()}.
+ */
+public interface CameraData {
+
+ /**
+ * Sends a camera fade instruction to the client.
+ * If an existing camera fade is already in progress, the current fade will be prolonged.
+ * Can be built using {@link CameraFade.Builder}.
+ * To stop a fade early, use {@link #clearCameraInstructions()}.
+ *
+ * @param fade the camera fade instruction to send
+ */
+ void sendCameraFade(@NonNull CameraFade fade);
+
+ /**
+ * Sends a camera position instruction to the client.
+ * If an existing camera movement is already in progress,
+ * the final camera position will be the one of the latest instruction, and
+ * the (optional) camera fade will be added on top of the existing fade.
+ * Can be built using {@link CameraPosition.Builder}.
+ * To stop reset the camera position/stop ongoing instructions, use {@link #clearCameraInstructions()}.
+ *
+ * @param position the camera position instruction to send
+ */
+ void sendCameraPosition(@NonNull CameraPosition position);
+
+ /**
+ * Stops all sent camera instructions (fades, movements, and perspective locks).
+ * This will not stop any camera shakes/input locks/fog effects, use the respective methods for those.
+ */
+ void clearCameraInstructions();
+
+ /**
+ * Forces a {@link CameraPerspective} on the client. This will prevent the client
+ * from changing their camera perspective until it is unlocked via {@link #clearCameraInstructions()}.
+ *
+ * Note: You cannot force a client into a free camera perspective with this method.
+ * To do that, send a {@link CameraPosition} via {@link #sendCameraPosition(CameraPosition)} - it requires a set position
+ * instead of being relative to the player.
+ *
+ * @param perspective the {@link CameraPerspective} to force
+ */
+ void forceCameraPerspective(@NonNull CameraPerspective perspective);
+
+ /**
+ * Gets the client's current {@link CameraPerspective}, if one is currently forced.
+ * This will return {@code null} if the client is not currently forced into a perspective.
+ * If a perspective is forced, the client will not be able to change their camera perspective until it is unlocked.
+ *
+ * @return the forced perspective, or {@code null} if none is forced
+ */
+ @Nullable CameraPerspective forcedCameraPerspective();
+
+ /**
+ * Shakes the client's camera.
+ *
+ * If the camera is already shaking with the same {@link CameraShake} type, then the additional intensity
+ * will be layered on top of the existing intensity, with their own distinct durations.
+ * If the existing shake type is different and the new intensity/duration are not positive, the existing shake only
+ * switches to the new type. Otherwise, the existing shake is completely overridden.
+ *
+ * @param intensity the intensity of the shake. The client has a maximum total intensity of 4.
+ * @param duration the time in seconds that the shake will occur for
+ * @param type the type of shake
+ */
+ void shakeCamera(float intensity, float duration, @NonNull CameraShake type);
+
+ /**
+ * Stops all camera shakes of any type.
+ */
+ void stopCameraShake();
+
+ /**
+ * Adds the given fog IDs to the fog cache, then sends all fog IDs in the cache to the client.
+ *
+ * Fog IDs can be found here
+ *
+ * @param fogNameSpaces the fog IDs to add. If empty, the existing cached IDs will still be sent.
+ */
+ void sendFog(String... fogNameSpaces);
+
+ /**
+ * Removes the given fog IDs from the fog cache, then sends all fog IDs in the cache to the client.
+ *
+ * @param fogNameSpaces the fog IDs to remove. If empty, all fog IDs will be removed.
+ */
+ void removeFog(String... fogNameSpaces);
+
+ /**
+ * Returns an immutable copy of all fog affects currently applied to this client.
+ */
+ @NonNull
+ Set fogEffects();
+
+ /**
+ * (Un)locks the client's camera, so that they cannot look around.
+ * To ensure the camera is only unlocked when all locks are released, you must supply
+ * a UUID when using method, and use the same UUID to unlock the camera.
+ *
+ * @param lock whether to lock the camera
+ * @param owner the owner of the lock, represented with a UUID
+ * @return if the camera is locked after this method call
+ */
+ boolean lockCamera(boolean lock, @NonNull UUID owner);
+
+ /**
+ * Returns whether the client's camera is locked.
+ *
+ * @return whether the camera is currently locked
+ */
+ boolean isCameraLocked();
+
+ /**
+ * Hides a {@link GuiElement} on the client's side.
+ *
+ * @param element the {@link GuiElement} to hide
+ */
+ void hideElement(@NonNull GuiElement... element);
+
+ /**
+ * Resets a {@link GuiElement} on the client's side.
+ * This makes the client decide on its own - e.g. based on client settings -
+ * whether to show or hide the gui element.
+ *
+ * If no elements are specified, this will reset all currently hidden elements
+ *
+ * @param element the {@link GuiElement} to reset
+ */
+ void resetElement(@NonNull GuiElement @Nullable... element);
+
+ /**
+ * Determines whether a {@link GuiElement} is currently hidden.
+ *
+ * @param element the {@link GuiElement} to check
+ */
+ boolean isHudElementHidden(@NonNull GuiElement element);
+
+ /**
+ * Returns the currently hidden {@link GuiElement}s.
+ *
+ * @return an unmodifiable view of all currently hidden {@link GuiElement}s
+ */
+ @NonNull Set hiddenElements();
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/CameraEaseType.java b/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/CameraEaseType.java
new file mode 100644
index 000000000..64c313ec1
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/CameraEaseType.java
@@ -0,0 +1,77 @@
+/*
+ * 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.bedrock.camera;
+
+/**
+ * These are all the easing types that can be used when sending a {@link CameraPosition} instruction.
+ * When using these, the client won't teleport to the new camera position, but instead transition to it.
+ *
+ * See https://easings.net/ for more information.
+ */
+public enum CameraEaseType {
+ LINEAR("linear"),
+ SPRING("spring"),
+ EASE_IN_SINE("in_sine"),
+ EASE_OUT_SINE("out_sine"),
+ EASE_IN_OUT_SINE("in_out_sine"),
+ EASE_IN_QUAD("in_quad"),
+ EASE_OUT_QUAD("out_quad"),
+ EASE_IN_OUT_QUAD("in_out_quad"),
+ EASE_IN_CUBIC("in_cubic"),
+ EASE_OUT_CUBIC("out_cubic"),
+ EASE_IN_OUT_CUBIC("in_out_cubic"),
+ EASE_IN_QUART("in_quart"),
+ EASE_OUT_QUART("out_quart"),
+ EASE_IN_OUT_QUART("in_out_quart"),
+ EASE_IN_QUINT("in_quint"),
+ EASE_OUT_QUINT("out_quint"),
+ EASE_IN_OUT_QUINT("in_out_quint"),
+ EASE_IN_EXPO("in_expo"),
+ EASE_OUT_EXPO("out_expo"),
+ EASE_IN_OUT_EXPO("in_out_expo"),
+ EASE_IN_CIRC("in_circ"),
+ EASE_OUT_CIRC("out_circ"),
+ EASE_IN_OUT_CIRC("in_out_circ"),
+ EASE_IN_BACK("in_back"),
+ EASE_OUT_BACK("out_back"),
+ EASE_IN_OUT_BACK("in_out_back"),
+ EASE_IN_ELASTIC("in_elastic"),
+ EASE_OUT_ELASTIC("out_elastic"),
+ EASE_IN_OUT_ELASTIC("in_out_elastic"),
+ EASE_IN_BOUNCE("in_bounce"),
+ EASE_OUT_BOUNCE("out_bounce"),
+ EASE_IN_OUT_BOUNCE("in_out_bounce");
+
+ private final String id;
+
+ CameraEaseType(String id) {
+ this.id = id;
+ }
+
+ public String id() {
+ return this.id;
+ }
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/CameraFade.java b/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/CameraFade.java
new file mode 100644
index 000000000..38baa73bb
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/CameraFade.java
@@ -0,0 +1,94 @@
+/*
+ * 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.bedrock.camera;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.common.value.qual.IntRange;
+import org.geysermc.geyser.api.GeyserApi;
+
+import java.awt.Color;
+
+/**
+ * Represents a coloured fade overlay on the camera.
+ *
+ * Can be sent with {@link CameraData#sendCameraFade(CameraFade)}, or with a {@link CameraPosition} instruction.
+ */
+public interface CameraFade {
+
+ /**
+ * Gets the color overlay of the camera.
+ * Bedrock uses an RGB color system.
+ *
+ * @return the color of the fade
+ */
+ @NonNull Color color();
+
+ /**
+ * Gets the seconds it takes to fade in.
+ * All fade times combined must take at least 0.5 seconds, and at most 30 seconds.
+ *
+ * @return the seconds it takes to fade in
+ */
+ float fadeInSeconds();
+
+ /**
+ * Gets the seconds the overlay is held.
+ * All fade times combined must take at least 0.5 seconds, and at most 30 seconds.
+ *
+ * @return the seconds the overlay is held
+ */
+ float fadeHoldSeconds();
+
+ /**
+ * Gets the seconds it takes to fade out.
+ * All fade times combined must take at least 0.5 seconds, and at most 30 seconds.
+ *
+ * @return the seconds it takes to fade out
+ */
+ float fadeOutSeconds();
+
+ /**
+ * Creates a Builder for CameraFade
+ *
+ * @return a CameraFade Builder
+ */
+ static CameraFade.Builder builder() {
+ return GeyserApi.api().provider(CameraFade.Builder.class);
+ }
+
+ interface Builder {
+
+ Builder color(@NonNull Color color);
+
+ Builder fadeInSeconds(@IntRange(from = 0, to = 10) float fadeInSeconds);
+
+ Builder fadeHoldSeconds(@IntRange(from = 0, to = 10) float fadeHoldSeconds);
+
+ Builder fadeOutSeconds(@IntRange(from = 0, to = 10) float fadeOutSeconds);
+
+ CameraFade build();
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/level/block/DoubleChestValue.java b/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/CameraPerspective.java
similarity index 63%
rename from core/src/main/java/org/geysermc/geyser/level/block/DoubleChestValue.java
rename to api/src/main/java/org/geysermc/geyser/api/bedrock/camera/CameraPerspective.java
index 8024af650..4167f2a34 100644
--- a/core/src/main/java/org/geysermc/geyser/level/block/DoubleChestValue.java
+++ b/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/CameraPerspective.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
@@ -23,23 +23,26 @@
* @link https://github.com/GeyserMC/Geyser
*/
-package org.geysermc.geyser.level.block;
+package org.geysermc.geyser.api.bedrock.camera;
/**
- * This stores all values of double chests that are part of the Java block state.
+ * Represents a camera perspective that a player's camera can take.
+ * All perspectives except for {@link #FREE} are locked to the player's head,
+ * and are therefore relative to the player's position and rotation.
*/
-public record DoubleChestValue(
- /**
- * If true, then chest is facing east/west; if false, south/north
- */
- boolean isFacingEast,
- /**
- * If true, direction is positive (east/south); if false, direction is negative (west/north)
- */
- boolean isDirectionPositive,
- /**
- * If true, chest is the left of a pair; if false, chest is the right of a pair.
- */
- boolean isLeft) {
+public enum CameraPerspective {
+ FIRST_PERSON("minecraft:first_person"),
+ FREE("minecraft:free"),
+ THIRD_PERSON("minecraft:third_person"),
+ THIRD_PERSON_FRONT("minecraft:third_person_front");
+ private final String id;
+
+ CameraPerspective(String id) {
+ this.id = id;
+ }
+
+ public String id() {
+ return this.id;
+ }
}
diff --git a/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/CameraPosition.java b/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/CameraPosition.java
new file mode 100644
index 000000000..6d42d499e
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/CameraPosition.java
@@ -0,0 +1,150 @@
+/*
+ * 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.bedrock.camera;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.checkerframework.common.value.qual.IntRange;
+import org.cloudburstmc.math.vector.Vector3f;
+import org.geysermc.geyser.api.GeyserApi;
+
+/**
+ * This interface represents a camera position instruction. Can be built with the {@link #builder()}.
+ *
+ * Any camera position instruction pins the client camera to a specific position and rotation.
+ * You can set {@link CameraEaseType} to ensure a smooth transition that will last {@link #easeSeconds()} seconds.
+ * A {@link CameraFade} can also be sent, which will transition the player to a coloured transition during the transition.
+ *
+ * Use {@link CameraData#sendCameraPosition(CameraPosition)} to send such an instruction to any connection.
+ */
+public interface CameraPosition {
+
+ /**
+ * Gets the camera's position.
+ *
+ * @return camera position vector
+ */
+ @NonNull Vector3f position();
+
+ /**
+ * Gets the {@link CameraEaseType} of the camera.
+ * If not set, there is no easing.
+ *
+ * @return camera ease type
+ */
+ @Nullable CameraEaseType easeType();
+
+ /**
+ * Gets the {@link CameraFade} to be sent along the camera position instruction.
+ * If set, they will run at once.
+ *
+ * @return camera fade, or null if not present
+ */
+ @Nullable CameraFade cameraFade();
+
+ /**
+ * Gets the easing duration of the camera, in seconds.
+ * Is only used if a {@link CameraEaseType} is set.
+ *
+ * @return camera easing duration in seconds
+ */
+ float easeSeconds();
+
+ /**
+ * Gets the x-axis rotation of the camera.
+ * To prevent the camera from being upside down, Bedrock limits the range to -90 to 90.
+ * Will be overridden if {@link #facingPosition()} is set.
+ *
+ * @return camera x-axis rotation
+ */
+ @IntRange(from = -90, to = 90) int rotationX();
+
+ /**
+ * Gets the y-axis rotation of the camera.
+ * Will be overridden if {@link #facingPosition()} is set.
+ *
+ * @return camera y-axis rotation
+ */
+ int rotationY();
+
+ /**
+ * Gets the position that the camera is facing.
+ * Can be used instead of manually setting rotation values.
+ *
+ * If set, the rotation values set via {@link #rotationX()} and {@link #rotationY()} will be ignored.
+ *
+ * @return Camera's facing position
+ */
+ @Nullable Vector3f facingPosition();
+
+ /**
+ * Controls whether player effects, such as night vision or blindness, should be rendered on the camera.
+ * Defaults to false.
+ *
+ * @return whether player effects should be rendered
+ */
+ boolean renderPlayerEffects();
+
+ /**
+ * Controls whether the player position should be used for directional audio.
+ * If false, the camera position will be used instead.
+ *
+ * @return whether the players position should be used for directional audio
+ */
+ boolean playerPositionForAudio();
+
+ /**
+ * Creates a Builder for CameraPosition
+ *
+ * @return a CameraPosition Builder
+ */
+ static CameraPosition.Builder builder() {
+ return GeyserApi.api().provider(CameraPosition.Builder.class);
+ }
+
+ interface Builder {
+
+ Builder cameraFade(@Nullable CameraFade cameraFade);
+
+ Builder renderPlayerEffects(boolean renderPlayerEffects);
+
+ Builder playerPositionForAudio(boolean playerPositionForAudio);
+
+ Builder easeType(@Nullable CameraEaseType easeType);
+
+ Builder easeSeconds(float easeSeconds);
+
+ Builder position(@NonNull Vector3f position);
+
+ Builder rotationX(@IntRange(from = -90, to = 90) int rotationX);
+
+ Builder rotationY(int rotationY);
+
+ Builder facingPosition(@Nullable Vector3f facingPosition);
+
+ CameraPosition build();
+ }
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/CameraShake.java b/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/CameraShake.java
index 18bafb428..304969edb 100644
--- a/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/CameraShake.java
+++ b/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/CameraShake.java
@@ -25,7 +25,10 @@
package org.geysermc.geyser.api.bedrock.camera;
+/**
+ * Represents a camera shake instruction. Can be sent in {@link CameraData#shakeCamera(float, float, CameraShake)}
+ */
public enum CameraShake {
POSITIONAL,
- ROTATIONAL;
+ ROTATIONAL
}
diff --git a/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/GuiElement.java b/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/GuiElement.java
new file mode 100644
index 000000000..4d3653648
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/GuiElement.java
@@ -0,0 +1,60 @@
+/*
+ * 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.api.bedrock.camera;
+
+/**
+ * Represent GUI elements on the players HUD display.
+ * These can be hidden using {@link CameraData#hideElement(GuiElement...)},
+ * and one can reset their visibility using {@link CameraData#resetElement(GuiElement...)}.
+ */
+public class GuiElement {
+ public static final GuiElement PAPER_DOLL = new GuiElement(0);
+ public static final GuiElement ARMOR = new GuiElement(1);
+ public static final GuiElement TOOL_TIPS = new GuiElement(2);
+ public static final GuiElement TOUCH_CONTROLS = new GuiElement(3);
+ public static final GuiElement CROSSHAIR = new GuiElement(4);
+ public static final GuiElement HOTBAR = new GuiElement(5);
+ public static final GuiElement HEALTH = new GuiElement(6);
+ public static final GuiElement PROGRESS_BAR = new GuiElement(7);
+ public static final GuiElement FOOD_BAR = new GuiElement(8);
+ public static final GuiElement AIR_BUBBLES_BAR = new GuiElement(9);
+ public static final GuiElement VEHICLE_HEALTH = new GuiElement(10);
+ public static final GuiElement EFFECTS_BAR = new GuiElement(11);
+ public static final GuiElement ITEM_TEXT_POPUP = new GuiElement(12);
+
+ private GuiElement(int id) {
+ this.id = id;
+ }
+
+ private final int id;
+
+ /**
+ * Internal use only; don't depend on these values being consistent.
+ */
+ public int id() {
+ return this.id;
+ }
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockData.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockData.java
index 9f142faab..2605cda21 100644
--- a/api/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockData.java
+++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/CustomBlockData.java
@@ -61,16 +61,16 @@ public interface CustomBlockData {
boolean includedInCreativeInventory();
/**
- * Gets the item's creative category, or tab id.
+ * Gets the block's creative category, or tab id.
*
- * @return the item's creative category
+ * @return the block's creative category
*/
@Nullable CreativeCategory creativeCategory();
/**
- * Gets the item's creative group.
+ * Gets the block's creative group.
*
- * @return the item's creative group
+ * @return the block's creative group
*/
@Nullable String creativeGroup();
diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/component/CustomBlockComponents.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/CustomBlockComponents.java
index 036723092..06608f787 100644
--- a/api/src/main/java/org/geysermc/geyser/api/block/custom/component/CustomBlockComponents.java
+++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/CustomBlockComponents.java
@@ -129,8 +129,11 @@ public interface CustomBlockComponents {
* Gets the unit cube component
* Equivalent to "minecraft:unit_cube"
*
+ * @deprecated Use {@link #geometry()} and compare with `minecraft:geometry.full_block` instead.
+ *
* @return The rotation.
*/
+ @Deprecated
boolean unitCube();
/**
@@ -181,11 +184,15 @@ public interface CustomBlockComponents {
Builder transformation(TransformationComponent transformation);
+ /**
+ * @deprecated Use {@link #geometry(GeometryComponent)} with `minecraft:geometry.full_block` instead.
+ */
+ @Deprecated
Builder unitCube(boolean unitCube);
Builder placeAir(boolean placeAir);
- Builder tags(Set tags);
+ Builder tags(@Nullable Set tags);
CustomBlockComponents build();
}
diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/component/PlacementConditions.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/PlacementConditions.java
index d06d9a967..e274fd8bd 100644
--- a/api/src/main/java/org/geysermc/geyser/api/block/custom/component/PlacementConditions.java
+++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/component/PlacementConditions.java
@@ -25,11 +25,11 @@
package org.geysermc.geyser.api.block.custom.component;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
import java.util.LinkedHashMap;
import java.util.Set;
-import org.checkerframework.checker.nullness.qual.NonNull;
-
/**
* This class is used to store conditions for a placement filter for a custom block.
*
@@ -43,7 +43,7 @@ public record PlacementConditions(@NonNull Set allowedFaces, @NonNull Link
NORTH,
SOUTH,
WEST,
- EAST;
+ EAST
}
public enum BlockFilterType {
diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockState.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockState.java
index 6293506a8..0dd0d3b33 100644
--- a/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockState.java
+++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBlockState.java
@@ -39,7 +39,7 @@ public interface JavaBlockState {
*
* @return whether the block state is waterlogged
*/
- @NonNull boolean waterlogged();
+ boolean waterlogged();
/**
* Gets the collision of the block state
@@ -53,7 +53,7 @@ public interface JavaBlockState {
*
* @return whether the block state can be broken with hand
*/
- @NonNull boolean canBreakWithHand();
+ boolean canBreakWithHand();
/**
* Gets the pick item of the block state
@@ -73,8 +73,11 @@ public interface JavaBlockState {
* Gets whether the block state has block entity
*
* @return whether the block state has block entity
+ * @deprecated Does not have an effect. If you were using this to
+ * set piston behavior, use {@link #pistonBehavior()} instead.
*/
- @Nullable boolean hasBlockEntity();
+ @Deprecated(forRemoval = true)
+ boolean hasBlockEntity();
/**
* Creates a new {@link JavaBlockState.Builder} instance
@@ -94,17 +97,22 @@ public interface JavaBlockState {
Builder blockHardness(@NonNegative float blockHardness);
- Builder waterlogged(@NonNull boolean waterlogged);
+ Builder waterlogged(boolean waterlogged);
Builder collision(@NonNull JavaBoundingBox[] collision);
- Builder canBreakWithHand(@NonNull boolean canBreakWithHand);
+ Builder canBreakWithHand(boolean canBreakWithHand);
Builder pickItem(@Nullable String pickItem);
Builder pistonBehavior(@Nullable String pistonBehavior);
- Builder hasBlockEntity(@Nullable boolean hasBlockEntity);
+ /**
+ * @deprecated Does not have an effect. If you were using this to
+ * * set piston behavior, use {@link #pistonBehavior(String)} instead.
+ */
+ @Deprecated(forRemoval = true)
+ Builder hasBlockEntity(boolean hasBlockEntity);
JavaBlockState build();
}
diff --git a/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBoundingBox.java b/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBoundingBox.java
index 56a4ca3da..9065e8711 100644
--- a/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBoundingBox.java
+++ b/api/src/main/java/org/geysermc/geyser/api/block/custom/nonvanilla/JavaBoundingBox.java
@@ -1,6 +1,4 @@
package org.geysermc.geyser.api.block.custom.nonvanilla;
-import org.checkerframework.checker.nullness.qual.NonNull;
-
-public record JavaBoundingBox(@NonNull double middleX, @NonNull double middleY, @NonNull double middleZ, @NonNull double sizeX, @NonNull double sizeY, @NonNull double sizeZ) {
+public record JavaBoundingBox(double middleX, double middleY, double middleZ, double sizeX, double sizeY, double sizeZ) {
}
diff --git a/api/src/main/java/org/geysermc/geyser/api/command/Command.java b/api/src/main/java/org/geysermc/geyser/api/command/Command.java
index 2f1f2b24d..29922ae1e 100644
--- a/api/src/main/java/org/geysermc/geyser/api/command/Command.java
+++ b/api/src/main/java/org/geysermc/geyser/api/command/Command.java
@@ -28,7 +28,9 @@ package org.geysermc.geyser.api.command;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.api.GeyserApi;
import org.geysermc.geyser.api.connection.GeyserConnection;
+import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent;
import org.geysermc.geyser.api.extension.Extension;
+import org.geysermc.geyser.api.util.TriState;
import java.util.Collections;
import java.util.List;
@@ -58,15 +60,15 @@ public interface Command {
* Gets the permission node associated with
* this command.
*
- * @return the permission node for this command
+ * @return the permission node for this command if defined, otherwise an empty string
*/
@NonNull
String permission();
/**
- * Gets the aliases for this command.
+ * Gets the aliases for this command, as an unmodifiable list
*
- * @return the aliases for this command
+ * @return the aliases for this command as an unmodifiable list
*/
@NonNull
List aliases();
@@ -75,35 +77,39 @@ public interface Command {
* Gets if this command is designed to be used only by server operators.
*
* @return if this command is designated to be used only by server operators.
+ * @deprecated this method is not guaranteed to provide meaningful or expected results.
*/
- boolean isSuggestedOpOnly();
-
- /**
- * Gets if this command is executable on console.
- *
- * @return if this command is executable on console
- */
- boolean isExecutableOnConsole();
-
- /**
- * Gets the subcommands associated with this
- * command. Mainly used within the Geyser Standalone
- * GUI to know what subcommands are supported.
- *
- * @return the subcommands associated with this command
- */
- @NonNull
- default List subCommands() {
- return Collections.emptyList();
+ @Deprecated(forRemoval = true)
+ default boolean isSuggestedOpOnly() {
+ return false;
}
/**
- * Used to send a deny message to Java players if this command can only be used by Bedrock players.
- *
- * @return true if this command can only be used by Bedrock players.
+ * @return true if this command is executable on console
+ * @deprecated use {@link #isPlayerOnly()} instead (inverted)
*/
- default boolean isBedrockOnly() {
- return false;
+ @Deprecated(forRemoval = true)
+ default boolean isExecutableOnConsole() {
+ return !isPlayerOnly();
+ }
+
+ /**
+ * @return true if this command can only be used by players
+ */
+ boolean isPlayerOnly();
+
+ /**
+ * @return true if this command can only be used by Bedrock players
+ */
+ boolean isBedrockOnly();
+
+ /**
+ * @deprecated this method will always return an empty immutable list
+ */
+ @Deprecated(forRemoval = true)
+ @NonNull
+ default List subCommands() {
+ return Collections.emptyList();
}
/**
@@ -128,7 +134,7 @@ public interface Command {
* is an instance of this source.
*
* @param sourceType the source type
- * @return the builder
+ * @return this builder
*/
Builder source(@NonNull Class extends T> sourceType);
@@ -136,7 +142,7 @@ public interface Command {
* Sets the command name.
*
* @param name the command name
- * @return the builder
+ * @return this builder
*/
Builder name(@NonNull String name);
@@ -144,23 +150,40 @@ public interface Command {
* Sets the command description.
*
* @param description the command description
- * @return the builder
+ * @return this builder
*/
Builder description(@NonNull String description);
/**
- * Sets the permission node.
+ * Sets the permission node required to run this command.
+ * It will not be registered with any permission registries, such as an underlying server,
+ * or a permissions Extension (unlike {@link #permission(String, TriState)}).
*
* @param permission the permission node
- * @return the builder
+ * @return this builder
*/
Builder 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 e75e7dcab..ba559a462 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
@@ -29,8 +29,10 @@ import org.checkerframework.checker.index.qual.NonNegative;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.api.connection.Connection;
+import org.geysermc.geyser.api.bedrock.camera.CameraData;
import org.geysermc.geyser.api.bedrock.camera.CameraShake;
import org.geysermc.geyser.api.command.CommandSource;
+import org.geysermc.geyser.api.entity.EntityData;
import org.geysermc.geyser.api.entity.type.GeyserEntity;
import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity;
@@ -41,10 +43,29 @@ import java.util.concurrent.CompletableFuture;
* Represents a player connection used in Geyser.
*/
public interface GeyserConnection extends Connection, CommandSource {
+
+ /**
+ * Exposes the {@link CameraData} for this connection.
+ * It allows you to send fogs, camera shakes, force camera perspectives, and more.
+ *
+ * @return the CameraData for this connection.
+ */
+ @NonNull CameraData camera();
+
+ /**
+ * Exposes the {@link EntityData} for this connection.
+ * It allows you to get entities by their Java entity ID, show emotes, and get the player entity.
+ *
+ * @return the EntityData for this connection.
+ */
+ @NonNull EntityData entities();
+
/**
* @param javaId the Java entity ID to look up.
* @return a {@link GeyserEntity} if present in this connection's entity tracker.
+ * @deprecated Use {@link EntityData#entityByJavaId(int)} instead
*/
+ @Deprecated
@NonNull
CompletableFuture<@Nullable GeyserEntity> entityByJavaId(@NonNegative int javaId);
@@ -53,11 +74,14 @@ public interface GeyserConnection extends Connection, CommandSource {
*
* @param emoter the player entity emoting.
* @param emoteId the emote ID to send to this client.
+ * @deprecated use {@link EntityData#showEmote(GeyserPlayerEntity, String)} instead
*/
+ @Deprecated
void showEmote(@NonNull GeyserPlayerEntity emoter, @NonNull String emoteId);
/**
- * Shakes the client's camera.
+ * Shakes the client's camera.
+ *
* If the camera is already shaking with the same {@link CameraShake} type, then the additional intensity
* will be layered on top of the existing intensity, with their own distinct durations.
* If the existing shake type is different and the new intensity/duration are not positive, the existing shake only
@@ -66,12 +90,18 @@ public interface GeyserConnection extends Connection, CommandSource {
* @param intensity the intensity of the shake. The client has a maximum total intensity of 4.
* @param duration the time in seconds that the shake will occur for
* @param type the type of shake
+ *
+ * @deprecated Use {@link CameraData#shakeCamera(float, float, CameraShake)} instead.
*/
+ @Deprecated
void shakeCamera(float intensity, float duration, @NonNull CameraShake type);
/**
* Stops all camera shake of any type.
+ *
+ * @deprecated Use {@link CameraData#stopCameraShake()} instead.
*/
+ @Deprecated
void stopCameraShake();
/**
@@ -80,19 +110,26 @@ public interface GeyserConnection extends Connection, CommandSource {
* Fog IDs can be found here
*
* @param fogNameSpaces the fog IDs to add. If empty, the existing cached IDs will still be sent.
+ * @deprecated Use {@link CameraData#sendFog(String...)} instead.
*/
+ @Deprecated
void sendFog(String... fogNameSpaces);
/**
* Removes the given fog IDs from the fog cache, then sends all fog IDs in the cache to the client.
*
* @param fogNameSpaces the fog IDs to remove. If empty, all fog IDs will be removed.
+ * @deprecated Use {@link CameraData#removeFog(String...)} instead.
*/
+ @Deprecated
void removeFog(String... fogNameSpaces);
/**
* Returns an immutable copy of all fog affects currently applied to this client.
+ *
+ * @deprecated Use {@link CameraData#fogEffects()} instead.
*/
+ @Deprecated
@NonNull
Set fogEffects();
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
new file mode 100644
index 000000000..90b3fc821
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/EntityData.java
@@ -0,0 +1,84 @@
+/*
+ * 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.entity;
+
+import org.checkerframework.checker.index.qual.NonNegative;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.geysermc.geyser.api.connection.GeyserConnection;
+import org.geysermc.geyser.api.entity.type.GeyserEntity;
+import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity;
+
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * This class holds all the methods that relate to entities.
+ * Can be accessed through {@link GeyserConnection#entities()}.
+ */
+public interface EntityData {
+
+ /**
+ * Returns a {@link GeyserEntity} to e.g. make them play an emote.
+ *
+ * @param javaId the Java entity ID to look up
+ * @return a {@link GeyserEntity} if present in this connection's entity tracker
+ */
+ @NonNull CompletableFuture<@Nullable GeyserEntity> entityByJavaId(@NonNegative int javaId);
+
+ /**
+ * Displays a player entity as emoting to this client.
+ *
+ * @param emoter the player entity emoting
+ * @param emoteId the emote ID to send to this client
+ */
+ void showEmote(@NonNull GeyserPlayerEntity emoter, @NonNull String emoteId);
+
+ /**
+ * Gets the {@link GeyserPlayerEntity} of this connection.
+ *
+ * @return the {@link GeyserPlayerEntity} of this connection
+ */
+ @NonNull GeyserPlayerEntity playerEntity();
+
+ /**
+ * (Un)locks the client's movement inputs, so that they cannot move.
+ * To ensure that movement is only unlocked when all locks are released, you must supply
+ * a UUID with this method, and use the same UUID to unlock the camera.
+ *
+ * @param lock whether to lock the movement
+ * @param owner the owner of the lock
+ * @return if the movement is locked after this method call
+ */
+ boolean lockMovement(boolean lock, @NonNull UUID owner);
+
+ /**
+ * Returns whether the client's movement is currently locked.
+ *
+ * @return whether the movement is locked
+ */
+ boolean isMovementLocked();
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/entity/type/player/GeyserPlayerEntity.java b/api/src/main/java/org/geysermc/geyser/api/entity/type/player/GeyserPlayerEntity.java
index da2e28609..bba4dbf3e 100644
--- a/api/src/main/java/org/geysermc/geyser/api/entity/type/player/GeyserPlayerEntity.java
+++ b/api/src/main/java/org/geysermc/geyser/api/entity/type/player/GeyserPlayerEntity.java
@@ -25,7 +25,15 @@
package org.geysermc.geyser.api.entity.type.player;
+import org.cloudburstmc.math.vector.Vector3f;
import org.geysermc.geyser.api.entity.type.GeyserEntity;
public interface GeyserPlayerEntity extends GeyserEntity {
+
+ /**
+ * Gets the position of the player.
+ *
+ * @return the position of the player.
+ */
+ Vector3f position();
}
diff --git a/api/src/main/java/org/geysermc/geyser/api/event/EventBus.java b/api/src/main/java/org/geysermc/geyser/api/event/EventBus.java
index 801bfa45f..3344e38f4 100644
--- a/api/src/main/java/org/geysermc/geyser/api/event/EventBus.java
+++ b/api/src/main/java/org/geysermc/geyser/api/event/EventBus.java
@@ -28,7 +28,6 @@ package org.geysermc.geyser.api.event;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.event.Event;
import org.geysermc.event.bus.OwnedEventBus;
-import org.geysermc.geyser.api.extension.Extension;
import java.util.Set;
diff --git a/api/src/main/java/org/geysermc/geyser/api/event/EventSubscriber.java b/api/src/main/java/org/geysermc/geyser/api/event/EventSubscriber.java
index 7f91d09a3..258ef9164 100644
--- a/api/src/main/java/org/geysermc/geyser/api/event/EventSubscriber.java
+++ b/api/src/main/java/org/geysermc/geyser/api/event/EventSubscriber.java
@@ -27,7 +27,6 @@ package org.geysermc.geyser.api.event;
import org.geysermc.event.Event;
import org.geysermc.event.subscribe.OwnedSubscriber;
-import org.geysermc.geyser.api.extension.Extension;
/**
* Represents a subscribed listener to a {@link Event}. Wraps around
diff --git a/core/src/main/java/org/geysermc/geyser/item/type/ChestItem.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDisconnectEvent.java
similarity index 56%
rename from core/src/main/java/org/geysermc/geyser/item/type/ChestItem.java
rename to api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDisconnectEvent.java
index 99857006c..05e3415a0 100644
--- a/core/src/main/java/org/geysermc/geyser/item/type/ChestItem.java
+++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionDisconnectEvent.java
@@ -23,31 +23,39 @@
* @link https://github.com/GeyserMC/Geyser
*/
-package org.geysermc.geyser.item.type;
+package org.geysermc.geyser.api.event.bedrock;
-import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import org.checkerframework.checker.nullness.qual.NonNull;
-import org.geysermc.geyser.session.GeyserSession;
+import org.geysermc.geyser.api.connection.GeyserConnection;
+import org.geysermc.geyser.api.event.connection.ConnectionEvent;
-public class ChestItem extends BlockItem {
+/**
+ * Called when a Geyser session disconnects.
+ */
+public class SessionDisconnectEvent extends ConnectionEvent {
+ private String disconnectReason;
- public ChestItem(String javaIdentifier, Builder builder) {
- super(javaIdentifier, builder);
+ public SessionDisconnectEvent(@NonNull GeyserConnection connection, @NonNull String reason) {
+ super(connection);
+ this.disconnectReason = reason;
}
- @Override
- public void translateNbtToBedrock(@NonNull GeyserSession session, @NonNull CompoundTag tag) {
- super.translateNbtToBedrock(session, tag);
+ /**
+ * Gets the disconnect reason.
+ *
+ * @return the reason for the disconnect
+ */
+ public @NonNull String disconnectReason() {
+ return disconnectReason;
+ }
- // Strip the BlockEntityTag from the chests contents
- // sent to the client. The client does not parse this
- // or use it for anything, as this tag is fully
- // server-side, so we remove it to reduce bandwidth and
- // solve potential issues with very large tags.
-
- // There was a problem in the past where this would strip
- // NBT data in creative mode, however with the new server
- // authoritative inventories, this is no longer a concern.
- tag.remove("BlockEntityTag");
+ /**
+ * Sets the disconnect reason, thereby overriding th original reason.
+ *
+ * @param disconnectReason the reason for the disconnect
+ */
+ public void disconnectReason(@NonNull String disconnectReason) {
+ this.disconnectReason = disconnectReason;
}
}
+
diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoginEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoginEvent.java
index c3c8198c1..86a5ec6f8 100644
--- a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoginEvent.java
+++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionLoginEvent.java
@@ -32,18 +32,27 @@ import org.geysermc.geyser.api.connection.GeyserConnection;
import org.geysermc.geyser.api.event.connection.ConnectionEvent;
import org.geysermc.geyser.api.network.RemoteServer;
+import java.util.Map;
+import java.util.Objects;
+
/**
- * Called when a session has logged in, and is about to connect to a remote java server.
+ * Called when a session has logged in, and is about to connect to a remote Java server.
* This event is cancellable, and can be used to prevent the player from connecting to the remote server.
*/
public final class SessionLoginEvent extends ConnectionEvent implements Cancellable {
private RemoteServer remoteServer;
private boolean cancelled;
private String disconnectReason;
+ private Map cookies;
+ private boolean transferring;
- public SessionLoginEvent(@NonNull GeyserConnection connection, @NonNull RemoteServer remoteServer) {
+ public SessionLoginEvent(@NonNull GeyserConnection connection,
+ @NonNull RemoteServer remoteServer,
+ @NonNull Map cookies) {
super(connection);
this.remoteServer = remoteServer;
+ this.cookies = cookies;
+ this.transferring = false;
}
/**
@@ -90,9 +99,9 @@ public final class SessionLoginEvent extends ConnectionEvent implements Cancella
}
/**
- * Gets the {@link RemoteServer} the section will attempt to connect to.
+ * Gets the {@link RemoteServer} the session will attempt to connect to.
*
- * @return the {@link RemoteServer} the section will attempt to connect to.
+ * @return the {@link RemoteServer} the session will attempt to connect to.
*/
public @NonNull RemoteServer remoteServer() {
return this.remoteServer;
@@ -106,4 +115,36 @@ public final class SessionLoginEvent extends ConnectionEvent implements Cancella
public void remoteServer(@NonNull RemoteServer remoteServer) {
this.remoteServer = remoteServer;
}
+
+ /**
+ * Sets a map of cookies from a possible previous session. The Java server can send and request these
+ * to store information on the client across server transfers.
+ */
+ public void cookies(@NonNull Map cookies) {
+ Objects.requireNonNull(cookies);
+ this.cookies = cookies;
+ }
+
+ /**
+ * Gets a map of the sessions cookies, if set.
+ * @return the connections cookies
+ */
+ public @NonNull Map cookies() {
+ return cookies;
+ }
+
+ /**
+ * Determines the connection intent of the connection
+ */
+ public void transferring(boolean transferring) {
+ this.transferring = transferring;
+ }
+
+ /**
+ * Gets whether this login attempt to the Java server
+ * has the transfer intent
+ */
+ public boolean transferring() {
+ return this.transferring;
+ }
}
diff --git a/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionSkinApplyEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionSkinApplyEvent.java
new file mode 100644
index 000000000..f22241e41
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/event/bedrock/SessionSkinApplyEvent.java
@@ -0,0 +1,144 @@
+/*
+ * 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.api.event.bedrock;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.geysermc.geyser.api.connection.GeyserConnection;
+import org.geysermc.geyser.api.event.connection.ConnectionEvent;
+import org.geysermc.geyser.api.skin.Cape;
+import org.geysermc.geyser.api.skin.Skin;
+import org.geysermc.geyser.api.skin.SkinData;
+import org.geysermc.geyser.api.skin.SkinGeometry;
+
+import java.util.UUID;
+
+/**
+ * Called when a skin is applied to a player.
+ *
+ * Won't be called when a fake player is spawned for a player skull.
+ */
+public abstract class SessionSkinApplyEvent extends ConnectionEvent {
+
+ private final String username;
+ private final UUID uuid;
+ private final boolean slim;
+ private final boolean bedrock;
+ private final SkinData originalSkinData;
+
+ public SessionSkinApplyEvent(@NonNull GeyserConnection connection, String username, UUID uuid, boolean slim, boolean bedrock, SkinData skinData) {
+ super(connection);
+ this.username = username;
+ this.uuid = uuid;
+ this.slim = slim;
+ this.bedrock = bedrock;
+ this.originalSkinData = skinData;
+ }
+
+ /**
+ * The username of the player.
+ *
+ * @return the username of the player
+ */
+ public @NonNull String username() {
+ return username;
+ }
+
+ /**
+ * The UUID of the player.
+ *
+ * @return the UUID of the player
+ */
+ public @NonNull UUID uuid() {
+ return uuid;
+ }
+
+ /**
+ * If the player is using a slim model.
+ *
+ * @return if the player is using a slim model
+ */
+ public boolean slim() {
+ return slim;
+ }
+
+ /**
+ * If the player is a Bedrock player.
+ *
+ * @return if the player is a Bedrock player
+ */
+ public boolean bedrock() {
+ return bedrock;
+ }
+
+ /**
+ * The original skin data of the player.
+ *
+ * @return the original skin data of the player
+ */
+ public @NonNull SkinData originalSkin() {
+ return originalSkinData;
+ }
+
+ /**
+ * The skin data of the player.
+ *
+ * @return the current skin data of the player
+ */
+ public abstract @NonNull SkinData skinData();
+
+ /**
+ * Change the skin of the player.
+ *
+ * @param newSkin the new skin
+ */
+ public abstract void skin(@NonNull Skin newSkin);
+
+ /**
+ * Change the cape of the player.
+ *
+ * @param newCape the new cape
+ */
+ public abstract void cape(@NonNull Cape newCape);
+
+ /**
+ * Change the geometry of the player.
+ *
+ * @param newGeometry the new geometry
+ */
+ public abstract void geometry(@NonNull SkinGeometry newGeometry);
+
+ /**
+ * Change the geometry of the player.
+ *
+ * Constructs a generic {@link SkinGeometry} object with the given data.
+ *
+ * @param geometryName the name of the geometry
+ * @param geometryData the data of the geometry
+ */
+ public void geometry(@NonNull String geometryName, @NonNull String geometryData) {
+ geometry(new SkinGeometry("{\"geometry\" :{\"default\" :\"" + geometryName + "\"}}", geometryData));
+ }
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/event/connection/ConnectionRequestEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/connection/ConnectionRequestEvent.java
new file mode 100644
index 000000000..b36ee8bfb
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/event/connection/ConnectionRequestEvent.java
@@ -0,0 +1,110 @@
+/*
+ * 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.api.event.connection;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.geysermc.event.Cancellable;
+import org.geysermc.event.Event;
+
+import java.net.InetSocketAddress;
+
+/**
+ * Called whenever a client attempts to connect to the server, before the connection is accepted.
+ */
+public final class ConnectionRequestEvent implements Event, Cancellable {
+
+ private boolean cancelled;
+ private final InetSocketAddress ip;
+ private final InetSocketAddress proxyIp;
+
+ public ConnectionRequestEvent(@NonNull InetSocketAddress ip, @Nullable InetSocketAddress proxyIp) {
+ this.ip = ip;
+ this.proxyIp = proxyIp;
+ }
+
+ /**
+ * The IP address of the client attempting to connect
+ *
+ * @return the IP address of the client attempting to connect
+ * @deprecated Use {@link #inetSocketAddress()} instead
+ */
+ @NonNull @Deprecated(forRemoval = true)
+ public InetSocketAddress getInetSocketAddress() {
+ return ip;
+ }
+
+ /**
+ * The IP address of the proxy handling the connection. It will return null if there is no proxy.
+ *
+ * @return the IP address of the proxy handling the connection
+ * @deprecated Use {@link #proxyIp()} instead
+ */
+ @Nullable @Deprecated(forRemoval = true)
+ public InetSocketAddress getProxyIp() {
+ return proxyIp;
+ }
+
+ /**
+ * The IP address of the client attempting to connect
+ *
+ * @return the IP address of the client attempting to connect
+ */
+ @NonNull
+ public InetSocketAddress inetSocketAddress() {
+ return ip;
+ }
+
+ /**
+ * The IP address of the proxy handling the connection. It will return null if there is no proxy.
+ *
+ * @return the IP address of the proxy handling the connection
+ */
+ @Nullable
+ public InetSocketAddress proxyIp() {
+ return proxyIp;
+ }
+
+ /**
+ * The cancel status of this event. If this event is cancelled, the connection will be rejected.
+ *
+ * @return the cancel status of this event
+ */
+ @Override
+ public boolean isCancelled() {
+ return cancelled;
+ }
+
+ /**
+ * Sets the cancel status of this event. If this event is canceled, the connection will be rejected.
+ *
+ * @param cancelled the cancel status of this event.
+ */
+ @Override
+ public void setCancelled(boolean cancelled) {
+ this.cancelled = cancelled;
+ }
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/event/connection/GeyserBedrockPingEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/connection/GeyserBedrockPingEvent.java
index 67a81ac58..10ccb93d5 100644
--- a/api/src/main/java/org/geysermc/geyser/api/event/connection/GeyserBedrockPingEvent.java
+++ b/api/src/main/java/org/geysermc/geyser/api/event/connection/GeyserBedrockPingEvent.java
@@ -34,7 +34,7 @@ import java.net.InetSocketAddress;
/**
* Called whenever Geyser gets pinged
- *
+ *
* This event allows you to modify/obtain the MOTD, maximum player count, and current number of players online,
* Geyser will reply to the client with what was given.
*/
diff --git a/api/src/main/java/org/geysermc/geyser/api/event/java/ServerTransferEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerTransferEvent.java
new file mode 100644
index 000000000..594e28ef0
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/event/java/ServerTransferEvent.java
@@ -0,0 +1,125 @@
+/*
+ * 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.api.event.java;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.checkerframework.common.value.qual.IntRange;
+import org.geysermc.geyser.api.connection.GeyserConnection;
+import org.geysermc.geyser.api.event.connection.ConnectionEvent;
+
+import java.util.Map;
+
+/**
+ * Fired when the Java server sends a transfer request to a different Java server.
+ * Geyser Extensions can listen to this event and set a target server ip/port for Bedrock players to be transferred to.
+ */
+public class ServerTransferEvent extends ConnectionEvent {
+
+ private final String host;
+ private final int port;
+ private String bedrockHost;
+ private int bedrockPort;
+ private final Map cookies;
+
+ public ServerTransferEvent(@NonNull GeyserConnection connection,
+ @NonNull String host, int port, @NonNull Map cookies) {
+ super(connection);
+ this.host = host;
+ this.port = port;
+ this.cookies = cookies;
+ this.bedrockHost = null;
+ this.bedrockPort = -1;
+ }
+
+ /**
+ * The host that the Java server requests a transfer to.
+ *
+ * @return the host
+ */
+ public @NonNull String host() {
+ return this.host;
+ }
+
+ /**
+ * The port that the Java server requests a transfer to.
+ *
+ * @return the port
+ */
+ public int port() {
+ return this.port;
+ }
+
+ /**
+ * The host that the Bedrock player should try and connect to.
+ * If this is not set, the Bedrock player will just be disconnected.
+ *
+ * @return the host where the Bedrock client will be transferred to, or null if not set.
+ */
+ public @Nullable String bedrockHost() {
+ return this.bedrockHost;
+ }
+
+ /**
+ * The port that the Bedrock player should try and connect to.
+ * If this is not set, the Bedrock player will just be disconnected.
+ *
+ * @return the port where the Bedrock client will be transferred to, or -1 if not set.
+ */
+ public int bedrockPort() {
+ return this.bedrockPort;
+ }
+
+ /**
+ * Sets the host for the Bedrock player to be transferred to
+ */
+ public void bedrockHost(@NonNull String host) {
+ if (host == null || host.isBlank()) {
+ throw new IllegalArgumentException("Server address cannot be null or blank");
+ }
+ this.bedrockHost = host;
+ }
+
+ /**
+ * Sets the port for the Bedrock player to be transferred to
+ */
+ public void bedrockPort(@IntRange(from = 0, to = 65535) int port) {
+ if (port < 0 || port > 65535) {
+ throw new IllegalArgumentException("Server port must be between 0 and 65535, was " + port);
+ }
+ this.bedrockPort = port;
+ }
+
+ /**
+ * Gets a map of the sessions current cookies.
+ *
+ * @return the connections cookies
+ */
+ public @NonNull Map cookies() {
+ return cookies;
+ }
+
+}
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 77d5efa65..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
@@ -33,7 +33,7 @@ import java.util.Map;
/**
* Called when commands are defined within Geyser.
- *
+ *
* This event allows you to register new commands using the {@link #register(Command)}
* method and retrieve the default commands defined.
*/
@@ -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/GeyserDefineCustomBlocksEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomBlocksEvent.java
index b1a01d7e6..a105578d9 100644
--- a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomBlocksEvent.java
+++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomBlocksEvent.java
@@ -28,13 +28,12 @@ package org.geysermc.geyser.api.event.lifecycle;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.api.block.custom.CustomBlockData;
import org.geysermc.geyser.api.block.custom.CustomBlockState;
-import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockItem;
import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockState;
import org.geysermc.event.Event;
/**
* Called on Geyser's startup when looking for custom blocks. Custom blocks must be registered through this event.
- *
+ *
* This event will not be called if the "add-non-bedrock-items" setting is disabled in the Geyser config.
*/
public abstract class GeyserDefineCustomBlocksEvent implements Event {
@@ -48,8 +47,8 @@ public abstract class GeyserDefineCustomBlocksEvent implements Event {
/**
* Registers the given {@link CustomBlockState} as an override for the
* given java state identifier
- * Java state identifiers are listed in
- * https://raw.githubusercontent.com/GeyserMC/mappings/master/blocks.json
+ * Java state identifiers are listed
+ * here
*
* @param javaIdentifier the java state identifier to override
* @param customBlockState the custom block state with which to override java state identifier
diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomItemsEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomItemsEvent.java
index 0957b8551..bd14aaf43 100644
--- a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomItemsEvent.java
+++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomItemsEvent.java
@@ -36,7 +36,7 @@ import java.util.Map;
/**
* Called on Geyser's startup when looking for custom items. Custom items must be registered through this event.
- *
+ *
* This event will not be called if the "add non-Bedrock items" setting is disabled in the Geyser config.
*/
public interface GeyserDefineCustomItemsEvent extends Event {
diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomSkullsEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomSkullsEvent.java
index 17f7b599a..6443bbeb3 100644
--- a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomSkullsEvent.java
+++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserDefineCustomSkullsEvent.java
@@ -5,7 +5,7 @@ import org.geysermc.event.Event;
/**
* Called on Geyser's startup when looking for custom skulls. Custom skulls must be registered through this event.
- *
+ *
* This event will not be called if the "add-non-bedrock-items" setting is disabled in the Geyser config.
*/
public abstract class GeyserDefineCustomSkullsEvent implements Event {
diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserPostReloadEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserPostReloadEvent.java
new file mode 100644
index 000000000..c421cda37
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserPostReloadEvent.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.api.event.lifecycle;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.geysermc.event.Event;
+import org.geysermc.geyser.api.event.EventBus;
+import org.geysermc.geyser.api.event.EventRegistrar;
+import org.geysermc.geyser.api.extension.ExtensionManager;
+
+/**
+ * Called when Geyser finished reloading and is accepting Bedrock connections again.
+ * Equivalent to the {@link GeyserPostInitializeEvent}
+ *
+ * @param extensionManager the extension manager
+ * @param eventBus the event bus
+ */
+public record GeyserPostReloadEvent(@NonNull ExtensionManager extensionManager, @NonNull EventBus eventBus) implements Event {
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserPreReloadEvent.java b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserPreReloadEvent.java
new file mode 100644
index 000000000..16d5058da
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/event/lifecycle/GeyserPreReloadEvent.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.api.event.lifecycle;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.geysermc.event.Event;
+import org.geysermc.geyser.api.event.EventBus;
+import org.geysermc.geyser.api.event.EventRegistrar;
+import org.geysermc.geyser.api.extension.ExtensionManager;
+
+/**
+ * Called when Geyser is about to reload. Primarily aimed at extensions, so they can decide on their own what to reload.
+ * After this event is fired, some lifecycle events can be fired again - such as the {@link GeyserLoadResourcePacksEvent}.
+ *
+ * @param extensionManager the extension manager
+ * @param eventBus the event bus
+ */
+public record GeyserPreReloadEvent(@NonNull ExtensionManager extensionManager, @NonNull EventBus eventBus) implements Event {
+}
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/ExtensionManager.java b/api/src/main/java/org/geysermc/geyser/api/extension/ExtensionManager.java
index a9d0d7376..5226221df 100644
--- a/api/src/main/java/org/geysermc/geyser/api/extension/ExtensionManager.java
+++ b/api/src/main/java/org/geysermc/geyser/api/extension/ExtensionManager.java
@@ -36,13 +36,13 @@ import java.util.Collection;
public abstract class ExtensionManager {
/**
- * Gets an extension with the given name.
+ * Gets an extension by the given ID.
*
- * @param name the name of the extension
- * @return an extension with the given name
+ * @param id the ID of the extension
+ * @return an extension with the given ID
*/
@Nullable
- public abstract Extension extension(@NonNull String name);
+ public abstract Extension extension(@NonNull String id);
/**
* Enables the given {@link Extension}.
diff --git a/api/src/main/java/org/geysermc/geyser/api/extension/exception/InvalidDescriptionException.java b/api/src/main/java/org/geysermc/geyser/api/extension/exception/InvalidDescriptionException.java
index 1fe88e9e9..5313c1f40 100644
--- a/api/src/main/java/org/geysermc/geyser/api/extension/exception/InvalidDescriptionException.java
+++ b/api/src/main/java/org/geysermc/geyser/api/extension/exception/InvalidDescriptionException.java
@@ -25,10 +25,16 @@
package org.geysermc.geyser.api.extension.exception;
+import java.io.Serial;
+
/**
* Thrown when an extension's description is invalid.
*/
public class InvalidDescriptionException extends Exception {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
public InvalidDescriptionException(Throwable cause) {
super(cause);
}
diff --git a/api/src/main/java/org/geysermc/geyser/api/extension/exception/InvalidExtensionException.java b/api/src/main/java/org/geysermc/geyser/api/extension/exception/InvalidExtensionException.java
index 7fb6b6922..89780fe46 100644
--- a/api/src/main/java/org/geysermc/geyser/api/extension/exception/InvalidExtensionException.java
+++ b/api/src/main/java/org/geysermc/geyser/api/extension/exception/InvalidExtensionException.java
@@ -25,10 +25,16 @@
package org.geysermc.geyser.api.extension.exception;
+import java.io.Serial;
+
/**
* Thrown when an extension is invalid.
*/
public class InvalidExtensionException extends Exception {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
public InvalidExtensionException(Throwable cause) {
super(cause);
}
diff --git a/api/src/main/java/org/geysermc/geyser/api/item/custom/CustomItemData.java b/api/src/main/java/org/geysermc/geyser/api/item/custom/CustomItemData.java
index d256b9ac0..3b871cd74 100644
--- a/api/src/main/java/org/geysermc/geyser/api/item/custom/CustomItemData.java
+++ b/api/src/main/java/org/geysermc/geyser/api/item/custom/CustomItemData.java
@@ -29,6 +29,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.api.GeyserApi;
+import java.util.OptionalInt;
+import java.util.Set;
+
/**
* This is used to store data for a custom item.
*/
@@ -75,6 +78,20 @@ public interface CustomItemData {
*/
boolean displayHandheld();
+ /**
+ * Gets the item's creative category, or tab id.
+ *
+ * @return the item's creative category
+ */
+ @NonNull OptionalInt creativeCategory();
+
+ /**
+ * Gets the item's creative group.
+ *
+ * @return the item's creative group
+ */
+ @Nullable String creativeGroup();
+
/**
* Gets the item's texture size. This is to resize the item if the texture is not 16x16.
*
@@ -89,6 +106,14 @@ public interface CustomItemData {
*/
@Nullable CustomRenderOffsets renderOffsets();
+ /**
+ * Gets the item's set of tags that can be used in Molang.
+ * Equivalent to "tag:some_tag"
+ *
+ * @return the item's tags, if they exist
+ */
+ @NonNull Set tags();
+
static CustomItemData.Builder builder() {
return GeyserApi.api().provider(CustomItemData.Builder.class);
}
@@ -109,10 +134,16 @@ public interface CustomItemData {
Builder displayHandheld(boolean displayHandheld);
+ Builder creativeCategory(int creativeCategory);
+
+ Builder creativeGroup(@Nullable String creativeGroup);
+
Builder textureSize(int textureSize);
Builder renderOffsets(@Nullable CustomRenderOffsets renderOffsets);
+ Builder tags(@Nullable Set tags);
+
CustomItemData build();
}
}
diff --git a/api/src/main/java/org/geysermc/geyser/api/item/custom/NonVanillaCustomItemData.java b/api/src/main/java/org/geysermc/geyser/api/item/custom/NonVanillaCustomItemData.java
index 0a09f6958..2c283780c 100644
--- a/api/src/main/java/org/geysermc/geyser/api/item/custom/NonVanillaCustomItemData.java
+++ b/api/src/main/java/org/geysermc/geyser/api/item/custom/NonVanillaCustomItemData.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
@@ -30,7 +30,6 @@ import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.api.GeyserApi;
-import java.util.OptionalInt;
import java.util.Set;
/**
@@ -65,6 +64,14 @@ public interface NonVanillaCustomItemData extends CustomItemData {
*/
int maxDamage();
+ /**
+ * Gets the attack damage of the item.
+ * This is purely visual, and only applied to tools
+ *
+ * @return the attack damage of the item
+ */
+ int attackDamage();
+
/**
* Gets the tool type of the item.
*
@@ -107,20 +114,6 @@ public interface NonVanillaCustomItemData extends CustomItemData {
*/
@Nullable Set repairMaterials();
- /**
- * Gets the item's creative category, or tab id.
- *
- * @return the item's creative category
- */
- @NonNull OptionalInt creativeCategory();
-
- /**
- * Gets the item's creative group.
- *
- * @return the item's creative group
- */
- @Nullable String creativeGroup();
-
/**
* Gets if the item is a hat. This is used to determine if the item should be rendered on the player's head, and
* normally allow the player to equip it. This is not meant for armor.
@@ -168,6 +161,13 @@ public interface NonVanillaCustomItemData extends CustomItemData {
return displayHandheld();
}
+ /**
+ * Gets the block the item places.
+ *
+ * @return the block the item places
+ */
+ String block();
+
static NonVanillaCustomItemData.Builder builder() {
return GeyserApi.api().provider(NonVanillaCustomItemData.Builder.class);
}
@@ -184,6 +184,8 @@ public interface NonVanillaCustomItemData extends CustomItemData {
Builder maxDamage(int maxDamage);
+ Builder attackDamage(int attackDamage);
+
Builder toolType(@Nullable String toolType);
Builder toolTier(@Nullable String toolTier);
@@ -196,10 +198,6 @@ public interface NonVanillaCustomItemData extends CustomItemData {
Builder repairMaterials(@Nullable Set repairMaterials);
- Builder creativeCategory(int creativeCategory);
-
- Builder creativeGroup(@Nullable String creativeGroup);
-
Builder hat(boolean isHat);
Builder foil(boolean isFoil);
@@ -210,6 +208,8 @@ public interface NonVanillaCustomItemData extends CustomItemData {
Builder chargeable(boolean isChargeable);
+ Builder block(String block);
+
/**
* @deprecated Use {@link #displayHandheld(boolean)} instead.
*/
@@ -218,6 +218,12 @@ public interface NonVanillaCustomItemData extends CustomItemData {
return displayHandheld(isTool);
}
+ @Override
+ Builder creativeCategory(int creativeCategory);
+
+ @Override
+ Builder creativeGroup(@Nullable String creativeGroup);
+
@Override
Builder customItemOptions(@NonNull CustomItemOptions customItemOptions);
@@ -239,6 +245,9 @@ public interface NonVanillaCustomItemData extends CustomItemData {
@Override
Builder renderOffsets(@Nullable CustomRenderOffsets renderOffsets);
+ @Override
+ Builder tags(@Nullable Set tags);
+
NonVanillaCustomItemData build();
}
}
diff --git a/api/src/main/java/org/geysermc/geyser/api/network/BedrockListener.java b/api/src/main/java/org/geysermc/geyser/api/network/BedrockListener.java
index 61fe286aa..af35d7ad1 100644
--- a/api/src/main/java/org/geysermc/geyser/api/network/BedrockListener.java
+++ b/api/src/main/java/org/geysermc/geyser/api/network/BedrockListener.java
@@ -50,6 +50,14 @@ public interface BedrockListener {
*/
int port();
+ /**
+ * Gets the broadcast port that's sent to Bedrock clients with the motd.
+ * This is the port that Bedrock clients will connect with. It usually does not differ from the listening port.
+ *
+ * @return the broadcast port
+ */
+ int broadcastPort();
+
/**
* Gets the primary MOTD shown to Bedrock players if a ping passthrough setting is not enabled.
*
diff --git a/api/src/main/java/org/geysermc/geyser/api/pack/PathPackCodec.java b/api/src/main/java/org/geysermc/geyser/api/pack/PathPackCodec.java
index ee5db8242..a3770451a 100644
--- a/api/src/main/java/org/geysermc/geyser/api/pack/PathPackCodec.java
+++ b/api/src/main/java/org/geysermc/geyser/api/pack/PathPackCodec.java
@@ -42,4 +42,4 @@ public abstract class PathPackCodec extends PackCodec {
*/
@NonNull
public abstract Path path();
-}
+}
\ No newline at end of file
diff --git a/core/src/main/java/org/geysermc/geyser/item/DyeableLeatherItem.java b/api/src/main/java/org/geysermc/geyser/api/permission/PermissionChecker.java
similarity index 55%
rename from core/src/main/java/org/geysermc/geyser/item/DyeableLeatherItem.java
rename to api/src/main/java/org/geysermc/geyser/api/permission/PermissionChecker.java
index e0eec767f..c0d4af2f4 100644
--- a/core/src/main/java/org/geysermc/geyser/item/DyeableLeatherItem.java
+++ b/api/src/main/java/org/geysermc/geyser/api/permission/PermissionChecker.java
@@ -23,34 +23,27 @@
* @link https://github.com/GeyserMC/Geyser
*/
-package org.geysermc.geyser.item;
+package org.geysermc.geyser.api.permission;
-import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
-import com.github.steveice10.opennbt.tag.builtin.IntTag;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.geysermc.geyser.api.command.CommandSource;
+import org.geysermc.geyser.api.util.TriState;
-public interface DyeableLeatherItem {
+/**
+ * Something capable of checking if a {@link CommandSource} has a permission
+ */
+@FunctionalInterface
+public interface PermissionChecker {
- static void translateNbtToBedrock(CompoundTag tag) {
- CompoundTag displayTag = tag.get("display");
- if (displayTag == null) {
- return;
- }
- IntTag color = displayTag.remove("color");
- if (color != null) {
- tag.put(new IntTag("customColor", color.getValue()));
- }
- }
-
- static void translateNbtToJava(CompoundTag tag) {
- IntTag color = tag.get("customColor");
- if (color == null) {
- return;
- }
- CompoundTag displayTag = tag.get("display");
- if (displayTag == null) {
- displayTag = new CompoundTag("display");
- }
- displayTag.put(color);
- tag.remove("customColor");
- }
+ /**
+ * 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/core/src/main/java/org/geysermc/geyser/util/collection/package-info.java b/api/src/main/java/org/geysermc/geyser/api/skin/Cape.java
similarity index 66%
rename from core/src/main/java/org/geysermc/geyser/util/collection/package-info.java
rename to api/src/main/java/org/geysermc/geyser/api/skin/Cape.java
index e17a38877..1e7341ae4 100644
--- a/core/src/main/java/org/geysermc/geyser/util/collection/package-info.java
+++ b/api/src/main/java/org/geysermc/geyser/api/skin/Cape.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
+ * 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
@@ -23,12 +23,18 @@
* @link https://github.com/GeyserMC/Geyser
*/
+package org.geysermc.geyser.api.skin;
+
/**
- * Contains useful collections for use in Geyser.
+ * Represents a cape.
*
- * Of note are the fixed int maps. Designed for use with block states that are positive and sequential, they do not allow keys to be
- * added that are not greater by one versus the previous key. Because of this, speedy operations of {@link java.util.Map#get(java.lang.Object)}
- * and {@link java.util.Map#containsKey(java.lang.Object)} can be performed by simply checking the bounds of the map
- * size and its "start" integer.
+ * @param textureUrl The URL of the cape texture
+ * @param capeId The ID of the cape
+ * @param capeData The raw cape image data in ARGB format
+ * @param failed If the cape failed to load, this is for things like fallback capes
*/
-package org.geysermc.geyser.util.collection;
\ No newline at end of file
+public record Cape(String textureUrl, String capeId, byte[] capeData, boolean failed) {
+ public Cape(String textureUrl, String capeId, byte[] capeData) {
+ this(textureUrl, capeId, capeData, false);
+ }
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/skin/Skin.java b/api/src/main/java/org/geysermc/geyser/api/skin/Skin.java
new file mode 100644
index 000000000..9b39ddfe8
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/skin/Skin.java
@@ -0,0 +1,39 @@
+/*
+ * 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.api.skin;
+
+/**
+ * Represents a skin.
+ *
+ * @param textureUrl The URL/ID of the skin texture
+ * @param skinData The raw skin image data in ARGB
+ * @param failed If the skin failed to load, this is for things like fallback skins
+ */
+public record Skin(String textureUrl, byte[] skinData, boolean failed) {
+ public Skin(String textureUrl, byte[] skinData) {
+ this(textureUrl, skinData, false);
+ }
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/skin/SkinData.java b/api/src/main/java/org/geysermc/geyser/api/skin/SkinData.java
new file mode 100644
index 000000000..9de4a3534
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/skin/SkinData.java
@@ -0,0 +1,32 @@
+/*
+ * 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.api.skin;
+
+/**
+ * Represents a full package of {@link Skin}, {@link Cape}, and {@link SkinGeometry}.
+ */
+public record SkinData(Skin skin, Cape cape, SkinGeometry geometry) {
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/skin/SkinGeometry.java b/api/src/main/java/org/geysermc/geyser/api/skin/SkinGeometry.java
new file mode 100644
index 000000000..5b40d2022
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/skin/SkinGeometry.java
@@ -0,0 +1,48 @@
+/*
+ * 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.api.skin;
+
+/**
+ * Represents geometry of a skin.
+ *
+ * @param geometryName The name of the geometry (JSON)
+ * @param geometryData The geometry data (JSON)
+ */
+public record SkinGeometry(String geometryName, String geometryData) {
+
+ public static SkinGeometry WIDE = getLegacy(false);
+ public static SkinGeometry SLIM = getLegacy(true);
+
+ /**
+ * Generate generic geometry
+ *
+ * @param isSlim if true, it will be the slimmer alex model
+ * @return The generic geometry object
+ */
+ private static SkinGeometry getLegacy(boolean isSlim) {
+ return new SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.custom" + (isSlim ? "Slim" : "") + "\"}}", "");
+ }
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/util/CreativeCategory.java b/api/src/main/java/org/geysermc/geyser/api/util/CreativeCategory.java
index 7519ff157..245eb9bc2 100644
--- a/api/src/main/java/org/geysermc/geyser/api/util/CreativeCategory.java
+++ b/api/src/main/java/org/geysermc/geyser/api/util/CreativeCategory.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2019-2023 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
@@ -31,11 +31,10 @@ import org.checkerframework.checker.nullness.qual.NonNull;
* Represents the creative menu categories or tabs.
*/
public enum CreativeCategory {
- COMMANDS("commands", 1),
- CONSTRUCTION("construction", 2),
+ CONSTRUCTION("construction", 1),
+ NATURE("nature", 2),
EQUIPMENT("equipment", 3),
ITEMS("items", 4),
- NATURE("nature", 5),
NONE("none", 6);
private final String internalName;
@@ -51,7 +50,7 @@ public enum CreativeCategory {
*
* @return the name of the category
*/
- @NonNull public String internalName() {
+ public @NonNull String internalName() {
return internalName;
}
diff --git a/api/src/main/java/org/geysermc/geyser/api/util/MinecraftVersion.java b/api/src/main/java/org/geysermc/geyser/api/util/MinecraftVersion.java
new file mode 100644
index 000000000..34a4b59af
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/util/MinecraftVersion.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.util;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+/**
+ * Represents a Minecraft version.
+ */
+public interface MinecraftVersion {
+
+ /**
+ * Gets the Minecraft version as a String.
+ * Example: "1.20.2", or "1.20.40/1.20.41"
+ *
+ * @return the version string
+ */
+ @NonNull String versionString();
+
+ /**
+ * Gets the protocol version of this Minecraft version.
+ *
+ * @return the protocol version
+ */
+ int protocolVersion();
+}
diff --git a/api/src/main/java/org/geysermc/geyser/api/util/PlatformType.java b/api/src/main/java/org/geysermc/geyser/api/util/PlatformType.java
index 815381d6b..cda5e06e4 100644
--- a/api/src/main/java/org/geysermc/geyser/api/util/PlatformType.java
+++ b/api/src/main/java/org/geysermc/geyser/api/util/PlatformType.java
@@ -34,10 +34,12 @@ public record PlatformType(String platformName) {
public static final PlatformType ANDROID = new PlatformType("Android");
public static final PlatformType BUNGEECORD = new PlatformType("BungeeCord");
public static final PlatformType FABRIC = new PlatformType("Fabric");
+ public static final PlatformType NEOFORGE = new PlatformType("NeoForge");
public static final PlatformType SPIGOT = new PlatformType("Spigot");
@Deprecated
public static final PlatformType SPONGE = new PlatformType("Sponge");
public static final PlatformType STANDALONE = new PlatformType("Standalone");
public static final PlatformType VELOCITY = new PlatformType("Velocity");
+ public static final PlatformType VIAPROXY = new PlatformType("ViaProxy");
}
diff --git a/api/src/main/java/org/geysermc/geyser/api/util/TriState.java b/api/src/main/java/org/geysermc/geyser/api/util/TriState.java
index 457a38e32..a8bb723d6 100644
--- a/api/src/main/java/org/geysermc/geyser/api/util/TriState.java
+++ b/api/src/main/java/org/geysermc/geyser/api/util/TriState.java
@@ -30,7 +30,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
/**
* This is a way to represent a boolean, but with a non set value added.
- * This class was inspired by adventure's version https://github.com/KyoriPowered/adventure/blob/main/4/api/src/main/java/net/kyori/adventure/util/TriState.java
+ * This class was inspired by adventure's TriState
*/
public enum TriState {
/**
diff --git a/bootstrap/bungeecord/build.gradle.kts b/bootstrap/bungeecord/build.gradle.kts
index 4025569dd..5fe7ea3d1 100644
--- a/bootstrap/bungeecord/build.gradle.kts
+++ b/bootstrap/bungeecord/build.gradle.kts
@@ -1,20 +1,24 @@
dependencies {
api(projects.core)
+ implementation(libs.cloud.bungee)
implementation(libs.adventure.text.serializer.bungeecord)
+ compileOnlyApi(libs.bungeecord.proxy)
}
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 {
@@ -22,6 +26,7 @@ tasks.withType {
dependencies {
exclude(dependency("com.google.*:.*"))
+ exclude(dependency("io.netty.incubator:.*"))
exclude(dependency("io.netty:netty-transport-native-epoll:.*"))
exclude(dependency("io.netty:netty-transport-native-unix-common:.*"))
exclude(dependency("io.netty:netty-handler:.*"))
@@ -33,3 +38,8 @@ tasks.withType {
exclude(dependency("io.netty:netty-resolver-dns:.*"))
}
}
+
+modrinth {
+ uploadFile.set(tasks.getByPath("shadowJar"))
+ loaders.add("bungeecord")
+}
diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeCompressionDisabler.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeCompressionDisabler.java
index 084e1d2dc..485079a05 100644
--- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeCompressionDisabler.java
+++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeCompressionDisabler.java
@@ -32,18 +32,26 @@ import net.md_5.bungee.protocol.packet.LoginSuccess;
import net.md_5.bungee.protocol.packet.SetCompression;
public class GeyserBungeeCompressionDisabler extends ChannelOutboundHandlerAdapter {
+ private boolean compressionDisabled = false;
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (!(msg instanceof SetCompression)) {
- if (msg instanceof LoginSuccess) {
- // We're past the point that compression can be enabled
+ // Fixes https://github.com/GeyserMC/Geyser/issues/4281
+ // The server may send a LoginDisconnect packet after compression is set.
+ if (!compressionDisabled) {
if (ctx.pipeline().get("compress") != null) {
ctx.pipeline().remove("compress");
+ compressionDisabled = true;
}
if (ctx.pipeline().get("decompress") != null) {
ctx.pipeline().remove("decompress");
+ compressionDisabled = true;
}
+ }
+
+ if (msg instanceof LoginSuccess) {
+ // We're past the point that compression can be enabled
ctx.pipeline().remove(this);
}
super.write(ctx, msg, promise);
diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeDumpInfo.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeDumpInfo.java
index ba7bc464f..85abd285b 100644
--- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeDumpInfo.java
+++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeDumpInfo.java
@@ -30,6 +30,7 @@ import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.plugin.Plugin;
import org.geysermc.geyser.dump.BootstrapDumpInfo;
+import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -51,7 +52,8 @@ public class GeyserBungeeDumpInfo extends BootstrapDumpInfo {
this.plugins = new ArrayList<>();
for (net.md_5.bungee.api.config.ListenerInfo listener : proxy.getConfig().getListeners()) {
- this.listeners.add(new ListenerInfo(listener.getHost().getHostString(), listener.getHost().getPort()));
+ InetSocketAddress address = (InetSocketAddress) listener.getSocketAddress();
+ this.listeners.add(new ListenerInfo(address.getHostString(), address.getPort()));
}
for (Plugin plugin : proxy.getPluginManager().getPlugins()) {
diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeInjector.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeInjector.java
index e10b3ce6f..7c60ba95d 100644
--- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeInjector.java
+++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeInjector.java
@@ -39,6 +39,7 @@ import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.event.EventHandler;
import net.md_5.bungee.netty.PipelineUtils;
+import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.GeyserBootstrap;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.network.netty.GeyserInjector;
@@ -125,7 +126,7 @@ public class GeyserBungeeInjector extends GeyserInjector implements Listener {
.channel(LocalServerChannelWrapper.class)
.childHandler(new ChannelInitializer<>() {
@Override
- protected void initChannel(Channel ch) throws Exception {
+ protected void initChannel(@NonNull Channel ch) throws Exception {
if (proxy.getConfig().getServers() == null) {
// Proxy hasn't finished loading all plugins - it loads the config after all plugins
// Probably doesn't need to be translatable?
diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeLogger.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeLogger.java
index daeb20102..e8cf7ee39 100644
--- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeLogger.java
+++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeeLogger.java
@@ -26,22 +26,19 @@
package org.geysermc.geyser.platform.bungeecord;
import lombok.Getter;
+import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.geysermc.geyser.GeyserLogger;
import java.util.logging.Level;
import java.util.logging.Logger;
+@RequiredArgsConstructor
public class GeyserBungeeLogger implements GeyserLogger {
private final Logger logger;
@Getter @Setter
private boolean debug;
- public GeyserBungeeLogger(Logger logger, boolean debug) {
- this.logger = logger;
- this.debug = debug;
- }
-
@Override
public void severe(String message) {
logger.severe(message);
diff --git a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePingPassthrough.java b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePingPassthrough.java
index 39fb9e134..3c3853ed8 100644
--- a/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePingPassthrough.java
+++ b/bootstrap/bungeecord/src/main/java/org/geysermc/geyser/platform/bungeecord/GeyserBungeePingPassthrough.java
@@ -35,12 +35,12 @@ import net.md_5.bungee.api.connection.PendingConnection;
import net.md_5.bungee.api.event.ProxyPingEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.protocol.ProtocolConstants;
+import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.ping.GeyserPingInfo;
import org.geysermc.geyser.ping.IGeyserPingPassthrough;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
-import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@@ -61,16 +61,11 @@ public class GeyserBungeePingPassthrough implements IGeyserPingPassthrough, List
}));
ProxyPingEvent event = future.join();
ServerPing response = event.getResponse();
- GeyserPingInfo geyserPingInfo = new GeyserPingInfo(
+ return new GeyserPingInfo(
response.getDescriptionComponent().toLegacyText(),
- new GeyserPingInfo.Players(response.getPlayers().getMax(), response.getPlayers().getOnline()),
- new GeyserPingInfo.Version(response.getVersion().getName(), response.getVersion().getProtocol())
+ response.getPlayers().getMax(),
+ response.getPlayers().getOnline()
);
- if (event.getResponse().getPlayers().getSample() != null) {
- Arrays.stream(event.getResponse().getPlayers().getSample()).forEach(proxiedPlayer ->
- geyserPingInfo.getPlayerList().add(proxiedPlayer.getName()));
- }
- return geyserPingInfo;
}
// This is static so pending connection can use it
@@ -110,7 +105,7 @@ public class GeyserBungeePingPassthrough implements IGeyserPingPassthrough, List
}
@Override
- public InetSocketAddress getVirtualHost() {
+ public @Nullable InetSocketAddress getVirtualHost() {
return null;
}
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 538e00a05..1c0049231 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,24 +27,28 @@ 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;
+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.GeyserBootstrap;
import org.geysermc.geyser.GeyserImpl;
-import org.geysermc.geyser.api.command.Command;
-import org.geysermc.geyser.api.extension.Extension;
-import org.geysermc.geyser.command.GeyserCommandManager;
+import org.geysermc.geyser.api.util.PlatformType;
+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.jetbrains.annotations.NotNull;
+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,69 +58,117 @@ 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;
-import java.util.logging.Level;
public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap {
- private GeyserCommandManager geyserCommandManager;
+ private CommandRegistry commandRegistry;
private GeyserBungeeConfiguration geyserConfig;
private GeyserBungeeInjector geyserInjector;
- private GeyserBungeeLogger geyserLogger;
+ private final GeyserBungeeLogger geyserLogger = new GeyserBungeeLogger(getLogger());
private IGeyserPingPassthrough geyserBungeePingPassthrough;
-
private GeyserImpl geyser;
+ // We can't disable the plugin; hence we need to keep track of it manually
+ private boolean disabled;
+
@Override
public void onLoad() {
+ onGeyserInitialize();
+ }
+
+ @Override
+ public void onGeyserInitialize() {
GeyserLocale.init(this);
// Copied from ViaVersion.
// https://github.com/ViaVersion/ViaVersion/blob/b8072aad86695cc8ec6f5e4103e43baf3abf6cc5/bungee/src/main/java/us/myles/ViaVersion/BungeePlugin.java#L43
try {
- ProtocolConstants.class.getField("MINECRAFT_1_19_3");
+ ProtocolConstants.class.getField("MINECRAFT_1_21");
} catch (NoSuchFieldException e) {
- getLogger().warning(" / \\");
- getLogger().warning(" / \\");
- getLogger().warning(" / | \\");
- getLogger().warning(" / | \\ " + GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_proxy", getProxy().getName()));
- getLogger().warning(" / \\ " + GeyserLocale.getLocaleStringLog("geyser.may_not_work_as_intended_all_caps"));
- getLogger().warning(" / o \\");
- getLogger().warning("/_____________\\");
+ geyserLogger.error(" / \\");
+ geyserLogger.error(" / \\");
+ geyserLogger.error(" / | \\");
+ geyserLogger.error(" / | \\ " + GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_proxy", getProxy().getName()));
+ geyserLogger.error(" / \\ " + GeyserLocale.getLocaleStringLog("geyser.may_not_work_as_intended_all_caps"));
+ geyserLogger.error(" / o \\");
+ geyserLogger.error("/_____________\\");
}
- if (!getDataFolder().exists())
- getDataFolder().mkdir();
-
- try {
- if (!getDataFolder().exists())
- getDataFolder().mkdir();
- File configFile = FileUtils.fileOrCopiedFromResource(new File(getDataFolder(), "config.yml"),
- "config.yml", (x) -> x.replaceAll("generateduuid", UUID.randomUUID().toString()), this);
- this.geyserConfig = FileUtils.loadConfig(configFile, GeyserBungeeConfiguration.class);
- } catch (IOException ex) {
- getLogger().log(Level.SEVERE, GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex);
- ex.printStackTrace();
+ if (!this.loadConfig()) {
+ disabled = true;
return;
}
-
- this.geyserLogger = new GeyserBungeeLogger(getLogger(), geyserConfig.isDebugMode());
+ this.geyserLogger.setDebug(geyserConfig.isDebugMode());
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() {
- // Remove this in like a year
- if (getProxy().getPluginManager().getPlugin("floodgate-bungee") != null) {
- geyserLogger.severe(GeyserLocale.getLocaleStringLog("geyser.bootstrap.floodgate.outdated", "https://ci.opencollab.dev/job/GeyserMC/job/Floodgate/job/master/"));
+ if (disabled) {
+ return; // Config did not load properly!
+ }
+ // 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
+ this.awaitStartupCompletion(0);
+ }
+
+ @SuppressWarnings("unchecked")
+ private void awaitStartupCompletion(int tries) {
+ // After 20 tries give up waiting. This will happen just after 3 minutes approximately
+ if (tries >= 20) {
+ this.geyserLogger.warning("BungeeCord plugin startup is taking abnormally long, so Geyser is starting now. " +
+ "If all your plugins are loaded properly, this is a bug! " +
+ "If not, consider cutting down the amount of plugins on your proxy as it is causing abnormally slow starting times.");
+ this.onGeyserEnable();
return;
}
+ try {
+ Field listenersField = BungeeCord.getInstance().getClass().getDeclaredField("listeners");
+ listenersField.setAccessible(true);
+
+ Collection listeners = (Collection) listenersField.get(BungeeCord.getInstance());
+ if (listeners.isEmpty()) {
+ this.getProxy().getScheduler().schedule(this, this::onGeyserEnable, tries, TimeUnit.SECONDS);
+ } else {
+ this.awaitStartupCompletion(++tries);
+ }
+ } catch (NoSuchFieldException | IllegalAccessException ex) {
+ ex.printStackTrace();
+ }
+ }
+
+ public void onGeyserEnable() {
+ if (GeyserImpl.getInstance().isReloading()) {
+ if (!loadConfig()) {
+ return;
+ }
+ this.geyserLogger.setDebug(geyserConfig.isDebugMode());
+ GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger);
+ } else {
+ 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
+ }
+
// Force-disable query if enabled, or else Geyser won't enable
for (ListenerInfo info : getProxy().getConfig().getListeners()) {
if (info.isQueryEnabled() && info.getQueryPort() == geyserConfig.getBedrock().port()) {
@@ -135,67 +187,31 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap {
}
}
- // 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
- this.awaitStartupCompletion(0);
- }
-
- @SuppressWarnings("unchecked")
- private void awaitStartupCompletion(int tries) {
- // After 20 tries give up waiting. This will happen
- // just after 3 minutes approximately
- if (tries >= 20) {
- this.geyserLogger.warning("BungeeCord plugin startup is taking abnormally long, so Geyser is starting now. " +
- "If all your plugins are loaded properly, this is a bug! " +
- "If not, consider cutting down the amount of plugins on your proxy as it is causing abnormally slow starting times.");
- this.postStartup();
- return;
- }
-
- try {
- Field listenersField = BungeeCord.getInstance().getClass().getDeclaredField("listeners");
- listenersField.setAccessible(true);
-
- Collection listeners = (Collection) listenersField.get(BungeeCord.getInstance());
- if (listeners.isEmpty()) {
- this.getProxy().getScheduler().schedule(this, this::postStartup, tries, TimeUnit.SECONDS);
- } else {
- this.awaitStartupCompletion(++tries);
- }
- } catch (NoSuchFieldException | IllegalAccessException ex) {
- ex.printStackTrace();
- }
- }
-
- private void postStartup() {
GeyserImpl.start();
- this.geyserInjector = new GeyserBungeeInjector(this);
- this.geyserInjector.initializeLocalChannel(this);
-
- this.geyserCommandManager = new GeyserCommandManager(geyser);
- this.geyserCommandManager.init();
-
- 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));
- }
-
if (geyserConfig.isLegacyPingPassthrough()) {
this.geyserBungeePingPassthrough = GeyserLegacyPingPassthrough.init(geyser);
} else {
this.geyserBungeePingPassthrough = new GeyserBungeePingPassthrough(getProxy());
}
+
+ // No need to re-register commands or re-init injector when reloading
+ if (GeyserImpl.getInstance().isReloading()) {
+ return;
+ }
+
+ this.geyserInjector.initializeLocalChannel(this);
}
@Override
- public void onDisable() {
+ public void onGeyserDisable() {
+ if (geyser != null) {
+ geyser.disable();
+ }
+ }
+
+ @Override
+ public void onGeyserShutdown() {
if (geyser != null) {
geyser.shutdown();
}
@@ -204,6 +220,11 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap {
}
}
+ @Override
+ public void onDisable() {
+ this.onGeyserShutdown();
+ }
+
@Override
public GeyserBungeeConfiguration getGeyserConfig() {
return geyserConfig;
@@ -215,8 +236,8 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap {
}
@Override
- public GeyserCommandManager getGeyserCommandManager() {
- return this.geyserCommandManager;
+ public CommandRegistry getCommandRegistry() {
+ return this.commandRegistry;
}
@Override
@@ -245,7 +266,7 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap {
return this.geyserInjector.getServerSocketAddress();
}
- @NotNull
+ @NonNull
@Override
public String getServerBindAddress() {
return findCompatibleListener().map(InetSocketAddress::getHostString).orElse("");
@@ -271,4 +292,20 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap {
.map(info -> (InetSocketAddress) info.getSocketAddress())
.findFirst();
}
+
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ private boolean loadConfig() {
+ try {
+ if (!getDataFolder().exists()) //noinspection ResultOfMethodCallIgnored
+ getDataFolder().mkdir();
+ File configFile = FileUtils.fileOrCopiedFromResource(new File(getDataFolder(), "config.yml"),
+ "config.yml", (x) -> x.replaceAll("generateduuid", UUID.randomUUID().toString()), this);
+ this.geyserConfig = FileUtils.loadConfig(configFile, GeyserBungeeConfiguration.class);
+ } catch (IOException ex) {
+ geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex);
+ ex.printStackTrace();
+ return false;
+ }
+ return true;
+ }
}
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 f65377643..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,18 +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());
@@ -50,7 +54,7 @@ public class BungeeCommandSource implements GeyserCommandSource {
}
@Override
- public void sendMessage(String message) {
+ public void sendMessage(@NonNull String message) {
handle.sendMessage(TextComponent.fromLegacyText(message));
}
@@ -71,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());
}
}
@@ -85,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/fabric/build.gradle.kts b/bootstrap/fabric/build.gradle.kts
deleted file mode 100644
index c260703a5..000000000
--- a/bootstrap/fabric/build.gradle.kts
+++ /dev/null
@@ -1,129 +0,0 @@
-import net.fabricmc.loom.task.RemapJarTask
-
-plugins {
- id("fabric-loom") version "1.0-SNAPSHOT"
- id("com.modrinth.minotaur") version "2.+"
-}
-
-java {
- targetCompatibility = JavaVersion.VERSION_17
- sourceCompatibility = JavaVersion.VERSION_17
-}
-
-dependencies {
- //to change the versions see the gradle.properties file
- minecraft(libs.fabric.minecraft)
- mappings(loom.officialMojangMappings())
- modImplementation(libs.fabric.loader)
-
- // Fabric API. This is technically optional, but you probably want it anyway.
- modImplementation(libs.fabric.api)
-
- // This should be in the libs TOML, but something about modImplementation AND include just doesn't work
- include(modImplementation("me.lucko", "fabric-permissions-api", "0.2-SNAPSHOT"))
-
- // PSA: Some older mods, compiled on Loom 0.2.1, might have outdated Maven POMs.
- // You may need to force-disable transitiveness on them.
-
- api(projects.core)
- shadow(projects.core) {
- exclude(group = "com.google.guava", module = "guava")
- exclude(group = "com.google.code.gson", module = "gson")
- exclude(group = "org.slf4j")
- exclude(group = "com.nukkitx.fastutil")
- exclude(group = "io.netty.incubator")
- }
-}
-
-loom {
- mixin.defaultRefmapName.set("geyser-fabric-refmap.json")
-}
-
-repositories {
- mavenLocal()
- maven("https://repo.opencollab.dev/maven-releases/")
- maven("https://repo.opencollab.dev/maven-snapshots/")
- maven("https://jitpack.io")
- maven("https://oss.sonatype.org/content/repositories/snapshots/")
- maven("https://s01.oss.sonatype.org/content/repositories/snapshots/")
-}
-
-application {
- mainClass.set("org.geysermc.geyser.platform.fabric.GeyserFabricMain")
-}
-
-tasks {
- // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task
- // if it is present.
- // If you remove this task, sources will not be generated.
- sourcesJar {
- archiveClassifier.set("sources")
- from(sourceSets.main.get().allSource)
- }
-
- shadowJar {
- // Mirrors the example fabric project, otherwise tons of dependencies are shaded that shouldn't be
- configurations = listOf(project.configurations.shadow.get())
- // The remapped shadowJar is the final desired Geyser-Fabric.jar
- archiveVersion.set(project.version.toString())
- archiveClassifier.set("shaded")
-
- relocate("org.objectweb.asm", "org.geysermc.relocate.asm")
- relocate("org.yaml", "org.geysermc.relocate.yaml") // https://github.com/CardboardPowered/cardboard/issues/139
- relocate("com.fasterxml.jackson", "org.geysermc.relocate.jackson")
- relocate("net.kyori", "org.geysermc.relocate.kyori")
-
- dependencies {
- // Exclude everything EXCEPT some DNS stuff required for HAProxy
- exclude(dependency("io.netty:netty-transport-classes-epoll:.*"))
- exclude(dependency("io.netty:netty-transport-native-epoll:.*"))
- exclude(dependency("io.netty:netty-transport-native-unix-common:.*"))
- exclude(dependency("io.netty:netty-transport-classes-kqueue:.*"))
- exclude(dependency("io.netty:netty-transport-native-kqueue:.*"))
- exclude(dependency("io.netty:netty-handler:.*"))
- exclude(dependency("io.netty:netty-common:.*"))
- exclude(dependency("io.netty:netty-buffer:.*"))
- exclude(dependency("io.netty:netty-resolver:.*"))
- exclude(dependency("io.netty:netty-transport:.*"))
- exclude(dependency("io.netty:netty-codec:.*"))
- exclude(dependency("io.netty:netty-resolver-dns:.*"))
- exclude(dependency("io.netty:netty-resolver-dns-native-macos:.*"))
- }
- }
-
- remapJar {
- dependsOn(shadowJar)
- inputFile.set(shadowJar.get().archiveFile)
- archiveBaseName.set("Geyser-Fabric")
- archiveVersion.set("")
- archiveClassifier.set("")
- }
-
- register("remapModrinthJar", RemapJarTask::class) {
- dependsOn(shadowJar)
- inputFile.set(shadowJar.get().archiveFile)
- archiveBaseName.set("geyser-fabric")
- archiveVersion.set(project.version.toString() + "+build." + System.getenv("GITHUB_RUN_NUMBER"))
- archiveClassifier.set("")
- }
-}
-
-modrinth {
- token.set(System.getenv("MODRINTH_TOKEN")) // Even though this is the default value, apparently this prevents GitHub Actions caching the token?
- projectId.set("wKkoqHrH")
- versionNumber.set(project.version as String + "-" + System.getenv("GITHUB_RUN_NUMBER"))
- versionType.set("beta")
- changelog.set("A changelog can be found at https://github.com/GeyserMC/Geyser/commits")
-
- syncBodyFrom.set(rootProject.file("README.md").readText())
-
- uploadFile.set(tasks.getByPath("remapModrinthJar"))
- gameVersions.addAll("1.20", "1.20.1")
-
- loaders.add("fabric")
- failSilently.set(true)
-
- dependencies {
- required.project("fabric-api")
- }
-}
diff --git a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricMod.java b/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricMod.java
deleted file mode 100644
index 0bbe73f16..000000000
--- a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricMod.java
+++ /dev/null
@@ -1,297 +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.fabric;
-
-import com.mojang.brigadier.arguments.StringArgumentType;
-import com.mojang.brigadier.builder.LiteralArgumentBuilder;
-import net.fabricmc.api.EnvType;
-import net.fabricmc.api.ModInitializer;
-import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
-import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
-import net.fabricmc.loader.api.FabricLoader;
-import net.fabricmc.loader.api.ModContainer;
-import net.minecraft.commands.CommandSourceStack;
-import net.minecraft.commands.Commands;
-import net.minecraft.server.MinecraftServer;
-import org.apache.logging.log4j.LogManager;
-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.api.util.PlatformType;
-import org.geysermc.geyser.command.GeyserCommand;
-import org.geysermc.geyser.command.GeyserCommandManager;
-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.fabric.command.GeyserFabricCommandExecutor;
-import org.geysermc.geyser.platform.fabric.world.GeyserFabricWorldManager;
-import org.geysermc.geyser.text.GeyserLocale;
-import org.geysermc.geyser.util.FileUtils;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.file.Path;
-import java.util.Map;
-import java.util.Optional;
-import java.util.UUID;
-
-public class GeyserFabricMod implements ModInitializer, GeyserBootstrap {
- private static GeyserFabricMod instance;
-
- private boolean reloading;
-
- private GeyserImpl geyser;
- private ModContainer mod;
- private Path dataFolder;
- private MinecraftServer server;
-
- private GeyserCommandManager geyserCommandManager;
- private GeyserFabricConfiguration geyserConfig;
- private GeyserFabricLogger geyserLogger;
- private IGeyserPingPassthrough geyserPingPassthrough;
- private WorldManager geyserWorldManager;
-
- @Override
- public void onInitialize() {
- instance = this;
- mod = FabricLoader.getInstance().getModContainer("geyser-fabric").orElseThrow();
-
- this.onEnable();
- if (FabricLoader.getInstance().getEnvironmentType() == EnvType.SERVER) {
- // Set as an event so we can get the proper IP and port if needed
- ServerLifecycleEvents.SERVER_STARTED.register(this::startGeyser);
- }
- }
-
- @Override
- public void onEnable() {
- dataFolder = FabricLoader.getInstance().getConfigDir().resolve("Geyser-Fabric");
- if (!dataFolder.toFile().exists()) {
- //noinspection ResultOfMethodCallIgnored
- dataFolder.toFile().mkdir();
- }
-
- // Init dataFolder first as local language overrides call getConfigFolder()
- GeyserLocale.init(this);
-
- try {
- File configFile = FileUtils.fileOrCopiedFromResource(dataFolder.resolve("config.yml").toFile(), "config.yml",
- (x) -> x.replaceAll("generateduuid", UUID.randomUUID().toString()), this);
- this.geyserConfig = FileUtils.loadConfig(configFile, GeyserFabricConfiguration.class);
- } catch (IOException ex) {
- LogManager.getLogger("geyser-fabric").error(GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex);
- ex.printStackTrace();
- return;
- }
-
- this.geyserLogger = new GeyserFabricLogger(geyserConfig.isDebugMode());
-
- GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger);
-
- this.geyser = GeyserImpl.load(PlatformType.FABRIC, this);
-
- if (server == null) {
- // Server has yet to start
- // Register onDisable so players are properly kicked
- ServerLifecycleEvents.SERVER_STOPPING.register((server) -> onDisable());
-
- ServerPlayConnectionEvents.JOIN.register((handler, $, $$) -> GeyserFabricUpdateListener.onPlayReady(handler));
- } else {
- // Server has started and this is a reload
- startGeyser(this.server);
- reloading = false;
- }
- }
-
- /**
- * Initialize core Geyser.
- * A function, as it needs to be called in different places depending on if Geyser is being reloaded or not.
- *
- * @param server The minecraft server.
- */
- public void startGeyser(MinecraftServer server) {
- this.server = server;
-
- GeyserImpl.start();
-
- this.geyserPingPassthrough = GeyserLegacyPingPassthrough.init(geyser);
-
- this.geyserCommandManager = new GeyserCommandManager(geyser);
- this.geyserCommandManager.init();
-
- this.geyserWorldManager = new GeyserFabricWorldManager(server);
-
- // Start command building
- // Set just "geyser" as the help command
- GeyserFabricCommandExecutor helpExecutor = new GeyserFabricCommandExecutor(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()) {
- GeyserFabricCommandExecutor executor = new GeyserFabricCommandExecutor(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 "/"
- GeyserFabricCommandExecutor extensionHelpExecutor = new GeyserFabricCommandExecutor(geyser,
- (GeyserCommand) extensionCommands.get("help"));
- LiteralArgumentBuilder extCmdBuilder = Commands.literal(extensionMapEntry.getKey().description().id()).executes(extensionHelpExecutor);
-
- for (Map.Entry command : extensionCommands.entrySet()) {
- GeyserFabricCommandExecutor executor = new GeyserFabricCommandExecutor(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
- public void onDisable() {
- if (geyser != null) {
- geyser.shutdown();
- geyser = null;
- }
- if (!reloading) {
- this.server = null;
- }
- }
-
- @Override
- public GeyserConfiguration getGeyserConfig() {
- return geyserConfig;
- }
-
- @Override
- public GeyserLogger getGeyserLogger() {
- return geyserLogger;
- }
-
- @Override
- public GeyserCommandManager getGeyserCommandManager() {
- return geyserCommandManager;
- }
-
- @Override
- public IGeyserPingPassthrough getGeyserPingPassthrough() {
- return geyserPingPassthrough;
- }
-
- @Override
- public WorldManager getWorldManager() {
- return geyserWorldManager;
- }
-
- @Override
- public Path getConfigFolder() {
- return dataFolder;
- }
-
- @Override
- public BootstrapDumpInfo getDumpInfo() {
- return new GeyserFabricDumpInfo(server);
- }
-
- @Override
- public String getMinecraftServerVersion() {
- return this.server.getServerVersion();
- }
-
- @NotNull
- @Override
- public String getServerBindAddress() {
- String ip = this.server.getLocalIp();
- return ip != null ? ip : ""; // See issue #3812
- }
-
- @Override
- public int getServerPort() {
- return ((GeyserServerPortGetter) server).geyser$getServerPort();
- }
-
- @Override
- public boolean testFloodgatePluginPresent() {
- Optional floodgate = FabricLoader.getInstance().getModContainer("floodgate");
- if (floodgate.isPresent()) {
- geyserConfig.loadFloodgate(this, floodgate.orElse(null));
- return true;
- }
- return false;
- }
-
- @Nullable
- @Override
- public InputStream getResourceOrNull(String resource) {
- // We need to handle this differently, because Fabric shares the classloader across multiple mods
- Path path = this.mod.findPath(resource).orElse(null);
- if (path == null) {
- return null;
- }
-
- try {
- return path.getFileSystem()
- .provider()
- .newInputStream(path);
- } catch (IOException e) {
- return null;
- }
- }
-
- public void setReloading(boolean reloading) {
- this.reloading = reloading;
- }
-
- public static GeyserFabricMod getInstance() {
- return instance;
- }
-}
diff --git a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/command/GeyserFabricCommandExecutor.java b/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/command/GeyserFabricCommandExecutor.java
deleted file mode 100644
index 8da7c8512..000000000
--- a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/command/GeyserFabricCommandExecutor.java
+++ /dev/null
@@ -1,79 +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.fabric.command;
-
-import com.mojang.brigadier.Command;
-import com.mojang.brigadier.context.CommandContext;
-import me.lucko.fabric.api.permissions.v0.Permissions;
-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.fabric.GeyserFabricMod;
-import org.geysermc.geyser.session.GeyserSession;
-import org.geysermc.geyser.text.ChatColor;
-import org.geysermc.geyser.text.GeyserLocale;
-
-import java.util.Collections;
-
-public class GeyserFabricCommandExecutor extends GeyserCommandExecutor implements Command {
- private final GeyserCommand command;
-
- public GeyserFabricCommandExecutor(GeyserImpl connector, GeyserCommand command) {
- super(connector, Collections.singletonMap(command.name(), command));
- this.command = command;
- }
-
- public boolean testPermission(CommandSourceStack source) {
- return Permissions.check(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 = (CommandSourceStack) context.getSource();
- FabricCommandSender sender = new FabricCommandSender(source);
- GeyserSession session = getGeyserSession(sender);
- if (!testPermission(source)) {
- sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", sender.locale()));
- return 0;
- }
- if (this.command.name().equals("reload")) {
- GeyserFabricMod.getInstance().setReloading(true);
- }
-
- 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/fabric/src/main/java/org/geysermc/geyser/platform/fabric/world/GeyserFabricWorldManager.java b/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/world/GeyserFabricWorldManager.java
deleted file mode 100644
index 923db9b25..000000000
--- a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/world/GeyserFabricWorldManager.java
+++ /dev/null
@@ -1,285 +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.fabric.world;
-
-import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
-import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityInfo;
-import me.lucko.fabric.api.permissions.v0.Permissions;
-import net.minecraft.core.BlockPos;
-import net.minecraft.nbt.*;
-import net.minecraft.server.MinecraftServer;
-import net.minecraft.server.level.ServerPlayer;
-import net.minecraft.world.item.ItemStack;
-import net.minecraft.world.item.WritableBookItem;
-import net.minecraft.world.item.WrittenBookItem;
-import net.minecraft.world.level.block.entity.BannerBlockEntity;
-import net.minecraft.world.level.block.entity.BlockEntity;
-import net.minecraft.world.level.block.entity.LecternBlockEntity;
-import net.minecraft.world.level.chunk.LevelChunk;
-import org.cloudburstmc.math.vector.Vector3i;
-import org.cloudburstmc.nbt.NbtMap;
-import org.cloudburstmc.nbt.NbtMapBuilder;
-import org.cloudburstmc.nbt.NbtType;
-import org.geysermc.erosion.util.LecternUtils;
-import org.geysermc.geyser.level.GeyserWorldManager;
-import org.geysermc.geyser.session.GeyserSession;
-import org.geysermc.geyser.util.BlockEntityUtils;
-
-import javax.annotation.Nonnull;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CompletableFuture;
-
-public class GeyserFabricWorldManager extends GeyserWorldManager {
- private final MinecraftServer server;
-
- public GeyserFabricWorldManager(MinecraftServer server) {
- this.server = server;
- }
-
- @Override
- public boolean shouldExpectLecternHandled(GeyserSession session) {
- return true;
- }
-
- @Override
- public void sendLecternData(GeyserSession session, int x, int z, List blockEntityInfos) {
- server.execute(() -> {
- ServerPlayer player = getPlayer(session);
- if (player == null) {
- return;
- }
-
- LevelChunk chunk = player.level().getChunk(x, z);
- final int chunkBlockX = x << 4;
- final int chunkBlockZ = z << 4;
- for (int i = 0; i < blockEntityInfos.size(); i++) {
- BlockEntityInfo blockEntityInfo = blockEntityInfos.get(i);
- BlockEntity blockEntity = chunk.getBlockEntity(new BlockPos(chunkBlockX + blockEntityInfo.getX(),
- blockEntityInfo.getY(), chunkBlockZ + blockEntityInfo.getZ()));
- sendLecternData(session, blockEntity, true);
- }
- });
- }
-
- @Override
- public void sendLecternData(GeyserSession session, int x, int y, int z) {
- server.execute(() -> {
- ServerPlayer player = getPlayer(session);
- if (player == null) {
- return;
- }
-
- BlockEntity blockEntity = player.level().getBlockEntity(new BlockPos(x, y, z));
- sendLecternData(session, blockEntity, false);
- });
- }
-
- private void sendLecternData(GeyserSession session, BlockEntity blockEntity, boolean isChunkLoad) {
- if (!(blockEntity instanceof LecternBlockEntity lectern)) {
- return;
- }
-
- int x = blockEntity.getBlockPos().getX();
- int y = blockEntity.getBlockPos().getY();
- int z = blockEntity.getBlockPos().getZ();
-
- if (!lectern.hasBook()) {
- if (!isChunkLoad) {
- BlockEntityUtils.updateBlockEntity(session, LecternUtils.getBaseLecternTag(x, y, z, 0).build(), Vector3i.from(x, y, z));
- }
- return;
- }
-
- ItemStack book = lectern.getBook();
- int pageCount = WrittenBookItem.getPageCount(book);
- boolean hasBookPages = pageCount > 0;
- NbtMapBuilder lecternTag = LecternUtils.getBaseLecternTag(x, y, z, hasBookPages ? pageCount : 1);
- lecternTag.putInt("page", lectern.getPage() / 2);
- NbtMapBuilder bookTag = NbtMap.builder()
- .putByte("Count", (byte) book.getCount())
- .putShort("Damage", (short) 0)
- .putString("Name", "minecraft:writable_book");
- List pages = new ArrayList<>(hasBookPages ? pageCount : 1);
- if (hasBookPages && WritableBookItem.makeSureTagIsValid(book.getTag())) {
- ListTag listTag = book.getTag().getList("pages", 8);
-
- for (int i = 0; i < listTag.size(); i++) {
- String page = listTag.getString(i);
- NbtMapBuilder pageBuilder = NbtMap.builder()
- .putString("photoname", "")
- .putString("text", page);
- pages.add(pageBuilder.build());
- }
- } else {
- // Empty page
- NbtMapBuilder pageBuilder = NbtMap.builder()
- .putString("photoname", "")
- .putString("text", "");
- pages.add(pageBuilder.build());
- }
-
- bookTag.putCompound("tag", NbtMap.builder().putList("pages", NbtType.COMPOUND, pages).build());
- lecternTag.putCompound("book", bookTag.build());
- NbtMap blockEntityTag = lecternTag.build();
- BlockEntityUtils.updateBlockEntity(session, blockEntityTag, Vector3i.from(x, y, z));
- }
-
- @Override
- public boolean hasPermission(GeyserSession session, String permission) {
- ServerPlayer player = getPlayer(session);
- return Permissions.check(player, permission);
- }
-
- @Override
- public GameMode getDefaultGameMode(GeyserSession session) {
- return GameMode.byId(server.getDefaultGameType().getId());
- }
-
- @Nonnull
- @Override
- public CompletableFuture getPickItemNbt(GeyserSession session, int x, int y, int z, boolean addNbtData) {
- CompletableFuture future = new CompletableFuture<>();
- server.execute(() -> {
- ServerPlayer player = getPlayer(session);
- if (player == null) {
- future.complete(null);
- return;
- }
-
- BlockPos pos = new BlockPos(x, y, z);
- // Don't create a new block entity if invalid
- BlockEntity blockEntity = player.level().getChunkAt(pos).getBlockEntity(pos);
- if (blockEntity instanceof BannerBlockEntity banner) {
- // Potentially exposes other NBT data? But we need to get the NBT data for the banner patterns *and*
- // the banner might have a custom name, both of which a Java client knows and caches
- ItemStack itemStack = banner.getItem();
- var tag = OpenNbtTagVisitor.convert("", itemStack.getOrCreateTag());
-
- future.complete(tag);
- return;
- }
- future.complete(null);
- });
- return future;
- }
-
- private ServerPlayer getPlayer(GeyserSession session) {
- return server.getPlayerList().getPlayer(session.getPlayerEntity().getUuid());
- }
-
- // Future considerations: option to clone; would affect arrays
- private static class OpenNbtTagVisitor implements TagVisitor {
- private String currentKey;
- private final com.github.steveice10.opennbt.tag.builtin.CompoundTag root;
- private com.github.steveice10.opennbt.tag.builtin.Tag currentTag;
-
- OpenNbtTagVisitor(String key) {
- root = new com.github.steveice10.opennbt.tag.builtin.CompoundTag(key);
- }
-
- @Override
- public void visitString(StringTag stringTag) {
- currentTag = new com.github.steveice10.opennbt.tag.builtin.StringTag(currentKey, stringTag.getAsString());
- }
-
- @Override
- public void visitByte(ByteTag byteTag) {
- currentTag = new com.github.steveice10.opennbt.tag.builtin.ByteTag(currentKey, byteTag.getAsByte());
- }
-
- @Override
- public void visitShort(ShortTag shortTag) {
- currentTag = new com.github.steveice10.opennbt.tag.builtin.ShortTag(currentKey, shortTag.getAsShort());
- }
-
- @Override
- public void visitInt(IntTag intTag) {
- currentTag = new com.github.steveice10.opennbt.tag.builtin.IntTag(currentKey, intTag.getAsInt());
- }
-
- @Override
- public void visitLong(LongTag longTag) {
- currentTag = new com.github.steveice10.opennbt.tag.builtin.LongTag(currentKey, longTag.getAsLong());
- }
-
- @Override
- public void visitFloat(FloatTag floatTag) {
- currentTag = new com.github.steveice10.opennbt.tag.builtin.FloatTag(currentKey, floatTag.getAsFloat());
- }
-
- @Override
- public void visitDouble(DoubleTag doubleTag) {
- currentTag = new com.github.steveice10.opennbt.tag.builtin.DoubleTag(currentKey, doubleTag.getAsDouble());
- }
-
- @Override
- public void visitByteArray(ByteArrayTag byteArrayTag) {
- currentTag = new com.github.steveice10.opennbt.tag.builtin.ByteArrayTag(currentKey, byteArrayTag.getAsByteArray());
- }
-
- @Override
- public void visitIntArray(IntArrayTag intArrayTag) {
- currentTag = new com.github.steveice10.opennbt.tag.builtin.IntArrayTag(currentKey, intArrayTag.getAsIntArray());
- }
-
- @Override
- public void visitLongArray(LongArrayTag longArrayTag) {
- currentTag = new com.github.steveice10.opennbt.tag.builtin.LongArrayTag(currentKey, longArrayTag.getAsLongArray());
- }
-
- @Override
- public void visitList(ListTag listTag) {
- var newList = new com.github.steveice10.opennbt.tag.builtin.ListTag(currentKey);
- for (Tag tag : listTag) {
- currentKey = "";
- tag.accept(this);
- newList.add(currentTag);
- }
- currentTag = newList;
- }
-
- @Override
- public void visitCompound(CompoundTag compoundTag) {
- currentTag = convert(currentKey, compoundTag);
- }
-
- private static com.github.steveice10.opennbt.tag.builtin.CompoundTag convert(String name, CompoundTag compoundTag) {
- OpenNbtTagVisitor visitor = new OpenNbtTagVisitor(name);
- for (String key : compoundTag.getAllKeys()) {
- visitor.currentKey = key;
- Tag tag = compoundTag.get(key);
- tag.accept(visitor);
- visitor.root.put(visitor.currentTag);
- }
- return visitor.root;
- }
-
- @Override
- public void visitEnd(EndTag endTag) {
- }
- }
-}
diff --git a/bootstrap/fabric/src/main/resources/geyser-fabric.mixins.json b/bootstrap/fabric/src/main/resources/geyser-fabric.mixins.json
deleted file mode 100644
index aeb051809..000000000
--- a/bootstrap/fabric/src/main/resources/geyser-fabric.mixins.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "required": true,
- "package": "org.geysermc.geyser.platform.fabric.mixin",
- "compatibilityLevel": "JAVA_16",
- "refmap": "geyser-fabric-refmap.json",
- "client": [
- "client.IntegratedServerMixin"
- ],
- "server": [
- "server.MinecraftDedicatedServerMixin"
- ],
- "injectors": {
- "defaultRequire": 1
- }
-}
diff --git a/bootstrap/mod/build.gradle.kts b/bootstrap/mod/build.gradle.kts
new file mode 100644
index 000000000..32224d00b
--- /dev/null
+++ b/bootstrap/mod/build.gradle.kts
@@ -0,0 +1,22 @@
+architectury {
+ common("neoforge", "fabric")
+}
+
+loom {
+ mixin.defaultRefmapName.set("geyser-refmap.json")
+}
+
+afterEvaluate {
+ // We don't need these
+ tasks.named("remapModrinthJar").configure {
+ enabled = false
+ }
+}
+
+dependencies {
+ api(projects.core)
+ compileOnly(libs.mixin)
+
+ // Only here to suppress "unknown enum constant EnvType.CLIENT" warnings. DO NOT USE!
+ compileOnly(libs.fabric.loader)
+}
\ No newline at end of file
diff --git a/bootstrap/mod/fabric/build.gradle.kts b/bootstrap/mod/fabric/build.gradle.kts
new file mode 100644
index 000000000..4193a7abe
--- /dev/null
+++ b/bootstrap/mod/fabric/build.gradle.kts
@@ -0,0 +1,65 @@
+architectury {
+ platformSetupLoomIde()
+ fabric()
+}
+
+val includeTransitive: Configuration = configurations.getByName("includeTransitive")
+
+dependencies {
+ modImplementation(libs.fabric.loader)
+ modApi(libs.fabric.api)
+
+ api(project(":mod", configuration = "namedElements"))
+ shadow(project(path = ":mod", configuration = "transformProductionFabric")) {
+ isTransitive = false
+ }
+ shadow(projects.core) { isTransitive = false }
+ includeTransitive(projects.core)
+
+ // These are NOT transitively included, and instead shadowed + relocated.
+ // Avoids fabric complaining about non-SemVer versioning
+ shadow(libs.protocol.connection) { isTransitive = false }
+ shadow(libs.protocol.common) { isTransitive = false }
+ shadow(libs.protocol.codec) { isTransitive = false }
+ shadow(libs.mcauthlib) { 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
+ shadow(libs.erosion.common) { isTransitive = false }
+
+ // Let's shade in our own api/common module
+ shadow(projects.api) { isTransitive = false }
+ shadow(projects.common) { isTransitive = false }
+
+ modImplementation(libs.cloud.fabric)
+ include(libs.cloud.fabric)
+}
+
+tasks.withType {
+ manifest.attributes["Main-Class"] = "org.geysermc.geyser.platform.fabric.GeyserFabricMain"
+}
+
+relocate("org.cloudburstmc.netty")
+relocate("org.cloudburstmc.protocol")
+relocate("com.github.steveice10.mc.auth")
+
+tasks {
+ remapJar {
+ archiveBaseName.set("Geyser-Fabric")
+ }
+
+ remapModrinthJar {
+ archiveBaseName.set("geyser-fabric")
+ }
+}
+
+modrinth {
+ loaders.add("fabric")
+ uploadFile.set(tasks.getByPath("remapModrinthJar"))
+ dependencies {
+ required.project("fabric-api")
+ }
+}
\ No newline at end of file
diff --git a/bootstrap/mod/fabric/gradle.properties b/bootstrap/mod/fabric/gradle.properties
new file mode 100644
index 000000000..90ee7a259
--- /dev/null
+++ b/bootstrap/mod/fabric/gradle.properties
@@ -0,0 +1 @@
+loom.platform=fabric
\ 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
new file mode 100644
index 000000000..149246d59
--- /dev/null
+++ b/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricBootstrap.java
@@ -0,0 +1,98 @@
+/*
+ * 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.fabric;
+
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.ModInitializer;
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
+import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
+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.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 {
+
+ public GeyserFabricBootstrap() {
+ super(new GeyserFabricPlatform());
+ }
+
+ @Override
+ public void onInitialize() {
+ if (isServer()) {
+ // Set as an event, so we can get the proper IP and port if needed
+ ServerLifecycleEvents.SERVER_STARTED.register((server) -> {
+ this.setServer(server);
+ onGeyserEnable();
+ });
+ } else {
+ ClientLifecycleEvents.CLIENT_STOPPING.register(($)-> {
+ onGeyserShutdown();
+ });
+ }
+
+ // These are only registered once
+ ServerLifecycleEvents.SERVER_STOPPING.register((server) -> {
+ if (isServer()) {
+ onGeyserShutdown();
+ } else {
+ onGeyserDisable();
+ }
+ });
+
+ 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);
+ }
+}
diff --git a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricDumpInfo.java b/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricDumpInfo.java
similarity index 90%
rename from bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricDumpInfo.java
rename to bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricDumpInfo.java
index ee986ee62..75da9125f 100644
--- a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricDumpInfo.java
+++ b/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricDumpInfo.java
@@ -43,21 +43,27 @@ import java.util.stream.Collectors;
@Getter
public class GeyserFabricDumpInfo extends BootstrapDumpInfo {
- private String platformVersion = null;
+ private final String platformName;
+ private String platformVersion;
+ private final String minecraftVersion;
private final EnvType environmentType;
@AsteriskSerializer.Asterisk(isIp = true)
private final String serverIP;
private final int serverPort;
+ private final boolean onlineMode;
private final List mods;
public GeyserFabricDumpInfo(MinecraftServer server) {
+ this.platformName = server.getServerModName();
FabricLoader.getInstance().getModContainer("fabricloader").ifPresent(mod ->
this.platformVersion = mod.getMetadata().getVersion().getFriendlyString());
+ this.minecraftVersion = server.getServerVersion();
this.environmentType = FabricLoader.getInstance().getEnvironmentType();
this.serverIP = server.getLocalIp() == null ? "unknown" : server.getLocalIp();
this.serverPort = server.getPort();
+ this.onlineMode = server.usesAuthentication();
this.mods = new ArrayList<>();
for (ModContainer mod : FabricLoader.getInstance().getAllMods()) {
diff --git a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricMain.java b/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricMain.java
similarity index 100%
rename from bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricMain.java
rename to bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricMain.java
diff --git a/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricPlatform.java b/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricPlatform.java
new file mode 100644
index 000000000..4631ab493
--- /dev/null
+++ b/bootstrap/mod/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricPlatform.java
@@ -0,0 +1,99 @@
+/*
+ * 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.fabric;
+
+import net.fabricmc.loader.api.FabricLoader;
+import net.fabricmc.loader.api.ModContainer;
+import net.minecraft.server.MinecraftServer;
+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.dump.BootstrapDumpInfo;
+import org.geysermc.geyser.platform.mod.GeyserModBootstrap;
+import org.geysermc.geyser.platform.mod.platform.GeyserModPlatform;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.util.Optional;
+
+public class GeyserFabricPlatform implements GeyserModPlatform {
+
+ private final ModContainer mod;
+
+ public GeyserFabricPlatform() {
+ this.mod = FabricLoader.getInstance().getModContainer("geyser-fabric").orElseThrow();
+ }
+
+ @Override
+ public @NonNull PlatformType platformType() {
+ return PlatformType.FABRIC;
+ }
+
+ @Override
+ public @NonNull String configPath() {
+ return "Geyser-Fabric";
+ }
+
+ @Override
+ public @NonNull Path dataFolder(@NonNull String modId) {
+ return FabricLoader.getInstance().getConfigDir().resolve(modId);
+ }
+
+ @Override
+ public @NonNull BootstrapDumpInfo dumpInfo(@NonNull MinecraftServer server) {
+ return new GeyserFabricDumpInfo(server);
+ }
+
+ @Override
+ public boolean testFloodgatePluginPresent(@NonNull GeyserModBootstrap bootstrap) {
+ Optional floodgate = FabricLoader.getInstance().getModContainer("floodgate");
+ if (floodgate.isPresent()) {
+ Path floodgateDataFolder = FabricLoader.getInstance().getConfigDir().resolve("floodgate");
+ bootstrap.getGeyserConfig().loadFloodgate(bootstrap, floodgateDataFolder);
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public @Nullable InputStream resolveResource(@NonNull String resource) {
+ // We need to handle this differently, because Fabric shares the classloader across multiple mods
+ Path path = this.mod.findPath(resource).orElse(null);
+ if (path == null) {
+ return null;
+ }
+
+ try {
+ return path.getFileSystem()
+ .provider()
+ .newInputStream(path);
+ } catch (IOException e) {
+ return null;
+ }
+ }
+}
diff --git a/bootstrap/fabric/src/main/resources/fabric.mod.json b/bootstrap/mod/fabric/src/main/resources/fabric.mod.json
similarity index 61%
rename from bootstrap/fabric/src/main/resources/fabric.mod.json
rename to bootstrap/mod/fabric/src/main/resources/fabric.mod.json
index 4c442017a..262f9833a 100644
--- a/bootstrap/fabric/src/main/resources/fabric.mod.json
+++ b/bootstrap/mod/fabric/src/main/resources/fabric.mod.json
@@ -9,23 +9,22 @@
],
"contact": {
"website": "${url}",
- "repo": "https://github.com/GeyserMC/Geyser-Fabric"
+ "repo": "https://github.com/GeyserMC/Geyser"
},
"license": "MIT",
- "icon": "assets/geyser-fabric/icon.png",
+ "icon": "assets/geyser/icon.png",
"environment": "*",
"entrypoints": {
"main": [
- "org.geysermc.geyser.platform.fabric.GeyserFabricMod"
+ "org.geysermc.geyser.platform.fabric.GeyserFabricBootstrap"
]
},
"mixins": [
- "geyser-fabric.mixins.json"
+ "geyser.mixins.json"
],
"depends": {
- "fabricloader": ">=0.14.21",
+ "fabricloader": ">=0.15.11",
"fabric": "*",
- "minecraft": ">=1.20",
- "fabric-permissions-api-v0": "*"
+ "minecraft": ">=1.21"
}
}
diff --git a/bootstrap/mod/neoforge/build.gradle.kts b/bootstrap/mod/neoforge/build.gradle.kts
new file mode 100644
index 000000000..bc8082265
--- /dev/null
+++ b/bootstrap/mod/neoforge/build.gradle.kts
@@ -0,0 +1,58 @@
+// 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()
+}
+
+val includeTransitive: Configuration = configurations.getByName("includeTransitive")
+
+dependencies {
+ // See https://github.com/google/guava/issues/6618
+ modules {
+ module("com.google.guava:listenablefuture") {
+ replacedBy("com.google.guava:guava", "listenablefuture is part of guava")
+ }
+ }
+
+ neoForge(libs.neoforge.minecraft)
+
+ api(project(":mod", configuration = "namedElements"))
+ shadow(project(path = ":mod", configuration = "transformProductionNeoForge")) {
+ isTransitive = false
+ }
+ shadow(projects.core) { isTransitive = false }
+
+ // Let's shade in our own api
+ shadow(projects.api) { isTransitive = false }
+
+ // cannot be shaded, since neoforge will complain if floodgate-neoforge tries to provide this
+ include(projects.common)
+
+ // Include all transitive deps of core via JiJ
+ includeTransitive(projects.core)
+
+ modImplementation(libs.cloud.neoforge)
+ include(libs.cloud.neoforge)
+}
+
+tasks.withType {
+ manifest.attributes["Main-Class"] = "org.geysermc.geyser.platform.neoforge.GeyserNeoForgeMain"
+}
+
+tasks {
+ remapJar {
+ archiveBaseName.set("Geyser-NeoForge")
+ }
+
+ remapModrinthJar {
+ archiveBaseName.set("geyser-neoforge")
+ }
+}
+
+modrinth {
+ loaders.add("neoforge")
+ uploadFile.set(tasks.getByPath("remapModrinthJar"))
+}
\ No newline at end of file
diff --git a/bootstrap/mod/neoforge/gradle.properties b/bootstrap/mod/neoforge/gradle.properties
new file mode 100644
index 000000000..2914393db
--- /dev/null
+++ b/bootstrap/mod/neoforge/gradle.properties
@@ -0,0 +1 @@
+loom.platform=neoforge
\ 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
new file mode 100644
index 000000000..7d3b9dc5f
--- /dev/null
+++ b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeBootstrap.java
@@ -0,0 +1,127 @@
+/*
+ * 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.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;
+import net.neoforged.neoforge.common.NeoForge;
+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 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 {
+
+ public GeyserNeoForgeBootstrap(ModContainer container) {
+ super(new GeyserNeoForgePlatform(container));
+
+ if (isServer()) {
+ // Set as an event so we can get the proper IP and port if needed
+ NeoForge.EVENT_BUS.addListener(this::onServerStarted);
+ } else {
+ NeoForge.EVENT_BUS.addListener(this::onClientStopping);
+ }
+
+ NeoForge.EVENT_BUS.addListener(this::onServerStopping);
+ NeoForge.EVENT_BUS.addListener(this::onPlayerJoin);
+
+ 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);
+ NeoForge.EVENT_BUS.addListener(EventPriority.LOWEST, registry::onPermissionGatherForUndefined);
+ }
+
+ private void onServerStarted(ServerStartedEvent event) {
+ this.setServer(event.getServer());
+ this.onGeyserEnable();
+ }
+
+ private void onServerStopping(ServerStoppingEvent event) {
+ if (isServer()) {
+ this.onGeyserShutdown();
+ } else {
+ this.onGeyserDisable();
+ }
+ }
+
+ private void onClientStopping(GameShuttingDownEvent ignored) {
+ this.onGeyserShutdown();
+ }
+
+ private void onPlayerJoin(PlayerEvent.PlayerLoggedInEvent event) {
+ GeyserModUpdateListener.onPlayReady(event.getEntity());
+ }
+
+ @Override
+ public boolean isServer() {
+ return FMLLoader.getDist().isDedicatedServer();
+ }
+
+ private void onPermissionGather(PermissionGatherEvent.Nodes event) {
+ getGeyser().eventBus().fire(
+ (GeyserRegisterPermissionsEvent) (permission, defaultValue) -> {
+ Objects.requireNonNull(permission, "permission");
+ Objects.requireNonNull(defaultValue, "permission default for " + permission);
+
+ 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/GeyserNeoForgeDumpInfo.java b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeDumpInfo.java
new file mode 100644
index 000000000..623f68d3a
--- /dev/null
+++ b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeDumpInfo.java
@@ -0,0 +1,84 @@
+/*
+ * 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 lombok.AllArgsConstructor;
+import lombok.Getter;
+import net.minecraft.server.MinecraftServer;
+import net.neoforged.api.distmarker.Dist;
+import net.neoforged.fml.ModList;
+import net.neoforged.fml.loading.FMLLoader;
+import net.neoforged.neoforgespi.language.IModInfo;
+import org.geysermc.geyser.dump.BootstrapDumpInfo;
+import org.geysermc.geyser.text.AsteriskSerializer;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+@Getter
+public class GeyserNeoForgeDumpInfo extends BootstrapDumpInfo {
+
+ private final String platformName;
+ private final String platformVersion;
+ private final String minecraftVersion;
+ private final Dist dist;
+
+ @AsteriskSerializer.Asterisk(isIp = true)
+ private final String serverIP;
+ private final int serverPort;
+ private final boolean onlineMode;
+ private final List mods;
+
+ public GeyserNeoForgeDumpInfo(MinecraftServer server) {
+ this.platformName = FMLLoader.launcherHandlerName();
+ this.platformVersion = FMLLoader.versionInfo().neoForgeVersion();
+ this.minecraftVersion = FMLLoader.versionInfo().mcVersion();
+ this.dist = FMLLoader.getDist();
+ this.serverIP = server.getLocalIp() == null ? "unknown" : server.getLocalIp();
+ this.serverPort = server.getPort();
+ this.onlineMode = server.usesAuthentication();
+ this.mods = new ArrayList<>();
+
+ for (IModInfo mod : ModList.get().getMods()) {
+ this.mods.add(new ModInfo(
+ ModList.get().isLoaded(mod.getModId()),
+ mod.getModId(),
+ mod.getVersion().toString(),
+ mod.getModURL().map(URL::toString).orElse("")
+ ));
+ }
+ }
+
+ @Getter
+ @AllArgsConstructor
+ public static class ModInfo {
+ public boolean enabled;
+ public String name;
+ public String version;
+ public String url;
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/java/org/geysermc/geyser/translator/collision/SpawnerCollision.java b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeMain.java
similarity index 71%
rename from core/src/main/java/org/geysermc/geyser/translator/collision/SpawnerCollision.java
rename to bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeMain.java
index 7d4dfedc2..70bac2a40 100644
--- a/core/src/main/java/org/geysermc/geyser/translator/collision/SpawnerCollision.java
+++ b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgeMain.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
@@ -23,16 +23,23 @@
* @link https://github.com/GeyserMC/Geyser
*/
-package org.geysermc.geyser.translator.collision;
+package org.geysermc.geyser.platform.neoforge;
-import lombok.EqualsAndHashCode;
+import org.geysermc.geyser.GeyserMain;
-@EqualsAndHashCode(callSuper = true)
-@CollisionRemapper(regex = "^spawner$")
-public class SpawnerCollision extends SolidCollision {
- public SpawnerCollision(String params) {
- super(params);
- // Increase pushAwayTolerance to work around https://bugs.mojang.com/browse/MCPE-41996
- pushAwayTolerance = 0.0002;
+public class GeyserNeoForgeMain extends GeyserMain {
+
+ public static void main(String[] args) {
+ new GeyserNeoForgeMain().displayMessage();
+ }
+
+ @Override
+ public String getPluginType() {
+ return "NeoForge";
+ }
+
+ @Override
+ public String getPluginFolder() {
+ return "mods";
}
}
diff --git a/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgePlatform.java b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgePlatform.java
new file mode 100644
index 000000000..41562baf3
--- /dev/null
+++ b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/GeyserNeoForgePlatform.java
@@ -0,0 +1,91 @@
+/*
+ * 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.server.MinecraftServer;
+import net.neoforged.fml.ModContainer;
+import net.neoforged.fml.ModList;
+import net.neoforged.fml.loading.FMLPaths;
+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.dump.BootstrapDumpInfo;
+import org.geysermc.geyser.platform.mod.GeyserModBootstrap;
+import org.geysermc.geyser.platform.mod.platform.GeyserModPlatform;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class GeyserNeoForgePlatform implements GeyserModPlatform {
+
+ private final ModContainer container;
+
+ public GeyserNeoForgePlatform(ModContainer container) {
+ this.container = container;
+ }
+
+ @Override
+ public @NonNull PlatformType platformType() {
+ return PlatformType.NEOFORGE;
+ }
+
+ @Override
+ public @NonNull String configPath() {
+ return "Geyser-NeoForge";
+ }
+
+ @Override
+ public @NonNull Path dataFolder(@NonNull String modId) {
+ return FMLPaths.CONFIGDIR.get().resolve(modId);
+ }
+
+ @Override
+ public @NonNull BootstrapDumpInfo dumpInfo(@NonNull MinecraftServer server) {
+ return new GeyserNeoForgeDumpInfo(server);
+ }
+
+ @Override
+ public boolean testFloodgatePluginPresent(@NonNull GeyserModBootstrap bootstrap) {
+ if (ModList.get().isLoaded("floodgate")) {
+ Path floodgateDataFolder = FMLPaths.CONFIGDIR.get().resolve("floodgate");
+ bootstrap.getGeyserConfig().loadFloodgate(bootstrap, floodgateDataFolder);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public @Nullable InputStream resolveResource(@NonNull String resource) {
+ try {
+ Path path = container.getModInfo().getOwningFile().getFile().findResource(resource);
+ return Files.newInputStream(path);
+ } catch (IOException e) {
+ return null;
+ }
+ }
+}
diff --git a/licenseheader.txt b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/ModConstants.java
similarity index 85%
rename from licenseheader.txt
rename to bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/ModConstants.java
index 9bfe117f9..aa72bb2a0 100644
--- a/licenseheader.txt
+++ b/bootstrap/mod/neoforge/src/main/java/org/geysermc/geyser/platform/neoforge/ModConstants.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
@@ -21,4 +21,10 @@
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
- */
\ No newline at end of file
+ */
+
+package org.geysermc.geyser.platform.neoforge;
+
+public class ModConstants {
+ public static final String MOD_ID = "geyser_neoforge";
+}
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
new file mode 100644
index 000000000..56b7d68e1
--- /dev/null
+++ b/bootstrap/mod/neoforge/src/main/resources/META-INF/neoforge.mods.toml
@@ -0,0 +1,27 @@
+modLoader="javafml"
+loaderVersion="[1,)"
+license="MIT"
+[[mods]]
+modId="geyser_neoforge"
+version="${version}"
+displayName="Geyser"
+displayURL="https://geysermc.org/"
+logoFile= "../assets/geyser/icon.png"
+authors="GeyserMC"
+description="${description}"
+[[mixins]]
+config = "geyser.mixins.json"
+[[mixins]]
+config = "geyser_neoforge.mixins.json"
+[[dependencies.geyser_neoforge]]
+ modId="neoforge"
+ type="required"
+ versionRange="[21.0.0-beta,)"
+ ordering="NONE"
+ side="BOTH"
+[[dependencies.geyser_neoforge]]
+ modId="minecraft"
+ type="required"
+ versionRange="[1.21,)"
+ ordering="NONE"
+ side="BOTH"
\ No newline at end of file
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/GeyserChannelGetter.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserChannelGetter.java
new file mode 100644
index 000000000..8dc0026bf
--- /dev/null
+++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserChannelGetter.java
@@ -0,0 +1,43 @@
+/*
+ * 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.mod;
+
+import io.netty.channel.ChannelFuture;
+
+import java.util.List;
+
+/**
+ * Represents a getter to the server channels in the connection listener class.
+ */
+public interface GeyserChannelGetter {
+
+ /**
+ * Returns the channels.
+ *
+ * @return The channels.
+ */
+ List geyser$getChannels();
+}
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
new file mode 100644
index 000000000..f11b5fbd6
--- /dev/null
+++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModBootstrap.java
@@ -0,0 +1,236 @@
+/*
+ * 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;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+import net.minecraft.server.MinecraftServer;
+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.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.platform.GeyserModPlatform;
+import org.geysermc.geyser.platform.mod.world.GeyserModWorldManager;
+import org.geysermc.geyser.text.GeyserLocale;
+import org.geysermc.geyser.util.FileUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.SocketAddress;
+import java.nio.file.Path;
+import java.util.UUID;
+
+@RequiredArgsConstructor
+public abstract class GeyserModBootstrap implements GeyserBootstrap {
+
+ @Getter
+ private static GeyserModBootstrap instance;
+
+ private final GeyserModPlatform platform;
+
+ @Getter
+ private GeyserImpl geyser;
+ private Path dataFolder;
+
+ @Setter @Getter
+ private MinecraftServer server;
+
+ @Setter
+ private CommandRegistry commandRegistry;
+ private GeyserModConfiguration geyserConfig;
+ private GeyserModInjector geyserInjector;
+ private final GeyserModLogger geyserLogger = new GeyserModLogger();
+ private IGeyserPingPassthrough geyserPingPassthrough;
+ private WorldManager geyserWorldManager;
+
+ @Override
+ public void onGeyserInitialize() {
+ instance = this;
+ dataFolder = this.platform.dataFolder(this.platform.configPath());
+ GeyserLocale.init(this);
+ if (!loadConfig()) {
+ return;
+ }
+ this.geyserLogger.setDebug(geyserConfig.isDebugMode());
+ GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger);
+ this.geyser = GeyserImpl.load(this.platform.platformType(), this);
+ }
+
+ public void onGeyserEnable() {
+ if (GeyserImpl.getInstance().isReloading()) {
+ if (!loadConfig()) {
+ return;
+ }
+ this.geyserLogger.setDebug(geyserConfig.isDebugMode());
+ GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger);
+ }
+
+ GeyserImpl.start();
+
+ if (geyserConfig.isLegacyPingPassthrough()) {
+ this.geyserPingPassthrough = GeyserLegacyPingPassthrough.init(geyser);
+ } else {
+ this.geyserPingPassthrough = new ModPingPassthrough(server, geyserLogger);
+ }
+
+ // No need to re-register commands, or try to re-inject
+ if (GeyserImpl.getInstance().isReloading()) {
+ return;
+ }
+
+ this.geyserWorldManager = new GeyserModWorldManager(server);
+
+ // We want to do this late in the server startup process to allow other mods
+ // To do their job injecting, then connect into *that*
+ this.geyserInjector = new GeyserModInjector(server, this.platform);
+ if (isServer()) {
+ this.geyserInjector.initializeLocalChannel(this);
+ }
+ }
+
+ @Override
+ public void onGeyserDisable() {
+ if (geyser != null) {
+ geyser.disable();
+ }
+ }
+
+ @Override
+ public void onGeyserShutdown() {
+ if (geyser != null) {
+ geyser.shutdown();
+ geyser = null;
+ }
+ if (geyserInjector != null) {
+ geyserInjector.shutdown();
+ this.server = null;
+ }
+ }
+
+ @Override
+ public GeyserModConfiguration getGeyserConfig() {
+ return geyserConfig;
+ }
+
+ @Override
+ public GeyserLogger getGeyserLogger() {
+ return geyserLogger;
+ }
+
+ @Override
+ public CommandRegistry getCommandRegistry() {
+ return commandRegistry;
+ }
+
+ @Override
+ public IGeyserPingPassthrough getGeyserPingPassthrough() {
+ return geyserPingPassthrough;
+ }
+
+ @Override
+ public WorldManager getWorldManager() {
+ return geyserWorldManager;
+ }
+
+ @Override
+ public Path getConfigFolder() {
+ return dataFolder;
+ }
+
+ @Override
+ public BootstrapDumpInfo getDumpInfo() {
+ return this.platform.dumpInfo(this.server);
+ }
+
+ @Override
+ public String getMinecraftServerVersion() {
+ return this.server.getServerVersion();
+ }
+
+ @SuppressWarnings("ConstantConditions") // Certain IDEA installations think that ip cannot be null
+ @NonNull
+ @Override
+ public String getServerBindAddress() {
+ String ip = this.server.getLocalIp();
+ return ip != null ? ip : ""; // See issue #3812
+ }
+
+ @Override
+ public SocketAddress getSocketAddress() {
+ return this.geyserInjector.getServerSocketAddress();
+ }
+
+ @Override
+ public int getServerPort() {
+ if (isServer()) {
+ return ((GeyserServerPortGetter) server).geyser$getServerPort();
+ } else {
+ // Set in the IntegratedServerMixin
+ return geyserConfig.getRemote().port();
+ }
+ }
+
+ public abstract boolean isServer();
+
+ @Override
+ public boolean testFloodgatePluginPresent() {
+ return this.platform.testFloodgatePluginPresent(this);
+ }
+
+ @Nullable
+ @Override
+ public InputStream getResourceOrNull(String resource) {
+ return this.platform.resolveResource(resource);
+ }
+
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ private boolean loadConfig() {
+ try {
+ if (!dataFolder.toFile().exists()) {
+ //noinspection ResultOfMethodCallIgnored
+ dataFolder.toFile().mkdir();
+ }
+
+ File configFile = FileUtils.fileOrCopiedFromResource(dataFolder.resolve("config.yml").toFile(), "config.yml",
+ (x) -> x.replaceAll("generateduuid", UUID.randomUUID().toString()), this);
+ this.geyserConfig = FileUtils.loadConfig(configFile, GeyserModConfiguration.class);
+ return true;
+ } catch (IOException ex) {
+ geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex);
+ ex.printStackTrace();
+ return false;
+ }
+ }
+}
diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModCompressionDisabler.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModCompressionDisabler.java
new file mode 100644
index 000000000..631a21510
--- /dev/null
+++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModCompressionDisabler.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2019-2021 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;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelOutboundHandlerAdapter;
+import io.netty.channel.ChannelPromise;
+import net.minecraft.network.protocol.login.ClientboundGameProfilePacket;
+import net.minecraft.network.protocol.login.ClientboundLoginCompressionPacket;
+
+/**
+ * Disables the compression packet (and the compression handlers from being added to the pipeline) for Geyser clients
+ * that won't be receiving the data over the network.
+ *
+ * As of 1.8 - 1.17.1, compression is enabled in the Netty pipeline by adding a listener after a packet is written.
+ * If we simply "cancel" or don't forward the packet, then the listener is never called.
+ */
+public class GeyserModCompressionDisabler extends ChannelOutboundHandlerAdapter {
+
+ @Override
+ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
+ Class> msgClass = msg.getClass();
+ // Don't let any compression packet get through
+ if (!ClientboundLoginCompressionPacket.class.isAssignableFrom(msgClass)) {
+ if (ClientboundGameProfilePacket.class.isAssignableFrom(msgClass)) {
+
+ // We're past the point that a compression packet can be sent, so we can safely yeet ourselves away
+ ctx.channel().pipeline().remove(this);
+ }
+ super.write(ctx, msg, promise);
+ }
+ }
+}
diff --git a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricConfiguration.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModConfiguration.java
similarity index 80%
rename from bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricConfiguration.java
rename to bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModConfiguration.java
index f557d16c0..a24380bd6 100644
--- a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricConfiguration.java
+++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModConfiguration.java
@@ -23,23 +23,20 @@
* @link https://github.com/GeyserMC/Geyser
*/
-package org.geysermc.geyser.platform.fabric;
+package org.geysermc.geyser.platform.mod;
import com.fasterxml.jackson.annotation.JsonIgnore;
-import net.fabricmc.loader.api.FabricLoader;
-import net.fabricmc.loader.api.ModContainer;
import org.geysermc.geyser.FloodgateKeyLoader;
import org.geysermc.geyser.configuration.GeyserJacksonConfiguration;
import java.nio.file.Path;
-public class GeyserFabricConfiguration extends GeyserJacksonConfiguration {
+public class GeyserModConfiguration extends GeyserJacksonConfiguration {
@JsonIgnore
private Path floodgateKeyPath;
- public void loadFloodgate(GeyserFabricMod geyser, ModContainer floodgate) {
+ public void loadFloodgate(GeyserModBootstrap geyser, Path floodgateDataFolder) {
Path geyserDataFolder = geyser.getConfigFolder();
- Path floodgateDataFolder = floodgate != null ? FabricLoader.getInstance().getConfigDir().resolve("floodgate") : null;
floodgateKeyPath = FloodgateKeyLoader.getKeyPath(this, floodgateDataFolder, geyserDataFolder, geyser.getGeyserLogger());
}
diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModInjector.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModInjector.java
new file mode 100644
index 000000000..624eccb3f
--- /dev/null
+++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModInjector.java
@@ -0,0 +1,159 @@
+/*
+ * 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.mod;
+
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.DefaultEventLoopGroup;
+import io.netty.channel.local.LocalAddress;
+import io.netty.util.concurrent.DefaultThreadFactory;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.network.ServerConnectionListener;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.geysermc.geyser.GeyserBootstrap;
+import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.network.netty.GeyserInjector;
+import org.geysermc.geyser.network.netty.LocalServerChannelWrapper;
+import org.geysermc.geyser.platform.mod.platform.GeyserModPlatform;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.List;
+
+public class GeyserModInjector extends GeyserInjector {
+
+ private final MinecraftServer server;
+ private final GeyserModPlatform platform;
+ private DefaultEventLoopGroup eventLoopGroup;
+
+ /**
+ * Used to uninject ourselves on shutdown.
+ */
+ private List allServerChannels;
+
+ public GeyserModInjector(MinecraftServer server, GeyserModPlatform platform) {
+ this.server = server;
+ this.platform = platform;
+ }
+
+ @Override
+ protected void initializeLocalChannel0(GeyserBootstrap bootstrap) throws Exception {
+ ServerConnectionListener connection = this.server.getConnection();
+
+ // Find the channel that Minecraft uses to listen to connections
+ ChannelFuture listeningChannel = null;
+ this.allServerChannels = ((GeyserChannelGetter) connection).geyser$getChannels();
+ for (ChannelFuture o : allServerChannels) {
+ listeningChannel = o;
+ break;
+ }
+
+ if (listeningChannel == null) {
+ throw new RuntimeException("Unable to find listening channel!");
+ }
+
+ // Making this a function prevents childHandler from being treated as a non-final variable
+ ChannelInitializer childHandler = getChildHandler(bootstrap, listeningChannel);
+ // This method is what initializes the connection in Java Edition, after Netty is all set.
+ Method initChannel = childHandler.getClass().getDeclaredMethod("initChannel", Channel.class);
+ initChannel.setAccessible(true);
+
+ // Separate variable so we can shut it down later
+ eventLoopGroup = new DefaultEventLoopGroup(0, new DefaultThreadFactory("Geyser " + this.platform.platformType().platformName() + " connection thread", Thread.MAX_PRIORITY));
+ ChannelFuture channelFuture = (new ServerBootstrap()
+ .channel(LocalServerChannelWrapper.class)
+ .childHandler(new ChannelInitializer<>() {
+ @Override
+ protected void initChannel(@NonNull Channel ch) throws Exception {
+ initChannel.invoke(childHandler, ch);
+
+ int index = ch.pipeline().names().indexOf("encoder");
+ String baseName = index != -1 ? "encoder" : "outbound_config";
+
+ if (bootstrap.getGeyserConfig().isDisableCompression()) {
+ ch.pipeline().addAfter(baseName, "geyser-compression-disabler", new GeyserModCompressionDisabler());
+ }
+ }
+ })
+ // Set to MAX_PRIORITY as MultithreadEventLoopGroup#newDefaultThreadFactory which DefaultEventLoopGroup implements does by default
+ .group(eventLoopGroup)
+ .localAddress(LocalAddress.ANY))
+ .bind()
+ .syncUninterruptibly();
+ // We don't need to add to the list, but plugins like ProtocolSupport and ProtocolLib that add to the main pipeline
+ // will work when we add to the list.
+ allServerChannels.add(channelFuture);
+ this.localChannel = channelFuture;
+ this.serverSocketAddress = channelFuture.channel().localAddress();
+ }
+
+ @SuppressWarnings("unchecked")
+ private ChannelInitializer getChildHandler(GeyserBootstrap bootstrap, ChannelFuture listeningChannel) {
+ List names = listeningChannel.channel().pipeline().names();
+ ChannelInitializer childHandler = null;
+ for (String name : names) {
+ ChannelHandler handler = listeningChannel.channel().pipeline().get(name);
+ try {
+ Field childHandlerField = handler.getClass().getDeclaredField("childHandler");
+ childHandlerField.setAccessible(true);
+ childHandler = (ChannelInitializer) childHandlerField.get(handler);
+ break;
+ } catch (Exception e) {
+ if (bootstrap.getGeyserConfig().isDebugMode()) {
+ bootstrap.getGeyserLogger().debug("The handler " + name + " isn't a ChannelInitializer. THIS ERROR IS SAFE TO IGNORE!");
+ e.printStackTrace();
+ }
+ }
+ }
+ if (childHandler == null) {
+ throw new RuntimeException();
+ }
+ return childHandler;
+ }
+
+ @Override
+ public void shutdown() {
+ if (this.allServerChannels != null) {
+ this.allServerChannels.remove(this.localChannel);
+ this.allServerChannels = null;
+ }
+
+ if (eventLoopGroup != null) {
+ try {
+ eventLoopGroup.shutdownGracefully().sync();
+ eventLoopGroup = null;
+ } catch (Exception e) {
+ GeyserImpl.getInstance().getLogger().error("Unable to shut down injector! " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+
+ super.shutdown();
+ }
+}
diff --git a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricLogger.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModLogger.java
similarity index 89%
rename from bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricLogger.java
rename to bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModLogger.java
index 180197f2d..9260288d7 100644
--- a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricLogger.java
+++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModLogger.java
@@ -23,7 +23,7 @@
* @link https://github.com/GeyserMC/Geyser
*/
-package org.geysermc.geyser.platform.fabric;
+package org.geysermc.geyser.platform.mod;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
@@ -32,15 +32,11 @@ import org.apache.logging.log4j.Logger;
import org.geysermc.geyser.GeyserLogger;
import org.geysermc.geyser.text.ChatColor;
-public class GeyserFabricLogger implements GeyserLogger {
- private final Logger logger = LogManager.getLogger("geyser-fabric");
+public class GeyserModLogger implements GeyserLogger {
+ private final Logger logger = LogManager.getLogger("geyser");
private boolean debug;
- public GeyserFabricLogger(boolean isDebug) {
- debug = isDebug;
- }
-
@Override
public void severe(String message) {
logger.fatal(message);
@@ -73,7 +69,7 @@ public class GeyserFabricLogger implements GeyserLogger {
@Override
public void sendMessage(Component message) {
- // As of Java Edition 1.19.2, Fabric's console doesn't natively support legacy format
+ // As of Java Edition 1.19.2, Minecraft's console doesn't natively support legacy format
String flattened = LegacyComponentSerializer.legacySection().serialize(message);
// Add the reset at the end, or else format will persist... forever.
// https://cdn.discordapp.com/attachments/573909525132738590/1033904509170225242/unknown.png
diff --git a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricUpdateListener.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModUpdateListener.java
similarity index 62%
rename from bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricUpdateListener.java
rename to bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModUpdateListener.java
index 1ea69cbe2..6a724155f 100644
--- a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricUpdateListener.java
+++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserModUpdateListener.java
@@ -23,21 +23,23 @@
* @link https://github.com/GeyserMC/Geyser
*/
-package org.geysermc.geyser.platform.fabric;
+package org.geysermc.geyser.platform.mod;
-import me.lucko.fabric.api.permissions.v0.Permissions;
-import net.minecraft.server.network.ServerGamePacketListenerImpl;
-import org.geysermc.geyser.Constants;
-import org.geysermc.geyser.platform.fabric.command.FabricCommandSender;
+import net.minecraft.world.entity.player.Player;
+import org.geysermc.geyser.Permissions;
+import org.geysermc.geyser.platform.mod.command.ModCommandSource;
import org.geysermc.geyser.util.VersionCheckUtils;
-public final class GeyserFabricUpdateListener {
- public static void onPlayReady(ServerGamePacketListenerImpl handler) {
- if (Permissions.check(handler.player, Constants.UPDATE_PERMISSION, 2)) {
- VersionCheckUtils.checkForGeyserUpdate(() -> new FabricCommandSender(handler.player.createCommandSourceStack()));
+public final class GeyserModUpdateListener {
+ public static void onPlayReady(Player player) {
+ // 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);
}
}
- private GeyserFabricUpdateListener() {
+ private GeyserModUpdateListener() {
}
}
diff --git a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserServerPortGetter.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserServerPortGetter.java
similarity index 97%
rename from bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserServerPortGetter.java
rename to bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserServerPortGetter.java
index 4f1c8b638..fad0d1678 100644
--- a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserServerPortGetter.java
+++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/GeyserServerPortGetter.java
@@ -23,7 +23,7 @@
* @link https://github.com/GeyserMC/Geyser
*/
-package org.geysermc.geyser.platform.fabric;
+package org.geysermc.geyser.platform.mod;
import net.minecraft.server.MinecraftServer;
diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/ModPingPassthrough.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/ModPingPassthrough.java
new file mode 100644
index 000000000..a2bbfa379
--- /dev/null
+++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/ModPingPassthrough.java
@@ -0,0 +1,112 @@
+/*
+ * 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.mod;
+
+import lombok.AllArgsConstructor;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
+import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+import net.minecraft.core.RegistryAccess;
+import net.minecraft.network.Connection;
+import net.minecraft.network.PacketSendListener;
+import net.minecraft.network.protocol.Packet;
+import net.minecraft.network.protocol.PacketFlow;
+import net.minecraft.network.protocol.status.ClientboundStatusResponsePacket;
+import net.minecraft.network.protocol.status.ServerStatus;
+import net.minecraft.network.protocol.status.ServerStatusPacketListener;
+import net.minecraft.network.protocol.status.ServerboundStatusRequestPacket;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.network.ServerStatusPacketListenerImpl;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.geysermc.geyser.GeyserLogger;
+import org.geysermc.geyser.ping.GeyserPingInfo;
+import org.geysermc.geyser.ping.IGeyserPingPassthrough;
+
+import java.net.InetSocketAddress;
+import java.util.Objects;
+
+@AllArgsConstructor
+public class ModPingPassthrough implements IGeyserPingPassthrough {
+
+ private static final GsonComponentSerializer GSON_SERIALIZER = GsonComponentSerializer.gson();
+ private static final LegacyComponentSerializer LEGACY_SERIALIZER = LegacyComponentSerializer.legacySection();
+
+ private final MinecraftServer server;
+ private final GeyserLogger logger;
+
+ @Nullable
+ @Override
+ public GeyserPingInfo getPingInformation(InetSocketAddress inetSocketAddress) {
+ ServerStatus status = server.getStatus();
+ if (status == null) {
+ return null;
+ }
+
+ try {
+ StatusInterceptor connection = new StatusInterceptor();
+ ServerStatusPacketListener statusPacketListener = new ServerStatusPacketListenerImpl(status, connection);
+
+ statusPacketListener.handleStatusRequest(ServerboundStatusRequestPacket.INSTANCE);
+ // mods like MiniMOTD (that inject into the above method) have now processed the response
+ status = Objects.requireNonNull(connection.status, "status response");
+ } catch (Exception e) {
+ if (logger.isDebug()) {
+ logger.debug("Failed to listen for modified ServerStatus: " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+
+ String jsonDescription = net.minecraft.network.chat.Component.Serializer.toJson(status.description(), RegistryAccess.EMPTY);
+ String legacyDescription = LEGACY_SERIALIZER.serialize(GSON_SERIALIZER.deserializeOr(jsonDescription, Component.empty()));
+
+ return new GeyserPingInfo(
+ legacyDescription,
+ status.players().map(ServerStatus.Players::max).orElse(1),
+ status.players().map(ServerStatus.Players::online).orElse(0)
+ );
+ }
+
+ /**
+ * Custom Connection that intercepts the status response right before it is sent
+ */
+ private static class StatusInterceptor extends Connection {
+
+ ServerStatus status;
+
+ StatusInterceptor() {
+ super(PacketFlow.SERVERBOUND); // we are the server.
+ }
+
+ @Override
+ public void send(@NonNull Packet> packet, @Nullable PacketSendListener packetSendListener, boolean bl) {
+ if (packet instanceof ClientboundStatusResponsePacket statusResponse) {
+ status = statusResponse.status();
+ }
+ super.send(packet, packetSendListener, bl);
+ }
+ }
+}
diff --git a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/command/FabricCommandSender.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/ModCommandSource.java
similarity index 67%
rename from bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/command/FabricCommandSender.java
rename to bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/ModCommandSource.java
index 6517ac133..af1f368b3 100644
--- a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/command/FabricCommandSender.java
+++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/command/ModCommandSource.java
@@ -23,25 +23,29 @@
* @link https://github.com/GeyserMC/Geyser
*/
-package org.geysermc.geyser.platform.fabric.command;
+package org.geysermc.geyser.platform.mod.command;
-import me.lucko.fabric.api.permissions.v0.Permissions;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.minecraft.commands.CommandSourceStack;
+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.text.ChatColor;
-import javax.annotation.Nonnull;
+import java.util.Objects;
+import java.util.UUID;
-public class FabricCommandSender implements GeyserCommandSource {
+public class ModCommandSource implements GeyserCommandSource {
private final CommandSourceStack source;
- public FabricCommandSender(CommandSourceStack source) {
+ public ModCommandSource(CommandSourceStack source) {
this.source = source;
+ // todo find locale?
}
@Override
@@ -50,7 +54,7 @@ public class FabricCommandSender implements GeyserCommandSource {
}
@Override
- public void sendMessage(@Nonnull String message) {
+ public void sendMessage(@NonNull String message) {
if (source.getEntity() instanceof ServerPlayer) {
((ServerPlayer) source.getEntity()).displayClientMessage(Component.literal(message), false);
} else {
@@ -62,7 +66,7 @@ public class FabricCommandSender implements GeyserCommandSource {
public void sendMessage(net.kyori.adventure.text.Component message) {
if (source.getEntity() instanceof ServerPlayer player) {
String decoded = GsonComponentSerializer.gson().serialize(message);
- player.displayClientMessage(Component.Serializer.fromJson(decoded), false);
+ player.displayClientMessage(Objects.requireNonNull(Component.Serializer.fromJson(decoded, RegistryAccess.EMPTY)), false);
return;
}
GeyserCommandSource.super.sendMessage(message);
@@ -73,8 +77,24 @@ public class FabricCommandSender 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 Permissions.check(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/fabric/src/main/java/org/geysermc/geyser/platform/fabric/mixin/client/IntegratedServerMixin.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/client/IntegratedServerMixin.java
similarity index 79%
rename from bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/mixin/client/IntegratedServerMixin.java
rename to bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/client/IntegratedServerMixin.java
index 942909068..ece2f730a 100644
--- a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/mixin/client/IntegratedServerMixin.java
+++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/client/IntegratedServerMixin.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
@@ -23,17 +23,16 @@
* @link https://github.com/GeyserMC/Geyser
*/
-package org.geysermc.geyser.platform.fabric.mixin.client;
+package org.geysermc.geyser.platform.mod.mixin.client;
-import net.fabricmc.api.EnvType;
-import net.fabricmc.api.Environment;
import net.minecraft.client.Minecraft;
import net.minecraft.client.server.IntegratedServer;
import net.minecraft.network.chat.Component;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.GameType;
-import org.geysermc.geyser.platform.fabric.GeyserFabricMod;
-import org.geysermc.geyser.platform.fabric.GeyserServerPortGetter;
+import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.platform.mod.GeyserModBootstrap;
+import org.geysermc.geyser.platform.mod.GeyserServerPortGetter;
import org.geysermc.geyser.text.GeyserLocale;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
@@ -42,7 +41,8 @@ import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
-@Environment(EnvType.CLIENT)
+import java.util.Objects;
+
@Mixin(IntegratedServer.class)
public class IntegratedServerMixin implements GeyserServerPortGetter {
@Shadow
@@ -54,12 +54,16 @@ public class IntegratedServerMixin implements GeyserServerPortGetter {
private void onOpenToLan(GameType gameType, boolean cheatsAllowed, int port, CallbackInfoReturnable cir) {
if (cir.getReturnValueZ()) {
// If the LAN is opened, starts Geyser.
- GeyserFabricMod.getInstance().startGeyser((MinecraftServer) (Object) this);
+ GeyserModBootstrap instance = GeyserModBootstrap.getInstance();
+ instance.setServer((MinecraftServer) (Object) this);
+ instance.getGeyserConfig().getRemote().setPort(port);
+ instance.onGeyserEnable();
// Ensure player locale has been loaded, in case it's different from Java system language
GeyserLocale.loadGeyserLocale(this.minecraft.options.languageCode);
// Give indication that Geyser is loaded
+ Objects.requireNonNull(this.minecraft.player);
this.minecraft.player.displayClientMessage(Component.literal(GeyserLocale.getPlayerLocaleString("geyser.core.start",
- this.minecraft.options.languageCode, "localhost", String.valueOf(this.publishedPort))), false);
+ this.minecraft.options.languageCode, "localhost", String.valueOf(GeyserImpl.getInstance().bedrockListener().port()))), false);
}
}
diff --git a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/mixin/server/MinecraftDedicatedServerMixin.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/DedicatedServerMixin.java
similarity index 76%
rename from bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/mixin/server/MinecraftDedicatedServerMixin.java
rename to bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/DedicatedServerMixin.java
index 23e148775..3b809d321 100644
--- a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/mixin/server/MinecraftDedicatedServerMixin.java
+++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/DedicatedServerMixin.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
@@ -23,7 +23,7 @@
* @link https://github.com/GeyserMC/Geyser
*/
-package org.geysermc.geyser.platform.fabric.mixin.server;
+package org.geysermc.geyser.platform.mod.mixin.server;
import com.mojang.datafixers.DataFixer;
import net.minecraft.server.MinecraftServer;
@@ -33,14 +33,14 @@ import net.minecraft.server.dedicated.DedicatedServer;
import net.minecraft.server.level.progress.ChunkProgressListenerFactory;
import net.minecraft.server.packs.repository.PackRepository;
import net.minecraft.world.level.storage.LevelStorageSource;
-import org.geysermc.geyser.platform.fabric.GeyserServerPortGetter;
+import org.geysermc.geyser.platform.mod.GeyserServerPortGetter;
import org.spongepowered.asm.mixin.Mixin;
import java.net.Proxy;
@Mixin(DedicatedServer.class)
-public abstract class MinecraftDedicatedServerMixin extends MinecraftServer implements GeyserServerPortGetter {
- public MinecraftDedicatedServerMixin(Thread thread, LevelStorageSource.LevelStorageAccess levelStorageAccess, PackRepository packRepository, WorldStem worldStem, Proxy proxy, DataFixer dataFixer, Services services, ChunkProgressListenerFactory chunkProgressListenerFactory) {
+public abstract class DedicatedServerMixin extends MinecraftServer implements GeyserServerPortGetter {
+ public DedicatedServerMixin(Thread thread, LevelStorageSource.LevelStorageAccess levelStorageAccess, PackRepository packRepository, WorldStem worldStem, Proxy proxy, DataFixer dataFixer, Services services, ChunkProgressListenerFactory chunkProgressListenerFactory) {
super(thread, levelStorageAccess, packRepository, worldStem, proxy, dataFixer, services, chunkProgressListenerFactory);
}
diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/ServerConnectionListenerMixin.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/ServerConnectionListenerMixin.java
new file mode 100644
index 000000000..52941d631
--- /dev/null
+++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/mixin/server/ServerConnectionListenerMixin.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.platform.mod.mixin.server;
+
+import io.netty.channel.ChannelFuture;
+import net.minecraft.server.network.ServerConnectionListener;
+import org.geysermc.geyser.platform.mod.GeyserChannelGetter;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+
+import java.util.List;
+
+@Mixin(ServerConnectionListener.class)
+public abstract class ServerConnectionListenerMixin implements GeyserChannelGetter {
+
+ @Shadow @Final private List channels;
+
+ @Override
+ public List geyser$getChannels() {
+ return this.channels;
+ }
+}
diff --git a/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/platform/GeyserModPlatform.java b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/platform/GeyserModPlatform.java
new file mode 100644
index 000000000..2f615591b
--- /dev/null
+++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/platform/GeyserModPlatform.java
@@ -0,0 +1,92 @@
+/*
+ * 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.mod.platform;
+
+import net.minecraft.server.MinecraftServer;
+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.dump.BootstrapDumpInfo;
+import org.geysermc.geyser.platform.mod.GeyserModBootstrap;
+
+import java.io.InputStream;
+import java.nio.file.Path;
+
+/**
+ * An interface which holds common methods that have different
+ * APIs on their respective mod platforms.
+ */
+public interface GeyserModPlatform {
+
+ /**
+ * Gets the {@link PlatformType} of the mod platform.
+ *
+ * @return the platform type of the mod platform
+ */
+ @NonNull
+ PlatformType platformType();
+
+ /**
+ * Gets the config path of the mod platform.
+ *
+ * @return the config path of the mod platform
+ */
+ @NonNull
+ String configPath();
+
+ /**
+ * Gets the data folder of the mod platform.
+ *
+ * @return the data folder of the mod platform
+ */
+ @NonNull
+ Path dataFolder(@NonNull String modId);
+
+ /**
+ * Gets the dump info of the mod platform.
+ *
+ * @param server the server to get the dump info from
+ * @return the dump info of the mod platform
+ */
+ @NonNull
+ BootstrapDumpInfo dumpInfo(@NonNull MinecraftServer server);
+
+ /**
+ * Tests if the Floodgate plugin is present on the mod platform.
+ *
+ * @return {@code true} if the Floodgate plugin is present on the mod platform, {@code false} otherwise
+ */
+ boolean testFloodgatePluginPresent(@NonNull GeyserModBootstrap bootstrap);
+
+ /**
+ * Resolves a resource from the mod jar.
+ *
+ * @param resource the name of the resource
+ * @return the input stream of the resource
+ */
+ @Nullable
+ InputStream resolveResource(@NonNull String resource);
+}
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
new file mode 100644
index 000000000..89452eba3
--- /dev/null
+++ b/bootstrap/mod/src/main/java/org/geysermc/geyser/platform/mod/world/GeyserModWorldManager.java
@@ -0,0 +1,201 @@
+/*
+ * 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.world;
+
+import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
+import net.minecraft.SharedConstants;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.RegistryAccess;
+import net.minecraft.core.component.DataComponents;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.network.chat.Component;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.block.entity.BannerBlockEntity;
+import net.minecraft.world.level.block.entity.BannerPatternLayers;
+import net.minecraft.world.level.block.entity.BlockEntity;
+import net.minecraft.world.level.block.entity.DecoratedPotBlockEntity;
+import net.minecraft.world.level.chunk.ChunkAccess;
+import net.minecraft.world.level.chunk.LevelChunkSection;
+import net.minecraft.world.level.chunk.status.ChunkStatus;
+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.session.GeyserSession;
+import org.geysermc.geyser.util.MinecraftKey;
+import org.geysermc.mcprotocollib.protocol.data.game.Holder;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode;
+import org.geysermc.mcprotocollib.protocol.data.game.item.component.BannerPatternLayer;
+import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+
+public class GeyserModWorldManager extends GeyserWorldManager {
+
+ private static final GsonComponentSerializer GSON_SERIALIZER = GsonComponentSerializer.gson();
+ private final MinecraftServer server;
+
+ public GeyserModWorldManager(MinecraftServer server) {
+ this.server = server;
+ }
+
+ @Override
+ public int getBlockAt(GeyserSession session, int x, int y, int z) {
+ // If the protocol version of Geyser and the server are not the
+ // same, fallback to the chunk cache. May be able to update this
+ // in the future to use ViaVersion however, like Spigot does.
+ if (SharedConstants.getCurrentVersion().getProtocolVersion() != GameProtocol.getJavaProtocolVersion()) {
+ return super.getBlockAt(session, x, y, z);
+ }
+
+ ServerPlayer player = this.getPlayer(session);
+ if (player == null) {
+ return 0;
+ }
+
+ Level level = player.level();
+ if (y < level.getMinBuildHeight()) {
+ return 0;
+ }
+
+ ChunkAccess chunk = level.getChunkSource().getChunk(x >> 4, z >> 4, ChunkStatus.FULL, false);
+ if (chunk == null) {
+ return 0;
+ }
+
+ int worldOffset = level.getMinBuildHeight() >> 4;
+ int chunkOffset = (y >> 4) - worldOffset;
+ if (chunkOffset < chunk.getSections().length) {
+ LevelChunkSection section = chunk.getSections()[chunkOffset];
+ if (section != null && !section.hasOnlyAir()) {
+ return Block.getId(section.getBlockState(x & 15, y & 15, z & 15));
+ }
+ }
+
+ return 0;
+ }
+
+ @Override
+ public boolean hasOwnChunkCache() {
+ return SharedConstants.getCurrentVersion().getProtocolVersion() == GameProtocol.getJavaProtocolVersion();
+ }
+
+ @Override
+ public GameMode getDefaultGameMode(GeyserSession session) {
+ return GameMode.byId(server.getDefaultGameType().getId());
+ }
+
+ @NonNull
+ @Override
+ public CompletableFuture getPickItemComponents(GeyserSession session, int x, int y, int z, boolean addNbtData) {
+ CompletableFuture future = new CompletableFuture<>();
+ server.execute(() -> {
+ ServerPlayer player = getPlayer(session);
+ if (player == null) {
+ future.complete(null);
+ return;
+ }
+
+ BlockPos pos = new BlockPos(x, y, z);
+ // Don't create a new block entity if invalid
+ //noinspection resource - level() is just a getter
+ BlockEntity blockEntity = player.level().getChunkAt(pos).getBlockEntity(pos);
+ if (blockEntity instanceof BannerBlockEntity banner) {
+ // Potentially exposes other NBT data? But we need to get the NBT data for the banner patterns *and*
+ // the banner might have a custom name, both of which a Java client knows and caches
+ ItemStack itemStack = banner.getItem();
+
+ org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents components =
+ new org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents(new HashMap<>());
+
+ components.put(DataComponentType.DAMAGE, itemStack.getDamageValue());
+
+ Component customName = itemStack.getComponents().get(DataComponents.CUSTOM_NAME);
+ if (customName != null) {
+ components.put(DataComponentType.CUSTOM_NAME, toKyoriComponent(customName));
+ }
+
+ BannerPatternLayers pattern = itemStack.get(DataComponents.BANNER_PATTERNS);
+ if (pattern != null) {
+ components.put(DataComponentType.BANNER_PATTERNS, toPatternList(pattern));
+ }
+
+ future.complete(components);
+ return;
+ }
+ future.complete(null);
+ });
+ return future;
+ }
+
+ @Override
+ public void getDecoratedPotData(GeyserSession session, Vector3i pos, Consumer> apply) {
+ server.execute(() -> {
+ ServerPlayer player = getPlayer(session);
+ if (player == null) {
+ return;
+ }
+
+ BlockPos blockPos = new BlockPos(pos.getX(), pos.getY(), pos.getZ());
+ // Don't create a new block entity if invalid
+ //noinspection resource - level() is just a getter
+ BlockEntity blockEntity = player.level().getChunkAt(blockPos).getBlockEntity(blockPos);
+ if (blockEntity instanceof DecoratedPotBlockEntity pot) {
+ List sherds = pot.getDecorations().ordered()
+ .stream().map(item -> BuiltInRegistries.ITEM.getKey(item).toString())
+ .toList();
+ apply.accept(sherds);
+ }
+ });
+ }
+
+ private ServerPlayer getPlayer(GeyserSession session) {
+ return server.getPlayerList().getPlayer(session.getPlayerEntity().getUuid());
+ }
+
+ private static net.kyori.adventure.text.Component toKyoriComponent(Component component) {
+ String json = Component.Serializer.toJson(component, RegistryAccess.EMPTY);
+ return GSON_SERIALIZER.deserializeOr(json, net.kyori.adventure.text.Component.empty());
+ }
+
+ private static List toPatternList(BannerPatternLayers patternLayers) {
+ return patternLayers.layers().stream()
+ .map(layer -> {
+ BannerPatternLayer.BannerPattern pattern = new BannerPatternLayer.BannerPattern(
+ MinecraftKey.key(layer.pattern().value().assetId().toString()), layer.pattern().value().translationKey()
+ );
+ return new BannerPatternLayer(Holder.ofCustom(pattern), layer.color().getId());
+ })
+ .toList();
+ }
+}
diff --git a/bootstrap/mod/src/main/resources/geyser.mixins.json b/bootstrap/mod/src/main/resources/geyser.mixins.json
new file mode 100644
index 000000000..47b2f60f3
--- /dev/null
+++ b/bootstrap/mod/src/main/resources/geyser.mixins.json
@@ -0,0 +1,18 @@
+{
+ "required": true,
+ "minVersion": "0.8",
+ "package": "org.geysermc.geyser.platform.mod.mixin",
+ "compatibilityLevel": "JAVA_17",
+ "mixins": [
+ "server.ServerConnectionListenerMixin"
+ ],
+ "server": [
+ "server.DedicatedServerMixin"
+ ],
+ "client": [
+ "client.IntegratedServerMixin"
+ ],
+ "injectors": {
+ "defaultRequire": 1
+ }
+}
diff --git a/bootstrap/spigot/build.gradle.kts b/bootstrap/spigot/build.gradle.kts
index 129064cd4..0a1271145 100644
--- a/bootstrap/spigot/build.gradle.kts
+++ b/bootstrap/spigot/build.gradle.kts
@@ -4,25 +4,27 @@ dependencies {
isTransitive = false
}
+ implementation(libs.erosion.bukkit.nms) {
+ attributes {
+ attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 21)
+ }
+ }
+
implementation(variantOf(libs.adapters.spigot) {
classifier("all") // otherwise the unshaded jar is used without the shaded NMS implementations
})
+ implementation(variantOf(libs.adapters.paper) {
+ 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)
-
- // Both folia-api and paper-mojangapi only provide Java 17 versions for 1.19
- compileOnly(libs.folia.api) {
- attributes {
- attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17)
- }
- }
- compileOnly(libs.paper.mojangapi) {
- attributes {
- attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17)
- }
- }
+
+ compileOnly(libs.folia.api)
+
+ compileOnlyApi(libs.viaversion)
}
platformRelocate("it.unimi.dsi.fastutil")
@@ -31,16 +33,24 @@ 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 {
+
+ // Prevents Paper 1.20.5+ from remapping Geyser
+ manifest {
+ attributes["paperweight-mappings-namespace"] = "mojang"
+ }
+
archiveBaseName.set("Geyser-Spigot")
dependencies {
@@ -48,6 +58,7 @@ tasks.withType {
// We cannot shade Netty, or else native libraries will not load
// Needed because older Spigot builds do not provide the haproxy module
+ exclude(dependency("io.netty.incubator:.*"))
exclude(dependency("io.netty:netty-transport-classes-epoll:.*"))
exclude(dependency("io.netty:netty-transport-native-epoll:.*"))
exclude(dependency("io.netty:netty-transport-native-unix-common:.*"))
@@ -67,3 +78,8 @@ tasks.withType {
exclude(dependency("com.mojang:.*"))
}
}
+
+modrinth {
+ uploadFile.set(tasks.getByPath("shadowJar"))
+ loaders.addAll("spigot", "paper")
+}
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperLogger.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperLogger.java
index 930f84cec..9ebd6519a 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperLogger.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperLogger.java
@@ -34,8 +34,8 @@ import java.util.logging.Logger;
public final class GeyserPaperLogger extends GeyserSpigotLogger {
private final ComponentLogger componentLogger;
- public GeyserPaperLogger(Plugin plugin, Logger logger, boolean debug) {
- super(logger, debug);
+ public GeyserPaperLogger(Plugin plugin, Logger logger) {
+ super(logger);
componentLogger = plugin.getComponentLogger();
}
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperPingPassthrough.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperPingPassthrough.java
index bb0f30e70..6a962f450 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperPingPassthrough.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserPaperPingPassthrough.java
@@ -27,13 +27,12 @@ package org.geysermc.geyser.platform.spigot;
import com.destroystokyo.paper.event.server.PaperServerListPingEvent;
import com.destroystokyo.paper.network.StatusClient;
-import com.destroystokyo.paper.profile.PlayerProfile;
import org.bukkit.Bukkit;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.ping.GeyserPingInfo;
import org.geysermc.geyser.ping.IGeyserPingPassthrough;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Constructor;
import java.net.InetSocketAddress;
@@ -51,6 +50,7 @@ public final class GeyserPaperPingPassthrough implements IGeyserPingPassthrough
this.logger = logger;
}
+ @SuppressWarnings("deprecation")
@Nullable
@Override
public GeyserPingInfo getPingInformation(InetSocketAddress inetSocketAddress) {
@@ -81,16 +81,7 @@ public final class GeyserPaperPingPassthrough implements IGeyserPingPassthrough
players = new GeyserPingInfo.Players(event.getMaxPlayers(), event.getNumPlayers());
}
- GeyserPingInfo geyserPingInfo = new GeyserPingInfo(event.getMotd(), players,
- new GeyserPingInfo.Version(Bukkit.getVersion(), GameProtocol.getJavaProtocolVersion()));
-
- if (!event.shouldHidePlayers()) {
- for (PlayerProfile profile : event.getPlayerSample()) {
- geyserPingInfo.getPlayerList().add(profile.getName());
- }
- }
-
- return geyserPingInfo;
+ return new GeyserPingInfo(event.getMotd(), players);
} catch (Exception | LinkageError e) { // LinkageError in the event that method/constructor signatures change
logger.debug("Error while getting Paper ping passthrough: " + e);
return null;
@@ -99,7 +90,7 @@ public final class GeyserPaperPingPassthrough implements IGeyserPingPassthrough
private record GeyserStatusClient(InetSocketAddress address) implements StatusClient {
@Override
- public @NotNull InetSocketAddress getAddress() {
+ public @NonNull InetSocketAddress getAddress() {
return address;
}
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotCompressionDisabler.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotCompressionDisabler.java
index 9b112f62f..2a6056df9 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotCompressionDisabler.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotCompressionDisabler.java
@@ -34,7 +34,7 @@ import org.geysermc.geyser.GeyserImpl;
/**
* Disables the compression packet (and the compression handlers from being added to the pipeline) for Geyser clients
* that won't be receiving the data over the network.
- *
+ *
* As of 1.8 - 1.17.1, compression is enabled in the Netty pipeline by adding a listener after a packet is written.
* If we simply "cancel" or don't forward the packet, then the listener is never called.
*/
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotDumpInfo.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotDumpInfo.java
index d340935b3..329663709 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotDumpInfo.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotDumpInfo.java
@@ -47,6 +47,7 @@ public class GeyserSpigotDumpInfo extends BootstrapDumpInfo {
private final int serverPort;
private final List plugins;
+ @SuppressWarnings("deprecation")
GeyserSpigotDumpInfo() {
super();
this.platformName = Bukkit.getName();
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotInjector.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotInjector.java
index e3d73fb19..5dcfbd0f8 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotInjector.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotInjector.java
@@ -25,13 +25,14 @@
package org.geysermc.geyser.platform.spigot;
-import com.github.steveice10.mc.protocol.MinecraftProtocol;
+import org.geysermc.mcprotocollib.protocol.MinecraftProtocol;
import com.viaversion.viaversion.bukkit.handlers.BukkitChannelInitializer;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.local.LocalAddress;
import io.netty.util.concurrent.DefaultThreadFactory;
import org.bukkit.Bukkit;
+import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.GeyserBootstrap;
import org.geysermc.geyser.network.netty.GeyserInjector;
import org.geysermc.geyser.network.netty.LocalServerChannelWrapper;
@@ -74,12 +75,10 @@ public class GeyserSpigotInjector extends GeyserInjector {
Object connection = null;
// Find the class that manages network IO
for (Method m : serverClazz.getDeclaredMethods()) {
- if (m.getReturnType() != null) {
- // First is Spigot-mapped name, second is Mojang-mapped name which is implemented as future-proofing
- if (m.getReturnType().getSimpleName().equals("ServerConnection") || m.getReturnType().getSimpleName().equals("ServerConnectionListener")) {
- if (m.getParameterTypes().length == 0) {
- connection = m.invoke(server);
- }
+ // First is Spigot-mapped name, second is Mojang-mapped name which is implemented as future-proofing
+ if (m.getReturnType().getSimpleName().equals("ServerConnection") || m.getReturnType().getSimpleName().equals("ServerConnectionListener")) {
+ if (m.getParameterTypes().length == 0) {
+ connection = m.invoke(server);
}
}
}
@@ -117,11 +116,14 @@ public class GeyserSpigotInjector extends GeyserInjector {
.channel(LocalServerChannelWrapper.class)
.childHandler(new ChannelInitializer<>() {
@Override
- protected void initChannel(Channel ch) throws Exception {
+ protected void initChannel(@NonNull Channel ch) throws Exception {
initChannel.invoke(childHandler, ch);
+ int index = ch.pipeline().names().indexOf("encoder");
+ String baseName = index != -1 ? "encoder" : "outbound_config";
+
if (bootstrap.getGeyserConfig().isDisableCompression() && GeyserSpigotCompressionDisabler.ENABLED) {
- ch.pipeline().addAfter("encoder", "geyser-compression-disabler", new GeyserSpigotCompressionDisabler());
+ ch.pipeline().addAfter(baseName, "geyser-compression-disabler", new GeyserSpigotCompressionDisabler());
}
}
})
@@ -151,7 +153,7 @@ public class GeyserSpigotInjector extends GeyserInjector {
childHandler = (ChannelInitializer) childHandlerField.get(handler);
// ViaVersion non-Paper-injector workaround so we aren't double-injecting
if (isViaVersion && childHandler instanceof BukkitChannelInitializer) {
- childHandler = ((BukkitChannelInitializer) childHandler).getOriginal();
+ childHandler = ((BukkitChannelInitializer) childHandler).original();
}
break;
} catch (Exception e) {
@@ -178,6 +180,7 @@ public class GeyserSpigotInjector extends GeyserInjector {
bootstrap.getGeyserConfig().getRemote().port(), this.serverSocketAddress,
InetAddress.getLoopbackAddress().getHostAddress(), protocol, protocol.createHelper());
session.connect();
+ session.disconnect("");
}
@Override
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotLogger.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotLogger.java
index fe56cba1c..5c6101eae 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotLogger.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotLogger.java
@@ -25,15 +25,15 @@
package org.geysermc.geyser.platform.spigot;
-import lombok.AllArgsConstructor;
import lombok.Getter;
+import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.geysermc.geyser.GeyserLogger;
import java.util.logging.Level;
import java.util.logging.Logger;
-@AllArgsConstructor
+@RequiredArgsConstructor
public class GeyserSpigotLogger implements GeyserLogger {
private final Logger logger;
@Getter @Setter
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPingPassthrough.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPingPassthrough.java
index 1e6a0ad6c..4b1e42871 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPingPassthrough.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotPingPassthrough.java
@@ -30,11 +30,12 @@ import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.server.ServerListPingEvent;
import org.bukkit.util.CachedServerIcon;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.ping.GeyserPingInfo;
import org.geysermc.geyser.ping.IGeyserPingPassthrough;
-import javax.annotation.Nonnull;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Collections;
@@ -45,17 +46,13 @@ public class GeyserSpigotPingPassthrough implements IGeyserPingPassthrough {
private final GeyserSpigotLogger logger;
+ @SuppressWarnings("deprecation")
@Override
- public GeyserPingInfo getPingInformation(InetSocketAddress inetSocketAddress) {
+ public @Nullable GeyserPingInfo getPingInformation(InetSocketAddress inetSocketAddress) {
try {
ServerListPingEvent event = new GeyserPingEvent(inetSocketAddress.getAddress(), Bukkit.getMotd(), Bukkit.getOnlinePlayers().size(), Bukkit.getMaxPlayers());
Bukkit.getPluginManager().callEvent(event);
- GeyserPingInfo geyserPingInfo = new GeyserPingInfo(event.getMotd(),
- new GeyserPingInfo.Players(event.getMaxPlayers(), event.getNumPlayers()),
- new GeyserPingInfo.Version(Bukkit.getVersion(), GameProtocol.getJavaProtocolVersion()) // thanks Spigot for not exposing this, just default to latest
- );
- Bukkit.getOnlinePlayers().stream().map(Player::getName).forEach(geyserPingInfo.getPlayerList()::add);
- return geyserPingInfo;
+ return new GeyserPingInfo(event.getMotd(), event.getMaxPlayers(), event.getNumPlayers());
} catch (Exception | LinkageError e) { // LinkageError in the event that method/constructor signatures change
logger.debug("Error while getting Bukkit ping passthrough: " + e);
return null;
@@ -73,7 +70,7 @@ public class GeyserSpigotPingPassthrough implements IGeyserPingPassthrough {
public void setServerIcon(CachedServerIcon icon) throws IllegalArgumentException, UnsupportedOperationException {
}
- @Nonnull
+ @NonNull
@Override
public Iterator iterator() throws UnsupportedOperationException {
return Collections.emptyIterator();
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 b932962a0..3bb44a4bc 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,35 +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.geysermc.geyser.api.util.PlatformType;
-import org.geysermc.geyser.Constants;
+import org.checkerframework.checker.nullness.qual.NonNull;
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.command.GeyserCommandManager;
+import org.geysermc.geyser.api.event.lifecycle.GeyserRegisterPermissionsEvent;
+import org.geysermc.geyser.api.util.PlatformType;
+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;
@@ -66,29 +65,25 @@ 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.jetbrains.annotations.NotNull;
+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;
-import java.util.logging.Level;
public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
- /**
- * Determines if the plugin has been ran once before, including before /geyser reload.
- */
- private static boolean INITIALIZED = false;
- private GeyserSpigotCommandManager geyserCommandManager;
+ private CommandRegistry commandRegistry;
private GeyserSpigotConfiguration geyserConfig;
private GeyserSpigotInjector geyserInjector;
- private GeyserSpigotLogger geyserLogger;
+ private final GeyserSpigotLogger geyserLogger = GeyserPaperLogger.supported() ?
+ new GeyserPaperLogger(this, getLogger()) : new GeyserSpigotLogger(getLogger());
private IGeyserPingPassthrough geyserSpigotPingPassthrough;
private GeyserSpigotWorldManager geyserWorldManager;
@@ -101,6 +96,11 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
@Override
public void onLoad() {
+ onGeyserInitialize();
+ }
+
+ @Override
+ public void onGeyserInitialize() {
GeyserLocale.init(this);
try {
@@ -111,12 +111,13 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
// We depend on this as a fallback in certain scenarios
BlockData.class.getMethod("getAsString");
} catch (ClassNotFoundException | NoSuchMethodException e) {
- getLogger().severe("*********************************************");
- getLogger().severe("");
- getLogger().severe(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_server.header"));
- getLogger().severe(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_server.message", "1.13.2"));
- getLogger().severe("");
- getLogger().severe("*********************************************");
+ geyserLogger.error("*********************************************");
+ geyserLogger.error("");
+ geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_server.header"));
+ geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_server.message", "1.13.2"));
+ geyserLogger.error("");
+ geyserLogger.error("*********************************************");
+ Bukkit.getPluginManager().disablePlugin(this);
return;
}
@@ -124,92 +125,96 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
Class.forName("net.md_5.bungee.chat.ComponentSerializer");
} catch (ClassNotFoundException e) {
if (!PaperAdventure.canSendMessageUsingComponent()) { // Prepare for Paper eventually removing Bungee chat
- getLogger().severe("*********************************************");
- getLogger().severe("");
- getLogger().severe(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_server_type.header", getServer().getName()));
- getLogger().severe(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_server_type.message", "Paper"));
- getLogger().severe("");
- getLogger().severe("*********************************************");
+ geyserLogger.error("*********************************************");
+ geyserLogger.error("");
+ geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_server_type.header", getServer().getName()));
+ geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_server_type.message", "Paper"));
+ geyserLogger.error("");
+ geyserLogger.error("*********************************************");
+ Bukkit.getPluginManager().disablePlugin(this);
return;
}
}
- // This is manually done instead of using Bukkit methods to save the config because otherwise comments get removed
try {
- if (!getDataFolder().exists()) {
- getDataFolder().mkdir();
- }
- File configFile = FileUtils.fileOrCopiedFromResource(new File(getDataFolder(), "config.yml"), "config.yml",
- (x) -> x.replaceAll("generateduuid", UUID.randomUUID().toString()), this);
- this.geyserConfig = FileUtils.loadConfig(configFile, GeyserSpigotConfiguration.class);
- } catch (IOException ex) {
- getLogger().log(Level.SEVERE, GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex);
- ex.printStackTrace();
+ Class.forName("io.netty.util.internal.ObjectPool$ObjectCreator");
+ } catch (ClassNotFoundException e) {
+ geyserLogger.error("*********************************************");
+ geyserLogger.error("");
+ 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;
}
- this.geyserLogger = GeyserPaperLogger.supported() ? new GeyserPaperLogger(this, getLogger(), geyserConfig.isDebugMode())
- : new GeyserSpigotLogger(getLogger(), geyserConfig.isDebugMode());
-
+ if (!loadConfig()) {
+ return;
+ }
+ this.geyserLogger.setDebug(geyserConfig.isDebugMode());
GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger);
+ // Turn "(MC: 1.16.4)" into 1.16.4.
+ this.minecraftVersion = Bukkit.getServer().getVersion().split("\\(MC: ")[1].split("\\)")[0];
+
this.geyser = GeyserImpl.load(PlatformType.SPIGOT, this);
}
@Override
public void onEnable() {
- if (this.geyserConfig == null) {
- // We failed to initialize correctly
- Bukkit.getPluginManager().disablePlugin(this);
- return;
+ // Create command manager early so we can add Geyser extension commands
+ 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);
}
- this.geyserCommandManager = new GeyserSpigotCommandManager(geyser);
- this.geyserCommandManager.init();
+ 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());
+ }
- if (!INITIALIZED) {
- // Needs to be an anonymous inner class otherwise Bukkit complains about missing classes
- Bukkit.getPluginManager().registerEvents(new Listener() {
+ this.commandRegistry = new SpigotCommandRegistry(geyser, cloud);
- @EventHandler
- public void onServerLoaded(ServerLoadEvent event) {
- // Wait until all plugins have loaded so Geyser can start
- postStartup();
- }
- }, this);
-
- // 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
- 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.description().name(), ex);
+ // 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) {
+ geyser.setShuttingDown(false);
}
+ onGeyserEnable();
}
- }
-
- if (INITIALIZED) {
- // Reload; continue with post startup
- postStartup();
- }
+ }, this);
}
- private void postStartup() {
- GeyserImpl.start();
+ public void onGeyserEnable() {
+ // Configs are loaded once early - so we can create the logger, then load extensions and finally register
+ // extension commands in #onEnable. To ensure reloading geyser also reloads the geyser config, this exists
+ if (GeyserImpl.getInstance().isReloading()) {
+ if (!loadConfig()) {
+ return;
+ }
+ this.geyserLogger.setDebug(this.geyserConfig.isDebugMode());
+ GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger);
+ }
- // Turn "(MC: 1.16.4)" into 1.16.4.
- this.minecraftVersion = Bukkit.getServer().getVersion().split("\\(MC: ")[1].split("\\)")[0];
+ GeyserImpl.start();
if (geyserConfig.isLegacyPingPassthrough()) {
this.geyserSpigotPingPassthrough = GeyserLegacyPingPassthrough.init(geyser);
@@ -225,20 +230,16 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
}
geyserLogger.debug("Spigot ping passthrough type: " + (this.geyserSpigotPingPassthrough == null ? null : this.geyserSpigotPingPassthrough.getClass()));
- boolean isViaVersion = Bukkit.getPluginManager().getPlugin("ViaVersion") != null;
- if (isViaVersion) {
- try {
- // Ensure that we have the latest 4.0.0 changes and not an older ViaVersion version
- Class.forName("com.viaversion.viaversion.api.ViaManager");
- } catch (ClassNotFoundException e) {
- GeyserSpigotVersionChecker.sendOutdatedViaVersionMessage(geyserLogger);
- isViaVersion = false;
- if (this.geyserConfig.isDebugMode()) {
- e.printStackTrace();
- }
- }
+ // Don't need to re-create the world manager/reinject when reloading
+ if (GeyserImpl.getInstance().isReloading()) {
+ return;
}
+ boolean isViaVersion = Bukkit.getPluginManager().getPlugin("ViaVersion") != null;
+
+ // Check to ensure the current setup can support the protocol version Geyser uses
+ GeyserSpigotVersionChecker.checkForSupportedProtocol(geyserLogger, isViaVersion);
+
// We want to do this late in the server startup process to allow plugins such as ViaVersion and ProtocolLib
// To do their job injecting, then connect into *that*
this.geyserInjector = new GeyserSpigotInjector(isViaVersion);
@@ -246,17 +247,29 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
if (Boolean.parseBoolean(System.getProperty("Geyser.UseDirectAdapters", "true"))) {
try {
- String name = Bukkit.getServer().getClass().getPackage().getName();
- String nmsVersion = name.substring(name.lastIndexOf('.') + 1);
- SpigotAdapters.registerWorldAdapter(nmsVersion);
+ boolean isPaper = false;
+ try {
+ String name = Bukkit.getServer().getClass().getPackage().getName();
+ String nmsVersion = name.substring(name.lastIndexOf('.') + 1);
+ SpigotAdapters.registerWorldAdapter(nmsVersion);
+ geyserLogger.debug("Using spigot NMS adapter for nms version: " + nmsVersion);
+ } catch (Exception e) { // Likely running on Paper 1.20.5+
+ geyserLogger.debug("Unable to find spigot world manager: " + e.getMessage());
+ //noinspection deprecation
+ int protocolVersion = Bukkit.getUnsafe().getProtocolVersion();
+ PaperAdapters.registerClosestWorldAdapter(protocolVersion);
+ isPaper = true;
+ geyserLogger.debug("Using paper world adapter for protocol version: " + protocolVersion);
+ }
+
if (isViaVersion && isViaVersionNeeded()) {
- this.geyserWorldManager = new GeyserSpigotLegacyNativeWorldManager(this);
+ this.geyserWorldManager = new GeyserSpigotLegacyNativeWorldManager(this, isPaper);
} else {
// No ViaVersion
- this.geyserWorldManager = new GeyserSpigotNativeWorldManager(this);
+ this.geyserWorldManager = new GeyserSpigotNativeWorldManager(this, isPaper);
}
- geyserLogger.debug("Using NMS adapter: " + this.geyserWorldManager.getClass() + ", " + nmsVersion);
- } catch (Exception e) {
+ geyserLogger.debug("Using world manager of type: " + this.geyserWorldManager.getClass().getSimpleName());
+ } catch (Throwable e) {
if (geyserConfig.isDebugMode()) {
geyserLogger.debug("Error while attempting to find NMS adapter. Most likely, this can be safely ignored. :)");
e.printStackTrace();
@@ -265,95 +278,58 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
} else {
geyserLogger.debug("Not using NMS adapter as it is disabled via system property.");
}
+
if (this.geyserWorldManager == null) {
// No NMS adapter
this.geyserWorldManager = new GeyserSpigotWorldManager(this);
geyserLogger.debug("Using default world manager.");
}
- PluginCommand geyserCommand = this.getCommand("geyser");
- geyserCommand.setExecutor(new GeyserSpigotCommandExecutor(geyser, geyserCommandManager.getCommands()));
+ // Register permissions so they appear in, for example, LuckPerms' UI
+ // 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);
- for (Map.Entry> entry : this.geyserCommandManager.extensionCommands().entrySet()) {
- Map commands = entry.getValue();
- if (commands.isEmpty()) {
- continue;
+ 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);
}
- PluginCommand command = this.getCommand(entry.getKey().description().id());
- if (command == null) {
- continue;
- }
+ pluginManager.addPermission(new Permission(permission, permissionDefault));
+ });
- command.setExecutor(new GeyserSpigotCommandExecutor(this.geyser, commands));
- }
+ // Events cannot be unregistered - re-registering results in duplicate firings
+ GeyserSpigotBlockPlaceListener blockPlaceListener = new GeyserSpigotBlockPlaceListener(geyser, this.geyserWorldManager);
+ pluginManager.registerEvents(blockPlaceListener, this);
- if (!INITIALIZED) {
- // 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;
- }
+ pluginManager.registerEvents(new GeyserPistonListener(geyser, this.geyserWorldManager), this);
- 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));
-
- // Events cannot be unregistered - re-registering results in duplicate firings
- GeyserSpigotBlockPlaceListener blockPlaceListener = new GeyserSpigotBlockPlaceListener(geyser, this.geyserWorldManager);
- Bukkit.getServer().getPluginManager().registerEvents(blockPlaceListener, this);
-
- Bukkit.getServer().getPluginManager().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);
- }
-
- // Check to ensure the current setup can support the protocol version Geyser uses
- GeyserSpigotVersionChecker.checkForSupportedProtocol(geyserLogger, isViaVersion);
-
- INITIALIZED = true;
+ pluginManager.registerEvents(new GeyserSpigotUpdateListener(), this);
}
@Override
- public void onDisable() {
+ public void onGeyserDisable() {
+ if (geyser != null) {
+ geyser.disable();
+ }
+ }
+
+ @Override
+ public void onGeyserShutdown() {
if (geyser != null) {
geyser.shutdown();
}
@@ -362,6 +338,11 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
}
}
+ @Override
+ public void onDisable() {
+ this.onGeyserShutdown();
+ }
+
@Override
public GeyserSpigotConfiguration getGeyserConfig() {
return geyserConfig;
@@ -373,8 +354,8 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
}
@Override
- public GeyserCommandManager getGeyserCommandManager() {
- return this.geyserCommandManager;
+ public CommandRegistry getCommandRegistry() {
+ return this.commandRegistry;
}
@Override
@@ -428,7 +409,7 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
return false;
}
for (int i = protocolList.size() - 1; i >= 0; i--) {
- MappingData mappingData = protocolList.get(i).getProtocol().getMappingData();
+ MappingData mappingData = protocolList.get(i).protocol().getMappingData();
if (mappingData != null) {
return true;
}
@@ -437,7 +418,7 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
return false;
}
- @NotNull
+ @NonNull
@Override
public String getServerBindAddress() {
return Bukkit.getIp();
@@ -456,4 +437,25 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
}
return false;
}
+
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ private boolean loadConfig() {
+ // This is manually done instead of using Bukkit methods to save the config because otherwise comments get removed
+ try {
+ if (!getDataFolder().exists()) {
+ //noinspection ResultOfMethodCallIgnored
+ getDataFolder().mkdir();
+ }
+ File configFile = FileUtils.fileOrCopiedFromResource(new File(getDataFolder(), "config.yml"), "config.yml",
+ (x) -> x.replaceAll("generateduuid", UUID.randomUUID().toString()), this);
+ this.geyserConfig = FileUtils.loadConfig(configFile, GeyserSpigotConfiguration.class);
+ } catch (IOException ex) {
+ geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex);
+ ex.printStackTrace();
+ Bukkit.getPluginManager().disablePlugin(this);
+ return false;
+ }
+
+ return true;
+ }
}
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/GeyserSpigotVersionChecker.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotVersionChecker.java
index 0212ff9b0..057304357 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotVersionChecker.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/GeyserSpigotVersionChecker.java
@@ -39,6 +39,7 @@ import java.lang.reflect.Modifier;
public final class GeyserSpigotVersionChecker {
private static final String VIAVERSION_DOWNLOAD_URL = "https://ci.viaversion.com/job/ViaVersion/";
+ @SuppressWarnings("deprecation")
public static void checkForSupportedProtocol(GeyserLogger logger, boolean viaversion) {
if (viaversion) {
checkViaVersionSupportedVersions(logger);
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/PaperAdventure.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/PaperAdventure.java
index 5dd16da33..fa7555ac6 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/PaperAdventure.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/PaperAdventure.java
@@ -25,11 +25,11 @@
package org.geysermc.geyser.platform.spigot;
-import com.github.steveice10.mc.protocol.data.DefaultComponentSerializer;
+import org.geysermc.mcprotocollib.protocol.data.DefaultComponentSerializer;
import net.kyori.adventure.text.Component;
import org.bukkit.command.CommandSender;
+import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.GeyserImpl;
-import org.jetbrains.annotations.Nullable;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
@@ -39,8 +39,8 @@ import java.lang.reflect.Method;
/**
* Utility class for converting our shaded Adventure into the Adventure bundled in Paper.
- *
- * Code mostly taken from https://github.com/KyoriPowered/adventure-platform/blob/94d5821f2e755170f42bd8a5fe1d5bf6f66d04ad/platform-bukkit/src/main/java/net/kyori/adventure/platform/bukkit/PaperFacet.java#L46
+ *
+ * Code mostly taken from here
* and the MinecraftReflection class.
*/
public final class PaperAdventure {
@@ -102,7 +102,7 @@ public final class PaperAdventure {
SEND_MESSAGE_COMPONENT = playerComponentSendMessage;
}
- public static Object toNativeComponent(final Component component) {
+ public static @Nullable Object toNativeComponent(final Component component) {
if (NATIVE_GSON_COMPONENT_SERIALIZER_DESERIALIZE_METHOD_BOUND == null) {
GeyserImpl.getInstance().getLogger().error("Illegal state where Component serialization was called when it wasn't available!");
return null;
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/ReflectedNames.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/ReflectedNames.java
index 67e31fea2..275fec657 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/ReflectedNames.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/ReflectedNames.java
@@ -29,8 +29,8 @@ import com.destroystokyo.paper.event.server.PaperServerListPingEvent;
import com.destroystokyo.paper.network.StatusClient;
import org.bukkit.event.server.ServerListPingEvent;
import org.bukkit.util.CachedServerIcon;
+import org.checkerframework.checker.nullness.qual.Nullable;
-import javax.annotation.Nullable;
import java.lang.reflect.Constructor;
import java.net.InetAddress;
@@ -40,15 +40,8 @@ import java.net.InetAddress;
public final class ReflectedNames {
static boolean checkPaperPingEvent() {
- return classExists("com.destroystokyo.paper.event.server.PaperServerListPingEvent");
- }
-
- /**
- * @return if this class name exists
- */
- private static boolean classExists(String clazz) {
try {
- Class.forName(clazz);
+ Class.forName("com.destroystokyo.paper.event.server.PaperServerListPingEvent");
return true;
} catch (ClassNotFoundException e) {
return false;
@@ -59,7 +52,7 @@ public final class ReflectedNames {
return getConstructor(ServerListPingEvent.class, InetAddress.class, String.class, boolean.class, int.class, int.class) != null;
}
- static Constructor getOldPaperPingConstructor() {
+ static @Nullable Constructor getOldPaperPingConstructor() {
if (getConstructor(PaperServerListPingEvent.class, StatusClient.class, String.class, int.class,
int.class, String.class, int.class, CachedServerIcon.class) != null) {
// @NotNull StatusClient client, @NotNull String motd, int numPlayers, int maxPlayers,
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 9375e3a62..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 {
-
- @EventHandler
- @SuppressWarnings("deprecation") // Used to indicate an unstable event
- 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 61d394214..000000000
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/command/GeyserSpigotCommandExecutor.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 org.bukkit.ChatColor;
-import org.bukkit.command.Command;
-import org.bukkit.command.CommandSender;
-import org.bukkit.command.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.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(CommandSender sender, Command command, 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(CommandSender sender, Command command, 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 95fba707f..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,16 +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());
@@ -48,10 +53,11 @@ public class SpigotCommandSource implements GeyserCommandSource {
}
@Override
- public void sendMessage(String message) {
+ public void sendMessage(@NonNull String message) {
handle.sendMessage(message);
}
+ @SuppressWarnings("deprecation")
@Override
public void sendMessage(Component message) {
if (PaperAdventure.canSendMessageUsingComponent()) {
@@ -63,14 +69,29 @@ 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() {
if (this.handle instanceof Player player) {
+ // getLocale() is deprecated on Paper, but not on Spigot
return player.getLocale();
}
@@ -79,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/GeyserPistonListener.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/GeyserPistonListener.java
index 01b0be9f2..963f5bac3 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/GeyserPistonListener.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/GeyserPistonListener.java
@@ -25,10 +25,8 @@
package org.geysermc.geyser.platform.spigot.world;
-import com.github.steveice10.mc.protocol.data.game.level.block.value.PistonValueType;
-import org.cloudburstmc.math.vector.Vector3i;
-import it.unimi.dsi.fastutil.objects.Object2IntArrayMap;
-import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap;
+import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
@@ -40,13 +38,17 @@ import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockPistonEvent;
import org.bukkit.event.block.BlockPistonExtendEvent;
import org.bukkit.event.block.BlockPistonRetractEvent;
+import org.cloudburstmc.math.vector.Vector3i;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.level.block.BlockStateValues;
+import org.geysermc.geyser.level.block.property.Properties;
+import org.geysermc.geyser.level.block.type.BlockState;
import org.geysermc.geyser.level.physics.Direction;
import org.geysermc.geyser.platform.spigot.world.manager.GeyserSpigotWorldManager;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.PistonCache;
import org.geysermc.geyser.translator.level.block.entity.PistonBlockEntity;
+import org.geysermc.mcprotocollib.protocol.data.game.level.block.value.PistonValueType;
import java.util.List;
import java.util.Map;
@@ -85,7 +87,7 @@ public class GeyserPistonListener implements Listener {
PistonValueType type = isExtend ? PistonValueType.PUSHING : PistonValueType.PULLING;
boolean sticky = event.isSticky();
- Object2IntMap attachedBlocks = new Object2IntArrayMap<>();
+ Object2ObjectMap attachedBlocks = new Object2ObjectArrayMap<>();
boolean blocksFilled = false;
for (Map.Entry entry : geyser.getSessionManager().getSessions().entrySet()) {
@@ -108,10 +110,10 @@ public class GeyserPistonListener implements Listener {
List blocks = isExtend ? ((BlockPistonExtendEvent) event).getBlocks() : ((BlockPistonRetractEvent) event).getBlocks();
for (Block block : blocks) {
Location attachedLocation = block.getLocation();
- int blockId = worldManager.getBlockNetworkId(block);
+ BlockState state = BlockState.of(worldManager.getBlockNetworkId(block));
// Ignore blocks that will be destroyed
- if (BlockStateValues.canPistonMoveBlock(blockId, isExtend)) {
- attachedBlocks.put(getVector(attachedLocation), blockId);
+ if (BlockStateValues.canPistonMoveBlock(state, isExtend)) {
+ attachedBlocks.put(getVector(attachedLocation), state);
}
}
blocksFilled = true;
@@ -119,7 +121,7 @@ public class GeyserPistonListener implements Listener {
int pistonBlockId = worldManager.getBlockNetworkId(event.getBlock());
// event.getDirection() is unreliable
- Direction orientation = BlockStateValues.getPistonOrientation(pistonBlockId);
+ Direction orientation = BlockState.of(pistonBlockId).getValue(Properties.FACING);
session.executeInEventLoop(() -> {
PistonCache pistonCache = session.getPistonCache();
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/GeyserSpigotBlockPlaceListener.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/GeyserSpigotBlockPlaceListener.java
index 71aba11f9..1cdd77c64 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/GeyserSpigotBlockPlaceListener.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/GeyserSpigotBlockPlaceListener.java
@@ -33,7 +33,7 @@ import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockPlaceEvent;
import org.geysermc.geyser.GeyserImpl;
-import org.geysermc.geyser.level.block.BlockStateValues;
+import org.geysermc.geyser.level.block.type.Block;
import org.geysermc.geyser.platform.spigot.world.manager.GeyserSpigotWorldManager;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.session.GeyserSession;
@@ -59,11 +59,11 @@ public class GeyserSpigotBlockPlaceListener implements Listener {
event.getBlockPlaced().getX(), event.getBlockPlaced().getY(), event.getBlockPlaced().getZ())));
} else {
String javaBlockId = event.getBlockPlaced().getBlockData().getAsString();
- placeBlockSoundPacket.setExtraData(session.getBlockMappings().getBedrockBlockId(BlockRegistries.JAVA_IDENTIFIER_TO_ID.get().getOrDefault(javaBlockId, BlockStateValues.JAVA_AIR_ID)));
+ placeBlockSoundPacket.setExtraData(session.getBlockMappings().getBedrockBlockId(BlockRegistries.JAVA_IDENTIFIER_TO_ID.get().getOrDefault(javaBlockId, Block.JAVA_AIR_ID)));
}
placeBlockSoundPacket.setIdentifier(":");
session.sendUpstreamPacket(placeBlockSoundPacket);
session.setLastBlockPlacePosition(null);
- session.setLastBlockPlacedId(null);
+ session.setLastBlockPlaced(null);
}
}
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotLegacyNativeWorldManager.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotLegacyNativeWorldManager.java
index baffc9679..fe2dda053 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotLegacyNativeWorldManager.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotLegacyNativeWorldManager.java
@@ -37,6 +37,7 @@ import org.geysermc.geyser.platform.spigot.GeyserSpigotPlugin;
import org.geysermc.geyser.session.GeyserSession;
import java.util.List;
+import java.util.Objects;
/**
* Used when block IDs need to be translated to the latest version
@@ -45,18 +46,19 @@ public class GeyserSpigotLegacyNativeWorldManager extends GeyserSpigotNativeWorl
private final Int2IntMap oldToNewBlockId;
- public GeyserSpigotLegacyNativeWorldManager(GeyserSpigotPlugin plugin) {
- super(plugin);
+ public GeyserSpigotLegacyNativeWorldManager(GeyserSpigotPlugin plugin, boolean isPaper) {
+ super(plugin, isPaper);
IntList allBlockStates = adapter.getAllBlockStates();
oldToNewBlockId = new Int2IntOpenHashMap(allBlockStates.size());
ProtocolVersion serverVersion = plugin.getServerProtocolVersion();
List protocolList = Via.getManager().getProtocolManager().getProtocolPath(GameProtocol.getJavaProtocolVersion(),
serverVersion.getVersion());
+ Objects.requireNonNull(protocolList, "protocolList cannot be null");
for (int oldBlockId : allBlockStates) {
int newBlockId = oldBlockId;
// protocolList should *not* be null; we checked for that before initializing this class
for (int i = protocolList.size() - 1; i >= 0; i--) {
- MappingData mappingData = protocolList.get(i).getProtocol().getMappingData();
+ MappingData mappingData = protocolList.get(i).protocol().getMappingData();
if (mappingData != null) {
newBlockId = mappingData.getNewBlockStateId(newBlockId);
}
diff --git a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotNativeWorldManager.java b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotNativeWorldManager.java
index 6b5d1ea1e..c99ca4e78 100644
--- a/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotNativeWorldManager.java
+++ b/bootstrap/spigot/src/main/java/org/geysermc/geyser/platform/spigot/world/manager/GeyserSpigotNativeWorldManager.java
@@ -26,27 +26,34 @@
package org.geysermc.geyser.platform.spigot.world.manager;
import org.bukkit.Bukkit;
+import org.bukkit.World;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.geysermc.geyser.adapters.WorldAdapter;
+import org.geysermc.geyser.adapters.paper.PaperAdapters;
import org.geysermc.geyser.adapters.spigot.SpigotAdapters;
-import org.geysermc.geyser.adapters.spigot.SpigotWorldAdapter;
import org.geysermc.geyser.level.block.BlockStateValues;
+import org.geysermc.geyser.level.block.type.Block;
import org.geysermc.geyser.session.GeyserSession;
-import org.jetbrains.annotations.Nullable;
public class GeyserSpigotNativeWorldManager extends GeyserSpigotWorldManager {
- protected final SpigotWorldAdapter adapter;
+ protected final WorldAdapter adapter;
- public GeyserSpigotNativeWorldManager(Plugin plugin) {
+ public GeyserSpigotNativeWorldManager(Plugin plugin, boolean isPaper) {
super(plugin);
- adapter = SpigotAdapters.getWorldAdapter();
+ if (isPaper) {
+ adapter = PaperAdapters.getWorldAdapter();
+ } else {
+ adapter = SpigotAdapters.getWorldAdapter();
+ }
}
@Override
public int getBlockAt(GeyserSession session, int x, int y, int z) {
Player player = Bukkit.getPlayer(session.getPlayerEntity().getUsername());
if (player == null) {
- return BlockStateValues.JAVA_AIR_ID;
+ return Block.JAVA_AIR_ID;
}
return adapter.getBlockAt(player.getWorld(), x, y, z);
}
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 c8ccfffd7..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
@@ -25,54 +25,52 @@
package org.geysermc.geyser.platform.spigot.world.manager;
-import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
-import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityInfo;
-import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import org.bukkit.Bukkit;
-import org.bukkit.Chunk;
import org.bukkit.World;
import org.bukkit.block.Block;
+import org.bukkit.block.DecoratedPot;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
-import org.cloudburstmc.nbt.NbtMap;
-import org.geysermc.erosion.bukkit.BukkitLecterns;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.cloudburstmc.math.vector.Vector3i;
import org.geysermc.erosion.bukkit.BukkitUtils;
import org.geysermc.erosion.bukkit.PickBlockUtils;
import org.geysermc.erosion.bukkit.SchedulerUtils;
+import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.level.GameRule;
import org.geysermc.geyser.level.WorldManager;
-import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.session.GeyserSession;
-import org.geysermc.geyser.util.BlockEntityUtils;
-import org.jetbrains.annotations.Nullable;
+import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode;
+import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents;
-import javax.annotation.Nonnull;
import java.util.List;
+import java.util.Objects;
import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
/**
* The base world manager to use when there is no supported NMS revision
*/
public class GeyserSpigotWorldManager extends WorldManager {
private final Plugin plugin;
- private final BukkitLecterns lecterns;
public GeyserSpigotWorldManager(Plugin plugin) {
this.plugin = plugin;
- this.lecterns = new BukkitLecterns(plugin);
}
@Override
public int getBlockAt(GeyserSession session, int x, int y, int z) {
Player bukkitPlayer;
if ((bukkitPlayer = Bukkit.getPlayer(session.getPlayerEntity().getUsername())) == null) {
- return BlockStateValues.JAVA_AIR_ID;
+ return org.geysermc.geyser.level.block.type.Block.JAVA_AIR_ID;
}
World world = bukkitPlayer.getWorld();
if (!world.isChunkLoaded(x >> 4, z >> 4)) {
// If the chunk isn't loaded, how could we even be here?
- return BlockStateValues.JAVA_AIR_ID;
+ return org.geysermc.geyser.level.block.type.Block.JAVA_AIR_ID;
}
return getBlockNetworkId(world.getBlockAt(x, y, z));
@@ -83,9 +81,9 @@ public class GeyserSpigotWorldManager extends WorldManager {
// Terrible behavior, but this is basically what's always been happening behind the scenes anyway.
CompletableFuture blockData = new CompletableFuture<>();
Bukkit.getRegionScheduler().execute(this.plugin, block.getLocation(), () -> blockData.complete(block.getBlockData().getAsString()));
- return BlockRegistries.JAVA_IDENTIFIER_TO_ID.getOrDefault(blockData.join(), BlockStateValues.JAVA_AIR_ID);
+ return BlockRegistries.JAVA_IDENTIFIER_TO_ID.getOrDefault(blockData.join(), org.geysermc.geyser.level.block.type.Block.JAVA_AIR_ID);
}
- return BlockRegistries.JAVA_IDENTIFIER_TO_ID.getOrDefault(block.getBlockData().getAsString(), BlockStateValues.JAVA_AIR_ID);
+ return BlockRegistries.JAVA_IDENTIFIER_TO_ID.getOrDefault(block.getBlockData().getAsString(), org.geysermc.geyser.level.block.type.Block.JAVA_AIR_ID); // TODO could just make this a BlockState lookup?
}
@Override
@@ -93,82 +91,35 @@ public class GeyserSpigotWorldManager extends WorldManager {
return true;
}
- @Override
- public void sendLecternData(GeyserSession session, int x, int y, int z) {
- Player bukkitPlayer;
- if ((bukkitPlayer = Bukkit.getPlayer(session.getPlayerEntity().getUsername())) == null) {
- return;
- }
-
- Block block = bukkitPlayer.getWorld().getBlockAt(x, y, z);
- // Run as a task to prevent async issues
- SchedulerUtils.runTask(this.plugin, () -> sendLecternData(session, block, false), block);
- }
-
- public void sendLecternData(GeyserSession session, int x, int z, List blockEntityInfos) {
- Player bukkitPlayer;
- if ((bukkitPlayer = Bukkit.getPlayer(session.getPlayerEntity().getUsername())) == null) {
- return;
- }
- if (SchedulerUtils.FOLIA) {
- Chunk chunk = getChunk(bukkitPlayer.getWorld(), x, z);
- if (chunk == null) {
- return;
- }
- Bukkit.getRegionScheduler().execute(this.plugin, bukkitPlayer.getWorld(), x, z, () ->
- sendLecternData(session, chunk, blockEntityInfos));
- } else {
- Bukkit.getScheduler().runTask(this.plugin, () -> {
- Chunk chunk = getChunk(bukkitPlayer.getWorld(), x, z);
- if (chunk == null) {
- return;
- }
- sendLecternData(session, chunk, blockEntityInfos);
- });
- }
- }
-
- private Chunk getChunk(World world, int x, int z) {
- if (!world.isChunkLoaded(x, z)) {
- return null;
- }
- return world.getChunkAt(x, z);
- }
-
- private void sendLecternData(GeyserSession session, Chunk chunk, List blockEntityInfos) {
- for (int i = 0; i < blockEntityInfos.size(); i++) {
- BlockEntityInfo info = blockEntityInfos.get(i);
- Block block = chunk.getBlock(info.getX(), info.getY(), info.getZ());
- sendLecternData(session, block, true);
- }
- }
-
- private void sendLecternData(GeyserSession session, Block block, boolean isChunkLoad) {
- NbtMap blockEntityTag = this.lecterns.getLecternData(block, isChunkLoad);
- if (blockEntityTag != null) {
- BlockEntityUtils.updateBlockEntity(session, blockEntityTag, BukkitUtils.getVector(block.getLocation()));
- }
- }
-
- @Override
- public boolean shouldExpectLecternHandled(GeyserSession session) {
- return true;
- }
-
public boolean getGameRuleBool(GeyserSession session, GameRule gameRule) {
- String value = Bukkit.getPlayer(session.getPlayerEntity().getUsername()).getWorld().getGameRuleValue(gameRule.getJavaID());
- if (!value.isEmpty()) {
- return Boolean.parseBoolean(value);
+ org.bukkit.GameRule> bukkitGameRule = org.bukkit.GameRule.getByName(gameRule.getJavaID());
+ if (bukkitGameRule == null) {
+ GeyserImpl.getInstance().getLogger().debug("Unknown game rule " + gameRule.getJavaID());
+ return gameRule.getDefaultBooleanValue();
}
+
+ Player bukkitPlayer = Objects.requireNonNull(Bukkit.getPlayer(session.getPlayerEntity().getUuid()));
+ Object value = bukkitPlayer.getWorld().getGameRuleValue(bukkitGameRule);
+ if (value instanceof Boolean booleanValue) {
+ return booleanValue;
+ }
+ GeyserImpl.getInstance().getLogger().debug("Expected a bool for " + gameRule + " but got " + value);
return gameRule.getDefaultBooleanValue();
}
@Override
public int getGameRuleInt(GeyserSession session, GameRule gameRule) {
- String value = Bukkit.getPlayer(session.getPlayerEntity().getUsername()).getWorld().getGameRuleValue(gameRule.getJavaID());
- if (!value.isEmpty()) {
- return Integer.parseInt(value);
+ org.bukkit.GameRule> bukkitGameRule = org.bukkit.GameRule.getByName(gameRule.getJavaID());
+ if (bukkitGameRule == null) {
+ GeyserImpl.getInstance().getLogger().debug("Unknown game rule " + gameRule.getJavaID());
+ return gameRule.getDefaultIntValue();
}
+ Player bukkitPlayer = Objects.requireNonNull(Bukkit.getPlayer(session.getPlayerEntity().getUuid()));
+ Object value = bukkitPlayer.getWorld().getGameRuleValue(bukkitGameRule);
+ if (value instanceof Integer intValue) {
+ return intValue;
+ }
+ GeyserImpl.getInstance().getLogger().debug("Expected an int for " + gameRule + " but got " + value);
return gameRule.getDefaultIntValue();
}
@@ -178,24 +129,32 @@ public class GeyserSpigotWorldManager extends WorldManager {
}
@Override
- public boolean hasPermission(GeyserSession session, String permission) {
- return Bukkit.getPlayer(session.getPlayerEntity().getUsername()).hasPermission(permission);
- }
-
- @Nonnull
- @Override
- public CompletableFuture<@Nullable CompoundTag> getPickItemNbt(GeyserSession session, int x, int y, int z, boolean addNbtData) {
- CompletableFuture<@Nullable CompoundTag> future = new CompletableFuture<>();
+ public @NonNull CompletableFuture<@Nullable DataComponents> getPickItemComponents(GeyserSession session, int x, int y, int z, boolean addNbtData) {
Player bukkitPlayer;
if ((bukkitPlayer = Bukkit.getPlayer(session.getPlayerEntity().getUuid())) == null) {
- future.complete(null);
- return future;
+ return CompletableFuture.completedFuture(null);
}
+ CompletableFuture> future = new CompletableFuture<>();
Block block = bukkitPlayer.getWorld().getBlockAt(x, y, z);
// Paper 1.19.3 complains about async access otherwise.
// java.lang.IllegalStateException: Tile is null, asynchronous access?
SchedulerUtils.runTask(this.plugin, () -> future.complete(PickBlockUtils.pickBlock(block)), block);
- return future;
+ return future.thenApply(RAW_TRANSFORMER);
+ }
+
+ public void getDecoratedPotData(GeyserSession session, Vector3i pos, Consumer> apply) {
+ Player bukkitPlayer;
+ if ((bukkitPlayer = Bukkit.getPlayer(session.getPlayerEntity().getUuid())) == null) {
+ return;
+ }
+ Block block = bukkitPlayer.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ());
+ SchedulerUtils.runTask(this.plugin, () -> {
+ var state = BukkitUtils.getBlockState(block);
+ if (!(state instanceof DecoratedPot pot)) {
+ return;
+ }
+ apply.accept(pot.getShards().stream().map(material -> material.getKey().toString()).toList());
+ }, block);
}
/**
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..fd81dad63 100644
--- a/bootstrap/standalone/build.gradle.kts
+++ b/bootstrap/standalone/build.gradle.kts
@@ -1,5 +1,9 @@
import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer
+plugins {
+ application
+}
+
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 b505b361e..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
@@ -38,10 +38,12 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.Logger;
import org.apache.logging.log4j.core.appender.ConsoleAppender;
-import org.geysermc.geyser.api.util.PlatformType;
+import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.GeyserBootstrap;
import org.geysermc.geyser.GeyserImpl;
-import org.geysermc.geyser.command.GeyserCommandManager;
+import org.geysermc.geyser.api.util.PlatformType;
+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;
@@ -51,7 +53,6 @@ import org.geysermc.geyser.platform.standalone.gui.GeyserStandaloneGUI;
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;
@@ -59,20 +60,25 @@ import java.lang.reflect.Method;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.MessageFormat;
-import java.util.*;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
import java.util.stream.Collectors;
public class GeyserStandaloneBootstrap implements GeyserBootstrap {
- private GeyserCommandManager geyserCommandManager;
+ private StandaloneCloudCommandManager cloud;
+ private CommandRegistry commandRegistry;
private GeyserStandaloneConfiguration geyserConfig;
- private GeyserStandaloneLogger geyserLogger;
+ private final GeyserStandaloneLogger geyserLogger = new GeyserStandaloneLogger();
private IGeyserPingPassthrough geyserPingPassthrough;
-
private GeyserStandaloneGUI gui;
-
@Getter
private boolean useGui = System.console() == null && !isHeadless();
+ private Logger log4jLogger;
private String configFilename = "config.yml";
private GeyserImpl geyser;
@@ -161,28 +167,22 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap {
}
}
}
- bootstrap.onEnable(useGuiOpts, configFilenameOpt);
- }
-
- public void onEnable(boolean useGui, String configFilename) {
- this.configFilename = configFilename;
- this.useGui = useGui;
- this.onEnable();
+ bootstrap.useGui = useGuiOpts;
+ bootstrap.configFilename = configFilenameOpt;
+ bootstrap.onGeyserInitialize();
}
@Override
- public void onEnable() {
- Logger logger = (Logger) LogManager.getRootLogger();
- for (Appender appender : logger.getAppenders().values()) {
+ public void onGeyserInitialize() {
+ log4jLogger = (Logger) LogManager.getRootLogger();
+ for (Appender appender : log4jLogger.getAppenders().values()) {
// Remove the appender that is not in use
// Prevents multiple appenders/double logging and removes harmless errors
if ((useGui && appender instanceof TerminalConsoleAppender) || (!useGui && appender instanceof ConsoleAppender)) {
- logger.removeAppender(appender);
+ log4jLogger.removeAppender(appender);
}
}
- this.geyserLogger = new GeyserStandaloneLogger();
-
if (useGui && gui == null) {
gui = new GeyserStandaloneGUI(geyserLogger);
gui.redirectSystemStreams();
@@ -190,7 +190,12 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap {
}
LoopbackUtil.checkAndApplyLoopback(geyserLogger);
-
+
+ this.onGeyserEnable();
+ }
+
+ @Override
+ public void onGeyserEnable() {
try {
File configFile = FileUtils.fileOrCopiedFromResource(new File(configFilename), "config.yml",
(x) -> x.replaceAll("generateduuid", UUID.randomUUID().toString()), this);
@@ -215,16 +220,28 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap {
GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger);
// Allow libraries like Protocol to have their debug information passthrough
- logger.get().setLevel(geyserConfig.isDebugMode() ? Level.DEBUG : Level.INFO);
+ log4jLogger.get().setLevel(geyserConfig.isDebugMode() ? Level.DEBUG : Level.INFO);
geyser = GeyserImpl.load(PlatformType.STANDALONE, this);
+
+ 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();
- geyserCommandManager = new GeyserCommandManager(geyser);
- geyserCommandManager.init();
+ 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);
@@ -250,7 +267,12 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap {
}
@Override
- public void onDisable() {
+ public void onGeyserDisable() {
+ geyser.disable();
+ }
+
+ @Override
+ public void onGeyserShutdown() {
geyser.shutdown();
System.exit(0);
}
@@ -266,8 +288,8 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap {
}
@Override
- public GeyserCommandManager getGeyserCommandManager() {
- return geyserCommandManager;
+ public CommandRegistry getCommandRegistry() {
+ return commandRegistry;
}
@Override
@@ -292,7 +314,7 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap {
return new GeyserStandaloneDumpInfo(this);
}
- @NotNull
+ @NonNull
@Override
public String getServerBindAddress() {
throw new IllegalStateException();
@@ -325,7 +347,7 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap {
// Get the ignored properties
Set ignoredProperties = OBJECT_MAPPER.getSerializationConfig().getAnnotationIntrospector()
- .findPropertyIgnorals(beanDescription.getClassInfo()).getIgnored();
+ .findPropertyIgnoralByName(OBJECT_MAPPER.getSerializationConfig() ,beanDescription.getClassInfo()).getIgnored();
// Filter properties removing the ignored ones
return properties.stream()
@@ -340,7 +362,7 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap {
* @param parentObject The object to alter
* @param value The new value of the property
*/
- @SuppressWarnings("unchecked") // Required for enum usage
+ @SuppressWarnings({"unchecked", "rawtypes"}) // Required for enum usage
private static void setConfigOption(BeanPropertyDefinition property, Object parentObject, Object value) {
Object parsedValue = value;
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 3c29bc648..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,12 +44,14 @@ 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
protected void shutdown() {
- GeyserImpl.getInstance().getBootstrap().onDisable();
+ GeyserImpl.getInstance().getBootstrap().onGeyserShutdown();
}
@Override
diff --git a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/ColorPane.java b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/ColorPane.java
index 1bdc90123..8e0eadb08 100644
--- a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/ColorPane.java
+++ b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/ColorPane.java
@@ -28,11 +28,16 @@ package org.geysermc.geyser.platform.standalone.gui;
import javax.swing.*;
import javax.swing.text.*;
import java.awt.*;
+import java.io.Serial;
/**
- * This class was based on this code: https://stackoverflow.com/a/6899478/5299903
+ * This class was based on this code
*/
public class ColorPane extends JTextPane {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
private static Color colorCurrent = ANSIColor.RESET.getColor();
private String remaining = "";
@@ -62,7 +67,7 @@ public class ColorPane extends JTextPane {
int aPos = 0; // current char position in addString
int aIndex; // index of next Escape sequence
int mIndex; // index of "m" terminating Escape sequence
- String tmpString = "";
+ String tmpString;
boolean stillSearching = true; // true until no more Escape sequences
String addString = remaining + s;
remaining = "";
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 af3e1069f..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
@@ -25,9 +25,10 @@
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;
@@ -64,7 +65,6 @@ public class GeyserStandaloneGUI {
private final List ramValues = new ArrayList<>();
private final DefaultTableModel playerTableModel = new DefaultTableModel();
- private final JTable playerTable = new JTable(playerTableModel);
/**
* Create and show the Geyser-Standalone GUI
@@ -100,7 +100,7 @@ public class GeyserStandaloneGUI {
Container cp = frame.getContentPane();
// Fetch and set the icon for the frame
- URL image = getClass().getClassLoader().getResource("icon.png");
+ URL image = getClass().getClassLoader().getResource("assets/geyser/icon.png");
if (image != null) {
ImageIcon icon = new ImageIcon(image);
frame.setIconImage(icon.getImage());
@@ -158,6 +158,7 @@ public class GeyserStandaloneGUI {
playerTableModel.addColumn(GeyserLocale.getLocaleStringLog("geyser.gui.table.ip"));
playerTableModel.addColumn(GeyserLocale.getLocaleStringLog("geyser.gui.table.username"));
+ JTable playerTable = new JTable(playerTableModel);
JScrollPane playerScrollPane = new JScrollPane(playerTable);
rightContentPane.add(playerScrollPane);
@@ -253,12 +254,12 @@ public class GeyserStandaloneGUI {
}
@Override
- public void write(byte[] b, int off, int len) {
+ public void write(byte @NonNull [] b, int off, int len) {
appendConsole(new String(b, off, len));
}
@Override
- public void write(byte[] b) {
+ public void write(byte @NonNull[] b) {
write(b, 0, b.length);
}
};
@@ -270,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();
}
@@ -343,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/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/GraphPanel.java b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/GraphPanel.java
index d8fca3e1b..063824218 100644
--- a/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/GraphPanel.java
+++ b/bootstrap/standalone/src/main/java/org/geysermc/geyser/platform/standalone/gui/GraphPanel.java
@@ -29,15 +29,20 @@ import lombok.Setter;
import javax.swing.*;
import java.awt.*;
+import java.io.Serial;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* This has been modified to fit Geyser more but is based on
- * https://gist.github.com/roooodcastro/6325153#gistcomment-3107524
+ * this Github gist
*/
public final class GraphPanel extends JPanel {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
private final static int padding = 10;
private final static int labelPadding = 25;
private final static int pointWidth = 4;
@@ -103,7 +108,7 @@ public final class GraphPanel extends JPanel {
g.drawLine(padding + labelPadding + 1 + pointWidth, y, width - padding, y);
g.setColor(Color.BLACK);
- final int tickValue = (int) (minScore + ((scoreRange * i) / numberYDivisions));
+ final int tickValue = minScore + ((scoreRange * i) / numberYDivisions);
final String yLabel = tickValue + "";
final int labelWidth = fontMetrics.stringWidth(yLabel);
g.drawString(yLabel, x1 - labelWidth - 5, y + (fontHeight / 2) - 3);
diff --git a/bootstrap/velocity/build.gradle.kts b/bootstrap/velocity/build.gradle.kts
index 8908b2afd..93e0c9c93 100644
--- a/bootstrap/velocity/build.gradle.kts
+++ b/bootstrap/velocity/build.gradle.kts
@@ -1,15 +1,22 @@
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.*:*")
-// Needed because Velocity provides every dependency except netty-resolver-dns
+// Needed because Velocity provides every dependency except netty-resolver-dns
+exclude("io.netty.incubator:.*")
exclude("io.netty:netty-transport-native-epoll:*")
exclude("io.netty:netty-transport-native-unix-common:*")
exclude("io.netty:netty-transport-native-kqueue:*")
@@ -34,8 +41,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 {
@@ -54,6 +61,7 @@ tasks.withType {
exclude(dependency("io.netty:netty-transport:.*"))
exclude(dependency("io.netty:netty-codec:.*"))
exclude(dependency("io.netty:netty-codec-haproxy:.*"))
+ exclude(dependency("io.netty.incubator:.*"))
exclude(dependency("org.slf4j:.*"))
exclude(dependency("org.ow2.asm:.*"))
// Exclude all Kyori dependencies except the legacy NBT serializer
@@ -64,4 +72,9 @@ tasks.withType {
exclude(dependency("net.kyori:adventure-text-serializer-legacy:.*"))
exclude(dependency("net.kyori:adventure-nbt:.*"))
}
-}
\ No newline at end of file
+}
+
+modrinth {
+ uploadFile.set(tasks.getByPath("shadowJar"))
+ loaders.addAll("velocity")
+}
diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityCompressionDisabler.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityCompressionDisabler.java
index e787e7355..99c759767 100644
--- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityCompressionDisabler.java
+++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityCompressionDisabler.java
@@ -47,8 +47,14 @@ public class GeyserVelocityCompressionDisabler extends ChannelDuplexHandler {
Method setCompressionMethod = null;
try {
- compressionPacketClass = Class.forName("com.velocitypowered.proxy.protocol.packet.SetCompression");
- loginSuccessPacketClass = Class.forName("com.velocitypowered.proxy.protocol.packet.ServerLoginSuccess");
+ try {
+ compressionPacketClass = Class.forName("com.velocitypowered.proxy.protocol.packet.SetCompressionPacket");
+ loginSuccessPacketClass = Class.forName("com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket");
+ } catch (Exception ignored) {
+ // Velocity renamed packet classes in https://github.com/PaperMC/Velocity/commit/2ac8751337befd04f4663575f5d752c748384110
+ compressionPacketClass = Class.forName("com.velocitypowered.proxy.protocol.packet.SetCompression");
+ loginSuccessPacketClass = Class.forName("com.velocitypowered.proxy.protocol.packet.ServerLoginSuccess");
+ }
compressionEnabledEvent = Class.forName("com.velocitypowered.proxy.protocol.VelocityConnectionEvent")
.getDeclaredField("COMPRESSION_ENABLED").get(null);
setCompressionMethod = Class.forName("com.velocitypowered.proxy.connection.MinecraftConnection")
diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityInjector.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityInjector.java
index 4ffb286b8..68a9eb40b 100644
--- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityInjector.java
+++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityInjector.java
@@ -29,6 +29,7 @@ import com.velocitypowered.api.proxy.ProxyServer;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.local.LocalAddress;
+import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.GeyserBootstrap;
import org.geysermc.geyser.network.netty.GeyserInjector;
import org.geysermc.geyser.network.netty.LocalServerChannelWrapper;
@@ -76,7 +77,7 @@ public class GeyserVelocityInjector extends GeyserInjector {
.channel(LocalServerChannelWrapper.class)
.childHandler(new ChannelInitializer<>() {
@Override
- protected void initChannel(Channel ch) throws Exception {
+ protected void initChannel(@NonNull Channel ch) throws Exception {
initChannel.invoke(channelInitializer, ch);
if (bootstrap.getGeyserConfig().isDisableCompression() && GeyserVelocityCompressionDisabler.ENABLED) {
diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityLogger.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityLogger.java
index 567870e7f..4d10e4daf 100644
--- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityLogger.java
+++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityLogger.java
@@ -25,13 +25,13 @@
package org.geysermc.geyser.platform.velocity;
-import lombok.AllArgsConstructor;
import lombok.Getter;
+import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.geysermc.geyser.GeyserLogger;
import org.slf4j.Logger;
-@AllArgsConstructor
+@RequiredArgsConstructor
public class GeyserVelocityLogger implements GeyserLogger {
private final Logger logger;
@Getter @Setter
diff --git a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPingPassthrough.java b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPingPassthrough.java
index 1a9b9bf26..b2258d3a3 100644
--- a/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPingPassthrough.java
+++ b/bootstrap/velocity/src/main/java/org/geysermc/geyser/platform/velocity/GeyserVelocityPingPassthrough.java
@@ -26,6 +26,7 @@
package org.geysermc.geyser.platform.velocity;
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
+import com.velocitypowered.api.network.ProtocolState;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.InboundConnection;
import com.velocitypowered.api.proxy.ProxyServer;
@@ -54,19 +55,11 @@ public class GeyserVelocityPingPassthrough implements IGeyserPingPassthrough {
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
}
- GeyserPingInfo geyserPingInfo = new GeyserPingInfo(
+ return new GeyserPingInfo(
LegacyComponentSerializer.legacy('§').serialize(event.getPing().getDescriptionComponent()),
- new GeyserPingInfo.Players(
- event.getPing().getPlayers().orElseThrow(IllegalStateException::new).getMax(),
- event.getPing().getPlayers().orElseThrow(IllegalStateException::new).getOnline()
- ),
- new GeyserPingInfo.Version(
- event.getPing().getVersion().getName(),
- event.getPing().getVersion().getProtocol()
- )
+ event.getPing().getPlayers().map(ServerPing.Players::getMax).orElse(1),
+ event.getPing().getPlayers().map(ServerPing.Players::getOnline).orElse(0)
);
- event.getPing().getPlayers().get().getSample().stream().map(ServerPing.SamplePlayer::getName).forEach(geyserPingInfo.getPlayerList()::add);
- return geyserPingInfo;
}
private static class GeyserInboundConnection implements InboundConnection {
@@ -96,6 +89,11 @@ public class GeyserVelocityPingPassthrough implements IGeyserPingPassthrough {
public ProtocolVersion getProtocolVersion() {
return ProtocolVersion.MAXIMUM_VERSION;
}
+
+ @Override
+ public ProtocolState getProtocolState() {
+ return ProtocolState.STATUS;
+ }
}
}
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 aac27fb65..868cdbf8e 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,31 +26,36 @@
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;
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 net.kyori.adventure.util.Codec;
-import org.geysermc.geyser.api.util.PlatformType;
+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.command.GeyserCommandManager;
+import org.geysermc.geyser.api.util.PlatformType;
+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.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
+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;
@@ -58,104 +63,101 @@ 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 {
- @Inject
- private Logger logger;
-
- @Inject
- private ProxyServer proxyServer;
-
- @Inject
- private CommandManager commandManager;
-
- private GeyserCommandManager geyserCommandManager;
+ private final ProxyServer proxyServer;
+ private final PluginContainer container;
+ private final GeyserVelocityLogger geyserLogger;
private GeyserVelocityConfiguration geyserConfig;
private GeyserVelocityInjector geyserInjector;
- private GeyserVelocityLogger geyserLogger;
private IGeyserPingPassthrough geyserPingPassthrough;
-
+ private CommandRegistry commandRegistry;
private GeyserImpl geyser;
@Getter
private final Path configFolder = Paths.get("plugins/" + GeyserImpl.NAME + "-Velocity/");
- @Override
- public void onEnable() {
- try {
- Codec.class.getMethod("codec", Codec.Decoder.class, Codec.Encoder.class);
- } catch (NoSuchMethodException e) {
- // velocitypowered.com has a build that is very outdated
- logger.error("Please download Velocity from https://papermc.io/downloads#Velocity - the 'stable' Velocity version " +
- "that has likely been downloaded is very outdated and does not support 1.19.");
- return;
- }
+ @Inject
+ public GeyserVelocityPlugin(ProxyServer server, PluginContainer container, Logger logger) {
+ this.proxyServer = server;
+ this.container = container;
+ this.geyserLogger = new GeyserVelocityLogger(logger);
+ }
+ @Override
+ public void onGeyserInitialize() {
GeyserLocale.init(this);
- try {
- if (!configFolder.toFile().exists())
- //noinspection ResultOfMethodCallIgnored
- configFolder.toFile().mkdirs();
- File configFile = FileUtils.fileOrCopiedFromResource(configFolder.resolve("config.yml").toFile(),
- "config.yml", (x) -> x.replaceAll("generateduuid", UUID.randomUUID().toString()), this);
- this.geyserConfig = FileUtils.loadConfig(configFile, GeyserVelocityConfiguration.class);
- } catch (IOException ex) {
- logger.error(GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex);
- ex.printStackTrace();
- return;
+ if (!ProtocolVersion.isSupported(GameProtocol.getJavaProtocolVersion())) {
+ geyserLogger.error(" / \\");
+ geyserLogger.error(" / \\");
+ geyserLogger.error(" / | \\");
+ geyserLogger.error(" / | \\ " + GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_proxy", proxyServer.getVersion().getName()));
+ geyserLogger.error(" / \\ " + GeyserLocale.getLocaleStringLog("geyser.may_not_work_as_intended_all_caps"));
+ geyserLogger.error(" / o \\");
+ geyserLogger.error("/_____________\\");
}
- this.geyserLogger = new GeyserVelocityLogger(logger, geyserConfig.isDebugMode());
+ if (!loadConfig()) {
+ return;
+ }
+ this.geyserLogger.setDebug(geyserConfig.isDebugMode());
GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger);
this.geyser = GeyserImpl.load(PlatformType.VELOCITY, this);
-
- // Remove this in like a year
- try {
- // Should only exist on 1.0
- Class.forName("org.geysermc.floodgate.FloodgateAPI");
- geyserLogger.severe(GeyserLocale.getLocaleStringLog("geyser.bootstrap.floodgate.outdated",
- "https://ci.opencollab.dev/job/GeyserMC/job/Floodgate/job/master/"));
- return;
- } catch (ClassNotFoundException ignored) {
- }
+ this.geyserInjector = new GeyserVelocityInjector(proxyServer);
}
- private void postStartup() {
- GeyserImpl.start();
-
- this.geyserInjector = new GeyserVelocityInjector(proxyServer);
- // Will be initialized after the proxy has been bound
-
- this.geyserCommandManager = new GeyserCommandManager(geyser);
- this.geyserCommandManager.init();
-
- 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;
+ @Override
+ public void onGeyserEnable() {
+ if (GeyserImpl.getInstance().isReloading()) {
+ if (!loadConfig()) {
+ return;
}
-
- this.commandManager.register(entry.getKey().description().id(), new GeyserVelocityCommandExecutor(this.geyser, commands));
+ this.geyserLogger.setDebug(geyserConfig.isDebugMode());
+ GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger);
+ } else {
+ 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
}
+ GeyserImpl.start();
+
if (geyserConfig.isLegacyPingPassthrough()) {
this.geyserPingPassthrough = GeyserLegacyPingPassthrough.init(geyser);
} else {
this.geyserPingPassthrough = new GeyserVelocityPingPassthrough(proxyServer);
}
- proxyServer.getEventManager().register(this, new GeyserVelocityUpdateListener());
+ // No need to re-register events
+ if (!GeyserImpl.getInstance().isReloading()) {
+ proxyServer.getEventManager().register(this, new GeyserVelocityUpdateListener());
+ }
}
@Override
- public void onDisable() {
+ public void onGeyserDisable() {
+ if (geyser != null) {
+ geyser.disable();
+ }
+ }
+
+ @Override
+ public void onGeyserShutdown() {
if (geyser != null) {
geyser.shutdown();
}
@@ -175,8 +177,8 @@ public class GeyserVelocityPlugin implements GeyserBootstrap {
}
@Override
- public GeyserCommandManager getGeyserCommandManager() {
- return this.geyserCommandManager;
+ public CommandRegistry getCommandRegistry() {
+ return this.commandRegistry;
}
@Override
@@ -186,19 +188,19 @@ public class GeyserVelocityPlugin implements GeyserBootstrap {
@Subscribe
public void onInit(ProxyInitializeEvent event) {
- onEnable();
+ this.onGeyserInitialize();
}
@Subscribe
public void onShutdown(ProxyShutdownEvent event) {
- onDisable();
+ this.onGeyserShutdown();
}
@Subscribe
public void onProxyBound(ListenerBoundEvent event) {
if (event.getListenerType() == ListenerType.MINECRAFT) {
// Once listener is bound, do our startup process
- this.postStartup();
+ this.onGeyserEnable();
if (geyserInjector != null) {
// After this bound, we know that the channel initializer cannot change without it being ineffective for Velocity, too
@@ -218,7 +220,7 @@ public class GeyserVelocityPlugin implements GeyserBootstrap {
return this.geyserInjector.getServerSocketAddress();
}
- @NotNull
+ @NonNull
@Override
public String getServerBindAddress() {
return proxyServer.getBoundAddress().getHostString();
@@ -238,4 +240,21 @@ public class GeyserVelocityPlugin implements GeyserBootstrap {
}
return false;
}
+
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ private boolean loadConfig() {
+ try {
+ if (!configFolder.toFile().exists())
+ //noinspection ResultOfMethodCallIgnored
+ configFolder.toFile().mkdirs();
+ File configFile = FileUtils.fileOrCopiedFromResource(configFolder.resolve("config.yml").toFile(),
+ "config.yml", (x) -> x.replaceAll("generateduuid", UUID.randomUUID().toString()), this);
+ this.geyserConfig = FileUtils.loadConfig(configFile, GeyserVelocityConfiguration.class);
+ } catch (IOException ex) {
+ geyserLogger.error(GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex);
+ ex.printStackTrace();
+ return false;
+ }
+ return true;
+ }
}
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 00c99e92b..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
@@ -30,10 +30,13 @@ import com.velocitypowered.api.proxy.ConsoleCommandSource;
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 {
@@ -56,7 +59,7 @@ public class VelocityCommandSource implements GeyserCommandSource {
}
@Override
- public void sendMessage(String message) {
+ public void sendMessage(@NonNull String message) {
handle.sendMessage(LegacyComponentSerializer.legacy('§').deserialize(message));
}
@@ -71,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) {
@@ -82,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
new file mode 100644
index 000000000..254787743
--- /dev/null
+++ b/bootstrap/viaproxy/build.gradle.kts
@@ -0,0 +1,31 @@
+dependencies {
+ api(projects.core)
+
+ compileOnlyApi(libs.viaproxy)
+}
+
+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)
+
+tasks.withType {
+ manifest.attributes["Main-Class"] = "org.geysermc.geyser.platform.viaproxy.GeyserViaProxyMain"
+}
+
+tasks.withType {
+ archiveBaseName.set("Geyser-ViaProxy")
+
+ dependencies {
+ exclude(dependency("com.google.*:.*"))
+ exclude(dependency("io.netty:.*"))
+ exclude(dependency("io.netty.incubator:.*"))
+ exclude(dependency("org.slf4j:.*"))
+ exclude(dependency("org.ow2.asm:.*"))
+ }
+}
diff --git a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyConfiguration.java b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyConfiguration.java
new file mode 100644
index 000000000..afc46fa6a
--- /dev/null
+++ b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyConfiguration.java
@@ -0,0 +1,67 @@
+/*
+ * 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.viaproxy;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import net.raphimc.vialegacy.api.LegacyProtocolVersion;
+import net.raphimc.viaproxy.ViaProxy;
+import net.raphimc.viaproxy.protocoltranslator.viaproxy.ViaProxyConfig;
+import org.geysermc.geyser.configuration.GeyserJacksonConfiguration;
+
+import java.io.File;
+import java.nio.file.Path;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+@SuppressWarnings("FieldMayBeFinal") // Jackson requires that the fields are not final
+public class GeyserViaProxyConfiguration extends GeyserJacksonConfiguration {
+
+ private RemoteConfiguration remote = new RemoteConfiguration() {
+ @Override
+ public boolean isForwardHost() {
+ return super.isForwardHost() || !ViaProxy.getConfig().getWildcardDomainHandling().equals(ViaProxyConfig.WildcardDomainHandling.NONE);
+ }
+ };
+
+ @Override
+ public Path getFloodgateKeyPath() {
+ return new File(GeyserViaProxyPlugin.ROOT_FOLDER, this.getFloodgateKeyFile()).toPath();
+ }
+
+ @Override
+ public int getPingPassthroughInterval() {
+ int interval = super.getPingPassthroughInterval();
+ if (interval < 15 && ViaProxy.getConfig().getTargetVersion() != null && ViaProxy.getConfig().getTargetVersion().olderThanOrEqualTo(LegacyProtocolVersion.r1_6_4)) {
+ // <= 1.6.4 servers sometimes block incoming connections from an IP address if too many connections are made
+ interval = 15;
+ }
+ return interval;
+ }
+
+ @Override
+ public RemoteConfiguration getRemote() {
+ return this.remote;
+ }
+
+}
diff --git a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyDumpInfo.java b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyDumpInfo.java
new file mode 100644
index 000000000..0bfc9d022
--- /dev/null
+++ b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyDumpInfo.java
@@ -0,0 +1,66 @@
+/*
+ * 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.viaproxy;
+
+import lombok.Getter;
+import net.raphimc.viaproxy.ViaProxy;
+import net.raphimc.viaproxy.plugins.ViaProxyPlugin;
+import org.geysermc.geyser.dump.BootstrapDumpInfo;
+import org.geysermc.geyser.text.AsteriskSerializer;
+
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+@Getter
+public class GeyserViaProxyDumpInfo extends BootstrapDumpInfo {
+
+ private final String platformVersion;
+ private final boolean onlineMode;
+
+ @AsteriskSerializer.Asterisk(isIp = true)
+ private final String serverIP;
+ private final int serverPort;
+ private final List plugins;
+
+ public GeyserViaProxyDumpInfo() {
+ this.platformVersion = ViaProxy.VERSION;
+ this.onlineMode = ViaProxy.getConfig().isProxyOnlineMode();
+ if (ViaProxy.getConfig().getBindAddress() instanceof InetSocketAddress inetSocketAddress) {
+ this.serverIP = inetSocketAddress.getHostString();
+ this.serverPort = inetSocketAddress.getPort();
+ } else {
+ this.serverIP = "unsupported";
+ this.serverPort = 0;
+ }
+ this.plugins = new ArrayList<>();
+
+ for (ViaProxyPlugin plugin : ViaProxy.getPluginManager().getPlugins()) {
+ this.plugins.add(new PluginInfo(true, plugin.getName(), plugin.getVersion(), "unknown", Collections.singletonList(plugin.getAuthor())));
+ }
+ }
+
+}
diff --git a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyLogger.java b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyLogger.java
new file mode 100644
index 000000000..10f414b51
--- /dev/null
+++ b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyLogger.java
@@ -0,0 +1,88 @@
+/*
+ * 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.viaproxy;
+
+import net.raphimc.viaproxy.cli.ConsoleFormatter;
+import org.apache.logging.log4j.Logger;
+import org.geysermc.geyser.GeyserLogger;
+import org.geysermc.geyser.command.GeyserCommandSource;
+
+public class GeyserViaProxyLogger implements GeyserLogger, GeyserCommandSource {
+
+ private final Logger logger;
+ private boolean debug;
+
+ public GeyserViaProxyLogger(Logger logger) {
+ this.logger = logger;
+ }
+
+ @Override
+ public void severe(String message) {
+ this.logger.fatal(ConsoleFormatter.convert(message));
+ }
+
+ @Override
+ public void severe(String message, Throwable error) {
+ this.logger.fatal(ConsoleFormatter.convert(message), error);
+ }
+
+ @Override
+ public void error(String message) {
+ this.logger.error(ConsoleFormatter.convert(message));
+ }
+
+ @Override
+ public void error(String message, Throwable error) {
+ this.logger.error(ConsoleFormatter.convert(message), error);
+ }
+
+ @Override
+ public void warning(String message) {
+ this.logger.warn(ConsoleFormatter.convert(message));
+ }
+
+ @Override
+ public void info(String message) {
+ this.logger.info(ConsoleFormatter.convert(message));
+ }
+
+ @Override
+ public void debug(String message) {
+ if (this.debug) {
+ this.logger.debug(ConsoleFormatter.convert(message));
+ }
+ }
+
+ @Override
+ public void setDebug(boolean debug) {
+ this.debug = debug;
+ }
+
+ @Override
+ public boolean isDebug() {
+ return this.debug;
+ }
+
+}
diff --git a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyMain.java b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyMain.java
new file mode 100644
index 000000000..582a97d78
--- /dev/null
+++ b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyMain.java
@@ -0,0 +1,44 @@
+/*
+ * 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.viaproxy;
+
+import org.geysermc.geyser.GeyserMain;
+
+public class GeyserViaProxyMain extends GeyserMain {
+
+ public static void main(String[] args) {
+ new GeyserViaProxyMain().displayMessage();
+ }
+
+ public String getPluginType() {
+ return "ViaProxy";
+ }
+
+ public String getPluginFolder() {
+ return "plugins";
+ }
+
+}
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
new file mode 100644
index 000000000..1eed778f2
--- /dev/null
+++ b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/GeyserViaProxyPlugin.java
@@ -0,0 +1,239 @@
+/*
+ * 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.viaproxy;
+
+import net.lenni0451.lambdaevents.EventHandler;
+import net.raphimc.vialegacy.api.LegacyProtocolVersion;
+import net.raphimc.viaproxy.ViaProxy;
+import net.raphimc.viaproxy.plugins.PluginManager;
+import net.raphimc.viaproxy.plugins.ViaProxyPlugin;
+import net.raphimc.viaproxy.plugins.events.ConsoleCommandEvent;
+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.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;
+import org.geysermc.geyser.ping.IGeyserPingPassthrough;
+import org.geysermc.geyser.platform.viaproxy.listener.GeyserServerTransferListener;
+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 java.io.File;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.UUID;
+
+public class GeyserViaProxyPlugin extends ViaProxyPlugin implements GeyserBootstrap, EventRegistrar {
+
+ public static final File ROOT_FOLDER = new File(PluginManager.PLUGINS_DIR, "Geyser");
+
+ private final GeyserViaProxyLogger logger = new GeyserViaProxyLogger(LogManager.getLogger("Geyser"));
+ private GeyserViaProxyConfiguration config;
+ private GeyserImpl geyser;
+ private StandaloneCloudCommandManager cloud;
+ private CommandRegistry commandRegistry;
+ private IGeyserPingPassthrough pingPassthrough;
+
+ @Override
+ public void onEnable() {
+ ROOT_FOLDER.mkdirs();
+
+ GeyserLocale.init(this);
+ this.onGeyserInitialize();
+
+ ViaProxy.EVENT_MANAGER.register(this);
+ }
+
+ @Override
+ public void onDisable() {
+ this.onGeyserShutdown();
+ }
+
+ @EventHandler
+ private void onConsoleCommand(final ConsoleCommandEvent event) {
+ final String command = event.getCommand().startsWith("/") ? event.getCommand().substring(1) : event.getCommand();
+ CommandRegistry registry = this.getCommandRegistry();
+ if (registry.rootCommands().contains(command)) {
+ registry.runCommand(this.getGeyserLogger(), command + " " + String.join(" ", event.getArgs()));
+ event.setCancelled(true);
+ }
+ }
+
+ @EventHandler
+ private void onShouldVerifyOnlineModeEvent(final ShouldVerifyOnlineModeEvent event) {
+ final UUID uuid = event.getProxyConnection().getGameProfile().getId();
+ if (uuid == null) return;
+
+ final GeyserSession connection = GeyserImpl.getInstance().onlineConnections().stream().filter(s -> s.javaUuid().equals(uuid)).findAny().orElse(null);
+ if (connection == null) return;
+
+ if (connection.javaUsername().equals(event.getProxyConnection().getGameProfile().getName())) {
+ event.setCancelled(true);
+ }
+ }
+
+ @EventHandler
+ private void onProxyStart(final ProxyStartEvent event) {
+ this.onGeyserEnable();
+ }
+
+ @EventHandler
+ private void onProxyStop(final ProxyStopEvent event) {
+ this.onGeyserDisable();
+ }
+
+ @Override
+ public void onGeyserInitialize() {
+ if (!this.loadConfig()) {
+ return;
+ }
+
+ this.geyser = GeyserImpl.load(PlatformType.VIAPROXY, this);
+ this.geyser.eventBus().register(this, new GeyserServerTransferListener());
+ LoopbackUtil.checkAndApplyLoopback(this.logger);
+ }
+
+ @Override
+ public void onGeyserEnable() {
+ 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);
+ }
+
+ 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);
+ }
+ }
+
+ @Override
+ public void onGeyserDisable() {
+ this.geyser.disable();
+ }
+
+ @Override
+ public void onGeyserShutdown() {
+ this.geyser.shutdown();
+ }
+
+ @Override
+ public GeyserConfiguration getGeyserConfig() {
+ return this.config;
+ }
+
+ @Override
+ public GeyserLogger getGeyserLogger() {
+ return this.logger;
+ }
+
+ @Override
+ public CommandRegistry getCommandRegistry() {
+ return this.commandRegistry;
+ }
+
+ @Override
+ public IGeyserPingPassthrough getGeyserPingPassthrough() {
+ return this.pingPassthrough;
+ }
+
+ @Override
+ public Path getConfigFolder() {
+ return ROOT_FOLDER.toPath();
+ }
+
+ @Override
+ public BootstrapDumpInfo getDumpInfo() {
+ return new GeyserViaProxyDumpInfo();
+ }
+
+ @NonNull
+ @Override
+ public String getServerBindAddress() {
+ if (ViaProxy.getConfig().getBindAddress() instanceof InetSocketAddress socketAddress) {
+ return socketAddress.getHostString();
+ } else {
+ throw new IllegalStateException("Unsupported bind address type: " + ViaProxy.getConfig().getBindAddress().getClass().getName());
+ }
+ }
+
+ @Override
+ public int getServerPort() {
+ if (ViaProxy.getConfig().getBindAddress() instanceof InetSocketAddress socketAddress) {
+ return socketAddress.getPort();
+ } else {
+ throw new IllegalStateException("Unsupported bind address type: " + ViaProxy.getConfig().getBindAddress().getClass().getName());
+ }
+ }
+
+ @Override
+ public boolean testFloodgatePluginPresent() {
+ 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);
+ this.config = FileUtils.loadConfig(configFile, GeyserViaProxyConfiguration.class);
+ } catch (IOException e) {
+ this.logger.severe(GeyserLocale.getLocaleStringLog("geyser.config.failed"), e);
+ return false;
+ }
+ this.config.getRemote().setAuthType(Files.isRegularFile(this.config.getFloodgateKeyPath()) ? AuthType.FLOODGATE : AuthType.OFFLINE);
+ this.logger.setDebug(this.config.isDebugMode());
+ GeyserConfiguration.checkGeyserConfiguration(this.config, this.logger);
+ return true;
+ }
+
+}
diff --git a/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/listener/GeyserServerTransferListener.java b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/listener/GeyserServerTransferListener.java
new file mode 100644
index 000000000..64b3cc56e
--- /dev/null
+++ b/bootstrap/viaproxy/src/main/java/org/geysermc/geyser/platform/viaproxy/listener/GeyserServerTransferListener.java
@@ -0,0 +1,62 @@
+/*
+ * 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.viaproxy.listener;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.net.HostAndPort;
+import org.geysermc.event.PostOrder;
+import org.geysermc.event.subscribe.Subscribe;
+import org.geysermc.geyser.api.event.bedrock.SessionLoginEvent;
+import org.geysermc.geyser.api.event.java.ServerTransferEvent;
+import org.geysermc.geyser.session.GeyserSession;
+
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+public class GeyserServerTransferListener {
+
+ private final Cache> cookieStorages = CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES).build();
+
+ @Subscribe(postOrder = PostOrder.FIRST)
+ private void onServerTransfer(final ServerTransferEvent event) {
+ this.cookieStorages.put(event.connection().xuid(), event.cookies());
+ final GeyserSession geyserSession = (GeyserSession) event.connection();
+ final HostAndPort hostAndPort = HostAndPort.fromString(geyserSession.getClientData().getServerAddress()).withDefaultPort(19132);
+ event.bedrockHost(hostAndPort.getHost());
+ event.bedrockPort(hostAndPort.getPort());
+ }
+
+ @Subscribe(postOrder = PostOrder.FIRST)
+ private void onSessionLogin(final SessionLoginEvent event) {
+ final Map cookies = this.cookieStorages.asMap().remove(event.connection().xuid());
+ if (cookies != null) {
+ event.cookies(cookies);
+ event.transferring(true);
+ }
+ }
+
+}
diff --git a/bootstrap/viaproxy/src/main/resources/viaproxy.yml b/bootstrap/viaproxy/src/main/resources/viaproxy.yml
new file mode 100644
index 000000000..66fbdb932
--- /dev/null
+++ b/bootstrap/viaproxy/src/main/resources/viaproxy.yml
@@ -0,0 +1,5 @@
+name: "${name}-ViaProxy"
+version: "${version}"
+author: "${author}"
+main: "org.geysermc.geyser.platform.viaproxy.GeyserViaProxyPlugin"
+min-version: "3.2.1"
diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts
index c0c683920..190386667 100644
--- a/build-logic/build.gradle.kts
+++ b/build-logic/build.gradle.kts
@@ -4,14 +4,20 @@ plugins {
repositories {
gradlePluginPortal()
- maven("https://repo.opencollab.dev/maven-snapshots")
+
+ maven("https://repo.opencollab.dev/maven-snapshots/")
+ maven("https://maven.fabricmc.net/")
+ maven("https://maven.neoforged.net/releases")
+ maven("https://maven.architectury.dev/")
}
dependencies {
- implementation("net.kyori", "indra-common", "3.1.1")
- implementation("com.github.johnrengelman", "shadow", "7.1.3-SNAPSHOT")
-
- // Within the gradle plugin classpath, there is a version conflict between loom and some other
- // plugin for databind. This fixes it: minimum 2.13.2 is required by loom.
- implementation("com.fasterxml.jackson.core:jackson-databind:2.14.0")
+ // 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))
+ implementation(libs.indra)
+ implementation(libs.shadow)
+ implementation(libs.architectury.plugin)
+ implementation(libs.architectury.loom)
+ implementation(libs.minotaur)
}
diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts
new file mode 100644
index 000000000..63bde189b
--- /dev/null
+++ b/build-logic/settings.gradle.kts
@@ -0,0 +1,11 @@
+@file:Suppress("UnstableApiUsage")
+
+dependencyResolutionManagement {
+ versionCatalogs {
+ create("libs") {
+ from(files("../gradle/libs.versions.toml"))
+ }
+ }
+}
+
+rootProject.name = "build-logic"
\ No newline at end of file
diff --git a/build-logic/src/main/kotlin/LibsAccessor.kt b/build-logic/src/main/kotlin/LibsAccessor.kt
new file mode 100644
index 000000000..2a0c09eb6
--- /dev/null
+++ b/build-logic/src/main/kotlin/LibsAccessor.kt
@@ -0,0 +1,6 @@
+import org.gradle.accessors.dm.LibrariesForLibs
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.getByType
+
+val Project.libs: LibrariesForLibs
+ get() = rootProject.extensions.getByType()
\ No newline at end of file
diff --git a/build-logic/src/main/kotlin/extensions.kt b/build-logic/src/main/kotlin/extensions.kt
index 1e1732852..41e11344b 100644
--- a/build-logic/src/main/kotlin/extensions.kt
+++ b/build-logic/src/main/kotlin/extensions.kt
@@ -24,11 +24,17 @@
*/
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
+import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.artifacts.MinimalExternalModuleDependency
import org.gradle.api.artifacts.ProjectDependency
import org.gradle.api.provider.Provider
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.options.Option
+import org.gradle.api.tasks.TaskAction
import org.gradle.kotlin.dsl.named
+import java.io.File
+import java.net.URL
fun Project.relocate(pattern: String) {
tasks.named("shadowJar") {
@@ -52,22 +58,63 @@ fun Project.platformRelocate(pattern: String, exclusion: String = "") {
val providedDependencies = mutableMapOf>()
-fun Project.provided(pattern: String, name: String, version: String, excludedOn: Int = 0b110) {
+fun getProvidedDependenciesForProject(projectName: String): MutableSet {
+ return providedDependencies.getOrDefault(projectName, emptySet()).toMutableSet()
+}
+
+fun Project.provided(pattern: String, name: String, excludedOn: Int = 0b110) {
providedDependencies.getOrPut(project.name) { mutableSetOf() }
- .add("${calcExclusion(pattern, 0b100, excludedOn)}:" +
- "${calcExclusion(name, 0b10, excludedOn)}:" +
- calcExclusion(version, 0b1, excludedOn))
- dependencies.add("compileOnlyApi", "$pattern:$name:$version")
+ .add("${calcExclusion(pattern, 0b100, excludedOn)}:${calcExclusion(name, 0b10, excludedOn)}")
}
fun Project.provided(dependency: ProjectDependency) =
- provided(dependency.group!!, dependency.name, dependency.version!!)
+ provided(dependency.group!!, dependency.name)
fun Project.provided(dependency: MinimalExternalModuleDependency) =
- provided(dependency.module.group, dependency.module.name, dependency.versionConstraint.requiredVersion)
+ provided(dependency.module.group, dependency.module.name)
fun Project.provided(provider: Provider) =
provided(provider.get())
+open class DownloadFilesTask : DefaultTask() {
+ @Input
+ var urls: List = listOf()
+
+ @Input
+ var destinationDir: String = ""
+
+ @Option(option="suffix", description="suffix")
+ @Input
+ var suffix: String = ""
+
+ @Input
+ var suffixedFiles: List = listOf()
+
+ @TaskAction
+ fun downloadAndAddSuffix() {
+ urls.forEach { fileUrl ->
+ val fileName = fileUrl.substringAfterLast("/")
+ val baseName = fileName.substringBeforeLast(".")
+ val extension = fileName.substringAfterLast(".", "")
+ val shouldSuffix = fileName in suffixedFiles
+ val suffixedFileName = if (shouldSuffix && extension.isNotEmpty()) "$baseName.$suffix.$extension" else fileName
+ val outputFile = File(destinationDir, suffixedFileName)
+
+ if (!outputFile.parentFile.exists()) {
+ outputFile.parentFile.mkdirs()
+ }
+
+ URL(fileUrl).openStream().use { input ->
+ outputFile.outputStream().use { output ->
+ input.copyTo(output)
+ }
+ }
+
+ println("Downloaded: $suffixedFileName")
+ }
+ }
+}
+
private fun calcExclusion(section: String, bit: Int, excludedOn: Int): String =
- if (excludedOn and bit > 0) section else ""
\ No newline at end of file
+ if (excludedOn and bit > 0) section else ""
+
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 95d4d5409..950c0184b 100644
--- a/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts
+++ b/build-logic/src/main/kotlin/geyser.base-conventions.gradle.kts
@@ -16,14 +16,14 @@ indra {
mitLicense()
javaVersions {
- target(16)
+ target(17)
}
}
tasks {
processResources {
- // Spigot, BungeeCord, Velocity, Fabric
- filesMatching(listOf("plugin.yml", "bungee.yml", "velocity-plugin.json", "fabric.mod.json")) {
+ // 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",
diff --git a/build-logic/src/main/kotlin/geyser.modded-conventions.gradle.kts b/build-logic/src/main/kotlin/geyser.modded-conventions.gradle.kts
new file mode 100644
index 000000000..20d14c443
--- /dev/null
+++ b/build-logic/src/main/kotlin/geyser.modded-conventions.gradle.kts
@@ -0,0 +1,127 @@
+@file:Suppress("UnstableApiUsage")
+
+import net.fabricmc.loom.task.RemapJarTask
+import org.gradle.kotlin.dsl.dependencies
+import org.gradle.kotlin.dsl.maven
+
+plugins {
+ id("geyser.publish-conventions")
+ id("architectury-plugin")
+ id("dev.architectury.loom")
+}
+
+// These are provided by Minecraft/modded platforms already, no need to include them
+provided("com.google.code.gson", "gson")
+provided("com.google.guava", ".*")
+provided("org.slf4j", "slf4j-api")
+provided("com.nukkitx.fastutil", ".*")
+provided("org.cloudburstmc.fastutil.maps", ".*")
+provided("org.cloudburstmc.fastutil.sets", ".*")
+provided("org.cloudburstmc.fastutil.commons", ".*")
+provided("org.cloudburstmc.fastutil", ".*")
+provided("org.checkerframework", "checker-qual")
+provided("io.netty", "netty-transport-classes-epoll")
+provided("io.netty", "netty-transport-native-epoll")
+provided("io.netty", "netty-transport-native-unix-common")
+provided("io.netty", "netty-transport-classes-kqueue")
+provided("io.netty", "netty-transport-native-kqueue")
+provided("io.netty.incubator", "netty-incubator-transport-native-io_uring")
+provided("io.netty.incubator", "netty-incubator-transport-classes-io_uring")
+provided("io.netty", "netty-handler")
+provided("io.netty", "netty-common")
+provided("io.netty", "netty-buffer")
+provided("io.netty", "netty-resolver")
+provided("io.netty", "netty-transport")
+provided("io.netty", "netty-codec")
+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
+}
+
+loom {
+ silentMojangMappingsLicense()
+}
+
+indra {
+ javaVersions {
+ target(21)
+ }
+}
+
+configurations {
+ create("includeTransitive").isTransitive = true
+}
+
+tasks {
+ // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task
+ // if it is present.
+ // If you remove this task, sources will not be generated.
+ sourcesJar {
+ archiveClassifier.set("sources")
+ from(sourceSets.main.get().allSource)
+ }
+
+ shadowJar {
+ // Mirrors the example fabric project, otherwise tons of dependencies are shaded that shouldn't be
+ configurations = listOf(project.configurations.shadow.get())
+ // The remapped shadowJar is the final desired mod jar
+ archiveVersion.set(project.version.toString())
+ archiveClassifier.set("shaded")
+ }
+
+ remapJar {
+ dependsOn(shadowJar)
+ inputFile.set(shadowJar.get().archiveFile)
+ archiveClassifier.set("")
+ archiveVersion.set("")
+ }
+
+ register("remapModrinthJar", RemapJarTask::class) {
+ dependsOn(shadowJar)
+ inputFile.set(shadowJar.get().archiveFile)
+ archiveVersion.set(project.version.toString() + "+build." + System.getenv("BUILD_NUMBER"))
+ archiveClassifier.set("")
+ }
+}
+
+afterEvaluate {
+ val providedDependencies = getProvidedDependenciesForProject(project.name)
+
+ // These are shaded, no need to JiJ them
+ configurations["shadow"].dependencies.forEach {shadowed ->
+ //println("Not including shadowed dependency: ${shadowed.group}:${shadowed.name}")
+ providedDependencies.add("${shadowed.group}:${shadowed.name}")
+ }
+
+ // Now: Include all transitive dependencies that aren't excluded
+ configurations["includeTransitive"].resolvedConfiguration.resolvedArtifacts.forEach { dep ->
+ if (!providedDependencies.contains("${dep.moduleVersion.id.group}:${dep.moduleVersion.id.name}")
+ and !providedDependencies.contains("${dep.moduleVersion.id.group}:.*")) {
+ //println("Including dependency via JiJ: ${dep.id}")
+ dependencies.add("include", dep.moduleVersion.id.toString())
+ } else {
+ //println("Not including ${dep.id} for ${project.name}!")
+ }
+ }
+}
+
+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")
+}
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
new file mode 100644
index 000000000..d710ae1a2
--- /dev/null
+++ b/build-logic/src/main/kotlin/geyser.modrinth-uploading-conventions.gradle.kts
@@ -0,0 +1,18 @@
+plugins {
+ id("com.modrinth.minotaur")
+}
+
+// Ensure that the readme is synched
+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"))
+ versionType.set("beta")
+ changelog.set(System.getenv("CHANGELOG") ?: "")
+ gameVersions.add(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..410e67404 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,3 @@
plugins {
- application
id("geyser.publish-conventions")
}
\ No newline at end of file
diff --git a/build-logic/src/main/kotlin/geyser.publish-conventions.gradle.kts b/build-logic/src/main/kotlin/geyser.publish-conventions.gradle.kts
index 036ee803c..eca587721 100644
--- a/build-logic/src/main/kotlin/geyser.publish-conventions.gradle.kts
+++ b/build-logic/src/main/kotlin/geyser.publish-conventions.gradle.kts
@@ -7,3 +7,9 @@ indra {
publishSnapshotsTo("geysermc", "https://repo.opencollab.dev/maven-snapshots")
publishReleasesTo("geysermc", "https://repo.opencollab.dev/maven-releases")
}
+
+publishing {
+ // skip shadow jar from publishing. Workaround for https://github.com/johnrengelman/shadow/issues/651
+ val javaComponent = project.components["java"] as AdhocComponentWithVariants
+ javaComponent.withVariantsFromConfiguration(configurations["shadowRuntimeElements"]) { skip() }
+}
\ No newline at end of file
diff --git a/build-logic/src/main/kotlin/geyser.shadow-conventions.gradle.kts b/build-logic/src/main/kotlin/geyser.shadow-conventions.gradle.kts
index dde85c33a..c160e5ec6 100644
--- a/build-logic/src/main/kotlin/geyser.shadow-conventions.gradle.kts
+++ b/build-logic/src/main/kotlin/geyser.shadow-conventions.gradle.kts
@@ -7,7 +7,6 @@ plugins {
tasks {
named("jar") {
- archiveClassifier.set("unshaded")
from(project.rootProject.file("LICENSE"))
}
val shadowJar = named("shadowJar") {
diff --git a/build.gradle.kts b/build.gradle.kts
index 9eb8a6ed0..dfbf9837f 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,7 +1,9 @@
plugins {
`java-library`
+ // Ensure AP works in eclipse (no effect on other IDEs)
+ eclipse
id("geyser.build-logic")
- id("io.freefair.lombok") version "6.3.0" apply false
+ alias(libs.plugins.lombok) apply false
}
allprojects {
@@ -10,17 +12,25 @@ allprojects {
description = properties["description"] as String
}
-java {
- toolchain {
- languageVersion.set(JavaLanguageVersion.of(16))
- }
-}
-
-val platforms = setOf(
- projects.fabric,
+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 }
@@ -32,7 +42,14 @@ subprojects {
}
when (this) {
- in platforms -> plugins.apply("geyser.platform-conventions")
+ 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/src/main/java/org/geysermc/floodgate/crypto/AesKeyProducer.java b/common/src/main/java/org/geysermc/floodgate/crypto/AesKeyProducer.java
index 77dd46fde..e39d5119a 100644
--- a/common/src/main/java/org/geysermc/floodgate/crypto/AesKeyProducer.java
+++ b/common/src/main/java/org/geysermc/floodgate/crypto/AesKeyProducer.java
@@ -32,7 +32,7 @@ import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
public final class AesKeyProducer implements KeyProducer {
- public static int KEY_SIZE = 128;
+ public static final int KEY_SIZE = 128;
@Override
public SecretKey produce() {
@@ -55,19 +55,20 @@ public final class AesKeyProducer implements KeyProducer {
}
private SecureRandom secureRandom() throws NoSuchAlgorithmException {
- // use Windows-PRNG for windows (default impl is SHA1PRNG)
- if (System.getProperty("os.name").startsWith("Windows")) {
- return SecureRandom.getInstance("Windows-PRNG");
- } else {
- try {
+ try {
+ // use Windows-PRNG for windows (default impl is SHA1PRNG)
+ if (System.getProperty("os.name").startsWith("Windows")) {
+ return SecureRandom.getInstance("Windows-PRNG");
+ } else {
// NativePRNG (which should be the default on unix-systems) can still block your
// system. Even though it isn't as bad as NativePRNGBlocking, we still try to
// prevent that if possible
return SecureRandom.getInstance("NativePRNGNonBlocking");
- } catch (NoSuchAlgorithmException ignored) {
- // at this point we just have to go with the default impl even if it blocks
- return new SecureRandom();
+
}
+ } catch (NoSuchAlgorithmException ignored) {
+ // Fall back to the default impl as we couldn't load any others
+ return new SecureRandom();
}
}
}
diff --git a/common/src/main/java/org/geysermc/floodgate/crypto/FloodgateCipher.java b/common/src/main/java/org/geysermc/floodgate/crypto/FloodgateCipher.java
index 2f7b442f4..d3001f374 100644
--- a/common/src/main/java/org/geysermc/floodgate/crypto/FloodgateCipher.java
+++ b/common/src/main/java/org/geysermc/floodgate/crypto/FloodgateCipher.java
@@ -25,6 +25,7 @@
package org.geysermc.floodgate.crypto;
+import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.floodgate.util.InvalidFormatException;
import java.security.Key;
@@ -97,7 +98,8 @@ public interface FloodgateCipher {
* @return the decrypted data in a UTF-8 String
* @throws Exception when the decrypting failed
*/
- default String decryptToString(byte[] data) throws Exception {
+ @SuppressWarnings("unused")
+ default @Nullable String decryptToString(byte[] data) throws Exception {
byte[] decrypted = decrypt(data);
if (decrypted == null) {
return null;
@@ -113,6 +115,7 @@ public interface FloodgateCipher {
* @return the decrypted data in a byte[]
* @throws Exception when the decrypting failed
*/
+ @SuppressWarnings("unused")
default byte[] decryptFromString(String data) throws Exception {
return decrypt(data.getBytes(UTF_8));
}
diff --git a/common/src/main/java/org/geysermc/floodgate/crypto/KeyProducer.java b/common/src/main/java/org/geysermc/floodgate/crypto/KeyProducer.java
index 4ee00f366..0b8679dad 100644
--- a/common/src/main/java/org/geysermc/floodgate/crypto/KeyProducer.java
+++ b/common/src/main/java/org/geysermc/floodgate/crypto/KeyProducer.java
@@ -31,7 +31,7 @@ import java.nio.file.Path;
import java.security.Key;
public interface KeyProducer {
- Key produce();
+ @SuppressWarnings("unused") Key produce();
Key produceFrom(byte[] keyFileData);
default Key produceFrom(Path keyFileLocation) throws IOException {
diff --git a/common/src/main/java/org/geysermc/floodgate/news/NewsItem.java b/common/src/main/java/org/geysermc/floodgate/news/NewsItem.java
index 8ae28f422..f0dd3fadb 100644
--- a/common/src/main/java/org/geysermc/floodgate/news/NewsItem.java
+++ b/common/src/main/java/org/geysermc/floodgate/news/NewsItem.java
@@ -109,6 +109,7 @@ public final class NewsItem {
return (T) data;
}
+ @SuppressWarnings("unused")
public String getRawMessage() {
return message;
}
diff --git a/common/src/main/java/org/geysermc/floodgate/news/NewsItemAction.java b/common/src/main/java/org/geysermc/floodgate/news/NewsItemAction.java
index 00e34b622..31995e25d 100644
--- a/common/src/main/java/org/geysermc/floodgate/news/NewsItemAction.java
+++ b/common/src/main/java/org/geysermc/floodgate/news/NewsItemAction.java
@@ -25,6 +25,8 @@
package org.geysermc.floodgate.news;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
public enum NewsItemAction {
ON_SERVER_STARTED,
ON_OPERATOR_JOIN,
@@ -33,7 +35,7 @@ public enum NewsItemAction {
private static final NewsItemAction[] VALUES = values();
- public static NewsItemAction getByName(String actionName) {
+ public static @Nullable NewsItemAction getByName(String actionName) {
for (NewsItemAction type : VALUES) {
if (type.name().equalsIgnoreCase(actionName)) {
return type;
diff --git a/common/src/main/java/org/geysermc/floodgate/news/NewsItemMessage.java b/common/src/main/java/org/geysermc/floodgate/news/NewsItemMessage.java
index 9c2f3d15d..ed5d8f949 100644
--- a/common/src/main/java/org/geysermc/floodgate/news/NewsItemMessage.java
+++ b/common/src/main/java/org/geysermc/floodgate/news/NewsItemMessage.java
@@ -26,6 +26,7 @@
package org.geysermc.floodgate.news;
import com.google.gson.JsonArray;
+import org.checkerframework.checker.nullness.qual.Nullable;
// {} is used for things that have to be filled in by the server,
// {@} is for things that have to be filled in by us
@@ -49,7 +50,7 @@ public enum NewsItemMessage {
this.messageSplitted = messageFormat.split(" ");
}
- public static NewsItemMessage getById(int id) {
+ public static @Nullable NewsItemMessage getById(int id) {
return VALUES.length > id ? VALUES[id] : null;
}
diff --git a/common/src/main/java/org/geysermc/floodgate/news/NewsType.java b/common/src/main/java/org/geysermc/floodgate/news/NewsType.java
index ed7c1553b..af1d8bc38 100644
--- a/common/src/main/java/org/geysermc/floodgate/news/NewsType.java
+++ b/common/src/main/java/org/geysermc/floodgate/news/NewsType.java
@@ -26,6 +26,7 @@
package org.geysermc.floodgate.news;
import com.google.gson.JsonObject;
+import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.floodgate.news.data.*;
import java.util.function.Function;
@@ -44,7 +45,7 @@ public enum NewsType {
this.readFunction = readFunction;
}
- public static NewsType getByName(String newsType) {
+ public static @Nullable NewsType getByName(String newsType) {
for (NewsType type : VALUES) {
if (type.name().equalsIgnoreCase(newsType)) {
return type;
diff --git a/common/src/main/java/org/geysermc/floodgate/news/data/BuildSpecificData.java b/common/src/main/java/org/geysermc/floodgate/news/data/BuildSpecificData.java
index 7f2c7360f..c987dcfbc 100644
--- a/common/src/main/java/org/geysermc/floodgate/news/data/BuildSpecificData.java
+++ b/common/src/main/java/org/geysermc/floodgate/news/data/BuildSpecificData.java
@@ -56,6 +56,7 @@ public final class BuildSpecificData implements ItemData {
(allAffected || buildId > affectedGreaterThan && buildId < affectedLessThan);
}
+ @SuppressWarnings("unused")
public String getBranch() {
return branch;
}
diff --git a/common/src/main/java/org/geysermc/floodgate/news/data/ConfigSpecificData.java b/common/src/main/java/org/geysermc/floodgate/news/data/ConfigSpecificData.java
index 1479d20a1..6993fe91d 100644
--- a/common/src/main/java/org/geysermc/floodgate/news/data/ConfigSpecificData.java
+++ b/common/src/main/java/org/geysermc/floodgate/news/data/ConfigSpecificData.java
@@ -51,6 +51,7 @@ public final class ConfigSpecificData implements ItemData {
return configSpecificData;
}
+ @SuppressWarnings("unused")
public boolean isAffected(Map config) {
for (Map.Entry entry : affectedKeys.entrySet()) {
if (config.containsKey(entry.getKey())) {
diff --git a/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java b/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java
index 95fc62229..f0ed6ea00 100644
--- a/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java
+++ b/common/src/main/java/org/geysermc/floodgate/util/BedrockData.java
@@ -72,6 +72,7 @@ public final class BedrockData implements Cloneable {
false, subscribeId, verifyCode);
}
+ @SuppressWarnings("unused")
public static BedrockData fromString(String data) {
String[] split = data.split("\0");
if (split.length != EXPECTED_LENGTH) {
@@ -92,6 +93,7 @@ public final class BedrockData implements Cloneable {
dataLength);
}
+ @SuppressWarnings("unused")
public boolean hasPlayerLink() {
return linkedPlayer != null;
}
diff --git a/common/src/main/java/org/geysermc/floodgate/util/InvalidFormatException.java b/common/src/main/java/org/geysermc/floodgate/util/InvalidFormatException.java
index e0630d48c..a2526700e 100644
--- a/common/src/main/java/org/geysermc/floodgate/util/InvalidFormatException.java
+++ b/common/src/main/java/org/geysermc/floodgate/util/InvalidFormatException.java
@@ -26,6 +26,9 @@
package org.geysermc.floodgate.util;
public class InvalidFormatException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
public InvalidFormatException(String message) {
super(message);
}
diff --git a/common/src/main/java/org/geysermc/floodgate/util/LinkedPlayer.java b/common/src/main/java/org/geysermc/floodgate/util/LinkedPlayer.java
index 681080a30..0a131071e 100644
--- a/common/src/main/java/org/geysermc/floodgate/util/LinkedPlayer.java
+++ b/common/src/main/java/org/geysermc/floodgate/util/LinkedPlayer.java
@@ -28,6 +28,7 @@ package org.geysermc.floodgate.util;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
+import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.UUID;
@@ -56,7 +57,7 @@ public final class LinkedPlayer implements Cloneable {
return new LinkedPlayer(javaUsername, javaUniqueId, bedrockId);
}
- public static LinkedPlayer fromString(String data) {
+ public static @Nullable LinkedPlayer fromString(String data) {
String[] split = data.split(";");
if (split.length != 3) {
return null;
diff --git a/common/src/main/java/org/geysermc/floodgate/util/UiProfile.java b/common/src/main/java/org/geysermc/floodgate/util/UiProfile.java
index d93042277..4438ae67f 100644
--- a/common/src/main/java/org/geysermc/floodgate/util/UiProfile.java
+++ b/common/src/main/java/org/geysermc/floodgate/util/UiProfile.java
@@ -25,6 +25,8 @@
package org.geysermc.floodgate.util;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
public enum UiProfile {
CLASSIC,
POCKET;
@@ -37,7 +39,7 @@ public enum UiProfile {
* @param id the UiProfile identifier
* @return The UiProfile or {@link #CLASSIC} if the UiProfile wasn't found
*/
- public static UiProfile fromId(int id) {
+ public static @NonNull UiProfile fromId(int id) {
return VALUES.length > id ? VALUES[id] : VALUES[0];
}
}
diff --git a/common/src/main/java/org/geysermc/floodgate/util/WebsocketEventType.java b/common/src/main/java/org/geysermc/floodgate/util/WebsocketEventType.java
index 61e6c63b1..dffbabaaf 100644
--- a/common/src/main/java/org/geysermc/floodgate/util/WebsocketEventType.java
+++ b/common/src/main/java/org/geysermc/floodgate/util/WebsocketEventType.java
@@ -25,6 +25,8 @@
package org.geysermc.floodgate.util;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
public enum WebsocketEventType {
/**
* Sent once we successfully connected to the server
@@ -81,7 +83,7 @@ public enum WebsocketEventType {
this.id = id;
}
- public static WebsocketEventType fromId(int id) {
+ public static @Nullable WebsocketEventType fromId(int id) {
return VALUES.length > id ? VALUES[id] : null;
}
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index f67b652be..58f4799f3 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -1,12 +1,15 @@
-import net.kyori.blossom.BlossomExtension
-
plugins {
- id("net.kyori.blossom")
- id("net.kyori.indra.git")
+ // Allow blossom to mark sources root of templates
+ idea
+ alias(libs.plugins.blossom)
id("geyser.publish-conventions")
}
dependencies {
+ constraints {
+ implementation(libs.raknet) // Ensure protocol does not override the RakNet version
+ }
+
api(projects.common)
api(projects.api)
@@ -21,7 +24,6 @@ dependencies {
implementation(libs.websocket)
api(libs.bundles.protocol)
- implementation(libs.blockstateupdater)
api(libs.mcauthlib)
api(libs.mcprotocollib) {
@@ -44,10 +46,15 @@ dependencies {
implementation(libs.netty.transport.native.epoll) { artifact { classifier = "linux-x86_64" } }
implementation(libs.netty.transport.native.epoll) { artifact { classifier = "linux-aarch_64" } }
implementation(libs.netty.transport.native.kqueue) { artifact { classifier = "osx-x86_64" } }
+ implementation(libs.netty.transport.native.io.uring) { artifact { classifier = "linux-x86_64" } }
+ implementation(libs.netty.transport.native.io.uring) { artifact { classifier = "linux-aarch_64" } }
// Adventure text serialization
api(libs.bundles.adventure)
+ // command library
+ api(libs.cloud.core)
+
api(libs.erosion.common) {
isTransitive = false
}
@@ -63,11 +70,6 @@ dependencies {
api(libs.events)
}
-configurations.api {
- // This is still experimental - additionally, it could only really benefit standalone
- exclude(group = "io.netty.incubator", module = "netty-incubator-transport-native-io_uring")
-}
-
tasks.processResources {
// This is solely for backwards compatibility for other programs that used this file before the switch to gradle.
// It used to be generated by the maven Git-Commit-Id-Plugin
@@ -76,7 +78,7 @@ tasks.processResources {
expand(
"branch" to info.branch,
"buildNumber" to info.buildNumber,
- "projectVersion" to project.version,
+ "projectVersion" to info.version,
"commit" to info.commit,
"commitAbbrev" to info.commitAbbrev,
"commitMessage" to info.commitMessage,
@@ -85,20 +87,29 @@ tasks.processResources {
}
}
-configure {
- val mainFile = "src/main/java/org/geysermc/geyser/GeyserImpl.java"
- val info = GitInfo()
-
- replaceToken("\${version}", "${project.version} (${info.gitVersion})", mainFile)
- replaceToken("\${gitVersion}", info.gitVersion, mainFile)
- replaceToken("\${buildNumber}", info.buildNumber, mainFile)
- replaceToken("\${branch}", info.branch, mainFile)
- replaceToken("\${commit}", info.commit, mainFile)
- replaceToken("\${repository}", info.repository, mainFile)
+sourceSets {
+ main {
+ blossom {
+ val info = GitInfo()
+ javaSources {
+ property("version", info.version)
+ property("gitVersion", info.gitVersion)
+ property("buildNumber", info.buildNumber.toString())
+ property("branch", info.branch)
+ property("commit", info.commit)
+ property("repository", info.repository)
+ property("devVersion", info.isDev.toString())
+ }
+ }
+ }
}
-fun Project.buildNumber(): Int =
- (System.getenv("GITHUB_RUN_NUMBER") ?: jenkinsBuildNumber())?.let { Integer.parseInt(it) } ?: -1
+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()
+}
inner class GitInfo {
val branch: String
@@ -112,24 +123,40 @@ inner class GitInfo {
val commitMessage: String
val repository: String
+ val isDev: Boolean
+
init {
- // On Jenkins, a detached head is checked out, so indra cannot determine the branch.
- // Fortunately, this environment variable is available.
- branch = indraGit.branchName() ?: System.getenv("BRANCH_NAME") ?: "DEV"
+ branch = indraGit.branchName() ?: "DEV"
val commit = indraGit.commit()
this.commit = commit?.name ?: "0".repeat(40)
commitAbbrev = commit?.name?.substring(0, 7) ?: "0".repeat(7)
gitVersion = "git-${branch}-${commitAbbrev}"
- version = "${project.version} ($gitVersion)"
- buildNumber = buildNumber()
val git = indraGit.git()
commitMessage = git?.commit()?.message ?: ""
repository = git?.repository?.config?.getString("remote", "origin", "url") ?: ""
+
+ buildNumber = buildNumber()
+ isDev = isDevBuild(branch, repository)
+ val projectVersion = if (isDev) project.version else project.version.toString().replace("SNAPSHOT", "b${buildNumber}")
+ version = "$projectVersion ($gitVersion)"
}
}
-// todo remove this when we're not using Jenkins anymore
-fun jenkinsBuildNumber(): String? = System.getenv("BUILD_NUMBER")
+// Manual task to download the bedrock data files from the CloudburstMC/Data repository
+// Invoke with ./gradlew :core:downloadBedrockData --suffix=1_20_70
+// Set suffix to the current Bedrock version
+tasks.register("downloadBedrockData") {
+ urls = listOf(
+ "https://raw.githubusercontent.com/CloudburstMC/Data/master/entity_identifiers.dat",
+ "https://raw.githubusercontent.com/CloudburstMC/Data/master/biome_definitions.dat",
+ "https://raw.githubusercontent.com/CloudburstMC/Data/master/block_palette.nbt",
+ "https://raw.githubusercontent.com/CloudburstMC/Data/master/creative_items.json",
+ "https://raw.githubusercontent.com/CloudburstMC/Data/master/runtime_item_states.json"
+ )
+ suffixedFiles = listOf("block_palette.nbt", "creative_items.json", "runtime_item_states.json")
+
+ destinationDir = "$projectDir/src/main/resources/bedrock"
+}
diff --git a/core/src/main/java/org/geysermc/connector/network/session/auth/AuthData.java b/core/src/main/java-templates/org/geysermc/geyser/BuildData.java
similarity index 66%
rename from core/src/main/java/org/geysermc/connector/network/session/auth/AuthData.java
rename to core/src/main/java-templates/org/geysermc/geyser/BuildData.java
index cca7aa48c..0e4d08bfe 100644
--- a/core/src/main/java/org/geysermc/connector/network/session/auth/AuthData.java
+++ b/core/src/main/java-templates/org/geysermc/geyser/BuildData.java
@@ -23,33 +23,20 @@
* @link https://github.com/GeyserMC/Geyser
*/
-package org.geysermc.connector.network.session.auth;
+package org.geysermc.geyser;
-import java.util.UUID;
+// The constants are replaced before compilation
+public class BuildData {
+ public static final String GIT_VERSION = "{{ gitVersion }}";
+ public static final String VERSION = "{{ version }}";
-/**
- * Deprecated, legacy code. Serves as a wrapper around
- * the class used now.
- *
- * @deprecated legacy code
- */
-@Deprecated
-public class AuthData {
- private final org.geysermc.geyser.session.auth.AuthData handle;
+ public static final String BUILD_NUMBER = "{{ buildNumber }}";
+ public static final String BRANCH = "{{ branch }}";
+ public static final String COMMIT = "{{ commit }}";
+ public static final String REPOSITORY = "{{ repository }}";
+ private static final String DEV = "{{ devVersion }}";
- public AuthData(org.geysermc.geyser.session.auth.AuthData handle) {
- this.handle = handle;
- }
-
- public String getName() {
- return this.handle.name();
- }
-
- public UUID getUUID() {
- return this.handle.uuid();
- }
-
- public String getXboxUUID() {
- return this.handle.xuid();
+ public static boolean isDevBuild() {
+ return Boolean.parseBoolean(DEV);
}
}
diff --git a/core/src/main/java/org/geysermc/connector/GeyserConnector.java b/core/src/main/java/org/geysermc/connector/GeyserConnector.java
deleted file mode 100644
index 381282a2a..000000000
--- a/core/src/main/java/org/geysermc/connector/GeyserConnector.java
+++ /dev/null
@@ -1,91 +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.connector;
-
-import org.geysermc.api.Geyser;
-import org.geysermc.geyser.api.util.PlatformType;
-import org.geysermc.connector.network.session.GeyserSession;
-import org.geysermc.geyser.GeyserImpl;
-
-import java.util.UUID;
-
-/**
- * Deprecated, please use {@link Geyser} or {@link GeyserImpl}.
- *
- * @deprecated legacy code
- */
-@Deprecated
-public class GeyserConnector {
- public static final String NAME = GeyserImpl.NAME;
- public static final String GIT_VERSION = GeyserImpl.GIT_VERSION; // A fallback for running in IDEs
- public static final String VERSION = GeyserImpl.VERSION; // A fallback for running in IDEs
-
- public static final String OAUTH_CLIENT_ID = GeyserImpl.OAUTH_CLIENT_ID;
-
- private static final GeyserConnector INSTANCE = new GeyserConnector();
-
- public static GeyserConnector getInstance() {
- return INSTANCE;
- }
-
- public boolean isShuttingDown() {
- return GeyserImpl.getInstance().isShuttingDown();
- }
-
- public PlatformType getPlatformType() {
- return GeyserImpl.getInstance().getPlatformType();
- }
-
- public void shutdown() {
- GeyserImpl.getInstance().shutdown();
- }
-
- public void reload() {
- GeyserImpl.getInstance().reload();
- }
-
- public GeyserSession getPlayerByXuid(String xuid) {
- org.geysermc.geyser.session.GeyserSession session = GeyserImpl.getInstance().connectionByXuid(xuid);
- if (session != null) {
- return new GeyserSession(session);
- } else {
- return null;
- }
- }
-
- public GeyserSession getPlayerByUuid(UUID uuid) {
- org.geysermc.geyser.session.GeyserSession session = GeyserImpl.getInstance().connectionByUuid(uuid);
- if (session != null) {
- return new GeyserSession(session);
- } else {
- return null;
- }
- }
-
- public boolean isProductionEnvironment() {
- return GeyserImpl.getInstance().isProductionEnvironment();
- }
-}
diff --git a/core/src/main/java/org/geysermc/connector/network/session/GeyserSession.java b/core/src/main/java/org/geysermc/connector/network/session/GeyserSession.java
deleted file mode 100644
index 6298a41f6..000000000
--- a/core/src/main/java/org/geysermc/connector/network/session/GeyserSession.java
+++ /dev/null
@@ -1,161 +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.connector.network.session;
-
-import com.github.steveice10.packetlib.packet.Packet;
-import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket;
-import org.geysermc.connector.network.session.auth.AuthData;
-
-/**
- * Deprecated, legacy code. Serves as a wrapper around
- * the class used now.
- *
- * @deprecated legacy code
- */
-@Deprecated
-public class GeyserSession {
- private final org.geysermc.geyser.session.GeyserSession handle;
-
- public GeyserSession(org.geysermc.geyser.session.GeyserSession handle) {
- this.handle = handle;
- }
-
- public AuthData getAuthData() {
- return new AuthData(this.handle.getAuthData());
- }
-
- public boolean isMicrosoftAccount() {
- return this.handle.isMicrosoftAccount();
- }
-
- public boolean isClosed() {
- return this.handle.isClosed();
- }
-
- public String getRemoteAddress() {
- return this.handle.remoteServer().address();
- }
-
- public int getRemotePort() {
- return this.handle.remoteServer().port();
- }
-
- public int getRenderDistance() {
- return this.handle.getServerRenderDistance();
- }
-
- public boolean isSentSpawnPacket() {
- return this.handle.isSentSpawnPacket();
- }
-
- public boolean isLoggedIn() {
- return this.handle.isLoggedIn();
- }
-
- public boolean isLoggingIn() {
- return this.handle.isLoggingIn();
- }
-
- public boolean isSpawned() {
- return this.handle.isSpawned();
- }
-
- public boolean isInteracting() {
- return this.handle.isInteracting();
- }
-
- public boolean isCanFly() {
- return this.handle.isCanFly();
- }
-
- public boolean isFlying() {
- return this.handle.isFlying();
- }
-
- public void connect() {
- this.handle.connect();
- }
-
- public void login() {
- throw new UnsupportedOperationException();
- }
-
- public void authenticate(String username) {
- this.handle.authenticate(username);
- }
-
- public void authenticate(String username, String password) {
- this.handle.authenticate(username, password);
- }
-
- public void authenticateWithMicrosoftCode() {
- this.handle.authenticateWithMicrosoftCode();
- }
-
- public void disconnect(String reason) {
- this.handle.disconnect(reason);
- }
-
- public void close() {
- throw new UnsupportedOperationException();
- }
-
- public void executeInEventLoop(Runnable runnable) {
- this.handle.executeInEventLoop(runnable);
- }
-
- public String getName() {
- return this.handle.bedrockUsername();
- }
-
- public boolean isConsole() {
- return this.handle.isConsole();
- }
-
- public String getLocale() {
- return this.handle.locale();
- }
-
- public void sendUpstreamPacket(BedrockPacket packet) {
- this.handle.sendUpstreamPacket(packet);
- }
-
- public void sendUpstreamPacketImmediately(BedrockPacket packet) {
- this.handle.sendUpstreamPacketImmediately(packet);
- }
-
- public void sendDownstreamPacket(Packet packet) {
- this.handle.sendDownstreamPacket(packet);
- }
-
- public boolean hasPermission(String permission) {
- return this.handle.hasPermission(permission);
- }
-
- public void sendAdventureSettings() {
- this.handle.sendAdventureSettings();
- }
-}
diff --git a/core/src/main/java/org/geysermc/geyser/Constants.java b/core/src/main/java/org/geysermc/geyser/Constants.java
index 5de8e6e6b..03c103031 100644
--- a/core/src/main/java/org/geysermc/geyser/Constants.java
+++ b/core/src/main/java/org/geysermc/geyser/Constants.java
@@ -34,10 +34,8 @@ public final class Constants {
public static final String NEWS_OVERVIEW_URL = "https://api.geysermc.org/v2/news/";
public static final String NEWS_PROJECT_NAME = "geyser";
- public static final String FLOODGATE_DOWNLOAD_LOCATION = "https://ci.opencollab.dev/job/GeyserMC/job/Floodgate/job/master/";
-
+ 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";
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 e4baeebb5..3063fa4f6 100644
--- a/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java
+++ b/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java
@@ -25,15 +25,15 @@
package org.geysermc.geyser;
-import org.geysermc.geyser.command.GeyserCommandManager;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.geysermc.geyser.command.CommandRegistry;
import org.geysermc.geyser.configuration.GeyserConfiguration;
import org.geysermc.geyser.dump.BootstrapDumpInfo;
import org.geysermc.geyser.level.GeyserWorldManager;
import org.geysermc.geyser.level.WorldManager;
import org.geysermc.geyser.ping.IGeyserPingPassthrough;
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
import java.io.InputStream;
import java.net.SocketAddress;
import java.nio.file.Path;
@@ -44,14 +44,28 @@ public interface GeyserBootstrap {
GeyserWorldManager DEFAULT_CHUNK_MANAGER = new GeyserWorldManager();
/**
- * Called when the GeyserBootstrap is enabled
+ * Called when the GeyserBootstrap is initialized.
+ * This will only be called once, when Geyser is loading. Calling this must
+ * happen before {@link #onGeyserEnable()}, since this "sets up" Geyser.
*/
- void onEnable();
+ void onGeyserInitialize();
/**
- * Called when the GeyserBootstrap is disabled
+ * Called when the GeyserBootstrap is enabled/reloaded.
+ * This starts Geyser, after which, Geyser is in a player-accepting state.
*/
- void onDisable();
+ void onGeyserEnable();
+
+ /**
+ * Called when the GeyserBootstrap is disabled - either before a reload,
+ * of before fully shutting down.
+ */
+ void onGeyserDisable();
+
+ /**
+ * Called when the GeyserBootstrap is shutting down.
+ */
+ void onGeyserShutdown();
/**
* Returns the current GeyserConfiguration
@@ -68,17 +82,18 @@ 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
*
* @return The current PingPassthrough manager
*/
+ @Nullable
IGeyserPingPassthrough getGeyserPingPassthrough();
/**
@@ -151,7 +166,7 @@ public interface GeyserBootstrap {
* @param resource Resource to get
* @return InputStream of the given resource
*/
- default @Nonnull InputStream getResource(String resource) {
+ default @NonNull InputStream getResourceOrThrow(@NonNull String resource) {
InputStream stream = getResourceOrNull(resource);
if (stream == null) {
throw new AssertionError("Unable to find resource: " + resource);
@@ -162,7 +177,7 @@ public interface GeyserBootstrap {
/**
* @return the bind address being used by the Java server.
*/
- @Nonnull
+ @NonNull
String getServerBindAddress();
/**
diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java
index d13836e9a..010d089a9 100644
--- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java
+++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java
@@ -29,7 +29,6 @@ import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.github.steveice10.packetlib.tcp.TcpSession;
import io.netty.channel.epoll.Epoll;
import io.netty.util.NettyRuntime;
import io.netty.util.concurrent.DefaultThreadFactory;
@@ -42,8 +41,8 @@ import net.kyori.adventure.text.format.NamedTextColor;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
+import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec;
import org.geysermc.api.Geyser;
-import org.geysermc.geyser.api.util.PlatformType;
import org.geysermc.cumulus.form.Form;
import org.geysermc.cumulus.form.util.FormBuilder;
import org.geysermc.erosion.packet.Packets;
@@ -53,29 +52,33 @@ import org.geysermc.floodgate.crypto.Base64Topping;
import org.geysermc.floodgate.crypto.FloodgateCipher;
import org.geysermc.floodgate.news.NewsItemAction;
import org.geysermc.geyser.api.GeyserApi;
+import org.geysermc.geyser.api.command.CommandSource;
import org.geysermc.geyser.api.event.EventBus;
import org.geysermc.geyser.api.event.EventRegistrar;
-import org.geysermc.geyser.api.event.lifecycle.GeyserPostInitializeEvent;
-import org.geysermc.geyser.api.event.lifecycle.GeyserPreInitializeEvent;
-import org.geysermc.geyser.api.event.lifecycle.GeyserShutdownEvent;
+import org.geysermc.geyser.api.event.lifecycle.*;
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.command.GeyserCommandManager;
+import org.geysermc.geyser.api.util.MinecraftVersion;
+import org.geysermc.geyser.api.util.PlatformType;
+import org.geysermc.geyser.command.CommandRegistry;
import org.geysermc.geyser.configuration.GeyserConfiguration;
import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.erosion.UnixSocketClientListener;
import org.geysermc.geyser.event.GeyserEventBus;
import org.geysermc.geyser.extension.GeyserExtensionManager;
+import org.geysermc.geyser.impl.MinecraftVersionImpl;
import org.geysermc.geyser.level.WorldManager;
+import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.network.netty.GeyserServer;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.Registries;
-import org.geysermc.geyser.registry.loader.RegistryLoaders;
+import org.geysermc.geyser.registry.provider.ProviderSupplier;
import org.geysermc.geyser.scoreboard.ScoreboardUpdater;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.PendingMicrosoftAuthentication;
import org.geysermc.geyser.session.SessionManager;
+import org.geysermc.geyser.session.cache.RegistryCache;
import org.geysermc.geyser.skin.FloodgateSkinUploader;
import org.geysermc.geyser.skin.ProvidedSkins;
import org.geysermc.geyser.skin.SkinProvider;
@@ -83,6 +86,7 @@ import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.text.MinecraftLocale;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.geyser.util.*;
+import org.geysermc.mcprotocollib.network.tcp.TcpSession;
import java.io.File;
import java.io.FileWriter;
@@ -102,7 +106,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)
@@ -111,13 +115,14 @@ public class GeyserImpl implements GeyserApi {
.enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES);
public static final String NAME = "Geyser";
- public static final String GIT_VERSION = "${gitVersion}"; // A fallback for running in IDEs
- public static final String VERSION = "${version}"; // A fallback for running in IDEs
+ public static final String GIT_VERSION = BuildData.GIT_VERSION;
+ public static final String VERSION = BuildData.VERSION;
- public static final String BUILD_NUMBER = "${buildNumber}";
- public static final String BRANCH = "${branch}";
- public static final String COMMIT = "${commit}";
- public static final String REPOSITORY = "${repository}";
+ public static final String BUILD_NUMBER = BuildData.BUILD_NUMBER;
+ public static final String BRANCH = BuildData.BRANCH;
+ public static final String COMMIT = BuildData.COMMIT;
+ public static final String REPOSITORY = BuildData.REPOSITORY;
+ public static final boolean IS_DEV = BuildData.isDevBuild();
/**
* Oauth client ID for Microsoft authentication
@@ -140,6 +145,7 @@ public class GeyserImpl implements GeyserApi {
private UnixSocketClientListener erosionUnixListener;
+ @Setter
private volatile boolean shuttingDown = false;
private ScheduledExecutorService scheduledThread;
@@ -157,8 +163,20 @@ public class GeyserImpl implements GeyserApi {
@Getter(AccessLevel.NONE)
private Map savedRefreshTokens;
+ @Getter
private static GeyserImpl instance;
+ /**
+ * Determines if we're currently reloading. Replaces per-bootstrap reload checks
+ */
+ private volatile boolean isReloading;
+
+ /**
+ * Determines if Geyser is currently enabled. This is used to determine if {@link #disable()} should be called during {@link #shutdown()}.
+ */
+ @Setter
+ private boolean isEnabled;
+
private GeyserImpl(PlatformType platformType, GeyserBootstrap bootstrap) {
instance = this;
@@ -167,13 +185,16 @@ public class GeyserImpl implements GeyserApi {
this.platformType = platformType;
this.bootstrap = bootstrap;
- GeyserLocale.finalizeDefaultLocale(this);
-
/* Initialize event bus */
this.eventBus = new GeyserEventBus();
- /* Load Extensions */
+ /* Create Extension Manager */
this.extensionManager = new GeyserExtensionManager();
+
+ /* Finalize locale loading now that we know the default locale from the config */
+ GeyserLocale.finalizeDefaultLocale(this);
+
+ /* Load Extensions */
this.extensionManager.init();
this.eventBus.fire(new GeyserPreInitializeEvent(this.extensionManager, this.eventBus));
}
@@ -187,12 +208,18 @@ public class GeyserImpl implements GeyserApi {
logger.info("");
logger.info(GeyserLocale.getLocaleStringLog("geyser.core.load", NAME, VERSION));
logger.info("");
+ if (IS_DEV) {
+ logger.info(GeyserLocale.getLocaleStringLog("geyser.core.dev_build", "https://discord.gg/geysermc"));
+ logger.info("");
+ }
logger.info("******************************************");
/* Initialize registries */
Registries.init();
BlockRegistries.init();
+ RegistryCache.init();
+
/* Initialize translators */
EntityDefinitions.init();
MessageTranslator.init();
@@ -202,6 +229,7 @@ public class GeyserImpl implements GeyserApi {
if (ex != null) {
return;
}
+
MinecraftLocale.ensureEN_US();
String locale = GeyserLocale.getDefaultLocale();
if (!"en_us".equals(locale)) {
@@ -214,6 +242,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();
@@ -231,11 +262,17 @@ public class GeyserImpl implements GeyserApi {
} else if (config.getRemote().authType() == AuthType.FLOODGATE) {
VersionCheckUtils.checkForOutdatedFloodgate(logger);
}
+
+ VersionCheckUtils.checkForOutdatedJava(logger);
}
private void startInstance() {
this.scheduledThread = Executors.newSingleThreadScheduledExecutor(new DefaultThreadFactory("Geyser Scheduled Thread"));
+ if (isReloading) {
+ // If we're reloading, the default locale in the config might have changed.
+ GeyserLocale.finalizeDefaultLocale(this);
+ }
GeyserLogger logger = bootstrap.getGeyserLogger();
GeyserConfiguration config = bootstrap.getGeyserConfig();
@@ -309,15 +346,33 @@ public class GeyserImpl implements GeyserApi {
}
}
- boolean floodgatePresent = bootstrap.testFloodgatePluginPresent();
- if (config.getRemote().authType() == AuthType.FLOODGATE && !floodgatePresent) {
- logger.severe(GeyserLocale.getLocaleStringLog("geyser.bootstrap.floodgate.not_installed") + " "
- + GeyserLocale.getLocaleStringLog("geyser.bootstrap.floodgate.disabling"));
- return;
- } else if (config.isAutoconfiguredRemote() && floodgatePresent) {
- // Floodgate installed means that the user wants Floodgate authentication
- logger.debug("Auto-setting to Floodgate authentication.");
- config.getRemote().setAuthType(AuthType.FLOODGATE);
+ 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) {
+ logger.severe(GeyserLocale.getLocaleStringLog("geyser.bootstrap.floodgate.not_installed") + " "
+ + GeyserLocale.getLocaleStringLog("geyser.bootstrap.floodgate.disabling"));
+ return;
+ } else if (config.isAutoconfiguredRemote() && floodgatePresent) {
+ // Floodgate installed means that the user wants Floodgate authentication
+ logger.debug("Auto-setting to Floodgate authentication.");
+ config.getRemote().setAuthType(AuthType.FLOODGATE);
+ }
}
}
@@ -479,12 +534,6 @@ public class GeyserImpl implements GeyserApi {
}
if (config.getRemote().authType() == AuthType.ONLINE) {
- if (config.getUserAuths() != null && !config.getUserAuths().isEmpty()) {
- getLogger().warning("The 'userAuths' config section is now deprecated, and will be removed in the near future! " +
- "Please migrate to the new 'saved-user-logins' config option: " +
- "https://wiki.geysermc.org/geyser/understanding-the-config/");
- }
-
// May be written/read to on multiple threads from each GeyserSession as well as writing the config
savedRefreshTokens = new ConcurrentHashMap<>();
@@ -521,12 +570,15 @@ public class GeyserImpl implements GeyserApi {
newsHandler.handleNews(null, NewsItemAction.ON_SERVER_STARTED);
- this.eventBus.fire(new GeyserPostInitializeEvent(this.extensionManager, this.eventBus));
+ if (isReloading) {
+ this.eventBus.fire(new GeyserPostReloadEvent(this.extensionManager, this.eventBus));
+ } else {
+ this.eventBus.fire(new GeyserPostInitializeEvent(this.extensionManager, this.eventBus));
+ }
+
if (config.isNotifyOnNewBedrockUpdate()) {
VersionCheckUtils.checkForGeyserUpdate(this::getLogger);
}
-
- VersionCheckUtils.checkForOutdatedJava(logger);
}
@Override
@@ -585,9 +637,8 @@ public class GeyserImpl implements GeyserApi {
return session.transfer(address, port);
}
- public void shutdown() {
+ public void disable() {
bootstrap.getGeyserLogger().info(GeyserLocale.getLocaleStringLog("geyser.core.shutdown"));
- shuttingDown = true;
if (sessionManager.size() >= 1) {
bootstrap.getGeyserLogger().info(GeyserLocale.getLocaleStringLog("geyser.core.shutdown.kick.log", sessionManager.size()));
@@ -601,7 +652,6 @@ public class GeyserImpl implements GeyserApi {
skinUploader.close();
}
newsHandler.shutdown();
- this.commandManager().getCommands().clear();
if (this.erosionUnixListener != null) {
this.erosionUnixListener.close();
@@ -609,16 +659,30 @@ public class GeyserImpl implements GeyserApi {
Registries.RESOURCE_PACKS.get().clear();
+ this.setEnabled(false);
+ }
+
+ public void shutdown() {
+ shuttingDown = true;
+ if (isEnabled) {
+ this.disable();
+ }
+
+ // Disable extensions, fire the shutdown event
this.eventBus.fire(new GeyserShutdownEvent(this.extensionManager, this.eventBus));
this.extensionManager.disableExtensions();
bootstrap.getGeyserLogger().info(GeyserLocale.getLocaleStringLog("geyser.core.shutdown.done"));
}
- public void reload() {
- shutdown();
- this.extensionManager.enableExtensions();
- bootstrap.onEnable();
+ public void reloadGeyser() {
+ isReloading = true;
+ this.eventBus.fire(new GeyserPreReloadEvent(this.extensionManager, this.eventBus));
+
+ bootstrap.onGeyserDisable();
+ bootstrap.onGeyserEnable();
+
+ isReloading = false;
}
/**
@@ -627,9 +691,10 @@ public class GeyserImpl implements GeyserApi {
*
* @return true if the version number is not 'DEV'.
*/
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean isProductionEnvironment() {
// First is if Blossom runs, second is if Blossom doesn't run
- // noinspection ConstantConditions - changes in production
+ //noinspection ConstantConditions - changes in production
return !("git-local/dev-0000000".equals(GeyserImpl.GIT_VERSION) || "${gitVersion}".equals(GeyserImpl.GIT_VERSION));
}
@@ -639,14 +704,22 @@ 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
+ @SuppressWarnings("unchecked")
public @NonNull R provider(@NonNull Class apiClass, @Nullable Object... args) {
- return (R) Registries.PROVIDERS.get(apiClass).create(args);
+ ProviderSupplier provider = Registries.PROVIDERS.get(apiClass);
+ if (provider == null) {
+ throw new IllegalArgumentException("No provider found for " + apiClass);
+ }
+ return (R) provider.create(args);
}
@Override
@@ -684,11 +757,31 @@ public class GeyserImpl implements GeyserApi {
return platformType;
}
+ @Override
+ public @NonNull MinecraftVersion supportedJavaVersion() {
+ return new MinecraftVersionImpl(GameProtocol.getJavaMinecraftVersion(), GameProtocol.getJavaProtocolVersion());
+ }
+
+ @Override
+ public @NonNull List supportedBedrockVersions() {
+ ArrayList versions = new ArrayList<>();
+ for (BedrockCodec codec : GameProtocol.SUPPORTED_BEDROCK_CODECS) {
+ versions.add(new MinecraftVersionImpl(codec.getMinecraftVersion(), codec.getProtocolVersion()));
+ }
+ return Collections.unmodifiableList(versions);
+ }
+
+ @Override
+ public @NonNull CommandSource consoleCommandSource() {
+ return getLogger();
+ }
+
public int buildNumber() {
if (!this.isProductionEnvironment()) {
return 0;
}
+ //noinspection DataFlowIssue
return Integer.parseInt(BUILD_NUMBER);
}
@@ -705,13 +798,12 @@ public class GeyserImpl implements GeyserApi {
throw new RuntimeException("Geyser has not been loaded yet!");
}
- // We've been reloaded
- if (instance.isShuttingDown()) {
- instance.shuttingDown = false;
+ if (getInstance().isReloading()) {
instance.startInstance();
} else {
instance.initialize();
}
+ instance.setEnabled(true);
}
public GeyserLogger getLogger() {
@@ -758,8 +850,4 @@ public class GeyserImpl implements GeyserApi {
}
});
}
-
- public static GeyserImpl getInstance() {
- return instance;
- }
}
diff --git a/core/src/main/java/org/geysermc/geyser/GeyserLogger.java b/core/src/main/java/org/geysermc/geyser/GeyserLogger.java
index 88220eec9..f408de29c 100644
--- a/core/src/main/java/org/geysermc/geyser/GeyserLogger.java
+++ b/core/src/main/java/org/geysermc/geyser/GeyserLogger.java
@@ -26,9 +26,11 @@
package org.geysermc.geyser;
import net.kyori.adventure.text.Component;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.command.GeyserCommandSource;
-import javax.annotation.Nullable;
+import java.util.UUID;
public interface GeyserLogger extends GeyserCommandSource {
@@ -119,7 +121,7 @@ public interface GeyserLogger extends GeyserCommandSource {
}
@Override
- default void sendMessage(String message) {
+ default void sendMessage(@NonNull String message) {
info(message);
}
@@ -128,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/GeyserMain.java b/core/src/main/java/org/geysermc/geyser/GeyserMain.java
index 8726c1b24..05cbebed4 100644
--- a/core/src/main/java/org/geysermc/geyser/GeyserMain.java
+++ b/core/src/main/java/org/geysermc/geyser/GeyserMain.java
@@ -28,7 +28,9 @@ package org.geysermc.geyser;
import javax.swing.*;
import java.io.InputStream;
import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
import java.util.Locale;
+import java.util.Objects;
import java.util.Scanner;
public class GeyserMain {
@@ -60,8 +62,8 @@ public class GeyserMain {
helpStream = GeyserMain.class.getClassLoader().getResourceAsStream("languages/run-help/en_US.txt");
}
- Scanner help = new Scanner(helpStream).useDelimiter("\\Z");
- String line = "";
+ Scanner help = new Scanner(Objects.requireNonNull(helpStream), StandardCharsets.UTF_8).useDelimiter("\\Z");
+ String line;
while (help.hasNext()) {
line = help.next();
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..f07092afd
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/command/CommandRegistry.java
@@ -0,0 +1,300 @@
+/*
+ * 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.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"));
+ 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 5808dbc2c..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,74 +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.geysermc.geyser.api.command.Command;
-import org.geysermc.geyser.session.GeyserSession;
+import org.checkerframework.checker.nullness.qual.Nullable;
+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 javax.annotation.Nullable;
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
+ 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 GeyserCommand(@NonNull String name, @NonNull String description, @NonNull String permission, @Nullable TriState permissionDefault) {
+ this(name, description, permission, permissionDefault, false, false);
+ }
+
@NonNull
@Override
- public List subCommands() {
- return Collections.emptyList();
+ 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);
}
/**
- * Shortcut to {@link #subCommands()} ()}{@code .isEmpty()}.
- *
- * @return true if there are subcommand present for this command.
+ * @return the first (literal) argument of this command, which comes before {@link #name()}.
*/
- public boolean hasSubCommands() {
- return !this.subCommands().isEmpty();
- }
-
- public void setAliases(List aliases) {
- this.aliases = aliases;
+ public String rootCommand() {
+ return DEFAULT_ROOT_COMMAND;
}
/**
- * Used for permission defaults on server implementations.
+ * Returns a {@link org.incendo.cloud.permission.Permission} that handles {@link #isBedrockOnly()}, {@link #isPlayerOnly()}, and {@link #permission()}.
*
- * @return if this command is designated to be used only by server operators.
+ * @param manager the manager to be used for permission node checking
+ * @return a permission that will properly restrict usage of this command
*/
- @Override
- public boolean isSuggestedOpOnly() {
- return false;
+ public final GeyserPermission commandPermission(CommandManager manager) {
+ return new GeyserPermission(bedrockOnly, playerOnly, permission, manager);
}
-}
\ No newline at end of file
+
+ /**
+ * 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 a9b1c734f..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.geysermc.geyser.GeyserImpl;
-import org.geysermc.geyser.api.command.Command;
-import org.geysermc.geyser.session.GeyserSession;
-
-import javax.annotation.Nullable;
-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 1a1f87c41..000000000
--- a/core/src/main/java/org/geysermc/geyser/command/GeyserCommandManager.java
+++ /dev/null
@@ -1,333 +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.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.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.session.GeyserSession;
-import org.geysermc.geyser.text.GeyserLocale;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-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<>(13);
- 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"));
- 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().size() > 0) {
- 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);
- }
- }
-
- @NotNull
- public Map commands() {
- return Collections.unmodifiableMap(this.commands);
- }
-
- @NotNull
- 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