diff --git a/README.md b/README.md
index bdd8cbe98..d58e2eb58 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ The ultimate goal of this project is to allow Minecraft: Bedrock Edition users t
Special thanks to the DragonProxy project for being a trailblazer in protocol translation and for all the team members who have joined us here!
-### Currently supporting Minecraft Bedrock 1.20.40 - 1.20.51 and Minecraft Java 1.20.4
+### Currently supporting Minecraft Bedrock 1.20.40 - 1.20.61 and Minecraft Java 1.20.4
## Setting Up
Take a look [here](https://wiki.geysermc.org/geyser/setup/) for how to set up Geyser.
diff --git a/api/build.gradle.kts b/api/build.gradle.kts
index c0ed242b6..bd54a9ce4 100644
--- a/api/build.gradle.kts
+++ b/api/build.gradle.kts
@@ -4,4 +4,5 @@ plugins {
dependencies {
api(libs.base.api)
+ api(libs.math)
}
\ No newline at end of file
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..2f715fa1e
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/CameraData.java
@@ -0,0 +1,148 @@
+/*
+ * 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();
+}
\ No newline at end of file
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/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/CameraPerspective.java b/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/CameraPerspective.java
new file mode 100644
index 000000000..4167f2a34
--- /dev/null
+++ b/api/src/main/java/org/geysermc/geyser/api/bedrock/camera/CameraPerspective.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+/**
+ * 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 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 67b56b1f5..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,6 +25,9 @@
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
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 7094812a0..9bda4f903 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/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/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 c5d5cdacc..4191c8578 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
@@ -32,11 +32,11 @@ 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.api.util.PlatformType;
import org.geysermc.geyser.command.GeyserCommandManager;
import org.geysermc.geyser.configuration.GeyserConfiguration;
import org.geysermc.geyser.dump.BootstrapDumpInfo;
@@ -70,11 +70,13 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap {
private GeyserImpl geyser;
- private static boolean INITIALIZED = false;
-
- @SuppressWarnings({"JavaReflectionMemberAccess", "ResultOfMethodCallIgnored"})
@Override
public void onLoad() {
+ onGeyserInitialize();
+ }
+
+ @Override
+ public void onGeyserInitialize() {
GeyserLocale.init(this);
// Copied from ViaVersion.
@@ -91,29 +93,62 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap {
getLogger().warning("/_____________\\");
}
- 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()) {
return;
}
-
this.geyserLogger = new GeyserBungeeLogger(getLogger(), geyserConfig.isDebugMode());
GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger);
-
this.geyser = GeyserImpl.load(PlatformType.BUNGEECORD, this);
+ this.geyserInjector = new GeyserBungeeInjector(this);
}
@Override
public void onEnable() {
+ // 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 {
+ // For consistency with other platforms - create command manager before GeyserImpl#start()
+ // This ensures the command events are called before the item/block ones are
+ this.geyserCommandManager = new GeyserCommandManager(geyser);
+ this.geyserCommandManager.init();
+ }
// Force-disable query if enabled, or else Geyser won't enable
for (ListenerInfo info : getProxy().getConfig().getListeners()) {
@@ -133,54 +168,20 @@ 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
- if (!INITIALIZED) {
- this.awaitStartupCompletion(0);
- } else {
- // No need to "wait" for startup completion, just start Geyser - we're reloading.
- this.postStartup();
- }
- }
+ GeyserImpl.start();
- @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();
+ 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;
}
- 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();
-
- if (!INITIALIZED) {
- this.geyserInjector = new GeyserBungeeInjector(this);
- this.geyserInjector.initializeLocalChannel(this);
- }
-
- this.geyserCommandManager = new GeyserCommandManager(geyser);
- this.geyserCommandManager.init();
+ this.geyserInjector.initializeLocalChannel(this);
this.getProxy().getPluginManager().registerCommand(this, new GeyserBungeeCommandExecutor("geyser", this.geyser, this.geyserCommandManager.getCommands()));
for (Map.Entry> entry : this.geyserCommandManager.extensionCommands().entrySet()) {
@@ -191,18 +192,17 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap {
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());
- }
-
- INITIALIZED = true;
}
@Override
- public void onDisable() {
+ public void onGeyserDisable() {
+ if (geyser != null) {
+ geyser.disable();
+ }
+ }
+
+ @Override
+ public void onGeyserShutdown() {
if (geyser != null) {
geyser.shutdown();
}
@@ -211,6 +211,11 @@ public class GeyserBungeePlugin extends Plugin implements GeyserBootstrap {
}
}
+ @Override
+ public void onDisable() {
+ this.onGeyserShutdown();
+ }
+
@Override
public GeyserBungeeConfiguration getGeyserConfig() {
return geyserConfig;
@@ -278,4 +283,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) {
+ getLogger().log(Level.SEVERE, GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex);
+ ex.printStackTrace();
+ return false;
+ }
+ return true;
+ }
}
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
index 071409046..756063af7 100644
--- 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
@@ -27,6 +27,8 @@ package org.geysermc.geyser.platform.fabric;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import lombok.Getter;
+import lombok.Setter;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
@@ -66,13 +68,14 @@ import java.util.Optional;
import java.util.UUID;
public class GeyserFabricMod implements ModInitializer, GeyserBootstrap {
+
+ @Getter
private static GeyserFabricMod instance;
-
- private boolean reloading;
-
private GeyserImpl geyser;
private ModContainer mod;
private Path dataFolder;
+
+ @Setter
private MinecraftServer server;
private GeyserCommandManager geyserCommandManager;
@@ -85,64 +88,45 @@ public class GeyserFabricMod implements ModInitializer, GeyserBootstrap {
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);
- }
+ onGeyserInitialize();
}
@Override
- public void onEnable() {
- dataFolder = FabricLoader.getInstance().getConfigDir().resolve("Geyser-Fabric");
- if (!dataFolder.toFile().exists()) {
- //noinspection ResultOfMethodCallIgnored
- dataFolder.toFile().mkdir();
+ public void onGeyserInitialize() {
+ 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((server) -> {
+ this.server = server;
+ onGeyserEnable();
+ });
}
- // Init dataFolder first as local language overrides call getConfigFolder()
- GeyserLocale.init(this);
+ // These are only registered once
+ ServerLifecycleEvents.SERVER_STOPPING.register((server) -> onGeyserShutdown());
+ ServerPlayConnectionEvents.JOIN.register((handler, $, $$) -> GeyserFabricUpdateListener.onPlayReady(handler));
- 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();
+ dataFolder = FabricLoader.getInstance().getConfigDir().resolve("Geyser-Fabric");
+ GeyserLocale.init(this);
+ if (!loadConfig()) {
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();
+ @Override
+ public void onGeyserEnable() {
+ if (GeyserImpl.getInstance().isReloading()) {
+ if (!loadConfig()) {
+ return;
+ }
+ this.geyserLogger.setDebug(geyserConfig.isDebugMode());
+ GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger);
+ } else {
+ this.geyserCommandManager = new GeyserCommandManager(geyser);
+ this.geyserCommandManager.init();
+ }
if (geyserConfig.isLegacyPingPassthrough()) {
this.geyserPingPassthrough = GeyserLegacyPingPassthrough.init(geyser);
@@ -150,8 +134,12 @@ public class GeyserFabricMod implements ModInitializer, GeyserBootstrap {
this.geyserPingPassthrough = new ModPingPassthrough(server, geyserLogger);
}
- this.geyserCommandManager = new GeyserCommandManager(geyser);
- this.geyserCommandManager.init();
+ GeyserImpl.start();
+
+ // No need to re-register commands, or re-recreate the world manager when reloading
+ if (GeyserImpl.getInstance().isReloading()) {
+ return;
+ }
this.geyserWorldManager = new GeyserFabricWorldManager(server);
@@ -201,14 +189,19 @@ public class GeyserFabricMod implements ModInitializer, GeyserBootstrap {
}
@Override
- public void onDisable() {
+ public void onGeyserDisable() {
+ if (geyser != null) {
+ geyser.disable();
+ }
+ }
+
+ @Override
+ public void onGeyserShutdown() {
if (geyser != null) {
geyser.shutdown();
geyser = null;
}
- if (!reloading) {
- this.server = null;
- }
+ this.server = null;
}
@Override
@@ -291,11 +284,22 @@ public class GeyserFabricMod implements ModInitializer, GeyserBootstrap {
}
}
- public void setReloading(boolean reloading) {
- this.reloading = reloading;
- }
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ private boolean loadConfig() {
+ try {
+ if (!dataFolder.toFile().exists()) {
+ //noinspection ResultOfMethodCallIgnored
+ dataFolder.toFile().mkdir();
+ }
- public static GeyserFabricMod getInstance() {
- return instance;
+ 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);
+ return true;
+ } catch (IOException ex) {
+ LogManager.getLogger("geyser-fabric").error(GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex);
+ ex.printStackTrace();
+ return false;
+ }
}
}
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
index 732b28ca7..86b50d431 100644
--- 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
@@ -32,7 +32,6 @@ 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;
@@ -64,9 +63,6 @@ public class GeyserFabricCommandExecutor extends GeyserCommandExecutor implement
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()));
diff --git a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/mixin/client/IntegratedServerMixin.java b/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/mixin/client/IntegratedServerMixin.java
index af11174dc..999a077bb 100644
--- a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/mixin/client/IntegratedServerMixin.java
+++ b/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/mixin/client/IntegratedServerMixin.java
@@ -57,7 +57,8 @@ 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);
+ GeyserFabricMod.getInstance().setServer((MinecraftServer) (Object) this);
+ GeyserFabricMod.getInstance().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
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 a2a08c3bf..1bc1718d7 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
@@ -81,10 +81,6 @@ 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 GeyserSpigotConfiguration geyserConfig;
@@ -102,6 +98,11 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
@Override
public void onLoad() {
+ onGeyserInitialize();
+ }
+
+ @Override
+ public void onGeyserInitialize() {
GeyserLocale.init(this);
try {
@@ -118,6 +119,7 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
getLogger().severe(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_server.message", "1.13.2"));
getLogger().severe("");
getLogger().severe("*********************************************");
+ Bukkit.getPluginManager().disablePlugin(this);
return;
}
@@ -131,6 +133,7 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
getLogger().severe(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_server_type.message", "Paper"));
getLogger().severe("");
getLogger().severe("*********************************************");
+ Bukkit.getPluginManager().disablePlugin(this);
return;
}
}
@@ -143,86 +146,72 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
getLogger().severe("This version of Spigot is using an outdated version of netty. Please use Paper instead!");
getLogger().severe("");
getLogger().severe("*********************************************");
- return;
- }
-
- // 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) {
- getLogger().log(Level.SEVERE, GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex);
- ex.printStackTrace();
Bukkit.getPluginManager().disablePlugin(this);
return;
}
+ if (!loadConfig()) {
+ return;
+ }
this.geyserLogger = GeyserPaperLogger.supported() ? new GeyserPaperLogger(this, getLogger(), geyserConfig.isDebugMode())
: new GeyserSpigotLogger(getLogger(), 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;
- }
-
this.geyserCommandManager = new GeyserSpigotCommandManager(geyser);
this.geyserCommandManager.init();
- if (!INITIALIZED) {
- // Needs to be an anonymous inner class otherwise Bukkit complains about missing classes
- Bukkit.getPluginManager().registerEvents(new Listener() {
+ // Because Bukkit locks its command map upon startup, we need to
+ // add our plugin commands in onEnable, but populating the executor
+ // can happen at any time (later in #onGeyserEnable())
+ CommandMap commandMap = GeyserSpigotCommandManager.getCommandMap();
+ for (Extension extension : this.geyserCommandManager.extensionCommands().keySet()) {
+ // Thanks again, Bukkit
+ try {
+ Constructor constructor = PluginCommand.class.getDeclaredConstructor(String.class, Plugin.class);
+ constructor.setAccessible(true);
- @EventHandler
- public void onServerLoaded(ServerLoadEvent event) {
- // Wait until all plugins have loaded so Geyser can start
- postStartup();
- }
- }, this);
+ PluginCommand pluginCommand = constructor.newInstance(extension.description().id(), this);
+ pluginCommand.setDescription("The main command for the " + extension.name() + " Geyser extension!");
- // 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.name(), ex);
- }
+ commandMap.register(extension.description().id(), "geyserext", pluginCommand);
+ } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) {
+ this.geyserLogger.error("Failed to construct PluginCommand for extension " + extension.name(), ex);
}
}
- if (INITIALIZED) {
- // Reload; continue with post startup
- postStartup();
- }
+ // 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();
+ }
+ }, 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);
@@ -238,20 +227,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/re-register commands/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);
@@ -278,6 +263,7 @@ 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);
@@ -302,72 +288,72 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
command.setExecutor(new GeyserSpigotCommandExecutor(this.geyser, commands));
}
- 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()) {
+ // 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;
+ }
+
+ 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));
}
-
- // 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);
}
+ 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;
}
@Override
- public void onDisable() {
+ public void onGeyserDisable() {
+ if (geyser != null) {
+ geyser.disable();
+ }
+ }
+
+ @Override
+ public void onGeyserShutdown() {
if (geyser != null) {
geyser.shutdown();
}
@@ -376,6 +362,11 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
}
}
+ @Override
+ public void onDisable() {
+ this.onGeyserShutdown();
+ }
+
@Override
public GeyserSpigotConfiguration getGeyserConfig() {
return geyserConfig;
@@ -470,4 +461,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) {
+ getLogger().log(Level.SEVERE, GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex);
+ ex.printStackTrace();
+ Bukkit.getPluginManager().disablePlugin(this);
+ return false;
+ }
+
+ return true;
+ }
}
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 9f2208ea8..039004867 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
@@ -39,9 +39,9 @@ import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.Logger;
import org.apache.logging.log4j.core.appender.ConsoleAppender;
import org.checkerframework.checker.nullness.qual.NonNull;
-import org.geysermc.geyser.api.util.PlatformType;
import org.geysermc.geyser.GeyserBootstrap;
import org.geysermc.geyser.GeyserImpl;
+import org.geysermc.geyser.api.util.PlatformType;
import org.geysermc.geyser.command.GeyserCommandManager;
import org.geysermc.geyser.configuration.GeyserConfiguration;
import org.geysermc.geyser.configuration.GeyserJacksonConfiguration;
@@ -59,7 +59,12 @@ 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 {
@@ -68,11 +73,10 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap {
private GeyserStandaloneConfiguration geyserConfig;
private GeyserStandaloneLogger geyserLogger;
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,23 +165,19 @@ 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);
}
}
@@ -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,14 +220,15 @@ 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);
- GeyserImpl.start();
geyserCommandManager = new GeyserCommandManager(geyser);
geyserCommandManager.init();
+ GeyserImpl.start();
+
if (gui != null) {
gui.enableCommands(geyser.getScheduledThread(), geyserCommandManager);
}
@@ -250,7 +256,14 @@ public class GeyserStandaloneBootstrap implements GeyserBootstrap {
}
@Override
- public void onDisable() {
+ public void onGeyserDisable() {
+ // We can re-register commands on standalone, so why not
+ GeyserImpl.getInstance().commandManager().getCommands().clear();
+ geyser.disable();
+ }
+
+ @Override
+ public void onGeyserShutdown() {
geyser.shutdown();
System.exit(0);
}
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..3a34920ce 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
@@ -49,7 +49,7 @@ public class GeyserStandaloneLogger extends SimpleTerminalConsole implements Gey
@Override
protected void shutdown() {
- GeyserImpl.getInstance().getBootstrap().onDisable();
+ GeyserImpl.getInstance().getBootstrap().onGeyserShutdown();
}
@Override
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 bd3d6085a..347a47d63 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
@@ -32,10 +32,10 @@ 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.proxy.ProxyServer;
import lombok.Getter;
-import net.kyori.adventure.util.Codec;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.GeyserBootstrap;
@@ -46,6 +46,7 @@ import org.geysermc.geyser.api.util.PlatformType;
import org.geysermc.geyser.command.GeyserCommandManager;
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;
@@ -63,12 +64,6 @@ 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 {
-
- /**
- * Determines if the plugin has been ran once before, including before /geyser reload.
- */
- private static boolean INITIALIZED = false;
-
@Inject
private Logger logger;
@@ -90,52 +85,54 @@ public class GeyserVelocityPlugin implements GeyserBootstrap {
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;
- }
-
+ 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())) {
+ logger.error(" / \\");
+ logger.error(" / \\");
+ logger.error(" / | \\");
+ logger.error(" / | \\ " + GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_proxy", proxyServer.getVersion().getName()));
+ logger.error(" / \\ " + GeyserLocale.getLocaleStringLog("geyser.may_not_work_as_intended_all_caps"));
+ logger.error(" / o \\");
+ logger.error("/_____________\\");
}
+ if (!loadConfig()) {
+ return;
+ }
this.geyserLogger = new GeyserVelocityLogger(logger, geyserConfig.isDebugMode());
GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger);
this.geyser = GeyserImpl.load(PlatformType.VELOCITY, this);
-
- // Hack: Normally triggered by ListenerBoundEvent, but that doesn't fire on /geyser reload
- if (INITIALIZED) {
- this.postStartup();
- }
+ this.geyserInjector = new GeyserVelocityInjector(proxyServer);
}
- private void postStartup() {
- GeyserImpl.start();
-
- if (!INITIALIZED) {
- this.geyserInjector = new GeyserVelocityInjector(proxyServer);
- // Will be initialized after the proxy has been bound
+ @Override
+ public void onGeyserEnable() {
+ if (GeyserImpl.getInstance().isReloading()) {
+ if (!loadConfig()) {
+ return;
+ }
+ this.geyserLogger.setDebug(geyserConfig.isDebugMode());
+ GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger);
+ } else {
+ this.geyserCommandManager = new GeyserCommandManager(geyser);
+ this.geyserCommandManager.init();
}
- this.geyserCommandManager = new GeyserCommandManager(geyser);
- this.geyserCommandManager.init();
+ GeyserImpl.start();
+
+ if (geyserConfig.isLegacyPingPassthrough()) {
+ this.geyserPingPassthrough = GeyserLegacyPingPassthrough.init(geyser);
+ } else {
+ this.geyserPingPassthrough = new GeyserVelocityPingPassthrough(proxyServer);
+ }
+
+ // No need to re-register commands when reloading
+ if (GeyserImpl.getInstance().isReloading()) {
+ return;
+ }
this.commandManager.register("geyser", new GeyserVelocityCommandExecutor(geyser, geyserCommandManager.getCommands()));
for (Map.Entry> entry : this.geyserCommandManager.extensionCommands().entrySet()) {
@@ -147,17 +144,18 @@ public class GeyserVelocityPlugin implements GeyserBootstrap {
this.commandManager.register(entry.getKey().description().id(), new GeyserVelocityCommandExecutor(this.geyser, commands));
}
- if (geyserConfig.isLegacyPingPassthrough()) {
- this.geyserPingPassthrough = GeyserLegacyPingPassthrough.init(geyser);
- } else {
- this.geyserPingPassthrough = new GeyserVelocityPingPassthrough(proxyServer);
- }
-
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();
}
@@ -188,26 +186,24 @@ 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
geyserInjector.initializeLocalChannel(this);
}
-
- INITIALIZED = true;
}
}
@@ -242,4 +238,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) {
+ logger.error(GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex);
+ ex.printStackTrace();
+ return false;
+ }
+ return true;
+ }
}
diff --git a/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java b/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java
index 4dbc1dca3..a9414d9d0 100644
--- a/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java
+++ b/core/src/main/java/org/geysermc/geyser/GeyserBootstrap.java
@@ -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
diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java
index e9ea08260..5ed0c3947 100644
--- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java
+++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java
@@ -58,9 +58,7 @@ import org.geysermc.floodgate.news.NewsItemAction;
import org.geysermc.geyser.api.GeyserApi;
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;
@@ -145,6 +143,7 @@ public class GeyserImpl implements GeyserApi {
private UnixSocketClientListener erosionUnixListener;
+ @Setter
private volatile boolean shuttingDown = false;
private ScheduledExecutorService scheduledThread;
@@ -162,8 +161,14 @@ 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;
+
private GeyserImpl(PlatformType platformType, GeyserBootstrap bootstrap) {
instance = this;
@@ -172,13 +177,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));
}
@@ -236,11 +244,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();
@@ -536,12 +550,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
@@ -600,9 +617,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()));
@@ -616,7 +632,6 @@ public class GeyserImpl implements GeyserApi {
skinUploader.close();
}
newsHandler.shutdown();
- this.commandManager().getCommands().clear();
if (this.erosionUnixListener != null) {
this.erosionUnixListener.close();
@@ -624,16 +639,29 @@ public class GeyserImpl implements GeyserApi {
Registries.RESOURCE_PACKS.get().clear();
+ bootstrap.getGeyserLogger().info(GeyserLocale.getLocaleStringLog("geyser.core.shutdown.done"));
+ }
+
+ public void shutdown() {
+ shuttingDown = true;
+ this.disable();
+ this.commandManager().getCommands().clear();
+
+ // 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;
}
/**
@@ -744,9 +772,7 @@ 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();
@@ -797,8 +823,4 @@ public class GeyserImpl implements GeyserApi {
}
});
}
-
- public static GeyserImpl getInstance() {
- return instance;
- }
}
diff --git a/core/src/main/java/org/geysermc/geyser/command/GeyserCommandManager.java b/core/src/main/java/org/geysermc/geyser/command/GeyserCommandManager.java
index d646845c7..72ed22381 100644
--- a/core/src/main/java/org/geysermc/geyser/command/GeyserCommandManager.java
+++ b/core/src/main/java/org/geysermc/geyser/command/GeyserCommandManager.java
@@ -86,7 +86,7 @@ public class GeyserCommandManager {
registerBuiltInCommand(new StopCommand(geyser, "stop", "geyser.commands.stop.desc", "geyser.command.stop"));
}
- if (this.geyser.extensionManager().extensions().size() > 0) {
+ if (!this.geyser.extensionManager().extensions().isEmpty()) {
registerBuiltInCommand(new ExtensionsCommand(this.geyser, "extensions", "geyser.commands.extensions.desc", "geyser.command.extensions"));
}
diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/ReloadCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/ReloadCommand.java
index a3cd8fa4c..987860238 100644
--- a/core/src/main/java/org/geysermc/geyser/command/defaults/ReloadCommand.java
+++ b/core/src/main/java/org/geysermc/geyser/command/defaults/ReloadCommand.java
@@ -55,7 +55,7 @@ public class ReloadCommand extends GeyserCommand {
geyser.getSessionManager().disconnectAll("geyser.commands.reload.kick");
//FIXME Without the tiny wait, players do not get kicked - same happens when Geyser tries to disconnect all sessions on shutdown
- geyser.getScheduledThread().schedule(geyser::reload, 10, TimeUnit.MILLISECONDS);
+ geyser.getScheduledThread().schedule(geyser::reloadGeyser, 10, TimeUnit.MILLISECONDS);
}
@Override
diff --git a/core/src/main/java/org/geysermc/geyser/command/defaults/StopCommand.java b/core/src/main/java/org/geysermc/geyser/command/defaults/StopCommand.java
index 7db539cc5..1cd3050c9 100644
--- a/core/src/main/java/org/geysermc/geyser/command/defaults/StopCommand.java
+++ b/core/src/main/java/org/geysermc/geyser/command/defaults/StopCommand.java
@@ -52,7 +52,7 @@ public class StopCommand extends GeyserCommand {
return;
}
- geyser.getBootstrap().onDisable();
+ geyser.getBootstrap().onGeyserShutdown();
}
@Override
diff --git a/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.java b/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.java
new file mode 100644
index 000000000..c9ef7a2dd
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/entity/GeyserEntityData.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.entity;
+
+import org.checkerframework.checker.index.qual.NonNegative;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.cloudburstmc.protocol.bedrock.packet.EmotePacket;
+import org.geysermc.geyser.api.entity.EntityData;
+import org.geysermc.geyser.api.entity.type.GeyserEntity;
+import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity;
+import org.geysermc.geyser.entity.type.Entity;
+import org.geysermc.geyser.session.GeyserSession;
+
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
+public class GeyserEntityData implements EntityData {
+
+ private final GeyserSession session;
+
+ private final Set movementLockOwners = new HashSet<>();
+
+ public GeyserEntityData(GeyserSession session) {
+ this.session = session;
+ }
+
+ @Override
+ public @NonNull CompletableFuture<@Nullable GeyserEntity> entityByJavaId(@NonNegative int javaId) {
+ CompletableFuture future = new CompletableFuture<>();
+ session.ensureInEventLoop(() -> future.complete(session.getEntityCache().getEntityByJavaId(javaId)));
+ return future;
+ }
+
+ @Override
+ public void showEmote(@NonNull GeyserPlayerEntity emoter, @NonNull String emoteId) {
+ Objects.requireNonNull(emoter, "emoter must not be null!");
+ Entity entity = (Entity) emoter;
+ if (entity.getSession() != session) {
+ throw new IllegalStateException("Given entity must be from this session!");
+ }
+
+ EmotePacket packet = new EmotePacket();
+ packet.setRuntimeEntityId(entity.getGeyserId());
+ packet.setXuid("");
+ packet.setPlatformId(""); // BDS sends empty
+ packet.setEmoteId(emoteId);
+ session.sendUpstreamPacket(packet);
+ }
+
+ @Override
+ public @NonNull GeyserPlayerEntity playerEntity() {
+ return session.getPlayerEntity();
+ }
+
+ @Override
+ public boolean lockMovement(boolean lock, @NonNull UUID owner) {
+ Objects.requireNonNull(owner, "owner must not be null!");
+ if (lock) {
+ movementLockOwners.add(owner);
+ } else {
+ movementLockOwners.remove(owner);
+ }
+
+ session.lockInputs(session.camera().isCameraLocked(), isMovementLocked());
+ return isMovementLocked();
+ }
+
+ @Override
+ public boolean isMovementLocked() {
+ return !movementLockOwners.isEmpty();
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java
index 7504db1b1..9e3888138 100644
--- a/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java
+++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java
@@ -39,12 +39,20 @@ import net.kyori.adventure.text.Component;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.math.vector.Vector3i;
-import org.cloudburstmc.protocol.bedrock.data.*;
+import org.cloudburstmc.protocol.bedrock.data.Ability;
+import org.cloudburstmc.protocol.bedrock.data.AbilityLayer;
+import org.cloudburstmc.protocol.bedrock.data.AttributeData;
+import org.cloudburstmc.protocol.bedrock.data.GameType;
+import org.cloudburstmc.protocol.bedrock.data.PlayerPermission;
import org.cloudburstmc.protocol.bedrock.data.command.CommandPermission;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityLinkData;
-import org.cloudburstmc.protocol.bedrock.packet.*;
+import org.cloudburstmc.protocol.bedrock.packet.AddPlayerPacket;
+import org.cloudburstmc.protocol.bedrock.packet.MovePlayerPacket;
+import org.cloudburstmc.protocol.bedrock.packet.SetEntityDataPacket;
+import org.cloudburstmc.protocol.bedrock.packet.SetEntityLinkPacket;
+import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket;
import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity;
import org.geysermc.geyser.entity.EntityDefinitions;
import org.geysermc.geyser.entity.type.Entity;
@@ -143,6 +151,10 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
addPlayerPacket.setGameType(GameType.SURVIVAL); //TODO
addPlayerPacket.setAbilityLayers(BASE_ABILITY_LAYER); // Recommended to be added since 1.19.10, but only needed here for permissions viewing
addPlayerPacket.getMetadata().putFlags(flags);
+
+ // Since 1.20.60, the nametag does not show properly if this is not set :/
+ // The nametag does disappear properly when the player is invisible though.
+ dirtyMetadata.put(EntityDataTypes.NAMETAG_ALWAYS_SHOW, (byte) 1);
dirtyMetadata.apply(addPlayerPacket.getMetadata());
setFlagsDirty(false);
@@ -433,4 +445,9 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
public UUID getTabListUuid() {
return getUuid();
}
+
+ @Override
+ public Vector3f position() {
+ return this.position.clone();
+ }
}
diff --git a/core/src/main/java/org/geysermc/geyser/impl/camera/CameraDefinitions.java b/core/src/main/java/org/geysermc/geyser/impl/camera/CameraDefinitions.java
new file mode 100644
index 000000000..80564bdf3
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/impl/camera/CameraDefinitions.java
@@ -0,0 +1,96 @@
+/*
+ * 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.impl.camera;
+
+import org.cloudburstmc.protocol.bedrock.data.camera.CameraAudioListener;
+import org.cloudburstmc.protocol.bedrock.data.camera.CameraPreset;
+import org.cloudburstmc.protocol.common.DefinitionRegistry;
+import org.cloudburstmc.protocol.common.NamedDefinition;
+import org.cloudburstmc.protocol.common.SimpleDefinitionRegistry;
+import org.cloudburstmc.protocol.common.util.OptionalBoolean;
+import org.geysermc.geyser.api.bedrock.camera.CameraPerspective;
+
+import java.util.List;
+
+public class CameraDefinitions {
+
+ public static final DefinitionRegistry CAMERA_DEFINITIONS;
+
+ public static final List CAMERA_PRESETS;
+
+ static {
+ CAMERA_PRESETS = List.of(
+ new CameraPreset(CameraPerspective.FIRST_PERSON.id(), "", null, null, null, null, OptionalBoolean.empty()),
+ new CameraPreset(CameraPerspective.FREE.id(), "", null, null, null, null, OptionalBoolean.empty()),
+ new CameraPreset(CameraPerspective.THIRD_PERSON.id(), "", null, null, null, null, OptionalBoolean.empty()),
+ new CameraPreset(CameraPerspective.THIRD_PERSON_FRONT.id(), "", null, null, null, null, OptionalBoolean.empty()),
+ new CameraPreset("geyser:free_audio", "minecraft:free", null, null, null, CameraAudioListener.PLAYER, OptionalBoolean.of(false)),
+ new CameraPreset("geyser:free_effects", "minecraft:free", null, null, null, CameraAudioListener.CAMERA, OptionalBoolean.of(true)),
+ new CameraPreset("geyser:free_audio_effects", "minecraft:free", null, null, null, CameraAudioListener.PLAYER, OptionalBoolean.of(true)));
+
+ SimpleDefinitionRegistry.Builder builder = SimpleDefinitionRegistry.builder();
+ for (int i = 0; i < CAMERA_PRESETS.size(); i++) {
+ builder.add(CameraDefinition.of(CAMERA_PRESETS.get(i).getIdentifier(), i));
+ }
+ CAMERA_DEFINITIONS = builder.build();
+ }
+
+ public static NamedDefinition getById(int id) {
+ return CAMERA_DEFINITIONS.getDefinition(id);
+ }
+
+ public static NamedDefinition getByFunctionality(boolean audio, boolean effects) {
+ if (!audio && !effects) {
+ return getById(1); // FREE
+ }
+ if (audio) {
+ if (effects) {
+ return getById(6); // FREE_AUDIO_EFFECTS
+ } else {
+ return getById(4); // FREE_AUDIO
+ }
+ } else {
+ return getById(5); // FREE_EFFECTS
+ }
+ }
+
+ public record CameraDefinition(String identifier, int runtimeId) implements NamedDefinition {
+
+ @Override
+ public String getIdentifier() {
+ return identifier;
+ }
+
+ @Override
+ public int getRuntimeId() {
+ return runtimeId;
+ }
+
+ public static CameraDefinition of(String identifier, int runtimeId) {
+ return new CameraDefinition(identifier, runtimeId);
+ }
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/impl/camera/GeyserCameraData.java b/core/src/main/java/org/geysermc/geyser/impl/camera/GeyserCameraData.java
new file mode 100644
index 000000000..28c881eba
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/impl/camera/GeyserCameraData.java
@@ -0,0 +1,235 @@
+/*
+ * 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.impl.camera;
+
+import lombok.Getter;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.cloudburstmc.math.vector.Vector2f;
+import org.cloudburstmc.math.vector.Vector3f;
+import org.cloudburstmc.protocol.bedrock.data.CameraShakeAction;
+import org.cloudburstmc.protocol.bedrock.data.CameraShakeType;
+import org.cloudburstmc.protocol.bedrock.data.camera.CameraEase;
+import org.cloudburstmc.protocol.bedrock.data.camera.CameraFadeInstruction;
+import org.cloudburstmc.protocol.bedrock.data.camera.CameraSetInstruction;
+import org.cloudburstmc.protocol.bedrock.packet.CameraInstructionPacket;
+import org.cloudburstmc.protocol.bedrock.packet.CameraShakePacket;
+import org.cloudburstmc.protocol.bedrock.packet.PlayerFogPacket;
+import org.geysermc.geyser.api.bedrock.camera.CameraEaseType;
+import org.geysermc.geyser.api.bedrock.camera.CameraData;
+import org.geysermc.geyser.api.bedrock.camera.CameraFade;
+import org.geysermc.geyser.api.bedrock.camera.CameraPerspective;
+import org.geysermc.geyser.api.bedrock.camera.CameraPosition;
+import org.geysermc.geyser.api.bedrock.camera.CameraShake;
+import org.geysermc.geyser.session.GeyserSession;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.UUID;
+
+public class GeyserCameraData implements CameraData {
+
+ private final GeyserSession session;
+
+ @Getter
+ private CameraPerspective cameraPerspective;
+
+ /**
+ * All fog effects that are currently applied to the client.
+ */
+ private final Set appliedFog = new HashSet<>();
+
+ private final Set cameraLockOwners = new HashSet<>();
+
+ public GeyserCameraData(GeyserSession session) {
+ this.session = session;
+ }
+
+ @Override
+ public void clearCameraInstructions() {
+ this.cameraPerspective = null;
+ CameraInstructionPacket packet = new CameraInstructionPacket();
+ packet.setClear(true);
+ session.sendUpstreamPacket(packet);
+ }
+
+ @Override
+ public void forceCameraPerspective(@NonNull CameraPerspective perspective) {
+ Objects.requireNonNull(perspective, "perspective cannot be null!");
+
+ if (perspective == cameraPerspective) {
+ return; // nothing to do
+ }
+
+ this.cameraPerspective = perspective;
+ CameraInstructionPacket packet = new CameraInstructionPacket();
+ CameraSetInstruction setInstruction = new CameraSetInstruction();
+
+ if (perspective == CameraPerspective.FREE) {
+ throw new IllegalArgumentException("Cannot force a stationary camera (CameraPerspective#FREE) on the player!" +
+ "Send a CameraPosition with an exact position instead");
+ }
+
+ setInstruction.setPreset(CameraDefinitions.getById(perspective.ordinal()));
+ packet.setSetInstruction(setInstruction);
+ session.sendUpstreamPacket(packet);
+ }
+
+ @Override
+ public @Nullable CameraPerspective forcedCameraPerspective() {
+ return this.cameraPerspective;
+ }
+
+ @Override
+ public void sendCameraFade(@NonNull CameraFade fade) {
+ Objects.requireNonNull(fade, "fade cannot be null!");
+ CameraFadeInstruction fadeInstruction = new CameraFadeInstruction();
+ fadeInstruction.setColor(fade.color());
+ fadeInstruction.setTimeData(
+ new CameraFadeInstruction.TimeData(
+ fade.fadeInSeconds(),
+ fade.fadeHoldSeconds(),
+ fade.fadeOutSeconds()
+ )
+ );
+
+ CameraInstructionPacket packet = new CameraInstructionPacket();
+ packet.setFadeInstruction(fadeInstruction);
+ session.sendUpstreamPacket(packet);
+ }
+
+ @Override
+ public void sendCameraPosition(@NonNull CameraPosition movement) {
+ Objects.requireNonNull(movement, "movement cannot be null!");
+ this.cameraPerspective = CameraPerspective.FREE; // Movements only work with the free preset
+ CameraSetInstruction setInstruction = new CameraSetInstruction();
+
+ CameraEaseType easeType = movement.easeType();
+ if (easeType != null) {
+ setInstruction.setEase(new CameraSetInstruction.EaseData(
+ CameraEase.fromName(easeType.id()),
+ movement.easeSeconds()
+ ));
+ }
+
+ Vector3f facingPosition = movement.facingPosition();
+ if (facingPosition != null) {
+ setInstruction.setFacing(facingPosition);
+ }
+
+ setInstruction.setPos(movement.position());
+ setInstruction.setRot(Vector2f.from(movement.rotationX(), movement.rotationY()));
+ setInstruction.setPreset(CameraDefinitions.getByFunctionality(movement.playerPositionForAudio(), movement.renderPlayerEffects()));
+
+ CameraInstructionPacket packet = new CameraInstructionPacket();
+ packet.setSetInstruction(setInstruction);
+
+ // If present, also send the fade
+ CameraFade fade = movement.cameraFade();
+ if (fade != null) {
+ CameraFadeInstruction fadeInstruction = new CameraFadeInstruction();
+ fadeInstruction.setColor(fade.color());
+ fadeInstruction.setTimeData(
+ new CameraFadeInstruction.TimeData(
+ fade.fadeInSeconds(),
+ fade.fadeHoldSeconds(),
+ fade.fadeOutSeconds()
+ )
+ );
+ packet.setFadeInstruction(fadeInstruction);
+ }
+ session.sendUpstreamPacket(packet);
+ }
+
+ @Override
+ public void shakeCamera(float intensity, float duration, @NonNull CameraShake type) {
+ Objects.requireNonNull(type, "camera shake type must be non null!");
+ CameraShakePacket packet = new CameraShakePacket();
+ packet.setIntensity(intensity);
+ packet.setDuration(duration);
+ packet.setShakeType(type == CameraShake.POSITIONAL ? CameraShakeType.POSITIONAL : CameraShakeType.ROTATIONAL);
+ packet.setShakeAction(CameraShakeAction.ADD);
+ session.sendUpstreamPacket(packet);
+ }
+
+ @Override
+ public void stopCameraShake() {
+ CameraShakePacket packet = new CameraShakePacket();
+ // CameraShakeAction.STOP removes all types regardless of the given type, but regardless it can't be null
+ packet.setShakeType(CameraShakeType.POSITIONAL);
+ packet.setShakeAction(CameraShakeAction.STOP);
+ session.sendUpstreamPacket(packet);
+ }
+
+ @Override
+ public void sendFog(String... fogNameSpaces) {
+ Collections.addAll(this.appliedFog, fogNameSpaces);
+
+ PlayerFogPacket packet = new PlayerFogPacket();
+ packet.getFogStack().addAll(this.appliedFog);
+ session.sendUpstreamPacket(packet);
+ }
+
+ @Override
+ public void removeFog(String... fogNameSpaces) {
+ if (fogNameSpaces.length == 0) {
+ this.appliedFog.clear();
+ } else {
+ for (String id : fogNameSpaces) {
+ this.appliedFog.remove(id);
+ }
+ }
+ PlayerFogPacket packet = new PlayerFogPacket();
+ packet.getFogStack().addAll(this.appliedFog);
+ session.sendUpstreamPacket(packet);
+ }
+
+ @Override
+ public @NonNull Set fogEffects() {
+ // Use a copy so that sendFog/removeFog can be called while iterating the returned set (avoid CME)
+ return Set.copyOf(this.appliedFog);
+ }
+
+ @Override
+ public boolean lockCamera(boolean lock, @NonNull UUID owner) {
+ Objects.requireNonNull(owner, "owner cannot be null!");
+ if (lock) {
+ this.cameraLockOwners.add(owner);
+ } else {
+ this.cameraLockOwners.remove(owner);
+ }
+
+ session.lockInputs(isCameraLocked(), session.entities().isMovementLocked());
+ return isCameraLocked();
+ }
+
+ @Override
+ public boolean isCameraLocked() {
+ return !this.cameraLockOwners.isEmpty();
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/java/org/geysermc/geyser/impl/camera/GeyserCameraFade.java b/core/src/main/java/org/geysermc/geyser/impl/camera/GeyserCameraFade.java
new file mode 100644
index 000000000..648e70c81
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/impl/camera/GeyserCameraFade.java
@@ -0,0 +1,104 @@
+/*
+ * 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.impl.camera;
+
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.common.value.qual.IntRange;
+import org.geysermc.geyser.api.bedrock.camera.CameraFade;
+
+import java.awt.Color;
+import java.util.Objects;
+
+public record GeyserCameraFade(
+ Color color,
+ float fadeInSeconds,
+ float fadeHoldSeconds,
+ float fadeOutSeconds
+
+) implements CameraFade {
+ public static class Builder implements CameraFade.Builder {
+ private Color color;
+ private float fadeInSeconds;
+ private float fadeHoldSeconds;
+ private float fadeOutSeconds;
+
+ @Override
+ public CameraFade.Builder color(@NonNull Color color) {
+ Objects.requireNonNull(color, "color cannot be null!");
+ this.color = color;
+ return this;
+ }
+
+ @Override
+ public CameraFade.Builder fadeInSeconds(@IntRange(from = 0, to = 10) float fadeInSeconds) {
+ if (fadeInSeconds < 0f) {
+ throw new IllegalArgumentException("Fade in seconds must be at least 0 seconds");
+ }
+
+ if (fadeInSeconds > 10f) {
+ throw new IllegalArgumentException("Fade in seconds must be at most 10 seconds");
+ }
+ this.fadeInSeconds = fadeInSeconds;
+ return this;
+ }
+
+ @Override
+ public CameraFade.Builder fadeHoldSeconds(@IntRange(from = 0, to = 10) float fadeHoldSeconds) {
+ if (fadeHoldSeconds < 0f) {
+ throw new IllegalArgumentException("Fade hold seconds must be at least 0 seconds");
+ }
+
+ if (fadeHoldSeconds > 10f) {
+ throw new IllegalArgumentException("Fade hold seconds must be at most 10 seconds");
+ }
+ this.fadeHoldSeconds = fadeHoldSeconds;
+ return this;
+ }
+
+ @Override
+ public CameraFade.Builder fadeOutSeconds(@IntRange(from = 0, to = 10) float fadeOutSeconds) {
+ if (fadeOutSeconds < 0f) {
+ throw new IllegalArgumentException("Fade out seconds must be at least 0 seconds");
+ }
+
+ if (fadeOutSeconds > 10f) {
+ throw new IllegalArgumentException("Fade out seconds must be at most 10 seconds");
+ }
+ this.fadeOutSeconds = fadeOutSeconds;
+ return this;
+ }
+
+ @Override
+ public CameraFade build() {
+ Objects.requireNonNull(color, "color must be non null!");
+ if (fadeInSeconds + fadeHoldSeconds + fadeOutSeconds < 0.5f) {
+ throw new IllegalArgumentException("Total fade time (in, hold, out) must be at least 0.5 seconds");
+ }
+
+ return new GeyserCameraFade(color, fadeInSeconds, fadeHoldSeconds, fadeOutSeconds);
+ }
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/impl/camera/GeyserCameraPosition.java b/core/src/main/java/org/geysermc/geyser/impl/camera/GeyserCameraPosition.java
new file mode 100644
index 000000000..13b382465
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/impl/camera/GeyserCameraPosition.java
@@ -0,0 +1,131 @@
+/*
+ * 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.impl.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.bedrock.camera.CameraEaseType;
+import org.geysermc.geyser.api.bedrock.camera.CameraFade;
+import org.geysermc.geyser.api.bedrock.camera.CameraPosition;
+
+import java.util.Objects;
+
+public record GeyserCameraPosition(CameraFade cameraFade,
+ boolean renderPlayerEffects,
+ boolean playerPositionForAudio,
+ CameraEaseType easeType,
+ float easeSeconds,
+ Vector3f position,
+ @IntRange(from = -90, to = 90) int rotationX,
+ int rotationY,
+ Vector3f facingPosition
+) implements CameraPosition {
+
+ public static class Builder implements CameraPosition.Builder {
+ private CameraFade cameraFade;
+ private boolean renderPlayerEffects;
+ private boolean playerPositionForAudio;
+ private CameraEaseType easeType;
+ private float easeSeconds;
+ private Vector3f position;
+ private @IntRange(from = -90, to = 90) int rotationX;
+ private int rotationY;
+ private Vector3f facingPosition;
+
+ @Override
+ public CameraPosition.Builder cameraFade(@Nullable CameraFade cameraFade) {
+ this.cameraFade = cameraFade;
+ return this;
+ }
+
+ @Override
+ public CameraPosition.Builder renderPlayerEffects(boolean renderPlayerEffects) {
+ this.renderPlayerEffects = renderPlayerEffects;
+ return this;
+ }
+
+ @Override
+ public CameraPosition.Builder playerPositionForAudio(boolean playerPositionForAudio) {
+ this.playerPositionForAudio = playerPositionForAudio;
+ return this;
+ }
+
+ @Override
+ public CameraPosition.Builder easeType(@Nullable CameraEaseType easeType) {
+ this.easeType = easeType;
+ return this;
+ }
+
+ @Override
+ public CameraPosition.Builder easeSeconds(float easeSeconds) {
+ if (easeSeconds < 0) {
+ throw new IllegalArgumentException("Camera ease duration cannot be negative!");
+ }
+ this.easeSeconds = easeSeconds;
+ return this;
+ }
+
+ @Override
+ public CameraPosition.Builder position(@NonNull Vector3f position) {
+ Objects.requireNonNull(position, "camera position cannot be null!");
+ this.position = position;
+ return this;
+ }
+
+ @Override
+ public CameraPosition.Builder rotationX(int rotationX) {
+ if (rotationX < -90 || rotationX > 90) {
+ throw new IllegalArgumentException("x-axis rotation needs to be between -90 and 90 degrees.");
+ }
+ this.rotationX = rotationX;
+ return this;
+ }
+
+ @Override
+ public CameraPosition.Builder rotationY(int rotationY) {
+ this.rotationY = rotationY;
+ return this;
+ }
+
+ @Override
+ public CameraPosition.Builder facingPosition(@Nullable Vector3f facingPosition) {
+ this.facingPosition = facingPosition;
+ return this;
+ }
+
+ @Override
+ public CameraPosition build() {
+ if (easeSeconds > 0 && easeType == null) {
+ throw new IllegalArgumentException("Camera ease type cannot be null if ease duration is greater than 0");
+ }
+
+ Objects.requireNonNull(position, "camera position must be non null!");
+ return new GeyserCameraPosition(cameraFade, renderPlayerEffects, playerPositionForAudio, easeType, easeSeconds, position, rotationX, rotationY, facingPosition);
+ }
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/item/GeyserCustomItemData.java b/core/src/main/java/org/geysermc/geyser/item/GeyserCustomItemData.java
index c86c370bb..2906a9be3 100644
--- a/core/src/main/java/org/geysermc/geyser/item/GeyserCustomItemData.java
+++ b/core/src/main/java/org/geysermc/geyser/item/GeyserCustomItemData.java
@@ -115,7 +115,7 @@ public class GeyserCustomItemData implements CustomItemData {
return tags;
}
- public static class CustomItemDataBuilder implements Builder {
+ public static class Builder implements CustomItemData.Builder {
protected String name = null;
protected CustomItemOptions customItemOptions = null;
diff --git a/core/src/main/java/org/geysermc/geyser/item/GeyserCustomItemOptions.java b/core/src/main/java/org/geysermc/geyser/item/GeyserCustomItemOptions.java
index 1434c49d9..966035743 100644
--- a/core/src/main/java/org/geysermc/geyser/item/GeyserCustomItemOptions.java
+++ b/core/src/main/java/org/geysermc/geyser/item/GeyserCustomItemOptions.java
@@ -37,7 +37,7 @@ public record GeyserCustomItemOptions(TriState unbreakable,
boolean defaultItem) implements CustomItemOptions {
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
- public static class CustomItemOptionsBuilder implements CustomItemOptions.Builder {
+ public static class Builder implements CustomItemOptions.Builder {
private TriState unbreakable = TriState.NOT_SET;
private OptionalInt customModelData = OptionalInt.empty();
private OptionalInt damagePredicate = OptionalInt.empty();
diff --git a/core/src/main/java/org/geysermc/geyser/item/GeyserNonVanillaCustomItemData.java b/core/src/main/java/org/geysermc/geyser/item/GeyserNonVanillaCustomItemData.java
index 52b0ee7d0..bb4e60589 100644
--- a/core/src/main/java/org/geysermc/geyser/item/GeyserNonVanillaCustomItemData.java
+++ b/core/src/main/java/org/geysermc/geyser/item/GeyserNonVanillaCustomItemData.java
@@ -59,7 +59,7 @@ public final class GeyserNonVanillaCustomItemData extends GeyserCustomItemData i
private final boolean canAlwaysEat;
private final boolean isChargeable;
- public GeyserNonVanillaCustomItemData(NonVanillaCustomItemDataBuilder builder) {
+ public GeyserNonVanillaCustomItemData(Builder builder) {
super(builder.name, builder.customItemOptions, builder.displayName, builder.icon, builder.allowOffhand,
builder.displayHandheld, builder.textureSize, builder.renderOffsets, builder.tags);
@@ -168,7 +168,7 @@ public final class GeyserNonVanillaCustomItemData extends GeyserCustomItemData i
return isChargeable;
}
- public static class NonVanillaCustomItemDataBuilder extends GeyserCustomItemData.CustomItemDataBuilder implements NonVanillaCustomItemData.Builder {
+ public static class Builder extends GeyserCustomItemData.Builder implements NonVanillaCustomItemData.Builder {
private String identifier = null;
private int javaId = -1;
@@ -197,49 +197,49 @@ public final class GeyserNonVanillaCustomItemData extends GeyserCustomItemData i
private boolean chargeable = false;
@Override
- public NonVanillaCustomItemData.Builder name(@NonNull String name) {
- return (NonVanillaCustomItemData.Builder) super.name(name);
+ public Builder name(@NonNull String name) {
+ return (Builder) super.name(name);
}
@Override
- public NonVanillaCustomItemData.Builder customItemOptions(@NonNull CustomItemOptions customItemOptions) {
+ public Builder customItemOptions(@NonNull CustomItemOptions customItemOptions) {
//Do nothing, as that value won't be read
return this;
}
@Override
- public NonVanillaCustomItemData.Builder allowOffhand(boolean allowOffhand) {
- return (NonVanillaCustomItemData.Builder) super.allowOffhand(allowOffhand);
+ public Builder allowOffhand(boolean allowOffhand) {
+ return (Builder) super.allowOffhand(allowOffhand);
}
@Override
- public NonVanillaCustomItemData.Builder displayHandheld(boolean displayHandheld) {
- return (NonVanillaCustomItemData.Builder) super.displayHandheld(displayHandheld);
+ public Builder displayHandheld(boolean displayHandheld) {
+ return (Builder) super.displayHandheld(displayHandheld);
}
@Override
- public NonVanillaCustomItemData.Builder displayName(@NonNull String displayName) {
- return (NonVanillaCustomItemData.Builder) super.displayName(displayName);
+ public Builder displayName(@NonNull String displayName) {
+ return (Builder) super.displayName(displayName);
}
@Override
- public NonVanillaCustomItemData.Builder icon(@NonNull String icon) {
- return (NonVanillaCustomItemData.Builder) super.icon(icon);
+ public Builder icon(@NonNull String icon) {
+ return (Builder) super.icon(icon);
}
@Override
- public NonVanillaCustomItemData.Builder textureSize(int textureSize) {
- return (NonVanillaCustomItemData.Builder) super.textureSize(textureSize);
+ public Builder textureSize(int textureSize) {
+ return (Builder) super.textureSize(textureSize);
}
@Override
- public NonVanillaCustomItemData.Builder renderOffsets(CustomRenderOffsets renderOffsets) {
- return (NonVanillaCustomItemData.Builder) super.renderOffsets(renderOffsets);
+ public Builder renderOffsets(CustomRenderOffsets renderOffsets) {
+ return (Builder) super.renderOffsets(renderOffsets);
}
@Override
- public NonVanillaCustomItemData.Builder tags(@Nullable Set tags) {
- return (NonVanillaCustomItemData.Builder) super.tags(tags);
+ public Builder tags(@Nullable Set tags) {
+ return (Builder) super.tags(tags);
}
@Override
diff --git a/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockComponents.java b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockComponents.java
index e401567e2..1fa863d55 100644
--- a/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockComponents.java
+++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockComponents.java
@@ -62,7 +62,7 @@ public class GeyserCustomBlockComponents implements CustomBlockComponents {
boolean placeAir;
Set tags;
- private GeyserCustomBlockComponents(CustomBlockComponentsBuilder builder) {
+ private GeyserCustomBlockComponents(Builder builder) {
this.selectionBox = builder.selectionBox;
this.collisionBox = builder.collisionBox;
this.displayName = builder.displayName;
@@ -157,7 +157,7 @@ public class GeyserCustomBlockComponents implements CustomBlockComponents {
return tags;
}
- public static class CustomBlockComponentsBuilder implements Builder {
+ public static class Builder implements CustomBlockComponents.Builder {
protected BoxComponent selectionBox;
protected BoxComponent collisionBox;
protected String displayName;
diff --git a/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockData.java b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockData.java
index dd58ebcb7..d717e33c5 100644
--- a/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockData.java
+++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockData.java
@@ -54,7 +54,7 @@ public class GeyserCustomBlockData implements CustomBlockData {
private final Map defaultProperties;
- GeyserCustomBlockData(CustomBlockDataBuilder builder) {
+ GeyserCustomBlockData(Builder builder) {
this.name = builder.name;
if (name == null) {
throw new IllegalStateException("Name must be set");
@@ -141,10 +141,10 @@ public class GeyserCustomBlockData implements CustomBlockData {
@Override
public CustomBlockState.@NonNull Builder blockStateBuilder() {
- return new GeyserCustomBlockState.CustomBlockStateBuilder(this);
+ return new GeyserCustomBlockState.Builder(this);
}
- public static class CustomBlockDataBuilder implements Builder {
+ public static class Builder implements CustomBlockData.Builder {
private String name;
private boolean includedInCreativeInventory;
private CreativeCategory creativeCategory;
diff --git a/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockState.java b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockState.java
index d147ffedc..b9a8fe5a2 100644
--- a/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockState.java
+++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserCustomBlockState.java
@@ -64,7 +64,7 @@ public class GeyserCustomBlockState implements CustomBlockState {
}
@RequiredArgsConstructor
- public static class CustomBlockStateBuilder implements CustomBlockState.Builder {
+ public static class Builder implements CustomBlockState.Builder {
private final CustomBlockData blockData;
private final Object2ObjectMap properties = new Object2ObjectOpenHashMap<>();
diff --git a/core/src/main/java/org/geysermc/geyser/level/block/GeyserGeometryComponent.java b/core/src/main/java/org/geysermc/geyser/level/block/GeyserGeometryComponent.java
index c23fc87a2..1e2d13ae6 100644
--- a/core/src/main/java/org/geysermc/geyser/level/block/GeyserGeometryComponent.java
+++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserGeometryComponent.java
@@ -37,7 +37,7 @@ public class GeyserGeometryComponent implements GeometryComponent {
private final String identifier;
private final Map boneVisibility;
- GeyserGeometryComponent(GeometryComponentBuilder builder) {
+ GeyserGeometryComponent(Builder builder) {
this.identifier = builder.identifier;
this.boneVisibility = builder.boneVisibility;
}
@@ -52,18 +52,18 @@ public class GeyserGeometryComponent implements GeometryComponent {
return boneVisibility;
}
- public static class GeometryComponentBuilder implements Builder {
+ public static class Builder implements GeometryComponent.Builder {
private String identifier;
private Map boneVisibility;
@Override
- public GeometryComponent.Builder identifier(@NonNull String identifier) {
+ public Builder identifier(@NonNull String identifier) {
this.identifier = identifier;
return this;
}
@Override
- public GeometryComponent.Builder boneVisibility(@Nullable Map boneVisibility) {
+ public Builder boneVisibility(@Nullable Map boneVisibility) {
this.boneVisibility = boneVisibility;
return this;
}
diff --git a/core/src/main/java/org/geysermc/geyser/level/block/GeyserJavaBlockState.java b/core/src/main/java/org/geysermc/geyser/level/block/GeyserJavaBlockState.java
index 5604a543e..8028a4355 100644
--- a/core/src/main/java/org/geysermc/geyser/level/block/GeyserJavaBlockState.java
+++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserJavaBlockState.java
@@ -18,7 +18,7 @@ public class GeyserJavaBlockState implements JavaBlockState {
String pistonBehavior;
boolean hasBlockEntity;
- private GeyserJavaBlockState(JavaBlockStateBuilder builder) {
+ private GeyserJavaBlockState(Builder builder) {
this.identifier = builder.identifier;
this.javaId = builder.javaId;
this.stateGroupId = builder.stateGroupId;
@@ -81,7 +81,7 @@ public class GeyserJavaBlockState implements JavaBlockState {
return hasBlockEntity;
}
- public static class JavaBlockStateBuilder implements Builder {
+ public static class Builder implements JavaBlockState.Builder {
private String identifier;
private int javaId;
private int stateGroupId;
diff --git a/core/src/main/java/org/geysermc/geyser/level/block/GeyserMaterialInstance.java b/core/src/main/java/org/geysermc/geyser/level/block/GeyserMaterialInstance.java
index 527b8fe79..acc16bf58 100644
--- a/core/src/main/java/org/geysermc/geyser/level/block/GeyserMaterialInstance.java
+++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserMaterialInstance.java
@@ -36,7 +36,7 @@ public class GeyserMaterialInstance implements MaterialInstance {
private final boolean faceDimming;
private final boolean ambientOcclusion;
- GeyserMaterialInstance(MaterialInstanceBuilder builder) {
+ GeyserMaterialInstance(Builder builder) {
this.texture = builder.texture;
this.renderMethod = builder.renderMethod;
this.faceDimming = builder.faceDimming;
@@ -63,7 +63,7 @@ public class GeyserMaterialInstance implements MaterialInstance {
return ambientOcclusion;
}
- public static class MaterialInstanceBuilder implements Builder {
+ public static class Builder implements MaterialInstance.Builder {
private String texture;
private String renderMethod;
private boolean faceDimming;
diff --git a/core/src/main/java/org/geysermc/geyser/level/block/GeyserNonVanillaCustomBlockData.java b/core/src/main/java/org/geysermc/geyser/level/block/GeyserNonVanillaCustomBlockData.java
index 8b5056a6f..1f557d0b0 100644
--- a/core/src/main/java/org/geysermc/geyser/level/block/GeyserNonVanillaCustomBlockData.java
+++ b/core/src/main/java/org/geysermc/geyser/level/block/GeyserNonVanillaCustomBlockData.java
@@ -37,7 +37,7 @@ import java.util.List;
public class GeyserNonVanillaCustomBlockData extends GeyserCustomBlockData implements NonVanillaCustomBlockData {
private final String namespace;
- GeyserNonVanillaCustomBlockData(NonVanillaCustomBlockDataBuilder builder) {
+ GeyserNonVanillaCustomBlockData(Builder builder) {
super(builder);
this.namespace = builder.namespace;
@@ -56,58 +56,58 @@ public class GeyserNonVanillaCustomBlockData extends GeyserCustomBlockData imple
return this.namespace;
}
- public static class NonVanillaCustomBlockDataBuilder extends CustomBlockDataBuilder implements NonVanillaCustomBlockData.Builder {
+ public static class Builder extends GeyserCustomBlockData.Builder implements NonVanillaCustomBlockData.Builder {
private String namespace;
@Override
- public NonVanillaCustomBlockDataBuilder namespace(@NonNull String namespace) {
+ public Builder namespace(@NonNull String namespace) {
this.namespace = namespace;
return this;
}
@Override
- public NonVanillaCustomBlockDataBuilder name(@NonNull String name) {
- return (NonVanillaCustomBlockDataBuilder) super.name(name);
+ public Builder name(@NonNull String name) {
+ return (Builder) super.name(name);
}
@Override
- public NonVanillaCustomBlockDataBuilder includedInCreativeInventory(boolean includedInCreativeInventory) {
- return (NonVanillaCustomBlockDataBuilder) super.includedInCreativeInventory(includedInCreativeInventory);
+ public Builder includedInCreativeInventory(boolean includedInCreativeInventory) {
+ return (Builder) super.includedInCreativeInventory(includedInCreativeInventory);
}
@Override
- public NonVanillaCustomBlockDataBuilder creativeCategory(@Nullable CreativeCategory creativeCategories) {
- return (NonVanillaCustomBlockDataBuilder) super.creativeCategory(creativeCategories);
+ public Builder creativeCategory(@Nullable CreativeCategory creativeCategories) {
+ return (Builder) super.creativeCategory(creativeCategories);
}
@Override
- public NonVanillaCustomBlockDataBuilder creativeGroup(@Nullable String creativeGroup) {
- return (NonVanillaCustomBlockDataBuilder) super.creativeGroup(creativeGroup);
+ public Builder creativeGroup(@Nullable String creativeGroup) {
+ return (Builder) super.creativeGroup(creativeGroup);
}
@Override
- public NonVanillaCustomBlockDataBuilder components(@NonNull CustomBlockComponents components) {
- return (NonVanillaCustomBlockDataBuilder) super.components(components);
+ public Builder components(@NonNull CustomBlockComponents components) {
+ return (Builder) super.components(components);
}
@Override
- public NonVanillaCustomBlockDataBuilder booleanProperty(@NonNull String propertyName) {
- return (NonVanillaCustomBlockDataBuilder) super.booleanProperty(propertyName);
+ public Builder booleanProperty(@NonNull String propertyName) {
+ return (Builder) super.booleanProperty(propertyName);
}
@Override
- public NonVanillaCustomBlockDataBuilder intProperty(@NonNull String propertyName, List values) {
- return (NonVanillaCustomBlockDataBuilder) super.intProperty(propertyName, values);
+ public Builder intProperty(@NonNull String propertyName, List values) {
+ return (Builder) super.intProperty(propertyName, values);
}
@Override
- public NonVanillaCustomBlockDataBuilder stringProperty(@NonNull String propertyName, List values) {
- return (NonVanillaCustomBlockDataBuilder) super.stringProperty(propertyName, values);
+ public Builder stringProperty(@NonNull String propertyName, List values) {
+ return (Builder) super.stringProperty(propertyName, values);
}
@Override
- public NonVanillaCustomBlockDataBuilder permutations(@NonNull List permutations) {
- return (NonVanillaCustomBlockDataBuilder) super.permutations(permutations);
+ public Builder permutations(@NonNull List permutations) {
+ return (Builder) super.permutations(permutations);
}
@Override
diff --git a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java
index 42cce607b..a03a36ad2 100644
--- a/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java
+++ b/core/src/main/java/org/geysermc/geyser/network/GameProtocol.java
@@ -31,6 +31,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec;
import org.cloudburstmc.protocol.bedrock.codec.v622.Bedrock_v622;
import org.cloudburstmc.protocol.bedrock.codec.v630.Bedrock_v630;
+import org.cloudburstmc.protocol.bedrock.codec.v649.Bedrock_v649;
import org.cloudburstmc.protocol.bedrock.netty.codec.packet.BedrockPacketCodec;
import org.geysermc.geyser.session.GeyserSession;
@@ -46,7 +47,7 @@ public final class GameProtocol {
* Default Bedrock codec that should act as a fallback. Should represent the latest available
* release of the game that Geyser supports.
*/
- public static final BedrockCodec DEFAULT_BEDROCK_CODEC = Bedrock_v630.CODEC;
+ public static final BedrockCodec DEFAULT_BEDROCK_CODEC = Bedrock_v649.CODEC;
/**
* A list of all supported Bedrock versions that can join Geyser
@@ -63,9 +64,12 @@ public final class GameProtocol {
SUPPORTED_BEDROCK_CODECS.add(Bedrock_v622.CODEC.toBuilder()
.minecraftVersion("1.20.40/1.20.41")
.build());
- SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC.toBuilder()
+ SUPPORTED_BEDROCK_CODECS.add(Bedrock_v630.CODEC.toBuilder()
.minecraftVersion("1.20.50/1.20.51")
.build());
+ SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC.toBuilder()
+ .minecraftVersion("1.20.60/1.20.61")
+ .build());
}
/**
@@ -88,6 +92,10 @@ public final class GameProtocol {
return session.getUpstream().getProtocolVersion() < Bedrock_v630.CODEC.getProtocolVersion();
}
+ public static boolean is1_20_60orHigher(int protocolVersion) {
+ return protocolVersion >= Bedrock_v649.CODEC.getProtocolVersion();
+ }
+
/**
* Gets the {@link PacketCodec} for Minecraft: Java Edition.
*
diff --git a/core/src/main/java/org/geysermc/geyser/network/LoggingPacketHandler.java b/core/src/main/java/org/geysermc/geyser/network/LoggingPacketHandler.java
index cf794261b..0cfcc3d46 100644
--- a/core/src/main/java/org/geysermc/geyser/network/LoggingPacketHandler.java
+++ b/core/src/main/java/org/geysermc/geyser/network/LoggingPacketHandler.java
@@ -100,6 +100,16 @@ public class LoggingPacketHandler implements BedrockPacketHandler {
return defaultHandler(packet);
}
+ @Override
+ public PacketSignal handle(CameraPresetsPacket packet) {
+ return defaultHandler(packet);
+ }
+
+ @Override
+ public PacketSignal handle(CameraInstructionPacket packet) {
+ return defaultHandler(packet);
+ }
+
@Override
public PacketSignal handle(CommandBlockUpdatePacket packet) {
return defaultHandler(packet);
diff --git a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java
index 4806cc7cf..da3f0c071 100644
--- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java
+++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java
@@ -33,8 +33,25 @@ import org.cloudburstmc.protocol.bedrock.codec.v622.Bedrock_v622;
import org.cloudburstmc.protocol.bedrock.data.ExperimentData;
import org.cloudburstmc.protocol.bedrock.data.PacketCompressionAlgorithm;
import org.cloudburstmc.protocol.bedrock.data.ResourcePackType;
-import org.cloudburstmc.protocol.bedrock.packet.*;
+import org.cloudburstmc.protocol.bedrock.netty.codec.compression.CompressionStrategy;
+import org.cloudburstmc.protocol.bedrock.netty.codec.compression.SimpleCompressionStrategy;
+import org.cloudburstmc.protocol.bedrock.netty.codec.compression.ZlibCompression;
+import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket;
+import org.cloudburstmc.protocol.bedrock.packet.LoginPacket;
+import org.cloudburstmc.protocol.bedrock.packet.ModalFormResponsePacket;
+import org.cloudburstmc.protocol.bedrock.packet.MovePlayerPacket;
+import org.cloudburstmc.protocol.bedrock.packet.NetworkSettingsPacket;
+import org.cloudburstmc.protocol.bedrock.packet.PlayStatusPacket;
+import org.cloudburstmc.protocol.bedrock.packet.RequestNetworkSettingsPacket;
+import org.cloudburstmc.protocol.bedrock.packet.ResourcePackChunkDataPacket;
+import org.cloudburstmc.protocol.bedrock.packet.ResourcePackChunkRequestPacket;
+import org.cloudburstmc.protocol.bedrock.packet.ResourcePackClientResponsePacket;
+import org.cloudburstmc.protocol.bedrock.packet.ResourcePackDataInfoPacket;
+import org.cloudburstmc.protocol.bedrock.packet.ResourcePackStackPacket;
+import org.cloudburstmc.protocol.bedrock.packet.ResourcePacksInfoPacket;
+import org.cloudburstmc.protocol.bedrock.packet.SetTitlePacket;
import org.cloudburstmc.protocol.common.PacketSignal;
+import org.cloudburstmc.protocol.common.util.Zlib;
import org.geysermc.geyser.Constants;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.network.AuthType;
@@ -57,18 +74,29 @@ import org.geysermc.geyser.util.VersionCheckUtils;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
-import java.util.*;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.OptionalInt;
+import java.util.Set;
+import java.util.UUID;
public class UpstreamPacketHandler extends LoggingPacketHandler {
private boolean networkSettingsRequested = false;
private final Deque packsToSent = new ArrayDeque<>();
private final Set brokenResourcePacks = new HashSet<>();
+ private final CompressionStrategy compressionStrategy;
private SessionLoadResourcePacksEventImpl resourcePackLoadEvent;
public UpstreamPacketHandler(GeyserImpl geyser, GeyserSession session) {
super(geyser, session);
+
+ ZlibCompression compression = new ZlibCompression(Zlib.RAW);
+ compression.setLevel(this.geyser.getConfig().getBedrock().getCompressionLevel());
+ this.compressionStrategy = new SimpleCompressionStrategy(compression);
}
private PacketSignal translateAndDefault(BedrockPacket packet) {
@@ -136,16 +164,15 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
responsePacket.setCompressionAlgorithm(algorithm);
responsePacket.setCompressionThreshold(512);
session.sendUpstreamPacketImmediately(responsePacket);
+ session.getUpstream().getSession().getPeer().setCompression(compressionStrategy);
- session.getUpstream().getSession().setCompression(algorithm);
- session.getUpstream().getSession().setCompressionLevel(this.geyser.getConfig().getBedrock().getCompressionLevel());
networkSettingsRequested = true;
return PacketSignal.HANDLED;
}
@Override
public PacketSignal handle(LoginPacket loginPacket) {
- if (geyser.isShuttingDown()) {
+ if (geyser.isShuttingDown() || geyser.isReloading()) {
// Don't allow new players in if we're no longer operating
session.disconnect(GeyserLocale.getLocaleStringLog("geyser.core.shutdown.kick.message"));
return PacketSignal.HANDLED;
@@ -331,6 +358,7 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
ResourcePack pack = this.resourcePackLoadEvent.getPacks().get(packID[0]);
PackCodec codec = pack.codec();
ResourcePackManifest.Header header = pack.manifest().header();
+
data.setPackId(header.uuid());
int chunkCount = (int) Math.ceil(codec.size() / (double) GeyserResourcePack.CHUNK_SIZE);
data.setChunkCount(chunkCount);
diff --git a/core/src/main/java/org/geysermc/geyser/network/netty/Bootstraps.java b/core/src/main/java/org/geysermc/geyser/network/netty/Bootstraps.java
new file mode 100644
index 000000000..9ffc45650
--- /dev/null
+++ b/core/src/main/java/org/geysermc/geyser/network/netty/Bootstraps.java
@@ -0,0 +1,130 @@
+/*
+ * 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.network.netty;
+
+import io.netty.bootstrap.AbstractBootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
+import io.netty.channel.epoll.Native;
+import io.netty.channel.unix.UnixChannelOption;
+import lombok.experimental.UtilityClass;
+
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+@UtilityClass
+public final class Bootstraps {
+ private static final Optional KERNEL_VERSION;
+
+ // The REUSEPORT_AVAILABLE socket option is available starting from kernel version 3.9.
+ // This option allows multiple sockets to listen on the same IP address and port without conflict.
+ private static final int[] REUSEPORT_VERSION = new int[]{3, 9, 0};
+ private static final boolean REUSEPORT_AVAILABLE;
+
+ static {
+ String kernelVersion;
+ try {
+ kernelVersion = Native.KERNEL_VERSION;
+ } catch (Throwable e) {
+ kernelVersion = null;
+ }
+ if (kernelVersion != null && kernelVersion.contains("-")) {
+ int index = kernelVersion.indexOf('-');
+ if (index > -1) {
+ kernelVersion = kernelVersion.substring(0, index);
+ }
+ int[] kernelVer = fromString(kernelVersion);
+ KERNEL_VERSION = Optional.of(kernelVer);
+ REUSEPORT_AVAILABLE = checkVersion(kernelVer, 0);
+ } else {
+ KERNEL_VERSION = Optional.empty();
+ REUSEPORT_AVAILABLE = false;
+ }
+ }
+
+ public static Optional getKernelVersion() {
+ return KERNEL_VERSION;
+ }
+
+ public static boolean isReusePortAvailable() {
+ return REUSEPORT_AVAILABLE;
+ }
+
+ @SuppressWarnings({"rawtypes, unchecked"})
+ public static void setupBootstrap(AbstractBootstrap bootstrap) {
+ if (REUSEPORT_AVAILABLE) {
+ bootstrap.option(UnixChannelOption.SO_REUSEPORT, true);
+ }
+ }
+
+ private static int[] fromString(String ver) {
+ String[] parts = ver.split("\\.");
+ if (parts.length < 2) {
+ throw new IllegalArgumentException("At least 2 version numbers required");
+ }
+
+ return new int[]{
+ Integer.parseInt(parts[0]),
+ Integer.parseInt(parts[1]),
+ parts.length == 2 ? 0 : Integer.parseInt(parts[2])
+ };
+ }
+
+ private static boolean checkVersion(int[] ver, int i) {
+ if (ver[i] > REUSEPORT_VERSION[i]) {
+ return true;
+ } else if (ver[i] == REUSEPORT_VERSION[i]) {
+ if (ver.length == (i + 1)) {
+ return true;
+ } else {
+ return checkVersion(ver, i + 1);
+ }
+ }
+ return false;
+ }
+
+ public static CompletableFuture allOf(ChannelFuture... futures) {
+ if (futures == null || futures.length == 0) {
+ return CompletableFuture.completedFuture(null);
+ }
+ @SuppressWarnings("unchecked")
+ CompletableFuture[] completableFutures = new CompletableFuture[futures.length];
+ for (int i = 0; i < futures.length; i++) {
+ ChannelFuture channelFuture = futures[i];
+ CompletableFuture completableFuture = new CompletableFuture<>();
+ channelFuture.addListener(future -> {
+ if (future.cause() != null) {
+ completableFuture.completeExceptionally(future.cause());
+ }
+ completableFuture.complete(channelFuture.channel());
+ });
+ completableFutures[i] = completableFuture;
+ }
+
+ return CompletableFuture.allOf(completableFutures);
+ }
+}
diff --git a/core/src/main/java/org/geysermc/geyser/network/netty/GeyserServer.java b/core/src/main/java/org/geysermc/geyser/network/netty/GeyserServer.java
index 401a7f2cf..ea1dcb509 100644
--- a/core/src/main/java/org/geysermc/geyser/network/netty/GeyserServer.java
+++ b/core/src/main/java/org/geysermc/geyser/network/netty/GeyserServer.java
@@ -94,13 +94,16 @@ public final class GeyserServer {
private final GeyserImpl geyser;
private EventLoopGroup group;
+ // Split childGroup may improve IO
+ private EventLoopGroup childGroup;
private final ServerBootstrap bootstrap;
private EventLoopGroup playerGroup;
@Getter
private final ExpiringMap proxiedAddresses;
+ private final int listenCount;
- private ChannelFuture bootstrapFuture;
+ private ChannelFuture[] bootstrapFutures;
/**
* The port to broadcast in the pong. This can be different from the port the server is bound to, e.g. due to port forwarding.
@@ -109,9 +112,14 @@ public final class GeyserServer {
public GeyserServer(GeyserImpl geyser, int threadCount) {
this.geyser = geyser;
- this.group = TRANSPORT.eventLoopGroupFactory().apply(threadCount);
+ this.listenCount = Bootstraps.isReusePortAvailable() ? Integer.getInteger("Geyser.ListenCount", 2) : 1;
+ GeyserImpl.getInstance().getLogger().debug("Listen thread count: " + listenCount);
+ this.group = TRANSPORT.eventLoopGroupFactory().apply(listenCount);
+ this.childGroup = TRANSPORT.eventLoopGroupFactory().apply(threadCount);
- this.bootstrap = this.createBootstrap(this.group);
+ this.bootstrap = this.createBootstrap();
+ // setup SO_REUSEPORT if exists
+ Bootstraps.setupBootstrap(this.bootstrap);
if (this.geyser.getConfig().getBedrock().isEnableProxyProtocol()) {
this.proxiedAddresses = ExpiringMap.builder()
@@ -130,46 +138,51 @@ public final class GeyserServer {
}
public CompletableFuture bind(InetSocketAddress address) {
- CompletableFuture future = new CompletableFuture<>();
- this.bootstrapFuture = this.bootstrap.bind(address).addListener(bindResult -> {
- if (bindResult.cause() != null) {
- future.completeExceptionally(bindResult.cause());
- return;
- }
- future.complete(null);
- });
+ bootstrapFutures = new ChannelFuture[listenCount];
+ for (int i = 0; i < listenCount; i++) {
+ ChannelFuture future = bootstrap.bind(address);
+ addHandlers(future);
+ bootstrapFutures[i] = future;
+ }
- Channel channel = this.bootstrapFuture.channel();
+ return Bootstraps.allOf(bootstrapFutures);
+ }
+ private void addHandlers(ChannelFuture future) {
+ Channel channel = future.channel();
// Add our ping handler
channel.pipeline()
.addFirst(RakConnectionRequestHandler.NAME, new RakConnectionRequestHandler(this))
.addAfter(RakServerOfflineHandler.NAME, RakPingHandler.NAME, new RakPingHandler(this));
-
+ // Add proxy handler
if (this.geyser.getConfig().getBedrock().isEnableProxyProtocol()) {
channel.pipeline().addFirst("proxy-protocol-decoder", new ProxyServerHandler());
}
-
- return future;
}
public void shutdown() {
try {
- Future> future1 = this.group.shutdownGracefully(SHUTDOWN_QUIET_PERIOD_MS, SHUTDOWN_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ Future> futureChildGroup = this.childGroup.shutdownGracefully(SHUTDOWN_QUIET_PERIOD_MS, SHUTDOWN_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ this.childGroup = null;
+ Future> futureGroup = this.group.shutdownGracefully(SHUTDOWN_QUIET_PERIOD_MS, SHUTDOWN_TIMEOUT_MS, TimeUnit.MILLISECONDS);
this.group = null;
- Future> future2 = this.playerGroup.shutdownGracefully(SHUTDOWN_QUIET_PERIOD_MS, SHUTDOWN_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+ Future> futurePlayerGroup = this.playerGroup.shutdownGracefully(SHUTDOWN_QUIET_PERIOD_MS, SHUTDOWN_TIMEOUT_MS, TimeUnit.MILLISECONDS);
this.playerGroup = null;
- future1.sync();
- future2.sync();
+
+ futureChildGroup.sync();
+ futureGroup.sync();
+ futurePlayerGroup.sync();
SkinProvider.shutdown();
} catch (InterruptedException e) {
GeyserImpl.getInstance().getLogger().severe("Exception in shutdown process", e);
}
- this.bootstrapFuture.channel().closeFuture().syncUninterruptibly();
+ for (ChannelFuture f : bootstrapFutures) {
+ f.channel().closeFuture().syncUninterruptibly();
+ }
}
- private ServerBootstrap createBootstrap(EventLoopGroup group) {
+ private ServerBootstrap createBootstrap() {
if (this.geyser.getConfig().isDebugMode()) {
this.geyser.getLogger().debug("EventLoop type: " + TRANSPORT.datagramChannel());
if (TRANSPORT.datagramChannel() == NioDatagramChannel.class) {
@@ -188,7 +201,7 @@ public final class GeyserServer {
this.geyser.getLogger().debug("Setting MTU to " + this.geyser.getConfig().getMtu());
return new ServerBootstrap()
.channelFactory(RakChannelFactory.server(TRANSPORT.datagramChannel()))
- .group(group)
+ .group(group, childGroup)
.option(RakChannelOption.RAK_HANDLE_PING, true)
.option(RakChannelOption.RAK_MAX_MTU, this.geyser.getConfig().getMtu())
.childHandler(serverInitializer);
@@ -224,7 +237,7 @@ public final class GeyserServer {
return true;
}
- public BedrockPong onQuery(InetSocketAddress inetSocketAddress) {
+ public BedrockPong onQuery(Channel channel, InetSocketAddress inetSocketAddress) {
if (geyser.getConfig().isDebugMode() && PRINT_DEBUG_PINGS) {
String ip;
if (geyser.getConfig().isLogPlayerIpAddresses()) {
@@ -257,7 +270,7 @@ public final class GeyserServer {
.version(GameProtocol.DEFAULT_BEDROCK_CODEC.getMinecraftVersion()) // Required to not be empty as of 1.16.210.59. Can only contain . and numbers.
.ipv4Port(this.broadcastPort)
.ipv6Port(this.broadcastPort)
- .serverId(bootstrapFuture.channel().config().getOption(RakChannelOption.RAK_GUID));
+ .serverId(channel.config().getOption(RakChannelOption.RAK_GUID));
if (config.isPassthroughMotd() && pingInfo != null && pingInfo.getDescription() != null) {
String[] motd = MessageTranslator.convertMessageLenient(pingInfo.getDescription()).split("\n");
diff --git a/core/src/main/java/org/geysermc/geyser/network/netty/handler/RakPingHandler.java b/core/src/main/java/org/geysermc/geyser/network/netty/handler/RakPingHandler.java
index e63bf9dd2..62b9c6d12 100644
--- a/core/src/main/java/org/geysermc/geyser/network/netty/handler/RakPingHandler.java
+++ b/core/src/main/java/org/geysermc/geyser/network/netty/handler/RakPingHandler.java
@@ -45,7 +45,7 @@ public class RakPingHandler extends SimpleChannelInboundHandler {
protected void channelRead0(ChannelHandlerContext ctx, RakPing msg) {
long guid = ctx.channel().config().getOption(RakChannelOption.RAK_GUID);
- RakPong pong = msg.reply(guid, this.server.onQuery(msg.getSender()).toByteBuf());
+ RakPong pong = msg.reply(guid, this.server.onQuery(ctx.channel(), msg.getSender()).toByteBuf());
ctx.writeAndFlush(pong);
}
}
diff --git a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java
index 59960098f..ba75bbc9b 100644
--- a/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java
+++ b/core/src/main/java/org/geysermc/geyser/registry/loader/ProviderRegistryLoader.java
@@ -25,6 +25,8 @@
package org.geysermc.geyser.registry.loader;
+import org.geysermc.geyser.api.bedrock.camera.CameraFade;
+import org.geysermc.geyser.api.bedrock.camera.CameraPosition;
import org.geysermc.geyser.api.block.custom.CustomBlockData;
import org.geysermc.geyser.api.block.custom.NonVanillaCustomBlockData;
import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents;
@@ -39,6 +41,8 @@ import org.geysermc.geyser.api.item.custom.CustomItemOptions;
import org.geysermc.geyser.api.item.custom.NonVanillaCustomItemData;
import org.geysermc.geyser.api.pack.PathPackCodec;
import org.geysermc.geyser.api.pack.UrlPackCodec;
+import org.geysermc.geyser.impl.camera.GeyserCameraFade;
+import org.geysermc.geyser.impl.camera.GeyserCameraPosition;
import org.geysermc.geyser.command.GeyserCommandManager;
import org.geysermc.geyser.event.GeyserEventRegistrar;
import org.geysermc.geyser.item.GeyserCustomItemData;
@@ -67,21 +71,25 @@ public class ProviderRegistryLoader implements RegistryLoader