diff --git a/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java b/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java index dd48ca739..20abc59b7 100644 --- a/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java +++ b/core/src/main/java/org/geysermc/geyser/entity/type/player/PlayerEntity.java @@ -103,6 +103,11 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity { */ private @Nullable ParrotEntity rightParrot; + /** + * Whether this player is currently listed. + */ + private boolean listed = false; + public PlayerEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw, String username, @Nullable String texturesProperty) { super(session, entityId, geyserId, uuid, EntityDefinitions.PLAYER, position, motion, yaw, pitch, headYaw); diff --git a/core/src/main/java/org/geysermc/geyser/level/GameRule.java b/core/src/main/java/org/geysermc/geyser/level/GameRule.java index 015f9c50c..8a1af095a 100644 --- a/core/src/main/java/org/geysermc/geyser/level/GameRule.java +++ b/core/src/main/java/org/geysermc/geyser/level/GameRule.java @@ -31,6 +31,7 @@ import lombok.Getter; * This enum stores each gamerule along with the value type and the default. * It is used to construct the list for the settings menu */ +// TODO gamerules with feature flags (e.g. minecart speed with minecart experiment) public enum GameRule { ANNOUNCEADVANCEMENTS("announceAdvancements", true), // JE only COMMANDBLOCKOUTPUT("commandBlockOutput", true), @@ -66,7 +67,8 @@ public enum GameRule { SHOWDEATHMESSAGES("showDeathMessages", true), SPAWNRADIUS("spawnRadius", 10), SPECTATORSGENERATECHUNKS("spectatorsGenerateChunks", true), // JE only - UNIVERSALANGER("universalAnger", false); // JE only + UNIVERSALANGER("universalAnger", false), + LOCATORBAR("locatorBar", true); public static final GameRule[] VALUES = values(); diff --git a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java index 55ca4ebe5..f5aaeed9b 100644 --- a/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java +++ b/core/src/main/java/org/geysermc/geyser/network/UpstreamPacketHandler.java @@ -244,6 +244,8 @@ public class UpstreamPacketHandler extends LoggingPacketHandler { stackPacket.getExperiments().add(new ExperimentData("experimental_graphics", true)); // Enables 2025 Content Drop 2 features stackPacket.getExperiments().add(new ExperimentData("y_2025_drop_2", true)); + // Enables the locator bar for clients below 1.21.90 + stackPacket.getExperiments().add(new ExperimentData("locator_bar", true)); session.sendUpstreamPacket(stackPacket); } diff --git a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java index e60d9c2fb..f99ccac46 100644 --- a/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java +++ b/core/src/main/java/org/geysermc/geyser/session/GeyserSession.java @@ -173,6 +173,7 @@ import org.geysermc.geyser.session.cache.SkullCache; import org.geysermc.geyser.session.cache.StructureBlockCache; import org.geysermc.geyser.session.cache.TagCache; import org.geysermc.geyser.session.cache.TeleportCache; +import org.geysermc.geyser.session.cache.waypoint.WaypointCache; import org.geysermc.geyser.session.cache.WorldBorder; import org.geysermc.geyser.session.cache.WorldCache; import org.geysermc.geyser.session.cache.registry.JavaRegistries; @@ -287,6 +288,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { private final SkullCache skullCache; private final StructureBlockCache structureBlockCache; private final TagCache tagCache; + private final WaypointCache waypointCache; private final WorldCache worldCache; @Setter @@ -312,7 +314,6 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { @Setter private @Nullable InventoryHolder inventoryHolder; - @Getter private final DialogManager dialogManager = new DialogManager(this); /** @@ -736,6 +737,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { this.skullCache = new SkullCache(this); this.structureBlockCache = new StructureBlockCache(); this.tagCache = new TagCache(this); + this.waypointCache = new WaypointCache(this); this.worldCache = new WorldCache(this); this.cameraData = new GeyserCameraData(this); this.entityData = new GeyserEntityData(this); @@ -1263,6 +1265,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { } dialogManager.tick(); + waypointCache.tick(); } catch (Throwable throwable) { throwable.printStackTrace(); } @@ -1693,6 +1696,8 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource { startGamePacket.getExperiments().add(new ExperimentData("experimental_graphics", true)); // Enables 2025 Content Drop 2 features startGamePacket.getExperiments().add(new ExperimentData("y_2025_drop_2", true)); + // Enables the locator bar for clients below 1.21.90 + startGamePacket.getExperiments().add(new ExperimentData("locator_bar", true)); startGamePacket.setVanillaVersion("*"); startGamePacket.setInventoriesServerAuthoritative(true); diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java index ab501cc68..57ce7ecbf 100644 --- a/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java +++ b/core/src/main/java/org/geysermc/geyser/session/cache/TagCache.java @@ -62,7 +62,7 @@ public final class TagCache { this.session = session; } - public void loadPacket(GeyserSession session, ClientboundUpdateTagsPacket packet) { + public void loadPacket(ClientboundUpdateTagsPacket packet) { Map> allTags = packet.getTags(); GeyserLogger logger = session.getGeyser().getLogger(); diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/waypoint/AzimuthWaypoint.java b/core/src/main/java/org/geysermc/geyser/session/cache/waypoint/AzimuthWaypoint.java new file mode 100644 index 000000000..3ca4c2498 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/session/cache/waypoint/AzimuthWaypoint.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2025 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.session.cache.waypoint; + +import org.cloudburstmc.math.vector.Vector3f; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.level.waypoint.AzimuthWaypointData; +import org.geysermc.mcprotocollib.protocol.data.game.level.waypoint.WaypointData; + +import java.awt.Color; +import java.util.Optional; +import java.util.OptionalLong; +import java.util.UUID; + +public class AzimuthWaypoint extends GeyserWaypoint implements TickingWaypoint { + + // In Java, this waypoint always appears really far, so set the distance far here too, + // This also makes the waypoint more accurate on the bar and less susceptible to the player moving + private static final float WAYPOINT_DISTANCE = 1000.0F; + + // The angle, in radians, where the waypoint should appear on the bar + private float angle = 0.0F; + + public AzimuthWaypoint(GeyserSession session, Optional uuid, OptionalLong entityId, Color color) { + super(session, uuid, entityId, color); + } + + @Override + public void setData(WaypointData data) { + angle = ((AzimuthWaypointData) data).angle(); + updatePosition(); + } + + @Override + public void tick() { + // Update position so that it remains accurate to the angle as the player moves around + updatePosition(); + sendLocationPacket(false); + } + + private void updatePosition() { + Vector3f playerPosition = session.getPlayerEntity().position(); + // Unit circle math! + float dx = (float) (Math.cos(angle) * WAYPOINT_DISTANCE); + float dz = (float) -(Math.sin(angle) * WAYPOINT_DISTANCE); + // Set Y to the player's Y since this waypoint always appears in the centre of the bar on Java + position = Vector3f.from(playerPosition.getX() + dx, playerPosition.getY(), playerPosition.getZ() + dz); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/waypoint/ChunkWaypoint.java b/core/src/main/java/org/geysermc/geyser/session/cache/waypoint/ChunkWaypoint.java new file mode 100644 index 000000000..264185b7e --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/session/cache/waypoint/ChunkWaypoint.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2025 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.session.cache.waypoint; + +import org.cloudburstmc.math.vector.Vector3f; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.level.waypoint.ChunkWaypointData; +import org.geysermc.mcprotocollib.protocol.data.game.level.waypoint.WaypointData; + +import java.awt.Color; +import java.util.Optional; +import java.util.OptionalLong; +import java.util.UUID; + +public class ChunkWaypoint extends GeyserWaypoint { + + public ChunkWaypoint(GeyserSession session, Optional uuid, OptionalLong entityId, Color color) { + super(session, uuid, entityId, color); + } + + @Override + public void setData(WaypointData data) { + ChunkWaypointData chunk = (ChunkWaypointData) data; + // Set position in centre of chunk + position = Vector3f.from(chunk.chunkX() * 16.0F + 8.0F, session.getPlayerEntity().position().getY(), chunk.chunkZ() * 16.0F + 8.0F); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/waypoint/CoordinatesWaypoint.java b/core/src/main/java/org/geysermc/geyser/session/cache/waypoint/CoordinatesWaypoint.java new file mode 100644 index 000000000..946f86f5f --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/session/cache/waypoint/CoordinatesWaypoint.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2025 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.session.cache.waypoint; + +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.level.waypoint.Vec3iWaypointData; +import org.geysermc.mcprotocollib.protocol.data.game.level.waypoint.WaypointData; + +import java.awt.Color; +import java.util.Optional; +import java.util.OptionalLong; +import java.util.UUID; + +public class CoordinatesWaypoint extends GeyserWaypoint { + + public CoordinatesWaypoint(GeyserSession session, Optional uuid, OptionalLong entityId, Color color) { + super(session, uuid, entityId, color); + } + + @Override + public void setData(WaypointData data) { + position = ((Vec3iWaypointData) data).vector().toFloat(); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/waypoint/GeyserWaypoint.java b/core/src/main/java/org/geysermc/geyser/session/cache/waypoint/GeyserWaypoint.java new file mode 100644 index 000000000..484ba5c21 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/session/cache/waypoint/GeyserWaypoint.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2025 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.session.cache.waypoint; + +import lombok.Getter; +import lombok.experimental.Accessors; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.cloudburstmc.math.vector.Vector3f; +import org.cloudburstmc.protocol.bedrock.packet.PlayerListPacket; +import org.cloudburstmc.protocol.bedrock.packet.PlayerLocationPacket; +import org.geysermc.geyser.entity.type.player.PlayerEntity; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.mcprotocollib.protocol.data.game.level.waypoint.TrackedWaypoint; +import org.geysermc.mcprotocollib.protocol.data.game.level.waypoint.WaypointData; + +import java.awt.Color; +import java.util.Optional; +import java.util.OptionalLong; +import java.util.UUID; + +@Accessors(fluent = true) +public abstract class GeyserWaypoint { + protected final GeyserSession session; + + @Getter + private final Color color; + private final UUID entityUuid; + private long entityId; + private boolean sendListPackets; + + protected Vector3f position = Vector3f.ZERO; + private Vector3f lastSent = null; + + public GeyserWaypoint(GeyserSession session, Optional uuid, OptionalLong entityId, Color color) { + this.session = session; + this.color = color; + + this.entityUuid = uuid.orElseGet(UUID::randomUUID); + this.entityId = entityId.orElseGet(() -> session.getEntityCache().getNextEntityId().incrementAndGet()); + this.sendListPackets = entityId.isEmpty(); + } + + public void track(WaypointData data) { + sendListPackets(PlayerListPacket.Action.ADD); + update(data); + } + + public void update(WaypointData data) { + setData(data); + sendLocationPacket(false); + } + + public void untrack() { + PlayerLocationPacket packet = new PlayerLocationPacket(); + packet.setType(PlayerLocationPacket.Type.HIDE); + packet.setTargetEntityId(entityId); + session.sendUpstreamPacket(packet); + sendListPackets(PlayerListPacket.Action.REMOVE); + } + + public void setPlayer(PlayerEntity entity) { + if (sendListPackets) { + untrack(); + entityId = entity.getGeyserId(); + sendListPackets = false; + sendLocationPacket(true); + } + } + + protected void sendLocationPacket(boolean force) { + if (force || lastSent == null || position.distanceSquared(lastSent) > 1.0F) { + PlayerLocationPacket packet = new PlayerLocationPacket(); + packet.setType(PlayerLocationPacket.Type.COORDINATES); + packet.setTargetEntityId(entityId); + packet.setPosition(position); + session.sendUpstreamPacket(packet); + + lastSent = position; + } + } + + private void sendListPackets(PlayerListPacket.Action action) { + if (sendListPackets) { + PlayerListPacket packet = new PlayerListPacket(); + packet.setAction(action); + + PlayerListPacket.Entry entry = new PlayerListPacket.Entry(entityUuid); + entry.setEntityId(entityId); + entry.setColor(color); + packet.getEntries().add(entry); + + session.sendUpstreamPacket(packet); + } + } + + public abstract void setData(WaypointData data); + + public static @Nullable GeyserWaypoint create(GeyserSession session, Optional uuid, OptionalLong entityId, TrackedWaypoint waypoint) { + Color color = getWaypointColor(waypoint); + return switch (waypoint.type()) { + case EMPTY -> null; + case VEC3I -> new CoordinatesWaypoint(session, uuid, entityId, color); + case CHUNK -> new ChunkWaypoint(session, uuid, entityId, color); + case AZIMUTH -> new AzimuthWaypoint(session, uuid, entityId, color); + }; + } + + private static Color getWaypointColor(TrackedWaypoint waypoint) { + // Use icon's colour, or calculate from UUID/ID if it is not specified + // This is similar to how Java does it, but they do some brightness modifications too, which is a lot of math (see LocatorBarRenderer) + return waypoint.icon().color() + .or(() -> Optional.ofNullable(waypoint.uuid()).map(UUID::hashCode)) + .or(() -> Optional.ofNullable(waypoint.id()).map(String::hashCode)) + .map(i -> new Color(i & 0xFFFFFF)) + .orElseThrow(); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/waypoint/TickingWaypoint.java b/core/src/main/java/org/geysermc/geyser/session/cache/waypoint/TickingWaypoint.java new file mode 100644 index 000000000..75d3b32f1 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/session/cache/waypoint/TickingWaypoint.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2025 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.session.cache.waypoint; + +public interface TickingWaypoint { + + void tick(); +} diff --git a/core/src/main/java/org/geysermc/geyser/session/cache/waypoint/WaypointCache.java b/core/src/main/java/org/geysermc/geyser/session/cache/waypoint/WaypointCache.java new file mode 100644 index 000000000..1b8b8e999 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/session/cache/waypoint/WaypointCache.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2025 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.session.cache.waypoint; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import org.cloudburstmc.protocol.bedrock.packet.PlayerListPacket; +import org.geysermc.geyser.entity.type.player.PlayerEntity; +import org.geysermc.geyser.session.GeyserSession; +import org.geysermc.geyser.skin.SkinManager; +import org.geysermc.mcprotocollib.protocol.data.game.level.waypoint.TrackedWaypoint; +import org.geysermc.mcprotocollib.protocol.data.game.level.waypoint.WaypointOperation; +import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.level.ClientboundTrackedWaypointPacket; + +import java.awt.Color; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalLong; +import java.util.UUID; + +public final class WaypointCache { + private final GeyserSession session; + private final Map waypoints = new Object2ObjectOpenHashMap<>(); + private final Map waypointColors = new Object2ObjectOpenHashMap<>(); + + public WaypointCache(GeyserSession session) { + this.session = session; + } + + public void handlePacket(ClientboundTrackedWaypointPacket packet) { + switch (packet.getOperation()) { + case TRACK -> track(packet.getWaypoint()); + case UNTRACK -> untrack(packet.getWaypoint()); + case UPDATE -> update(packet.getWaypoint()); + } + + if (packet.getOperation() == WaypointOperation.TRACK || packet.getOperation()== WaypointOperation.UNTRACK) { + // Only show locator bar when there are waypoints on it + // This is equivalent to Java, and the Java locatorBar game rule won't work otherwise + session.sendGameRule("locatorBar", !waypoints.isEmpty()); + } + } + + public void trackPlayer(PlayerEntity player) { + GeyserWaypoint waypoint = waypoints.get(player.getUuid().toString()); + if (waypoint != null) { + // This will remove the fake player packet previously sent to the client, + // and change the waypoint to use the player's entity ID instead. + // This is important because sometimes a waypoint is sent before player info, so a fake player packet is sent to the client + // When the player becomes listed the right colour will already be used, this is always put in the colours map, no matter if the + // player info existed or not + waypoint.setPlayer(player); + } + } + + public Optional getWaypointColor(UUID uuid) { + return Optional.ofNullable(waypointColors.get(uuid)); + } + + public void tick() { + for (GeyserWaypoint waypoint : waypoints.values()) { + if (waypoint instanceof TickingWaypoint ticking) { + ticking.tick(); + } + } + } + + private void track(TrackedWaypoint waypoint) { + untrack(waypoint); + + Optional uuid = Optional.ofNullable(waypoint.uuid()); + Optional player = session.getEntityCache().getAllPlayerEntities().stream() + .filter(entity -> entity.getUuid().equals(waypoint.uuid())) + .findFirst(); + OptionalLong playerId = player.stream().mapToLong(PlayerEntity::getGeyserId).findFirst(); + + GeyserWaypoint tracked = GeyserWaypoint.create(session, uuid, playerId, waypoint); + if (tracked != null) { + uuid.ifPresent(id -> waypointColors.put(id, tracked.color())); + // Resend player entry with new waypoint colour + player.ifPresent(this::updatePlayerEntry); + + tracked.track(waypoint.data()); + waypoints.put(waypointId(waypoint), tracked); + } + } + + private void update(TrackedWaypoint waypoint) { + getWaypoint(waypoint).ifPresent(tracked -> tracked.update(waypoint.data())); + } + + private void untrack(TrackedWaypoint waypoint) { + getWaypoint(waypoint).ifPresent(GeyserWaypoint::untrack); + waypoints.remove(waypointId(waypoint)); + waypointColors.remove(waypoint.uuid()); + } + + private Optional getWaypoint(TrackedWaypoint waypoint) { + return Optional.ofNullable(waypoints.get(waypointId(waypoint))); + } + + private static String waypointId(TrackedWaypoint waypoint) { + return Optional.ofNullable(waypoint.uuid()) + .map(UUID::toString) + .orElse(waypoint.id()); + } + + private void updatePlayerEntry(PlayerEntity player) { + // No need to resend the entry if the player wasn't listed anyway, + // it will become listed later with the right colour + if (!player.isListed()) { + return; + } + PlayerListPacket.Entry entry = SkinManager.buildCachedEntry(session, player); + + PlayerListPacket removePacket = new PlayerListPacket(); + removePacket.setAction(PlayerListPacket.Action.REMOVE); + removePacket.getEntries().add(entry); + session.sendUpstreamPacket(removePacket); + + PlayerListPacket addPacket = new PlayerListPacket(); + addPacket.setAction(PlayerListPacket.Action.ADD); + addPacket.getEntries().add(entry); + session.sendUpstreamPacket(addPacket); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java b/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java index 3a79c59ea..798c4ba7f 100644 --- a/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java +++ b/core/src/main/java/org/geysermc/geyser/skin/SkinManager.java @@ -84,6 +84,9 @@ public class SkinManager { } } + // Default to white when waypoint colour is unknown, which is the most visible + Color color = session.getWaypointCache().getWaypointColor(playerEntity.getUuid()).orElse(Color.WHITE); + return buildEntryManually( session, playerEntity.getUuid(), @@ -91,7 +94,8 @@ public class SkinManager { playerEntity.getGeyserId(), skin, cape, - geometry + geometry, + color ); } @@ -101,7 +105,7 @@ public class SkinManager { public static PlayerListPacket.Entry buildEntryManually(GeyserSession session, UUID uuid, String username, long geyserId, Skin skin, Cape cape, - SkinGeometry geometry) { + SkinGeometry geometry, Color color) { SerializedSkin serializedSkin = getSkin(session, skin.textureUrl(), skin, cape, geometry); // This attempts to find the XUID of the player so profile images show up for Xbox accounts @@ -129,8 +133,7 @@ public class SkinManager { entry.setPlatformChatId(""); entry.setTeacher(false); entry.setTrustedSkin(true); - // Without a color set, player list entries will not show up. - entry.setColor(Color.BLACK); + entry.setColor(color); return entry; } @@ -138,6 +141,7 @@ public class SkinManager { Skin skin = skinData.skin(); Cape cape = skinData.cape(); SkinGeometry geometry = skinData.geometry(); + Color color = session.getWaypointCache().getWaypointColor(entity.getUuid()).orElse(Color.WHITE); if (entity.getUuid().equals(session.getPlayerEntity().getUuid())) { PlayerListPacket.Entry updatedEntry = buildEntryManually( @@ -147,7 +151,8 @@ public class SkinManager { entity.getGeyserId(), skin, cape, - geometry + geometry, + color ); PlayerListPacket playerAddPacket = new PlayerListPacket(); diff --git a/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java index 234cbb7a3..d7532333a 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/item/ItemTranslator.java @@ -255,7 +255,7 @@ public final class ItemTranslator { // convert the modifier tag to a lore entry String loreEntry = attributeToLore(session, entry.getAttribute(), entry.getModifier(), entry.getDisplay(), language); if (loreEntry == null) { - continue; // invalid or failed + continue; // invalid, failed, or hidden } slotsToModifiers.computeIfAbsent(entry.getSlot(), s -> new ArrayList<>()).add(loreEntry); diff --git a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateTagsTranslator.java b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateTagsTranslator.java index 4df73b4c2..3a64dde77 100644 --- a/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateTagsTranslator.java +++ b/core/src/main/java/org/geysermc/geyser/translator/protocol/java/JavaUpdateTagsTranslator.java @@ -35,6 +35,6 @@ public class JavaUpdateTagsTranslator extends PacketTranslator