Mirror von
https://github.com/GeyserMC/Geyser.git
synchronisiert 2024-11-20 06:50:09 +01:00
Merge branch 'master' into feature/protocol-3.0
Dieser Commit ist enthalten in:
Commit
1769f2a85c
@ -17,7 +17,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!
|
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.19.20 - 1.19.51 and Minecraft Java 1.19.3.
|
### Currently supporting Minecraft Bedrock 1.19.20 - 1.19.60 and Minecraft Java 1.19.3.
|
||||||
|
|
||||||
## Setting Up
|
## Setting Up
|
||||||
Take a look [here](https://wiki.geysermc.org/geyser/setup/) for how to set up Geyser.
|
Take a look [here](https://wiki.geysermc.org/geyser/setup/) for how to set up Geyser.
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
dependencies {
|
|
||||||
api(libs.cumulus)
|
|
||||||
api(libs.events) {
|
|
||||||
exclude(group = "com.google.guava", module = "guava")
|
|
||||||
exclude(group = "org.lanternpowered", module = "lmbda")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,95 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in
|
|
||||||
* all copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
* THE SOFTWARE.
|
|
||||||
*
|
|
||||||
* @author GeyserMC
|
|
||||||
* @link https://github.com/GeyserMC/Geyser
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.geysermc.api;
|
|
||||||
|
|
||||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* General API class for Geyser.
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
public class Geyser {
|
|
||||||
private static GeyserApiBase api;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the base api.
|
|
||||||
*
|
|
||||||
* @return the base api
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
public static GeyserApiBase api() {
|
|
||||||
if (api == null) {
|
|
||||||
throw new RuntimeException("Api has not been registered yet!");
|
|
||||||
}
|
|
||||||
|
|
||||||
return api;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the api of the given type.
|
|
||||||
*
|
|
||||||
* @param apiClass the api class
|
|
||||||
* @param <T> the type
|
|
||||||
* @return the api of the given type
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public static <T extends GeyserApiBase> T api(@NonNull Class<T> apiClass) {
|
|
||||||
if (apiClass.isInstance(api)) {
|
|
||||||
return (T) api;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (api == null) {
|
|
||||||
throw new RuntimeException("Api has not been registered yet!");
|
|
||||||
} else {
|
|
||||||
throw new RuntimeException("Api was not an instance of " + apiClass + "! Was " + api.getClass().getCanonicalName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers the given api type. The api cannot be
|
|
||||||
* registered if {@link #isRegistered()} is true as
|
|
||||||
* an api has already been specified.
|
|
||||||
*
|
|
||||||
* @param api the api
|
|
||||||
*/
|
|
||||||
public static void set(@NonNull GeyserApiBase api) {
|
|
||||||
if (Geyser.api != null) {
|
|
||||||
throw new RuntimeException("Cannot redefine already registered api!");
|
|
||||||
}
|
|
||||||
|
|
||||||
Geyser.api = api;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets if the api has been registered and
|
|
||||||
* is ready for usage.
|
|
||||||
*
|
|
||||||
* @return if the api has been registered
|
|
||||||
*/
|
|
||||||
public static boolean isRegistered() {
|
|
||||||
return api != null;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,130 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in
|
|
||||||
* all copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
* THE SOFTWARE.
|
|
||||||
*
|
|
||||||
* @author GeyserMC
|
|
||||||
* @link https://github.com/GeyserMC/Geyser
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.geysermc.api;
|
|
||||||
|
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|
||||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
|
||||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
|
||||||
import org.checkerframework.common.value.qual.IntRange;
|
|
||||||
import org.geysermc.api.connection.Connection;
|
|
||||||
import org.geysermc.cumulus.form.Form;
|
|
||||||
import org.geysermc.cumulus.form.util.FormBuilder;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The base API class.
|
|
||||||
*/
|
|
||||||
public interface GeyserApiBase {
|
|
||||||
/**
|
|
||||||
* Gets the connection from the given UUID, if applicable. The player must be logged in to the Java server
|
|
||||||
* for this to return a non-null value.
|
|
||||||
*
|
|
||||||
* @param uuid the UUID of the connection
|
|
||||||
* @return the connection from the given UUID, if applicable
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
Connection connectionByUuid(@NonNull UUID uuid);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the connection from the given XUID, if applicable. This method only works for online connections.
|
|
||||||
*
|
|
||||||
* @param xuid the XUID of the session
|
|
||||||
* @return the connection from the given UUID, if applicable
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
Connection connectionByXuid(@NonNull String xuid);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to determine if the given <b>online</b> player is a Bedrock player.
|
|
||||||
*
|
|
||||||
* @param uuid the uuid of the online player
|
|
||||||
* @return true if the given online player is a Bedrock player
|
|
||||||
*/
|
|
||||||
boolean isBedrockPlayer(@NonNull UUID uuid);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a form to the given connection and opens it.
|
|
||||||
*
|
|
||||||
* @param uuid the uuid of the connection to open it on
|
|
||||||
* @param form the form to send
|
|
||||||
* @return whether the form was successfully sent
|
|
||||||
*/
|
|
||||||
boolean sendForm(@NonNull UUID uuid, @NonNull Form form);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a form to the given connection and opens it.
|
|
||||||
*
|
|
||||||
* @param uuid the uuid of the connection to open it on
|
|
||||||
* @param formBuilder the formBuilder to send
|
|
||||||
* @return whether the form was successfully sent
|
|
||||||
*/
|
|
||||||
boolean sendForm(@NonNull UUID uuid, @NonNull FormBuilder<?, ?, ?> formBuilder);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transfer the given connection to a server. A Bedrock player can successfully transfer to the same server they are
|
|
||||||
* currently playing on.
|
|
||||||
*
|
|
||||||
* @param uuid the uuid of the connection
|
|
||||||
* @param address the address of the server
|
|
||||||
* @param port the port of the server
|
|
||||||
* @return true if the transfer was a success
|
|
||||||
*/
|
|
||||||
boolean transfer(@NonNull UUID uuid, @NonNull String address, @IntRange(from = 0, to = 65535) int port);
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns all the online connections.
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
List<? extends Connection> onlineConnections();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the amount of online connections.
|
|
||||||
*/
|
|
||||||
int onlineConnectionsCount();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the prefix used by Floodgate. Will be null when the auth-type isn't Floodgate.
|
|
||||||
*/
|
|
||||||
@MonotonicNonNull
|
|
||||||
String usernamePrefix();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the major API version. Bumped whenever a significant breaking change or feature addition is added.
|
|
||||||
*/
|
|
||||||
default int majorApiVersion() {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the minor API version. May be bumped for new API additions.
|
|
||||||
*/
|
|
||||||
default int minorApiVersion() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,121 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in
|
|
||||||
* all copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
* THE SOFTWARE.
|
|
||||||
*
|
|
||||||
* @author GeyserMC
|
|
||||||
* @link https://github.com/GeyserMC/Geyser
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.geysermc.api.connection;
|
|
||||||
|
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|
||||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
|
||||||
import org.checkerframework.common.value.qual.IntRange;
|
|
||||||
import org.geysermc.api.util.BedrockPlatform;
|
|
||||||
import org.geysermc.api.util.InputMode;
|
|
||||||
import org.geysermc.api.util.UiProfile;
|
|
||||||
import org.geysermc.cumulus.form.Form;
|
|
||||||
import org.geysermc.cumulus.form.util.FormBuilder;
|
|
||||||
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a player connection.
|
|
||||||
*/
|
|
||||||
public interface Connection {
|
|
||||||
/**
|
|
||||||
* Returns the bedrock name of the connection.
|
|
||||||
*/
|
|
||||||
@NonNull String bedrockUsername();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the java name of the connection.
|
|
||||||
*/
|
|
||||||
@MonotonicNonNull
|
|
||||||
String javaUsername();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the UUID of the connection.
|
|
||||||
*/
|
|
||||||
@MonotonicNonNull
|
|
||||||
UUID javaUuid();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the XUID of the connection.
|
|
||||||
*/
|
|
||||||
@NonNull String xuid();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the version of the Bedrock client.
|
|
||||||
*/
|
|
||||||
@NonNull String version();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the platform that the connection is playing on.
|
|
||||||
*/
|
|
||||||
@NonNull BedrockPlatform platform();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the language code of the connection.
|
|
||||||
*/
|
|
||||||
@NonNull String languageCode();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the User Interface Profile of the connection.
|
|
||||||
*/
|
|
||||||
@NonNull UiProfile uiProfile();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the Input Mode of the Bedrock client.
|
|
||||||
*/
|
|
||||||
@NonNull InputMode inputMode();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether the connection is linked.
|
|
||||||
* This will always return false when the auth-type isn't Floodgate.
|
|
||||||
*/
|
|
||||||
boolean isLinked();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a form to the connection and opens it.
|
|
||||||
*
|
|
||||||
* @param form the form to send
|
|
||||||
* @return whether the form was successfully sent
|
|
||||||
*/
|
|
||||||
boolean sendForm(@NonNull Form form);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a form to the connection and opens it.
|
|
||||||
*
|
|
||||||
* @param formBuilder the formBuilder to send
|
|
||||||
* @return whether the form was successfully sent
|
|
||||||
*/
|
|
||||||
boolean sendForm(@NonNull FormBuilder<?, ?, ?> formBuilder);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transfer the connection to a server. A Bedrock player can successfully transfer to the same server they are
|
|
||||||
* currently playing on.
|
|
||||||
*
|
|
||||||
* @param address the address of the server
|
|
||||||
* @param port the port of the server
|
|
||||||
* @return true if the transfer was a success
|
|
||||||
*/
|
|
||||||
boolean transfer(@NonNull String address, @IntRange(from = 0, to = 65535) int port);
|
|
||||||
}
|
|
7
api/build.gradle.kts
Normale Datei
7
api/build.gradle.kts
Normale Datei
@ -0,0 +1,7 @@
|
|||||||
|
plugins {
|
||||||
|
id("geyser.publish-conventions")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api(libs.base.api)
|
||||||
|
}
|
@ -1,14 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id("geyser.api-conventions")
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
api(projects.api)
|
|
||||||
}
|
|
||||||
|
|
||||||
publishing {
|
|
||||||
publications.named<MavenPublication>("mavenJava") {
|
|
||||||
groupId = rootProject.group as String + ".geyser"
|
|
||||||
artifactId = "api"
|
|
||||||
}
|
|
||||||
}
|
|
@ -23,51 +23,44 @@
|
|||||||
* @link https://github.com/GeyserMC/Geyser
|
* @link https://github.com/GeyserMC/Geyser
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.geysermc.api.util;
|
package org.geysermc.geyser.api.event.bedrock;
|
||||||
|
|
||||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||||
|
import org.geysermc.event.Cancellable;
|
||||||
|
import org.geysermc.geyser.api.connection.GeyserConnection;
|
||||||
|
import org.geysermc.geyser.api.event.connection.ConnectionEvent;
|
||||||
|
|
||||||
public enum BedrockPlatform {
|
/**
|
||||||
UNKNOWN("Unknown"),
|
* Called whenever a Bedrock player performs an emote on their end, before it is broadcasted to the rest of the server.
|
||||||
GOOGLE("Android"),
|
*/
|
||||||
IOS("iOS"),
|
public final class BedrockEmoteEvent extends ConnectionEvent implements Cancellable {
|
||||||
OSX("macOS"),
|
private final String emoteId;
|
||||||
AMAZON("Amazon"),
|
private boolean cancelled;
|
||||||
GEARVR("Gear VR"),
|
|
||||||
HOLOLENS("Hololens"),
|
|
||||||
UWP("Windows"),
|
|
||||||
WIN32("Windows x86"),
|
|
||||||
DEDICATED("Dedicated"),
|
|
||||||
TVOS("Apple TV"),
|
|
||||||
PS4("PS4"),
|
|
||||||
NX("Switch"),
|
|
||||||
XBOX("Xbox One"),
|
|
||||||
WINDOWS_PHONE("Windows Phone");
|
|
||||||
|
|
||||||
private static final BedrockPlatform[] VALUES = values();
|
public BedrockEmoteEvent(@NonNull GeyserConnection connection, @NonNull String emoteId) {
|
||||||
|
super(connection);
|
||||||
private final String displayName;
|
this.emoteId = emoteId;
|
||||||
|
|
||||||
BedrockPlatform(String displayName) {
|
|
||||||
this.displayName = displayName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the BedrockPlatform from the identifier.
|
* @return the emote ID that the Bedrock player is attempting to perform.
|
||||||
*
|
|
||||||
* @param id the BedrockPlatform identifier
|
|
||||||
* @return The BedrockPlatform or {@link #UNKNOWN} if the platform wasn't found
|
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public static BedrockPlatform fromId(int id) {
|
public String emoteId() {
|
||||||
return id < VALUES.length ? VALUES[id] : VALUES[0];
|
return emoteId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return friendly display name of platform.
|
* @return the cancel status of this event. A Bedrock player will still play this emote on its end even if this
|
||||||
|
* event is cancelled, but other Bedrock players will not see.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public boolean isCancelled() {
|
||||||
return displayName;
|
return cancelled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setCancelled(boolean cancelled) {
|
||||||
|
this.cancelled = cancelled;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*
|
||||||
|
* @author GeyserMC
|
||||||
|
* @link https://github.com/GeyserMC/Geyser
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.geysermc.geyser.api.event.downstream;
|
||||||
|
|
||||||
|
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||||
|
import org.geysermc.event.Cancellable;
|
||||||
|
import org.geysermc.geyser.api.connection.GeyserConnection;
|
||||||
|
import org.geysermc.geyser.api.event.connection.ConnectionEvent;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated please use {@link org.geysermc.geyser.api.event.java.ServerDefineCommandsEvent}.
|
||||||
|
*/
|
||||||
|
@Deprecated(forRemoval = true)
|
||||||
|
public class ServerDefineCommandsEvent extends ConnectionEvent implements Cancellable {
|
||||||
|
private final Set<? extends CommandInfo> commands;
|
||||||
|
private boolean cancelled;
|
||||||
|
|
||||||
|
public ServerDefineCommandsEvent(@NonNull GeyserConnection connection, @NonNull Set<? extends CommandInfo> commands) {
|
||||||
|
super(connection);
|
||||||
|
this.commands = commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A collection of commands sent from the server. Any element in this collection can be removed, but no element can
|
||||||
|
* be added.
|
||||||
|
*
|
||||||
|
* @return a collection of the commands sent over
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public Set<? extends CommandInfo> commands() {
|
||||||
|
return this.commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCancelled() {
|
||||||
|
return this.cancelled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setCancelled(boolean cancelled) {
|
||||||
|
this.cancelled = cancelled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated(forRemoval = true)
|
||||||
|
public interface CommandInfo {
|
||||||
|
/**
|
||||||
|
* Gets the name of the command.
|
||||||
|
*
|
||||||
|
* @return the name of the command
|
||||||
|
*/
|
||||||
|
String name();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the description of the command.
|
||||||
|
*
|
||||||
|
* @return the description of the command
|
||||||
|
*/
|
||||||
|
String description();
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
|
* Copyright (c) 2019-2023 GeyserMC. http://geysermc.org
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
@ -23,7 +23,7 @@
|
|||||||
* @link https://github.com/GeyserMC/Geyser
|
* @link https://github.com/GeyserMC/Geyser
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.geysermc.geyser.api.event.downstream;
|
package org.geysermc.geyser.api.event.java;
|
||||||
|
|
||||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||||
import org.geysermc.event.Cancellable;
|
import org.geysermc.event.Cancellable;
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
|
* Copyright (c) 2019-2021 GeyserMC. http://geysermc.org
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
@ -23,27 +23,30 @@
|
|||||||
* @link https://github.com/GeyserMC/Geyser
|
* @link https://github.com/GeyserMC/Geyser
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.geysermc.api.util;
|
package org.geysermc.geyser.platform.bungeecord;
|
||||||
|
|
||||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
|
import io.netty.channel.ChannelOutboundHandlerAdapter;
|
||||||
|
import io.netty.channel.ChannelPromise;
|
||||||
|
import net.md_5.bungee.protocol.packet.LoginSuccess;
|
||||||
|
import net.md_5.bungee.protocol.packet.SetCompression;
|
||||||
|
|
||||||
public enum InputMode {
|
public class GeyserBungeeCompressionDisabler extends ChannelOutboundHandlerAdapter {
|
||||||
UNKNOWN,
|
|
||||||
KEYBOARD_MOUSE,
|
|
||||||
TOUCH,
|
|
||||||
CONTROLLER,
|
|
||||||
VR;
|
|
||||||
|
|
||||||
private static final InputMode[] VALUES = values();
|
@Override
|
||||||
|
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
|
||||||
/**
|
if (!(msg instanceof SetCompression)) {
|
||||||
* Get the InputMode from the identifier.
|
if (msg instanceof LoginSuccess) {
|
||||||
*
|
// We're past the point that compression can be enabled
|
||||||
* @param id the InputMode identifier
|
if (ctx.pipeline().get("compress") != null) {
|
||||||
* @return The InputMode or {@link #UNKNOWN} if the mode wasn't found
|
ctx.pipeline().remove("compress");
|
||||||
*/
|
}
|
||||||
@NonNull
|
if (ctx.pipeline().get("decompress") != null) {
|
||||||
public static InputMode fromId(int id) {
|
ctx.pipeline().remove("decompress");
|
||||||
return VALUES.length > id ? VALUES[id] : VALUES[0];
|
}
|
||||||
|
ctx.pipeline().remove(this);
|
||||||
|
}
|
||||||
|
super.write(ctx, msg, promise);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -140,6 +140,11 @@ public class GeyserBungeeInjector extends GeyserInjector implements Listener {
|
|||||||
channelInitializer = PipelineUtils.SERVER_CHILD;
|
channelInitializer = PipelineUtils.SERVER_CHILD;
|
||||||
}
|
}
|
||||||
initChannel.invoke(channelInitializer, ch);
|
initChannel.invoke(channelInitializer, ch);
|
||||||
|
|
||||||
|
if (bootstrap.getGeyserConfig().isDisableCompression()) {
|
||||||
|
ch.pipeline().addAfter(PipelineUtils.PACKET_ENCODER, "geyser-compression-disabler",
|
||||||
|
new GeyserBungeeCompressionDisabler());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.childAttr(listener, listenerInfo)
|
.childAttr(listener, listenerInfo)
|
||||||
@ -163,7 +168,7 @@ public class GeyserBungeeInjector extends GeyserInjector implements Listener {
|
|||||||
// If native compression is enabled, then this line is tripped up if a heap buffer is sent over in such a situation
|
// If native compression is enabled, then this line is tripped up if a heap buffer is sent over in such a situation
|
||||||
// as a new direct buffer is not created with that patch (HeapByteBufs throw an UnsupportedOperationException here):
|
// as a new direct buffer is not created with that patch (HeapByteBufs throw an UnsupportedOperationException here):
|
||||||
// https://github.com/SpigotMC/BungeeCord/blob/a283aaf724d4c9a815540cd32f3aafaa72df9e05/native/src/main/java/net/md_5/bungee/jni/zlib/NativeZlib.java#L43
|
// https://github.com/SpigotMC/BungeeCord/blob/a283aaf724d4c9a815540cd32f3aafaa72df9e05/native/src/main/java/net/md_5/bungee/jni/zlib/NativeZlib.java#L43
|
||||||
// This issue could be mitigated down the line by preventing Bungee from setting compression
|
// If disable compression is enabled, this can probably be disabled now, but BungeeCord (not Waterfall) complains
|
||||||
LocalSession.createDirectByteBufAllocator();
|
LocalSession.createDirectByteBufAllocator();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,12 +34,13 @@ import org.cloudburstmc.nbt.NbtType;
|
|||||||
>>>>>>> d1febe0b3904d52cdc6301711950f22d1caf09b5
|
>>>>>>> d1febe0b3904d52cdc6301711950f22d1caf09b5
|
||||||
import me.lucko.fabric.api.permissions.v0.Permissions;
|
import me.lucko.fabric.api.permissions.v0.Permissions;
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
import net.minecraft.nbt.ListTag;
|
import net.minecraft.nbt.*;
|
||||||
import net.minecraft.server.MinecraftServer;
|
import net.minecraft.server.MinecraftServer;
|
||||||
import net.minecraft.server.level.ServerPlayer;
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
import net.minecraft.world.item.WritableBookItem;
|
import net.minecraft.world.item.WritableBookItem;
|
||||||
import net.minecraft.world.item.WrittenBookItem;
|
import net.minecraft.world.item.WrittenBookItem;
|
||||||
|
import net.minecraft.world.level.block.entity.BannerBlockEntity;
|
||||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||||
import net.minecraft.world.level.block.entity.LecternBlockEntity;
|
import net.minecraft.world.level.block.entity.LecternBlockEntity;
|
||||||
import org.cloudburstmc.nbt.NbtMap;
|
import org.cloudburstmc.nbt.NbtMap;
|
||||||
@ -50,8 +51,10 @@ import org.geysermc.geyser.session.GeyserSession;
|
|||||||
import org.geysermc.geyser.translator.inventory.LecternInventoryTranslator;
|
import org.geysermc.geyser.translator.inventory.LecternInventoryTranslator;
|
||||||
import org.geysermc.geyser.util.BlockEntityUtils;
|
import org.geysermc.geyser.util.BlockEntityUtils;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class GeyserFabricWorldManager extends GeyserWorldManager {
|
public class GeyserFabricWorldManager extends GeyserWorldManager {
|
||||||
@ -133,7 +136,127 @@ public class GeyserFabricWorldManager extends GeyserWorldManager {
|
|||||||
return Permissions.check(player, permission);
|
return Permissions.check(player, permission);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<com.github.steveice10.opennbt.tag.builtin.CompoundTag> getPickItemNbt(GeyserSession session, int x, int y, int z, boolean addNbtData) {
|
||||||
|
CompletableFuture<com.github.steveice10.opennbt.tag.builtin.CompoundTag> future = new CompletableFuture<>();
|
||||||
|
server.execute(() -> {
|
||||||
|
ServerPlayer player = getPlayer(session);
|
||||||
|
if (player == null) {
|
||||||
|
future.complete(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockPos pos = new BlockPos(x, y, z);
|
||||||
|
// Don't create a new block entity if invalid
|
||||||
|
BlockEntity blockEntity = player.level.getChunkAt(pos).getBlockEntity(pos);
|
||||||
|
if (blockEntity instanceof BannerBlockEntity banner) {
|
||||||
|
// Potentially exposes other NBT data? But we need to get the NBT data for the banner patterns *and*
|
||||||
|
// the banner might have a custom name, both of which a Java client knows and caches
|
||||||
|
ItemStack itemStack = banner.getItem();
|
||||||
|
var tag = OpenNbtTagVisitor.convert("", itemStack.getOrCreateTag());
|
||||||
|
|
||||||
|
future.complete(tag);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
future.complete(null);
|
||||||
|
});
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
private ServerPlayer getPlayer(GeyserSession session) {
|
private ServerPlayer getPlayer(GeyserSession session) {
|
||||||
return server.getPlayerList().getPlayer(session.getPlayerEntity().getUuid());
|
return server.getPlayerList().getPlayer(session.getPlayerEntity().getUuid());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Future considerations: option to clone; would affect arrays
|
||||||
|
private static class OpenNbtTagVisitor implements TagVisitor {
|
||||||
|
private String currentKey;
|
||||||
|
private final com.github.steveice10.opennbt.tag.builtin.CompoundTag root;
|
||||||
|
private com.github.steveice10.opennbt.tag.builtin.Tag currentTag;
|
||||||
|
|
||||||
|
OpenNbtTagVisitor(String key) {
|
||||||
|
root = new com.github.steveice10.opennbt.tag.builtin.CompoundTag(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitString(StringTag stringTag) {
|
||||||
|
currentTag = new com.github.steveice10.opennbt.tag.builtin.StringTag(currentKey, stringTag.getAsString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitByte(ByteTag byteTag) {
|
||||||
|
currentTag = new com.github.steveice10.opennbt.tag.builtin.ByteTag(currentKey, byteTag.getAsByte());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitShort(ShortTag shortTag) {
|
||||||
|
currentTag = new com.github.steveice10.opennbt.tag.builtin.ShortTag(currentKey, shortTag.getAsShort());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitInt(IntTag intTag) {
|
||||||
|
currentTag = new com.github.steveice10.opennbt.tag.builtin.IntTag(currentKey, intTag.getAsInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitLong(LongTag longTag) {
|
||||||
|
currentTag = new com.github.steveice10.opennbt.tag.builtin.LongTag(currentKey, longTag.getAsLong());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitFloat(FloatTag floatTag) {
|
||||||
|
currentTag = new com.github.steveice10.opennbt.tag.builtin.FloatTag(currentKey, floatTag.getAsFloat());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitDouble(DoubleTag doubleTag) {
|
||||||
|
currentTag = new com.github.steveice10.opennbt.tag.builtin.DoubleTag(currentKey, doubleTag.getAsDouble());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitByteArray(ByteArrayTag byteArrayTag) {
|
||||||
|
currentTag = new com.github.steveice10.opennbt.tag.builtin.ByteArrayTag(currentKey, byteArrayTag.getAsByteArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitIntArray(IntArrayTag intArrayTag) {
|
||||||
|
currentTag = new com.github.steveice10.opennbt.tag.builtin.IntArrayTag(currentKey, intArrayTag.getAsIntArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitLongArray(LongArrayTag longArrayTag) {
|
||||||
|
currentTag = new com.github.steveice10.opennbt.tag.builtin.LongArrayTag(currentKey, longArrayTag.getAsLongArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitList(ListTag listTag) {
|
||||||
|
var newList = new com.github.steveice10.opennbt.tag.builtin.ListTag(currentKey);
|
||||||
|
for (Tag tag : listTag) {
|
||||||
|
currentKey = "";
|
||||||
|
tag.accept(this);
|
||||||
|
newList.add(currentTag);
|
||||||
|
}
|
||||||
|
currentTag = newList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitCompound(CompoundTag compoundTag) {
|
||||||
|
currentTag = convert(currentKey, compoundTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static com.github.steveice10.opennbt.tag.builtin.CompoundTag convert(String name, CompoundTag compoundTag) {
|
||||||
|
OpenNbtTagVisitor visitor = new OpenNbtTagVisitor(name);
|
||||||
|
for (String key : compoundTag.getAllKeys()) {
|
||||||
|
visitor.currentKey = key;
|
||||||
|
Tag tag = compoundTag.get(key);
|
||||||
|
tag.accept(visitor);
|
||||||
|
visitor.root.put(visitor.currentTag);
|
||||||
|
}
|
||||||
|
return visitor.root;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visitEnd(EndTag endTag) {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2021 GeyserMC. http://geysermc.org
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*
|
||||||
|
* @author GeyserMC
|
||||||
|
* @link https://github.com/GeyserMC/Geyser
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.geysermc.geyser.platform.spigot;
|
||||||
|
|
||||||
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
|
import io.netty.channel.ChannelOutboundHandlerAdapter;
|
||||||
|
import io.netty.channel.ChannelPromise;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.geysermc.geyser.GeyserImpl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables the compression packet (and the compression handlers from being added to the pipeline) for Geyser clients
|
||||||
|
* that won't be receiving the data over the network.
|
||||||
|
*
|
||||||
|
* As of 1.8 - 1.17.1, compression is enabled in the Netty pipeline by adding a listener after a packet is written.
|
||||||
|
* If we simply "cancel" or don't forward the packet, then the listener is never called.
|
||||||
|
*/
|
||||||
|
public class GeyserSpigotCompressionDisabler extends ChannelOutboundHandlerAdapter {
|
||||||
|
static final boolean ENABLED;
|
||||||
|
|
||||||
|
private static final Class<?> COMPRESSION_PACKET_CLASS;
|
||||||
|
private static final Class<?> LOGIN_SUCCESS_PACKET_CLASS;
|
||||||
|
private static final boolean PROTOCOL_SUPPORT_INSTALLED;
|
||||||
|
|
||||||
|
static {
|
||||||
|
PROTOCOL_SUPPORT_INSTALLED = Bukkit.getPluginManager().getPlugin("ProtocolSupport") != null;
|
||||||
|
|
||||||
|
Class<?> compressionPacketClass = null;
|
||||||
|
Class<?> loginSuccessPacketClass = null;
|
||||||
|
boolean enabled = false;
|
||||||
|
try {
|
||||||
|
compressionPacketClass = findCompressionPacket();
|
||||||
|
loginSuccessPacketClass = findLoginSuccessPacket();
|
||||||
|
enabled = true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
GeyserImpl.getInstance().getLogger().error("Could not initialize compression disabler!", e);
|
||||||
|
}
|
||||||
|
COMPRESSION_PACKET_CLASS = compressionPacketClass;
|
||||||
|
LOGIN_SUCCESS_PACKET_CLASS = loginSuccessPacketClass;
|
||||||
|
ENABLED = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GeyserSpigotCompressionDisabler() {
|
||||||
|
if (!ENABLED) {
|
||||||
|
throw new RuntimeException("Geyser compression disabler cannot be initialized in its current state!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
|
||||||
|
Class<?> msgClass = msg.getClass();
|
||||||
|
// Don't let any compression packet get through
|
||||||
|
if (!COMPRESSION_PACKET_CLASS.isAssignableFrom(msgClass)) {
|
||||||
|
if (LOGIN_SUCCESS_PACKET_CLASS.isAssignableFrom(msgClass)) {
|
||||||
|
if (PROTOCOL_SUPPORT_INSTALLED) {
|
||||||
|
// ProtocolSupport must send the compression packet, so let's remove what it did before it does damage
|
||||||
|
if (ctx.pipeline().get("compress") != null) {
|
||||||
|
ctx.pipeline().remove("compress");
|
||||||
|
}
|
||||||
|
if (ctx.pipeline().get("decompress") != null) {
|
||||||
|
ctx.pipeline().remove("decompress");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// We're past the point that a compression packet can be sent, so we can safely yeet ourselves away
|
||||||
|
ctx.channel().pipeline().remove(this);
|
||||||
|
}
|
||||||
|
super.write(ctx, msg, promise);
|
||||||
|
} else if (PROTOCOL_SUPPORT_INSTALLED) {
|
||||||
|
// We must indicate it "succeeded" or ProtocolSupport will time us out
|
||||||
|
promise.setSuccess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Class<?> findCompressionPacket() throws ClassNotFoundException {
|
||||||
|
try {
|
||||||
|
return Class.forName("net.minecraft.network.protocol.login.PacketLoginOutSetCompression");
|
||||||
|
} catch (ClassNotFoundException e) {
|
||||||
|
String prefix = Bukkit.getServer().getClass().getPackage().getName().replace("org.bukkit.craftbukkit", "net.minecraft.server");
|
||||||
|
return Class.forName(prefix + ".PacketLoginOutSetCompression");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Class<?> findLoginSuccessPacket() throws ClassNotFoundException {
|
||||||
|
try {
|
||||||
|
return Class.forName("net.minecraft.network.protocol.login.PacketLoginOutSuccess");
|
||||||
|
} catch (ClassNotFoundException e) {
|
||||||
|
String prefix = Bukkit.getServer().getClass().getPackage().getName().replace("org.bukkit.craftbukkit", "net.minecraft.server");
|
||||||
|
return Class.forName(prefix + ".PacketLoginOutSuccess");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -115,10 +115,14 @@ public class GeyserSpigotInjector extends GeyserInjector {
|
|||||||
|
|
||||||
ChannelFuture channelFuture = (new ServerBootstrap()
|
ChannelFuture channelFuture = (new ServerBootstrap()
|
||||||
.channel(LocalServerChannelWrapper.class)
|
.channel(LocalServerChannelWrapper.class)
|
||||||
.childHandler(new ChannelInitializer<Channel>() {
|
.childHandler(new ChannelInitializer<>() {
|
||||||
@Override
|
@Override
|
||||||
protected void initChannel(Channel ch) throws Exception {
|
protected void initChannel(Channel ch) throws Exception {
|
||||||
initChannel.invoke(childHandler, ch);
|
initChannel.invoke(childHandler, ch);
|
||||||
|
|
||||||
|
if (bootstrap.getGeyserConfig().isDisableCompression() && GeyserSpigotCompressionDisabler.ENABLED) {
|
||||||
|
ch.pipeline().addAfter("encoder", "geyser-compression-disabler", new GeyserSpigotCompressionDisabler());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// Set to MAX_PRIORITY as MultithreadEventLoopGroup#newDefaultThreadFactory which DefaultEventLoopGroup implements does by default
|
// Set to MAX_PRIORITY as MultithreadEventLoopGroup#newDefaultThreadFactory which DefaultEventLoopGroup implements does by default
|
||||||
|
@ -195,6 +195,9 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
|
|||||||
|
|
||||||
geyserConfig.loadFloodgate(this);
|
geyserConfig.loadFloodgate(this);
|
||||||
|
|
||||||
|
this.geyserCommandManager = new GeyserSpigotCommandManager(geyser);
|
||||||
|
this.geyserCommandManager.init();
|
||||||
|
|
||||||
if (!INITIALIZED) {
|
if (!INITIALIZED) {
|
||||||
// Needs to be an anonymous inner class otherwise Bukkit complains about missing classes
|
// Needs to be an anonymous inner class otherwise Bukkit complains about missing classes
|
||||||
Bukkit.getPluginManager().registerEvents(new Listener() {
|
Bukkit.getPluginManager().registerEvents(new Listener() {
|
||||||
@ -206,9 +209,6 @@ public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap {
|
|||||||
}
|
}
|
||||||
}, this);
|
}, this);
|
||||||
|
|
||||||
this.geyserCommandManager = new GeyserSpigotCommandManager(geyser);
|
|
||||||
this.geyserCommandManager.init();
|
|
||||||
|
|
||||||
// Because Bukkit locks its command map upon startup, we need to
|
// Because Bukkit locks its command map upon startup, we need to
|
||||||
// add our plugin commands in onEnable, but populating the executor
|
// add our plugin commands in onEnable, but populating the executor
|
||||||
// can happen at any time
|
// can happen at any time
|
||||||
|
@ -25,21 +25,18 @@
|
|||||||
|
|
||||||
package org.geysermc.geyser.platform.spigot.world.manager;
|
package org.geysermc.geyser.platform.spigot.world.manager;
|
||||||
|
|
||||||
import org.cloudburstmc.math.vector.Vector3i;
|
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
||||||
<<<<<<< HEAD
|
import com.github.steveice10.opennbt.tag.builtin.ListTag;
|
||||||
import org.cloudburstmc.nbt.NbtMap;
|
import com.github.steveice10.opennbt.tag.builtin.Tag;
|
||||||
import org.cloudburstmc.nbt.NbtMapBuilder;
|
|
||||||
import org.cloudburstmc.nbt.NbtType;
|
|
||||||
=======
|
|
||||||
>>>>>>> d1febe0b3904d52cdc6301711950f22d1caf09b5
|
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
import org.bukkit.block.Block;
|
import org.bukkit.block.*;
|
||||||
import org.bukkit.block.Lectern;
|
import org.bukkit.block.banner.Pattern;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
import org.bukkit.inventory.meta.BookMeta;
|
import org.bukkit.inventory.meta.BookMeta;
|
||||||
import org.bukkit.plugin.Plugin;
|
import org.bukkit.plugin.Plugin;
|
||||||
|
import org.cloudburstmc.math.vector.Vector3i;
|
||||||
import org.cloudburstmc.nbt.NbtMap;
|
import org.cloudburstmc.nbt.NbtMap;
|
||||||
import org.cloudburstmc.nbt.NbtMapBuilder;
|
import org.cloudburstmc.nbt.NbtMapBuilder;
|
||||||
import org.cloudburstmc.nbt.NbtType;
|
import org.cloudburstmc.nbt.NbtType;
|
||||||
@ -49,10 +46,14 @@ import org.geysermc.geyser.level.block.BlockStateValues;
|
|||||||
import org.geysermc.geyser.registry.BlockRegistries;
|
import org.geysermc.geyser.registry.BlockRegistries;
|
||||||
import org.geysermc.geyser.session.GeyserSession;
|
import org.geysermc.geyser.session.GeyserSession;
|
||||||
import org.geysermc.geyser.translator.inventory.LecternInventoryTranslator;
|
import org.geysermc.geyser.translator.inventory.LecternInventoryTranslator;
|
||||||
|
import org.geysermc.geyser.translator.inventory.item.nbt.BannerTranslator;
|
||||||
import org.geysermc.geyser.util.BlockEntityUtils;
|
import org.geysermc.geyser.util.BlockEntityUtils;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base world manager to use when there is no supported NMS revision
|
* The base world manager to use when there is no supported NMS revision
|
||||||
@ -179,6 +180,46 @@ public class GeyserSpigotWorldManager extends WorldManager {
|
|||||||
return Bukkit.getPlayer(session.getPlayerEntity().getUsername()).hasPermission(permission);
|
return Bukkit.getPlayer(session.getPlayerEntity().getUsername()).hasPermission(permission);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<@Nullable CompoundTag> getPickItemNbt(GeyserSession session, int x, int y, int z, boolean addNbtData) {
|
||||||
|
CompletableFuture<@Nullable CompoundTag> future = new CompletableFuture<>();
|
||||||
|
// Paper 1.19.3 complains about async access otherwise.
|
||||||
|
// java.lang.IllegalStateException: Tile is null, asynchronous access?
|
||||||
|
Bukkit.getScheduler().runTask(this.plugin, () -> {
|
||||||
|
Player bukkitPlayer;
|
||||||
|
if ((bukkitPlayer = Bukkit.getPlayer(session.getPlayerEntity().getUuid())) == null) {
|
||||||
|
future.complete(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Block block = bukkitPlayer.getWorld().getBlockAt(x, y, z);
|
||||||
|
BlockState state = block.getState();
|
||||||
|
if (state instanceof Banner banner) {
|
||||||
|
ListTag list = new ListTag("Patterns");
|
||||||
|
for (int i = 0; i < banner.numberOfPatterns(); i++) {
|
||||||
|
Pattern pattern = banner.getPattern(i);
|
||||||
|
list.add(BannerTranslator.getJavaPatternTag(pattern.getPattern().getIdentifier(), pattern.getColor().ordinal()));
|
||||||
|
}
|
||||||
|
|
||||||
|
CompoundTag root = addToBlockEntityTag(list);
|
||||||
|
|
||||||
|
future.complete(root);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
future.complete(null);
|
||||||
|
});
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompoundTag addToBlockEntityTag(Tag tag) {
|
||||||
|
CompoundTag compoundTag = new CompoundTag("");
|
||||||
|
CompoundTag blockEntityTag = new CompoundTag("BlockEntityTag");
|
||||||
|
blockEntityTag.put(tag);
|
||||||
|
compoundTag.put(blockEntityTag);
|
||||||
|
return compoundTag;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This should be set to true if we are post-1.13 but before the latest version, and we should convert the old block state id
|
* This should be set to true if we are post-1.13 but before the latest version, and we should convert the old block state id
|
||||||
* to the current one.
|
* to the current one.
|
||||||
|
@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2021 GeyserMC. http://geysermc.org
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*
|
||||||
|
* @author GeyserMC
|
||||||
|
* @link https://github.com/GeyserMC/Geyser
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.geysermc.geyser.platform.velocity;
|
||||||
|
|
||||||
|
import io.netty.channel.ChannelDuplexHandler;
|
||||||
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
|
import io.netty.channel.ChannelPromise;
|
||||||
|
import org.geysermc.geyser.GeyserImpl;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
public class GeyserVelocityCompressionDisabler extends ChannelDuplexHandler {
|
||||||
|
static final boolean ENABLED;
|
||||||
|
private static final Class<?> COMPRESSION_PACKET_CLASS;
|
||||||
|
private static final Class<?> LOGIN_SUCCESS_PACKET_CLASS;
|
||||||
|
private static final Object COMPRESSION_ENABLED_EVENT;
|
||||||
|
private static final Method SET_COMPRESSION_METHOD;
|
||||||
|
|
||||||
|
static {
|
||||||
|
boolean enabled = false;
|
||||||
|
Class<?> compressionPacketClass = null;
|
||||||
|
Class<?> loginSuccessPacketClass = null;
|
||||||
|
Object compressionEnabledEvent = null;
|
||||||
|
Method setCompressionMethod = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
compressionPacketClass = Class.forName("com.velocitypowered.proxy.protocol.packet.SetCompression");
|
||||||
|
loginSuccessPacketClass = Class.forName("com.velocitypowered.proxy.protocol.packet.ServerLoginSuccess");
|
||||||
|
compressionEnabledEvent = Class.forName("com.velocitypowered.proxy.protocol.VelocityConnectionEvent")
|
||||||
|
.getDeclaredField("COMPRESSION_ENABLED").get(null);
|
||||||
|
setCompressionMethod = Class.forName("com.velocitypowered.proxy.connection.MinecraftConnection")
|
||||||
|
.getMethod("setCompressionThreshold", int.class);
|
||||||
|
enabled = true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
GeyserImpl.getInstance().getLogger().error("Could not initialize compression disabler!", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
ENABLED = enabled;
|
||||||
|
COMPRESSION_PACKET_CLASS = compressionPacketClass;
|
||||||
|
LOGIN_SUCCESS_PACKET_CLASS = loginSuccessPacketClass;
|
||||||
|
COMPRESSION_ENABLED_EVENT = compressionEnabledEvent;
|
||||||
|
SET_COMPRESSION_METHOD = setCompressionMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GeyserVelocityCompressionDisabler() {
|
||||||
|
if (!ENABLED) {
|
||||||
|
throw new RuntimeException("Geyser compression disabler cannot be initialized in its current state!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
|
||||||
|
Class<?> msgClass = msg.getClass();
|
||||||
|
if (!COMPRESSION_PACKET_CLASS.isAssignableFrom(msgClass)) {
|
||||||
|
if (LOGIN_SUCCESS_PACKET_CLASS.isAssignableFrom(msgClass)) {
|
||||||
|
// We're past the point that compression can be enabled
|
||||||
|
|
||||||
|
ctx.pipeline().remove(this);
|
||||||
|
}
|
||||||
|
super.write(ctx, msg, promise);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
||||||
|
if (evt != COMPRESSION_ENABLED_EVENT) {
|
||||||
|
super.userEventTriggered(ctx, evt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invoke the method as it calls a Netty event and handles removing cleaner than we could
|
||||||
|
Object minecraftConnection = ctx.pipeline().get("handler");
|
||||||
|
SET_COMPRESSION_METHOD.invoke(minecraftConnection, -1);
|
||||||
|
// Do not call super and let the new compression enabled event continue firing
|
||||||
|
}
|
||||||
|
}
|
@ -34,6 +34,7 @@ import org.geysermc.geyser.network.netty.GeyserInjector;
|
|||||||
import org.geysermc.geyser.network.netty.LocalServerChannelWrapper;
|
import org.geysermc.geyser.network.netty.LocalServerChannelWrapper;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
public class GeyserVelocityInjector extends GeyserInjector {
|
public class GeyserVelocityInjector extends GeyserInjector {
|
||||||
@ -67,9 +68,23 @@ public class GeyserVelocityInjector extends GeyserInjector {
|
|||||||
workerGroupField.setAccessible(true);
|
workerGroupField.setAccessible(true);
|
||||||
EventLoopGroup workerGroup = (EventLoopGroup) workerGroupField.get(connectionManager);
|
EventLoopGroup workerGroup = (EventLoopGroup) workerGroupField.get(connectionManager);
|
||||||
|
|
||||||
|
// This method is what initializes the connection in Java Edition, after Netty is all set.
|
||||||
|
Method initChannel = ChannelInitializer.class.getDeclaredMethod("initChannel", Channel.class);
|
||||||
|
initChannel.setAccessible(true);
|
||||||
|
|
||||||
ChannelFuture channelFuture = (new ServerBootstrap()
|
ChannelFuture channelFuture = (new ServerBootstrap()
|
||||||
.channel(LocalServerChannelWrapper.class)
|
.channel(LocalServerChannelWrapper.class)
|
||||||
.childHandler(channelInitializer)
|
.childHandler(new ChannelInitializer<>() {
|
||||||
|
@Override
|
||||||
|
protected void initChannel(Channel ch) throws Exception {
|
||||||
|
initChannel.invoke(channelInitializer, ch);
|
||||||
|
|
||||||
|
if (bootstrap.getGeyserConfig().isDisableCompression() && GeyserVelocityCompressionDisabler.ENABLED) {
|
||||||
|
ch.pipeline().addAfter("minecraft-encoder", "geyser-compression-disabler",
|
||||||
|
new GeyserVelocityCompressionDisabler());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
.group(bossGroup, workerGroup) // Cannot be DefaultEventLoopGroup
|
.group(bossGroup, workerGroup) // Cannot be DefaultEventLoopGroup
|
||||||
.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, serverWriteMark) // Required or else rare network freezes can occur
|
.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, serverWriteMark) // Required or else rare network freezes can occur
|
||||||
.localAddress(LocalAddress.ANY))
|
.localAddress(LocalAddress.ANY))
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id("geyser.publish-conventions")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks {
|
|
||||||
shadowJar {
|
|
||||||
archiveBaseName.set(archiveBaseName.get() + "-api")
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,7 +5,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
allprojects {
|
||||||
group = "org.geysermc"
|
group = "org.geysermc.geyser"
|
||||||
version = "2.1.0-SNAPSHOT"
|
version = "2.1.0-SNAPSHOT"
|
||||||
description = "Allows for players from Minecraft: Bedrock Edition to join Minecraft: Java Edition servers."
|
description = "Allows for players from Minecraft: Bedrock Edition to join Minecraft: Java Edition servers."
|
||||||
|
|
||||||
@ -23,8 +23,6 @@ val platforms = setOf(
|
|||||||
projects.velocity
|
projects.velocity
|
||||||
).map { it.dependencyProject }
|
).map { it.dependencyProject }
|
||||||
|
|
||||||
val api: Project = projects.api.dependencyProject
|
|
||||||
|
|
||||||
subprojects {
|
subprojects {
|
||||||
apply {
|
apply {
|
||||||
plugin("java-library")
|
plugin("java-library")
|
||||||
@ -32,16 +30,8 @@ subprojects {
|
|||||||
plugin("geyser.build-logic")
|
plugin("geyser.build-logic")
|
||||||
}
|
}
|
||||||
|
|
||||||
val relativePath = projectDir.relativeTo(rootProject.projectDir).path
|
|
||||||
|
|
||||||
if (relativePath.contains("api")) {
|
|
||||||
plugins.apply("geyser.api-conventions")
|
|
||||||
} else {
|
|
||||||
group = rootProject.group as String + ".geyser"
|
|
||||||
when (this) {
|
when (this) {
|
||||||
in platforms -> plugins.apply("geyser.platform-conventions")
|
in platforms -> plugins.apply("geyser.platform-conventions")
|
||||||
api -> plugins.apply("geyser.publish-conventions")
|
|
||||||
else -> plugins.apply("geyser.base-conventions")
|
else -> plugins.apply("geyser.base-conventions")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
@ -7,8 +7,8 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api(projects.geyserApi)
|
|
||||||
api(projects.common)
|
api(projects.common)
|
||||||
|
api(projects.api)
|
||||||
|
|
||||||
// Jackson JSON and YAML serialization
|
// Jackson JSON and YAML serialization
|
||||||
api(libs.bundles.jackson)
|
api(libs.bundles.jackson)
|
||||||
|
@ -30,7 +30,6 @@ import java.net.URISyntaxException;
|
|||||||
|
|
||||||
public final class Constants {
|
public final class Constants {
|
||||||
public static final URI GLOBAL_API_WS_URI;
|
public static final URI GLOBAL_API_WS_URI;
|
||||||
public static final String NTP_SERVER = "time.cloudflare.com";
|
|
||||||
|
|
||||||
public static final String NEWS_OVERVIEW_URL = "https://api.geysermc.org/v2/news/";
|
public static final String NEWS_OVERVIEW_URL = "https://api.geysermc.org/v2/news/";
|
||||||
public static final String NEWS_PROJECT_NAME = "geyser";
|
public static final String NEWS_PROJECT_NAME = "geyser";
|
||||||
@ -47,6 +46,7 @@ public final class Constants {
|
|||||||
try {
|
try {
|
||||||
wsUri = new URI("wss://api.geysermc.org/ws");
|
wsUri = new URI("wss://api.geysermc.org/ws");
|
||||||
} catch (URISyntaxException e) {
|
} catch (URISyntaxException e) {
|
||||||
|
GeyserImpl.getInstance().getLogger().error("Unable to resolve api.geysermc.org! Check your internet connection.");
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
GLOBAL_API_WS_URI = wsUri;
|
GLOBAL_API_WS_URI = wsUri;
|
||||||
|
@ -76,17 +76,13 @@ import org.geysermc.geyser.session.GeyserSession;
|
|||||||
import org.geysermc.geyser.session.PendingMicrosoftAuthentication;
|
import org.geysermc.geyser.session.PendingMicrosoftAuthentication;
|
||||||
import org.geysermc.geyser.session.SessionManager;
|
import org.geysermc.geyser.session.SessionManager;
|
||||||
import org.geysermc.geyser.skin.FloodgateSkinUploader;
|
import org.geysermc.geyser.skin.FloodgateSkinUploader;
|
||||||
|
import org.geysermc.geyser.skin.ProvidedSkins;
|
||||||
import org.geysermc.geyser.skin.SkinProvider;
|
import org.geysermc.geyser.skin.SkinProvider;
|
||||||
import org.geysermc.geyser.text.GeyserLocale;
|
import org.geysermc.geyser.text.GeyserLocale;
|
||||||
import org.geysermc.geyser.text.MinecraftLocale;
|
import org.geysermc.geyser.text.MinecraftLocale;
|
||||||
import org.geysermc.geyser.translator.inventory.item.ItemTranslator;
|
import org.geysermc.geyser.translator.inventory.item.ItemTranslator;
|
||||||
import org.geysermc.geyser.translator.text.MessageTranslator;
|
import org.geysermc.geyser.translator.text.MessageTranslator;
|
||||||
import org.geysermc.geyser.util.CooldownUtils;
|
import org.geysermc.geyser.util.*;
|
||||||
import org.geysermc.geyser.util.DimensionUtils;
|
|
||||||
import org.geysermc.geyser.util.Metrics;
|
|
||||||
import org.geysermc.geyser.util.NewsHandler;
|
|
||||||
import org.geysermc.geyser.util.VersionCheckUtils;
|
|
||||||
import org.geysermc.geyser.util.WebUtils;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileWriter;
|
import java.io.FileWriter;
|
||||||
@ -96,12 +92,8 @@ import java.net.InetSocketAddress;
|
|||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import java.security.Key;
|
import java.security.Key;
|
||||||
import java.text.DecimalFormat;
|
import java.text.DecimalFormat;
|
||||||
import java.util.HashMap;
|
import java.util.*;
|
||||||
import java.util.List;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
@ -202,7 +194,23 @@ public class GeyserImpl implements GeyserApi {
|
|||||||
EntityDefinitions.init();
|
EntityDefinitions.init();
|
||||||
ItemTranslator.init();
|
ItemTranslator.init();
|
||||||
MessageTranslator.init();
|
MessageTranslator.init();
|
||||||
MinecraftLocale.init();
|
|
||||||
|
// Download the latest asset list and cache it
|
||||||
|
AssetUtils.generateAssetCache().whenComplete((aVoid, ex) -> {
|
||||||
|
if (ex != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MinecraftLocale.ensureEN_US();
|
||||||
|
String locale = GeyserLocale.getDefaultLocale();
|
||||||
|
if (!"en_us".equals(locale)) {
|
||||||
|
// English will be loaded after assets are downloaded, if necessary
|
||||||
|
MinecraftLocale.downloadAndLoadLocale(locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProvidedSkins.init();
|
||||||
|
|
||||||
|
CompletableFuture.runAsync(AssetUtils::downloadAndRunClientJarTasks);
|
||||||
|
});
|
||||||
|
|
||||||
startInstance();
|
startInstance();
|
||||||
|
|
||||||
@ -230,7 +238,10 @@ public class GeyserImpl implements GeyserApi {
|
|||||||
logger.info(message);
|
logger.info(message);
|
||||||
|
|
||||||
if (platformType == PlatformType.STANDALONE) {
|
if (platformType == PlatformType.STANDALONE) {
|
||||||
|
if (config.getRemote().authType() != AuthType.FLOODGATE) {
|
||||||
|
// If the auth-type is Floodgate, then this Geyser instance is probably owned by the Java server
|
||||||
logger.warning(GeyserLocale.getLocaleStringLog("geyser.core.movement_warn"));
|
logger.warning(GeyserLocale.getLocaleStringLog("geyser.core.movement_warn"));
|
||||||
|
}
|
||||||
} else if (config.getRemote().authType() == AuthType.FLOODGATE) {
|
} else if (config.getRemote().authType() == AuthType.FLOODGATE) {
|
||||||
VersionCheckUtils.checkForOutdatedFloodgate(logger);
|
VersionCheckUtils.checkForOutdatedFloodgate(logger);
|
||||||
}
|
}
|
||||||
@ -311,7 +322,7 @@ public class GeyserImpl implements GeyserApi {
|
|||||||
Key key = new AesKeyProducer().produceFrom(config.getFloodgateKeyPath());
|
Key key = new AesKeyProducer().produceFrom(config.getFloodgateKeyPath());
|
||||||
cipher = new AesCipher(new Base64Topping());
|
cipher = new AesCipher(new Base64Topping());
|
||||||
cipher.init(key);
|
cipher.init(key);
|
||||||
logger.debug(GeyserLocale.getLocaleStringLog("geyser.auth.floodgate.loaded_key"));
|
logger.debug("Loaded Floodgate key!");
|
||||||
// Note: this is positioned after the bind so the skin uploader doesn't try to run if Geyser fails
|
// Note: this is positioned after the bind so the skin uploader doesn't try to run if Geyser fails
|
||||||
// to load successfully. Spigot complains about class loader if the plugin is disabled.
|
// to load successfully. Spigot complains about class loader if the plugin is disabled.
|
||||||
skinUploader = new FloodgateSkinUploader(this).start();
|
skinUploader = new FloodgateSkinUploader(this).start();
|
||||||
|
@ -182,6 +182,8 @@ public interface GeyserConfiguration {
|
|||||||
|
|
||||||
boolean isUseDirectConnection();
|
boolean isUseDirectConnection();
|
||||||
|
|
||||||
|
boolean isDisableCompression();
|
||||||
|
|
||||||
int getConfigVersion();
|
int getConfigVersion();
|
||||||
|
|
||||||
static void checkGeyserConfiguration(GeyserConfiguration geyserConfig, GeyserLogger geyserLogger) {
|
static void checkGeyserConfiguration(GeyserConfiguration geyserConfig, GeyserLogger geyserLogger) {
|
||||||
|
@ -335,6 +335,9 @@ public abstract class GeyserJacksonConfiguration implements GeyserConfiguration
|
|||||||
@JsonProperty("use-direct-connection")
|
@JsonProperty("use-direct-connection")
|
||||||
private boolean useDirectConnection = true;
|
private boolean useDirectConnection = true;
|
||||||
|
|
||||||
|
@JsonProperty("disable-compression")
|
||||||
|
private boolean isDisableCompression = true;
|
||||||
|
|
||||||
@JsonProperty("config-version")
|
@JsonProperty("config-version")
|
||||||
private int configVersion = 0;
|
private int configVersion = 0;
|
||||||
|
|
||||||
|
@ -46,6 +46,7 @@ public class CommandBlockMinecartEntity extends DefaultBlockMinecartEntity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initializeMetadata() {
|
protected void initializeMetadata() {
|
||||||
|
super.initializeMetadata();
|
||||||
// Required, or else the GUI will not open
|
// Required, or else the GUI will not open
|
||||||
dirtyMetadata.put(EntityDataTypes.CONTAINER_TYPE, (byte) 16);
|
dirtyMetadata.put(EntityDataTypes.CONTAINER_TYPE, (byte) 16);
|
||||||
dirtyMetadata.put(EntityDataTypes.CONTAINER_SIZE, 1);
|
dirtyMetadata.put(EntityDataTypes.CONTAINER_SIZE, 1);
|
||||||
|
@ -48,8 +48,6 @@ import org.cloudburstmc.protocol.bedrock.packet.RemoveEntityPacket;
|
|||||||
import org.cloudburstmc.protocol.bedrock.packet.SetEntityDataPacket;
|
import org.cloudburstmc.protocol.bedrock.packet.SetEntityDataPacket;
|
||||||
import org.geysermc.geyser.entity.EntityDefinition;
|
import org.geysermc.geyser.entity.EntityDefinition;
|
||||||
import org.geysermc.geyser.entity.GeyserDirtyMetadata;
|
import org.geysermc.geyser.entity.GeyserDirtyMetadata;
|
||||||
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
|
|
||||||
import org.geysermc.geyser.network.GameProtocol;
|
|
||||||
import org.geysermc.geyser.session.GeyserSession;
|
import org.geysermc.geyser.session.GeyserSession;
|
||||||
import org.geysermc.geyser.translator.text.MessageTranslator;
|
import org.geysermc.geyser.translator.text.MessageTranslator;
|
||||||
import org.geysermc.geyser.util.EntityUtils;
|
import org.geysermc.geyser.util.EntityUtils;
|
||||||
|
@ -40,6 +40,13 @@ public class AgeableEntity extends CreatureEntity {
|
|||||||
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
|
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initializeMetadata() {
|
||||||
|
super.initializeMetadata();
|
||||||
|
// Required as of 1.19.3 Java
|
||||||
|
dirtyMetadata.put(EntityDataTypes.SCALE, getAdultSize());
|
||||||
|
}
|
||||||
|
|
||||||
public void setBaby(BooleanEntityMetadata entityMetadata) {
|
public void setBaby(BooleanEntityMetadata entityMetadata) {
|
||||||
boolean isBaby = entityMetadata.getPrimitiveValue();
|
boolean isBaby = entityMetadata.getPrimitiveValue();
|
||||||
dirtyMetadata.put(EntityDataTypes.SCALE, isBaby ? getBabySize() : getAdultSize());
|
dirtyMetadata.put(EntityDataTypes.SCALE, isBaby ? getBabySize() : getAdultSize());
|
||||||
|
@ -25,7 +25,6 @@
|
|||||||
|
|
||||||
package org.geysermc.geyser.entity.type.living.animal;
|
package org.geysermc.geyser.entity.type.living.animal;
|
||||||
|
|
||||||
import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
|
|
||||||
import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
|
import com.github.steveice10.mc.protocol.data.game.entity.metadata.type.IntEntityMetadata;
|
||||||
import org.cloudburstmc.math.vector.Vector3f;
|
import org.cloudburstmc.math.vector.Vector3f;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
|
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
|
||||||
@ -43,11 +42,6 @@ public class RabbitEntity extends AnimalEntity {
|
|||||||
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
|
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setBaby(BooleanEntityMetadata entityMetadata) {
|
|
||||||
super.setBaby(entityMetadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRabbitVariant(IntEntityMetadata entityMetadata) {
|
public void setRabbitVariant(IntEntityMetadata entityMetadata) {
|
||||||
int variant = entityMetadata.getPrimitiveValue();
|
int variant = entityMetadata.getPrimitiveValue();
|
||||||
|
|
||||||
|
@ -51,6 +51,13 @@ public class CatEntity extends TameableEntity {
|
|||||||
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
|
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initializeMetadata() {
|
||||||
|
super.initializeMetadata();
|
||||||
|
// Default value (minecraft:black).
|
||||||
|
dirtyMetadata.put(EntityDataTypes.VARIANT, 1);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateRotation(float yaw, float pitch, boolean isOnGround) {
|
public void updateRotation(float yaw, float pitch, boolean isOnGround) {
|
||||||
moveRelative(0, 0, 0, yaw, pitch, yaw, isOnGround);
|
moveRelative(0, 0, 0, yaw, pitch, yaw, isOnGround);
|
||||||
|
@ -85,7 +85,6 @@ public class PlayerEntity extends LivingEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String username;
|
private String username;
|
||||||
private boolean playerList = true; // Player is in the player list
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The textures property from the GameProfile.
|
* The textures property from the GameProfile.
|
||||||
@ -109,6 +108,7 @@ public class PlayerEntity extends LivingEntity {
|
|||||||
super(session, entityId, geyserId, uuid, EntityDefinitions.PLAYER, position, motion, yaw, pitch, headYaw);
|
super(session, entityId, geyserId, uuid, EntityDefinitions.PLAYER, position, motion, yaw, pitch, headYaw);
|
||||||
|
|
||||||
this.username = username;
|
this.username = username;
|
||||||
|
this.nametag = username;
|
||||||
this.texturesProperty = texturesProperty;
|
this.texturesProperty = texturesProperty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,8 +127,10 @@ public class PlayerEntity extends LivingEntity {
|
|||||||
setBelowNameText(objective);
|
setBelowNameText(objective);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update in case this entity has been despawned, then respawned
|
||||||
|
this.nametag = this.username;
|
||||||
// The name can't be updated later (the entity metadata for it is ignored), so we need to check for this now
|
// The name can't be updated later (the entity metadata for it is ignored), so we need to check for this now
|
||||||
updateDisplayName(null, false);
|
updateDisplayName(session.getWorldCache().getScoreboard().getTeamFor(username));
|
||||||
|
|
||||||
AddPlayerPacket addPlayerPacket = new AddPlayerPacket();
|
AddPlayerPacket addPlayerPacket = new AddPlayerPacket();
|
||||||
addPlayerPacket.setUuid(uuid);
|
addPlayerPacket.setUuid(uuid);
|
||||||
@ -324,19 +326,10 @@ public class PlayerEntity extends LivingEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//todo this will become common entity logic once UUID support is implemented for them
|
//todo this will become common entity logic once UUID support is implemented for them
|
||||||
/**
|
public void updateDisplayName(@Nullable Team team) {
|
||||||
* @param useGivenTeam even if there is no team, update the username in the entity metadata anyway, and don't look for a team
|
|
||||||
*/
|
|
||||||
public void updateDisplayName(@Nullable Team team, boolean useGivenTeam) {
|
|
||||||
if (team == null && !useGivenTeam) {
|
|
||||||
// Only search for the team if we are not supposed to use the given team
|
|
||||||
// If the given team is null, this is intentional that we are being removed from the team
|
|
||||||
team = session.getWorldCache().getScoreboard().getTeamFor(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean needsUpdate;
|
boolean needsUpdate;
|
||||||
String newDisplayName = this.username;
|
|
||||||
if (team != null) {
|
if (team != null) {
|
||||||
|
String newDisplayName;
|
||||||
if (team.isVisibleFor(session.getPlayerEntity().getUsername())) {
|
if (team.isVisibleFor(session.getPlayerEntity().getUsername())) {
|
||||||
TeamColor color = team.getColor();
|
TeamColor color = team.getColor();
|
||||||
String chatColor = MessageTranslator.toChatColor(color);
|
String chatColor = MessageTranslator.toChatColor(color);
|
||||||
@ -348,23 +341,16 @@ public class PlayerEntity extends LivingEntity {
|
|||||||
// The name is not visible to the session player; clear name
|
// The name is not visible to the session player; clear name
|
||||||
newDisplayName = "";
|
newDisplayName = "";
|
||||||
}
|
}
|
||||||
needsUpdate = useGivenTeam && !newDisplayName.equals(nametag);
|
needsUpdate = !newDisplayName.equals(this.nametag);
|
||||||
nametag = newDisplayName;
|
this.nametag = newDisplayName;
|
||||||
dirtyMetadata.put(EntityDataTypes.NAME, newDisplayName);
|
|
||||||
} else if (useGivenTeam) {
|
|
||||||
// The name has reset, if it was previously something else
|
|
||||||
needsUpdate = !newDisplayName.equals(nametag);
|
|
||||||
dirtyMetadata.put(EntityDataTypes.NAME, this.username);
|
|
||||||
} else {
|
} else {
|
||||||
needsUpdate = false;
|
// The name has reset, if it was previously something else
|
||||||
|
needsUpdate = !this.nametag.equals(this.username);
|
||||||
|
this.nametag = this.username;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needsUpdate) {
|
if (needsUpdate) {
|
||||||
// Update the metadata as it won't be updated later
|
dirtyMetadata.put(EntityDataTypes.NAME, this.nametag);
|
||||||
SetEntityDataPacket packet = new SetEntityDataPacket();
|
|
||||||
packet.getMetadata().put(EntityDataTypes.NAME, newDisplayName);
|
|
||||||
packet.setRuntimeEntityId(geyserId);
|
|
||||||
session.sendUpstreamPacket(packet);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -425,4 +411,11 @@ public class PlayerEntity extends LivingEntity {
|
|||||||
session.sendUpstreamPacket(packet);
|
session.sendUpstreamPacket(packet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the UUID that should be used when dealing with Bedrock's tab list.
|
||||||
|
*/
|
||||||
|
public UUID getTabListUuid() {
|
||||||
|
return getUuid();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,7 @@ import javax.annotation.Nullable;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The entity class specifically for a {@link GeyserSession}'s player.
|
* The entity class specifically for a {@link GeyserSession}'s player.
|
||||||
@ -249,4 +250,9 @@ public class SessionPlayerEntity extends PlayerEntity {
|
|||||||
dirtyMetadata.put(EntityDataTypes.PLAYER_HAS_DIED, false);
|
dirtyMetadata.put(EntityDataTypes.PLAYER_HAS_DIED, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UUID getTabListUuid() {
|
||||||
|
return session.getAuthData().uuid();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,17 +26,20 @@
|
|||||||
package org.geysermc.geyser.entity.type.player;
|
package org.geysermc.geyser.entity.type.player;
|
||||||
|
|
||||||
import org.cloudburstmc.math.vector.Vector3f;
|
import org.cloudburstmc.math.vector.Vector3f;
|
||||||
|
import org.cloudburstmc.math.vector.Vector3i;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.GameType;
|
import org.cloudburstmc.protocol.bedrock.data.GameType;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.PlayerPermission;
|
import org.cloudburstmc.protocol.bedrock.data.PlayerPermission;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.command.CommandPermission;
|
import org.cloudburstmc.protocol.bedrock.data.command.CommandPermission;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
|
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
|
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
|
||||||
import org.cloudburstmc.protocol.bedrock.packet.AddPlayerPacket;
|
import org.cloudburstmc.protocol.bedrock.packet.AddPlayerPacket;
|
||||||
|
import lombok.Getter;
|
||||||
import org.geysermc.geyser.level.block.BlockStateValues;
|
import org.geysermc.geyser.level.block.BlockStateValues;
|
||||||
import org.geysermc.geyser.session.GeyserSession;
|
import org.geysermc.geyser.session.GeyserSession;
|
||||||
import org.geysermc.geyser.session.cache.SkullCache;
|
import org.geysermc.geyser.session.cache.SkullCache;
|
||||||
import org.geysermc.geyser.skin.SkullSkinManager;
|
import org.geysermc.geyser.skin.SkullSkinManager;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@ -46,9 +49,14 @@ import java.util.concurrent.TimeUnit;
|
|||||||
*/
|
*/
|
||||||
public class SkullPlayerEntity extends PlayerEntity {
|
public class SkullPlayerEntity extends PlayerEntity {
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private UUID skullUUID;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private Vector3i skullPosition;
|
||||||
|
|
||||||
public SkullPlayerEntity(GeyserSession session, long geyserId) {
|
public SkullPlayerEntity(GeyserSession session, long geyserId) {
|
||||||
super(session, 0, geyserId, UUID.randomUUID(), Vector3f.ZERO, Vector3f.ZERO, 0, 0, 0, "", null);
|
super(session, 0, geyserId, UUID.randomUUID(), Vector3f.ZERO, Vector3f.ZERO, 0, 0, 0, "", null);
|
||||||
setPlayerList(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -103,11 +111,14 @@ public class SkullPlayerEntity extends PlayerEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void updateSkull(SkullCache.Skull skull) {
|
public void updateSkull(SkullCache.Skull skull) {
|
||||||
if (!skull.getTexturesProperty().equals(getTexturesProperty())) {
|
skullPosition = skull.getPosition();
|
||||||
|
|
||||||
|
if (!Objects.equals(skull.getTexturesProperty(), getTexturesProperty()) || !Objects.equals(skullUUID, skull.getUuid())) {
|
||||||
// Make skull invisible as we change skins
|
// Make skull invisible as we change skins
|
||||||
setFlag(EntityFlag.INVISIBLE, true);
|
setFlag(EntityFlag.INVISIBLE, true);
|
||||||
updateBedrockMetadata();
|
updateBedrockMetadata();
|
||||||
|
|
||||||
|
skullUUID = skull.getUuid();
|
||||||
setTexturesProperty(skull.getTexturesProperty());
|
setTexturesProperty(skull.getTexturesProperty());
|
||||||
|
|
||||||
SkullSkinManager.requestAndHandleSkin(this, session, (skin -> session.scheduleInEventLoop(() -> {
|
SkullSkinManager.requestAndHandleSkin(this, session, (skin -> session.scheduleInEventLoop(() -> {
|
||||||
|
@ -27,12 +27,15 @@ package org.geysermc.geyser.level;
|
|||||||
|
|
||||||
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
|
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
|
||||||
import com.github.steveice10.mc.protocol.data.game.setting.Difficulty;
|
import com.github.steveice10.mc.protocol.data.game.setting.Difficulty;
|
||||||
|
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
||||||
import org.cloudburstmc.math.vector.Vector3i;
|
import org.cloudburstmc.math.vector.Vector3i;
|
||||||
import org.cloudburstmc.nbt.NbtMap;
|
import org.cloudburstmc.nbt.NbtMap;
|
||||||
import org.geysermc.geyser.session.GeyserSession;
|
import org.geysermc.geyser.session.GeyserSession;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nonnull;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that manages or retrieves various information
|
* Class that manages or retrieves various information
|
||||||
@ -166,4 +169,14 @@ public abstract class WorldManager {
|
|||||||
public String[] getBiomeIdentifiers(boolean withTags) {
|
public String[] getBiomeIdentifiers(boolean withTags) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for pick block, so we don't need to cache more data than necessary.
|
||||||
|
*
|
||||||
|
* @return expected NBT for this item.
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
public CompletableFuture<@Nullable CompoundTag> getPickItemNbt(GeyserSession session, int x, int y, int z, boolean addNbtData) {
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,7 @@ import org.cloudburstmc.protocol.bedrock.codec.v545.Bedrock_v545;
|
|||||||
import org.cloudburstmc.protocol.bedrock.codec.v554.Bedrock_v554;
|
import org.cloudburstmc.protocol.bedrock.codec.v554.Bedrock_v554;
|
||||||
import org.cloudburstmc.protocol.bedrock.codec.v557.Bedrock_v557;
|
import org.cloudburstmc.protocol.bedrock.codec.v557.Bedrock_v557;
|
||||||
import org.cloudburstmc.protocol.bedrock.codec.v560.Bedrock_v560;
|
import org.cloudburstmc.protocol.bedrock.codec.v560.Bedrock_v560;
|
||||||
|
import org.cloudburstmc.protocol.bedrock.codec.v567.Bedrock_v567;
|
||||||
import org.cloudburstmc.protocol.bedrock.netty.codec.packet.BedrockPacketCodec;
|
import org.cloudburstmc.protocol.bedrock.netty.codec.packet.BedrockPacketCodec;
|
||||||
import org.geysermc.geyser.session.GeyserSession;
|
import org.geysermc.geyser.session.GeyserSession;
|
||||||
|
|
||||||
@ -74,6 +75,7 @@ public final class GameProtocol {
|
|||||||
SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC.toBuilder()
|
SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC.toBuilder()
|
||||||
.minecraftVersion("1.19.50/1.19.51")
|
.minecraftVersion("1.19.50/1.19.51")
|
||||||
.build());
|
.build());
|
||||||
|
SUPPORTED_BEDROCK_CODECS.add(Bedrock_v567.CODEC);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -100,6 +102,10 @@ public final class GameProtocol {
|
|||||||
return session.getUpstream().getProtocolVersion() >= Bedrock_v560.CODEC.getProtocolVersion();
|
return session.getUpstream().getProtocolVersion() >= Bedrock_v560.CODEC.getProtocolVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean supports1_19_60(GeyserSession session) {
|
||||||
|
return session.getUpstream().getProtocolVersion() >= Bedrock_v567.CODEC.getProtocolVersion();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the {@link PacketCodec} for Minecraft: Java Edition.
|
* Gets the {@link PacketCodec} for Minecraft: Java Edition.
|
||||||
*
|
*
|
||||||
|
@ -38,8 +38,8 @@ import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
|||||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||||
import org.cloudburstmc.nbt.NbtMap;
|
import org.cloudburstmc.nbt.NbtMap;
|
||||||
import org.cloudburstmc.nbt.NbtMapBuilder;
|
import org.cloudburstmc.nbt.NbtMapBuilder;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.CraftingData;
|
|
||||||
import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.PotionMixData;
|
import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.PotionMixData;
|
||||||
|
import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.RecipeData;
|
||||||
import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket;
|
import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket;
|
||||||
import org.geysermc.geyser.entity.EntityDefinition;
|
import org.geysermc.geyser.entity.EntityDefinition;
|
||||||
import org.geysermc.geyser.inventory.item.Enchantment.JavaEnchantment;
|
import org.geysermc.geyser.inventory.item.Enchantment.JavaEnchantment;
|
||||||
@ -97,9 +97,9 @@ public final class Registries {
|
|||||||
public static final IntMappedRegistry<BlockCollision> COLLISIONS = IntMappedRegistry.create(Pair.of("org.geysermc.geyser.translator.collision.CollisionRemapper", "mappings/collision.json"), CollisionRegistryLoader::new);
|
public static final IntMappedRegistry<BlockCollision> COLLISIONS = IntMappedRegistry.create(Pair.of("org.geysermc.geyser.translator.collision.CollisionRemapper", "mappings/collision.json"), CollisionRegistryLoader::new);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A versioned registry which holds a {@link RecipeType} to a corresponding list of {@link CraftingData}.
|
* A versioned registry which holds a {@link RecipeType} to a corresponding list of {@link RecipeData}.
|
||||||
*/
|
*/
|
||||||
public static final VersionedRegistry<Map<RecipeType, List<CraftingData>>> CRAFTING_DATA = VersionedRegistry.create(RegistryLoaders.empty(Int2ObjectOpenHashMap::new));
|
public static final VersionedRegistry<Map<RecipeType, List<RecipeData>>> CRAFTING_DATA = VersionedRegistry.create(RegistryLoaders.empty(Int2ObjectOpenHashMap::new));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A registry holding data of all the known enchantments.
|
* A registry holding data of all the known enchantments.
|
||||||
|
@ -34,6 +34,7 @@ import it.unimi.dsi.fastutil.objects.*;
|
|||||||
import org.cloudburstmc.nbt.*;
|
import org.cloudburstmc.nbt.*;
|
||||||
import org.cloudburstmc.protocol.bedrock.codec.v544.Bedrock_v544;
|
import org.cloudburstmc.protocol.bedrock.codec.v544.Bedrock_v544;
|
||||||
import org.cloudburstmc.protocol.bedrock.codec.v560.Bedrock_v560;
|
import org.cloudburstmc.protocol.bedrock.codec.v560.Bedrock_v560;
|
||||||
|
import org.cloudburstmc.protocol.bedrock.codec.v567.Bedrock_v567;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.defintions.BlockDefinition;
|
import org.cloudburstmc.protocol.bedrock.data.defintions.BlockDefinition;
|
||||||
import org.geysermc.geyser.GeyserImpl;
|
import org.geysermc.geyser.GeyserImpl;
|
||||||
import org.geysermc.geyser.level.block.BlockStateValues;
|
import org.geysermc.geyser.level.block.BlockStateValues;
|
||||||
@ -71,6 +72,7 @@ public final class BlockRegistryPopulator {
|
|||||||
ImmutableMap<ObjectIntPair<String>, BiFunction<String, NbtMapBuilder, String>> blockMappers = ImmutableMap.<ObjectIntPair<String>, BiFunction<String, NbtMapBuilder, String>>builder()
|
ImmutableMap<ObjectIntPair<String>, BiFunction<String, NbtMapBuilder, String>> blockMappers = ImmutableMap.<ObjectIntPair<String>, BiFunction<String, NbtMapBuilder, String>>builder()
|
||||||
.put(ObjectIntPair.of("1_19_20", Bedrock_v544.CODEC.getProtocolVersion()), emptyMapper)
|
.put(ObjectIntPair.of("1_19_20", Bedrock_v544.CODEC.getProtocolVersion()), emptyMapper)
|
||||||
.put(ObjectIntPair.of("1_19_50", Bedrock_v560.CODEC.getProtocolVersion()), emptyMapper)
|
.put(ObjectIntPair.of("1_19_50", Bedrock_v560.CODEC.getProtocolVersion()), emptyMapper)
|
||||||
|
.put(ObjectIntPair.of("1_19_60", Bedrock_v567.CODEC.getProtocolVersion()), emptyMapper)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
for (Map.Entry<ObjectIntPair<String>, BiFunction<String, NbtMapBuilder, String>> palette : blockMappers.entrySet()) {
|
for (Map.Entry<ObjectIntPair<String>, BiFunction<String, NbtMapBuilder, String>> palette : blockMappers.entrySet()) {
|
||||||
|
@ -39,11 +39,13 @@ import org.cloudburstmc.nbt.NbtMapBuilder;
|
|||||||
import org.cloudburstmc.nbt.NbtType;
|
import org.cloudburstmc.nbt.NbtType;
|
||||||
import org.cloudburstmc.protocol.bedrock.codec.v544.Bedrock_v544;
|
import org.cloudburstmc.protocol.bedrock.codec.v544.Bedrock_v544;
|
||||||
import org.cloudburstmc.protocol.bedrock.codec.v560.Bedrock_v560;
|
import org.cloudburstmc.protocol.bedrock.codec.v560.Bedrock_v560;
|
||||||
|
import org.cloudburstmc.protocol.bedrock.codec.v567.Bedrock_v567;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.defintions.BlockDefinition;
|
import org.cloudburstmc.protocol.bedrock.data.defintions.BlockDefinition;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.defintions.ItemDefinition;
|
import org.cloudburstmc.protocol.bedrock.data.defintions.ItemDefinition;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.defintions.SimpleItemDefinition;
|
import org.cloudburstmc.protocol.bedrock.data.defintions.SimpleItemDefinition;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ComponentItemData;
|
import org.cloudburstmc.protocol.bedrock.data.inventory.ComponentItemData;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
|
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
|
||||||
|
import org.cloudburstmc.protocol.bedrock.data.SoundEvent;
|
||||||
import org.geysermc.geyser.GeyserBootstrap;
|
import org.geysermc.geyser.GeyserBootstrap;
|
||||||
import org.geysermc.geyser.GeyserImpl;
|
import org.geysermc.geyser.GeyserImpl;
|
||||||
import org.geysermc.geyser.api.item.custom.CustomItemData;
|
import org.geysermc.geyser.api.item.custom.CustomItemData;
|
||||||
@ -73,6 +75,7 @@ public class ItemRegistryPopulator {
|
|||||||
Map<String, PaletteVersion> paletteVersions = new Object2ObjectOpenHashMap<>();
|
Map<String, PaletteVersion> paletteVersions = new Object2ObjectOpenHashMap<>();
|
||||||
paletteVersions.put("1_19_20", new PaletteVersion(Bedrock_v544.CODEC.getProtocolVersion(), Collections.emptyMap()));
|
paletteVersions.put("1_19_20", new PaletteVersion(Bedrock_v544.CODEC.getProtocolVersion(), Collections.emptyMap()));
|
||||||
paletteVersions.put("1_19_50", new PaletteVersion(Bedrock_v560.CODEC.getProtocolVersion(), Collections.emptyMap()));
|
paletteVersions.put("1_19_50", new PaletteVersion(Bedrock_v560.CODEC.getProtocolVersion(), Collections.emptyMap()));
|
||||||
|
paletteVersions.put("1_19_60", new PaletteVersion(Bedrock_v567.CODEC.getProtocolVersion(), Collections.emptyMap()));
|
||||||
|
|
||||||
GeyserBootstrap bootstrap = GeyserImpl.getInstance().getBootstrap();
|
GeyserBootstrap bootstrap = GeyserImpl.getInstance().getBootstrap();
|
||||||
|
|
||||||
|
@ -35,7 +35,10 @@ import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
|||||||
import org.cloudburstmc.nbt.NbtMap;
|
import org.cloudburstmc.nbt.NbtMap;
|
||||||
import org.cloudburstmc.nbt.NbtUtils;
|
import org.cloudburstmc.nbt.NbtUtils;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
|
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.CraftingData;
|
import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.MultiRecipeData;
|
||||||
|
import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.RecipeData;
|
||||||
|
import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapedRecipeData;
|
||||||
|
import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapelessRecipeData;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.ItemDescriptorWithCount;
|
import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.ItemDescriptorWithCount;
|
||||||
import org.geysermc.geyser.GeyserImpl;
|
import org.geysermc.geyser.GeyserImpl;
|
||||||
import org.geysermc.geyser.inventory.recipe.GeyserRecipe;
|
import org.geysermc.geyser.inventory.recipe.GeyserRecipe;
|
||||||
@ -71,17 +74,17 @@ public class RecipeRegistryPopulator {
|
|||||||
for (Int2ObjectMap.Entry<ItemMappings> version : Registries.ITEMS.get().int2ObjectEntrySet()) {
|
for (Int2ObjectMap.Entry<ItemMappings> version : Registries.ITEMS.get().int2ObjectEntrySet()) {
|
||||||
// Make a bit of an assumption here that the last recipe net ID will be equivalent between all versions
|
// Make a bit of an assumption here that the last recipe net ID will be equivalent between all versions
|
||||||
LAST_RECIPE_NET_ID = currentRecipeId;
|
LAST_RECIPE_NET_ID = currentRecipeId;
|
||||||
Map<RecipeType, List<CraftingData>> craftingData = new EnumMap<>(RecipeType.class);
|
Map<RecipeType, List<RecipeData>> craftingData = new EnumMap<>(RecipeType.class);
|
||||||
Int2ObjectMap<GeyserRecipe> recipes = new Int2ObjectOpenHashMap<>();
|
Int2ObjectMap<GeyserRecipe> recipes = new Int2ObjectOpenHashMap<>();
|
||||||
|
|
||||||
craftingData.put(RecipeType.CRAFTING_SPECIAL_BOOKCLONING,
|
craftingData.put(RecipeType.CRAFTING_SPECIAL_BOOKCLONING,
|
||||||
Collections.singletonList(CraftingData.fromMulti(UUID.fromString("d1ca6b84-338e-4f2f-9c6b-76cc8b4bd98d"), ++LAST_RECIPE_NET_ID)));
|
Collections.singletonList(MultiRecipeData.of(UUID.fromString("d1ca6b84-338e-4f2f-9c6b-76cc8b4bd98d"), ++LAST_RECIPE_NET_ID)));
|
||||||
craftingData.put(RecipeType.CRAFTING_SPECIAL_REPAIRITEM,
|
craftingData.put(RecipeType.CRAFTING_SPECIAL_REPAIRITEM,
|
||||||
Collections.singletonList(CraftingData.fromMulti(UUID.fromString("00000000-0000-0000-0000-000000000001"), ++LAST_RECIPE_NET_ID)));
|
Collections.singletonList(MultiRecipeData.of(UUID.fromString("00000000-0000-0000-0000-000000000001"), ++LAST_RECIPE_NET_ID)));
|
||||||
craftingData.put(RecipeType.CRAFTING_SPECIAL_MAPEXTENDING,
|
craftingData.put(RecipeType.CRAFTING_SPECIAL_MAPEXTENDING,
|
||||||
Collections.singletonList(CraftingData.fromMulti(UUID.fromString("d392b075-4ba1-40ae-8789-af868d56f6ce"), ++LAST_RECIPE_NET_ID)));
|
Collections.singletonList(MultiRecipeData.of(UUID.fromString("d392b075-4ba1-40ae-8789-af868d56f6ce"), ++LAST_RECIPE_NET_ID)));
|
||||||
craftingData.put(RecipeType.CRAFTING_SPECIAL_MAPCLONING,
|
craftingData.put(RecipeType.CRAFTING_SPECIAL_MAPCLONING,
|
||||||
Collections.singletonList(CraftingData.fromMulti(UUID.fromString("85939755-ba10-4d9d-a4cc-efb7a8e943c4"), ++LAST_RECIPE_NET_ID)));
|
Collections.singletonList(MultiRecipeData.of(UUID.fromString("85939755-ba10-4d9d-a4cc-efb7a8e943c4"), ++LAST_RECIPE_NET_ID)));
|
||||||
|
|
||||||
// https://github.com/pmmp/PocketMine-MP/blob/stable/src/pocketmine/inventory/MultiRecipe.php
|
// https://github.com/pmmp/PocketMine-MP/blob/stable/src/pocketmine/inventory/MultiRecipe.php
|
||||||
|
|
||||||
@ -121,9 +124,9 @@ public class RecipeRegistryPopulator {
|
|||||||
* Computes a Bedrock crafting recipe from the given JSON data.
|
* Computes a Bedrock crafting recipe from the given JSON data.
|
||||||
* @param node the JSON data to compute
|
* @param node the JSON data to compute
|
||||||
* @param recipes a list of all the recipes
|
* @param recipes a list of all the recipes
|
||||||
* @return the {@link CraftingData} to send to the Bedrock client.
|
* @return the {@link RecipeData} to send to the Bedrock client.
|
||||||
*/
|
*/
|
||||||
private static CraftingData getCraftingDataFromJsonNode(JsonNode node, Int2ObjectMap<GeyserRecipe> recipes, ItemMappings mappings) {
|
private static RecipeData getCraftingDataFromJsonNode(JsonNode node, Int2ObjectMap<GeyserRecipe> recipes, ItemMappings mappings) {
|
||||||
int netId = ++LAST_RECIPE_NET_ID;
|
int netId = ++LAST_RECIPE_NET_ID;
|
||||||
int type = node.get("bedrockRecipeType").asInt();
|
int type = node.get("bedrockRecipeType").asInt();
|
||||||
JsonNode outputNode = node.get("output");
|
JsonNode outputNode = node.get("output");
|
||||||
@ -169,7 +172,7 @@ public class RecipeRegistryPopulator {
|
|||||||
recipes.put(netId, recipe);
|
recipes.put(netId, recipe);
|
||||||
/* Convert end */
|
/* Convert end */
|
||||||
|
|
||||||
return CraftingData.fromShaped(uuid.toString(), shape.get(0).length(), shape.size(),
|
return ShapedRecipeData.shaped(uuid.toString(), shape.get(0).length(), shape.size(),
|
||||||
inputs.stream().map(ItemDescriptorWithCount::fromItem).toList(), Collections.singletonList(output), uuid, "crafting_table", 0, netId);
|
inputs.stream().map(ItemDescriptorWithCount::fromItem).toList(), Collections.singletonList(output), uuid, "crafting_table", 0, netId);
|
||||||
}
|
}
|
||||||
List<ItemData> inputs = new ObjectArrayList<>();
|
List<ItemData> inputs = new ObjectArrayList<>();
|
||||||
@ -189,10 +192,10 @@ public class RecipeRegistryPopulator {
|
|||||||
|
|
||||||
if (type == 5) {
|
if (type == 5) {
|
||||||
// Shulker box
|
// Shulker box
|
||||||
return CraftingData.fromShulkerBox(uuid.toString(),
|
return ShapelessRecipeData.shulkerBox(uuid.toString(),
|
||||||
inputs.stream().map(ItemDescriptorWithCount::fromItem).toList(), Collections.singletonList(output), uuid, "crafting_table", 0, netId);
|
inputs.stream().map(ItemDescriptorWithCount::fromItem).toList(), Collections.singletonList(output), uuid, "crafting_table", 0, netId);
|
||||||
}
|
}
|
||||||
return CraftingData.fromShapeless(uuid.toString(),
|
return ShapelessRecipeData.shapeless(uuid.toString(),
|
||||||
inputs.stream().map(ItemDescriptorWithCount::fromItem).toList(), Collections.singletonList(output), uuid, "crafting_table", 0, netId);
|
inputs.stream().map(ItemDescriptorWithCount::fromItem).toList(), Collections.singletonList(output), uuid, "crafting_table", 0, netId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,6 +30,7 @@ import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
|
|||||||
import org.cloudburstmc.protocol.bedrock.packet.RemoveObjectivePacket;
|
import org.cloudburstmc.protocol.bedrock.packet.RemoveObjectivePacket;
|
||||||
import org.cloudburstmc.protocol.bedrock.packet.SetDisplayObjectivePacket;
|
import org.cloudburstmc.protocol.bedrock.packet.SetDisplayObjectivePacket;
|
||||||
import org.cloudburstmc.protocol.bedrock.packet.SetScorePacket;
|
import org.cloudburstmc.protocol.bedrock.packet.SetScorePacket;
|
||||||
|
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import org.geysermc.geyser.GeyserImpl;
|
import org.geysermc.geyser.GeyserImpl;
|
||||||
import org.geysermc.geyser.GeyserLogger;
|
import org.geysermc.geyser.GeyserLogger;
|
||||||
@ -37,6 +38,7 @@ import org.geysermc.geyser.entity.type.Entity;
|
|||||||
import org.geysermc.geyser.entity.type.player.PlayerEntity;
|
import org.geysermc.geyser.entity.type.player.PlayerEntity;
|
||||||
import org.geysermc.geyser.session.GeyserSession;
|
import org.geysermc.geyser.session.GeyserSession;
|
||||||
import org.geysermc.geyser.text.GeyserLocale;
|
import org.geysermc.geyser.text.GeyserLocale;
|
||||||
|
import org.jetbrains.annotations.Contract;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@ -46,6 +48,8 @@ import java.util.concurrent.atomic.AtomicLong;
|
|||||||
import static org.geysermc.geyser.scoreboard.UpdateType.*;
|
import static org.geysermc.geyser.scoreboard.UpdateType.*;
|
||||||
|
|
||||||
public final class Scoreboard {
|
public final class Scoreboard {
|
||||||
|
private static final boolean SHOW_SCOREBOARD_LOGS = Boolean.parseBoolean(System.getProperty("Geyser.ShowScoreboardLogs", "true"));
|
||||||
|
|
||||||
private final GeyserSession session;
|
private final GeyserSession session;
|
||||||
private final GeyserLogger logger;
|
private final GeyserLogger logger;
|
||||||
@Getter
|
@Getter
|
||||||
@ -55,6 +59,13 @@ public final class Scoreboard {
|
|||||||
@Getter
|
@Getter
|
||||||
private final Map<ScoreboardPosition, Objective> objectiveSlots = new EnumMap<>(ScoreboardPosition.class);
|
private final Map<ScoreboardPosition, Objective> objectiveSlots = new EnumMap<>(ScoreboardPosition.class);
|
||||||
private final Map<String, Team> teams = new ConcurrentHashMap<>(); // updated on multiple threads
|
private final Map<String, Team> teams = new ConcurrentHashMap<>(); // updated on multiple threads
|
||||||
|
/**
|
||||||
|
* Required to preserve vanilla behavior, which also uses a map.
|
||||||
|
* Otherwise, for example, if TAB has a team for a player and vanilla has a team, "race conditions" that do not
|
||||||
|
* match vanilla could occur.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
private final Map<String, Team> playerToTeam = new Object2ObjectOpenHashMap<>();
|
||||||
|
|
||||||
private int lastAddScoreCount = 0;
|
private int lastAddScoreCount = 0;
|
||||||
private int lastRemoveScoreCount = 0;
|
private int lastRemoveScoreCount = 0;
|
||||||
@ -125,13 +136,19 @@ public final class Scoreboard {
|
|||||||
public Team registerNewTeam(String teamName, String[] players) {
|
public Team registerNewTeam(String teamName, String[] players) {
|
||||||
Team team = teams.get(teamName);
|
Team team = teams.get(teamName);
|
||||||
if (team != null) {
|
if (team != null) {
|
||||||
|
if (SHOW_SCOREBOARD_LOGS) {
|
||||||
logger.info(GeyserLocale.getLocaleStringLog("geyser.network.translator.team.failed_overrides", teamName));
|
logger.info(GeyserLocale.getLocaleStringLog("geyser.network.translator.team.failed_overrides", teamName));
|
||||||
|
}
|
||||||
return team;
|
return team;
|
||||||
}
|
}
|
||||||
|
|
||||||
team = new Team(this, teamName);
|
team = new Team(this, teamName);
|
||||||
team.addEntities(players);
|
team.addEntities(players);
|
||||||
teams.put(teamName, team);
|
teams.put(teamName, team);
|
||||||
|
|
||||||
|
// Update command parameters - is safe to send even if the command enum doesn't exist on the client (as of 1.19.51)
|
||||||
|
session.addCommandEnum("Geyser_Teams", team.getId());
|
||||||
|
|
||||||
return team;
|
return team;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -328,12 +345,7 @@ public final class Scoreboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Team getTeamFor(String entity) {
|
public Team getTeamFor(String entity) {
|
||||||
for (Team team : teams.values()) {
|
return playerToTeam.get(entity);
|
||||||
if (team.hasEntity(entity)) {
|
|
||||||
return team;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeTeam(String teamName) {
|
public void removeTeam(String teamName) {
|
||||||
@ -343,7 +355,18 @@ public final class Scoreboard {
|
|||||||
// We need to use the direct entities list here, so #refreshSessionPlayerDisplays also updates accordingly
|
// We need to use the direct entities list here, so #refreshSessionPlayerDisplays also updates accordingly
|
||||||
// With the player's lack of a team in visibility checks
|
// With the player's lack of a team in visibility checks
|
||||||
updateEntityNames(remove, remove.getEntities(), true);
|
updateEntityNames(remove, remove.getEntities(), true);
|
||||||
|
for (String name : remove.getEntities()) {
|
||||||
|
// 1.19.3 Mojmap Scoreboard#removePlayerTeam(PlayerTeam)
|
||||||
|
playerToTeam.remove(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
session.removeCommandEnum("Geyser_Teams", remove.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Contract("-> new")
|
||||||
|
public String[] getTeamNames() {
|
||||||
|
return teams.keySet().toArray(new String[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -368,7 +391,8 @@ public final class Scoreboard {
|
|||||||
for (Entity entity : session.getEntityCache().getEntities().values()) {
|
for (Entity entity : session.getEntityCache().getEntities().values()) {
|
||||||
// This more complex logic is for the future to iterate over all entities, not just players
|
// This more complex logic is for the future to iterate over all entities, not just players
|
||||||
if (entity instanceof PlayerEntity player && names.remove(player.getUsername())) {
|
if (entity instanceof PlayerEntity player && names.remove(player.getUsername())) {
|
||||||
player.updateDisplayName(team, true);
|
player.updateDisplayName(team);
|
||||||
|
player.updateBedrockMetadata();
|
||||||
if (names.isEmpty()) {
|
if (names.isEmpty()) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -384,7 +408,8 @@ public final class Scoreboard {
|
|||||||
for (Entity entity : session.getEntityCache().getEntities().values()) {
|
for (Entity entity : session.getEntityCache().getEntities().values()) {
|
||||||
if (entity instanceof PlayerEntity player) {
|
if (entity instanceof PlayerEntity player) {
|
||||||
Team playerTeam = session.getWorldCache().getScoreboard().getTeamFor(player.getUsername());
|
Team playerTeam = session.getWorldCache().getScoreboard().getTeamFor(player.getUsername());
|
||||||
player.updateDisplayName(playerTeam, true);
|
player.updateDisplayName(playerTeam);
|
||||||
|
player.updateBedrockMetadata();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,6 +65,14 @@ public final class Team {
|
|||||||
if (entities.add(name)) {
|
if (entities.add(name)) {
|
||||||
added.add(name);
|
added.add(name);
|
||||||
}
|
}
|
||||||
|
scoreboard.getPlayerToTeam().compute(name, (player, oldTeam) -> {
|
||||||
|
if (oldTeam != null) {
|
||||||
|
// Remove old team from this map, and from the set of players of the old team.
|
||||||
|
// Java 1.19.3 Mojmap: Scoreboard#addPlayerToTeam calls #removePlayerFromTeam
|
||||||
|
oldTeam.entities.remove(player);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (added.isEmpty()) {
|
if (added.isEmpty()) {
|
||||||
@ -93,6 +101,7 @@ public final class Team {
|
|||||||
if (entities.remove(name)) {
|
if (entities.remove(name)) {
|
||||||
removed.add(name);
|
removed.add(name);
|
||||||
}
|
}
|
||||||
|
scoreboard.getPlayerToTeam().remove(name, this);
|
||||||
}
|
}
|
||||||
return removed;
|
return removed;
|
||||||
}
|
}
|
||||||
|
@ -86,7 +86,9 @@ import org.cloudburstmc.math.vector.*;
|
|||||||
import org.cloudburstmc.nbt.NbtMap;
|
import org.cloudburstmc.nbt.NbtMap;
|
||||||
import org.cloudburstmc.protocol.bedrock.BedrockServerSession;
|
import org.cloudburstmc.protocol.bedrock.BedrockServerSession;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.*;
|
import org.cloudburstmc.protocol.bedrock.data.*;
|
||||||
|
import org.cloudburstmc.protocol.bedrock.data.command.CommandEnumData;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.command.CommandPermission;
|
import org.cloudburstmc.protocol.bedrock.data.command.CommandPermission;
|
||||||
|
import org.cloudburstmc.protocol.bedrock.data.command.SoftEnumUpdateType;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
|
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
|
||||||
import org.cloudburstmc.protocol.bedrock.packet.*;
|
import org.cloudburstmc.protocol.bedrock.packet.*;
|
||||||
import org.cloudburstmc.protocol.common.DefinitionRegistry;
|
import org.cloudburstmc.protocol.common.DefinitionRegistry;
|
||||||
@ -136,7 +138,6 @@ import org.geysermc.geyser.translator.text.MessageTranslator;
|
|||||||
import org.geysermc.geyser.util.ChunkUtils;
|
import org.geysermc.geyser.util.ChunkUtils;
|
||||||
import org.geysermc.geyser.util.DimensionUtils;
|
import org.geysermc.geyser.util.DimensionUtils;
|
||||||
import org.geysermc.geyser.util.LoginEncryptionUtils;
|
import org.geysermc.geyser.util.LoginEncryptionUtils;
|
||||||
import org.geysermc.geyser.util.MathUtils;
|
|
||||||
|
|
||||||
import java.net.ConnectException;
|
import java.net.ConnectException;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
@ -458,9 +459,8 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Counts how many ticks have occurred since an arm animation started.
|
* Counts how many ticks have occurred since an arm animation started.
|
||||||
* -1 means there is no active arm swing.
|
* -1 means there is no active arm swing; -2 means an arm swing will start in a tick.
|
||||||
*/
|
*/
|
||||||
@Getter(AccessLevel.NONE)
|
|
||||||
private int armAnimationTicks = -1;
|
private int armAnimationTicks = -1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -540,6 +540,12 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
|||||||
@Setter
|
@Setter
|
||||||
private ScheduledFuture<?> lookBackScheduledFuture = null;
|
private ScheduledFuture<?> lookBackScheduledFuture = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to return players back to their vehicles if the server doesn't want them unmounting.
|
||||||
|
*/
|
||||||
|
@Setter
|
||||||
|
private ScheduledFuture<?> mountVehicleScheduledFuture = null;
|
||||||
|
|
||||||
private MinecraftProtocol protocol;
|
private MinecraftProtocol protocol;
|
||||||
|
|
||||||
public GeyserSession(GeyserImpl geyser, BedrockServerSession bedrockServerSession, EventLoop eventLoop) {
|
public GeyserSession(GeyserImpl geyser, BedrockServerSession bedrockServerSession, EventLoop eventLoop) {
|
||||||
@ -1066,6 +1072,17 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
|||||||
closed = true;
|
closed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves task to the session event loop if already not in it. Otherwise, the task is automatically ran.
|
||||||
|
*/
|
||||||
|
public void ensureInEventLoop(Runnable runnable) {
|
||||||
|
if (eventLoop.inEventLoop()) {
|
||||||
|
runnable.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
executeInEventLoop(runnable);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a task and prints a stack trace if an error occurs.
|
* Executes a task and prints a stack trace if an error occurs.
|
||||||
*/
|
*/
|
||||||
@ -1136,7 +1153,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
|||||||
entity.tick();
|
entity.tick();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (armAnimationTicks != -1) {
|
if (armAnimationTicks >= 0) {
|
||||||
// As of 1.18.2 Java Edition, it appears that the swing time is dynamically updated depending on the
|
// As of 1.18.2 Java Edition, it appears that the swing time is dynamically updated depending on the
|
||||||
// player's effect status, but the animation can cut short if the duration suddenly decreases
|
// player's effect status, but the animation can cut short if the duration suddenly decreases
|
||||||
// (from suddenly no longer having mining fatigue, for example)
|
// (from suddenly no longer having mining fatigue, for example)
|
||||||
@ -1175,7 +1192,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
|||||||
public void startSneaking() {
|
public void startSneaking() {
|
||||||
// Toggle the shield, if there is no ongoing arm animation
|
// Toggle the shield, if there is no ongoing arm animation
|
||||||
// This matches Bedrock Edition behavior as of 1.18.12
|
// This matches Bedrock Edition behavior as of 1.18.12
|
||||||
if (armAnimationTicks == -1) {
|
if (armAnimationTicks < 0) {
|
||||||
attemptToBlock();
|
attemptToBlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1305,6 +1322,16 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For https://github.com/GeyserMC/Geyser/issues/2113 and combating arm ticking activating being delayed in
|
||||||
|
* BedrockAnimateTranslator.
|
||||||
|
*/
|
||||||
|
public void armSwingPending() {
|
||||||
|
if (armAnimationTicks == -1) {
|
||||||
|
armAnimationTicks = -2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates to the client to stop blocking and tells the Java server the same.
|
* Indicates to the client to stop blocking and tells the Java server the same.
|
||||||
*/
|
*/
|
||||||
@ -1369,7 +1396,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setServerRenderDistance(int renderDistance) {
|
public void setServerRenderDistance(int renderDistance) {
|
||||||
renderDistance = GenericMath.ceil(++renderDistance * MathUtils.SQRT_OF_TWO); //square to circle
|
// +1 is for Fabric and Spigot
|
||||||
|
// Without the client misses loading some chunks per https://github.com/GeyserMC/Geyser/issues/3490
|
||||||
|
// Fog still appears essentially normally
|
||||||
|
renderDistance = renderDistance + 1;
|
||||||
this.serverRenderDistance = renderDistance;
|
this.serverRenderDistance = renderDistance;
|
||||||
|
|
||||||
ChunkRadiusUpdatedPacket chunkRadiusUpdatedPacket = new ChunkRadiusUpdatedPacket();
|
ChunkRadiusUpdatedPacket chunkRadiusUpdatedPacket = new ChunkRadiusUpdatedPacket();
|
||||||
@ -1865,4 +1895,19 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
|||||||
sendUpstreamPacket(transferPacket);
|
sendUpstreamPacket(transferPacket);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void addCommandEnum(String name, String... enums) {
|
||||||
|
softEnumPacket(name, SoftEnumUpdateType.ADD, enums);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeCommandEnum(String name, String... enums) {
|
||||||
|
softEnumPacket(name, SoftEnumUpdateType.REMOVE, enums);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void softEnumPacket(String name, SoftEnumUpdateType type, String... enums) {
|
||||||
|
UpdateSoftEnumPacket packet = new UpdateSoftEnumPacket();
|
||||||
|
packet.setType(type);
|
||||||
|
packet.setSoftEnum(new CommandEnumData(name, enums, true));
|
||||||
|
sendUpstreamPacket(packet);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,7 +123,8 @@ public class EntityCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void addPlayerEntity(PlayerEntity entity) {
|
public void addPlayerEntity(PlayerEntity entity) {
|
||||||
playerEntities.put(entity.getUuid(), entity);
|
// putIfAbsent matches the behavior of playerInfoMap in Java as of 1.19.3
|
||||||
|
playerEntities.putIfAbsent(entity.getUuid(), entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PlayerEntity getPlayerEntity(UUID uuid) {
|
public PlayerEntity getPlayerEntity(UUID uuid) {
|
||||||
|
@ -71,8 +71,9 @@ public class SkullCache {
|
|||||||
this.skullRenderDistanceSquared = distance * distance;
|
this.skullRenderDistanceSquared = distance * distance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void putSkull(Vector3i position, String texturesProperty, int blockState) {
|
public void putSkull(Vector3i position, UUID uuid, String texturesProperty, int blockState) {
|
||||||
Skull skull = skulls.computeIfAbsent(position, Skull::new);
|
Skull skull = skulls.computeIfAbsent(position, Skull::new);
|
||||||
|
skull.uuid = uuid;
|
||||||
skull.texturesProperty = texturesProperty;
|
skull.texturesProperty = texturesProperty;
|
||||||
skull.blockState = blockState;
|
skull.blockState = blockState;
|
||||||
|
|
||||||
@ -201,6 +202,7 @@ public class SkullCache {
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Data
|
@Data
|
||||||
public static class Skull {
|
public static class Skull {
|
||||||
|
private UUID uuid;
|
||||||
private String texturesProperty;
|
private String texturesProperty;
|
||||||
private int blockState;
|
private int blockState;
|
||||||
private SkullPlayerEntity entity;
|
private SkullPlayerEntity entity;
|
||||||
|
@ -33,6 +33,8 @@ import it.unimi.dsi.fastutil.objects.Object2IntMaps;
|
|||||||
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
|
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||||
|
import org.geysermc.geyser.api.util.TriState;
|
||||||
import org.geysermc.geyser.scoreboard.Scoreboard;
|
import org.geysermc.geyser.scoreboard.Scoreboard;
|
||||||
import org.geysermc.geyser.scoreboard.ScoreboardUpdater.ScoreboardSession;
|
import org.geysermc.geyser.scoreboard.ScoreboardUpdater.ScoreboardSession;
|
||||||
import org.geysermc.geyser.session.GeyserSession;
|
import org.geysermc.geyser.session.GeyserSession;
|
||||||
@ -61,6 +63,17 @@ public final class WorldCache {
|
|||||||
private int currentSequence;
|
private int currentSequence;
|
||||||
private final Object2IntMap<Vector3i> unverifiedPredictions = new Object2IntOpenHashMap<>(1);
|
private final Object2IntMap<Vector3i> unverifiedPredictions = new Object2IntOpenHashMap<>(1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <ul>
|
||||||
|
* <li>NOT_SET = not yet triggered</li>
|
||||||
|
* <li>FALSE = enforce-secure-profile is true but player hasn't chatted yet</li>
|
||||||
|
* <li>TRUE = enforce-secure-profile is enabled, and player has chatted and they have seen our message.</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
private @NonNull TriState chatWarningSent = TriState.NOT_SET;
|
||||||
|
|
||||||
public WorldCache(GeyserSession session) {
|
public WorldCache(GeyserSession session) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
this.scoreboard = new Scoreboard(session);
|
this.scoreboard = new Scoreboard(session);
|
||||||
|
@ -29,10 +29,6 @@ import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
|||||||
import com.google.common.cache.CacheBuilder;
|
import com.google.common.cache.CacheBuilder;
|
||||||
import com.google.common.cache.CacheLoader;
|
import com.google.common.cache.CacheLoader;
|
||||||
import com.google.common.cache.LoadingCache;
|
import com.google.common.cache.LoadingCache;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.skin.ImageData;
|
|
||||||
import org.cloudburstmc.protocol.bedrock.data.skin.SerializedSkin;
|
|
||||||
import org.cloudburstmc.protocol.bedrock.packet.PlayerListPacket;
|
|
||||||
import org.cloudburstmc.protocol.bedrock.packet.PlayerSkinPacket;
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
@ -45,7 +41,6 @@ import org.geysermc.geyser.text.GeyserLocale;
|
|||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@ -68,7 +63,7 @@ public class FakeHeadProvider {
|
|||||||
|
|
||||||
SkinProvider.Skin skin = skinData.skin();
|
SkinProvider.Skin skin = skinData.skin();
|
||||||
SkinProvider.Cape cape = skinData.cape();
|
SkinProvider.Cape cape = skinData.cape();
|
||||||
SkinProvider.SkinGeometry geometry = skinData.geometry().getGeometryName().equals("{\"geometry\" :{\"default\" :\"geometry.humanoid.customSlim\"}}")
|
SkinProvider.SkinGeometry geometry = skinData.geometry().geometryName().equals("{\"geometry\" :{\"default\" :\"geometry.humanoid.customSlim\"}}")
|
||||||
? SkinProvider.WEARING_CUSTOM_SKULL_SLIM : SkinProvider.WEARING_CUSTOM_SKULL;
|
? SkinProvider.WEARING_CUSTOM_SKULL_SLIM : SkinProvider.WEARING_CUSTOM_SKULL;
|
||||||
|
|
||||||
SkinProvider.Skin headSkin = SkinProvider.getOrDefault(
|
SkinProvider.Skin headSkin = SkinProvider.getOrDefault(
|
||||||
@ -111,7 +106,7 @@ public class FakeHeadProvider {
|
|||||||
try {
|
try {
|
||||||
SkinProvider.SkinData mergedSkinData = MERGED_SKINS_LOADING_CACHE.get(new FakeHeadEntry(texturesProperty, fakeHeadSkinUrl, entity));
|
SkinProvider.SkinData mergedSkinData = MERGED_SKINS_LOADING_CACHE.get(new FakeHeadEntry(texturesProperty, fakeHeadSkinUrl, entity));
|
||||||
|
|
||||||
sendSkinPacket(session, entity, mergedSkinData);
|
SkinManager.sendSkinPacket(session, entity, mergedSkinData);
|
||||||
} catch (ExecutionException e) {
|
} catch (ExecutionException e) {
|
||||||
GeyserImpl.getInstance().getLogger().error("Couldn't merge skin of " + entity.getUsername() + " with head skin url " + fakeHeadSkinUrl, e);
|
GeyserImpl.getInstance().getLogger().error("Couldn't merge skin of " + entity.getUsername() + " with head skin url " + fakeHeadSkinUrl, e);
|
||||||
}
|
}
|
||||||
@ -133,50 +128,10 @@ public class FakeHeadProvider {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendSkinPacket(session, entity, skinData);
|
SkinManager.sendSkinPacket(session, entity, skinData);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void sendSkinPacket(GeyserSession session, PlayerEntity entity, SkinProvider.SkinData skinData) {
|
|
||||||
SkinProvider.Skin skin = skinData.skin();
|
|
||||||
SkinProvider.Cape cape = skinData.cape();
|
|
||||||
SkinProvider.SkinGeometry geometry = skinData.geometry();
|
|
||||||
|
|
||||||
if (entity.getUuid().equals(session.getPlayerEntity().getUuid())) {
|
|
||||||
PlayerListPacket.Entry updatedEntry = SkinManager.buildEntryManually(
|
|
||||||
session,
|
|
||||||
entity.getUuid(),
|
|
||||||
entity.getUsername(),
|
|
||||||
entity.getGeyserId(),
|
|
||||||
skin.getTextureUrl(),
|
|
||||||
skin.getSkinData(),
|
|
||||||
cape.getCapeId(),
|
|
||||||
cape.getCapeData(),
|
|
||||||
geometry
|
|
||||||
);
|
|
||||||
|
|
||||||
PlayerListPacket playerAddPacket = new PlayerListPacket();
|
|
||||||
playerAddPacket.setAction(PlayerListPacket.Action.ADD);
|
|
||||||
playerAddPacket.getEntries().add(updatedEntry);
|
|
||||||
session.sendUpstreamPacket(playerAddPacket);
|
|
||||||
} else {
|
|
||||||
PlayerSkinPacket packet = new PlayerSkinPacket();
|
|
||||||
packet.setUuid(entity.getUuid());
|
|
||||||
packet.setOldSkinName("");
|
|
||||||
packet.setNewSkinName(skin.getTextureUrl());
|
|
||||||
packet.setSkin(getSkin(skin.getTextureUrl(), skin, cape, geometry));
|
|
||||||
packet.setTrustedSkin(true);
|
|
||||||
session.sendUpstreamPacket(packet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static SerializedSkin getSkin(String skinId, SkinProvider.Skin skin, SkinProvider.Cape cape, SkinProvider.SkinGeometry geometry) {
|
|
||||||
return SerializedSkin.of(skinId, "", geometry.getGeometryName(),
|
|
||||||
ImageData.of(skin.getSkinData()), Collections.emptyList(),
|
|
||||||
ImageData.of(cape.getCapeData()), geometry.getGeometryData(),
|
|
||||||
"", true, false, false, cape.getCapeId(), skinId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
|
@ -1,63 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in
|
|
||||||
* all copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
* THE SOFTWARE.
|
|
||||||
*
|
|
||||||
* @author GeyserMC
|
|
||||||
* @link https://github.com/GeyserMC/Geyser
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.geysermc.geyser.skin;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import org.geysermc.geyser.GeyserImpl;
|
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
public class ProvidedSkin {
|
|
||||||
@Getter private byte[] skin;
|
|
||||||
|
|
||||||
public ProvidedSkin(String internalUrl) {
|
|
||||||
try {
|
|
||||||
BufferedImage image;
|
|
||||||
try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResource(internalUrl)) {
|
|
||||||
image = ImageIO.read(stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(image.getWidth() * 4 + image.getHeight() * 4);
|
|
||||||
for (int y = 0; y < image.getHeight(); y++) {
|
|
||||||
for (int x = 0; x < image.getWidth(); x++) {
|
|
||||||
int rgba = image.getRGB(x, y);
|
|
||||||
outputStream.write((rgba >> 16) & 0xFF); // Red
|
|
||||||
outputStream.write((rgba >> 8) & 0xFF); // Green
|
|
||||||
outputStream.write(rgba & 0xFF); // Blue
|
|
||||||
outputStream.write((rgba >> 24) & 0xFF); // Alpha
|
|
||||||
}
|
|
||||||
}
|
|
||||||
image.flush();
|
|
||||||
skin = outputStream.toByteArray();
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
128
core/src/main/java/org/geysermc/geyser/skin/ProvidedSkins.java
Normale Datei
128
core/src/main/java/org/geysermc/geyser/skin/ProvidedSkins.java
Normale Datei
@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*
|
||||||
|
* @author GeyserMC
|
||||||
|
* @link https://github.com/GeyserMC/Geyser
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.geysermc.geyser.skin;
|
||||||
|
|
||||||
|
import org.geysermc.geyser.GeyserImpl;
|
||||||
|
import org.geysermc.geyser.util.AssetUtils;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public final class ProvidedSkins {
|
||||||
|
private static final ProvidedSkin[] PROVIDED_SKINS = {
|
||||||
|
new ProvidedSkin("textures/entity/player/slim/alex.png", true),
|
||||||
|
new ProvidedSkin("textures/entity/player/slim/ari.png", true),
|
||||||
|
new ProvidedSkin("textures/entity/player/slim/efe.png", true),
|
||||||
|
new ProvidedSkin("textures/entity/player/slim/kai.png", true),
|
||||||
|
new ProvidedSkin("textures/entity/player/slim/makena.png", true),
|
||||||
|
new ProvidedSkin("textures/entity/player/slim/noor.png", true),
|
||||||
|
new ProvidedSkin("textures/entity/player/slim/steve.png", true),
|
||||||
|
new ProvidedSkin("textures/entity/player/slim/sunny.png", true),
|
||||||
|
new ProvidedSkin("textures/entity/player/slim/zuri.png", true),
|
||||||
|
new ProvidedSkin("textures/entity/player/wide/alex.png", false),
|
||||||
|
new ProvidedSkin("textures/entity/player/wide/ari.png", false),
|
||||||
|
new ProvidedSkin("textures/entity/player/wide/efe.png", false),
|
||||||
|
new ProvidedSkin("textures/entity/player/wide/kai.png", false),
|
||||||
|
new ProvidedSkin("textures/entity/player/wide/makena.png", false),
|
||||||
|
new ProvidedSkin("textures/entity/player/wide/noor.png", false),
|
||||||
|
new ProvidedSkin("textures/entity/player/wide/steve.png", false),
|
||||||
|
new ProvidedSkin("textures/entity/player/wide/sunny.png", false),
|
||||||
|
new ProvidedSkin("textures/entity/player/wide/zuri.png", false)
|
||||||
|
};
|
||||||
|
|
||||||
|
public static ProvidedSkin getDefaultPlayerSkin(UUID uuid) {
|
||||||
|
return PROVIDED_SKINS[Math.floorMod(uuid.hashCode(), PROVIDED_SKINS.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProvidedSkins() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class ProvidedSkin {
|
||||||
|
private SkinProvider.Skin data;
|
||||||
|
private final boolean slim;
|
||||||
|
|
||||||
|
ProvidedSkin(String asset, boolean slim) {
|
||||||
|
this.slim = slim;
|
||||||
|
|
||||||
|
Path folder = GeyserImpl.getInstance().getBootstrap().getConfigFolder()
|
||||||
|
.resolve("cache")
|
||||||
|
.resolve("default_player_skins")
|
||||||
|
.resolve(slim ? "slim" : "wide");
|
||||||
|
String assetName = asset.substring(asset.lastIndexOf('/') + 1);
|
||||||
|
|
||||||
|
File location = folder.resolve(assetName).toFile();
|
||||||
|
AssetUtils.addTask(!location.exists(), new AssetUtils.ClientJarTask("assets/minecraft/" + asset,
|
||||||
|
(stream) -> AssetUtils.saveFile(location, stream),
|
||||||
|
() -> {
|
||||||
|
try {
|
||||||
|
// TODO lazy initialize?
|
||||||
|
BufferedImage image;
|
||||||
|
try (InputStream stream = new FileInputStream(location)) {
|
||||||
|
image = ImageIO.read(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] byteData = SkinProvider.bufferedImageToImageData(image);
|
||||||
|
image.flush();
|
||||||
|
|
||||||
|
String identifier = "geysermc:" + assetName + "_" + (slim ? "slim" : "wide");
|
||||||
|
this.data = new SkinProvider.Skin(-1, identifier, byteData);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public SkinProvider.Skin getData() {
|
||||||
|
// Fall back to the default skin if we can't load our skins, or it's not loaded yet.
|
||||||
|
return Objects.requireNonNullElse(data, SkinProvider.EMPTY_SKIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSlim() {
|
||||||
|
return slim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void init() {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
Path folder = GeyserImpl.getInstance().getBootstrap().getConfigFolder()
|
||||||
|
.resolve("cache")
|
||||||
|
.resolve("default_player_skins");
|
||||||
|
folder.toFile().mkdirs();
|
||||||
|
// Two directories since there are two skins for each model: one slim, one wide
|
||||||
|
folder.resolve("slim").toFile().mkdir();
|
||||||
|
folder.resolve("wide").toFile().mkdir();
|
||||||
|
}
|
||||||
|
}
|
@ -32,9 +32,10 @@ import com.github.steveice10.opennbt.tag.builtin.StringTag;
|
|||||||
import org.cloudburstmc.protocol.bedrock.data.skin.ImageData;
|
import org.cloudburstmc.protocol.bedrock.data.skin.ImageData;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.skin.SerializedSkin;
|
import org.cloudburstmc.protocol.bedrock.data.skin.SerializedSkin;
|
||||||
import org.cloudburstmc.protocol.bedrock.packet.PlayerListPacket;
|
import org.cloudburstmc.protocol.bedrock.packet.PlayerListPacket;
|
||||||
|
import org.cloudburstmc.protocol.bedrock.packet.PlayerSkinPacket;
|
||||||
import org.geysermc.geyser.GeyserImpl;
|
import org.geysermc.geyser.GeyserImpl;
|
||||||
import org.geysermc.geyser.api.network.AuthType;
|
|
||||||
import org.geysermc.geyser.entity.type.player.PlayerEntity;
|
import org.geysermc.geyser.entity.type.player.PlayerEntity;
|
||||||
|
import org.geysermc.geyser.entity.type.player.SkullPlayerEntity;
|
||||||
import org.geysermc.geyser.session.GeyserSession;
|
import org.geysermc.geyser.session.GeyserSession;
|
||||||
import org.geysermc.geyser.session.auth.BedrockClientData;
|
import org.geysermc.geyser.session.auth.BedrockClientData;
|
||||||
import org.geysermc.geyser.text.GeyserLocale;
|
import org.geysermc.geyser.text.GeyserLocale;
|
||||||
@ -53,13 +54,30 @@ public class SkinManager {
|
|||||||
* Builds a Bedrock player list entry from our existing, cached Bedrock skin information
|
* Builds a Bedrock player list entry from our existing, cached Bedrock skin information
|
||||||
*/
|
*/
|
||||||
public static PlayerListPacket.Entry buildCachedEntry(GeyserSession session, PlayerEntity playerEntity) {
|
public static PlayerListPacket.Entry buildCachedEntry(GeyserSession session, PlayerEntity playerEntity) {
|
||||||
|
// First: see if we have the cached skin texture ID.
|
||||||
GameProfileData data = GameProfileData.from(playerEntity);
|
GameProfileData data = GameProfileData.from(playerEntity);
|
||||||
SkinProvider.Cape cape = SkinProvider.getCachedCape(data.capeUrl());
|
SkinProvider.Skin skin = null;
|
||||||
SkinProvider.SkinGeometry geometry = SkinProvider.SkinGeometry.getLegacy(data.isAlex());
|
SkinProvider.Cape cape = null;
|
||||||
|
SkinProvider.SkinGeometry geometry = SkinProvider.SkinGeometry.WIDE;
|
||||||
|
if (data != null) {
|
||||||
|
// GameProfileData is not null = server provided us with textures data to work with.
|
||||||
|
skin = SkinProvider.getCachedSkin(data.skinUrl());
|
||||||
|
cape = SkinProvider.getCachedCape(data.capeUrl());
|
||||||
|
geometry = data.isAlex() ? SkinProvider.SkinGeometry.SLIM : SkinProvider.SkinGeometry.WIDE;
|
||||||
|
}
|
||||||
|
|
||||||
SkinProvider.Skin skin = SkinProvider.getCachedSkin(data.skinUrl());
|
if (skin == null || cape == null) {
|
||||||
|
// The server either didn't have a texture to send, or we didn't have the texture ID cached.
|
||||||
|
// Let's see if this player is a Bedrock player, and if so, let's pull their skin.
|
||||||
|
// Otherwise, grab the default player skin
|
||||||
|
SkinProvider.SkinData fallbackSkinData = SkinProvider.determineFallbackSkinData(playerEntity.getUuid());
|
||||||
if (skin == null) {
|
if (skin == null) {
|
||||||
skin = SkinProvider.EMPTY_SKIN;
|
skin = fallbackSkinData.skin();
|
||||||
|
geometry = fallbackSkinData.geometry();
|
||||||
|
}
|
||||||
|
if (cape == null) {
|
||||||
|
cape = fallbackSkinData.cape();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildEntryManually(
|
return buildEntryManually(
|
||||||
@ -67,10 +85,8 @@ public class SkinManager {
|
|||||||
playerEntity.getUuid(),
|
playerEntity.getUuid(),
|
||||||
playerEntity.getUsername(),
|
playerEntity.getUsername(),
|
||||||
playerEntity.getGeyserId(),
|
playerEntity.getGeyserId(),
|
||||||
skin.getTextureUrl(),
|
skin,
|
||||||
skin.getSkinData(),
|
cape,
|
||||||
cape.getCapeId(),
|
|
||||||
cape.getCapeData(),
|
|
||||||
geometry
|
geometry
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -79,14 +95,10 @@ public class SkinManager {
|
|||||||
* With all the information needed, build a Bedrock player entry with translated skin information.
|
* With all the information needed, build a Bedrock player entry with translated skin information.
|
||||||
*/
|
*/
|
||||||
public static PlayerListPacket.Entry buildEntryManually(GeyserSession session, UUID uuid, String username, long geyserId,
|
public static PlayerListPacket.Entry buildEntryManually(GeyserSession session, UUID uuid, String username, long geyserId,
|
||||||
String skinId, byte[] skinData,
|
SkinProvider.Skin skin,
|
||||||
String capeId, byte[] capeData,
|
SkinProvider.Cape cape,
|
||||||
SkinProvider.SkinGeometry geometry) {
|
SkinProvider.SkinGeometry geometry) {
|
||||||
SerializedSkin serializedSkin = SerializedSkin.of(
|
SerializedSkin serializedSkin = getSkin(skin.getTextureUrl(), skin, cape, geometry);
|
||||||
skinId, "", geometry.getGeometryName(), ImageData.of(skinData), Collections.emptyList(),
|
|
||||||
ImageData.of(capeData), geometry.getGeometryData(), "", true, false,
|
|
||||||
!capeId.equals(SkinProvider.EMPTY_CAPE.getCapeId()), capeId, skinId
|
|
||||||
);
|
|
||||||
|
|
||||||
// This attempts to find the XUID of the player so profile images show up for Xbox accounts
|
// This attempts to find the XUID of the player so profile images show up for Xbox accounts
|
||||||
String xuid = "";
|
String xuid = "";
|
||||||
@ -116,6 +128,45 @@ public class SkinManager {
|
|||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void sendSkinPacket(GeyserSession session, PlayerEntity entity, SkinProvider.SkinData skinData) {
|
||||||
|
SkinProvider.Skin skin = skinData.skin();
|
||||||
|
SkinProvider.Cape cape = skinData.cape();
|
||||||
|
SkinProvider.SkinGeometry geometry = skinData.geometry();
|
||||||
|
|
||||||
|
if (entity.getUuid().equals(session.getPlayerEntity().getUuid())) {
|
||||||
|
// TODO is this special behavior needed?
|
||||||
|
PlayerListPacket.Entry updatedEntry = buildEntryManually(
|
||||||
|
session,
|
||||||
|
entity.getUuid(),
|
||||||
|
entity.getUsername(),
|
||||||
|
entity.getGeyserId(),
|
||||||
|
skin,
|
||||||
|
cape,
|
||||||
|
geometry
|
||||||
|
);
|
||||||
|
|
||||||
|
PlayerListPacket playerAddPacket = new PlayerListPacket();
|
||||||
|
playerAddPacket.setAction(PlayerListPacket.Action.ADD);
|
||||||
|
playerAddPacket.getEntries().add(updatedEntry);
|
||||||
|
session.sendUpstreamPacket(playerAddPacket);
|
||||||
|
} else {
|
||||||
|
PlayerSkinPacket packet = new PlayerSkinPacket();
|
||||||
|
packet.setUuid(entity.getUuid());
|
||||||
|
packet.setOldSkinName("");
|
||||||
|
packet.setNewSkinName(skin.getTextureUrl());
|
||||||
|
packet.setSkin(getSkin(skin.getTextureUrl(), skin, cape, geometry));
|
||||||
|
packet.setTrustedSkin(true);
|
||||||
|
session.sendUpstreamPacket(packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SerializedSkin getSkin(String skinId, SkinProvider.Skin skin, SkinProvider.Cape cape, SkinProvider.SkinGeometry geometry) {
|
||||||
|
return SerializedSkin.of(skinId, "", geometry.geometryName(),
|
||||||
|
ImageData.of(skin.getSkinData()), Collections.emptyList(),
|
||||||
|
ImageData.of(cape.capeData()), geometry.geometryData(),
|
||||||
|
"", true, false, false, cape.capeId(), skinId);
|
||||||
|
}
|
||||||
|
|
||||||
public static void requestAndHandleSkinAndCape(PlayerEntity entity, GeyserSession session,
|
public static void requestAndHandleSkinAndCape(PlayerEntity entity, GeyserSession session,
|
||||||
Consumer<SkinProvider.SkinAndCape> skinAndCapeConsumer) {
|
Consumer<SkinProvider.SkinAndCape> skinAndCapeConsumer) {
|
||||||
SkinProvider.requestSkinData(entity).whenCompleteAsync((skinData, throwable) -> {
|
SkinProvider.requestSkinData(entity).whenCompleteAsync((skinData, throwable) -> {
|
||||||
@ -128,34 +179,7 @@ public class SkinManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (skinData.geometry() != null) {
|
if (skinData.geometry() != null) {
|
||||||
SkinProvider.Skin skin = skinData.skin();
|
sendSkinPacket(session, entity, skinData);
|
||||||
SkinProvider.Cape cape = skinData.cape();
|
|
||||||
SkinProvider.SkinGeometry geometry = skinData.geometry();
|
|
||||||
|
|
||||||
PlayerListPacket.Entry updatedEntry = buildEntryManually(
|
|
||||||
session,
|
|
||||||
entity.getUuid(),
|
|
||||||
entity.getUsername(),
|
|
||||||
entity.getGeyserId(),
|
|
||||||
skin.getTextureUrl(),
|
|
||||||
skin.getSkinData(),
|
|
||||||
cape.getCapeId(),
|
|
||||||
cape.getCapeData(),
|
|
||||||
geometry
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
PlayerListPacket playerAddPacket = new PlayerListPacket();
|
|
||||||
playerAddPacket.setAction(PlayerListPacket.Action.ADD);
|
|
||||||
playerAddPacket.getEntries().add(updatedEntry);
|
|
||||||
session.sendUpstreamPacket(playerAddPacket);
|
|
||||||
|
|
||||||
if (!entity.isPlayerList()) {
|
|
||||||
PlayerListPacket playerRemovePacket = new PlayerListPacket();
|
|
||||||
playerRemovePacket.setAction(PlayerListPacket.Action.REMOVE);
|
|
||||||
playerRemovePacket.getEntries().add(updatedEntry);
|
|
||||||
session.sendUpstreamPacket(playerRemovePacket);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (skinAndCapeConsumer != null) {
|
if (skinAndCapeConsumer != null) {
|
||||||
@ -186,7 +210,7 @@ public class SkinManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!clientData.getCapeId().equals("")) {
|
if (!clientData.getCapeId().equals("")) {
|
||||||
SkinProvider.storeBedrockCape(playerEntity.getUuid(), capeBytes);
|
SkinProvider.storeBedrockCape(clientData.getCapeId(), capeBytes);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new AssertionError("Failed to cache skin for bedrock user (" + playerEntity.getUsername() + "): ", e);
|
throw new AssertionError("Failed to cache skin for bedrock user (" + playerEntity.getUsername() + "): ", e);
|
||||||
@ -231,30 +255,29 @@ public class SkinManager {
|
|||||||
* @param entity entity to build the GameProfileData from
|
* @param entity entity to build the GameProfileData from
|
||||||
* @return The built GameProfileData
|
* @return The built GameProfileData
|
||||||
*/
|
*/
|
||||||
public static GameProfileData from(PlayerEntity entity) {
|
public static @Nullable GameProfileData from(PlayerEntity entity) {
|
||||||
try {
|
|
||||||
String texturesProperty = entity.getTexturesProperty();
|
String texturesProperty = entity.getTexturesProperty();
|
||||||
|
|
||||||
if (texturesProperty == null) {
|
if (texturesProperty == null) {
|
||||||
// Likely offline mode
|
// Likely offline mode
|
||||||
return loadBedrockOrOfflineSkin(entity);
|
return null;
|
||||||
}
|
}
|
||||||
GameProfileData data = loadFromJson(texturesProperty);
|
|
||||||
if (data != null) {
|
try {
|
||||||
return data;
|
return loadFromJson(texturesProperty);
|
||||||
|
} catch (Exception exception) {
|
||||||
|
if (entity instanceof SkullPlayerEntity skullEntity) {
|
||||||
|
GeyserImpl.getInstance().getLogger().debug("Something went wrong while processing skin for skull at " + skullEntity.getSkullPosition() + " with Value: " + texturesProperty);
|
||||||
} else {
|
} else {
|
||||||
return loadBedrockOrOfflineSkin(entity);
|
GeyserImpl.getInstance().getLogger().debug("Something went wrong while processing skin for " + entity.getUsername() + " with Value: " + texturesProperty);
|
||||||
}
|
}
|
||||||
} catch (IOException exception) {
|
|
||||||
GeyserImpl.getInstance().getLogger().debug("Something went wrong while processing skin for " + entity.getUsername());
|
|
||||||
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
|
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
|
||||||
exception.printStackTrace();
|
exception.printStackTrace();
|
||||||
}
|
}
|
||||||
return loadBedrockOrOfflineSkin(entity);
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static GameProfileData loadFromJson(String encodedJson) throws IOException {
|
private static GameProfileData loadFromJson(String encodedJson) throws IOException, IllegalArgumentException {
|
||||||
JsonNode skinObject = GeyserImpl.JSON_MAPPER.readTree(new String(Base64.getDecoder().decode(encodedJson), StandardCharsets.UTF_8));
|
JsonNode skinObject = GeyserImpl.JSON_MAPPER.readTree(new String(Base64.getDecoder().decode(encodedJson), StandardCharsets.UTF_8));
|
||||||
JsonNode textures = skinObject.get("textures");
|
JsonNode textures = skinObject.get("textures");
|
||||||
|
|
||||||
@ -267,39 +290,26 @@ public class SkinManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
String skinUrl = skinTexture.get("url").asText().replace("http://", "https://");
|
String skinUrl;
|
||||||
|
JsonNode skinUrlNode = skinTexture.get("url");
|
||||||
|
if (skinUrlNode != null && skinUrlNode.isTextual()) {
|
||||||
|
skinUrl = skinUrlNode.asText().replace("http://", "https://");
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
boolean isAlex = skinTexture.has("metadata");
|
boolean isAlex = skinTexture.has("metadata");
|
||||||
|
|
||||||
String capeUrl = null;
|
String capeUrl = null;
|
||||||
JsonNode capeTexture = textures.get("CAPE");
|
JsonNode capeTexture = textures.get("CAPE");
|
||||||
if (capeTexture != null) {
|
if (capeTexture != null) {
|
||||||
capeUrl = capeTexture.get("url").asText().replace("http://", "https://");
|
JsonNode capeUrlNode = capeTexture.get("url");
|
||||||
|
if (capeUrlNode != null && capeUrlNode.isTextual()) {
|
||||||
|
capeUrl = capeUrlNode.asText().replace("http://", "https://");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new GameProfileData(skinUrl, capeUrl, isAlex);
|
return new GameProfileData(skinUrl, capeUrl, isAlex);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return default skin with default cape when texture data is invalid, or the Bedrock player's skin if this
|
|
||||||
* is a Bedrock player.
|
|
||||||
*/
|
|
||||||
private static GameProfileData loadBedrockOrOfflineSkin(PlayerEntity entity) {
|
|
||||||
// Fallback to the offline mode of working it out
|
|
||||||
UUID uuid = entity.getUuid();
|
|
||||||
boolean isAlex = (Math.abs(uuid.hashCode() % 2) == 1);
|
|
||||||
|
|
||||||
String skinUrl = isAlex ? SkinProvider.EMPTY_SKIN_ALEX.getTextureUrl() : SkinProvider.EMPTY_SKIN.getTextureUrl();
|
|
||||||
String capeUrl = SkinProvider.EMPTY_CAPE.getTextureUrl();
|
|
||||||
if (("steve".equals(skinUrl) || "alex".equals(skinUrl)) && GeyserImpl.getInstance().getConfig().getRemote().authType() != AuthType.ONLINE) {
|
|
||||||
GeyserSession session = GeyserImpl.getInstance().connectionByUuid(uuid);
|
|
||||||
|
|
||||||
if (session != null) {
|
|
||||||
skinUrl = session.getClientData().getSkinId();
|
|
||||||
capeUrl = session.getClientData().getCapeId();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new GameProfileData(skinUrl, capeUrl, isAlex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,22 +26,22 @@
|
|||||||
package org.geysermc.geyser.skin;
|
package org.geysermc.geyser.skin;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
|
||||||
import com.github.steveice10.opennbt.tag.builtin.IntArrayTag;
|
|
||||||
import com.github.steveice10.opennbt.tag.builtin.Tag;
|
|
||||||
import com.google.common.cache.Cache;
|
import com.google.common.cache.Cache;
|
||||||
import com.google.common.cache.CacheBuilder;
|
import com.google.common.cache.CacheBuilder;
|
||||||
|
import it.unimi.dsi.fastutil.bytes.ByteArrays;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import org.geysermc.geyser.GeyserImpl;
|
import org.geysermc.geyser.GeyserImpl;
|
||||||
|
import org.geysermc.geyser.api.network.AuthType;
|
||||||
import org.geysermc.geyser.entity.type.player.PlayerEntity;
|
import org.geysermc.geyser.entity.type.player.PlayerEntity;
|
||||||
import org.geysermc.geyser.session.GeyserSession;
|
import org.geysermc.geyser.session.GeyserSession;
|
||||||
import org.geysermc.geyser.text.GeyserLocale;
|
import org.geysermc.geyser.text.GeyserLocale;
|
||||||
import org.geysermc.geyser.util.FileUtils;
|
import org.geysermc.geyser.util.FileUtils;
|
||||||
import org.geysermc.geyser.util.WebUtils;
|
import org.geysermc.geyser.util.WebUtils;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
@ -57,28 +57,28 @@ import java.util.concurrent.*;
|
|||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
public class SkinProvider {
|
public class SkinProvider {
|
||||||
public static final boolean ALLOW_THIRD_PARTY_CAPES = GeyserImpl.getInstance().getConfig().isAllowThirdPartyCapes();
|
private static final boolean ALLOW_THIRD_PARTY_CAPES = GeyserImpl.getInstance().getConfig().isAllowThirdPartyCapes();
|
||||||
static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(ALLOW_THIRD_PARTY_CAPES ? 21 : 14);
|
static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(ALLOW_THIRD_PARTY_CAPES ? 21 : 14);
|
||||||
|
|
||||||
public static final byte[] STEVE_SKIN = new ProvidedSkin("bedrock/skin/skin_steve.png").getSkin();
|
static final Skin EMPTY_SKIN;
|
||||||
public static final Skin EMPTY_SKIN = new Skin(-1, "steve", STEVE_SKIN);
|
static final Cape EMPTY_CAPE = new Cape("", "no-cape", ByteArrays.EMPTY_ARRAY, -1, true);
|
||||||
public static final byte[] ALEX_SKIN = new ProvidedSkin("bedrock/skin/skin_alex.png").getSkin();
|
|
||||||
public static final Skin EMPTY_SKIN_ALEX = new Skin(-1, "alex", ALEX_SKIN);
|
private static final Cache<String, Cape> CACHED_JAVA_CAPES = CacheBuilder.newBuilder()
|
||||||
private static final Map<String, Skin> permanentSkins = new HashMap<>() {{
|
.expireAfterAccess(1, TimeUnit.HOURS)
|
||||||
put("steve", EMPTY_SKIN);
|
.build();
|
||||||
put("alex", EMPTY_SKIN_ALEX);
|
private static final Cache<String, Skin> CACHED_JAVA_SKINS = CacheBuilder.newBuilder()
|
||||||
}};
|
|
||||||
private static final Cache<String, Skin> cachedSkins = CacheBuilder.newBuilder()
|
|
||||||
.expireAfterAccess(1, TimeUnit.HOURS)
|
.expireAfterAccess(1, TimeUnit.HOURS)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
private static final Map<String, CompletableFuture<Skin>> requestedSkins = new ConcurrentHashMap<>();
|
private static final Cache<String, Cape> CACHED_BEDROCK_CAPES = CacheBuilder.newBuilder()
|
||||||
|
|
||||||
public static final Cape EMPTY_CAPE = new Cape("", "no-cape", new byte[0], -1, true);
|
|
||||||
private static final Cache<String, Cape> cachedCapes = CacheBuilder.newBuilder()
|
|
||||||
.expireAfterAccess(1, TimeUnit.HOURS)
|
.expireAfterAccess(1, TimeUnit.HOURS)
|
||||||
.build();
|
.build();
|
||||||
|
private static final Cache<String, Skin> CACHED_BEDROCK_SKINS = CacheBuilder.newBuilder()
|
||||||
|
.expireAfterAccess(1, TimeUnit.HOURS)
|
||||||
|
.build();
|
||||||
|
|
||||||
private static final Map<String, CompletableFuture<Cape>> requestedCapes = new ConcurrentHashMap<>();
|
private static final Map<String, CompletableFuture<Cape>> requestedCapes = new ConcurrentHashMap<>();
|
||||||
|
private static final Map<String, CompletableFuture<Skin>> requestedSkins = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private static final Map<UUID, SkinGeometry> cachedGeometry = new ConcurrentHashMap<>();
|
private static final Map<UUID, SkinGeometry> cachedGeometry = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
@ -86,18 +86,36 @@ public class SkinProvider {
|
|||||||
* Citizens NPCs use UUID version 2, while legitimate Minecraft players use version 4, and
|
* Citizens NPCs use UUID version 2, while legitimate Minecraft players use version 4, and
|
||||||
* offline mode players use version 3.
|
* offline mode players use version 3.
|
||||||
*/
|
*/
|
||||||
public static final Predicate<UUID> IS_NPC = uuid -> uuid.version() == 2;
|
private static final Predicate<UUID> IS_NPC = uuid -> uuid.version() == 2;
|
||||||
|
|
||||||
public static final boolean ALLOW_THIRD_PARTY_EARS = GeyserImpl.getInstance().getConfig().isAllowThirdPartyEars();
|
private static final boolean ALLOW_THIRD_PARTY_EARS = GeyserImpl.getInstance().getConfig().isAllowThirdPartyEars();
|
||||||
public static final String EARS_GEOMETRY;
|
private static final String EARS_GEOMETRY;
|
||||||
public static final String EARS_GEOMETRY_SLIM;
|
private static final String EARS_GEOMETRY_SLIM;
|
||||||
public static final SkinGeometry SKULL_GEOMETRY;
|
static final SkinGeometry SKULL_GEOMETRY;
|
||||||
public static final SkinGeometry WEARING_CUSTOM_SKULL;
|
static final SkinGeometry WEARING_CUSTOM_SKULL;
|
||||||
public static final SkinGeometry WEARING_CUSTOM_SKULL_SLIM;
|
static final SkinGeometry WEARING_CUSTOM_SKULL_SLIM;
|
||||||
|
|
||||||
public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
|
||||||
|
|
||||||
static {
|
static {
|
||||||
|
// Generate the empty texture to use as an emergency fallback
|
||||||
|
final int pink = -524040;
|
||||||
|
final int black = -16777216;
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(64 * 4 + 64 * 4);
|
||||||
|
for (int y = 0; y < 64; y++) {
|
||||||
|
for (int x = 0; x < 64; x++) {
|
||||||
|
int rgba;
|
||||||
|
if (y > 32) {
|
||||||
|
rgba = x >= 32 ? pink : black;
|
||||||
|
} else {
|
||||||
|
rgba = x >= 32 ? black : pink;
|
||||||
|
}
|
||||||
|
outputStream.write((rgba >> 16) & 0xFF); // Red
|
||||||
|
outputStream.write((rgba >> 8) & 0xFF); // Green
|
||||||
|
outputStream.write(rgba & 0xFF); // Blue
|
||||||
|
outputStream.write((rgba >> 24) & 0xFF); // Alpha
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EMPTY_SKIN = new Skin(-1, "geysermc:empty", outputStream.toByteArray());
|
||||||
|
|
||||||
/* Load in the normal ears geometry */
|
/* Load in the normal ears geometry */
|
||||||
EARS_GEOMETRY = new String(FileUtils.readAllBytes("bedrock/skin/geometry.humanoid.ears.json"), StandardCharsets.UTF_8);
|
EARS_GEOMETRY = new String(FileUtils.readAllBytes("bedrock/skin/geometry.humanoid.ears.json"), StandardCharsets.UTF_8);
|
||||||
|
|
||||||
@ -141,48 +159,103 @@ public class SkinProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean hasCapeCached(String capeUrl) {
|
/**
|
||||||
return cachedCapes.getIfPresent(capeUrl) != null;
|
* Search our cached database for an already existing, translated skin of this Java URL.
|
||||||
|
*/
|
||||||
|
static Skin getCachedSkin(String skinUrl) {
|
||||||
|
return CACHED_JAVA_SKINS.getIfPresent(skinUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Skin getCachedSkin(String skinUrl) {
|
/**
|
||||||
return permanentSkins.getOrDefault(skinUrl, cachedSkins.getIfPresent(skinUrl));
|
* If skin data fails to apply, or there is no skin data to apply, determine what skin we should give as a fallback.
|
||||||
|
*/
|
||||||
|
static SkinData determineFallbackSkinData(UUID uuid) {
|
||||||
|
Skin skin = null;
|
||||||
|
Cape cape = null;
|
||||||
|
SkinGeometry geometry = SkinGeometry.WIDE;
|
||||||
|
|
||||||
|
if (GeyserImpl.getInstance().getConfig().getRemote().authType() != AuthType.ONLINE) {
|
||||||
|
// Let's see if this player is a Bedrock player, and if so, let's pull their skin.
|
||||||
|
GeyserSession session = GeyserImpl.getInstance().connectionByUuid(uuid);
|
||||||
|
if (session != null) {
|
||||||
|
String skinId = session.getClientData().getSkinId();
|
||||||
|
skin = CACHED_BEDROCK_SKINS.getIfPresent(skinId);
|
||||||
|
String capeId = session.getClientData().getCapeId();
|
||||||
|
cape = CACHED_BEDROCK_CAPES.getIfPresent(capeId);
|
||||||
|
geometry = cachedGeometry.getOrDefault(uuid, geometry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Cape getCachedCape(String capeUrl) {
|
if (skin == null) {
|
||||||
Cape cape = capeUrl != null ? cachedCapes.getIfPresent(capeUrl) : EMPTY_CAPE;
|
// We don't have a skin for the player right now. Fall back to a default.
|
||||||
return cape != null ? cape : EMPTY_CAPE;
|
ProvidedSkins.ProvidedSkin providedSkin = ProvidedSkins.getDefaultPlayerSkin(uuid);
|
||||||
|
skin = providedSkin.getData();
|
||||||
|
geometry = providedSkin.isSlim() ? SkinProvider.SkinGeometry.SLIM : SkinProvider.SkinGeometry.WIDE;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CompletableFuture<SkinProvider.SkinData> requestSkinData(PlayerEntity entity) {
|
if (cape == null) {
|
||||||
|
cape = EMPTY_CAPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SkinData(skin, cape, geometry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used as a fallback if an official Java cape doesn't exist for this user.
|
||||||
|
*/
|
||||||
|
@Nonnull
|
||||||
|
private static Cape getCachedBedrockCape(UUID uuid) {
|
||||||
|
GeyserSession session = GeyserImpl.getInstance().connectionByUuid(uuid);
|
||||||
|
if (session != null) {
|
||||||
|
String capeId = session.getClientData().getCapeId();
|
||||||
|
Cape bedrockCape = CACHED_BEDROCK_CAPES.getIfPresent(capeId);
|
||||||
|
if (bedrockCape != null) {
|
||||||
|
return bedrockCape;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return EMPTY_CAPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
static Cape getCachedCape(String capeUrl) {
|
||||||
|
if (capeUrl == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return CACHED_JAVA_CAPES.getIfPresent(capeUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
static CompletableFuture<SkinProvider.SkinData> requestSkinData(PlayerEntity entity) {
|
||||||
SkinManager.GameProfileData data = SkinManager.GameProfileData.from(entity);
|
SkinManager.GameProfileData data = SkinManager.GameProfileData.from(entity);
|
||||||
|
if (data == null) {
|
||||||
|
// This player likely does not have a textures property
|
||||||
|
return CompletableFuture.completedFuture(determineFallbackSkinData(entity.getUuid()));
|
||||||
|
}
|
||||||
|
|
||||||
return requestSkinAndCape(entity.getUuid(), data.skinUrl(), data.capeUrl())
|
return requestSkinAndCape(entity.getUuid(), data.skinUrl(), data.capeUrl())
|
||||||
.thenApplyAsync(skinAndCape -> {
|
.thenApplyAsync(skinAndCape -> {
|
||||||
try {
|
try {
|
||||||
Skin skin = skinAndCape.getSkin();
|
Skin skin = skinAndCape.skin();
|
||||||
Cape cape = skinAndCape.getCape();
|
Cape cape = skinAndCape.cape();
|
||||||
SkinGeometry geometry = SkinGeometry.getLegacy(data.isAlex());
|
SkinGeometry geometry = data.isAlex() ? SkinGeometry.SLIM : SkinGeometry.WIDE;
|
||||||
|
|
||||||
if (cape.isFailed()) {
|
// Whether we should see if this player has a Bedrock skin we should check for on failure of
|
||||||
cape = getOrDefault(requestBedrockCape(entity.getUuid()),
|
// any skin property
|
||||||
EMPTY_CAPE, 3);
|
boolean checkForBedrock = entity.getUuid().version() != 4;
|
||||||
|
|
||||||
|
if (cape.failed() && checkForBedrock) {
|
||||||
|
cape = getCachedBedrockCape(entity.getUuid());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cape.isFailed() && ALLOW_THIRD_PARTY_CAPES) {
|
if (cape.failed() && ALLOW_THIRD_PARTY_CAPES) {
|
||||||
cape = getOrDefault(requestUnofficialCape(
|
cape = getOrDefault(requestUnofficialCape(
|
||||||
cape, entity.getUuid(),
|
cape, entity.getUuid(),
|
||||||
entity.getUsername(), false
|
entity.getUsername(), false
|
||||||
), EMPTY_CAPE, CapeProvider.VALUES.length * 3);
|
), EMPTY_CAPE, CapeProvider.VALUES.length * 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
geometry = getOrDefault(requestBedrockGeometry(
|
|
||||||
geometry, entity.getUuid()
|
|
||||||
), geometry, 3);
|
|
||||||
|
|
||||||
boolean isDeadmau5 = "deadmau5".equals(entity.getUsername());
|
boolean isDeadmau5 = "deadmau5".equals(entity.getUsername());
|
||||||
// Not a bedrock player check for ears
|
// Not a bedrock player check for ears
|
||||||
if (geometry.isFailed() && (ALLOW_THIRD_PARTY_EARS || isDeadmau5)) {
|
if (geometry.failed() && (ALLOW_THIRD_PARTY_EARS || isDeadmau5)) {
|
||||||
boolean isEars;
|
boolean isEars;
|
||||||
|
|
||||||
// Its deadmau5, gotta support his skin :)
|
// Its deadmau5, gotta support his skin :)
|
||||||
@ -213,26 +286,17 @@ public class SkinProvider {
|
|||||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e);
|
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new SkinData(skinAndCape.getSkin(), skinAndCape.getCape(), null);
|
return new SkinData(skinAndCape.skin(), skinAndCape.cape(), null);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CompletableFuture<SkinAndCape> requestSkinAndCape(UUID playerId, String skinUrl, String capeUrl) {
|
private static CompletableFuture<SkinAndCape> requestSkinAndCape(UUID playerId, String skinUrl, String capeUrl) {
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
long time = System.currentTimeMillis();
|
long time = System.currentTimeMillis();
|
||||||
String newSkinUrl = skinUrl;
|
|
||||||
|
|
||||||
if ("steve".equals(skinUrl) || "alex".equals(skinUrl)) {
|
|
||||||
GeyserSession session = GeyserImpl.getInstance().connectionByUuid(playerId);
|
|
||||||
|
|
||||||
if (session != null) {
|
|
||||||
newSkinUrl = session.getClientData().getSkinId();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CapeProvider provider = capeUrl != null ? CapeProvider.MINECRAFT : null;
|
CapeProvider provider = capeUrl != null ? CapeProvider.MINECRAFT : null;
|
||||||
SkinAndCape skinAndCape = new SkinAndCape(
|
SkinAndCape skinAndCape = new SkinAndCape(
|
||||||
getOrDefault(requestSkin(playerId, newSkinUrl, false), EMPTY_SKIN, 5),
|
getOrDefault(requestSkin(playerId, skinUrl, false), EMPTY_SKIN, 5),
|
||||||
getOrDefault(requestCape(capeUrl, provider, false), EMPTY_CAPE, 5)
|
getOrDefault(requestCape(capeUrl, provider, false), EMPTY_CAPE, 5)
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -241,7 +305,7 @@ public class SkinProvider {
|
|||||||
}, EXECUTOR_SERVICE);
|
}, EXECUTOR_SERVICE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CompletableFuture<Skin> requestSkin(UUID playerId, String textureUrl, boolean newThread) {
|
static CompletableFuture<Skin> requestSkin(UUID playerId, String textureUrl, boolean newThread) {
|
||||||
if (textureUrl == null || textureUrl.isEmpty()) return CompletableFuture.completedFuture(EMPTY_SKIN);
|
if (textureUrl == null || textureUrl.isEmpty()) return CompletableFuture.completedFuture(EMPTY_SKIN);
|
||||||
CompletableFuture<Skin> requestedSkin = requestedSkins.get(textureUrl);
|
CompletableFuture<Skin> requestedSkin = requestedSkins.get(textureUrl);
|
||||||
if (requestedSkin != null) {
|
if (requestedSkin != null) {
|
||||||
@ -249,7 +313,7 @@ public class SkinProvider {
|
|||||||
return requestedSkin;
|
return requestedSkin;
|
||||||
}
|
}
|
||||||
|
|
||||||
Skin cachedSkin = getCachedSkin(textureUrl);
|
Skin cachedSkin = CACHED_JAVA_SKINS.getIfPresent(textureUrl);
|
||||||
if (cachedSkin != null) {
|
if (cachedSkin != null) {
|
||||||
return CompletableFuture.completedFuture(cachedSkin);
|
return CompletableFuture.completedFuture(cachedSkin);
|
||||||
}
|
}
|
||||||
@ -259,23 +323,26 @@ public class SkinProvider {
|
|||||||
future = CompletableFuture.supplyAsync(() -> supplySkin(playerId, textureUrl), EXECUTOR_SERVICE)
|
future = CompletableFuture.supplyAsync(() -> supplySkin(playerId, textureUrl), EXECUTOR_SERVICE)
|
||||||
.whenCompleteAsync((skin, throwable) -> {
|
.whenCompleteAsync((skin, throwable) -> {
|
||||||
skin.updated = true;
|
skin.updated = true;
|
||||||
cachedSkins.put(textureUrl, skin);
|
CACHED_JAVA_SKINS.put(textureUrl, skin);
|
||||||
requestedSkins.remove(textureUrl);
|
requestedSkins.remove(textureUrl);
|
||||||
});
|
});
|
||||||
requestedSkins.put(textureUrl, future);
|
requestedSkins.put(textureUrl, future);
|
||||||
} else {
|
} else {
|
||||||
Skin skin = supplySkin(playerId, textureUrl);
|
Skin skin = supplySkin(playerId, textureUrl);
|
||||||
future = CompletableFuture.completedFuture(skin);
|
future = CompletableFuture.completedFuture(skin);
|
||||||
cachedSkins.put(textureUrl, skin);
|
CACHED_JAVA_SKINS.put(textureUrl, skin);
|
||||||
}
|
}
|
||||||
return future;
|
return future;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CompletableFuture<Cape> requestCape(String capeUrl, CapeProvider provider, boolean newThread) {
|
private static CompletableFuture<Cape> requestCape(String capeUrl, CapeProvider provider, boolean newThread) {
|
||||||
if (capeUrl == null || capeUrl.isEmpty()) return CompletableFuture.completedFuture(EMPTY_CAPE);
|
if (capeUrl == null || capeUrl.isEmpty()) return CompletableFuture.completedFuture(EMPTY_CAPE);
|
||||||
if (requestedCapes.containsKey(capeUrl)) return requestedCapes.get(capeUrl); // already requested
|
CompletableFuture<Cape> requestedCape = requestedCapes.get(capeUrl);
|
||||||
|
if (requestedCape != null) {
|
||||||
|
return requestedCape;
|
||||||
|
}
|
||||||
|
|
||||||
Cape cachedCape = cachedCapes.getIfPresent(capeUrl);
|
Cape cachedCape = CACHED_JAVA_CAPES.getIfPresent(capeUrl);
|
||||||
if (cachedCape != null) {
|
if (cachedCape != null) {
|
||||||
return CompletableFuture.completedFuture(cachedCape);
|
return CompletableFuture.completedFuture(cachedCape);
|
||||||
}
|
}
|
||||||
@ -284,21 +351,21 @@ public class SkinProvider {
|
|||||||
if (newThread) {
|
if (newThread) {
|
||||||
future = CompletableFuture.supplyAsync(() -> supplyCape(capeUrl, provider), EXECUTOR_SERVICE)
|
future = CompletableFuture.supplyAsync(() -> supplyCape(capeUrl, provider), EXECUTOR_SERVICE)
|
||||||
.whenCompleteAsync((cape, throwable) -> {
|
.whenCompleteAsync((cape, throwable) -> {
|
||||||
cachedCapes.put(capeUrl, cape);
|
CACHED_JAVA_CAPES.put(capeUrl, cape);
|
||||||
requestedCapes.remove(capeUrl);
|
requestedCapes.remove(capeUrl);
|
||||||
});
|
});
|
||||||
requestedCapes.put(capeUrl, future);
|
requestedCapes.put(capeUrl, future);
|
||||||
} else {
|
} else {
|
||||||
Cape cape = supplyCape(capeUrl, provider); // blocking
|
Cape cape = supplyCape(capeUrl, provider); // blocking
|
||||||
future = CompletableFuture.completedFuture(cape);
|
future = CompletableFuture.completedFuture(cape);
|
||||||
cachedCapes.put(capeUrl, cape);
|
CACHED_JAVA_CAPES.put(capeUrl, cape);
|
||||||
}
|
}
|
||||||
return future;
|
return future;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CompletableFuture<Cape> requestUnofficialCape(Cape officialCape, UUID playerId,
|
private static CompletableFuture<Cape> requestUnofficialCape(Cape officialCape, UUID playerId,
|
||||||
String username, boolean newThread) {
|
String username, boolean newThread) {
|
||||||
if (officialCape.isFailed() && ALLOW_THIRD_PARTY_CAPES) {
|
if (officialCape.failed() && ALLOW_THIRD_PARTY_CAPES) {
|
||||||
for (CapeProvider provider : CapeProvider.VALUES) {
|
for (CapeProvider provider : CapeProvider.VALUES) {
|
||||||
if (provider.type != CapeUrlType.USERNAME && IS_NPC.test(playerId)) {
|
if (provider.type != CapeUrlType.USERNAME && IS_NPC.test(playerId)) {
|
||||||
continue;
|
continue;
|
||||||
@ -308,7 +375,7 @@ public class SkinProvider {
|
|||||||
requestCape(provider.getUrlFor(playerId, username), provider, newThread),
|
requestCape(provider.getUrlFor(playerId, username), provider, newThread),
|
||||||
EMPTY_CAPE, 4
|
EMPTY_CAPE, 4
|
||||||
);
|
);
|
||||||
if (!cape1.isFailed()) {
|
if (!cape1.failed()) {
|
||||||
return CompletableFuture.completedFuture(cape1);
|
return CompletableFuture.completedFuture(cape1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -316,7 +383,7 @@ public class SkinProvider {
|
|||||||
return CompletableFuture.completedFuture(officialCape);
|
return CompletableFuture.completedFuture(officialCape);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CompletableFuture<Skin> requestEars(String earsUrl, boolean newThread, Skin skin) {
|
private static CompletableFuture<Skin> requestEars(String earsUrl, boolean newThread, Skin skin) {
|
||||||
if (earsUrl == null || earsUrl.isEmpty()) return CompletableFuture.completedFuture(skin);
|
if (earsUrl == null || earsUrl.isEmpty()) return CompletableFuture.completedFuture(skin);
|
||||||
|
|
||||||
CompletableFuture<Skin> future;
|
CompletableFuture<Skin> future;
|
||||||
@ -339,7 +406,7 @@ public class SkinProvider {
|
|||||||
* @param newThread Should we start in a new thread
|
* @param newThread Should we start in a new thread
|
||||||
* @return The updated skin with ears
|
* @return The updated skin with ears
|
||||||
*/
|
*/
|
||||||
public static CompletableFuture<Skin> requestUnofficialEars(Skin officialSkin, UUID playerId, String username, boolean newThread) {
|
private static CompletableFuture<Skin> requestUnofficialEars(Skin officialSkin, UUID playerId, String username, boolean newThread) {
|
||||||
for (EarsProvider provider : EarsProvider.VALUES) {
|
for (EarsProvider provider : EarsProvider.VALUES) {
|
||||||
if (provider.type != CapeUrlType.USERNAME && IS_NPC.test(playerId)) {
|
if (provider.type != CapeUrlType.USERNAME && IS_NPC.test(playerId)) {
|
||||||
continue;
|
continue;
|
||||||
@ -357,30 +424,17 @@ public class SkinProvider {
|
|||||||
return CompletableFuture.completedFuture(officialSkin);
|
return CompletableFuture.completedFuture(officialSkin);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CompletableFuture<Cape> requestBedrockCape(UUID playerID) {
|
static void storeBedrockSkin(UUID playerID, String skinId, byte[] skinData) {
|
||||||
Cape bedrockCape = cachedCapes.getIfPresent(playerID.toString() + ".Bedrock");
|
Skin skin = new Skin(playerID, skinId, skinData, System.currentTimeMillis(), true, false);
|
||||||
if (bedrockCape == null) {
|
CACHED_BEDROCK_SKINS.put(skin.getTextureUrl(), skin);
|
||||||
bedrockCape = EMPTY_CAPE;
|
|
||||||
}
|
|
||||||
return CompletableFuture.completedFuture(bedrockCape);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CompletableFuture<SkinGeometry> requestBedrockGeometry(SkinGeometry currentGeometry, UUID playerID) {
|
static void storeBedrockCape(String capeId, byte[] capeData) {
|
||||||
SkinGeometry bedrockGeometry = cachedGeometry.getOrDefault(playerID, currentGeometry);
|
Cape cape = new Cape(capeId, capeId, capeData, System.currentTimeMillis(), false);
|
||||||
return CompletableFuture.completedFuture(bedrockGeometry);
|
CACHED_BEDROCK_CAPES.put(capeId, cape);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void storeBedrockSkin(UUID playerID, String skinID, byte[] skinData) {
|
static void storeBedrockGeometry(UUID playerID, byte[] geometryName, byte[] geometryData) {
|
||||||
Skin skin = new Skin(playerID, skinID, skinData, System.currentTimeMillis(), true, false);
|
|
||||||
cachedSkins.put(skin.getTextureUrl(), skin);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void storeBedrockCape(UUID playerID, byte[] capeData) {
|
|
||||||
Cape cape = new Cape(playerID.toString() + ".Bedrock", playerID.toString(), capeData, System.currentTimeMillis(), false);
|
|
||||||
cachedCapes.put(playerID.toString() + ".Bedrock", cape);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void storeBedrockGeometry(UUID playerID, byte[] geometryName, byte[] geometryData) {
|
|
||||||
SkinGeometry geometry = new SkinGeometry(new String(geometryName), new String(geometryData), false);
|
SkinGeometry geometry = new SkinGeometry(new String(geometryName), new String(geometryData), false);
|
||||||
cachedGeometry.put(playerID, geometry);
|
cachedGeometry.put(playerID, geometry);
|
||||||
}
|
}
|
||||||
@ -391,7 +445,7 @@ public class SkinProvider {
|
|||||||
* @param skin The skin to cache
|
* @param skin The skin to cache
|
||||||
*/
|
*/
|
||||||
public static void storeEarSkin(Skin skin) {
|
public static void storeEarSkin(Skin skin) {
|
||||||
cachedSkins.put(skin.getTextureUrl(), skin);
|
CACHED_JAVA_SKINS.put(skin.getTextureUrl(), skin);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -400,7 +454,7 @@ public class SkinProvider {
|
|||||||
* @param playerID The UUID to cache it against
|
* @param playerID The UUID to cache it against
|
||||||
* @param isSlim If the player is using an slim base
|
* @param isSlim If the player is using an slim base
|
||||||
*/
|
*/
|
||||||
public static void storeEarGeometry(UUID playerID, boolean isSlim) {
|
private static void storeEarGeometry(UUID playerID, boolean isSlim) {
|
||||||
cachedGeometry.put(playerID, SkinGeometry.getEars(isSlim));
|
cachedGeometry.put(playerID, SkinGeometry.getEars(isSlim));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -414,7 +468,7 @@ public class SkinProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static Cape supplyCape(String capeUrl, CapeProvider provider) {
|
private static Cape supplyCape(String capeUrl, CapeProvider provider) {
|
||||||
byte[] cape = EMPTY_CAPE.getCapeData();
|
byte[] cape = EMPTY_CAPE.capeData();
|
||||||
try {
|
try {
|
||||||
cape = requestImage(capeUrl, provider);
|
cape = requestImage(capeUrl, provider);
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
@ -539,48 +593,23 @@ public class SkinProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If a skull has a username but no textures, request them.
|
* Request textures from a player's UUID
|
||||||
*
|
*
|
||||||
* @param skullOwner the CompoundTag of the skull with no textures
|
* @param uuid the player's UUID without any hyphens
|
||||||
* @return a completable GameProfile with textures included
|
* @return a completable GameProfile with textures included
|
||||||
*/
|
*/
|
||||||
public static CompletableFuture<String> requestTexturesFromUsername(CompoundTag skullOwner) {
|
public static CompletableFuture<String> requestTexturesFromUUID(String uuid) {
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
Tag uuidTag = skullOwner.get("Id");
|
|
||||||
String uuidToString = "";
|
|
||||||
JsonNode node;
|
|
||||||
boolean retrieveUuidFromInternet = !(uuidTag instanceof IntArrayTag); // also covers null check
|
|
||||||
|
|
||||||
if (!retrieveUuidFromInternet) {
|
|
||||||
int[] uuidAsArray = ((IntArrayTag) uuidTag).getValue();
|
|
||||||
// thank u viaversion
|
|
||||||
UUID uuid = new UUID((long) uuidAsArray[0] << 32 | ((long) uuidAsArray[1] & 0xFFFFFFFFL),
|
|
||||||
(long) uuidAsArray[2] << 32 | ((long) uuidAsArray[3] & 0xFFFFFFFFL));
|
|
||||||
retrieveUuidFromInternet = uuid.version() != 4;
|
|
||||||
uuidToString = uuid.toString().replace("-", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (retrieveUuidFromInternet) {
|
JsonNode node = WebUtils.getJson("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid);
|
||||||
// Offline skin, or no present UUID
|
|
||||||
node = WebUtils.getJson("https://api.mojang.com/users/profiles/minecraft/" + skullOwner.get("Name").getValue());
|
|
||||||
JsonNode id = node.get("id");
|
|
||||||
if (id == null) {
|
|
||||||
GeyserImpl.getInstance().getLogger().debug("No UUID found in Mojang response for " + skullOwner.get("Name").getValue());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
uuidToString = id.asText();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get textures from UUID
|
|
||||||
node = WebUtils.getJson("https://sessionserver.mojang.com/session/minecraft/profile/" + uuidToString);
|
|
||||||
JsonNode properties = node.get("properties");
|
JsonNode properties = node.get("properties");
|
||||||
if (properties == null) {
|
if (properties == null) {
|
||||||
GeyserImpl.getInstance().getLogger().debug("No properties found in Mojang response for " + uuidToString);
|
GeyserImpl.getInstance().getLogger().debug("No properties found in Mojang response for " + uuid);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return node.get("properties").get(0).get("value").asText();
|
return node.get("properties").get(0).get("value").asText();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
GeyserImpl.getInstance().getLogger().debug("Unable to request textures for " + uuid);
|
||||||
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
|
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
@ -589,6 +618,37 @@ public class SkinProvider {
|
|||||||
}, EXECUTOR_SERVICE);
|
}, EXECUTOR_SERVICE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request textures from a player's username
|
||||||
|
*
|
||||||
|
* @param username the player's username
|
||||||
|
* @return a completable GameProfile with textures included
|
||||||
|
*/
|
||||||
|
public static CompletableFuture<String> requestTexturesFromUsername(String username) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
try {
|
||||||
|
// Offline skin, or no present UUID
|
||||||
|
JsonNode node = WebUtils.getJson("https://api.mojang.com/users/profiles/minecraft/" + username);
|
||||||
|
JsonNode id = node.get("id");
|
||||||
|
if (id == null) {
|
||||||
|
GeyserImpl.getInstance().getLogger().debug("No UUID found in Mojang response for " + username);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return id.asText();
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, EXECUTOR_SERVICE).thenCompose(uuid -> {
|
||||||
|
if (uuid == null) {
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
}
|
||||||
|
return requestTexturesFromUUID(uuid);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static BufferedImage downloadImage(String imageUrl, CapeProvider provider) throws IOException {
|
private static BufferedImage downloadImage(String imageUrl, CapeProvider provider) throws IOException {
|
||||||
if (provider == CapeProvider.FIVEZIG)
|
if (provider == CapeProvider.FIVEZIG)
|
||||||
return readFiveZigCape(imageUrl);
|
return readFiveZigCape(imageUrl);
|
||||||
@ -604,7 +664,7 @@ public class SkinProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static BufferedImage readFiveZigCape(String url) throws IOException {
|
private static BufferedImage readFiveZigCape(String url) throws IOException {
|
||||||
JsonNode element = OBJECT_MAPPER.readTree(WebUtils.getBody(url));
|
JsonNode element = GeyserImpl.JSON_MAPPER.readTree(WebUtils.getBody(url));
|
||||||
if (element != null && element.isObject()) {
|
if (element != null && element.isObject()) {
|
||||||
JsonNode capeElement = element.get("d");
|
JsonNode capeElement = element.get("d");
|
||||||
if (capeElement == null || capeElement.isNull()) return null;
|
if (capeElement == null || capeElement.isNull()) return null;
|
||||||
@ -683,13 +743,12 @@ public class SkinProvider {
|
|||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@AllArgsConstructor
|
public record SkinAndCape(Skin skin, Cape cape) {
|
||||||
@Getter
|
|
||||||
public static class SkinAndCape {
|
|
||||||
private final Skin skin;
|
|
||||||
private final Cape cape;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a full package of skin, cape, and geometry.
|
||||||
|
*/
|
||||||
public record SkinData(Skin skin, Cape cape, SkinGeometry geometry) {
|
public record SkinData(Skin skin, Cape cape, SkinGeometry geometry) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -703,29 +762,19 @@ public class SkinProvider {
|
|||||||
private boolean updated;
|
private boolean updated;
|
||||||
private boolean ears;
|
private boolean ears;
|
||||||
|
|
||||||
private Skin(long requestedOn, String textureUrl, byte[] skinData) {
|
Skin(long requestedOn, String textureUrl, byte[] skinData) {
|
||||||
this.requestedOn = requestedOn;
|
this.requestedOn = requestedOn;
|
||||||
this.textureUrl = textureUrl;
|
this.textureUrl = textureUrl;
|
||||||
this.skinData = skinData;
|
this.skinData = skinData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AllArgsConstructor
|
public record Cape(String textureUrl, String capeId, byte[] capeData, long requestedOn, boolean failed) {
|
||||||
@Getter
|
|
||||||
public static class Cape {
|
|
||||||
private final String textureUrl;
|
|
||||||
private final String capeId;
|
|
||||||
private final byte[] capeData;
|
|
||||||
private final long requestedOn;
|
|
||||||
private final boolean failed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@AllArgsConstructor
|
public record SkinGeometry(String geometryName, String geometryData, boolean failed) {
|
||||||
@Getter
|
public static SkinGeometry WIDE = getLegacy(false);
|
||||||
public static class SkinGeometry {
|
public static SkinGeometry SLIM = getLegacy(true);
|
||||||
private final String geometryName;
|
|
||||||
private final String geometryData;
|
|
||||||
private final boolean failed;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate generic geometry
|
* Generate generic geometry
|
||||||
@ -733,7 +782,7 @@ public class SkinProvider {
|
|||||||
* @param isSlim Should it be the alex model
|
* @param isSlim Should it be the alex model
|
||||||
* @return The generic geometry object
|
* @return The generic geometry object
|
||||||
*/
|
*/
|
||||||
public static SkinGeometry getLegacy(boolean isSlim) {
|
private static SkinGeometry getLegacy(boolean isSlim) {
|
||||||
return new SkinProvider.SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.custom" + (isSlim ? "Slim" : "") + "\"}}", "", true);
|
return new SkinProvider.SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.custom" + (isSlim ? "Slim" : "") + "\"}}", "", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -743,7 +792,7 @@ public class SkinProvider {
|
|||||||
* @param isSlim Should it be the alex model
|
* @param isSlim Should it be the alex model
|
||||||
* @return The generated geometry for the ears model
|
* @return The generated geometry for the ears model
|
||||||
*/
|
*/
|
||||||
public static SkinGeometry getEars(boolean isSlim) {
|
private static SkinGeometry getEars(boolean isSlim) {
|
||||||
return new SkinProvider.SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.ears" + (isSlim ? "Slim" : "") + "\"}}", (isSlim ? EARS_GEOMETRY_SLIM : EARS_GEOMETRY), false);
|
return new SkinProvider.SkinGeometry("{\"geometry\" :{\"default\" :\"geometry.humanoid.ears" + (isSlim ? "Slim" : "") + "\"}}", (isSlim ? EARS_GEOMETRY_SLIM : EARS_GEOMETRY), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,11 +29,12 @@ import org.cloudburstmc.protocol.bedrock.data.skin.ImageData;
|
|||||||
import org.cloudburstmc.protocol.bedrock.data.skin.SerializedSkin;
|
import org.cloudburstmc.protocol.bedrock.data.skin.SerializedSkin;
|
||||||
import org.cloudburstmc.protocol.bedrock.packet.PlayerSkinPacket;
|
import org.cloudburstmc.protocol.bedrock.packet.PlayerSkinPacket;
|
||||||
import org.geysermc.geyser.GeyserImpl;
|
import org.geysermc.geyser.GeyserImpl;
|
||||||
import org.geysermc.geyser.entity.type.player.PlayerEntity;
|
import org.geysermc.geyser.entity.type.player.SkullPlayerEntity;
|
||||||
import org.geysermc.geyser.session.GeyserSession;
|
import org.geysermc.geyser.session.GeyserSession;
|
||||||
import org.geysermc.geyser.text.GeyserLocale;
|
import org.geysermc.geyser.text.GeyserLocale;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
public class SkullSkinManager extends SkinManager {
|
public class SkullSkinManager extends SkinManager {
|
||||||
@ -42,18 +43,15 @@ public class SkullSkinManager extends SkinManager {
|
|||||||
// Prevents https://cdn.discordapp.com/attachments/613194828359925800/779458146191147008/unknown.png
|
// Prevents https://cdn.discordapp.com/attachments/613194828359925800/779458146191147008/unknown.png
|
||||||
skinId = skinId + "_skull";
|
skinId = skinId + "_skull";
|
||||||
return SerializedSkin.of(
|
return SerializedSkin.of(
|
||||||
skinId, "", SkinProvider.SKULL_GEOMETRY.getGeometryName(), ImageData.of(skinData), Collections.emptyList(),
|
skinId, "", SkinProvider.SKULL_GEOMETRY.geometryName(), ImageData.of(skinData), Collections.emptyList(),
|
||||||
ImageData.of(SkinProvider.EMPTY_CAPE.getCapeData()), SkinProvider.SKULL_GEOMETRY.getGeometryData(),
|
ImageData.of(SkinProvider.EMPTY_CAPE.capeData()), SkinProvider.SKULL_GEOMETRY.geometryData(),
|
||||||
"", true, false, false, SkinProvider.EMPTY_CAPE.getCapeId(), skinId
|
"", true, false, false, SkinProvider.EMPTY_CAPE.capeId(), skinId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void requestAndHandleSkin(PlayerEntity entity, GeyserSession session,
|
public static void requestAndHandleSkin(SkullPlayerEntity entity, GeyserSession session,
|
||||||
Consumer<SkinProvider.Skin> skinConsumer) {
|
Consumer<SkinProvider.Skin> skinConsumer) {
|
||||||
GameProfileData data = GameProfileData.from(entity);
|
BiConsumer<SkinProvider.Skin, Throwable> applySkin = (skin, throwable) -> {
|
||||||
|
|
||||||
SkinProvider.requestSkin(entity.getUuid(), data.skinUrl(), true)
|
|
||||||
.whenCompleteAsync((skin, throwable) -> {
|
|
||||||
try {
|
try {
|
||||||
PlayerSkinPacket packet = new PlayerSkinPacket();
|
PlayerSkinPacket packet = new PlayerSkinPacket();
|
||||||
packet.setUuid(entity.getUuid());
|
packet.setUuid(entity.getUuid());
|
||||||
@ -69,7 +67,19 @@ public class SkullSkinManager extends SkinManager {
|
|||||||
if (skinConsumer != null) {
|
if (skinConsumer != null) {
|
||||||
skinConsumer.accept(skin);
|
skinConsumer.accept(skin);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
GameProfileData data = GameProfileData.from(entity);
|
||||||
|
if (data == null) {
|
||||||
|
GeyserImpl.getInstance().getLogger().debug("Using fallback skin for skull at " + entity.getSkullPosition() +
|
||||||
|
" with texture value: " + entity.getTexturesProperty() + " and UUID: " + entity.getSkullUUID());
|
||||||
|
// No texture available, fallback using the UUID
|
||||||
|
SkinProvider.SkinData fallback = SkinProvider.determineFallbackSkinData(entity.getSkullUUID());
|
||||||
|
applySkin.accept(fallback.skin(), null);
|
||||||
|
} else {
|
||||||
|
SkinProvider.requestSkin(entity.getUuid(), data.skinUrl(), true)
|
||||||
|
.whenCompleteAsync(applySkin);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -25,91 +25,45 @@
|
|||||||
|
|
||||||
package org.geysermc.geyser.text;
|
package org.geysermc.geyser.text;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import lombok.Getter;
|
|
||||||
import org.geysermc.geyser.GeyserImpl;
|
import org.geysermc.geyser.GeyserImpl;
|
||||||
import org.geysermc.geyser.network.GameProtocol;
|
import org.geysermc.geyser.util.AssetUtils;
|
||||||
import org.geysermc.geyser.util.FileUtils;
|
import org.geysermc.geyser.util.FileUtils;
|
||||||
import org.geysermc.geyser.util.WebUtils;
|
import org.geysermc.geyser.util.WebUtils;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.util.HashMap;
|
||||||
import java.util.*;
|
import java.util.Iterator;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.Locale;
|
||||||
import java.util.zip.ZipFile;
|
import java.util.Map;
|
||||||
|
|
||||||
public class MinecraftLocale {
|
public class MinecraftLocale {
|
||||||
|
|
||||||
public static final Map<String, Map<String, String>> LOCALE_MAPPINGS = new HashMap<>();
|
public static final Map<String, Map<String, String>> LOCALE_MAPPINGS = new HashMap<>();
|
||||||
|
|
||||||
private static final Map<String, Asset> ASSET_MAP = new HashMap<>();
|
|
||||||
|
|
||||||
private static VersionDownload clientJarInfo;
|
|
||||||
|
|
||||||
static {
|
static {
|
||||||
// Create the locales folder
|
// Create the locales folder
|
||||||
File localesFolder = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales").toFile();
|
File localesFolder = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales").toFile();
|
||||||
//noinspection ResultOfMethodCallIgnored
|
//noinspection ResultOfMethodCallIgnored
|
||||||
localesFolder.mkdir();
|
localesFolder.mkdir();
|
||||||
|
|
||||||
// Download the latest asset list and cache it
|
// FIXME TEMPORARY
|
||||||
generateAssetCache().whenComplete((aVoid, ex) -> downloadAndLoadLocale(GeyserLocale.getDefaultLocale()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch the latest versions asset cache from Mojang so we can grab the locale files later
|
|
||||||
*/
|
|
||||||
private static CompletableFuture<Void> generateAssetCache() {
|
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
|
||||||
try {
|
try {
|
||||||
// Get the version manifest from Mojang
|
Files.delete(localesFolder.toPath().resolve("en_us.hash"));
|
||||||
VersionManifest versionManifest = GeyserImpl.JSON_MAPPER.readValue(WebUtils.getBody("https://launchermeta.mojang.com/mc/game/version_manifest.json"), VersionManifest.class);
|
} catch (IOException ignored) {
|
||||||
|
|
||||||
// Get the url for the latest version of the games manifest
|
|
||||||
String latestInfoURL = "";
|
|
||||||
for (Version version : versionManifest.getVersions()) {
|
|
||||||
if (version.getId().equals(GameProtocol.getJavaCodec().getMinecraftVersion())) {
|
|
||||||
latestInfoURL = version.getUrl();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure we definitely got a version
|
public static void ensureEN_US() {
|
||||||
if (latestInfoURL.isEmpty()) {
|
File localeFile = getFile("en_us");
|
||||||
throw new Exception(GeyserLocale.getLocaleStringLog("geyser.locale.fail.latest_version"));
|
AssetUtils.addTask(!localeFile.exists(), new AssetUtils.ClientJarTask("assets/minecraft/lang/en_us.json",
|
||||||
|
(stream) -> AssetUtils.saveFile(localeFile, stream),
|
||||||
|
() -> {
|
||||||
|
if ("en_us".equals(GeyserLocale.getDefaultLocale())) {
|
||||||
|
loadLocale("en_us");
|
||||||
}
|
}
|
||||||
|
}));
|
||||||
// Get the individual version manifest
|
|
||||||
VersionInfo versionInfo = GeyserImpl.JSON_MAPPER.readValue(WebUtils.getBody(latestInfoURL), VersionInfo.class);
|
|
||||||
|
|
||||||
// Get the client jar for use when downloading the en_us locale
|
|
||||||
GeyserImpl.getInstance().getLogger().debug(GeyserImpl.JSON_MAPPER.writeValueAsString(versionInfo.getDownloads()));
|
|
||||||
clientJarInfo = versionInfo.getDownloads().get("client");
|
|
||||||
GeyserImpl.getInstance().getLogger().debug(GeyserImpl.JSON_MAPPER.writeValueAsString(clientJarInfo));
|
|
||||||
|
|
||||||
// Get the assets list
|
|
||||||
JsonNode assets = GeyserImpl.JSON_MAPPER.readTree(WebUtils.getBody(versionInfo.getAssetIndex().getUrl())).get("objects");
|
|
||||||
|
|
||||||
// Put each asset into an array for use later
|
|
||||||
Iterator<Map.Entry<String, JsonNode>> assetIterator = assets.fields();
|
|
||||||
while (assetIterator.hasNext()) {
|
|
||||||
Map.Entry<String, JsonNode> entry = assetIterator.next();
|
|
||||||
if (!entry.getKey().startsWith("minecraft/lang/")) {
|
|
||||||
// No need to cache non-language assets as we don't use them
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Asset asset = GeyserImpl.JSON_MAPPER.treeToValue(entry.getValue(), Asset.class);
|
|
||||||
ASSET_MAP.put(entry.getKey(), asset);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.locale.fail.asset_cache", (!e.getMessage().isEmpty() ? e.getMessage() : e.getStackTrace())));
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -125,7 +79,7 @@ public class MinecraftLocale {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check the locale isn't already loaded
|
// Check the locale isn't already loaded
|
||||||
if (!ASSET_MAP.containsKey("minecraft/lang/" + locale + ".json") && !locale.equals("en_us")) {
|
if (!AssetUtils.isAssetKnown("minecraft/lang/" + locale + ".json") && !locale.equals("en_us")) {
|
||||||
if (loadLocale(locale)) {
|
if (loadLocale(locale)) {
|
||||||
GeyserImpl.getInstance().getLogger().debug("Loaded locale locally while not being in asset map: " + locale);
|
GeyserImpl.getInstance().getLogger().debug("Loaded locale locally while not being in asset map: " + locale);
|
||||||
} else {
|
} else {
|
||||||
@ -148,33 +102,15 @@ public class MinecraftLocale {
|
|||||||
* @param locale Locale to download
|
* @param locale Locale to download
|
||||||
*/
|
*/
|
||||||
private static void downloadLocale(String locale) {
|
private static void downloadLocale(String locale) {
|
||||||
File localeFile = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales/" + locale + ".json").toFile();
|
if (locale.equals("en_us")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
File localeFile = getFile(locale);
|
||||||
|
|
||||||
// Check if we have already downloaded the locale file
|
// Check if we have already downloaded the locale file
|
||||||
if (localeFile.exists()) {
|
if (localeFile.exists()) {
|
||||||
String curHash = "";
|
String curHash = byteArrayToHexString(FileUtils.calculateSHA1(localeFile));
|
||||||
String targetHash;
|
String targetHash = AssetUtils.getAsset("minecraft/lang/" + locale + ".json").getHash();
|
||||||
|
|
||||||
if (locale.equals("en_us")) {
|
|
||||||
try {
|
|
||||||
File hashFile = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales/en_us.hash").toFile();
|
|
||||||
if (hashFile.exists()) {
|
|
||||||
try (BufferedReader br = new BufferedReader(new FileReader(hashFile))) {
|
|
||||||
curHash = br.readLine().trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException ignored) { }
|
|
||||||
|
|
||||||
if (clientJarInfo == null) {
|
|
||||||
// Likely failed to download
|
|
||||||
GeyserImpl.getInstance().getLogger().debug("Skipping en_US hash check as client jar is null.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
targetHash = clientJarInfo.getSha1();
|
|
||||||
} else {
|
|
||||||
curHash = byteArrayToHexString(FileUtils.calculateSHA1(localeFile));
|
|
||||||
targetHash = ASSET_MAP.get("minecraft/lang/" + locale + ".json").getHash();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!curHash.equals(targetHash)) {
|
if (!curHash.equals(targetHash)) {
|
||||||
GeyserImpl.getInstance().getLogger().debug("Locale out of date; re-downloading: " + locale);
|
GeyserImpl.getInstance().getLogger().debug("Locale out of date; re-downloading: " + locale);
|
||||||
@ -184,22 +120,19 @@ public class MinecraftLocale {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the en_us locale
|
|
||||||
if (locale.equals("en_us")) {
|
|
||||||
downloadEN_US(localeFile);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the hash and download the locale
|
// Get the hash and download the locale
|
||||||
String hash = ASSET_MAP.get("minecraft/lang/" + locale + ".json").getHash();
|
String hash = AssetUtils.getAsset("minecraft/lang/" + locale + ".json").getHash();
|
||||||
WebUtils.downloadFile("https://resources.download.minecraft.net/" + hash.substring(0, 2) + "/" + hash, localeFile.toString());
|
WebUtils.downloadFile("https://resources.download.minecraft.net/" + hash.substring(0, 2) + "/" + hash, localeFile.toString());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
GeyserImpl.getInstance().getLogger().error("Unable to download locale file hash", e);
|
GeyserImpl.getInstance().getLogger().error("Unable to download locale file hash", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static File getFile(String locale) {
|
||||||
|
return GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales/" + locale + ".json").toFile();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads a locale already downloaded, if the file doesn't exist it just logs a warning
|
* Loads a locale already downloaded, if the file doesn't exist it just logs a warning
|
||||||
*
|
*
|
||||||
@ -254,51 +187,6 @@ public class MinecraftLocale {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Download then en_us locale by downloading the server jar and extracting it from there.
|
|
||||||
*
|
|
||||||
* @param localeFile File to save the locale to
|
|
||||||
*/
|
|
||||||
private static void downloadEN_US(File localeFile) {
|
|
||||||
try {
|
|
||||||
// Let the user know we are downloading the JAR
|
|
||||||
GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.locale.download.en_us"));
|
|
||||||
GeyserImpl.getInstance().getLogger().debug("Download URL: " + clientJarInfo.getUrl());
|
|
||||||
|
|
||||||
// Download the smallest JAR (client or server)
|
|
||||||
Path tmpFilePath = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("tmp_locale.jar");
|
|
||||||
WebUtils.downloadFile(clientJarInfo.getUrl(), tmpFilePath.toString());
|
|
||||||
|
|
||||||
// Load in the JAR as a zip and extract the file
|
|
||||||
try (ZipFile localeJar = new ZipFile(tmpFilePath.toString())) {
|
|
||||||
try (InputStream fileStream = localeJar.getInputStream(localeJar.getEntry("assets/minecraft/lang/en_us.json"))) {
|
|
||||||
try (FileOutputStream outStream = new FileOutputStream(localeFile)) {
|
|
||||||
|
|
||||||
// Write the file to the locale dir
|
|
||||||
byte[] buf = new byte[fileStream.available()];
|
|
||||||
int length;
|
|
||||||
while ((length = fileStream.read(buf)) != -1) {
|
|
||||||
outStream.write(buf, 0, length);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flush all changes to disk and cleanup
|
|
||||||
outStream.flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the latest jar hash
|
|
||||||
FileUtils.writeFile(GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("locales/en_us.hash").toString(), clientJarInfo.getSha1().toCharArray());
|
|
||||||
|
|
||||||
// Delete the nolonger needed client/server jar
|
|
||||||
Files.delete(tmpFilePath);
|
|
||||||
|
|
||||||
GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.locale.download.en_us.done"));
|
|
||||||
} catch (Exception e) {
|
|
||||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.locale.fail.en_us"), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Translate the given language string into the given locale, or falls back to the default locale
|
* Translate the given language string into the given locale, or falls back to the default locale
|
||||||
*
|
*
|
||||||
@ -333,111 +221,4 @@ public class MinecraftLocale {
|
|||||||
}
|
}
|
||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void init() {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
||||||
@Getter
|
|
||||||
static class VersionManifest {
|
|
||||||
@JsonProperty("latest")
|
|
||||||
private LatestVersion latestVersion;
|
|
||||||
|
|
||||||
@JsonProperty("versions")
|
|
||||||
private List<Version> versions;
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
||||||
@Getter
|
|
||||||
static class LatestVersion {
|
|
||||||
@JsonProperty("release")
|
|
||||||
private String release;
|
|
||||||
|
|
||||||
@JsonProperty("snapshot")
|
|
||||||
private String snapshot;
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
||||||
@Getter
|
|
||||||
static class Version {
|
|
||||||
@JsonProperty("id")
|
|
||||||
private String id;
|
|
||||||
|
|
||||||
@JsonProperty("type")
|
|
||||||
private String type;
|
|
||||||
|
|
||||||
@JsonProperty("url")
|
|
||||||
private String url;
|
|
||||||
|
|
||||||
@JsonProperty("time")
|
|
||||||
private String time;
|
|
||||||
|
|
||||||
@JsonProperty("releaseTime")
|
|
||||||
private String releaseTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
||||||
@Getter
|
|
||||||
static class VersionInfo {
|
|
||||||
@JsonProperty("id")
|
|
||||||
private String id;
|
|
||||||
|
|
||||||
@JsonProperty("type")
|
|
||||||
private String type;
|
|
||||||
|
|
||||||
@JsonProperty("time")
|
|
||||||
private String time;
|
|
||||||
|
|
||||||
@JsonProperty("releaseTime")
|
|
||||||
private String releaseTime;
|
|
||||||
|
|
||||||
@JsonProperty("assetIndex")
|
|
||||||
private AssetIndex assetIndex;
|
|
||||||
|
|
||||||
@JsonProperty("downloads")
|
|
||||||
private Map<String, VersionDownload> downloads;
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
||||||
@Getter
|
|
||||||
static class VersionDownload {
|
|
||||||
@JsonProperty("sha1")
|
|
||||||
private String sha1;
|
|
||||||
|
|
||||||
@JsonProperty("size")
|
|
||||||
private int size;
|
|
||||||
|
|
||||||
@JsonProperty("url")
|
|
||||||
private String url;
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
||||||
@Getter
|
|
||||||
static class AssetIndex {
|
|
||||||
@JsonProperty("id")
|
|
||||||
private String id;
|
|
||||||
|
|
||||||
@JsonProperty("sha1")
|
|
||||||
private String sha1;
|
|
||||||
|
|
||||||
@JsonProperty("size")
|
|
||||||
private int size;
|
|
||||||
|
|
||||||
@JsonProperty("totalSize")
|
|
||||||
private int totalSize;
|
|
||||||
|
|
||||||
@JsonProperty("url")
|
|
||||||
private String url;
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
||||||
@Getter
|
|
||||||
static class Asset {
|
|
||||||
@JsonProperty("hash")
|
|
||||||
private String hash;
|
|
||||||
|
|
||||||
@JsonProperty("size")
|
|
||||||
private int size;
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -59,17 +59,17 @@ public class BannerTranslator extends NbtItemStackTranslator {
|
|||||||
static {
|
static {
|
||||||
OMINOUS_BANNER_PATTERN = new ListTag("Patterns");
|
OMINOUS_BANNER_PATTERN = new ListTag("Patterns");
|
||||||
// Construct what an ominous banner is supposed to look like
|
// Construct what an ominous banner is supposed to look like
|
||||||
OMINOUS_BANNER_PATTERN.add(getPatternTag("mr", 9));
|
OMINOUS_BANNER_PATTERN.add(getJavaPatternTag("mr", 9));
|
||||||
OMINOUS_BANNER_PATTERN.add(getPatternTag("bs", 8));
|
OMINOUS_BANNER_PATTERN.add(getJavaPatternTag("bs", 8));
|
||||||
OMINOUS_BANNER_PATTERN.add(getPatternTag("cs", 7));
|
OMINOUS_BANNER_PATTERN.add(getJavaPatternTag("cs", 7));
|
||||||
OMINOUS_BANNER_PATTERN.add(getPatternTag("bo", 8));
|
OMINOUS_BANNER_PATTERN.add(getJavaPatternTag("bo", 8));
|
||||||
OMINOUS_BANNER_PATTERN.add(getPatternTag("ms", 15));
|
OMINOUS_BANNER_PATTERN.add(getJavaPatternTag("ms", 15));
|
||||||
OMINOUS_BANNER_PATTERN.add(getPatternTag("hh", 8));
|
OMINOUS_BANNER_PATTERN.add(getJavaPatternTag("hh", 8));
|
||||||
OMINOUS_BANNER_PATTERN.add(getPatternTag("mc", 8));
|
OMINOUS_BANNER_PATTERN.add(getJavaPatternTag("mc", 8));
|
||||||
OMINOUS_BANNER_PATTERN.add(getPatternTag("bo", 15));
|
OMINOUS_BANNER_PATTERN.add(getJavaPatternTag("bo", 15));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CompoundTag getPatternTag(String pattern, int color) {
|
public static CompoundTag getJavaPatternTag(String pattern, int color) {
|
||||||
StringTag patternType = new StringTag("Pattern", pattern);
|
StringTag patternType = new StringTag("Pattern", pattern);
|
||||||
IntTag colorTag = new IntTag("Color", color);
|
IntTag colorTag = new IntTag("Color", color);
|
||||||
CompoundTag tag = new CompoundTag("");
|
CompoundTag tag = new CompoundTag("");
|
||||||
@ -120,11 +120,7 @@ public class BannerTranslator extends NbtItemStackTranslator {
|
|||||||
* @return The Java edition format pattern nbt
|
* @return The Java edition format pattern nbt
|
||||||
*/
|
*/
|
||||||
public static CompoundTag getJavaBannerPattern(NbtMap pattern) {
|
public static CompoundTag getJavaBannerPattern(NbtMap pattern) {
|
||||||
Map<String, Tag> tags = new HashMap<>();
|
return BannerTranslator.getJavaPatternTag(pattern.getString("Pattern"), 15 - pattern.getInt("Color"));
|
||||||
tags.put("Color", new IntTag("Color", 15 - pattern.getInt("Color")));
|
|
||||||
tags.put("Pattern", new StringTag("Pattern", pattern.getString("Pattern")));
|
|
||||||
|
|
||||||
return new CompoundTag("", tags);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -53,6 +53,11 @@ public class ShulkerBoxItemTranslator extends NbtItemStackTranslator {
|
|||||||
|
|
||||||
ItemMapping boxMapping = session.getItemMappings().getMapping(Identifier.formalize(((StringTag) itemData.get("id")).getValue()));
|
ItemMapping boxMapping = session.getItemMappings().getMapping(Identifier.formalize(((StringTag) itemData.get("id")).getValue()));
|
||||||
|
|
||||||
|
if (boxMapping == null) {
|
||||||
|
// If invalid ID
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
boxItemTag.put(new StringTag("Name", boxMapping.getBedrockIdentifier()));
|
boxItemTag.put(new StringTag("Name", boxMapping.getBedrockIdentifier()));
|
||||||
boxItemTag.put(new ShortTag("Damage", (short) boxMapping.getBedrockData()));
|
boxItemTag.put(new ShortTag("Damage", (short) boxMapping.getBedrockData()));
|
||||||
boxItemTag.put(new ByteTag("Count", MathUtils.getNbtByte(itemData.get("Count").getValue())));
|
boxItemTag.put(new ByteTag("Count", MathUtils.getNbtByte(itemData.get("Count").getValue())));
|
||||||
|
@ -42,6 +42,7 @@ import org.geysermc.geyser.level.physics.Axis;
|
|||||||
import org.geysermc.geyser.level.physics.BoundingBox;
|
import org.geysermc.geyser.level.physics.BoundingBox;
|
||||||
import org.geysermc.geyser.level.physics.CollisionManager;
|
import org.geysermc.geyser.level.physics.CollisionManager;
|
||||||
import org.geysermc.geyser.level.physics.Direction;
|
import org.geysermc.geyser.level.physics.Direction;
|
||||||
|
import org.geysermc.geyser.network.GameProtocol;
|
||||||
import org.geysermc.geyser.registry.Registries;
|
import org.geysermc.geyser.registry.Registries;
|
||||||
import org.geysermc.geyser.session.GeyserSession;
|
import org.geysermc.geyser.session.GeyserSession;
|
||||||
import org.geysermc.geyser.session.cache.PistonCache;
|
import org.geysermc.geyser.session.cache.PistonCache;
|
||||||
@ -621,8 +622,10 @@ public class PistonBlockEntity {
|
|||||||
Vector3i movement = getMovement();
|
Vector3i movement = getMovement();
|
||||||
attachedBlocks.forEach((blockPos, javaId) -> {
|
attachedBlocks.forEach((blockPos, javaId) -> {
|
||||||
blockPos = blockPos.add(movement);
|
blockPos = blockPos.add(movement);
|
||||||
// Send a final block entity packet to detach blocks
|
if (!GameProtocol.supports1_19_50(session)) {
|
||||||
|
// Send a final block entity packet to detach blocks for clients older than 1.19.50
|
||||||
BlockEntityUtils.updateBlockEntity(session, buildMovingBlockTag(blockPos, javaId, Direction.DOWN.getUnitVector()), blockPos);
|
BlockEntityUtils.updateBlockEntity(session, buildMovingBlockTag(blockPos, javaId, Direction.DOWN.getUnitVector()), blockPos);
|
||||||
|
}
|
||||||
// Don't place blocks that collide with the player
|
// Don't place blocks that collide with the player
|
||||||
if (!SOLID_BOUNDING_BOX.checkIntersection(blockPos.toDouble(), session.getCollisionManager().getPlayerBoundingBox())) {
|
if (!SOLID_BOUNDING_BOX.checkIntersection(blockPos.toDouble(), session.getCollisionManager().getPlayerBoundingBox())) {
|
||||||
ChunkUtils.updateBlock(session, javaId, blockPos);
|
ChunkUtils.updateBlock(session, javaId, blockPos);
|
||||||
@ -739,8 +742,8 @@ public class PistonBlockEntity {
|
|||||||
.putFloat("LastProgress", lastProgress)
|
.putFloat("LastProgress", lastProgress)
|
||||||
.putByte("NewState", getState())
|
.putByte("NewState", getState())
|
||||||
.putByte("State", getState())
|
.putByte("State", getState())
|
||||||
.putByte("Sticky", (byte) (sticky ? 1 : 0))
|
.putBoolean("Sticky", sticky)
|
||||||
.putByte("isMovable", (byte) 0)
|
.putBoolean("isMovable", false)
|
||||||
.putInt("x", position.getX())
|
.putInt("x", position.getX())
|
||||||
.putInt("y", position.getY())
|
.putInt("y", position.getY())
|
||||||
.putInt("z", position.getZ());
|
.putInt("z", position.getZ());
|
||||||
@ -762,8 +765,8 @@ public class PistonBlockEntity {
|
|||||||
.putFloat("LastProgress", extended ? 1.0f : 0.0f)
|
.putFloat("LastProgress", extended ? 1.0f : 0.0f)
|
||||||
.putByte("NewState", (byte) (extended ? 2 : 0))
|
.putByte("NewState", (byte) (extended ? 2 : 0))
|
||||||
.putByte("State", (byte) (extended ? 2 : 0))
|
.putByte("State", (byte) (extended ? 2 : 0))
|
||||||
.putByte("Sticky", (byte) (sticky ? 1 : 0))
|
.putBoolean("Sticky", sticky)
|
||||||
.putByte("isMovable", (byte) 0)
|
.putBoolean("isMovable", false)
|
||||||
.putInt("x", position.getX())
|
.putInt("x", position.getX())
|
||||||
.putInt("y", position.getY())
|
.putInt("y", position.getY())
|
||||||
.putInt("z", position.getZ());
|
.putInt("z", position.getZ());
|
||||||
@ -783,8 +786,9 @@ public class PistonBlockEntity {
|
|||||||
NbtMap movingBlock = session.getBlockMappings().getBedrockBlock(javaId).getState();
|
NbtMap movingBlock = session.getBlockMappings().getBedrockBlock(javaId).getState();
|
||||||
NbtMapBuilder builder = NbtMap.builder()
|
NbtMapBuilder builder = NbtMap.builder()
|
||||||
.putString("id", "MovingBlock")
|
.putString("id", "MovingBlock")
|
||||||
|
.putBoolean("expanding", action == PistonValueType.PUSHING)
|
||||||
.putCompound("movingBlock", movingBlock)
|
.putCompound("movingBlock", movingBlock)
|
||||||
.putByte("isMovable", (byte) 1)
|
.putBoolean("isMovable", true)
|
||||||
.putInt("pistonPosX", pistonPosition.getX())
|
.putInt("pistonPosX", pistonPosition.getX())
|
||||||
.putInt("pistonPosY", pistonPosition.getY())
|
.putInt("pistonPosY", pistonPosition.getY())
|
||||||
.putInt("pistonPosZ", pistonPosition.getZ())
|
.putInt("pistonPosZ", pistonPosition.getZ())
|
||||||
|
@ -27,6 +27,7 @@ package org.geysermc.geyser.translator.level.block.entity;
|
|||||||
|
|
||||||
import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityType;
|
import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityType;
|
||||||
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
||||||
|
import com.github.steveice10.opennbt.tag.builtin.IntArrayTag;
|
||||||
import com.github.steveice10.opennbt.tag.builtin.ListTag;
|
import com.github.steveice10.opennbt.tag.builtin.ListTag;
|
||||||
import com.github.steveice10.opennbt.tag.builtin.StringTag;
|
import com.github.steveice10.opennbt.tag.builtin.StringTag;
|
||||||
import org.cloudburstmc.math.vector.Vector3i;
|
import org.cloudburstmc.math.vector.Vector3i;
|
||||||
@ -35,7 +36,10 @@ import org.geysermc.geyser.level.block.BlockStateValues;
|
|||||||
import org.geysermc.geyser.session.GeyserSession;
|
import org.geysermc.geyser.session.GeyserSession;
|
||||||
import org.geysermc.geyser.skin.SkinProvider;
|
import org.geysermc.geyser.skin.SkinProvider;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
@BlockEntity(type = BlockEntityType.SKULL)
|
@BlockEntity(type = BlockEntityType.SKULL)
|
||||||
@ -53,12 +57,32 @@ public class SkullBlockEntityTranslator extends BlockEntityTranslator implements
|
|||||||
builder.put("SkullType", skullVariant);
|
builder.put("SkullType", skullVariant);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CompletableFuture<String> getTextures(CompoundTag tag) {
|
private static UUID getUUID(CompoundTag owner) {
|
||||||
CompoundTag owner = tag.get("SkullOwner");
|
if (owner.get("Id") instanceof IntArrayTag uuidTag && uuidTag.length() == 4) {
|
||||||
if (owner != null) {
|
int[] uuidAsArray = uuidTag.getValue();
|
||||||
|
// thank u viaversion
|
||||||
|
return new UUID((long) uuidAsArray[0] << 32 | ((long) uuidAsArray[1] & 0xFFFFFFFFL),
|
||||||
|
(long) uuidAsArray[2] << 32 | ((long) uuidAsArray[3] & 0xFFFFFFFFL));
|
||||||
|
}
|
||||||
|
// Convert username to an offline UUID
|
||||||
|
String username = null;
|
||||||
|
if (owner.get("Name") instanceof StringTag nameTag) {
|
||||||
|
username = nameTag.getValue().toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
return UUID.nameUUIDFromBytes(("OfflinePlayer:" + username).getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CompletableFuture<String> getTextures(CompoundTag owner, UUID uuid) {
|
||||||
CompoundTag properties = owner.get("Properties");
|
CompoundTag properties = owner.get("Properties");
|
||||||
if (properties == null) {
|
if (properties == null) {
|
||||||
return SkinProvider.requestTexturesFromUsername(owner);
|
if (uuid != null && uuid.version() == 4) {
|
||||||
|
String uuidString = uuid.toString().replace("-", "");
|
||||||
|
return SkinProvider.requestTexturesFromUUID(uuidString);
|
||||||
|
} else if (owner.get("Name") instanceof StringTag nameTag) {
|
||||||
|
// Fall back to username if UUID was missing or was an offline mode UUID
|
||||||
|
return SkinProvider.requestTexturesFromUsername(nameTag.getValue());
|
||||||
|
}
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
ListTag textures = properties.get("textures");
|
ListTag textures = properties.get("textures");
|
||||||
@ -66,20 +90,21 @@ public class SkullBlockEntityTranslator extends BlockEntityTranslator implements
|
|||||||
StringTag texture = (StringTag) tag1.get("Value");
|
StringTag texture = (StringTag) tag1.get("Value");
|
||||||
return CompletableFuture.completedFuture(texture.getValue());
|
return CompletableFuture.completedFuture(texture.getValue());
|
||||||
}
|
}
|
||||||
return CompletableFuture.completedFuture(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void translateSkull(GeyserSession session, CompoundTag tag, int posX, int posY, int posZ, int blockState) {
|
public static void translateSkull(GeyserSession session, CompoundTag tag, int posX, int posY, int posZ, int blockState) {
|
||||||
Vector3i blockPosition = Vector3i.from(posX, posY, posZ);
|
Vector3i blockPosition = Vector3i.from(posX, posY, posZ);
|
||||||
getTextures(tag).whenComplete((texturesProperty, throwable) -> {
|
CompoundTag owner = tag.get("SkullOwner");
|
||||||
if (texturesProperty == null) {
|
if (owner == null) {
|
||||||
session.getGeyser().getLogger().debug("Custom skull with invalid SkullOwner tag: " + blockPosition + " " + tag);
|
session.getSkullCache().removeSkull(blockPosition);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UUID uuid = getUUID(owner);
|
||||||
|
getTextures(owner, uuid).whenComplete((texturesProperty, throwable) -> {
|
||||||
if (session.getEventLoop().inEventLoop()) {
|
if (session.getEventLoop().inEventLoop()) {
|
||||||
session.getSkullCache().putSkull(blockPosition, texturesProperty, blockState);
|
session.getSkullCache().putSkull(blockPosition, uuid, texturesProperty, blockState);
|
||||||
} else {
|
} else {
|
||||||
session.executeInEventLoop(() -> session.getSkullCache().putSkull(blockPosition, texturesProperty, blockState));
|
session.executeInEventLoop(() -> session.getSkullCache().putSkull(blockPosition, uuid, texturesProperty, blockState));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -46,15 +46,32 @@ public class BedrockAnimateTranslator extends PacketTranslator<AnimatePacket> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (packet.getAction()) {
|
switch (packet.getAction()) {
|
||||||
case SWING_ARM ->
|
case SWING_ARM -> {
|
||||||
|
session.armSwingPending();
|
||||||
// Delay so entity damage can be processed first
|
// Delay so entity damage can be processed first
|
||||||
session.scheduleInEventLoop(() -> {
|
session.scheduleInEventLoop(() -> {
|
||||||
|
if (session.getArmAnimationTicks() != 0) {
|
||||||
|
// So, generally, a Java player can only do one *thing* at a time.
|
||||||
|
// If a player right-clicks, for example, then there's probably only one action associated with
|
||||||
|
// that right-click that will send a swing.
|
||||||
|
// The only exception I can think of to this, *maybe*, is a player dropping items
|
||||||
|
// Bedrock is a little funkier than this - it can send several arm animation packets in the
|
||||||
|
// same tick, notably with high levels of haste applied.
|
||||||
|
// Packet limiters do not like this and can crash the player.
|
||||||
|
// If arm animation ticks is 0, then we just sent an arm swing packet this tick.
|
||||||
|
// See https://github.com/GeyserMC/Geyser/issues/2875
|
||||||
|
// This behavior was last touched on with ViaVersion 4.5.1 (with its packet limiter), Java 1.16.5,
|
||||||
|
// and Bedrock 1.19.51.
|
||||||
|
// Note for the future: we should probably largely ignore this packet and instead replicate
|
||||||
|
// all actions on our end, and send swings where needed.
|
||||||
session.sendDownstreamPacket(new ServerboundSwingPacket(Hand.MAIN_HAND));
|
session.sendDownstreamPacket(new ServerboundSwingPacket(Hand.MAIN_HAND));
|
||||||
session.activateArmAnimationTicking();
|
session.activateArmAnimationTicking();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
25,
|
25,
|
||||||
TimeUnit.MILLISECONDS
|
TimeUnit.MILLISECONDS
|
||||||
);
|
);
|
||||||
|
}
|
||||||
// These two might need to be flipped, but my recommendation is getting moving working first
|
// These two might need to be flipped, but my recommendation is getting moving working first
|
||||||
case ROW_LEFT -> {
|
case ROW_LEFT -> {
|
||||||
// Packet value is a float of how long one has been rowing, so we convert that into a boolean
|
// Packet value is a float of how long one has been rowing, so we convert that into a boolean
|
||||||
|
@ -25,12 +25,18 @@
|
|||||||
|
|
||||||
package org.geysermc.geyser.translator.protocol.bedrock;
|
package org.geysermc.geyser.translator.protocol.bedrock;
|
||||||
|
|
||||||
|
import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
|
||||||
|
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
||||||
|
import com.github.steveice10.opennbt.tag.builtin.ListTag;
|
||||||
|
import com.github.steveice10.opennbt.tag.builtin.StringTag;
|
||||||
import org.cloudburstmc.math.vector.Vector3i;
|
import org.cloudburstmc.math.vector.Vector3i;
|
||||||
import org.cloudburstmc.protocol.bedrock.packet.BlockPickRequestPacket;
|
import org.cloudburstmc.protocol.bedrock.packet.BlockPickRequestPacket;
|
||||||
import org.geysermc.geyser.entity.EntityDefinitions;
|
import org.geysermc.geyser.entity.EntityDefinitions;
|
||||||
import org.geysermc.geyser.entity.type.ItemFrameEntity;
|
import org.geysermc.geyser.entity.type.ItemFrameEntity;
|
||||||
import org.geysermc.geyser.level.block.BlockStateValues;
|
import org.geysermc.geyser.level.block.BlockStateValues;
|
||||||
import org.geysermc.geyser.registry.BlockRegistries;
|
import org.geysermc.geyser.registry.BlockRegistries;
|
||||||
|
import org.geysermc.geyser.registry.type.BlockMapping;
|
||||||
|
import org.geysermc.geyser.registry.type.ItemMapping;
|
||||||
import org.geysermc.geyser.session.GeyserSession;
|
import org.geysermc.geyser.session.GeyserSession;
|
||||||
import org.geysermc.geyser.translator.protocol.PacketTranslator;
|
import org.geysermc.geyser.translator.protocol.PacketTranslator;
|
||||||
import org.geysermc.geyser.translator.protocol.Translator;
|
import org.geysermc.geyser.translator.protocol.Translator;
|
||||||
@ -61,6 +67,41 @@ public class BedrockBlockPickRequestTranslator extends PacketTranslator<BlockPic
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
InventoryUtils.findOrCreateItem(session, BlockRegistries.JAVA_BLOCKS.get(blockToPick).getPickItem());
|
BlockMapping blockMapping = BlockRegistries.JAVA_BLOCKS.getOrDefault(blockToPick, BlockMapping.AIR);
|
||||||
|
boolean addNbtData = packet.isAddUserData() && blockMapping.isBlockEntity(); // Holding down CTRL
|
||||||
|
if (BlockStateValues.getBannerColor(blockToPick) != -1 || addNbtData) {
|
||||||
|
session.getGeyser().getWorldManager().getPickItemNbt(session, vector.getX(), vector.getY(), vector.getZ(), addNbtData)
|
||||||
|
.whenComplete((tag, ex) -> {
|
||||||
|
if (tag == null) {
|
||||||
|
pickItem(session, blockMapping);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
session.ensureInEventLoop(() -> {
|
||||||
|
if (addNbtData) {
|
||||||
|
ListTag lore = new ListTag("Lore");
|
||||||
|
lore.add(new StringTag("", "\"(+NBT)\""));
|
||||||
|
CompoundTag display = tag.get("display");
|
||||||
|
if (display == null) {
|
||||||
|
display = new CompoundTag("display");
|
||||||
|
tag.put(display);
|
||||||
|
}
|
||||||
|
display.put(lore);
|
||||||
|
}
|
||||||
|
// I don't really like this... I'd rather get an ID from the block mapping I think
|
||||||
|
ItemMapping mapping = session.getItemMappings().getMapping(blockMapping.getPickItem());
|
||||||
|
|
||||||
|
ItemStack itemStack = new ItemStack(mapping.getJavaItem().javaId(), 1, tag);
|
||||||
|
InventoryUtils.findOrCreateItem(session, itemStack);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pickItem(session, blockMapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pickItem(GeyserSession session, BlockMapping blockToPick) {
|
||||||
|
InventoryUtils.findOrCreateItem(session, blockToPick.getPickItem());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,6 @@
|
|||||||
|
|
||||||
package org.geysermc.geyser.translator.protocol.bedrock;
|
package org.geysermc.geyser.translator.protocol.bedrock;
|
||||||
|
|
||||||
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
|
|
||||||
import org.cloudburstmc.protocol.bedrock.packet.EntityPickRequestPacket;
|
import org.cloudburstmc.protocol.bedrock.packet.EntityPickRequestPacket;
|
||||||
import org.geysermc.geyser.entity.type.BoatEntity;
|
import org.geysermc.geyser.entity.type.BoatEntity;
|
||||||
import org.geysermc.geyser.entity.type.Entity;
|
import org.geysermc.geyser.entity.type.Entity;
|
||||||
@ -45,7 +44,10 @@ public class BedrockEntityPickRequestTranslator extends PacketTranslator<EntityP
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void translate(GeyserSession session, EntityPickRequestPacket packet) {
|
public void translate(GeyserSession session, EntityPickRequestPacket packet) {
|
||||||
if (session.getGameMode() != GameMode.CREATIVE) return; // Apparently Java behavior
|
if (!session.isInstabuild()) {
|
||||||
|
// As of Java Edition 1.19.3
|
||||||
|
return;
|
||||||
|
}
|
||||||
Entity entity = session.getEntityCache().getEntityByGeyserId(packet.getRuntimeEntityId());
|
Entity entity = session.getEntityCache().getEntityByGeyserId(packet.getRuntimeEntityId());
|
||||||
if (entity == null) return;
|
if (entity == null) return;
|
||||||
|
|
||||||
|
@ -26,8 +26,9 @@
|
|||||||
package org.geysermc.geyser.translator.protocol.bedrock;
|
package org.geysermc.geyser.translator.protocol.bedrock;
|
||||||
|
|
||||||
import org.cloudburstmc.protocol.bedrock.packet.TextPacket;
|
import org.cloudburstmc.protocol.bedrock.packet.TextPacket;
|
||||||
|
import org.geysermc.geyser.api.util.TriState;
|
||||||
import org.geysermc.geyser.session.GeyserSession;
|
import org.geysermc.geyser.session.GeyserSession;
|
||||||
import org.geysermc.geyser.text.ChatColor;
|
import org.geysermc.geyser.text.GeyserLocale;
|
||||||
import org.geysermc.geyser.translator.protocol.PacketTranslator;
|
import org.geysermc.geyser.translator.protocol.PacketTranslator;
|
||||||
import org.geysermc.geyser.translator.protocol.Translator;
|
import org.geysermc.geyser.translator.protocol.Translator;
|
||||||
import org.geysermc.geyser.translator.text.MessageTranslator;
|
import org.geysermc.geyser.translator.text.MessageTranslator;
|
||||||
@ -48,6 +49,15 @@ public class BedrockTextTranslator extends PacketTranslator<TextPacket> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (session.getWorldCache().getChatWarningSent() == TriState.FALSE) {
|
||||||
|
if (Boolean.parseBoolean(System.getProperty("Geyser.PrintSecureChatInformation", "true"))) {
|
||||||
|
session.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.chat.secure_info_1", session.locale()));
|
||||||
|
session.sendMessage(GeyserLocale.getPlayerLocaleString("geyser.chat.secure_info_2", session.locale(), "https://geysermc.link/secure-chat"));
|
||||||
|
}
|
||||||
|
// Never send this message again for this session.
|
||||||
|
session.getWorldCache().setChatWarningSent(TriState.TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
session.sendChat(message);
|
session.sendChat(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,9 +32,11 @@ import com.github.steveice10.mc.protocol.data.game.entity.type.EntityType;
|
|||||||
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundInteractPacket;
|
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundInteractPacket;
|
||||||
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundPlayerCommandPacket;
|
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundPlayerCommandPacket;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
|
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
|
||||||
|
import org.cloudburstmc.protocol.bedrock.data.entity.EntityLinkData;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType;
|
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType;
|
||||||
import org.cloudburstmc.protocol.bedrock.packet.ContainerOpenPacket;
|
import org.cloudburstmc.protocol.bedrock.packet.ContainerOpenPacket;
|
||||||
import org.cloudburstmc.protocol.bedrock.packet.InteractPacket;
|
import org.cloudburstmc.protocol.bedrock.packet.InteractPacket;
|
||||||
|
import org.cloudburstmc.protocol.bedrock.packet.SetEntityLinkPacket;
|
||||||
import org.geysermc.geyser.entity.type.Entity;
|
import org.geysermc.geyser.entity.type.Entity;
|
||||||
import org.geysermc.geyser.entity.type.living.animal.horse.AbstractHorseEntity;
|
import org.geysermc.geyser.entity.type.living.animal.horse.AbstractHorseEntity;
|
||||||
import org.geysermc.geyser.item.Items;
|
import org.geysermc.geyser.item.Items;
|
||||||
@ -42,6 +44,8 @@ import org.geysermc.geyser.session.GeyserSession;
|
|||||||
import org.geysermc.geyser.translator.protocol.PacketTranslator;
|
import org.geysermc.geyser.translator.protocol.PacketTranslator;
|
||||||
import org.geysermc.geyser.translator.protocol.Translator;
|
import org.geysermc.geyser.translator.protocol.Translator;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
@Translator(packet = InteractPacket.class)
|
@Translator(packet = InteractPacket.class)
|
||||||
public class BedrockInteractTranslator extends PacketTranslator<InteractPacket> {
|
public class BedrockInteractTranslator extends PacketTranslator<InteractPacket> {
|
||||||
|
|
||||||
@ -74,6 +78,23 @@ public class BedrockInteractTranslator extends PacketTranslator<InteractPacket>
|
|||||||
case LEAVE_VEHICLE:
|
case LEAVE_VEHICLE:
|
||||||
ServerboundPlayerCommandPacket sneakPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.START_SNEAKING);
|
ServerboundPlayerCommandPacket sneakPacket = new ServerboundPlayerCommandPacket(entity.getEntityId(), PlayerState.START_SNEAKING);
|
||||||
session.sendDownstreamPacket(sneakPacket);
|
session.sendDownstreamPacket(sneakPacket);
|
||||||
|
|
||||||
|
Entity currentVehicle = session.getPlayerEntity().getVehicle();
|
||||||
|
session.setMountVehicleScheduledFuture(session.scheduleInEventLoop(() -> {
|
||||||
|
if (session.getPlayerEntity().getVehicle() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long vehicleBedrockId = currentVehicle.getGeyserId();
|
||||||
|
if (session.getPlayerEntity().getVehicle().getGeyserId() == vehicleBedrockId) {
|
||||||
|
// The Bedrock client, as of 1.19.51, dismounts on its end. The server may not agree with this.
|
||||||
|
// If the server doesn't agree with our dismount (sends a packet saying we dismounted),
|
||||||
|
// then remount the player.
|
||||||
|
SetEntityLinkPacket linkPacket = new SetEntityLinkPacket();
|
||||||
|
linkPacket.setEntityLink(new EntityLinkData(vehicleBedrockId, session.getPlayerEntity().getGeyserId(), EntityLinkData.Type.PASSENGER, true, false));
|
||||||
|
session.sendUpstreamPacket(linkPacket);
|
||||||
|
}
|
||||||
|
}, 1, TimeUnit.SECONDS));
|
||||||
break;
|
break;
|
||||||
case MOUSEOVER:
|
case MOUSEOVER:
|
||||||
// Handle the buttons for mobile - "Mount", etc; and the suggestions for console - "ZL: Mount", etc
|
// Handle the buttons for mobile - "Mount", etc; and the suggestions for console - "ZL: Mount", etc
|
||||||
|
@ -92,11 +92,29 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
|
|||||||
if (isValidMove(session, entity.getPosition(), packet.getPosition())) {
|
if (isValidMove(session, entity.getPosition(), packet.getPosition())) {
|
||||||
Vector3d position = session.getCollisionManager().adjustBedrockPosition(packet.getPosition(), packet.isOnGround(), packet.getMode() == MovePlayerPacket.Mode.TELEPORT);
|
Vector3d position = session.getCollisionManager().adjustBedrockPosition(packet.getPosition(), packet.isOnGround(), packet.getMode() == MovePlayerPacket.Mode.TELEPORT);
|
||||||
if (position != null) { // A null return value cancels the packet
|
if (position != null) { // A null return value cancels the packet
|
||||||
|
boolean onGround = packet.isOnGround();
|
||||||
|
|
||||||
|
boolean teleportThroughVoidFloor;
|
||||||
|
// Compare positions here for void floor fix below before the player's position variable is set to the packet position
|
||||||
|
if (entity.getPosition().getY() >= packet.getPosition().getY()) {
|
||||||
|
int floorY = position.getFloorY();
|
||||||
|
// The void floor is offset about 40 blocks below the bottom of the world
|
||||||
|
BedrockDimension bedrockDimension = session.getChunkCache().getBedrockDimension();
|
||||||
|
int voidFloorLocation = bedrockDimension.minY() - 40;
|
||||||
|
teleportThroughVoidFloor = floorY <= (voidFloorLocation + 2) && floorY >= voidFloorLocation;
|
||||||
|
if (teleportThroughVoidFloor) {
|
||||||
|
// https://github.com/GeyserMC/Geyser/issues/3521 - no void floor in Java so we cannot be on the ground.
|
||||||
|
onGround = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
teleportThroughVoidFloor = false;
|
||||||
|
}
|
||||||
|
|
||||||
Packet movePacket;
|
Packet movePacket;
|
||||||
if (rotationChanged) {
|
if (rotationChanged) {
|
||||||
// Send rotation updates as well
|
// Send rotation updates as well
|
||||||
movePacket = new ServerboundMovePlayerPosRotPacket(
|
movePacket = new ServerboundMovePlayerPosRotPacket(
|
||||||
packet.isOnGround(),
|
onGround,
|
||||||
position.getX(), position.getY(), position.getZ(),
|
position.getX(), position.getY(), position.getZ(),
|
||||||
yaw, pitch
|
yaw, pitch
|
||||||
);
|
);
|
||||||
@ -105,24 +123,16 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
|
|||||||
entity.setHeadYaw(headYaw);
|
entity.setHeadYaw(headYaw);
|
||||||
} else {
|
} else {
|
||||||
// Rotation did not change; don't send an update with rotation
|
// Rotation did not change; don't send an update with rotation
|
||||||
movePacket = new ServerboundMovePlayerPosPacket(packet.isOnGround(), position.getX(), position.getY(), position.getZ());
|
movePacket = new ServerboundMovePlayerPosPacket(onGround, position.getX(), position.getY(), position.getZ());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compare positions here for void floor fix below before the player's position variable is set to the packet position
|
|
||||||
boolean notMovingUp = entity.getPosition().getY() >= packet.getPosition().getY();
|
|
||||||
|
|
||||||
entity.setPositionManual(packet.getPosition());
|
entity.setPositionManual(packet.getPosition());
|
||||||
entity.setOnGround(packet.isOnGround());
|
entity.setOnGround(onGround);
|
||||||
|
|
||||||
// Send final movement changes
|
// Send final movement changes
|
||||||
session.sendDownstreamPacket(movePacket);
|
session.sendDownstreamPacket(movePacket);
|
||||||
|
|
||||||
if (notMovingUp) {
|
if (teleportThroughVoidFloor) {
|
||||||
int floorY = position.getFloorY();
|
|
||||||
// The void floor is offset about 40 blocks below the bottom of the world
|
|
||||||
BedrockDimension bedrockDimension = session.getChunkCache().getBedrockDimension();
|
|
||||||
int voidFloorLocation = bedrockDimension.minY() - 40;
|
|
||||||
if (floorY <= (voidFloorLocation + 2) && floorY >= voidFloorLocation) {
|
|
||||||
// Work around there being a floor at the bottom of the world and teleport the player below it
|
// Work around there being a floor at the bottom of the world and teleport the player below it
|
||||||
// Moving from below to above the void floor works fine
|
// Moving from below to above the void floor works fine
|
||||||
entity.setPosition(entity.getPosition().sub(0, 4f, 0));
|
entity.setPosition(entity.getPosition().sub(0, 4f, 0));
|
||||||
@ -134,7 +144,6 @@ public class BedrockMovePlayerTranslator extends PacketTranslator<MovePlayerPack
|
|||||||
movePlayerPacket.setTeleportationCause(MovePlayerPacket.TeleportationCause.BEHAVIOR);
|
movePlayerPacket.setTeleportationCause(MovePlayerPacket.TeleportationCause.BEHAVIOR);
|
||||||
session.sendUpstreamPacket(movePlayerPacket);
|
session.sendUpstreamPacket(movePlayerPacket);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
session.getSkullCache().updateVisibleSkulls();
|
session.getSkullCache().updateVisibleSkulls();
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,10 @@
|
|||||||
|
|
||||||
package org.geysermc.geyser.translator.protocol.bedrock.world;
|
package org.geysermc.geyser.translator.protocol.bedrock.world;
|
||||||
|
|
||||||
|
import com.github.steveice10.mc.protocol.data.game.entity.player.Hand;
|
||||||
|
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundSwingPacket;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.SoundEvent;
|
import org.cloudburstmc.protocol.bedrock.data.SoundEvent;
|
||||||
|
import org.cloudburstmc.protocol.bedrock.packet.AnimatePacket;
|
||||||
import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket;
|
import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket;
|
||||||
import org.geysermc.geyser.session.GeyserSession;
|
import org.geysermc.geyser.session.GeyserSession;
|
||||||
import org.geysermc.geyser.translator.protocol.PacketTranslator;
|
import org.geysermc.geyser.translator.protocol.PacketTranslator;
|
||||||
@ -46,5 +49,22 @@ public class BedrockLevelSoundEventTranslator extends PacketTranslator<LevelSoun
|
|||||||
// Sent here because Java still sends a cooldown if the player doesn't hit anything but Bedrock always sends a sound
|
// Sent here because Java still sends a cooldown if the player doesn't hit anything but Bedrock always sends a sound
|
||||||
CooldownUtils.sendCooldown(session);
|
CooldownUtils.sendCooldown(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (packet.getSound() == SoundEvent.ATTACK_NODAMAGE && session.getArmAnimationTicks() == -1) {
|
||||||
|
// https://github.com/GeyserMC/Geyser/issues/2113
|
||||||
|
// Seems like consoles and Android with keyboard send the animation packet on 1.19.51, hence the animation
|
||||||
|
// tick check - the animate packet is sent first.
|
||||||
|
// ATTACK_NODAMAGE = player clicked air
|
||||||
|
// This should only be revisited if Bedrock packets get full Java parity, or Bedrock starts sending arm
|
||||||
|
// animation packets after ATTACK_NODAMAGE, OR ATTACK_NODAMAGE gets removed/isn't sent in the same spot
|
||||||
|
session.sendDownstreamPacket(new ServerboundSwingPacket(Hand.MAIN_HAND));
|
||||||
|
session.activateArmAnimationTicking();
|
||||||
|
|
||||||
|
// Send packet to Bedrock so it knows
|
||||||
|
AnimatePacket animatePacket = new AnimatePacket();
|
||||||
|
animatePacket.setRuntimeEntityId(session.getPlayerEntity().getGeyserId());
|
||||||
|
animatePacket.setAction(AnimatePacket.Action.SWING_ARM);
|
||||||
|
session.sendUpstreamPacket(animatePacket);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ import lombok.ToString;
|
|||||||
import net.kyori.adventure.text.format.NamedTextColor;
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
import org.geysermc.geyser.GeyserImpl;
|
import org.geysermc.geyser.GeyserImpl;
|
||||||
import org.geysermc.geyser.api.event.downstream.ServerDefineCommandsEvent;
|
import org.geysermc.geyser.api.event.java.ServerDefineCommandsEvent;
|
||||||
import org.geysermc.geyser.command.GeyserCommandManager;
|
import org.geysermc.geyser.command.GeyserCommandManager;
|
||||||
import org.geysermc.geyser.inventory.item.Enchantment;
|
import org.geysermc.geyser.inventory.item.Enchantment;
|
||||||
import org.geysermc.geyser.registry.BlockRegistries;
|
import org.geysermc.geyser.registry.BlockRegistries;
|
||||||
@ -58,6 +58,7 @@ import org.geysermc.geyser.translator.protocol.Translator;
|
|||||||
import org.geysermc.geyser.util.EntityUtils;
|
import org.geysermc.geyser.util.EntityUtils;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Translator(packet = ClientboundCommandsPacket.class)
|
@Translator(packet = ClientboundCommandsPacket.class)
|
||||||
public class JavaCommandsTranslator extends PacketTranslator<ClientboundCommandsPacket> {
|
public class JavaCommandsTranslator extends PacketTranslator<ClientboundCommandsPacket> {
|
||||||
@ -151,12 +152,20 @@ public class JavaCommandsTranslator extends PacketTranslator<ClientboundCommands
|
|||||||
index -> new HashSet<>()).add(node.getName().toLowerCase());
|
index -> new HashSet<>()).add(node.getName().toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerDefineCommandsEvent event = new ServerDefineCommandsEvent(session, commands.keySet());
|
var eventBus = session.getGeyser().eventBus();
|
||||||
session.getGeyser().eventBus().fire(event);
|
|
||||||
|
var event = new ServerDefineCommandsEvent(session, commands.keySet());
|
||||||
|
eventBus.fire(event);
|
||||||
if (event.isCancelled()) {
|
if (event.isCancelled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var oldEvent = new org.geysermc.geyser.api.event.downstream.ServerDefineCommandsEvent(session, commands.keySet());
|
||||||
|
eventBus.fire(oldEvent);
|
||||||
|
if (oldEvent.isCancelled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// The command flags, not sure what these do apart from break things
|
// The command flags, not sure what these do apart from break things
|
||||||
Set<CommandData.Flag> flags = Set.of();
|
Set<CommandData.Flag> flags = Set.of();
|
||||||
|
|
||||||
@ -247,6 +256,7 @@ public class JavaCommandsTranslator extends PacketTranslator<ClientboundCommands
|
|||||||
case RESOURCE -> handleResource(context, ((ResourceProperties) node.getProperties()).getRegistryKey(), false);
|
case RESOURCE -> handleResource(context, ((ResourceProperties) node.getProperties()).getRegistryKey(), false);
|
||||||
case RESOURCE_OR_TAG -> handleResource(context, ((ResourceProperties) node.getProperties()).getRegistryKey(), true);
|
case RESOURCE_OR_TAG -> handleResource(context, ((ResourceProperties) node.getProperties()).getRegistryKey(), true);
|
||||||
case DIMENSION -> context.session.getLevels();
|
case DIMENSION -> context.session.getLevels();
|
||||||
|
case TEAM -> context.getTeams(); // Note: as of Java 1.19.3, objectives are currently parsed from the server
|
||||||
default -> CommandParam.STRING;
|
default -> CommandParam.STRING;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -265,7 +275,10 @@ public class JavaCommandsTranslator extends PacketTranslator<ClientboundCommands
|
|||||||
/**
|
/**
|
||||||
* Stores the command description and parameter data for best optimizing the Bedrock commands packet.
|
* Stores the command description and parameter data for best optimizing the Bedrock commands packet.
|
||||||
*/
|
*/
|
||||||
private record BedrockCommandInfo(String name, String description, CommandParamData[][] paramData) implements ServerDefineCommandsEvent.CommandInfo {
|
private record BedrockCommandInfo(String name, String description, CommandParamData[][] paramData) implements
|
||||||
|
org.geysermc.geyser.api.event.downstream.ServerDefineCommandsEvent.CommandInfo,
|
||||||
|
ServerDefineCommandsEvent.CommandInfo
|
||||||
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -279,6 +292,7 @@ public class JavaCommandsTranslator extends PacketTranslator<ClientboundCommands
|
|||||||
private String[] blockStates;
|
private String[] blockStates;
|
||||||
private String[] entityTypes;
|
private String[] entityTypes;
|
||||||
private String[] itemNames;
|
private String[] itemNames;
|
||||||
|
private CommandEnumData teams;
|
||||||
|
|
||||||
CommandBuilderContext(GeyserSession session) {
|
CommandBuilderContext(GeyserSession session) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
@ -322,6 +336,17 @@ public class JavaCommandsTranslator extends PacketTranslator<ClientboundCommands
|
|||||||
}
|
}
|
||||||
return (itemNames = Registries.JAVA_ITEM_IDENTIFIERS.get().keySet().toArray(new String[0]));
|
return (itemNames = Registries.JAVA_ITEM_IDENTIFIERS.get().keySet().toArray(new String[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private CommandEnumData getTeams() {
|
||||||
|
if (teams != null) {
|
||||||
|
return teams;
|
||||||
|
}
|
||||||
|
return (teams = new CommandEnumData("Geyser_Teams",
|
||||||
|
Arrays.stream(session.getWorldCache().getScoreboard().getTeamNames())
|
||||||
|
.collect(Collectors.toMap(o -> o, o -> EnumSet.noneOf(CommandEnumConstraint.class), (o1, o2) -> o1, LinkedHashMap::new)),
|
||||||
|
true
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@ -407,7 +432,10 @@ public class JavaCommandsTranslator extends PacketTranslator<ClientboundCommands
|
|||||||
CommandEnumData enumData = null;
|
CommandEnumData enumData = null;
|
||||||
CommandParam type = null;
|
CommandParam type = null;
|
||||||
boolean optional = this.paramNode.isExecutable();
|
boolean optional = this.paramNode.isExecutable();
|
||||||
if (mappedType instanceof String[]) {
|
if (mappedType instanceof CommandEnumData) {
|
||||||
|
// Likely to specify isSoft, to be possibly updated later.
|
||||||
|
enumData = (CommandEnumData) mappedType;
|
||||||
|
} else if (mappedType instanceof String[]) {
|
||||||
LinkedHashMap<String, Set<CommandEnumConstraint>> map = new LinkedHashMap<>();
|
LinkedHashMap<String, Set<CommandEnumConstraint>> map = new LinkedHashMap<>();
|
||||||
for (String s : (String[]) mappedType) {
|
for (String s : (String[]) mappedType) {
|
||||||
map.put(s, Set.of());
|
map.put(s, Set.of());
|
||||||
|
@ -23,23 +23,22 @@
|
|||||||
* @link https://github.com/GeyserMC/Geyser
|
* @link https://github.com/GeyserMC/Geyser
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.geysermc.api.util;
|
package org.geysermc.geyser.translator.protocol.java;
|
||||||
|
|
||||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.ClientboundServerDataPacket;
|
||||||
|
import org.geysermc.geyser.api.util.TriState;
|
||||||
|
import org.geysermc.geyser.session.GeyserSession;
|
||||||
|
import org.geysermc.geyser.translator.protocol.PacketTranslator;
|
||||||
|
import org.geysermc.geyser.translator.protocol.Translator;
|
||||||
|
|
||||||
public enum UiProfile {
|
@Translator(packet = ClientboundServerDataPacket.class)
|
||||||
CLASSIC, POCKET;
|
public class JavaServerDataTranslator extends PacketTranslator<ClientboundServerDataPacket> {
|
||||||
|
|
||||||
private static final UiProfile[] VALUES = values();
|
@Override
|
||||||
|
public void translate(GeyserSession session, ClientboundServerDataPacket packet) {
|
||||||
/**
|
// We only want to warn about chat maybe not working once
|
||||||
* Get the UiProfile from the identifier.
|
if (packet.isEnforcesSecureChat() && session.getWorldCache().getChatWarningSent() == TriState.NOT_SET) {
|
||||||
*
|
session.getWorldCache().setChatWarningSent(TriState.FALSE);
|
||||||
* @param id the UiProfile identifier
|
}
|
||||||
* @return The UiProfile or {@link #CLASSIC} if the profile wasn't found
|
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
public static UiProfile fromId(int id) {
|
|
||||||
return VALUES.length > id ? VALUES[id] : VALUES[0];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -39,7 +39,9 @@ import lombok.AllArgsConstructor;
|
|||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.defintions.ItemDefinition;
|
import org.cloudburstmc.protocol.bedrock.data.defintions.ItemDefinition;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
|
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.CraftingData;
|
import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.MultiRecipeData;
|
||||||
|
import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.RecipeData;
|
||||||
|
import org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.SmithingTransformRecipeData;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.DefaultDescriptor;
|
import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.DefaultDescriptor;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.ItemDescriptorWithCount;
|
import org.cloudburstmc.protocol.bedrock.data.inventory.descriptor.ItemDescriptorWithCount;
|
||||||
import org.cloudburstmc.protocol.bedrock.packet.CraftingDataPacket;
|
import org.cloudburstmc.protocol.bedrock.packet.CraftingDataPacket;
|
||||||
@ -47,6 +49,7 @@ import org.geysermc.geyser.inventory.recipe.GeyserRecipe;
|
|||||||
import org.geysermc.geyser.inventory.recipe.GeyserShapedRecipe;
|
import org.geysermc.geyser.inventory.recipe.GeyserShapedRecipe;
|
||||||
import org.geysermc.geyser.inventory.recipe.GeyserShapelessRecipe;
|
import org.geysermc.geyser.inventory.recipe.GeyserShapelessRecipe;
|
||||||
import org.geysermc.geyser.inventory.recipe.GeyserStonecutterData;
|
import org.geysermc.geyser.inventory.recipe.GeyserStonecutterData;
|
||||||
|
import org.geysermc.geyser.network.GameProtocol;
|
||||||
import org.geysermc.geyser.registry.Registries;
|
import org.geysermc.geyser.registry.Registries;
|
||||||
import org.geysermc.geyser.registry.type.ItemMapping;
|
import org.geysermc.geyser.registry.type.ItemMapping;
|
||||||
import org.geysermc.geyser.session.GeyserSession;
|
import org.geysermc.geyser.session.GeyserSession;
|
||||||
@ -70,16 +73,16 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
|
|||||||
/**
|
/**
|
||||||
* Required to use the specified cartography table recipes
|
* Required to use the specified cartography table recipes
|
||||||
*/
|
*/
|
||||||
private static final List<CraftingData> CARTOGRAPHY_RECIPES = Arrays.asList(
|
private static final List<RecipeData> CARTOGRAPHY_RECIPES = Arrays.asList(
|
||||||
CraftingData.fromMulti(UUID.fromString("8b36268c-1829-483c-a0f1-993b7156a8f2"), ++LAST_RECIPE_NET_ID), // Map extending
|
MultiRecipeData.of(UUID.fromString("8b36268c-1829-483c-a0f1-993b7156a8f2"), ++LAST_RECIPE_NET_ID), // Map extending
|
||||||
CraftingData.fromMulti(UUID.fromString("442d85ed-8272-4543-a6f1-418f90ded05d"), ++LAST_RECIPE_NET_ID), // Map cloning
|
MultiRecipeData.of(UUID.fromString("442d85ed-8272-4543-a6f1-418f90ded05d"), ++LAST_RECIPE_NET_ID), // Map cloning
|
||||||
CraftingData.fromMulti(UUID.fromString("98c84b38-1085-46bd-b1ce-dd38c159e6cc"), ++LAST_RECIPE_NET_ID), // Map upgrading
|
MultiRecipeData.of(UUID.fromString("98c84b38-1085-46bd-b1ce-dd38c159e6cc"), ++LAST_RECIPE_NET_ID), // Map upgrading
|
||||||
CraftingData.fromMulti(UUID.fromString("602234e4-cac1-4353-8bb7-b1ebff70024b"), ++LAST_RECIPE_NET_ID) // Map locking
|
MultiRecipeData.of(UUID.fromString("602234e4-cac1-4353-8bb7-b1ebff70024b"), ++LAST_RECIPE_NET_ID) // Map locking
|
||||||
);
|
);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void translate(GeyserSession session, ClientboundUpdateRecipesPacket packet) {
|
public void translate(GeyserSession session, ClientboundUpdateRecipesPacket packet) {
|
||||||
Map<RecipeType, List<CraftingData>> recipeTypes = Registries.CRAFTING_DATA.forVersion(session.getUpstream().getProtocolVersion());
|
Map<RecipeType, List<RecipeData>> recipeTypes = Registries.CRAFTING_DATA.forVersion(session.getUpstream().getProtocolVersion());
|
||||||
// Get the last known network ID (first used for the pregenerated recipes) and increment from there.
|
// Get the last known network ID (first used for the pregenerated recipes) and increment from there.
|
||||||
int netId = InventoryUtils.LAST_RECIPE_NET_ID + 1;
|
int netId = InventoryUtils.LAST_RECIPE_NET_ID + 1;
|
||||||
|
|
||||||
@ -101,7 +104,7 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
|
|||||||
ItemDescriptorWithCount[][] inputCombinations = combinations(session, shapelessRecipeData.getIngredients());
|
ItemDescriptorWithCount[][] inputCombinations = combinations(session, shapelessRecipeData.getIngredients());
|
||||||
for (ItemDescriptorWithCount[] inputs : inputCombinations) {
|
for (ItemDescriptorWithCount[] inputs : inputCombinations) {
|
||||||
UUID uuid = UUID.randomUUID();
|
UUID uuid = UUID.randomUUID();
|
||||||
craftingDataPacket.getCraftingData().add(CraftingData.fromShapeless(uuid.toString(),
|
craftingDataPacket.getCraftingData().add(org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapelessRecipeData.shapeless(uuid.toString(),
|
||||||
Arrays.asList(inputs), Collections.singletonList(output), uuid, "crafting_table", 0, netId));
|
Arrays.asList(inputs), Collections.singletonList(output), uuid, "crafting_table", 0, netId));
|
||||||
recipeMap.put(netId++, new GeyserShapelessRecipe(shapelessRecipeData));
|
recipeMap.put(netId++, new GeyserShapelessRecipe(shapelessRecipeData));
|
||||||
}
|
}
|
||||||
@ -118,7 +121,7 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
|
|||||||
ItemDescriptorWithCount[][] inputCombinations = combinations(session, shapedRecipeData.getIngredients());
|
ItemDescriptorWithCount[][] inputCombinations = combinations(session, shapedRecipeData.getIngredients());
|
||||||
for (ItemDescriptorWithCount[] inputs : inputCombinations) {
|
for (ItemDescriptorWithCount[] inputs : inputCombinations) {
|
||||||
UUID uuid = UUID.randomUUID();
|
UUID uuid = UUID.randomUUID();
|
||||||
craftingDataPacket.getCraftingData().add(CraftingData.fromShaped(uuid.toString(),
|
craftingDataPacket.getCraftingData().add(org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapedRecipeData.shaped(uuid.toString(),
|
||||||
shapedRecipeData.getWidth(), shapedRecipeData.getHeight(), Arrays.asList(inputs),
|
shapedRecipeData.getWidth(), shapedRecipeData.getHeight(), Arrays.asList(inputs),
|
||||||
Collections.singletonList(output), uuid, "crafting_table", 0, netId));
|
Collections.singletonList(output), uuid, "crafting_table", 0, netId));
|
||||||
recipeMap.put(netId++, new GeyserShapedRecipe(shapedRecipeData));
|
recipeMap.put(netId++, new GeyserShapedRecipe(shapedRecipeData));
|
||||||
@ -145,15 +148,21 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
|
|||||||
for (ItemStack addition : recipeData.getAddition().getOptions()) {
|
for (ItemStack addition : recipeData.getAddition().getOptions()) {
|
||||||
ItemDescriptorWithCount bedrockAddition = ItemDescriptorWithCount.fromItem(ItemTranslator.translateToBedrock(session, addition));
|
ItemDescriptorWithCount bedrockAddition = ItemDescriptorWithCount.fromItem(ItemTranslator.translateToBedrock(session, addition));
|
||||||
|
|
||||||
|
if (GameProtocol.supports1_19_60(session)) {
|
||||||
|
// Note: vanilla inputs use aux value of Short.MAX_VALUE
|
||||||
|
craftingDataPacket.getCraftingData().add(SmithingTransformRecipeData.of(recipe.getIdentifier(),
|
||||||
|
bedrockBase, bedrockAddition, output, "smithing_table", netId++));
|
||||||
|
} else {
|
||||||
UUID uuid = UUID.randomUUID();
|
UUID uuid = UUID.randomUUID();
|
||||||
craftingDataPacket.getCraftingData().add(CraftingData.fromShapeless(uuid.toString(),
|
craftingDataPacket.getCraftingData().add(org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapelessRecipeData.shapeless(uuid.toString(),
|
||||||
List.of(bedrockBase, bedrockAddition),
|
List.of(bedrockBase, bedrockAddition),
|
||||||
Collections.singletonList(output), uuid, "smithing_table", 2, netId++));
|
Collections.singletonList(output), uuid, "smithing_table", 2, netId++));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
default -> {
|
default -> {
|
||||||
List<CraftingData> craftingData = recipeTypes.get(recipe.getType());
|
List<RecipeData> craftingData = recipeTypes.get(recipe.getType());
|
||||||
if (craftingData != null) {
|
if (craftingData != null) {
|
||||||
craftingDataPacket.getCraftingData().addAll(craftingData);
|
craftingDataPacket.getCraftingData().addAll(craftingData);
|
||||||
}
|
}
|
||||||
@ -187,7 +196,7 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
|
|||||||
UUID uuid = UUID.randomUUID();
|
UUID uuid = UUID.randomUUID();
|
||||||
|
|
||||||
// We need to register stonecutting recipes so they show up on Bedrock
|
// We need to register stonecutting recipes so they show up on Bedrock
|
||||||
craftingDataPacket.getCraftingData().add(CraftingData.fromShapeless(uuid.toString(),
|
craftingDataPacket.getCraftingData().add(org.cloudburstmc.protocol.bedrock.data.inventory.crafting.recipe.ShapelessRecipeData.shapeless(uuid.toString(),
|
||||||
Collections.singletonList(descriptor), Collections.singletonList(output), uuid, "stonecutter", 0, netId));
|
Collections.singletonList(descriptor), Collections.singletonList(output), uuid, "stonecutter", 0, netId));
|
||||||
|
|
||||||
// Save the recipe list for reference when crafting
|
// Save the recipe list for reference when crafting
|
||||||
|
@ -44,17 +44,15 @@ public class JavaPlayerInfoRemoveTranslator extends PacketTranslator<Clientbound
|
|||||||
for (UUID id : packet.getProfileIds()) {
|
for (UUID id : packet.getProfileIds()) {
|
||||||
// As the player entity is no longer present, we can remove the entry
|
// As the player entity is no longer present, we can remove the entry
|
||||||
PlayerEntity entity = session.getEntityCache().removePlayerEntity(id);
|
PlayerEntity entity = session.getEntityCache().removePlayerEntity(id);
|
||||||
|
UUID removeId;
|
||||||
if (entity != null) {
|
if (entity != null) {
|
||||||
// Just remove the entity's player list status
|
// Just remove the entity's player list status
|
||||||
// Don't despawn the entity - the Java server will also take care of that.
|
// Don't despawn the entity - the Java server will also take care of that.
|
||||||
entity.setPlayerList(false);
|
removeId = entity.getTabListUuid();
|
||||||
}
|
|
||||||
if (entity == session.getPlayerEntity()) {
|
|
||||||
// If removing ourself we use our AuthData UUID
|
|
||||||
translate.getEntries().add(new PlayerListPacket.Entry(session.getAuthData().uuid()));
|
|
||||||
} else {
|
} else {
|
||||||
translate.getEntries().add(new PlayerListPacket.Entry(id));
|
removeId = id;
|
||||||
}
|
}
|
||||||
|
translate.getEntries().add(new PlayerListPacket.Entry(removeId));
|
||||||
}
|
}
|
||||||
|
|
||||||
session.sendUpstreamPacket(translate);
|
session.sendUpstreamPacket(translate);
|
||||||
|
Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden Mehr anzeigen
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren