mirror of
https://github.com/GeyserMC/Geyser.git
synced 2024-08-14 23:57:35 +00:00
Merge remote-tracking branch 'origin/master' into feature/dedicated-api-repo
This commit is contained in:
commit
effe046308
15 changed files with 276 additions and 156 deletions
|
@ -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
|
||||||
|
|
|
@ -100,6 +100,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +120,7 @@ public class PlayerEntity extends LivingEntity {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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);
|
||||||
|
@ -315,19 +316,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);
|
||||||
|
@ -339,23 +331,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(EntityData.NAMETAG, newDisplayName);
|
|
||||||
} else if (useGivenTeam) {
|
|
||||||
// The name has reset, if it was previously something else
|
|
||||||
needsUpdate = !newDisplayName.equals(nametag);
|
|
||||||
dirtyMetadata.put(EntityData.NAMETAG, 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(EntityData.NAMETAG, this.nametag);
|
||||||
SetEntityDataPacket packet = new SetEntityDataPacket();
|
|
||||||
packet.getMetadata().put(EntityData.NAMETAG, newDisplayName);
|
|
||||||
packet.setRuntimeEntityId(geyserId);
|
|
||||||
session.sendUpstreamPacket(packet);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,17 +26,20 @@
|
||||||
package org.geysermc.geyser.entity.type.player;
|
package org.geysermc.geyser.entity.type.player;
|
||||||
|
|
||||||
import com.nukkitx.math.vector.Vector3f;
|
import com.nukkitx.math.vector.Vector3f;
|
||||||
|
import com.nukkitx.math.vector.Vector3i;
|
||||||
import com.nukkitx.protocol.bedrock.data.GameType;
|
import com.nukkitx.protocol.bedrock.data.GameType;
|
||||||
import com.nukkitx.protocol.bedrock.data.PlayerPermission;
|
import com.nukkitx.protocol.bedrock.data.PlayerPermission;
|
||||||
import com.nukkitx.protocol.bedrock.data.command.CommandPermission;
|
import com.nukkitx.protocol.bedrock.data.command.CommandPermission;
|
||||||
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
|
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
|
||||||
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
|
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
|
||||||
import com.nukkitx.protocol.bedrock.packet.AddPlayerPacket;
|
import com.nukkitx.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,6 +49,12 @@ 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);
|
||||||
}
|
}
|
||||||
|
@ -102,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(() -> {
|
||||||
|
|
|
@ -30,6 +30,7 @@ import com.nukkitx.protocol.bedrock.data.ScoreInfo;
|
||||||
import com.nukkitx.protocol.bedrock.packet.RemoveObjectivePacket;
|
import com.nukkitx.protocol.bedrock.packet.RemoveObjectivePacket;
|
||||||
import com.nukkitx.protocol.bedrock.packet.SetDisplayObjectivePacket;
|
import com.nukkitx.protocol.bedrock.packet.SetDisplayObjectivePacket;
|
||||||
import com.nukkitx.protocol.bedrock.packet.SetScorePacket;
|
import com.nukkitx.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.*;
|
||||||
|
@ -55,6 +57,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;
|
||||||
|
@ -132,6 +141,10 @@ public final class Scoreboard {
|
||||||
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 +341,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,9 +351,19 @@ 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()) {
|
||||||
|
playerToTeam.remove(name, remove);
|
||||||
|
}
|
||||||
|
|
||||||
|
session.removeCommandEnum("Geyser_Teams", remove.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Contract("-> new")
|
||||||
|
public String[] getTeamNames() {
|
||||||
|
return teams.keySet().toArray(new String[0]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the display names of all entities in a given team.
|
* Updates the display names of all entities in a given team.
|
||||||
* @param teamChange the players have either joined or left the team. Used for optimizations when just the display name updated.
|
* @param teamChange the players have either joined or left the team. Used for optimizations when just the display name updated.
|
||||||
|
@ -368,7 +386,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 +403,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,7 @@ public final class Team {
|
||||||
if (entities.add(name)) {
|
if (entities.add(name)) {
|
||||||
added.add(name);
|
added.add(name);
|
||||||
}
|
}
|
||||||
|
scoreboard.getPlayerToTeam().put(name, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (added.isEmpty()) {
|
if (added.isEmpty()) {
|
||||||
|
@ -93,6 +94,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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,7 +67,9 @@ import com.nukkitx.nbt.NbtMap;
|
||||||
import com.nukkitx.protocol.bedrock.BedrockPacket;
|
import com.nukkitx.protocol.bedrock.BedrockPacket;
|
||||||
import com.nukkitx.protocol.bedrock.BedrockServerSession;
|
import com.nukkitx.protocol.bedrock.BedrockServerSession;
|
||||||
import com.nukkitx.protocol.bedrock.data.*;
|
import com.nukkitx.protocol.bedrock.data.*;
|
||||||
|
import com.nukkitx.protocol.bedrock.data.command.CommandEnumData;
|
||||||
import com.nukkitx.protocol.bedrock.data.command.CommandPermission;
|
import com.nukkitx.protocol.bedrock.data.command.CommandPermission;
|
||||||
|
import com.nukkitx.protocol.bedrock.data.command.SoftEnumUpdateType;
|
||||||
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
|
import com.nukkitx.protocol.bedrock.data.entity.EntityFlag;
|
||||||
import com.nukkitx.protocol.bedrock.packet.*;
|
import com.nukkitx.protocol.bedrock.packet.*;
|
||||||
import io.netty.channel.Channel;
|
import io.netty.channel.Channel;
|
||||||
|
@ -1402,6 +1404,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setServerRenderDistance(int renderDistance) {
|
public void setServerRenderDistance(int renderDistance) {
|
||||||
|
// +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();
|
||||||
|
@ -1891,4 +1897,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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -35,6 +35,7 @@ import com.nukkitx.protocol.bedrock.packet.PlayerListPacket;
|
||||||
import com.nukkitx.protocol.bedrock.packet.PlayerSkinPacket;
|
import com.nukkitx.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.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;
|
||||||
|
@ -69,7 +70,7 @@ public class SkinManager {
|
||||||
// The server either didn't have a texture to send, or we didn't have the texture ID cached.
|
// 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.
|
// Let's see if this player is a Bedrock player, and if so, let's pull their skin.
|
||||||
// Otherwise, grab the default player skin
|
// Otherwise, grab the default player skin
|
||||||
SkinProvider.SkinData fallbackSkinData = SkinProvider.determineFallbackSkinData(playerEntity);
|
SkinProvider.SkinData fallbackSkinData = SkinProvider.determineFallbackSkinData(playerEntity.getUuid());
|
||||||
if (skin == null) {
|
if (skin == null) {
|
||||||
skin = fallbackSkinData.skin();
|
skin = fallbackSkinData.skin();
|
||||||
geometry = fallbackSkinData.geometry();
|
geometry = fallbackSkinData.geometry();
|
||||||
|
@ -255,24 +256,28 @@ public class SkinManager {
|
||||||
* @return The built GameProfileData
|
* @return The built GameProfileData
|
||||||
*/
|
*/
|
||||||
public static @Nullable GameProfileData from(PlayerEntity entity) {
|
public static @Nullable GameProfileData from(PlayerEntity entity) {
|
||||||
try {
|
String texturesProperty = entity.getTexturesProperty();
|
||||||
String texturesProperty = entity.getTexturesProperty();
|
if (texturesProperty == null) {
|
||||||
|
// Likely offline mode
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (texturesProperty == null) {
|
try {
|
||||||
// Likely offline mode
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return loadFromJson(texturesProperty);
|
return loadFromJson(texturesProperty);
|
||||||
} catch (IOException exception) {
|
} catch (Exception exception) {
|
||||||
GeyserImpl.getInstance().getLogger().debug("Something went wrong while processing skin for " + entity.getUsername());
|
if (entity instanceof SkullPlayerEntity skullEntity) {
|
||||||
|
GeyserImpl.getInstance().getLogger().debug("Something went wrong while processing skin for skull at " + skullEntity.getSkullPosition() + " with Value: " + texturesProperty);
|
||||||
|
} else {
|
||||||
|
GeyserImpl.getInstance().getLogger().debug("Something went wrong while processing skin for " + entity.getUsername() + " with Value: " + texturesProperty);
|
||||||
|
}
|
||||||
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
|
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
|
||||||
exception.printStackTrace();
|
exception.printStackTrace();
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
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");
|
||||||
|
|
||||||
|
@ -285,14 +290,23 @@ 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);
|
||||||
|
|
|
@ -26,9 +26,6 @@
|
||||||
package org.geysermc.geyser.skin;
|
package org.geysermc.geyser.skin;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
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 it.unimi.dsi.fastutil.bytes.ByteArrays;
|
||||||
|
@ -172,14 +169,13 @@ public class SkinProvider {
|
||||||
/**
|
/**
|
||||||
* If skin data fails to apply, or there is no skin data to apply, determine what skin we should give as a fallback.
|
* 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(PlayerEntity entity) {
|
static SkinData determineFallbackSkinData(UUID uuid) {
|
||||||
Skin skin = null;
|
Skin skin = null;
|
||||||
Cape cape = null;
|
Cape cape = null;
|
||||||
SkinGeometry geometry = SkinGeometry.WIDE;
|
SkinGeometry geometry = SkinGeometry.WIDE;
|
||||||
|
|
||||||
if (GeyserImpl.getInstance().getConfig().getRemote().authType() != AuthType.ONLINE) {
|
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.
|
// Let's see if this player is a Bedrock player, and if so, let's pull their skin.
|
||||||
UUID uuid = entity.getUuid();
|
|
||||||
GeyserSession session = GeyserImpl.getInstance().connectionByUuid(uuid);
|
GeyserSession session = GeyserImpl.getInstance().connectionByUuid(uuid);
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
String skinId = session.getClientData().getSkinId();
|
String skinId = session.getClientData().getSkinId();
|
||||||
|
@ -192,7 +188,7 @@ public class SkinProvider {
|
||||||
|
|
||||||
if (skin == null) {
|
if (skin == null) {
|
||||||
// We don't have a skin for the player right now. Fall back to a default.
|
// We don't have a skin for the player right now. Fall back to a default.
|
||||||
ProvidedSkins.ProvidedSkin providedSkin = ProvidedSkins.getDefaultPlayerSkin(entity.getUuid());
|
ProvidedSkins.ProvidedSkin providedSkin = ProvidedSkins.getDefaultPlayerSkin(uuid);
|
||||||
skin = providedSkin.getData();
|
skin = providedSkin.getData();
|
||||||
geometry = providedSkin.isSlim() ? SkinProvider.SkinGeometry.SLIM : SkinProvider.SkinGeometry.WIDE;
|
geometry = providedSkin.isSlim() ? SkinProvider.SkinGeometry.SLIM : SkinProvider.SkinGeometry.WIDE;
|
||||||
}
|
}
|
||||||
|
@ -232,7 +228,7 @@ public class SkinProvider {
|
||||||
SkinManager.GameProfileData data = SkinManager.GameProfileData.from(entity);
|
SkinManager.GameProfileData data = SkinManager.GameProfileData.from(entity);
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
// This player likely does not have a textures property
|
// This player likely does not have a textures property
|
||||||
return CompletableFuture.completedFuture(determineFallbackSkinData(entity));
|
return CompletableFuture.completedFuture(determineFallbackSkinData(entity.getUuid()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return requestSkinAndCape(entity.getUuid(), data.skinUrl(), data.capeUrl())
|
return requestSkinAndCape(entity.getUuid(), data.skinUrl(), data.capeUrl())
|
||||||
|
@ -597,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();
|
||||||
}
|
}
|
||||||
|
@ -647,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);
|
||||||
|
|
|
@ -29,11 +29,12 @@ import com.nukkitx.protocol.bedrock.data.skin.ImageData;
|
||||||
import com.nukkitx.protocol.bedrock.data.skin.SerializedSkin;
|
import com.nukkitx.protocol.bedrock.data.skin.SerializedSkin;
|
||||||
import com.nukkitx.protocol.bedrock.packet.PlayerSkinPacket;
|
import com.nukkitx.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 {
|
||||||
|
@ -48,28 +49,37 @@ public class SkullSkinManager extends SkinManager {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void requestAndHandleSkin(PlayerEntity entity, GeyserSession session,
|
public static void requestAndHandleSkin(SkullPlayerEntity entity, GeyserSession session,
|
||||||
Consumer<SkinProvider.Skin> skinConsumer) {
|
Consumer<SkinProvider.Skin> skinConsumer) {
|
||||||
|
BiConsumer<SkinProvider.Skin, Throwable> applySkin = (skin, throwable) -> {
|
||||||
|
try {
|
||||||
|
PlayerSkinPacket packet = new PlayerSkinPacket();
|
||||||
|
packet.setUuid(entity.getUuid());
|
||||||
|
packet.setOldSkinName("");
|
||||||
|
packet.setNewSkinName(skin.getTextureUrl());
|
||||||
|
packet.setSkin(buildSkullEntryManually(skin.getTextureUrl(), skin.getSkinData()));
|
||||||
|
packet.setTrustedSkin(true);
|
||||||
|
session.sendUpstreamPacket(packet);
|
||||||
|
} catch (Exception e) {
|
||||||
|
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skinConsumer != null) {
|
||||||
|
skinConsumer.accept(skin);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
GameProfileData data = GameProfileData.from(entity);
|
GameProfileData data = GameProfileData.from(entity);
|
||||||
|
if (data == null) {
|
||||||
SkinProvider.requestSkin(entity.getUuid(), data.skinUrl(), true)
|
GeyserImpl.getInstance().getLogger().debug("Using fallback skin for skull at " + entity.getSkullPosition() +
|
||||||
.whenCompleteAsync((skin, throwable) -> {
|
" with texture value: " + entity.getTexturesProperty() + " and UUID: " + entity.getSkullUUID());
|
||||||
try {
|
// No texture available, fallback using the UUID
|
||||||
PlayerSkinPacket packet = new PlayerSkinPacket();
|
SkinProvider.SkinData fallback = SkinProvider.determineFallbackSkinData(entity.getSkullUUID());
|
||||||
packet.setUuid(entity.getUuid());
|
applySkin.accept(fallback.skin(), null);
|
||||||
packet.setOldSkinName("");
|
} else {
|
||||||
packet.setNewSkinName(skin.getTextureUrl());
|
SkinProvider.requestSkin(entity.getUuid(), data.skinUrl(), true)
|
||||||
packet.setSkin(buildSkullEntryManually(skin.getTextureUrl(), skin.getSkinData()));
|
.whenCompleteAsync(applySkin);
|
||||||
packet.setTrustedSkin(true);
|
}
|
||||||
session.sendUpstreamPacket(packet);
|
|
||||||
} catch (Exception e) {
|
|
||||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.skin.fail", entity.getUuid()), e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (skinConsumer != null) {
|
|
||||||
skinConsumer.accept(skin);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,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())));
|
||||||
|
|
|
@ -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 com.nukkitx.math.vector.Vector3i;
|
import com.nukkitx.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,33 +57,54 @@ 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();
|
||||||
CompoundTag properties = owner.get("Properties");
|
// thank u viaversion
|
||||||
if (properties == null) {
|
return new UUID((long) uuidAsArray[0] << 32 | ((long) uuidAsArray[1] & 0xFFFFFFFFL),
|
||||||
return SkinProvider.requestTexturesFromUsername(owner);
|
(long) uuidAsArray[2] << 32 | ((long) uuidAsArray[3] & 0xFFFFFFFFL));
|
||||||
}
|
|
||||||
|
|
||||||
ListTag textures = properties.get("textures");
|
|
||||||
LinkedHashMap<?,?> tag1 = (LinkedHashMap<?,?>) textures.get(0).getValue();
|
|
||||||
StringTag texture = (StringTag) tag1.get("Value");
|
|
||||||
return CompletableFuture.completedFuture(texture.getValue());
|
|
||||||
}
|
}
|
||||||
return CompletableFuture.completedFuture(null);
|
// 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");
|
||||||
|
if (properties == null) {
|
||||||
|
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");
|
||||||
|
LinkedHashMap<?,?> tag1 = (LinkedHashMap<?,?>) textures.get(0).getValue();
|
||||||
|
StringTag texture = (StringTag) tag1.get("Value");
|
||||||
|
return CompletableFuture.completedFuture(texture.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
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));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,35 +123,26 @@ 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();
|
// Work around there being a floor at the bottom of the world and teleport the player below it
|
||||||
// The void floor is offset about 40 blocks below the bottom of the world
|
// Moving from below to above the void floor works fine
|
||||||
BedrockDimension bedrockDimension = session.getChunkCache().getBedrockDimension();
|
entity.setPosition(entity.getPosition().sub(0, 4f, 0));
|
||||||
int voidFloorLocation = bedrockDimension.minY() - 40;
|
MovePlayerPacket movePlayerPacket = new MovePlayerPacket();
|
||||||
if (floorY <= (voidFloorLocation + 2) && floorY >= voidFloorLocation) {
|
movePlayerPacket.setRuntimeEntityId(entity.getGeyserId());
|
||||||
// Work around there being a floor at the bottom of the world and teleport the player below it
|
movePlayerPacket.setPosition(entity.getPosition());
|
||||||
// Moving from below to above the void floor works fine
|
movePlayerPacket.setRotation(entity.getBedrockRotation());
|
||||||
entity.setPosition(entity.getPosition().sub(0, 4f, 0));
|
movePlayerPacket.setMode(MovePlayerPacket.Mode.TELEPORT);
|
||||||
MovePlayerPacket movePlayerPacket = new MovePlayerPacket();
|
movePlayerPacket.setTeleportationCause(MovePlayerPacket.TeleportationCause.BEHAVIOR);
|
||||||
movePlayerPacket.setRuntimeEntityId(entity.getGeyserId());
|
session.sendUpstreamPacket(movePlayerPacket);
|
||||||
movePlayerPacket.setPosition(entity.getPosition());
|
|
||||||
movePlayerPacket.setRotation(entity.getBedrockRotation());
|
|
||||||
movePlayerPacket.setMode(MovePlayerPacket.Mode.TELEPORT);
|
|
||||||
movePlayerPacket.setTeleportationCause(MovePlayerPacket.TeleportationCause.BEHAVIOR);
|
|
||||||
session.sendUpstreamPacket(movePlayerPacket);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
session.getSkullCache().updateVisibleSkulls();
|
session.getSkullCache().updateVisibleSkulls();
|
||||||
|
|
|
@ -248,6 +248,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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -282,6 +283,7 @@ public class JavaCommandsTranslator extends PacketTranslator<ClientboundCommands
|
||||||
private Object biomesNoTags;
|
private Object biomesNoTags;
|
||||||
private String[] blockStates;
|
private String[] blockStates;
|
||||||
private String[] entityTypes;
|
private String[] entityTypes;
|
||||||
|
private CommandEnumData teams;
|
||||||
|
|
||||||
CommandBuilderContext(GeyserSession session) {
|
CommandBuilderContext(GeyserSession session) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
|
@ -318,6 +320,14 @@ public class JavaCommandsTranslator extends PacketTranslator<ClientboundCommands
|
||||||
}
|
}
|
||||||
return (entityTypes = Registries.JAVA_ENTITY_IDENTIFIERS.get().keySet().toArray(new String[0]));
|
return (entityTypes = Registries.JAVA_ENTITY_IDENTIFIERS.get().keySet().toArray(new String[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private CommandEnumData getTeams() {
|
||||||
|
if (teams != null) {
|
||||||
|
return teams;
|
||||||
|
}
|
||||||
|
return (teams = new CommandEnumData("Geyser_Teams",
|
||||||
|
session.getWorldCache().getScoreboard().getTeamNames(), true));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
|
@ -387,7 +397,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[]) {
|
||||||
enumData = new CommandEnumData(getEnumDataName(paramNode).toLowerCase(Locale.ROOT), (String[]) mappedType, false);
|
enumData = new CommandEnumData(getEnumDataName(paramNode).toLowerCase(Locale.ROOT), (String[]) mappedType, false);
|
||||||
} else {
|
} else {
|
||||||
type = (CommandParam) mappedType;
|
type = (CommandParam) mappedType;
|
||||||
|
|
|
@ -8,10 +8,10 @@ netty = "4.1.80.Final"
|
||||||
guava = "29.0-jre"
|
guava = "29.0-jre"
|
||||||
gson = "2.3.1" # Provided by Spigot 1.8.8
|
gson = "2.3.1" # Provided by Spigot 1.8.8
|
||||||
websocket = "1.5.1"
|
websocket = "1.5.1"
|
||||||
protocol = "2.9.15-20221129.204554-2"
|
protocol = "2.9.15-20230106.005737-3"
|
||||||
raknet = "1.6.28-20220125.214016-6"
|
raknet = "1.6.28-20220125.214016-6"
|
||||||
mcauthlib = "d9d773e"
|
mcauthlib = "d9d773e"
|
||||||
mcprotocollib = "1.19.3-20230104.210231-9"
|
mcprotocollib = "1.19.3-20230107.194116-10"
|
||||||
packetlib = "3.0.1"
|
packetlib = "3.0.1"
|
||||||
adventure = "4.12.0-20220629.025215-9"
|
adventure = "4.12.0-20220629.025215-9"
|
||||||
adventure-platform = "4.1.2"
|
adventure-platform = "4.1.2"
|
||||||
|
|
Loading…
Reference in a new issue