3
0
Mirror von https://github.com/GeyserMC/Geyser.git synchronisiert 2024-11-19 22:40:18 +01:00

Merge branch 'master' into fix-fabric-world-manager-performance

Dieser Commit ist enthalten in:
chris 2024-10-14 15:03:17 +08:00 committet von GitHub
Commit fb634e8528
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden
GPG-Schlüssel-ID: B5690EEEBB952194
56 geänderte Dateien mit 5341 neuen und 1268 gelöschten Zeilen

Datei anzeigen

@ -61,6 +61,7 @@ dependencies {
// Test // Test
testImplementation(libs.junit) testImplementation(libs.junit)
testImplementation(libs.mockito)
// Annotation Processors // Annotation Processors
compileOnly(projects.ap) compileOnly(projects.ap)

Datei anzeigen

@ -76,6 +76,7 @@ import org.geysermc.geyser.erosion.UnixSocketClientListener;
import org.geysermc.geyser.event.GeyserEventBus; import org.geysermc.geyser.event.GeyserEventBus;
import org.geysermc.geyser.extension.GeyserExtensionManager; import org.geysermc.geyser.extension.GeyserExtensionManager;
import org.geysermc.geyser.impl.MinecraftVersionImpl; import org.geysermc.geyser.impl.MinecraftVersionImpl;
import org.geysermc.geyser.level.BedrockDimension;
import org.geysermc.geyser.level.WorldManager; import org.geysermc.geyser.level.WorldManager;
import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.network.GameProtocol;
import org.geysermc.geyser.network.netty.GeyserServer; import org.geysermc.geyser.network.netty.GeyserServer;
@ -95,7 +96,6 @@ import org.geysermc.geyser.text.MinecraftLocale;
import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.geyser.util.AssetUtils; import org.geysermc.geyser.util.AssetUtils;
import org.geysermc.geyser.util.CooldownUtils; import org.geysermc.geyser.util.CooldownUtils;
import org.geysermc.geyser.util.DimensionUtils;
import org.geysermc.geyser.util.Metrics; import org.geysermc.geyser.util.Metrics;
import org.geysermc.geyser.util.MinecraftAuthLogger; import org.geysermc.geyser.util.MinecraftAuthLogger;
import org.geysermc.geyser.util.NewsHandler; import org.geysermc.geyser.util.NewsHandler;
@ -425,7 +425,7 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
} }
CooldownUtils.setDefaultShowCooldown(config.getShowCooldown()); CooldownUtils.setDefaultShowCooldown(config.getShowCooldown());
DimensionUtils.changeBedrockNetherId(config.isAboveBedrockNetherBuilding()); // Apply End dimension ID workaround to Nether BedrockDimension.changeBedrockNetherId(config.isAboveBedrockNetherBuilding()); // Apply End dimension ID workaround to Nether
Integer bedrockThreadCount = Integer.getInteger("Geyser.BedrockNetworkThreads"); Integer bedrockThreadCount = Integer.getInteger("Geyser.BedrockNetworkThreads");
if (bedrockThreadCount == null) { if (bedrockThreadCount == null) {

Datei anzeigen

@ -25,10 +25,10 @@
package org.geysermc.geyser.entity; package org.geysermc.geyser.entity;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.MetadataType;
import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType;
import it.unimi.dsi.fastutil.objects.ObjectArrayList; import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import java.util.List;
import java.util.Locale;
import java.util.function.BiConsumer;
import lombok.Setter; import lombok.Setter;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserImpl;
@ -37,10 +37,10 @@ import org.geysermc.geyser.entity.properties.GeyserEntityProperties;
import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.translator.entity.EntityMetadataTranslator; import org.geysermc.geyser.translator.entity.EntityMetadataTranslator;
import org.geysermc.geyser.util.EnvironmentUtils;
import java.util.List; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata;
import java.util.Locale; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.MetadataType;
import java.util.function.BiConsumer; import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType;
/** /**
* Represents data for an entity. This includes properties such as height and width, as well as the list of entity * Represents data for an entity. This includes properties such as height and width, as well as the list of entity
@ -146,8 +146,13 @@ public record EntityDefinition<T extends Entity>(EntityFactory<T> factory, Entit
return this; return this;
} }
/**
* Build the given entity. If a testing environment has been discovered the entity is not registered,
* otherwise it is. This is to prevent all the registries from loading, which will fail (and should
* not be loaded) while testing
*/
public EntityDefinition<T> build() { public EntityDefinition<T> build() {
return build(true); return build(!EnvironmentUtils.isUnitTesting);
} }
/** /**

Datei anzeigen

@ -25,34 +25,131 @@
package org.geysermc.geyser.entity; package org.geysermc.geyser.entity;
import org.geysermc.geyser.entity.type.AbstractWindChargeEntity;
import org.geysermc.geyser.entity.factory.EntityFactory;
import org.geysermc.geyser.entity.type.living.monster.raid.RavagerEntity;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.MetadataType;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.FloatEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType;
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.geysermc.geyser.entity.factory.EntityFactory;
import org.geysermc.geyser.entity.properties.GeyserEntityProperties; import org.geysermc.geyser.entity.properties.GeyserEntityProperties;
import org.geysermc.geyser.entity.type.*; import org.geysermc.geyser.entity.type.AbstractArrowEntity;
import org.geysermc.geyser.entity.type.living.*; import org.geysermc.geyser.entity.type.AbstractWindChargeEntity;
import org.geysermc.geyser.entity.type.living.animal.*; import org.geysermc.geyser.entity.type.AreaEffectCloudEntity;
import org.geysermc.geyser.entity.type.living.animal.horse.*; import org.geysermc.geyser.entity.type.ArrowEntity;
import org.geysermc.geyser.entity.type.BoatEntity;
import org.geysermc.geyser.entity.type.ChestBoatEntity;
import org.geysermc.geyser.entity.type.CommandBlockMinecartEntity;
import org.geysermc.geyser.entity.type.DisplayBaseEntity;
import org.geysermc.geyser.entity.type.EnderCrystalEntity;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.EvokerFangsEntity;
import org.geysermc.geyser.entity.type.ExpOrbEntity;
import org.geysermc.geyser.entity.type.FallingBlockEntity;
import org.geysermc.geyser.entity.type.FireballEntity;
import org.geysermc.geyser.entity.type.FireworkEntity;
import org.geysermc.geyser.entity.type.FishingHookEntity;
import org.geysermc.geyser.entity.type.FurnaceMinecartEntity;
import org.geysermc.geyser.entity.type.InteractionEntity;
import org.geysermc.geyser.entity.type.ItemEntity;
import org.geysermc.geyser.entity.type.ItemFrameEntity;
import org.geysermc.geyser.entity.type.LeashKnotEntity;
import org.geysermc.geyser.entity.type.LightningEntity;
import org.geysermc.geyser.entity.type.LivingEntity;
import org.geysermc.geyser.entity.type.MinecartEntity;
import org.geysermc.geyser.entity.type.PaintingEntity;
import org.geysermc.geyser.entity.type.SpawnerMinecartEntity;
import org.geysermc.geyser.entity.type.TNTEntity;
import org.geysermc.geyser.entity.type.TextDisplayEntity;
import org.geysermc.geyser.entity.type.ThrowableEntity;
import org.geysermc.geyser.entity.type.ThrowableItemEntity;
import org.geysermc.geyser.entity.type.ThrownPotionEntity;
import org.geysermc.geyser.entity.type.TridentEntity;
import org.geysermc.geyser.entity.type.WitherSkullEntity;
import org.geysermc.geyser.entity.type.living.AbstractFishEntity;
import org.geysermc.geyser.entity.type.living.AgeableEntity;
import org.geysermc.geyser.entity.type.living.AllayEntity;
import org.geysermc.geyser.entity.type.living.ArmorStandEntity;
import org.geysermc.geyser.entity.type.living.BatEntity;
import org.geysermc.geyser.entity.type.living.DolphinEntity;
import org.geysermc.geyser.entity.type.living.GlowSquidEntity;
import org.geysermc.geyser.entity.type.living.IronGolemEntity;
import org.geysermc.geyser.entity.type.living.MagmaCubeEntity;
import org.geysermc.geyser.entity.type.living.MobEntity;
import org.geysermc.geyser.entity.type.living.SlimeEntity;
import org.geysermc.geyser.entity.type.living.SnowGolemEntity;
import org.geysermc.geyser.entity.type.living.SquidEntity;
import org.geysermc.geyser.entity.type.living.TadpoleEntity;
import org.geysermc.geyser.entity.type.living.animal.ArmadilloEntity;
import org.geysermc.geyser.entity.type.living.animal.AxolotlEntity;
import org.geysermc.geyser.entity.type.living.animal.BeeEntity;
import org.geysermc.geyser.entity.type.living.animal.ChickenEntity;
import org.geysermc.geyser.entity.type.living.animal.CowEntity;
import org.geysermc.geyser.entity.type.living.animal.FoxEntity;
import org.geysermc.geyser.entity.type.living.animal.FrogEntity;
import org.geysermc.geyser.entity.type.living.animal.GoatEntity;
import org.geysermc.geyser.entity.type.living.animal.HoglinEntity;
import org.geysermc.geyser.entity.type.living.animal.MooshroomEntity;
import org.geysermc.geyser.entity.type.living.animal.OcelotEntity;
import org.geysermc.geyser.entity.type.living.animal.PandaEntity;
import org.geysermc.geyser.entity.type.living.animal.PigEntity;
import org.geysermc.geyser.entity.type.living.animal.PolarBearEntity;
import org.geysermc.geyser.entity.type.living.animal.PufferFishEntity;
import org.geysermc.geyser.entity.type.living.animal.RabbitEntity;
import org.geysermc.geyser.entity.type.living.animal.SheepEntity;
import org.geysermc.geyser.entity.type.living.animal.SnifferEntity;
import org.geysermc.geyser.entity.type.living.animal.StriderEntity;
import org.geysermc.geyser.entity.type.living.animal.TropicalFishEntity;
import org.geysermc.geyser.entity.type.living.animal.TurtleEntity;
import org.geysermc.geyser.entity.type.living.animal.horse.AbstractHorseEntity;
import org.geysermc.geyser.entity.type.living.animal.horse.CamelEntity;
import org.geysermc.geyser.entity.type.living.animal.horse.ChestedHorseEntity;
import org.geysermc.geyser.entity.type.living.animal.horse.HorseEntity;
import org.geysermc.geyser.entity.type.living.animal.horse.LlamaEntity;
import org.geysermc.geyser.entity.type.living.animal.horse.SkeletonHorseEntity;
import org.geysermc.geyser.entity.type.living.animal.horse.TraderLlamaEntity;
import org.geysermc.geyser.entity.type.living.animal.horse.ZombieHorseEntity;
import org.geysermc.geyser.entity.type.living.animal.tameable.CatEntity; import org.geysermc.geyser.entity.type.living.animal.tameable.CatEntity;
import org.geysermc.geyser.entity.type.living.animal.tameable.ParrotEntity; import org.geysermc.geyser.entity.type.living.animal.tameable.ParrotEntity;
import org.geysermc.geyser.entity.type.living.animal.tameable.TameableEntity; import org.geysermc.geyser.entity.type.living.animal.tameable.TameableEntity;
import org.geysermc.geyser.entity.type.living.animal.tameable.WolfEntity; import org.geysermc.geyser.entity.type.living.animal.tameable.WolfEntity;
import org.geysermc.geyser.entity.type.living.merchant.AbstractMerchantEntity; import org.geysermc.geyser.entity.type.living.merchant.AbstractMerchantEntity;
import org.geysermc.geyser.entity.type.living.merchant.VillagerEntity; import org.geysermc.geyser.entity.type.living.merchant.VillagerEntity;
import org.geysermc.geyser.entity.type.living.monster.*; import org.geysermc.geyser.entity.type.living.monster.AbstractSkeletonEntity;
import org.geysermc.geyser.entity.type.living.monster.BasePiglinEntity;
import org.geysermc.geyser.entity.type.living.monster.BlazeEntity;
import org.geysermc.geyser.entity.type.living.monster.BoggedEntity;
import org.geysermc.geyser.entity.type.living.monster.BreezeEntity;
import org.geysermc.geyser.entity.type.living.monster.CreeperEntity;
import org.geysermc.geyser.entity.type.living.monster.ElderGuardianEntity;
import org.geysermc.geyser.entity.type.living.monster.EnderDragonEntity;
import org.geysermc.geyser.entity.type.living.monster.EnderDragonPartEntity;
import org.geysermc.geyser.entity.type.living.monster.EndermanEntity;
import org.geysermc.geyser.entity.type.living.monster.GhastEntity;
import org.geysermc.geyser.entity.type.living.monster.GiantEntity;
import org.geysermc.geyser.entity.type.living.monster.GuardianEntity;
import org.geysermc.geyser.entity.type.living.monster.MonsterEntity;
import org.geysermc.geyser.entity.type.living.monster.PhantomEntity;
import org.geysermc.geyser.entity.type.living.monster.PiglinEntity;
import org.geysermc.geyser.entity.type.living.monster.ShulkerEntity;
import org.geysermc.geyser.entity.type.living.monster.SkeletonEntity;
import org.geysermc.geyser.entity.type.living.monster.SpiderEntity;
import org.geysermc.geyser.entity.type.living.monster.VexEntity;
import org.geysermc.geyser.entity.type.living.monster.WardenEntity;
import org.geysermc.geyser.entity.type.living.monster.WitherEntity;
import org.geysermc.geyser.entity.type.living.monster.ZoglinEntity;
import org.geysermc.geyser.entity.type.living.monster.ZombieEntity;
import org.geysermc.geyser.entity.type.living.monster.ZombieVillagerEntity;
import org.geysermc.geyser.entity.type.living.monster.ZombifiedPiglinEntity;
import org.geysermc.geyser.entity.type.living.monster.raid.PillagerEntity; import org.geysermc.geyser.entity.type.living.monster.raid.PillagerEntity;
import org.geysermc.geyser.entity.type.living.monster.raid.RaidParticipantEntity; import org.geysermc.geyser.entity.type.living.monster.raid.RaidParticipantEntity;
import org.geysermc.geyser.entity.type.living.monster.raid.RavagerEntity;
import org.geysermc.geyser.entity.type.living.monster.raid.SpellcasterIllagerEntity; import org.geysermc.geyser.entity.type.living.monster.raid.SpellcasterIllagerEntity;
import org.geysermc.geyser.entity.type.living.monster.raid.VindicatorEntity; import org.geysermc.geyser.entity.type.living.monster.raid.VindicatorEntity;
import org.geysermc.geyser.entity.type.player.PlayerEntity; import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.registry.Registries; import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.geyser.util.EnvironmentUtils;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.MetadataType;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.FloatEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType;
public final class EntityDefinitions { public final class EntityDefinitions {
public static final EntityDefinition<AllayEntity> ALLAY; public static final EntityDefinition<AllayEntity> ALLAY;
@ -1025,7 +1122,10 @@ public final class EntityDefinitions {
.identifier("minecraft:armor_stand") // Emulated .identifier("minecraft:armor_stand") // Emulated
.build(false); // Never sent over the network .build(false); // Never sent over the network
Registries.JAVA_ENTITY_IDENTIFIERS.get().put("minecraft:marker", null); // We don't need an entity definition for this as it is never sent over the network // causes the registries to load
if (!EnvironmentUtils.isUnitTesting) {
Registries.JAVA_ENTITY_IDENTIFIERS.get().put("minecraft:marker", null); // We don't need an entity definition for this as it is never sent over the network
}
} }
public static void init() { public static void init() {

Datei anzeigen

@ -25,6 +25,12 @@
package org.geysermc.geyser.entity.type; package org.geysermc.geyser.entity.type;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@ -35,12 +41,18 @@ import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType; import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType;
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.AddEntityPacket;
import org.cloudburstmc.protocol.bedrock.packet.EntityEventPacket;
import org.cloudburstmc.protocol.bedrock.packet.MoveEntityAbsolutePacket;
import org.cloudburstmc.protocol.bedrock.packet.MoveEntityDeltaPacket;
import org.cloudburstmc.protocol.bedrock.packet.RemoveEntityPacket;
import org.cloudburstmc.protocol.bedrock.packet.SetEntityDataPacket;
import org.geysermc.geyser.api.entity.type.GeyserEntity; import org.geysermc.geyser.api.entity.type.GeyserEntity;
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.properties.GeyserEntityPropertyManager; import org.geysermc.geyser.entity.properties.GeyserEntityPropertyManager;
import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.scoreboard.Team;
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;
@ -55,12 +67,9 @@ import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEnt
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand;
import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType;
import java.util.*;
@Getter @Getter
@Setter @Setter
public class Entity implements GeyserEntity { public class Entity implements GeyserEntity {
private static final boolean PRINT_ENTITY_SPAWN_DEBUG = Boolean.parseBoolean(System.getProperty("Geyser.PrintEntitySpawnDebug", "false")); private static final boolean PRINT_ENTITY_SPAWN_DEBUG = Boolean.parseBoolean(System.getProperty("Geyser.PrintEntitySpawnDebug", "false"));
protected final GeyserSession session; protected final GeyserSession session;
@ -68,6 +77,12 @@ public class Entity implements GeyserEntity {
protected int entityId; protected int entityId;
protected final long geyserId; protected final long geyserId;
protected UUID uuid; protected UUID uuid;
/**
* Do not call this setter directly!
* This will bypass the scoreboard and setting the metadata
*/
@Setter(AccessLevel.NONE)
protected String nametag = "";
protected Vector3f position; protected Vector3f position;
protected Vector3f motion; protected Vector3f motion;
@ -97,7 +112,7 @@ public class Entity implements GeyserEntity {
@Setter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
private float boundingBoxWidth; private float boundingBoxWidth;
@Setter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
protected String nametag = ""; private String displayName;
@Setter(AccessLevel.NONE) @Setter(AccessLevel.NONE)
protected boolean silent = false; protected boolean silent = false;
/* Metadata end */ /* Metadata end */
@ -126,11 +141,12 @@ public class Entity implements GeyserEntity {
public Entity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { public Entity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
this.session = session; this.session = session;
this.definition = definition;
this.displayName = standardDisplayName();
this.entityId = entityId; this.entityId = entityId;
this.geyserId = geyserId; this.geyserId = geyserId;
this.uuid = uuid; this.uuid = uuid;
this.definition = definition;
this.motion = motion; this.motion = motion;
this.yaw = yaw; this.yaw = yaw;
this.pitch = pitch; this.pitch = pitch;
@ -341,7 +357,7 @@ public class Entity implements GeyserEntity {
* Sends the Bedrock metadata to the client * Sends the Bedrock metadata to the client
*/ */
public void updateBedrockMetadata() { public void updateBedrockMetadata() {
if (!valid) { if (!isValid()) {
return; return;
} }
@ -410,17 +426,84 @@ public class Entity implements GeyserEntity {
return 300; return 300;
} }
public String teamIdentifier() {
// experience orbs are the only known entities that do not send an uuid (even though they do have one),
// but to be safe in the future it's done in the entity class itself instead of the entity specific one.
// All entities without an uuid cannot show up in the scoreboard!
return uuid != null ? uuid.toString() : null;
}
public void setDisplayName(EntityMetadata<Optional<Component>, ?> entityMetadata) { public void setDisplayName(EntityMetadata<Optional<Component>, ?> entityMetadata) {
// displayName is shown when always display name is enabled. Either with or without team.
// That's why there are both a displayName and a nametag variable.
// Displayname is ignored for players, and is always their username.
Optional<Component> name = entityMetadata.getValue(); Optional<Component> name = entityMetadata.getValue();
if (name.isPresent()) { if (name.isPresent()) {
nametag = MessageTranslator.convertMessage(name.get(), session.locale()); String displayName = MessageTranslator.convertMessage(name.get(), session.locale());
dirtyMetadata.put(EntityDataTypes.NAME, nametag); this.displayName = displayName;
} else if (!nametag.isEmpty()) { setNametag(displayName, true);
// Clear nametag return;
dirtyMetadata.put(EntityDataTypes.NAME, "");
} }
// if no displayName is set, use entity name (ENDER_DRAGON -> Ender Dragon)
// maybe we can/should use a translatable here instead?
this.displayName = standardDisplayName();
setNametag(null, true);
} }
protected String standardDisplayName() {
return EntityUtils.translatedEntityName(definition.entityType(), session);
}
protected void setNametag(@Nullable String nametag, boolean fromDisplayName) {
// ensure that the team format is used when nametag changes
if (nametag != null && fromDisplayName) {
var team = session.getWorldCache().getScoreboard().getTeamFor(teamIdentifier());
if (team != null) {
updateNametag(team);
return;
}
}
if (nametag == null) {
nametag = "";
}
boolean changed = !Objects.equals(this.nametag, nametag);
this.nametag = nametag;
// we only update metadata if the value has changed
if (!changed) {
return;
}
dirtyMetadata.put(EntityDataTypes.NAME, nametag);
// if nametag (player with team) is hidden for player, so should the score (belowname)
scoreVisibility(!nametag.isEmpty());
}
public void updateNametag(@Nullable Team team) {
// allow LivingEntity+ to have a different visibility check
updateNametag(team, true);
}
protected void updateNametag(@Nullable Team team, boolean visible) {
if (team != null) {
String newNametag;
// (team) visibility is LivingEntity+, team displayName is Entity+
if (visible) {
newNametag = team.displayName(getDisplayName());
} else {
// The name is not visible to the session player; clear name
newNametag = "";
}
setNametag(newNametag, false);
return;
}
// The name has reset, if it was previously something else
setNametag(null, false);
}
protected void scoreVisibility(boolean show) {}
public void setDisplayNameVisible(BooleanEntityMetadata entityMetadata) { public void setDisplayNameVisible(BooleanEntityMetadata entityMetadata) {
dirtyMetadata.put(EntityDataTypes.NAMETAG_ALWAYS_SHOW, (byte) (entityMetadata.getPrimitiveValue() ? 1 : 0)); dirtyMetadata.put(EntityDataTypes.NAMETAG_ALWAYS_SHOW, (byte) (entityMetadata.getPrimitiveValue() ? 1 : 0));
} }

Datei anzeigen

@ -25,6 +25,11 @@
package org.geysermc.geyser.entity.type; package org.geysermc.geyser.entity.type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@ -45,6 +50,7 @@ import org.geysermc.geyser.entity.vehicle.ClientVehicle;
import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.registry.type.ItemMapping; import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.scoreboard.Team;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.item.ItemTranslator; import org.geysermc.geyser.translator.item.ItemTranslator;
import org.geysermc.geyser.util.AttributeUtils; import org.geysermc.geyser.util.AttributeUtils;
@ -65,12 +71,9 @@ import org.geysermc.mcprotocollib.protocol.data.game.level.particle.EntityEffect
import org.geysermc.mcprotocollib.protocol.data.game.level.particle.Particle; import org.geysermc.mcprotocollib.protocol.data.game.level.particle.Particle;
import org.geysermc.mcprotocollib.protocol.data.game.level.particle.ParticleType; import org.geysermc.mcprotocollib.protocol.data.game.level.particle.ParticleType;
import java.util.*;
@Getter @Getter
@Setter @Setter
public class LivingEntity extends Entity { public class LivingEntity extends Entity {
protected ItemData helmet = ItemData.AIR; protected ItemData helmet = ItemData.AIR;
protected ItemData chestplate = ItemData.AIR; protected ItemData chestplate = ItemData.AIR;
protected ItemData leggings = ItemData.AIR; protected ItemData leggings = ItemData.AIR;
@ -150,6 +153,16 @@ public class LivingEntity extends Entity {
dirtyMetadata.put(EntityDataTypes.STRUCTURAL_INTEGRITY, 1); dirtyMetadata.put(EntityDataTypes.STRUCTURAL_INTEGRITY, 1);
} }
@Override
public void updateNametag(@Nullable Team team) {
// if name not visible, don't mark it as visible
updateNametag(team, team == null || team.isVisibleFor(session.getPlayerEntity().getUsername()));
}
public void hideNametag() {
setNametag("", false);
}
public void setLivingEntityFlags(ByteEntityMetadata entityMetadata) { public void setLivingEntityFlags(ByteEntityMetadata entityMetadata) {
byte xd = entityMetadata.getPrimitiveValue(); byte xd = entityMetadata.getPrimitiveValue();

Datei anzeigen

@ -25,6 +25,7 @@
package org.geysermc.geyser.entity.type.living.animal; package org.geysermc.geyser.entity.type.living.animal;
import net.kyori.adventure.key.Key;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
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;
@ -32,11 +33,13 @@ import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.geysermc.geyser.entity.EntityDefinition; import org.geysermc.geyser.entity.EntityDefinition;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.tags.ItemTag; import org.geysermc.geyser.session.cache.tags.ItemTag;
import org.geysermc.geyser.util.EntityUtils;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.IntEntityMetadata;
import java.util.UUID; import java.util.UUID;
public class RabbitEntity extends AnimalEntity { public class RabbitEntity extends AnimalEntity {
private boolean isKillerBunny;
public RabbitEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) { public RabbitEntity(GeyserSession session, int entityId, long geyserId, UUID uuid, EntityDefinition<?> definition, Vector3f position, Vector3f motion, float yaw, float pitch, float headYaw) {
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw); super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
@ -46,7 +49,7 @@ public class RabbitEntity extends AnimalEntity {
int variant = entityMetadata.getPrimitiveValue(); int variant = entityMetadata.getPrimitiveValue();
// Change the killer bunny to display as white since it only exists on Java Edition // Change the killer bunny to display as white since it only exists on Java Edition
boolean isKillerBunny = variant == 99; isKillerBunny = variant == 99;
if (isKillerBunny) { if (isKillerBunny) {
variant = 1; variant = 1;
} }
@ -56,6 +59,14 @@ public class RabbitEntity extends AnimalEntity {
dirtyMetadata.put(EntityDataTypes.VARIANT, variant); dirtyMetadata.put(EntityDataTypes.VARIANT, variant);
} }
@Override
protected String standardDisplayName() {
if (isKillerBunny) {
return EntityUtils.translatedEntityName(Key.key("killer_bunny"), session);
}
return super.standardDisplayName();
}
@Override @Override
protected float getAdultSize() { protected float getAdultSize() {
return 0.55f; return 0.55f;
@ -71,4 +82,4 @@ public class RabbitEntity extends AnimalEntity {
protected ItemTag getFoodTag() { protected ItemTag getFoodTag() {
return ItemTag.RABBIT_FOOD; return ItemTag.RABBIT_FOOD;
} }
} }

Datei anzeigen

@ -55,6 +55,17 @@ public class TurtleEntity extends AnimalEntity {
return ItemTag.TURTLE_FOOD; return ItemTag.TURTLE_FOOD;
} }
@Override
protected float getAdultSize() {
return super.getAdultSize() * 0.7f;
}
@Override
protected float getBabySize() {
// 0.3f is Java scale, plus Bedrock difference
return 0.3f * 0.5f;
}
@Override @Override
public boolean canBeLeashed() { public boolean canBeLeashed() {
return false; return false;

Datei anzeigen

@ -25,6 +25,12 @@
package org.geysermc.geyser.entity.type.player; package org.geysermc.geyser.entity.type.player;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
@ -32,19 +38,18 @@ import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.nbt.NbtMap; import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtMapBuilder;
import org.cloudburstmc.protocol.bedrock.data.Ability; import org.cloudburstmc.protocol.bedrock.data.Ability;
import org.cloudburstmc.protocol.bedrock.data.AbilityLayer; import org.cloudburstmc.protocol.bedrock.data.AbilityLayer;
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.EntityDataMap;
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.data.entity.EntityLinkData; import org.cloudburstmc.protocol.bedrock.data.entity.EntityLinkData;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
import org.cloudburstmc.protocol.bedrock.packet.AddPlayerPacket; import org.cloudburstmc.protocol.bedrock.packet.AddPlayerPacket;
import org.cloudburstmc.protocol.bedrock.packet.MovePlayerPacket; import org.cloudburstmc.protocol.bedrock.packet.MovePlayerPacket;
import org.cloudburstmc.protocol.bedrock.packet.SetEntityDataPacket;
import org.cloudburstmc.protocol.bedrock.packet.SetEntityLinkPacket; import org.cloudburstmc.protocol.bedrock.packet.SetEntityLinkPacket;
import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket; import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket;
import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity; import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity;
@ -53,32 +58,13 @@ import org.geysermc.geyser.entity.attribute.GeyserAttributeType;
import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.LivingEntity; import org.geysermc.geyser.entity.type.LivingEntity;
import org.geysermc.geyser.entity.type.living.animal.tameable.ParrotEntity; import org.geysermc.geyser.entity.type.living.animal.tameable.ParrotEntity;
import org.geysermc.geyser.scoreboard.Objective;
import org.geysermc.geyser.scoreboard.Score;
import org.geysermc.geyser.scoreboard.Team;
import org.geysermc.geyser.scoreboard.UpdateType;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.geyser.util.ChunkUtils; import org.geysermc.geyser.util.ChunkUtils;
import org.geysermc.mcprotocollib.protocol.codec.NbtComponentSerializer;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.BlankFormat;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.FixedFormat;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.NumberFormat;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.StyledFormat;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.FloatEntityMetadata; import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.FloatEntityMetadata;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Getter @Setter @Getter @Setter
public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity { public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
@ -96,6 +82,9 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
private String username; private String username;
private String cachedScore = "";
private boolean scoreVisible = true;
/** /**
* The textures property from the GameProfile. * The textures property from the GameProfile.
*/ */
@ -123,6 +112,20 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
this.texturesProperty = texturesProperty; this.texturesProperty = texturesProperty;
} }
/**
* Do not use! For testing purposes only
*/
public PlayerEntity(GeyserSession session, long geyserId, UUID uuid, String username) {
super(session, -1, geyserId, uuid, EntityDefinitions.PLAYER, Vector3f.ZERO, Vector3f.ZERO, 0, 0, 0);
this.username = username;
this.nametag = username;
this.texturesProperty = null;
// clear initial metadata
dirtyMetadata.apply(new EntityDataMap());
setFlagsDirty(false);
}
@Override @Override
protected void initializeMetadata() { protected void initializeMetadata() {
super.initializeMetadata(); super.initializeMetadata();
@ -132,17 +135,6 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
@Override @Override
public void spawnEntity() { public void spawnEntity() {
// Check to see if the player should have a belowname counterpart added
Objective objective = session.getWorldCache().getScoreboard().getObjectiveSlots().get(ScoreboardPosition.BELOW_NAME);
if (objective != null) {
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
updateDisplayName(session.getWorldCache().getScoreboard().getTeamFor(username));
AddPlayerPacket addPlayerPacket = new AddPlayerPacket(); AddPlayerPacket addPlayerPacket = new AddPlayerPacket();
addPlayerPacket.setUuid(uuid); addPlayerPacket.setUuid(uuid);
addPlayerPacket.setUsername(username); addPlayerPacket.setUsername(username);
@ -177,6 +169,7 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
// Since we re-use player entities: Clear flags, held item, etc // Since we re-use player entities: Clear flags, held item, etc
this.resetMetadata(); this.resetMetadata();
this.nametag = username;
this.hand = ItemData.AIR; this.hand = ItemData.AIR;
this.offhand = ItemData.AIR; this.offhand = ItemData.AIR;
this.boots = ItemData.AIR; this.boots = ItemData.AIR;
@ -386,38 +379,30 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
} }
} }
@Override
public String getDisplayName() {
return username;
}
@Override @Override
public void setDisplayName(EntityMetadata<Optional<Component>, ?> entityMetadata) { public void setDisplayName(EntityMetadata<Optional<Component>, ?> entityMetadata) {
// Doesn't do anything for players // Doesn't do anything for players
} }
//todo this will become common entity logic once UUID support is implemented for them @Override
public void updateDisplayName(@Nullable Team team) { public String teamIdentifier() {
boolean needsUpdate; return username;
if (team != null) { }
String newDisplayName;
if (team.isVisibleFor(session.getPlayerEntity().getUsername())) {
TeamColor color = team.getColor();
String chatColor = MessageTranslator.toChatColor(color);
// We have to emulate what modern Java text already does for us and add the color to each section
String prefix = team.getCurrentData().getPrefix();
String suffix = team.getCurrentData().getSuffix();
newDisplayName = chatColor + prefix + chatColor + this.username + chatColor + suffix;
} else {
// The name is not visible to the session player; clear name
newDisplayName = "";
}
needsUpdate = !newDisplayName.equals(this.nametag);
this.nametag = newDisplayName;
} else {
// The name has reset, if it was previously something else
needsUpdate = !this.nametag.equals(this.username);
this.nametag = this.username;
}
if (needsUpdate) { @Override
dirtyMetadata.put(EntityDataTypes.NAME, this.nametag); protected void setNametag(@Nullable String nametag, boolean fromDisplayName) {
// when fromDisplayName, LivingEntity will call scoreboard code. After that
// setNametag is called again with fromDisplayName on false
if (nametag == null && !fromDisplayName) {
// nametag = null means reset, so reset it back to username
nametag = username;
} }
super.setNametag(nametag, fromDisplayName);
} }
@Override @Override
@ -425,6 +410,33 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
// Doesn't do anything for players // Doesn't do anything for players
} }
public void setBelowNameText(String text) {
if (text == null) {
text = "";
}
boolean changed = !Objects.equals(cachedScore, text);
cachedScore = text;
if (isScoreVisible() && changed) {
dirtyMetadata.put(EntityDataTypes.SCORE, text);
}
}
@Override
protected void scoreVisibility(boolean show) {
boolean visibilityChanged = scoreVisible != show;
scoreVisible = show;
if (!visibilityChanged) {
return;
}
// if the player has no cachedScore, we never have to change the score.
// hide = set to "" (does nothing), show = change from "" (does nothing)
if (cachedScore.isEmpty()) {
return;
}
dirtyMetadata.put(EntityDataTypes.SCORE, show ? cachedScore : "");
}
@Override @Override
protected void setDimensions(Pose pose) { protected void setDimensions(Pose pose) {
float height; float height;
@ -451,64 +463,6 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
setBoundingBoxHeight(height); setBoundingBoxHeight(height);
} }
public void setBelowNameText(Objective objective) {
if (objective != null && objective.getUpdateType() != UpdateType.REMOVE) {
Score score = objective.getScores().get(username);
String numberString;
NumberFormat numberFormat;
int amount;
if (score != null) {
amount = score.getScore();
numberFormat = score.getNumberFormat();
if (numberFormat == null) {
numberFormat = objective.getNumberFormat();
}
} else {
amount = 0;
numberFormat = objective.getNumberFormat();
}
if (numberFormat instanceof BlankFormat) {
numberString = "";
} else if (numberFormat instanceof FixedFormat fixedFormat) {
numberString = MessageTranslator.convertMessage(fixedFormat.getValue());
} else if (numberFormat instanceof StyledFormat styledFormat) {
NbtMapBuilder styledAmount = styledFormat.getStyle().toBuilder();
styledAmount.putString("text", String.valueOf(amount));
numberString = MessageTranslator.convertJsonMessage(
NbtComponentSerializer.tagComponentToJson(styledAmount.build()).toString(), session.locale());
} else {
numberString = String.valueOf(amount);
}
String displayString = numberString + " " + ChatColor.RESET + objective.getDisplayName();
if (valid) {
// Already spawned - we still need to run the rest of this code because the spawn packet will be
// providing the information
SetEntityDataPacket packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(geyserId);
packet.getMetadata().put(EntityDataTypes.SCORE, displayString);
session.sendUpstreamPacket(packet);
} else {
// Not spawned yet, store score value in dirtyMetadata to be picked up by #spawnEntity
dirtyMetadata.put(EntityDataTypes.SCORE, displayString);
}
} else {
if (valid) {
SetEntityDataPacket packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(geyserId);
packet.getMetadata().put(EntityDataTypes.SCORE, "");
session.sendUpstreamPacket(packet);
} else {
// Not spawned yet, store score value in dirtyMetadata to be picked up by #spawnEntity
dirtyMetadata.put(EntityDataTypes.SCORE, "");
}
}
}
/** /**
* @return the UUID that should be used when dealing with Bedrock's tab list. * @return the UUID that should be used when dealing with Bedrock's tab list.
*/ */

Datei anzeigen

@ -38,6 +38,7 @@ import org.geysermc.geyser.command.GeyserCommandSource;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.incendo.cloud.CommandManager; import org.incendo.cloud.CommandManager;
import org.incendo.cloud.context.CommandContext; import org.incendo.cloud.context.CommandContext;
import org.incendo.cloud.description.CommandDescription;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -193,11 +194,17 @@ public abstract class GeyserExtensionCommand extends GeyserCommand {
.handler(this::execute)); .handler(this::execute));
} }
@Override
protected org.incendo.cloud.Command.Builder.Applicable<GeyserCommandSource> meta() {
// We don't want to localize the extension command description
return builder -> builder.commandDescription(CommandDescription.commandDescription(description));
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
public void execute(CommandContext<GeyserCommandSource> context) { public void execute(CommandContext<GeyserCommandSource> context) {
GeyserCommandSource source = context.sender(); GeyserCommandSource source = context.sender();
String[] args = context.getOrDefault("args", "").split(" "); String[] args = context.getOrDefault("args", " ").split(" ");
if (sourceType.isInstance(source)) { if (sourceType.isInstance(source)) {
executor.execute((T) source, this, args); executor.execute((T) source, this, args);

Datei anzeigen

@ -25,17 +25,84 @@
package org.geysermc.geyser.level; package org.geysermc.geyser.level;
import lombok.ToString;
/** /**
* A data structure to represent what Bedrock believes are the height requirements for a specific dimension. * A data structure to represent what Bedrock believes are the height requirements for a specific dimension.
* As of 1.18.30, biome count is representative of the height of the world, and out-of-bounds chunks can crash * As of 1.18.30, biome count is representative of the height of the world, and out-of-bounds chunks can crash
* the client. * the client.
*
* @param minY The minimum height Bedrock Edition will accept.
* @param height The maximum chunk height Bedrock Edition will accept, from the lowest point to the highest.
* @param doUpperHeightWarn whether to warn in the console if the Java dimension height exceeds Bedrock's.
*/ */
public record BedrockDimension(int minY, int height, boolean doUpperHeightWarn) { @ToString
public static final BedrockDimension OVERWORLD = new BedrockDimension(-64, 384, true); public class BedrockDimension {
public static final BedrockDimension THE_NETHER = new BedrockDimension(0, 128, false);
public static final BedrockDimension THE_END = new BedrockDimension(0, 256, true); public static final int OVERWORLD_ID = 0;
public static final int DEFAULT_NETHER_ID = 1;
public static final int END_ID = 2;
// Changes if the above-bedrock Nether building workaround is applied
public static int BEDROCK_NETHER_ID = DEFAULT_NETHER_ID;
public static final BedrockDimension OVERWORLD = new BedrockDimension(-64, 384, true, OVERWORLD_ID);
public static final BedrockDimension THE_NETHER = new BedrockDimension(0, 128, false, -1) {
@Override
public int bedrockId() {
return BEDROCK_NETHER_ID;
}
};
public static final BedrockDimension THE_END = new BedrockDimension(0, 256, true, END_ID);
public static final String NETHER_IDENTIFIER = "minecraft:the_nether";
private final int minY;
private final int height;
private final boolean doUpperHeightWarn;
private final int bedrockId;
/**
* @param minY The minimum height Bedrock Edition will accept.
* @param height The maximum chunk height Bedrock Edition will accept, from the lowest point to the highest.
* @param doUpperHeightWarn whether to warn in the console if the Java dimension height exceeds Bedrock's.
* @param bedrockId the Bedrock dimension ID of this dimension.
*/
public BedrockDimension(int minY, int height, boolean doUpperHeightWarn, int bedrockId) {
this.minY = minY;
this.height = height;
this.doUpperHeightWarn = doUpperHeightWarn;
this.bedrockId = bedrockId;
}
/**
* The Nether dimension in Bedrock does not permit building above Y128 - the Bedrock above the dimension.
* This workaround sets the Nether as the End dimension to ignore this limit.
*
* @param isAboveNetherBedrockBuilding true if we should apply The End workaround
*/
public static void changeBedrockNetherId(boolean isAboveNetherBedrockBuilding) {
// Change dimension ID to the End to allow for building above Bedrock
BEDROCK_NETHER_ID = isAboveNetherBedrockBuilding ? END_ID : DEFAULT_NETHER_ID;
}
public static boolean isCustomBedrockNetherId() {
return BEDROCK_NETHER_ID == END_ID;
}
public int maxY() {
return minY + height;
}
public int minY() {
return minY;
}
public int height() {
return height;
}
public boolean doUpperHeightWarn() {
return doUpperHeightWarn;
}
public int bedrockId() {
return bedrockId;
}
} }

Datei anzeigen

@ -63,12 +63,19 @@ public record JavaDimension(int minY, int maxY, boolean piglinSafe, boolean ultr
if ("minecraft".equals(id.namespace())) { if ("minecraft".equals(id.namespace())) {
String identifier = id.asString(); String identifier = id.asString();
bedrockId = DimensionUtils.javaToBedrock(identifier); bedrockId = DimensionUtils.javaToBedrock(identifier);
isNetherLike = DimensionUtils.NETHER_IDENTIFIER.equals(identifier); isNetherLike = BedrockDimension.NETHER_IDENTIFIER.equals(identifier);
} else { } else {
// Effects should give is a clue on how this (custom) dimension is supposed to look like // Effects should give is a clue on how this (custom) dimension is supposed to look like
String effects = dimension.getString("effects"); String effects = dimension.getString("effects");
bedrockId = DimensionUtils.javaToBedrock(effects); bedrockId = DimensionUtils.javaToBedrock(effects);
isNetherLike = DimensionUtils.NETHER_IDENTIFIER.equals(effects); isNetherLike = BedrockDimension.NETHER_IDENTIFIER.equals(effects);
}
if (minY % 16 != 0) {
throw new RuntimeException("Minimum Y must be a multiple of 16!");
}
if (maxY % 16 != 0) {
throw new RuntimeException("Maximum Y must be a multiple of 16!");
} }
return new JavaDimension(minY, maxY, piglinSafe, ultrawarm, coordinateScale, bedrockId, isNetherLike); return new JavaDimension(minY, maxY, piglinSafe, ultrawarm, coordinateScale, bedrockId, isNetherLike);

Datei anzeigen

@ -51,7 +51,7 @@ public final class GameProtocol {
* release of the game that Geyser supports. * release of the game that Geyser supports.
*/ */
public static final BedrockCodec DEFAULT_BEDROCK_CODEC = CodecProcessor.processCodec(Bedrock_v729.CODEC.toBuilder() public static final BedrockCodec DEFAULT_BEDROCK_CODEC = CodecProcessor.processCodec(Bedrock_v729.CODEC.toBuilder()
.minecraftVersion("1.21.30") .minecraftVersion("1.21.31")
.build()); .build());
/** /**
@ -78,7 +78,9 @@ public final class GameProtocol {
SUPPORTED_BEDROCK_CODECS.add(CodecProcessor.processCodec(Bedrock_v712.CODEC.toBuilder() SUPPORTED_BEDROCK_CODECS.add(CodecProcessor.processCodec(Bedrock_v712.CODEC.toBuilder()
.minecraftVersion("1.21.20 - 1.21.23") .minecraftVersion("1.21.20 - 1.21.23")
.build())); .build()));
SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC); SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC.toBuilder()
.minecraftVersion("1.21.30/1.21.31")
.build());
} }
/** /**

Datei anzeigen

@ -27,15 +27,30 @@ package org.geysermc.geyser.network.netty;
import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.*; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.DefaultEventLoopGroup;
import io.netty.channel.unix.PreferredDirectByteBufAllocator; import io.netty.channel.unix.PreferredDirectByteBufAllocator;
import io.netty.handler.codec.haproxy.*; import io.netty.handler.codec.haproxy.HAProxyCommand;
import io.netty.handler.codec.haproxy.HAProxyMessage;
import io.netty.handler.codec.haproxy.HAProxyMessageEncoder;
import io.netty.handler.codec.haproxy.HAProxyProtocolVersion;
import io.netty.handler.codec.haproxy.HAProxyProxiedProtocol;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import io.netty.util.concurrent.DefaultThreadFactory; import io.netty.util.concurrent.DefaultThreadFactory;
import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.mcprotocollib.network.BuiltinFlags; import org.geysermc.mcprotocollib.network.BuiltinFlags;
import org.geysermc.mcprotocollib.network.codec.PacketCodecHelper; import org.geysermc.mcprotocollib.network.codec.PacketCodecHelper;
import org.geysermc.mcprotocollib.network.packet.PacketProtocol; import org.geysermc.mcprotocollib.network.packet.PacketProtocol;
import org.geysermc.mcprotocollib.network.tcp.FlushHandler;
import org.geysermc.mcprotocollib.network.tcp.TcpFlowControlHandler;
import org.geysermc.mcprotocollib.network.tcp.TcpPacketCodec; import org.geysermc.mcprotocollib.network.tcp.TcpPacketCodec;
import org.geysermc.mcprotocollib.network.tcp.TcpPacketCompression;
import org.geysermc.mcprotocollib.network.tcp.TcpPacketEncryptor;
import org.geysermc.mcprotocollib.network.tcp.TcpPacketSizer; import org.geysermc.mcprotocollib.network.tcp.TcpPacketSizer;
import org.geysermc.mcprotocollib.network.tcp.TcpSession; import org.geysermc.mcprotocollib.network.tcp.TcpSession;
import org.geysermc.mcprotocollib.protocol.codec.MinecraftCodecHelper; import org.geysermc.mcprotocollib.protocol.codec.MinecraftCodecHelper;
@ -43,6 +58,7 @@ import org.geysermc.mcprotocollib.protocol.codec.MinecraftCodecHelper;
import java.net.Inet4Address; import java.net.Inet4Address;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.SocketAddress; import java.net.SocketAddress;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
@ -72,44 +88,53 @@ public final class LocalSession extends TcpSession {
if (DEFAULT_EVENT_LOOP_GROUP == null) { if (DEFAULT_EVENT_LOOP_GROUP == null) {
DEFAULT_EVENT_LOOP_GROUP = new DefaultEventLoopGroup(new DefaultThreadFactory(this.getClass(), true)); DEFAULT_EVENT_LOOP_GROUP = new DefaultEventLoopGroup(new DefaultThreadFactory(this.getClass(), true));
Runtime.getRuntime().addShutdownHook(new Thread( Runtime.getRuntime().addShutdownHook(new Thread(
() -> DEFAULT_EVENT_LOOP_GROUP.shutdownGracefully(100, 500, TimeUnit.MILLISECONDS))); () -> DEFAULT_EVENT_LOOP_GROUP.shutdownGracefully(100, 500, TimeUnit.MILLISECONDS)));
} }
try { final Bootstrap bootstrap = new Bootstrap();
final Bootstrap bootstrap = new Bootstrap(); bootstrap.channel(LocalChannelWithRemoteAddress.class);
bootstrap.channel(LocalChannelWithRemoteAddress.class); bootstrap.handler(new ChannelInitializer<LocalChannelWithRemoteAddress>() {
bootstrap.handler(new ChannelInitializer<LocalChannelWithRemoteAddress>() { @Override
@Override public void initChannel(@NonNull LocalChannelWithRemoteAddress channel) {
public void initChannel(@NonNull LocalChannelWithRemoteAddress channel) { channel.spoofedRemoteAddress(new InetSocketAddress(clientIp, 0));
channel.spoofedRemoteAddress(new InetSocketAddress(clientIp, 0)); PacketProtocol protocol = getPacketProtocol();
PacketProtocol protocol = getPacketProtocol(); protocol.newClientSession(LocalSession.this, transferring);
protocol.newClientSession(LocalSession.this, transferring);
refreshReadTimeoutHandler(channel); ChannelPipeline pipeline = channel.pipeline();
refreshWriteTimeoutHandler(channel);
ChannelPipeline pipeline = channel.pipeline(); addHAProxySupport(pipeline);
pipeline.addLast("sizer", new TcpPacketSizer(LocalSession.this, protocol.getPacketHeader().getLengthSize()));
pipeline.addLast("codec", new TcpPacketCodec(LocalSession.this, true));
pipeline.addLast("manager", LocalSession.this);
addHAProxySupport(pipeline); pipeline.addLast("read-timeout", new ReadTimeoutHandler(getFlag(BuiltinFlags.READ_TIMEOUT, 30)));
} pipeline.addLast("write-timeout", new WriteTimeoutHandler(getFlag(BuiltinFlags.WRITE_TIMEOUT, 0)));
}).group(DEFAULT_EVENT_LOOP_GROUP).option(ChannelOption.CONNECT_TIMEOUT_MILLIS, getConnectTimeout() * 1000);
if (PREFERRED_DIRECT_BYTE_BUF_ALLOCATOR != null) { pipeline.addLast("encryption", new TcpPacketEncryptor());
bootstrap.option(ChannelOption.ALLOCATOR, PREFERRED_DIRECT_BYTE_BUF_ALLOCATOR); pipeline.addLast("sizer", new TcpPacketSizer(protocol.getPacketHeader(), getCodecHelper()));
pipeline.addLast("compression", new TcpPacketCompression(getCodecHelper()));
pipeline.addLast("flow-control", new TcpFlowControlHandler());
pipeline.addLast("codec", new TcpPacketCodec(LocalSession.this, true));
pipeline.addLast("flush-handler", new FlushHandler());
pipeline.addLast("manager", LocalSession.this);
}
}).group(DEFAULT_EVENT_LOOP_GROUP).option(ChannelOption.CONNECT_TIMEOUT_MILLIS, getFlag(BuiltinFlags.CLIENT_CONNECT_TIMEOUT, 30) * 1000);
if (PREFERRED_DIRECT_BYTE_BUF_ALLOCATOR != null) {
bootstrap.option(ChannelOption.ALLOCATOR, PREFERRED_DIRECT_BYTE_BUF_ALLOCATOR);
}
bootstrap.remoteAddress(targetAddress);
CompletableFuture<Void> handleFuture = new CompletableFuture<>();
bootstrap.connect().addListener((futureListener) -> {
if (!futureListener.isSuccess()) {
exceptionCaught(null, futureListener.cause());
} }
bootstrap.remoteAddress(targetAddress); handleFuture.complete(null);
});
bootstrap.connect().addListener((future) -> { if (wait) {
if (!future.isSuccess()) { handleFuture.join();
exceptionCaught(null, future.cause());
}
});
} catch (Throwable t) {
exceptionCaught(null, t);
} }
} }
@ -121,7 +146,7 @@ public final class LocalSession extends TcpSession {
// TODO duplicate code // TODO duplicate code
private void addHAProxySupport(ChannelPipeline pipeline) { private void addHAProxySupport(ChannelPipeline pipeline) {
InetSocketAddress clientAddress = getFlag(BuiltinFlags.CLIENT_PROXIED_ADDRESS); InetSocketAddress clientAddress = getFlag(BuiltinFlags.CLIENT_PROXIED_ADDRESS);
if (getFlag(BuiltinFlags.ENABLE_CLIENT_PROXY_PROTOCOL, false) && clientAddress != null) { if (clientAddress != null) {
pipeline.addFirst("proxy-protocol-packet-sender", new ChannelInboundHandlerAdapter() { pipeline.addFirst("proxy-protocol-packet-sender", new ChannelInboundHandlerAdapter() {
@Override @Override
public void channelActive(@NonNull ChannelHandlerContext ctx) throws Exception { public void channelActive(@NonNull ChannelHandlerContext ctx) throws Exception {
@ -133,9 +158,9 @@ public final class LocalSession extends TcpSession {
remoteAddress = new InetSocketAddress(host, port); remoteAddress = new InetSocketAddress(host, port);
} }
ctx.channel().writeAndFlush(new HAProxyMessage( ctx.channel().writeAndFlush(new HAProxyMessage(
HAProxyProtocolVersion.V2, HAProxyCommand.PROXY, proxiedProtocol, HAProxyProtocolVersion.V2, HAProxyCommand.PROXY, proxiedProtocol,
clientAddress.getAddress().getHostAddress(), remoteAddress.getAddress().getHostAddress(), clientAddress.getAddress().getHostAddress(), remoteAddress.getAddress().getHostAddress(),
clientAddress.getPort(), remoteAddress.getPort() clientAddress.getPort(), remoteAddress.getPort()
)); ));
ctx.pipeline().remove(this); ctx.pipeline().remove(this);
ctx.pipeline().remove("proxy-protocol-encoder"); ctx.pipeline().remove("proxy-protocol-encoder");
@ -144,7 +169,7 @@ public final class LocalSession extends TcpSession {
}); });
pipeline.addFirst("proxy-protocol-encoder", HAProxyMessageEncoder.INSTANCE); pipeline.addFirst("proxy-protocol-encoder", HAProxyMessageEncoder.INSTANCE);
} }
} }
/** /**
* Should only be called when direct ByteBufs should be preferred. At this moment, this should only be called on BungeeCord. * Should only be called when direct ByteBufs should be preferred. At this moment, this should only be called on BungeeCord.

Datei anzeigen

@ -25,185 +25,100 @@
package org.geysermc.geyser.scoreboard; package org.geysermc.geyser.scoreboard;
import lombok.Getter; import java.util.ArrayList;
import lombok.Setter; import java.util.List;
import net.kyori.adventure.text.Component;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.NumberFormat;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import lombok.Getter;
import net.kyori.adventure.text.Component;
import org.geysermc.geyser.scoreboard.display.slot.DisplaySlot;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.NumberFormat;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreType;
@Getter @Getter
public final class Objective { public final class Objective {
private final Scoreboard scoreboard; private final Scoreboard scoreboard;
private final long id; private final List<DisplaySlot> activeSlots = new ArrayList<>();
private boolean active = true;
@Setter private final String objectiveName;
private UpdateType updateType = UpdateType.ADD; private final Map<String, ScoreReference> scores = new ConcurrentHashMap<>();
private String objectiveName; private String displayName;
private ScoreboardPosition displaySlot;
private String displaySlotName;
private String displayName = "unknown";
private NumberFormat numberFormat; private NumberFormat numberFormat;
private int type = 0; // 0 = integer, 1 = heart private ScoreType type;
private Map<String, Score> scores = new ConcurrentHashMap<>();
private Objective(Scoreboard scoreboard) {
this.id = scoreboard.getNextId().getAndIncrement();
this.scoreboard = scoreboard;
}
/**
* /!\ This method is made for temporary objectives until the real objective is received
*
* @param scoreboard the scoreboard
* @param objectiveName the name of the objective
*/
public Objective(Scoreboard scoreboard, String objectiveName) { public Objective(Scoreboard scoreboard, String objectiveName) {
this(scoreboard); this.scoreboard = scoreboard;
this.objectiveName = objectiveName; this.objectiveName = objectiveName;
this.active = false;
}
public Objective(Scoreboard scoreboard, String objectiveName, ScoreboardPosition displaySlot, String displayName, int type) {
this(scoreboard);
this.objectiveName = objectiveName;
this.displaySlot = displaySlot;
this.displaySlotName = translateDisplaySlot(displaySlot);
this.displayName = displayName;
this.type = type;
}
private static String translateDisplaySlot(ScoreboardPosition displaySlot) {
return switch (displaySlot) {
case BELOW_NAME -> "belowname";
case PLAYER_LIST -> "list";
default -> "sidebar";
};
} }
public void registerScore(String id, int score, Component displayName, NumberFormat numberFormat) { public void registerScore(String id, int score, Component displayName, NumberFormat numberFormat) {
if (!scores.containsKey(id)) { if (scores.containsKey(id)) {
long scoreId = scoreboard.getNextId().getAndIncrement(); return;
Score scoreObject = new Score(scoreId, id) }
.setScore(score) var reference = new ScoreReference(scoreboard, id, score, displayName, numberFormat);
.setTeam(scoreboard.getTeamFor(id)) scores.put(id, reference);
.setDisplayName(displayName)
.setNumberFormat(numberFormat) for (var slot : activeSlots) {
.setUpdateType(UpdateType.ADD); slot.addScore(reference);
scores.put(id, scoreObject);
} }
} }
public void setScore(String id, int score, Component displayName, NumberFormat numberFormat) { public void setScore(String id, int score, Component displayName, NumberFormat numberFormat) {
Score stored = scores.get(id); ScoreReference stored = scores.get(id);
if (stored != null) { if (stored != null) {
stored.setScore(score) stored.updateProperties(scoreboard, score, displayName, numberFormat);
.setDisplayName(displayName)
.setNumberFormat(numberFormat)
.setUpdateType(UpdateType.UPDATE);
return; return;
} }
registerScore(id, score, displayName, numberFormat); registerScore(id, score, displayName, numberFormat);
} }
public void removeScore(String id) { public void removeScore(String id) {
Score stored = scores.get(id); ScoreReference stored = scores.remove(id);
if (stored != null) { if (stored != null) {
stored.setUpdateType(UpdateType.REMOVE); stored.markDeleted();
} }
} }
/** public void updateProperties(Component displayNameComponent, ScoreType type, NumberFormat format) {
* Used internally to remove a score from the score map String displayName = MessageTranslator.convertMessageRaw(displayNameComponent, scoreboard.session().locale());
*/ boolean changed = !Objects.equals(this.displayName, displayName) || this.type != type;
public void removeScore0(String id) {
scores.remove(id);
}
public Objective setDisplayName(String displayName) {
this.displayName = displayName; this.displayName = displayName;
if (updateType == UpdateType.NOTHING) { this.type = type;
updateType = UpdateType.UPDATE;
}
return this;
}
public Objective setNumberFormat(NumberFormat numberFormat) { if (!Objects.equals(this.numberFormat, format)) {
if (Objects.equals(this.numberFormat, numberFormat)) { this.numberFormat = format;
return this; // update the number format for scores that are following this objective's number format,
} // but only if the objective itself doesn't need to be updated.
// When the objective itself has to update all scores are updated anyway
this.numberFormat = numberFormat; if (!changed) {
if (updateType == UpdateType.NOTHING) { for (ScoreReference score : scores.values()) {
updateType = UpdateType.UPDATE; if (score.numberFormat() == null) {
} score.markChanged();
}
// Update the number format for scores that are following this objective's number format }
for (Score score : scores.values()) {
if (score.getNumberFormat() == null) {
score.setUpdateType(UpdateType.UPDATE);
} }
} }
return this; if (changed) {
} for (DisplaySlot slot : activeSlots) {
slot.markNeedsUpdate();
public Objective setType(int type) { }
this.type = type;
if (updateType == UpdateType.NOTHING) {
updateType = UpdateType.UPDATE;
}
return this;
}
public void setActive(ScoreboardPosition displaySlot) {
if (!active) {
active = true;
this.displaySlot = displaySlot;
displaySlotName = translateDisplaySlot(displaySlot);
} }
} }
/** public boolean hasDisplaySlot() {
* The objective will be removed on the next update return !activeSlots.isEmpty();
*/
public void pendingRemove() {
updateType = UpdateType.REMOVE;
} }
public @Nullable TeamColor getTeamColor() { public void addDisplaySlot(DisplaySlot slot) {
return switch (displaySlot) { activeSlots.add(slot);
case SIDEBAR_TEAM_RED -> TeamColor.RED;
case SIDEBAR_TEAM_AQUA -> TeamColor.AQUA;
case SIDEBAR_TEAM_BLUE -> TeamColor.BLUE;
case SIDEBAR_TEAM_GOLD -> TeamColor.GOLD;
case SIDEBAR_TEAM_GRAY -> TeamColor.GRAY;
case SIDEBAR_TEAM_BLACK -> TeamColor.BLACK;
case SIDEBAR_TEAM_GREEN -> TeamColor.GREEN;
case SIDEBAR_TEAM_WHITE -> TeamColor.WHITE;
case SIDEBAR_TEAM_YELLOW -> TeamColor.YELLOW;
case SIDEBAR_TEAM_DARK_RED -> TeamColor.DARK_RED;
case SIDEBAR_TEAM_DARK_AQUA -> TeamColor.DARK_AQUA;
case SIDEBAR_TEAM_DARK_BLUE -> TeamColor.DARK_BLUE;
case SIDEBAR_TEAM_DARK_GRAY -> TeamColor.DARK_GRAY;
case SIDEBAR_TEAM_DARK_GREEN -> TeamColor.DARK_GREEN;
case SIDEBAR_TEAM_DARK_PURPLE -> TeamColor.DARK_PURPLE;
case SIDEBAR_TEAM_LIGHT_PURPLE -> TeamColor.LIGHT_PURPLE;
default -> null;
};
} }
public void removed() { public void removeDisplaySlot(DisplaySlot slot) {
active = false; activeSlots.remove(slot);
updateType = UpdateType.REMOVE;
scores = null;
} }
} }

Datei anzeigen

@ -1,199 +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.scoreboard;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.FixedFormat;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.NumberFormat;
import net.kyori.adventure.text.Component;
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
import lombok.Getter;
import lombok.experimental.Accessors;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.translator.text.MessageTranslator;
import java.util.Objects;
@Getter
@Accessors(chain = true)
public final class Score {
private final long id;
private final String name;
private ScoreInfo cachedInfo;
/**
* Changes that have been made since the last cached data.
*/
private final Score.ScoreData currentData;
/**
* The data that is currently displayed to the Bedrock client.
*/
private Score.ScoreData cachedData;
public Score(long id, String name) {
this.id = id;
this.name = name;
this.currentData = new ScoreData();
}
public String getDisplayName() {
String displayName = cachedData.displayName;
if (displayName != null) {
return displayName;
}
Team team = cachedData.team;
if (team != null) {
return team.getDisplayName(name);
}
return name;
}
public int getScore() {
return currentData.getScore();
}
public Score setScore(int score) {
currentData.score = score;
return this;
}
public Team getTeam() {
return currentData.team;
}
public Score setTeam(Team team) {
if (currentData.team != null && team != null) {
if (!currentData.team.equals(team)) {
currentData.team = team;
setUpdateType(UpdateType.UPDATE);
}
return this;
}
// simplified from (this.team != null && team == null) || (this.team == null && team != null)
if (currentData.team != null || team != null) {
currentData.team = team;
setUpdateType(UpdateType.UPDATE);
}
return this;
}
public Score setDisplayName(Component displayName) {
if (currentData.displayName != null && displayName != null) {
String convertedDisplayName = MessageTranslator.convertMessage(displayName);
if (!currentData.displayName.equals(convertedDisplayName)) {
currentData.displayName = convertedDisplayName;
setUpdateType(UpdateType.UPDATE);
}
return this;
}
// simplified from (this.displayName != null && displayName == null) || (this.displayName == null && displayName != null)
if (currentData.displayName != null || displayName != null) {
currentData.displayName = MessageTranslator.convertMessage(displayName);
setUpdateType(UpdateType.UPDATE);
}
return this;
}
public NumberFormat getNumberFormat() {
return currentData.numberFormat;
}
public Score setNumberFormat(NumberFormat numberFormat) {
if (!Objects.equals(currentData.numberFormat, numberFormat)) {
currentData.numberFormat = numberFormat;
setUpdateType(UpdateType.UPDATE);
}
return this;
}
public UpdateType getUpdateType() {
return currentData.updateType;
}
public Score setUpdateType(UpdateType updateType) {
if (updateType != UpdateType.NOTHING) {
currentData.changed = true;
}
currentData.updateType = updateType;
return this;
}
public boolean shouldUpdate() {
return cachedData == null || currentData.changed ||
(currentData.team != null && currentData.team.shouldUpdate());
}
public void update(Objective objective) {
if (cachedData == null) {
cachedData = new ScoreData();
cachedData.updateType = UpdateType.ADD;
if (currentData.updateType == UpdateType.REMOVE) {
cachedData.updateType = UpdateType.REMOVE;
}
} else {
cachedData.updateType = currentData.updateType;
}
currentData.changed = false;
cachedData.team = currentData.team;
cachedData.score = currentData.score;
cachedData.displayName = currentData.displayName;
cachedData.numberFormat = currentData.numberFormat;
String name = this.name;
if (cachedData.displayName != null) {
name = cachedData.displayName;
} else if (cachedData.team != null) {
cachedData.team.prepareUpdate();
name = cachedData.team.getDisplayName(name);
}
NumberFormat numberFormat = cachedData.numberFormat;
if (numberFormat == null) {
numberFormat = objective.getNumberFormat();
}
if (numberFormat instanceof FixedFormat fixedFormat) {
name += " " + ChatColor.RESET + MessageTranslator.convertMessage(fixedFormat.getValue());
}
cachedInfo = new ScoreInfo(id, objective.getObjectiveName(), cachedData.score, name);
}
@Getter
public static final class ScoreData {
private UpdateType updateType;
private boolean changed;
private Team team;
private int score;
private String displayName;
private NumberFormat numberFormat;
private ScoreData() {
updateType = UpdateType.ADD;
}
}
}

Datei anzeigen

@ -0,0 +1,132 @@
/*
* 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.scoreboard;
import java.util.Objects;
import net.kyori.adventure.text.Component;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.NumberFormat;
public final class ScoreReference {
public static final long LAST_UPDATE_DEFAULT = -1;
private static final long LAST_UPDATE_REMOVE = -2;
private final String name;
private final boolean hidden;
private String displayName;
private int score;
private NumberFormat numberFormat;
private long lastUpdate;
public ScoreReference(
Scoreboard scoreboard, String name, int score, Component displayName, NumberFormat format) {
this.name = name;
// hidden is a sidebar exclusive feature
this.hidden = name.startsWith("#");
updateProperties(scoreboard, score, displayName, format);
this.lastUpdate = LAST_UPDATE_DEFAULT;
}
public String name() {
return name;
}
public boolean hidden() {
return hidden;
}
public String displayName() {
return displayName;
}
public void displayName(Component displayName, Scoreboard scoreboard) {
if (this.displayName != null && displayName != null) {
String convertedDisplayName = MessageTranslator.convertMessage(displayName, scoreboard.session().locale());
if (!this.displayName.equals(convertedDisplayName)) {
this.displayName = convertedDisplayName;
markChanged();
}
return;
}
// simplified from (this.displayName != null && displayName == null) || (this.displayName == null && displayName != null)
if (this.displayName != null || displayName != null) {
this.displayName = MessageTranslator.convertMessage(displayName, scoreboard.session().locale());
markChanged();
}
}
public int score() {
return score;
}
private void score(int score) {
boolean changed = this.score != score;
this.score = score;
if (changed) {
markChanged();
}
}
public NumberFormat numberFormat() {
return numberFormat;
}
private void numberFormat(NumberFormat numberFormat) {
if (Objects.equals(numberFormat(), numberFormat)) {
return;
}
this.numberFormat = numberFormat;
markChanged();
}
public void updateProperties(Scoreboard scoreboard, int score, Component displayName, NumberFormat numberFormat) {
score(score);
displayName(displayName, scoreboard);
numberFormat(numberFormat);
}
public long lastUpdate() {
return lastUpdate;
}
public boolean isRemoved() {
return lastUpdate == LAST_UPDATE_REMOVE;
}
public void markChanged() {
if (lastUpdate == LAST_UPDATE_REMOVE) {
return;
}
lastUpdate = System.currentTimeMillis();
}
public void markDeleted() {
lastUpdate = -1;
}
}

Datei anzeigen

@ -25,43 +25,72 @@
package org.geysermc.geyser.scoreboard; package org.geysermc.geyser.scoreboard;
import static org.geysermc.geyser.scoreboard.UpdateType.REMOVE;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.Getter; import lombok.Getter;
import net.kyori.adventure.text.Component;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo; import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
import org.cloudburstmc.protocol.bedrock.data.command.CommandEnumConstraint; import org.cloudburstmc.protocol.bedrock.data.command.CommandEnumConstraint;
import org.cloudburstmc.protocol.bedrock.packet.RemoveObjectivePacket;
import org.cloudburstmc.protocol.bedrock.packet.SetDisplayObjectivePacket;
import org.cloudburstmc.protocol.bedrock.packet.SetScorePacket; import org.cloudburstmc.protocol.bedrock.packet.SetScorePacket;
import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.GeyserLogger; import org.geysermc.geyser.GeyserLogger;
import org.geysermc.geyser.entity.type.Entity; 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.scoreboard.display.slot.BelownameDisplaySlot;
import org.geysermc.geyser.scoreboard.display.slot.DisplaySlot;
import org.geysermc.geyser.scoreboard.display.slot.PlayerlistDisplaySlot;
import org.geysermc.geyser.scoreboard.display.slot.SidebarDisplaySlot;
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.mcprotocollib.protocol.data.game.scoreboard.NameTagVisibility;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition; import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Contract;
import java.util.*; /**
import java.util.concurrent.ConcurrentHashMap; * Here follows some information about how scoreboards work in Java Edition, that is related to the workings of this
import java.util.concurrent.atomic.AtomicLong; * class:
import java.util.function.Function; * <p>
import java.util.stream.Collectors; * Objectives can be divided in two states: inactive and active.
* Inactive objectives is the default state for objectives that have been created using the SetObjective packet.
import static org.geysermc.geyser.scoreboard.UpdateType.*; * Scores can be added, updated and removed, but as long as they're inactive they aren't shown to the player.
* An objective becomes active when a SetDisplayObjective packet is received, which contains the slot that
* the objective should be displayed at.
* <p>
* While Bedrock can handle showing one objective on multiple slots at the same time, we have to help Bedrock a bit
* for example by limiting the amount of sidebar scores to the amount of lines that can be shown
* (otherwise Bedrock may lag) and only showing online players in the playerlist (otherwise it's too cluttered.)
* This fact is the biggest contributor for the class being structured like it is.
*/
public final class Scoreboard { public final class Scoreboard {
private static final boolean SHOW_SCOREBOARD_LOGS = Boolean.parseBoolean(System.getProperty("Geyser.ShowScoreboardLogs", "true")); private static final boolean SHOW_SCOREBOARD_LOGS = Boolean.parseBoolean(System.getProperty("Geyser.ShowScoreboardLogs", "true"));
private static final boolean ADD_TEAM_SUGGESTIONS = Boolean.parseBoolean(System.getProperty("Geyser.AddTeamSuggestions", "true")); private static final boolean ADD_TEAM_SUGGESTIONS = Boolean.parseBoolean(System.getProperty("Geyser.AddTeamSuggestions", "true"));
private final GeyserSession session; private final GeyserSession session;
private final GeyserLogger logger; private final GeyserLogger logger;
@Getter
private final AtomicLong nextId = new AtomicLong(0); private final AtomicLong nextId = new AtomicLong(0);
private final Map<String, Objective> objectives = new ConcurrentHashMap<>(); private final Map<String, Objective> objectives = new ConcurrentHashMap<>();
@Getter @Getter
private final Map<ScoreboardPosition, Objective> objectiveSlots = new EnumMap<>(ScoreboardPosition.class); private final Map<ScoreboardPosition, DisplaySlot> objectiveSlots = Collections.synchronizedMap(new EnumMap<>(ScoreboardPosition.class));
private final List<DisplaySlot> removedSlots = Collections.synchronizedList(new ArrayList<>());
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. * Required to preserve vanilla behavior, which also uses a map.
@ -71,6 +100,7 @@ public final class Scoreboard {
@Getter @Getter
private final Map<String, Team> playerToTeam = new Object2ObjectOpenHashMap<>(); private final Map<String, Team> playerToTeam = new Object2ObjectOpenHashMap<>();
private final AtomicBoolean updateLockActive = new AtomicBoolean(false);
private int lastAddScoreCount = 0; private int lastAddScoreCount = 0;
private int lastRemoveScoreCount = 0; private int lastRemoveScoreCount = 0;
@ -80,24 +110,22 @@ public final class Scoreboard {
} }
public void removeScoreboard() { public void removeScoreboard() {
Iterator<Objective> iterator = objectives.values().iterator(); var copy = new HashMap<>(objectiveSlots);
while (iterator.hasNext()) { objectiveSlots.clear();
Objective objective = iterator.next();
iterator.remove();
deleteObjective(objective, false); for (DisplaySlot slot : copy.values()) {
slot.remove();
} }
} }
public @Nullable Objective registerNewObjective(String objectiveId) { public @Nullable Objective registerNewObjective(String objectiveId) {
Objective objective = objectives.get(objectiveId); Objective objective = objectives.get(objectiveId);
if (objective != null) { if (objective != null) {
// we have no other choice, or we have to make a new map? // matches vanilla behaviour
// if the objective hasn't been deleted, we have to force it if (SHOW_SCOREBOARD_LOGS) {
if (objective.getUpdateType() != REMOVE) { logger.warning("An objective with the same name '" + objectiveId + "' already exists! Ignoring new objective!");
return null;
} }
deleteObjective(objective, true); return null;
} }
objective = new Objective(this, objectiveId); objective = new Objective(this, objectiveId);
@ -105,273 +133,162 @@ public final class Scoreboard {
return objective; return objective;
} }
public void displayObjective(String objectiveId, ScoreboardPosition displaySlot) { public void displayObjective(String objectiveId, ScoreboardPosition slot) {
if (objectiveId.isEmpty()) {
// matches vanilla behaviour
var display = objectiveSlots.get(slot);
if (display != null) {
removedSlots.add(display);
objectiveSlots.remove(slot, display);
var objective = display.objective();
objective.removeDisplaySlot(display);
}
return;
}
Objective objective = objectives.get(objectiveId); Objective objective = objectives.get(objectiveId);
if (objective == null) { if (objective == null) {
return; return;
} }
if (!objective.isActive()) { var display = objectiveSlots.get(slot);
objective.setActive(displaySlot); if (display != null && display.objective() != objective) {
// for reactivated objectives removedSlots.add(display);
objective.setUpdateType(ADD);
} }
Objective storedObjective = objectiveSlots.get(displaySlot); display = switch (DisplaySlot.slotCategory(slot)) {
if (storedObjective != null && storedObjective != objective) { case SIDEBAR -> new SidebarDisplaySlot(session, objective, slot);
storedObjective.pendingRemove(); case BELOW_NAME -> new BelownameDisplaySlot(session, objective);
} case PLAYER_LIST -> new PlayerlistDisplaySlot(session, objective);
objectiveSlots.put(displaySlot, objective); default -> throw new IllegalStateException("Unexpected value: " + slot);
};
if (displaySlot == ScoreboardPosition.BELOW_NAME) { objectiveSlots.put(slot, display);
// Display the below name score option to all players objective.addDisplaySlot(display);
// Of note: unlike Bedrock, if there is an objective in the below name slot, everyone has a display
for (PlayerEntity entity : session.getEntityCache().getAllPlayerEntities()) {
if (!entity.isValid()) {
// Player hasn't spawned yet - don't bother, it'll be done then
continue;
}
entity.setBelowNameText(objective);
}
}
} }
public Team registerNewTeam(String teamName, String[] players) { public void registerNewTeam(
String teamName,
String[] players,
Component name,
Component prefix,
Component suffix,
NameTagVisibility visibility,
TeamColor color
) {
Team team = teams.get(teamName); Team team = teams.get(teamName);
if (team != null) { if (team != null) {
if (SHOW_SCOREBOARD_LOGS) { 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 = new Team(this, teamName); team = new Team(this, teamName, players, name, prefix, suffix, visibility, color);
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) // Update command parameters - is safe to send even if the command enum doesn't exist on the client (as of 1.19.51)
if (ADD_TEAM_SUGGESTIONS) { if (ADD_TEAM_SUGGESTIONS) {
session.addCommandEnum("Geyser_Teams", team.getId()); session.addCommandEnum("Geyser_Teams", team.id());
} }
return team;
} }
public void onUpdate() { public void onUpdate() {
// if an update is already running, let it finish
if (updateLockActive.getAndSet(true)) {
return;
}
List<ScoreInfo> addScores = new ArrayList<>(lastAddScoreCount); List<ScoreInfo> addScores = new ArrayList<>(lastAddScoreCount);
List<ScoreInfo> removeScores = new ArrayList<>(lastRemoveScoreCount); List<ScoreInfo> removeScores = new ArrayList<>(lastRemoveScoreCount);
List<Objective> removedObjectives = new ArrayList<>();
Team playerTeam = getTeamFor(session.getPlayerEntity().getUsername()); Team playerTeam = getTeamFor(session.getPlayerEntity().getUsername());
Objective correctSidebar = null; DisplaySlot correctSidebarSlot = null;
for (Objective objective : objectives.values()) { for (DisplaySlot slot : objectiveSlots.values()) {
// objective has been deleted // slot has been removed
if (objective.getUpdateType() == REMOVE) { if (slot.updateType() == REMOVE) {
removedObjectives.add(objective);
continue; continue;
} }
// there's nothing we can do with inactive objectives if (playerTeam != null && playerTeam.color() == slot.teamColor()) {
// after checking if the objective has been deleted, correctSidebarSlot = slot;
// except waiting for the objective to become activated (:
if (!objective.isActive()) {
continue;
}
if (playerTeam != null && playerTeam.getColor() == objective.getTeamColor()) {
correctSidebar = objective;
} }
} }
if (correctSidebar == null) { if (correctSidebarSlot == null) {
correctSidebar = objectiveSlots.get(ScoreboardPosition.SIDEBAR); correctSidebarSlot = objectiveSlots.get(ScoreboardPosition.SIDEBAR);
} }
for (Objective objective : removedObjectives) { var actualRemovedSlots = new ArrayList<>(removedSlots);
for (var slot : actualRemovedSlots) {
// Deletion must be handled before the active objectives are handled - otherwise if a scoreboard display is changed before the current // Deletion must be handled before the active objectives are handled - otherwise if a scoreboard display is changed before the current
// scoreboard is removed, the client can crash // scoreboard is removed, the client can crash
deleteObjective(objective, true); slot.remove();
} }
removedSlots.removeAll(actualRemovedSlots);
handleObjective(objectiveSlots.get(ScoreboardPosition.PLAYER_LIST), addScores, removeScores); handleDisplaySlot(objectiveSlots.get(ScoreboardPosition.PLAYER_LIST), addScores, removeScores);
handleObjective(correctSidebar, addScores, removeScores); handleDisplaySlot(correctSidebarSlot, addScores, removeScores);
handleObjective(objectiveSlots.get(ScoreboardPosition.BELOW_NAME), addScores, removeScores); handleDisplaySlot(objectiveSlots.get(ScoreboardPosition.BELOW_NAME), addScores, removeScores);
Iterator<Team> teamIterator = teams.values().iterator();
while (teamIterator.hasNext()) {
Team current = teamIterator.next();
switch (current.getCachedUpdateType()) {
case ADD, UPDATE -> current.markUpdated();
case REMOVE -> teamIterator.remove();
}
}
if (!removeScores.isEmpty()) { if (!removeScores.isEmpty()) {
SetScorePacket setScorePacket = new SetScorePacket(); SetScorePacket packet = new SetScorePacket();
setScorePacket.setAction(SetScorePacket.Action.REMOVE); packet.setAction(SetScorePacket.Action.REMOVE);
setScorePacket.setInfos(removeScores); packet.setInfos(removeScores);
session.sendUpstreamPacket(setScorePacket); session.sendUpstreamPacket(packet);
} }
if (!addScores.isEmpty()) { if (!addScores.isEmpty()) {
SetScorePacket setScorePacket = new SetScorePacket(); SetScorePacket packet = new SetScorePacket();
setScorePacket.setAction(SetScorePacket.Action.SET); packet.setAction(SetScorePacket.Action.SET);
setScorePacket.setInfos(addScores); packet.setInfos(addScores);
session.sendUpstreamPacket(setScorePacket); session.sendUpstreamPacket(packet);
} }
lastAddScoreCount = addScores.size(); lastAddScoreCount = addScores.size();
lastRemoveScoreCount = removeScores.size(); lastRemoveScoreCount = removeScores.size();
updateLockActive.set(false);
} }
private void handleObjective(Objective objective, List<ScoreInfo> addScores, List<ScoreInfo> removeScores) { private void handleDisplaySlot(DisplaySlot slot, List<ScoreInfo> addScores, List<ScoreInfo> removeScores) {
if (objective == null || objective.getUpdateType() == REMOVE) { if (slot != null) {
return; slot.render(addScores, removeScores);
} }
// hearts can't hold teams, so we treat them differently
if (objective.getType() == 1) {
for (Score score : objective.getScores().values()) {
boolean update = score.shouldUpdate();
if (update) {
score.update(objective);
}
if (score.getUpdateType() != REMOVE && update) {
addScores.add(score.getCachedInfo());
}
if (score.getUpdateType() != ADD && update) {
removeScores.add(score.getCachedInfo());
}
}
return;
}
boolean objectiveAdd = objective.getUpdateType() == ADD;
boolean objectiveUpdate = objective.getUpdateType() == UPDATE;
for (Score score : objective.getScores().values()) {
if (score.getUpdateType() == REMOVE) {
ScoreInfo cachedInfo = score.getCachedInfo();
// cachedInfo can be null here when ScoreboardUpdater is being used and a score is added and
// removed before a single update cycle is performed
if (cachedInfo != null) {
removeScores.add(cachedInfo);
}
// score is pending to be removed, so we can remove it from the objective
objective.removeScore0(score.getName());
break;
}
Team team = score.getTeam();
boolean add = objectiveAdd || objectiveUpdate;
if (team != null) {
if (team.getUpdateType() == REMOVE || !team.hasEntity(score.getName())) {
score.setTeam(null);
add = true;
}
}
if (score.shouldUpdate()) {
score.update(objective);
add = true;
}
if (add) {
addScores.add(score.getCachedInfo());
}
// we need this as long as MCPE-143063 hasn't been fixed.
// the checks after 'add' are there to prevent removing scores that
// are going to be removed anyway / don't need to be removed
if (add && score.getUpdateType() != ADD && !(objectiveUpdate || objectiveAdd)) {
removeScores.add(score.getCachedInfo());
}
score.setUpdateType(NOTHING);
}
if (objectiveUpdate) {
RemoveObjectivePacket removeObjectivePacket = new RemoveObjectivePacket();
removeObjectivePacket.setObjectiveId(objective.getObjectiveName());
session.sendUpstreamPacket(removeObjectivePacket);
}
if (objectiveAdd || objectiveUpdate) {
SetDisplayObjectivePacket displayObjectivePacket = new SetDisplayObjectivePacket();
displayObjectivePacket.setObjectiveId(objective.getObjectiveName());
displayObjectivePacket.setDisplayName(objective.getDisplayName());
displayObjectivePacket.setCriteria("dummy");
displayObjectivePacket.setDisplaySlot(objective.getDisplaySlotName());
displayObjectivePacket.setSortOrder(1); // 0 = ascending, 1 = descending
session.sendUpstreamPacket(displayObjectivePacket);
}
objective.setUpdateType(NOTHING);
}
/**
* @param remove if we should remove the objective from the objectives map.
*/
public void deleteObjective(Objective objective, boolean remove) {
if (remove) {
objectives.remove(objective.getObjectiveName());
}
objectiveSlots.remove(objective.getDisplaySlot(), objective);
objective.removed();
RemoveObjectivePacket removeObjectivePacket = new RemoveObjectivePacket();
removeObjectivePacket.setObjectiveId(objective.getObjectiveName());
session.sendUpstreamPacket(removeObjectivePacket);
} }
public Objective getObjective(String objectiveName) { public Objective getObjective(String objectiveName) {
return objectives.get(objectiveName); return objectives.get(objectiveName);
} }
public Collection<Objective> getObjectives() { public void removeObjective(Objective objective) {
return objectives.values(); objectives.remove(objective.getObjectiveName());
} for (DisplaySlot slot : objective.getActiveSlots()) {
objectiveSlots.remove(slot.position(), slot);
public void unregisterObjective(String objectiveName) { removedSlots.add(slot);
Objective objective = getObjective(objectiveName);
if (objective != null) {
objective.pendingRemove();
} }
} }
public Objective getSlot(ScoreboardPosition slot) { public void resetPlayerScores(String playerNameOrEntityUuid) {
return objectiveSlots.get(slot); for (Objective objective : objectives.values()) {
objective.removeScore(playerNameOrEntityUuid);
}
} }
public Team getTeam(String teamName) { public Team getTeam(String teamName) {
return teams.get(teamName); return teams.get(teamName);
} }
public Team getTeamFor(String entity) { public Team getTeamFor(String playerNameOrEntityUuid) {
return playerToTeam.get(entity); return playerToTeam.get(playerNameOrEntityUuid);
} }
public void removeTeam(String teamName) { public void removeTeam(String teamName) {
Team remove = teams.remove(teamName); Team remove = teams.remove(teamName);
if (remove != null) { if (remove == null) {
remove.setUpdateType(REMOVE); return;
// 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
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());
} }
remove.remove();
session.removeCommandEnum("Geyser_Teams", remove.id());
} }
@Contract("-> new") @Contract("-> new")
@ -381,48 +298,46 @@ public final class Scoreboard {
(o1, o2) -> o1, LinkedHashMap::new)); (o1, o2) -> o1, LinkedHashMap::new));
} }
/** public void playerRegistered(PlayerEntity player) {
* Updates the display names of all entities in a given team. for (DisplaySlot slot : objectiveSlots.values()) {
* @param teamChange the players have either joined or left the team. Used for optimizations when just the display name updated. slot.playerRegistered(player);
*/ }
public void updateEntityNames(Team team, boolean teamChange) {
Set<String> names = new HashSet<>(team.getEntities());
updateEntityNames(team, names, teamChange);
} }
/** public void playerRemoved(PlayerEntity player) {
* Updates the display name of a set of entities within a given team. The team may also be null if the set is being removed for (DisplaySlot slot : objectiveSlots.values()) {
* from a team. slot.playerRemoved(player);
*/
public void updateEntityNames(@Nullable Team team, Set<String> names, boolean teamChange) {
if (names.remove(session.getPlayerEntity().getUsername()) && teamChange) {
// If the player's team changed, then other entities' teams may modify their visibility based on team status
refreshSessionPlayerDisplays();
} }
if (!names.isEmpty()) { }
for (Entity entity : session.getEntityCache().getEntities().values()) {
// This more complex logic is for the future to iterate over all entities, not just players public void entityRegistered(Entity entity) {
if (entity instanceof PlayerEntity player && names.remove(player.getUsername())) { var team = getTeamFor(entity.teamIdentifier());
player.updateDisplayName(team); if (team != null) {
player.updateBedrockMetadata(); team.onEntitySpawn(entity);
if (names.isEmpty()) { }
break; }
}
} public void entityRemoved(Entity entity) {
var team = getTeamFor(entity.teamIdentifier());
if (team != null) {
team.onEntityRemove(entity);
}
}
public void setTeamFor(Team team, Set<String> entities) {
for (DisplaySlot slot : objectiveSlots.values()) {
// only sidebar slots use teams
if (slot instanceof SidebarDisplaySlot sidebar) {
sidebar.setTeamFor(team, entities);
} }
} }
} }
/** public long nextId() {
* If the team's player was refreshed, then we need to go through every entity and check... return nextId.getAndIncrement();
*/ }
private void refreshSessionPlayerDisplays() {
for (Entity entity : session.getEntityCache().getEntities().values()) { public GeyserSession session() {
if (entity instanceof PlayerEntity player) { return session;
Team playerTeam = session.getWorldCache().getScoreboard().getTeamFor(player.getUsername());
player.updateDisplayName(playerTeam);
player.updateBedrockMetadata();
}
}
} }
} }

Datei anzeigen

@ -173,7 +173,6 @@ public final class ScoreboardUpdater extends Thread {
@Getter @Getter
public static final class ScoreboardSession { public static final class ScoreboardSession {
private final GeyserSession session; private final GeyserSession session;
@SuppressWarnings("WriteOnlyObject")
private final AtomicInteger pendingPacketsPerSecond = new AtomicInteger(0); private final AtomicInteger pendingPacketsPerSecond = new AtomicInteger(0);
private int packetsPerSecond; private int packetsPerSecond;
private long lastUpdate; private long lastUpdate;

Datei anzeigen

@ -25,48 +25,66 @@
package org.geysermc.geyser.scoreboard; package org.geysermc.geyser.scoreboard;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.NameTagVisibility;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import net.kyori.adventure.text.Component;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.NameTagVisibility;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
@Getter
@Accessors(chain = true)
public final class Team { public final class Team {
public static final long LAST_UPDATE_DEFAULT = -1;
private static final long LAST_UPDATE_REMOVE = -2;
private final Scoreboard scoreboard; private final Scoreboard scoreboard;
private final String id; private final String id;
@Getter(AccessLevel.PACKAGE)
private final Set<String> entities; private final Set<String> entities;
private final Set<Entity> managedEntities;
@NonNull private NameTagVisibility nameTagVisibility = NameTagVisibility.ALWAYS; @NonNull private NameTagVisibility nameTagVisibility = NameTagVisibility.ALWAYS;
@Setter private TeamColor color; private TeamColor color;
private final TeamData currentData; private String name;
private TeamData cachedData; private String prefix;
private String suffix;
private long lastUpdate;
private boolean updating; public Team(
Scoreboard scoreboard,
public Team(Scoreboard scoreboard, String id) { String id,
String[] players,
Component name,
Component prefix,
Component suffix,
NameTagVisibility visibility,
TeamColor color
) {
this.scoreboard = scoreboard; this.scoreboard = scoreboard;
this.id = id; this.id = id;
currentData = new TeamData(); this.entities = new ObjectOpenHashSet<>();
entities = new ObjectOpenHashSet<>(); this.managedEntities = new ObjectOpenHashSet<>();
this.lastUpdate = LAST_UPDATE_DEFAULT;
// doesn't call entity update
updateProperties(name, prefix, suffix, visibility, color);
// calls entity update
addEntities(players);
lastUpdate = LAST_UPDATE_DEFAULT;
} }
public Set<String> addEntities(String... names) { public void addEntities(String... names) {
Set<String> added = new HashSet<>(); Set<String> added = new HashSet<>();
for (String name : names) { for (String name : names) {
if (entities.add(name)) { // go to next score if score is already present
added.add(name); if (!entities.add(name)) {
continue;
} }
added.add(name);
scoreboard.getPlayerToTeam().compute(name, (player, oldTeam) -> { scoreboard.getPlayerToTeam().compute(name, (player, oldTeam) -> {
if (oldTeam != null) { if (oldTeam != null) {
// Remove old team from this map, and from the set of players of the old team. // Remove old team from this map, and from the set of players of the old team.
@ -78,26 +96,15 @@ public final class Team {
} }
if (added.isEmpty()) { if (added.isEmpty()) {
return added; return;
} }
// we don't have to change the updateType, // we don't have to change our updateType,
// because the scores itself need updating, not the team // because the scores themselves need updating, not the team
for (Objective objective : scoreboard.getObjectives()) { scoreboard.setTeamFor(this, added);
for (String addedEntity : added) { addAddedEntities(added);
Score score = objective.getScores().get(addedEntity);
if (score != null) {
score.setTeam(this);
}
}
}
return added;
} }
/** public void removeEntities(String... names) {
* @return all removed entities from this team
*/
public Set<String> removeEntities(String... names) {
Set<String> removed = new HashSet<>(); Set<String> removed = new HashSet<>();
for (String name : names) { for (String name : names) {
if (entities.remove(name)) { if (entities.remove(name)) {
@ -105,87 +112,22 @@ public final class Team {
} }
scoreboard.getPlayerToTeam().remove(name, this); scoreboard.getPlayerToTeam().remove(name, this);
} }
return removed; removeRemovedEntities(removed);
} }
public boolean hasEntity(String name) { public boolean hasEntity(String name) {
return entities.contains(name); return entities.contains(name);
} }
public Team setName(String name) { public String displayName(String score) {
currentData.name = name; String chatColor = ChatColor.chatColorFor(color);
return this; // most sidebar plugins will use the reset color, because they don't want color
} // skip the unneeded double reset color in that case
if (ChatColor.RESET.equals(chatColor)) {
public Team setPrefix(String prefix) { chatColor = "";
// replace "null" to an empty string,
// we do this here to improve the performance of Score#getDisplayName
if (prefix.length() == 4 && "null".equals(prefix)) {
currentData.prefix = "";
return this;
} }
currentData.prefix = prefix; // also add reset because setting the color does not reset the formatting, unlike Java
return this; return chatColor + prefix + ChatColor.RESET + chatColor + score + ChatColor.RESET + chatColor + suffix;
}
public Team setSuffix(String suffix) {
// replace "null" to an empty string,
// we do this here to improve the performance of Score#getDisplayName
if (suffix.length() == 4 && "null".equals(suffix)) {
currentData.suffix = "";
return this;
}
currentData.suffix = suffix;
return this;
}
public String getDisplayName(String score) {
return cachedData != null ?
cachedData.getDisplayName(score) :
currentData.getDisplayName(score);
}
public void markUpdated() {
updating = false;
}
public boolean shouldUpdate() {
return updating || cachedData == null || currentData.changed;
}
public void prepareUpdate() {
if (updating) {
return;
}
updating = true;
if (cachedData == null) {
cachedData = new TeamData();
cachedData.updateType = currentData.updateType != UpdateType.REMOVE ? UpdateType.ADD : UpdateType.REMOVE;
} else {
cachedData.updateType = currentData.updateType;
}
currentData.changed = false;
cachedData.name = currentData.name;
cachedData.prefix = currentData.prefix;
cachedData.suffix = currentData.suffix;
}
public UpdateType getUpdateType() {
return currentData.updateType;
}
public UpdateType getCachedUpdateType() {
return cachedData != null ? cachedData.updateType : currentData.updateType;
}
public Team setUpdateType(UpdateType updateType) {
if (updateType != UpdateType.NOTHING) {
currentData.changed = true;
}
currentData.updateType = updateType;
return this;
} }
public boolean isVisibleFor(String entity) { public boolean isVisibleFor(String entity) {
@ -201,34 +143,178 @@ public final class Team {
}; };
} }
public Team setNameTagVisibility(@Nullable NameTagVisibility nameTagVisibility) { public void updateProperties(Component name, Component prefix, Component suffix, NameTagVisibility visibility, TeamColor color) {
if (nameTagVisibility != null) { // this shouldn't happen but hey!
// Null check like this (and this.nameTagVisibility defaults to ALWAYS) as of Java 1.19.4 if (lastUpdate == LAST_UPDATE_REMOVE) {
this.nameTagVisibility = nameTagVisibility; return;
} }
return this;
String oldName = this.name;
String oldPrefix = this.prefix;
String oldSuffix = this.suffix;
boolean oldVisible = isVisibleFor(playerName());
var oldColor = this.color;
this.name = MessageTranslator.convertMessageRaw(name, session().locale());
this.prefix = MessageTranslator.convertMessageRaw(prefix, session().locale());
this.suffix = MessageTranslator.convertMessageRaw(suffix, session().locale());
// matches vanilla behaviour, the visibility is not reset (to ALWAYS) if it is null.
// instead the visibility is not altered
if (visibility != null) {
this.nameTagVisibility = visibility;
}
this.color = color;
if (lastUpdate == LAST_UPDATE_DEFAULT) {
// addEntities is called after the initial updateProperties, so no need to do any entity updates here
if (this.color != TeamColor.RESET || !this.prefix.isEmpty() || !this.suffix.isEmpty()) {
markChanged();
}
return;
}
if (!this.name.equals(oldName)
|| !this.prefix.equals(oldPrefix)
|| !this.suffix.equals(oldSuffix)
|| color != oldColor) {
markChanged();
updateEntities();
return;
}
if (isVisibleFor(playerName()) != oldVisible) {
// if just the visibility changed, we only have to update the entities.
// We don't have to mark it as changed
updateEntities();
}
}
public boolean shouldRemove() {
return lastUpdate == LAST_UPDATE_REMOVE;
}
public void markChanged() {
if (lastUpdate == LAST_UPDATE_REMOVE) {
return;
}
lastUpdate = System.currentTimeMillis();
}
public void remove() {
lastUpdate = LAST_UPDATE_REMOVE;
for (String name : entities()) {
// 1.19.3 Mojmap Scoreboard#removePlayerTeam(PlayerTeam)
scoreboard.getPlayerToTeam().remove(name);
}
if (entities().contains(playerName())) {
refreshAllEntities();
return;
}
for (Entity entity : managedEntities) {
entity.updateNametag(null);
entity.updateBedrockMetadata();
}
}
private void updateEntities() {
for (Entity entity : managedEntities) {
entity.updateNametag(this);
entity.updateBedrockMetadata();
}
}
public void onEntitySpawn(Entity entity) {
// I've basically ported addAddedEntities
if (entities.contains(entity.teamIdentifier())) {
managedEntities.add(entity);
// onEntitySpawn includes all entities but players, so it cannot contain self
entity.updateNametag(this);
entity.updateBedrockMetadata();
}
}
public void onEntityRemove(Entity entity) {
// we don't have to update anything, since the player is removed.
managedEntities.remove(entity);
}
private void addAddedEntities(Set<String> names) {
// can't contain self if none are added
if (names.isEmpty()) {
return;
}
boolean containsSelf = names.contains(playerName());
for (Entity entity : session().getEntityCache().getEntities().values()) {
if (names.contains(entity.teamIdentifier())) {
managedEntities.add(entity);
if (!containsSelf) {
entity.updateNametag(this);
entity.updateBedrockMetadata();
}
}
}
if (containsSelf) {
refreshAllEntities();
}
}
private void removeRemovedEntities(Set<String> names) {
boolean containsSelf = names.contains(playerName());
var iterator = managedEntities.iterator();
while (iterator.hasNext()) {
var entity = iterator.next();
if (names.contains(entity.teamIdentifier())) {
iterator.remove();
if (!containsSelf) {
entity.updateNametag(null);
entity.updateBedrockMetadata();
}
}
}
if (containsSelf) {
refreshAllEntities();
}
}
private void refreshAllEntities() {
for (Entity entity : session().getEntityCache().getEntities().values()) {
entity.updateNametag(scoreboard.getTeamFor(entity.teamIdentifier()));
entity.updateBedrockMetadata();
}
}
private GeyserSession session() {
return scoreboard.session();
}
private String playerName() {
return session().getPlayerEntity().getUsername();
}
public String id() {
return id;
}
public TeamColor color() {
return color;
}
public long lastUpdate() {
return lastUpdate;
}
public Set<String> entities() {
return entities;
} }
@Override @Override
public int hashCode() { public int hashCode() {
return id.hashCode(); return id.hashCode();
} }
@Getter
public static final class TeamData {
private UpdateType updateType;
private boolean changed;
private String name;
private String prefix;
private String suffix;
private TeamData() {
updateType = UpdateType.ADD;
}
public String getDisplayName(String score) {
return prefix + score + suffix;
}
}
} }

Datei anzeigen

@ -0,0 +1,56 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.display.score;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.scoreboard.Objective;
import org.geysermc.geyser.scoreboard.ScoreReference;
import org.geysermc.geyser.scoreboard.display.slot.DisplaySlot;
public class BelownameDisplayScore extends DisplayScore {
private final PlayerEntity player;
public BelownameDisplayScore(DisplaySlot slot, long scoreId, ScoreReference reference, PlayerEntity player) {
super(slot, scoreId, reference);
this.player = player;
}
@Override
public void update(Objective objective) {}
public PlayerEntity player() {
return player;
}
@Override
public void markUpdated() {
super.markUpdated();
}
public ScoreReference reference() {
return reference;
}
}

Datei anzeigen

@ -0,0 +1,70 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.display.score;
import org.geysermc.geyser.scoreboard.Objective;
import org.geysermc.geyser.scoreboard.ScoreReference;
import org.geysermc.geyser.scoreboard.display.slot.DisplaySlot;
public abstract class DisplayScore {
protected final DisplaySlot slot;
protected final long id;
protected final ScoreReference reference;
protected long lastTeamUpdate;
protected long lastUpdate;
public DisplayScore(DisplaySlot slot, long scoreId, ScoreReference reference) {
this.slot = slot;
this.id = scoreId;
this.reference = reference;
}
public boolean shouldUpdate() {
return reference.lastUpdate() != lastUpdate;
}
public abstract void update(Objective objective);
public String name() {
return reference.name();
}
public int score() {
return reference.score();
}
public boolean referenceRemoved() {
return reference.isRemoved();
}
protected void markUpdated() {
// with the last update (also for team) we rather have an old lastUpdate
// (and have to update again the next cycle) than potentially losing information
// by fetching the lastUpdate after update was performed
this.lastUpdate = reference.lastUpdate();
}
}

Datei anzeigen

@ -0,0 +1,61 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.display.score;
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
import org.geysermc.geyser.scoreboard.Objective;
import org.geysermc.geyser.scoreboard.ScoreReference;
import org.geysermc.geyser.scoreboard.display.slot.DisplaySlot;
public final class PlayerlistDisplayScore extends DisplayScore {
private final long playerId;
private ScoreInfo cachedInfo;
public PlayerlistDisplayScore(DisplaySlot slot, long scoreId, ScoreReference reference, long playerId) {
super(slot, scoreId, reference);
this.playerId = playerId;
}
@Override
public boolean shouldUpdate() {
// for player references the player's name is shown,
// so we only have to update when the score has changed
return cachedInfo == null || cachedInfo.getScore() != reference.score();
}
@Override
public void update(Objective objective) {
cachedInfo = new ScoreInfo(id, slot.objectiveId(), reference.score(), ScoreInfo.ScorerType.PLAYER, playerId);
}
public ScoreInfo cachedInfo() {
return cachedInfo;
}
public boolean exists() {
return cachedInfo != null;
}
}

Datei anzeigen

@ -0,0 +1,139 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.display.score;
import java.util.Objects;
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
import org.geysermc.geyser.scoreboard.Objective;
import org.geysermc.geyser.scoreboard.ScoreReference;
import org.geysermc.geyser.scoreboard.Team;
import org.geysermc.geyser.scoreboard.display.slot.DisplaySlot;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.FixedFormat;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.NumberFormat;
public final class SidebarDisplayScore extends DisplayScore {
private ScoreInfo cachedInfo;
private Team team;
private String order;
private boolean onlyScoreValueChanged;
public SidebarDisplayScore(DisplaySlot slot, long scoreId, ScoreReference reference) {
super(slot, scoreId, reference);
team(slot.objective().getScoreboard().getTeamFor(reference.name()));
}
@Override
public boolean shouldUpdate() {
return super.shouldUpdate() || shouldTeamUpdate();
}
private boolean shouldTeamUpdate() {
return team != null && team.lastUpdate() != lastTeamUpdate;
}
@Override
public void update(Objective objective) {
markUpdated();
String finalName = reference.name();
String displayName = reference.displayName();
if (displayName != null) {
finalName = displayName;
} else if (team != null) {
this.lastTeamUpdate = team.lastUpdate();
finalName = team.displayName(reference.name());
}
NumberFormat numberFormat = reference.numberFormat();
if (numberFormat == null) {
numberFormat = objective.getNumberFormat();
}
if (numberFormat instanceof FixedFormat fixedFormat) {
finalName += " " + ChatColor.RESET + MessageTranslator.convertMessage(fixedFormat.getValue(), objective.getScoreboard().session().locale());
}
if (order != null) {
finalName = order + ChatColor.RESET + finalName;
}
if (cachedInfo != null) {
onlyScoreValueChanged = finalName.equals(cachedInfo.getName());
}
cachedInfo = new ScoreInfo(id, slot.objectiveId(), reference.score(), finalName);
}
public String order() {
return order;
}
public DisplayScore order(String order) {
if (Objects.equals(this.order, order)) {
return this;
}
this.order = order;
// this guarantees an update
requestUpdate();
return this;
}
public Team team() {
return team;
}
public void team(Team team) {
if (this.team != null && team != null) {
if (!this.team.equals(team)) {
this.team = team;
requestUpdate();
}
return;
}
// simplified from (this.team != null && team == null) || (this.team == null && team != null)
if (this.team != null || team != null) {
this.team = team;
requestUpdate();
}
}
private void requestUpdate() {
this.lastUpdate = 0;
}
public ScoreInfo cachedInfo() {
return cachedInfo;
}
public boolean exists() {
return cachedInfo != null;
}
public boolean onlyScoreValueChanged() {
return onlyScoreValueChanged;
}
}

Datei anzeigen

@ -0,0 +1,182 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.display.slot;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import java.util.List;
import org.cloudburstmc.nbt.NbtMapBuilder;
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.scoreboard.Objective;
import org.geysermc.geyser.scoreboard.ScoreReference;
import org.geysermc.geyser.scoreboard.UpdateType;
import org.geysermc.geyser.scoreboard.display.score.BelownameDisplayScore;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.translator.text.MessageTranslator;
import org.geysermc.mcprotocollib.protocol.codec.NbtComponentSerializer;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.BlankFormat;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.FixedFormat;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.NumberFormat;
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.StyledFormat;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
public class BelownameDisplaySlot extends DisplaySlot {
private final Long2ObjectMap<BelownameDisplayScore> displayScores = new Long2ObjectOpenHashMap<>();
public BelownameDisplaySlot(GeyserSession session, Objective objective) {
super(session, objective, ScoreboardPosition.BELOW_NAME);
}
@Override
protected void render0(List<ScoreInfo> addScores, List<ScoreInfo> removeScores) {
// how belowname works is that if the player itself has belowname as a display slot,
// every player entity will show a score below their name.
// when the objective is added, updated or removed we thus have to update the belowname for every player
// when an individual score is updated (score or number format) we have to update the individual player
// remove is handled in #remove()
if (updateType == UpdateType.ADD) {
for (PlayerEntity player : session.getEntityCache().getAllPlayerEntities()) {
playerRegistered(player);
}
return;
}
if (updateType == UpdateType.UPDATE) {
for (PlayerEntity player : session.getEntityCache().getAllPlayerEntities()) {
setBelowNameText(player, scoreFor(player.getUsername()));
}
updateType = UpdateType.NOTHING;
return;
}
for (var score : displayScores.values()) {
// we don't have to worry about a score not existing, because that's handled by both
// this method when an objective is added and addScore/playerRegistered.
// we only have to update them, if they have changed
// (or delete them, if the score no longer exists)
if (!score.shouldUpdate()) {
continue;
}
if (score.referenceRemoved()) {
clearBelowNameText(score.player());
continue;
}
score.markUpdated();
setBelowNameText(score.player(), score.reference());
}
}
@Override
public void remove() {
updateType = UpdateType.REMOVE;
for (PlayerEntity player : session.getEntityCache().getAllPlayerEntities()) {
clearBelowNameText(player);
}
}
@Override
public void addScore(ScoreReference reference) {
addDisplayScore(reference);
}
@Override
public void playerRegistered(PlayerEntity player) {
var reference = scoreFor(player.getUsername());
setBelowNameText(player, reference);
// keep track of score when the player is active
if (reference != null) {
// we already set the text, so we only have to update once the score does
addDisplayScore(player, reference).markUpdated();
}
}
@Override
public void playerRemoved(PlayerEntity player) {
displayScores.remove(player.getGeyserId());
}
private void addDisplayScore(ScoreReference reference) {
var players = session.getEntityCache().getPlayersByName(reference.name());
for (PlayerEntity player : players) {
addDisplayScore(player, reference);
}
}
private BelownameDisplayScore addDisplayScore(PlayerEntity player, ScoreReference reference) {
var score = new BelownameDisplayScore(this, objective.getScoreboard().nextId(), reference, player);
displayScores.put(player.getGeyserId(), score);
return score;
}
private void setBelowNameText(PlayerEntity player, ScoreReference reference) {
player.setBelowNameText(calculateBelowNameText(reference));
player.updateBedrockMetadata();
}
private void clearBelowNameText(PlayerEntity player) {
player.setBelowNameText(null);
player.updateBedrockMetadata();
}
private String calculateBelowNameText(ScoreReference reference) {
String numberString;
NumberFormat numberFormat = null;
// even if the player doesn't have a score, as long as belowname is on the client Java behaviour is
// to show them with a score of 0
int score = 0;
if (reference != null) {
score = reference.score();
numberFormat = reference.numberFormat();
}
if (numberFormat == null) {
numberFormat = objective.getNumberFormat();
}
if (numberFormat instanceof BlankFormat) {
numberString = "";
} else if (numberFormat instanceof FixedFormat fixedFormat) {
numberString = MessageTranslator.convertMessage(fixedFormat.getValue(), session.locale());
} else if (numberFormat instanceof StyledFormat styledFormat) {
NbtMapBuilder styledAmount = styledFormat.getStyle().toBuilder();
styledAmount.putString("text", String.valueOf(score));
numberString = MessageTranslator.convertJsonMessage(
NbtComponentSerializer.tagComponentToJson(styledAmount.build()).toString(), session.locale());
} else {
numberString = String.valueOf(score);
}
return numberString + " " + ChatColor.RESET + objective.getDisplayName();
}
private ScoreReference scoreFor(String username) {
return objective.getScores().get(username);
}
}

Datei anzeigen

@ -0,0 +1,162 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.display.slot;
import java.util.List;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
import org.cloudburstmc.protocol.bedrock.packet.RemoveObjectivePacket;
import org.cloudburstmc.protocol.bedrock.packet.SetDisplayObjectivePacket;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.scoreboard.Objective;
import org.geysermc.geyser.scoreboard.ScoreReference;
import org.geysermc.geyser.scoreboard.UpdateType;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
public abstract class DisplaySlot {
protected final GeyserSession session;
protected final Objective objective;
/**
* Use this instead of objective name because one objective can be shared in multiple slots,
* but each slot has its own logic and might not contain all scores
*/
protected final String objectiveId;
protected final ScoreboardPosition slot;
protected final TeamColor teamColor;
protected final String positionName;
protected UpdateType updateType = UpdateType.ADD;
public DisplaySlot(GeyserSession session, Objective objective, ScoreboardPosition slot) {
this.session = session;
this.objective = objective;
this.objectiveId = String.valueOf(objective.getScoreboard().nextId());
this.slot = slot;
this.teamColor = teamColor(slot);
this.positionName = positionName(slot);
}
public final void render(List<ScoreInfo> addScores, List<ScoreInfo> removeScores) {
if (updateType == UpdateType.REMOVE) {
return;
}
render0(addScores, removeScores);
}
protected abstract void render0(List<ScoreInfo> addScores, List<ScoreInfo> removeScores);
public abstract void addScore(ScoreReference reference);
public abstract void playerRegistered(PlayerEntity player);
public abstract void playerRemoved(PlayerEntity player);
public void remove() {
updateType = UpdateType.REMOVE;
sendRemoveObjective();
}
public void markNeedsUpdate() {
if (updateType == UpdateType.NOTHING) {
updateType = UpdateType.UPDATE;
}
}
protected void sendDisplayObjective() {
SetDisplayObjectivePacket packet = new SetDisplayObjectivePacket();
packet.setObjectiveId(objectiveId());
packet.setDisplayName(objective.getDisplayName());
packet.setCriteria("dummy");
packet.setDisplaySlot(positionName);
packet.setSortOrder(1); // 0 = ascending, 1 = descending
session.sendUpstreamPacket(packet);
}
protected void sendRemoveObjective() {
RemoveObjectivePacket packet = new RemoveObjectivePacket();
packet.setObjectiveId(objectiveId());
session.sendUpstreamPacket(packet);
}
public Objective objective() {
return objective;
}
public String objectiveId() {
return objectiveId;
}
public ScoreboardPosition position() {
return slot;
}
public @Nullable TeamColor teamColor() {
return teamColor;
}
public UpdateType updateType() {
return updateType;
}
public static ScoreboardPosition slotCategory(ScoreboardPosition slot) {
return switch (slot) {
case BELOW_NAME -> ScoreboardPosition.BELOW_NAME;
case PLAYER_LIST -> ScoreboardPosition.PLAYER_LIST;
default -> ScoreboardPosition.SIDEBAR;
};
}
private static String positionName(ScoreboardPosition slot) {
return switch (slot) {
case BELOW_NAME -> "belowname";
case PLAYER_LIST -> "list";
default -> "sidebar";
};
}
private static @Nullable TeamColor teamColor(ScoreboardPosition slot) {
return switch (slot) {
case SIDEBAR_TEAM_RED -> TeamColor.RED;
case SIDEBAR_TEAM_AQUA -> TeamColor.AQUA;
case SIDEBAR_TEAM_BLUE -> TeamColor.BLUE;
case SIDEBAR_TEAM_GOLD -> TeamColor.GOLD;
case SIDEBAR_TEAM_GRAY -> TeamColor.GRAY;
case SIDEBAR_TEAM_BLACK -> TeamColor.BLACK;
case SIDEBAR_TEAM_GREEN -> TeamColor.GREEN;
case SIDEBAR_TEAM_WHITE -> TeamColor.WHITE;
case SIDEBAR_TEAM_YELLOW -> TeamColor.YELLOW;
case SIDEBAR_TEAM_DARK_RED -> TeamColor.DARK_RED;
case SIDEBAR_TEAM_DARK_AQUA -> TeamColor.DARK_AQUA;
case SIDEBAR_TEAM_DARK_BLUE -> TeamColor.DARK_BLUE;
case SIDEBAR_TEAM_DARK_GRAY -> TeamColor.DARK_GRAY;
case SIDEBAR_TEAM_DARK_GREEN -> TeamColor.DARK_GREEN;
case SIDEBAR_TEAM_DARK_PURPLE -> TeamColor.DARK_PURPLE;
case SIDEBAR_TEAM_LIGHT_PURPLE -> TeamColor.LIGHT_PURPLE;
default -> null;
};
}
}

Datei anzeigen

@ -0,0 +1,158 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.display.slot;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectMaps;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.scoreboard.Objective;
import org.geysermc.geyser.scoreboard.ScoreReference;
import org.geysermc.geyser.scoreboard.UpdateType;
import org.geysermc.geyser.scoreboard.display.score.PlayerlistDisplayScore;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
public class PlayerlistDisplaySlot extends DisplaySlot {
private final Long2ObjectMap<PlayerlistDisplayScore> displayScores =
Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap<>());
private final List<PlayerlistDisplayScore> removedScores = Collections.synchronizedList(new ArrayList<>());
public PlayerlistDisplaySlot(GeyserSession session, Objective objective) {
super(session, objective, ScoreboardPosition.PLAYER_LIST);
registerExisting();
}
@Override
protected void render0(List<ScoreInfo> addScores, List<ScoreInfo> removeScores) {
boolean objectiveAdd = updateType == UpdateType.ADD;
boolean objectiveUpdate = updateType == UpdateType.UPDATE;
boolean objectiveNothing = updateType == UpdateType.NOTHING;
// if 'add' the scores aren't present, if 'update' the objective is re-added so the scores don't have to be
// manually removed, if 'remove' the scores are removed anyway
if (objectiveNothing) {
var removedScoresCopy = new ArrayList<>(removedScores);
for (var removedScore : removedScoresCopy) {
//todo idk if this if-statement is needed
if (removedScore.cachedInfo() != null) {
removeScores.add(removedScore.cachedInfo());
}
}
removedScores.removeAll(removedScoresCopy);
} else {
removedScores.clear();
}
for (var score : displayScores.values()) {
if (score.referenceRemoved()) {
ScoreInfo cachedInfo = score.cachedInfo();
// cachedInfo can be null here when ScoreboardUpdater is being used and a score is added and
// removed before a single update cycle is performed
if (cachedInfo != null) {
removeScores.add(cachedInfo);
}
continue;
}
//todo does an animated title exist on tab?
boolean add = objectiveAdd || objectiveUpdate;
boolean exists = score.exists();
if (score.shouldUpdate()) {
score.update(objective);
add = true;
}
if (add) {
addScores.add(score.cachedInfo());
}
// we need this as long as MCPE-143063 hasn't been fixed.
// the checks after 'add' are there to prevent removing scores that
// are going to be removed anyway / don't need to be removed
if (add && exists && objectiveNothing) {
removeScores.add(score.cachedInfo());
}
}
if (objectiveUpdate) {
sendRemoveObjective();
}
if (objectiveAdd || objectiveUpdate) {
sendDisplayObjective();
}
updateType = UpdateType.NOTHING;
}
@Override
public void addScore(ScoreReference reference) {
// while it breaks a lot of stuff in Java, scoreboard do work fine with multiple players having
// the same username
var players = session.getEntityCache().getPlayersByName(reference.name());
var selfPlayer = session.getPlayerEntity();
if (reference.name().equals(selfPlayer.getUsername())) {
players.add(selfPlayer);
}
for (PlayerEntity player : players) {
var score =
new PlayerlistDisplayScore(this, objective.getScoreboard().nextId(), reference, player.getGeyserId());
displayScores.put(player.getGeyserId(), score);
}
}
private void registerExisting() {
playerRegistered(session.getPlayerEntity());
session.getEntityCache().getAllPlayerEntities().forEach(this::playerRegistered);
}
@Override
public void playerRegistered(PlayerEntity player) {
var reference = objective.getScores().get(player.getUsername());
if (reference == null) {
return;
}
var score =
new PlayerlistDisplayScore(this, objective.getScoreboard().nextId(), reference, player.getGeyserId());
displayScores.put(player.getGeyserId(), score);
}
@Override
public void playerRemoved(PlayerEntity player) {
var score = displayScores.remove(player.getGeyserId());
if (score == null) {
return;
}
removedScores.add(score);
}
}

Datei anzeigen

@ -0,0 +1,189 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.display.slot;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.scoreboard.Objective;
import org.geysermc.geyser.scoreboard.ScoreReference;
import org.geysermc.geyser.scoreboard.Team;
import org.geysermc.geyser.scoreboard.UpdateType;
import org.geysermc.geyser.scoreboard.display.score.SidebarDisplayScore;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
public final class SidebarDisplaySlot extends DisplaySlot {
private static final int SCORE_DISPLAY_LIMIT = 15;
private static final Comparator<ScoreReference> SCORE_DISPLAY_ORDER =
Comparator.comparing(ScoreReference::score)
.reversed()
.thenComparing(ScoreReference::name, String.CASE_INSENSITIVE_ORDER);
private List<SidebarDisplayScore> displayScores = new ArrayList<>(SCORE_DISPLAY_LIMIT);
public SidebarDisplaySlot(GeyserSession session, Objective objective, ScoreboardPosition position) {
super(session, objective, position);
}
@Override
protected void render0(List<ScoreInfo> addScores, List<ScoreInfo> removeScores) {
// while one could argue that we may not have to do this fancy Java filter when there are fewer scores than the
// line limit, we would lose the correct order of the scores if we don't
var newDisplayScores =
objective.getScores().values().stream()
.filter(score -> !score.hidden())
.sorted(SCORE_DISPLAY_ORDER)
.limit(SCORE_DISPLAY_LIMIT)
.map(reference -> {
// pretty much an ArrayList#remove
var iterator = this.displayScores.iterator();
while (iterator.hasNext()) {
var score = iterator.next();
if (score.name().equals(reference.name())) {
iterator.remove();
return score;
}
}
// new score, so it should be added
return new SidebarDisplayScore(this, objective.getScoreboard().nextId(), reference);
}).collect(Collectors.toList());
// in newDisplayScores we removed the items that were already present from displayScores,
// meaning that the items that remain are items that are no longer displayed
for (var score : this.displayScores) {
removeScores.add(score.cachedInfo());
}
// preserves the new order
this.displayScores = newDisplayScores;
// fixes ordering issues with multiple entries with same score
if (!this.displayScores.isEmpty()) {
SidebarDisplayScore lastScore = null;
int count = 0;
for (var score : this.displayScores) {
if (lastScore == null) {
lastScore = score;
continue;
}
if (score.score() == lastScore.score()) {
// something to keep in mind is that Bedrock doesn't support some legacy color codes and adds some
// codes as well, so if the line limit is every increased keep that in mind
if (count == 0) {
lastScore.order(ChatColor.styleOrder(count++));
}
score.order(ChatColor.styleOrder(count++));
} else {
if (count == 0) {
lastScore.order(null);
}
count = 0;
}
lastScore = score;
}
if (count == 0 && lastScore != null) {
lastScore.order(null);
}
}
boolean objectiveAdd = updateType == UpdateType.ADD;
boolean objectiveUpdate = updateType == UpdateType.UPDATE;
for (var score : this.displayScores) {
Team team = score.team();
boolean add = objectiveAdd || objectiveUpdate;
boolean exists = score.exists();
if (team != null) {
// entities are mostly removed from teams without notifying the scores.
if (team.shouldRemove() || !team.hasEntity(score.name())) {
score.team(null);
add = true;
}
}
if (score.shouldUpdate()) {
score.update(objective);
add = true;
}
if (add) {
addScores.add(score.cachedInfo());
}
// we need this as long as MCPE-143063 hasn't been fixed.
// the checks after 'add' are there to prevent removing scores that
// are going to be removed anyway / don't need to be removed
if (add && exists && !(objectiveUpdate || objectiveAdd) && !score.onlyScoreValueChanged()) {
removeScores.add(score.cachedInfo());
}
}
if (objectiveUpdate) {
sendRemoveObjective();
}
if (objectiveAdd || objectiveUpdate) {
sendDisplayObjective();
}
updateType = UpdateType.NOTHING;
}
@Override
public void addScore(ScoreReference reference) {
// we handle them a bit different: we sort the scores, and we add them ourselves
}
@Override
public void playerRegistered(PlayerEntity player) {
}
@Override
public void playerRemoved(PlayerEntity player) {
}
public void setTeamFor(Team team, Set<String> entities) {
// we only have to worry about scores that are currently displayed,
// because the constructor of the display score fetches the team
for (var score : displayScores) {
if (entities.contains(score.name())) {
score.team(team);
}
}
}
}

Datei anzeigen

@ -74,6 +74,7 @@ import org.cloudburstmc.protocol.bedrock.data.SpawnBiomeType;
import org.cloudburstmc.protocol.bedrock.data.command.CommandEnumData; 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.command.SoftEnumUpdateType;
import org.cloudburstmc.protocol.bedrock.data.definitions.DimensionDefinition;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag; import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData; import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
import org.cloudburstmc.protocol.bedrock.packet.AvailableEntityIdentifiersPacket; import org.cloudburstmc.protocol.bedrock.packet.AvailableEntityIdentifiersPacket;
@ -84,6 +85,7 @@ import org.cloudburstmc.protocol.bedrock.packet.ChunkRadiusUpdatedPacket;
import org.cloudburstmc.protocol.bedrock.packet.ClientboundCloseFormPacket; import org.cloudburstmc.protocol.bedrock.packet.ClientboundCloseFormPacket;
import org.cloudburstmc.protocol.bedrock.packet.CraftingDataPacket; import org.cloudburstmc.protocol.bedrock.packet.CraftingDataPacket;
import org.cloudburstmc.protocol.bedrock.packet.CreativeContentPacket; import org.cloudburstmc.protocol.bedrock.packet.CreativeContentPacket;
import org.cloudburstmc.protocol.bedrock.packet.DimensionDataPacket;
import org.cloudburstmc.protocol.bedrock.packet.EmoteListPacket; import org.cloudburstmc.protocol.bedrock.packet.EmoteListPacket;
import org.cloudburstmc.protocol.bedrock.packet.GameRulesChangedPacket; import org.cloudburstmc.protocol.bedrock.packet.GameRulesChangedPacket;
import org.cloudburstmc.protocol.bedrock.packet.ItemComponentPacket; import org.cloudburstmc.protocol.bedrock.packet.ItemComponentPacket;
@ -175,7 +177,6 @@ import org.geysermc.geyser.text.MinecraftLocale;
import org.geysermc.geyser.translator.inventory.InventoryTranslator; import org.geysermc.geyser.translator.inventory.InventoryTranslator;
import org.geysermc.geyser.translator.text.MessageTranslator; 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.EntityUtils; import org.geysermc.geyser.util.EntityUtils;
import org.geysermc.geyser.util.LoginEncryptionUtils; import org.geysermc.geyser.util.LoginEncryptionUtils;
import org.geysermc.geyser.util.MinecraftAuthLogger; import org.geysermc.geyser.util.MinecraftAuthLogger;
@ -388,6 +389,10 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
@Setter @Setter
private boolean sprinting; private boolean sprinting;
/**
* The overworld dimension which Bedrock Edition uses.
*/
private BedrockDimension bedrockOverworldDimension = BedrockDimension.OVERWORLD;
/** /**
* The dimension of the player. * The dimension of the player.
* As all entities are in the same world, this can be safely applied to all other entities. * As all entities are in the same world, this can be safely applied to all other entities.
@ -401,7 +406,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
* right before the StartGamePacket is sent. * right before the StartGamePacket is sent.
*/ */
@Setter @Setter
private BedrockDimension bedrockDimension = BedrockDimension.OVERWORLD; private BedrockDimension bedrockDimension = this.bedrockOverworldDimension;
@Setter @Setter
private int breakingBlock; private int breakingBlock;
@ -711,6 +716,31 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
* Send all necessary packets to load Bedrock into the server * Send all necessary packets to load Bedrock into the server
*/ */
public void connect() { public void connect() {
// Note: this.dimensionType may be null here if the player is connecting from online mode
int minY = BedrockDimension.OVERWORLD.minY();
int maxY = BedrockDimension.OVERWORLD.maxY();
for (JavaDimension javaDimension : this.registryCache.dimensions().values()) {
if (javaDimension.bedrockId() == BedrockDimension.OVERWORLD_ID) {
minY = Math.min(minY, javaDimension.minY());
maxY = Math.max(maxY, javaDimension.maxY());
}
}
minY = Math.max(minY, -512);
maxY = Math.min(maxY, 512);
if (minY < BedrockDimension.OVERWORLD.minY() || maxY > BedrockDimension.OVERWORLD.maxY()) {
final boolean isInOverworld = this.bedrockDimension == this.bedrockOverworldDimension;
this.bedrockOverworldDimension = new BedrockDimension(minY, maxY - minY, true, BedrockDimension.OVERWORLD_ID);
if (isInOverworld) {
this.bedrockDimension = this.bedrockOverworldDimension;
}
geyser.getLogger().debug("Extending overworld dimension to " + minY + " - " + maxY);
DimensionDataPacket dimensionDataPacket = new DimensionDataPacket();
dimensionDataPacket.getDefinitions().add(new DimensionDefinition("minecraft:overworld", maxY, minY, 5 /* Void */));
upstream.sendPacket(dimensionDataPacket);
}
startGame(); startGame();
sentSpawnPacket = true; sentSpawnPacket = true;
syncEntityProperties(); syncEntityProperties();
@ -933,8 +963,6 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
// Start ticking // Start ticking
tickThread = eventLoop.scheduleAtFixedRate(this::tick, 50, 50, TimeUnit.MILLISECONDS); tickThread = eventLoop.scheduleAtFixedRate(this::tick, 50, 50, TimeUnit.MILLISECONDS);
this.protocol.setUseDefaultListeners(false);
TcpSession downstream; TcpSession downstream;
if (geyser.getBootstrap().getSocketAddress() != null) { if (geyser.getBootstrap().getSocketAddress() != null) {
// We're going to connect through the JVM and not through TCP // We're going to connect through the JVM and not through TCP
@ -960,7 +988,6 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
this.downstream.getSession().setFlag(MinecraftConstants.FOLLOW_TRANSFERS, false); this.downstream.getSession().setFlag(MinecraftConstants.FOLLOW_TRANSFERS, false);
if (geyser.getConfig().getRemote().isUseProxyProtocol()) { if (geyser.getConfig().getRemote().isUseProxyProtocol()) {
downstream.setFlag(BuiltinFlags.ENABLE_CLIENT_PROXY_PROTOCOL, true);
downstream.setFlag(BuiltinFlags.CLIENT_PROXIED_ADDRESS, upstream.getAddress()); downstream.setFlag(BuiltinFlags.CLIENT_PROXIED_ADDRESS, upstream.getAddress());
} }
if (geyser.getConfig().isForwardPlayerPing()) { if (geyser.getConfig().isForwardPlayerPing()) {
@ -970,22 +997,6 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
// We'll handle this since we have the registry data on hand // We'll handle this since we have the registry data on hand
downstream.setFlag(MinecraftConstants.SEND_BLANK_KNOWN_PACKS_RESPONSE, false); downstream.setFlag(MinecraftConstants.SEND_BLANK_KNOWN_PACKS_RESPONSE, false);
// This isn't a great solution, but... we want to make sure the finish configuration packet cannot be sent
// before the KnownPacks packet.
this.downstream.getSession().addListener(new ClientListener(ProtocolState.LOGIN, loginEvent.transferring()) {
@Override
public void packetReceived(Session session, Packet packet) {
if (protocol.getState() == ProtocolState.CONFIGURATION) {
if (packet instanceof ClientboundFinishConfigurationPacket) {
// Prevent
GeyserSession.this.ensureInEventLoop(() -> GeyserSession.this.sendDownstreamPacket(new ServerboundFinishConfigurationPacket()));
return;
}
}
super.packetReceived(session, packet);
}
});
downstream.addListener(new SessionAdapter() { downstream.addListener(new SessionAdapter() {
@Override @Override
public void packetSending(PacketSendingEvent event) { public void packetSending(PacketSendingEvent event) {
@ -1594,7 +1605,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
startGamePacket.setRotation(Vector2f.from(1, 1)); startGamePacket.setRotation(Vector2f.from(1, 1));
startGamePacket.setSeed(-1L); startGamePacket.setSeed(-1L);
startGamePacket.setDimensionId(DimensionUtils.javaToBedrock(bedrockDimension)); startGamePacket.setDimensionId(bedrockDimension.bedrockId());
startGamePacket.setGeneratorId(1); startGamePacket.setGeneratorId(1);
startGamePacket.setLevelGameType(GameType.SURVIVAL); startGamePacket.setLevelGameType(GameType.SURVIVAL);
startGamePacket.setDifficulty(1); startGamePacket.setDifficulty(1);
@ -1758,8 +1769,8 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
return; return;
} }
if (protocol.getState() != intendedState) { if (protocol.getOutboundState() != intendedState) {
geyser.getLogger().debug("Tried to send " + packet.getClass().getSimpleName() + " packet while not in " + intendedState.name() + " state"); geyser.getLogger().debug("Tried to send " + packet.getClass().getSimpleName() + " packet while not in " + intendedState.name() + " outbound state");
return; return;
} }
@ -1793,7 +1804,7 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
} }
private void sendDownstreamPacket0(Packet packet) { private void sendDownstreamPacket0(Packet packet) {
ProtocolState state = protocol.getState(); ProtocolState state = protocol.getOutboundState();
if (state == ProtocolState.GAME || state == ProtocolState.CONFIGURATION || packet.getClass() == ServerboundCustomQueryAnswerPacket.class) { if (state == ProtocolState.GAME || state == ProtocolState.CONFIGURATION || packet.getClass() == ServerboundCustomQueryAnswerPacket.class) {
downstream.sendPacket(packet); downstream.sendPacket(packet);
} else { } else {

Datei anzeigen

@ -31,15 +31,18 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectArrayList; import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
import lombok.Getter; import lombok.Getter;
import org.geysermc.geyser.entity.type.Entity; import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.Tickable; import org.geysermc.geyser.entity.type.Tickable;
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 java.util.*;
import java.util.concurrent.atomic.AtomicLong;
/** /**
* Each session has its own EntityCache in the occasion that an entity packet is sent specifically * Each session has its own EntityCache in the occasion that an entity packet is sent specifically
* for that player (e.g. seeing vanished players from /vanish) * for that player (e.g. seeing vanished players from /vanish)
@ -68,6 +71,10 @@ public class EntityCache {
if (cacheEntity(entity)) { if (cacheEntity(entity)) {
entity.spawnEntity(); entity.spawnEntity();
// start tracking newly spawned entities.
// This is however not called for players, that's done in addPlayerEntity
session.getWorldCache().getScoreboard().entityRegistered(entity);
if (entity instanceof Tickable) { if (entity instanceof Tickable) {
// Start ticking it // Start ticking it
tickableEntities.add((Tickable) entity); tickableEntities.add((Tickable) entity);
@ -86,21 +93,24 @@ public class EntityCache {
} }
public void removeEntity(Entity entity) { public void removeEntity(Entity entity) {
if (entity == null) {
return;
}
if (entity instanceof PlayerEntity player) { if (entity instanceof PlayerEntity player) {
session.getPlayerWithCustomHeads().remove(player.getUuid()); session.getPlayerWithCustomHeads().remove(player.getUuid());
} }
if (entity != null) { if (entity.isValid()) {
if (entity.isValid()) { entity.despawnEntity();
entity.despawnEntity(); }
} entities.remove(entityIdTranslations.remove(entity.getEntityId()));
long geyserId = entityIdTranslations.remove(entity.getEntityId()); // don't track the entity anymore, now that it's removed
entities.remove(geyserId); session.getWorldCache().getScoreboard().entityRemoved(entity);
if (entity instanceof Tickable) { if (entity instanceof Tickable) {
tickableEntities.remove(entity); tickableEntities.remove(entity);
}
} }
} }
@ -126,15 +136,39 @@ public class EntityCache {
public void addPlayerEntity(PlayerEntity entity) { public void addPlayerEntity(PlayerEntity entity) {
// putIfAbsent matches the behavior of playerInfoMap in Java as of 1.19.3 // putIfAbsent matches the behavior of playerInfoMap in Java as of 1.19.3
playerEntities.putIfAbsent(entity.getUuid(), entity); boolean exists = playerEntities.putIfAbsent(entity.getUuid(), entity) != null;
if (exists) {
return;
}
// notify scoreboard for new entity
var scoreboard = session.getWorldCache().getScoreboard();
scoreboard.playerRegistered(entity);
// spawnPlayer's entityRegistered is not called for players
scoreboard.entityRegistered(entity);
} }
public PlayerEntity getPlayerEntity(UUID uuid) { public PlayerEntity getPlayerEntity(UUID uuid) {
return playerEntities.get(uuid); return playerEntities.get(uuid);
} }
public List<PlayerEntity> getPlayersByName(String name) {
var list = new ArrayList<PlayerEntity>();
for (PlayerEntity player : playerEntities.values()) {
if (name.equals(player.getUsername())) {
list.add(player);
}
}
return list;
}
public PlayerEntity removePlayerEntity(UUID uuid) { public PlayerEntity removePlayerEntity(UUID uuid) {
return playerEntities.remove(uuid); var player = playerEntities.remove(uuid);
if (player != null) {
// notify scoreboard
session.getWorldCache().getScoreboard().playerRemoved(player);
}
return player;
} }
public Collection<PlayerEntity> getAllPlayerEntities() { public Collection<PlayerEntity> getAllPlayerEntities() {

Datei anzeigen

@ -31,6 +31,7 @@ import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.protocol.bedrock.packet.SetTitlePacket; import org.cloudburstmc.protocol.bedrock.packet.SetTitlePacket;
@ -49,7 +50,7 @@ public final class WorldCache {
@Getter @Getter
private final ScoreboardSession scoreboardSession; private final ScoreboardSession scoreboardSession;
@Getter @Getter
private Scoreboard scoreboard; private @NonNull Scoreboard scoreboard;
@Getter @Getter
@Setter @Setter
private Difficulty difficulty = Difficulty.EASY; private Difficulty difficulty = Difficulty.EASY;
@ -81,10 +82,8 @@ public final class WorldCache {
} }
public void removeScoreboard() { public void removeScoreboard() {
if (scoreboard != null) { scoreboard.removeScoreboard();
scoreboard.removeScoreboard(); scoreboard = new Scoreboard(session);
scoreboard = new Scoreboard(session);
}
} }
public int increaseAndGetScoreboardPacketsPerSecond() { public int increaseAndGetScoreboardPacketsPerSecond() {

Datei anzeigen

@ -25,6 +25,8 @@
package org.geysermc.geyser.text; package org.geysermc.geyser.text;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
public class ChatColor { public class ChatColor {
public static final String ANSI_RESET = (char) 0x1b + "[0m"; public static final String ANSI_RESET = (char) 0x1b + "[0m";
@ -84,4 +86,58 @@ public class ChatColor {
string = string.replace(WHITE, (char) 0x1b + "[37;1m"); string = string.replace(WHITE, (char) 0x1b + "[37;1m");
return string; return string;
} }
}
public static String styleOrder(int index) {
// https://bugs.mojang.com/browse/MCPE-41729
// strikethrough and underlined do not exist on Bedrock
return switch (index) {
case 0 -> BLACK;
case 1 -> DARK_BLUE;
case 2 -> DARK_GREEN;
case 3 -> DARK_AQUA;
case 4 -> DARK_RED;
case 5 -> DARK_PURPLE;
case 6 -> GOLD;
case 7 -> GRAY;
case 8 -> DARK_GRAY;
case 9 -> BLUE;
case 10 -> GREEN;
case 11 -> AQUA;
case 12 -> RED;
case 13 -> LIGHT_PURPLE;
case 14 -> YELLOW;
case 15 -> WHITE;
case 16 -> OBFUSCATED;
case 17 -> BOLD;
default -> ITALIC;
};
}
public static String chatColorFor(TeamColor teamColor) {
// https://bugs.mojang.com/browse/MCPE-41729
// strikethrough and underlined do not exist on Bedrock
return switch (teamColor) {
case BLACK -> BLACK;
case DARK_BLUE -> DARK_BLUE;
case DARK_GREEN -> DARK_GREEN;
case DARK_AQUA -> DARK_AQUA;
case DARK_RED -> DARK_RED;
case DARK_PURPLE -> DARK_PURPLE;
case GOLD -> GOLD;
case GRAY -> GRAY;
case DARK_GRAY -> DARK_GRAY;
case BLUE -> BLUE;
case GREEN -> GREEN;
case AQUA -> AQUA;
case RED -> RED;
case LIGHT_PURPLE -> LIGHT_PURPLE;
case YELLOW -> YELLOW;
case WHITE -> WHITE;
case OBFUSCATED -> OBFUSCATED;
case BOLD -> BOLD;
case STRIKETHROUGH, UNDERLINED -> "";
case ITALIC -> ITALIC;
default -> RESET;
};
}
}

Datei anzeigen

@ -33,6 +33,7 @@ import org.geysermc.erosion.Constants;
import org.geysermc.floodgate.pluginmessage.PluginMessageChannels; import org.geysermc.floodgate.pluginmessage.PluginMessageChannels;
import org.geysermc.geyser.api.network.AuthType; import org.geysermc.geyser.api.network.AuthType;
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity; import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
import org.geysermc.geyser.level.BedrockDimension;
import org.geysermc.geyser.level.JavaDimension; import org.geysermc.geyser.level.JavaDimension;
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;
@ -62,7 +63,7 @@ public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket
// If the player is already initialized and a join game packet is sent, they // If the player is already initialized and a join game packet is sent, they
// are swapping servers // are swapping servers
if (session.isSpawned()) { if (session.isSpawned()) {
int fakeDim = DimensionUtils.getTemporaryDimension(DimensionUtils.javaToBedrock(session.getBedrockDimension()), newDimension.bedrockId()); int fakeDim = DimensionUtils.getTemporaryDimension(session.getBedrockDimension().bedrockId(), newDimension.bedrockId());
if (fakeDim != newDimension.bedrockId()) { if (fakeDim != newDimension.bedrockId()) {
// The player's current dimension and new dimension are the same // The player's current dimension and new dimension are the same
// We want a dimension switch to clear old chunks out, so switch to a dimension that isn't the one we're currently in. // We want a dimension switch to clear old chunks out, so switch to a dimension that isn't the one we're currently in.
@ -121,9 +122,9 @@ public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket
} }
session.sendDownstreamPacket(new ServerboundCustomPayloadPacket(register, Constants.PLUGIN_MESSAGE.getBytes(StandardCharsets.UTF_8))); session.sendDownstreamPacket(new ServerboundCustomPayloadPacket(register, Constants.PLUGIN_MESSAGE.getBytes(StandardCharsets.UTF_8)));
if (DimensionUtils.javaToBedrock(session.getBedrockDimension()) != newDimension.bedrockId()) { if (session.getBedrockDimension().bedrockId() != newDimension.bedrockId()) {
DimensionUtils.switchDimension(session, newDimension); DimensionUtils.switchDimension(session, newDimension);
} else if (DimensionUtils.isCustomBedrockNetherId() && newDimension.isNetherLike()) { } else if (BedrockDimension.isCustomBedrockNetherId() && newDimension.isNetherLike()) {
// If the player is spawning into the "fake" nether, send them some fog // If the player is spawning into the "fake" nether, send them some fog
session.camera().sendFog(DimensionUtils.BEDROCK_FOG_HELL); session.camera().sendFog(DimensionUtils.BEDROCK_FOG_HELL);
} }

Datei anzeigen

@ -29,7 +29,12 @@ import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.ByteBufOutputStream; import io.netty.buffer.ByteBufOutputStream;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
import it.unimi.dsi.fastutil.ints.*; import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntImmutableList;
import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.ints.IntLists;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import it.unimi.dsi.fastutil.objects.ObjectArrayList; import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import org.cloudburstmc.math.vector.Vector3i; import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.nbt.NBTOutputStream; import org.cloudburstmc.nbt.NBTOutputStream;
@ -56,7 +61,6 @@ 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.util.BlockEntityUtils; import org.geysermc.geyser.util.BlockEntityUtils;
import org.geysermc.geyser.util.ChunkUtils; import org.geysermc.geyser.util.ChunkUtils;
import org.geysermc.geyser.util.DimensionUtils;
import org.geysermc.mcprotocollib.protocol.data.game.chunk.BitStorage; import org.geysermc.mcprotocollib.protocol.data.game.chunk.BitStorage;
import org.geysermc.mcprotocollib.protocol.data.game.chunk.ChunkSection; import org.geysermc.mcprotocollib.protocol.data.game.chunk.ChunkSection;
import org.geysermc.mcprotocollib.protocol.data.game.chunk.DataPalette; import org.geysermc.mcprotocollib.protocol.data.game.chunk.DataPalette;
@ -509,7 +513,7 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
levelChunkPacket.setChunkX(packet.getX()); levelChunkPacket.setChunkX(packet.getX());
levelChunkPacket.setChunkZ(packet.getZ()); levelChunkPacket.setChunkZ(packet.getZ());
levelChunkPacket.setData(Unpooled.wrappedBuffer(payload)); levelChunkPacket.setData(Unpooled.wrappedBuffer(payload));
levelChunkPacket.setDimension(DimensionUtils.javaToBedrock(session.getBedrockDimension())); levelChunkPacket.setDimension(session.getBedrockDimension().bedrockId());
session.sendUpstreamPacket(levelChunkPacket); session.sendUpstreamPacket(levelChunkPacket);
for (Map.Entry<Vector3i, ItemFrameEntity> entry : session.getItemFrameCache().entrySet()) { for (Map.Entry<Vector3i, ItemFrameEntity> entry : session.getItemFrameCache().entrySet()) {

Datei anzeigen

@ -32,40 +32,22 @@ import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.WorldCache; import org.geysermc.geyser.session.cache.WorldCache;
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.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundResetScorePacket; import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundResetScorePacket;
@Translator(packet = ClientboundResetScorePacket.class) @Translator(packet = ClientboundResetScorePacket.class)
public class JavaResetScorePacket extends PacketTranslator<ClientboundResetScorePacket> { public class JavaResetScorePacket extends PacketTranslator<ClientboundResetScorePacket> {
@Override @Override
public void translate(GeyserSession session, ClientboundResetScorePacket packet) { public void translate(GeyserSession session, ClientboundResetScorePacket packet) {
WorldCache worldCache = session.getWorldCache(); WorldCache worldCache = session.getWorldCache();
Scoreboard scoreboard = worldCache.getScoreboard(); Scoreboard scoreboard = worldCache.getScoreboard();
int pps = worldCache.increaseAndGetScoreboardPacketsPerSecond(); int pps = worldCache.increaseAndGetScoreboardPacketsPerSecond();
Objective belowName = scoreboard.getObjectiveSlots().get(ScoreboardPosition.BELOW_NAME);
if (packet.getObjective() == null) { if (packet.getObjective() == null) {
// No objective name means all scores are reset for that player (/scoreboard players reset PLAYERNAME) // No objective name means all scores are reset for that player (/scoreboard players reset PLAYERNAME)
for (Objective otherObjective : scoreboard.getObjectives()) { scoreboard.resetPlayerScores(packet.getOwner());
otherObjective.removeScore(packet.getOwner());
}
// as described below
if (belowName != null) {
JavaSetScoreTranslator.setBelowName(session, belowName, packet.getOwner());
}
} else { } else {
Objective objective = scoreboard.getObjective(packet.getObjective()); Objective objective = scoreboard.getObjective(packet.getObjective());
objective.removeScore(packet.getOwner()); objective.removeScore(packet.getOwner());
// If this is the objective that is in use to show the below name text, we need to update the player
// attached to this score.
if (objective == belowName) {
// Update the score on this player to now reflect 0
JavaSetScoreTranslator.setBelowName(session, objective, packet.getOwner());
}
} }
// ScoreboardUpdater will handle it for us if the packets per second // ScoreboardUpdater will handle it for us if the packets per second

Datei anzeigen

@ -25,72 +25,45 @@
package org.geysermc.geyser.translator.protocol.java.scoreboard; package org.geysermc.geyser.translator.protocol.java.scoreboard;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ObjectiveAction;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetObjectivePacket;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.GeyserLogger;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.scoreboard.Objective; import org.geysermc.geyser.scoreboard.Objective;
import org.geysermc.geyser.scoreboard.Scoreboard; import org.geysermc.geyser.scoreboard.Scoreboard;
import org.geysermc.geyser.scoreboard.ScoreboardUpdater; import org.geysermc.geyser.scoreboard.ScoreboardUpdater;
import org.geysermc.geyser.scoreboard.UpdateType;
import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.WorldCache; import org.geysermc.geyser.session.cache.WorldCache;
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.mcprotocollib.protocol.data.game.scoreboard.ObjectiveAction;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetObjectivePacket;
@Translator(packet = ClientboundSetObjectivePacket.class) @Translator(packet = ClientboundSetObjectivePacket.class)
public class JavaSetObjectiveTranslator extends PacketTranslator<ClientboundSetObjectivePacket> { public class JavaSetObjectiveTranslator extends PacketTranslator<ClientboundSetObjectivePacket> {
private final GeyserLogger logger = GeyserImpl.getInstance().getLogger();
@Override @Override
public void translate(GeyserSession session, ClientboundSetObjectivePacket packet) { public void translate(GeyserSession session, ClientboundSetObjectivePacket packet) {
WorldCache worldCache = session.getWorldCache(); WorldCache worldCache = session.getWorldCache();
Scoreboard scoreboard = worldCache.getScoreboard(); Scoreboard scoreboard = worldCache.getScoreboard();
int pps = worldCache.increaseAndGetScoreboardPacketsPerSecond(); int pps = worldCache.increaseAndGetScoreboardPacketsPerSecond();
Objective objective = scoreboard.getObjective(packet.getName()); Objective objective;
if (objective != null && objective.getUpdateType() != UpdateType.REMOVE && packet.getAction() == ObjectiveAction.ADD) { if (packet.getAction() == ObjectiveAction.ADD) {
// matches vanilla behaviour objective = scoreboard.registerNewObjective(packet.getName());
logger.warning("An objective with the same name '" + packet.getName() + "' already exists! Ignoring packet"); } else {
objective = scoreboard.getObjective(packet.getName());
}
// matches vanilla
if (objective == null) {
return; return;
} }
if ((objective == null || objective.getUpdateType() == UpdateType.REMOVE) && packet.getAction() != ObjectiveAction.REMOVE) {
objective = scoreboard.registerNewObjective(packet.getName());
}
switch (packet.getAction()) { switch (packet.getAction()) {
case ADD, UPDATE -> { case ADD, UPDATE ->
objective.setDisplayName(MessageTranslator.convertMessage(packet.getDisplayName())) objective.updateProperties(packet.getDisplayName(), packet.getType(), packet.getNumberFormat());
.setNumberFormat(packet.getNumberFormat()) case REMOVE -> scoreboard.removeObjective(objective);
.setType(packet.getType().ordinal());
if (objective == scoreboard.getObjectiveSlots().get(ScoreboardPosition.BELOW_NAME)) {
// Update the score tag of all players
for (PlayerEntity entity : session.getEntityCache().getAllPlayerEntities()) {
if (entity.isValid()) {
entity.setBelowNameText(objective);
}
}
}
}
case REMOVE -> {
scoreboard.unregisterObjective(packet.getName());
if (objective != null && objective == scoreboard.getObjectiveSlots().get(ScoreboardPosition.BELOW_NAME)) {
// Clear the score tag from all players
for (PlayerEntity entity : session.getEntityCache().getAllPlayerEntities()) {
// Other places we check for the entity being valid,
// but we must set the below name text as null for all players
// or else PlayerEntity#spawnEntity will find a null objective and not touch EntityData#SCORE_TAG
entity.setBelowNameText(null);
}
}
}
} }
if (objective == null || !objective.isActive()) { // Scoreboard#removeObjective doesn't touch the display slot(s) that were attached to it.
// So Objective#hasDisplaySlot will be true as long as it's currently present on the Bedrock client
if (!objective.hasDisplaySlot()) {
return; return;
} }

Datei anzeigen

@ -25,23 +25,17 @@
package org.geysermc.geyser.translator.protocol.java.scoreboard; package org.geysermc.geyser.translator.protocol.java.scoreboard;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.NameTagVisibility; import java.util.Arrays;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamAction;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetPlayerTeamPacket;
import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.GeyserLogger; import org.geysermc.geyser.GeyserLogger;
import org.geysermc.geyser.scoreboard.Scoreboard; import org.geysermc.geyser.scoreboard.Scoreboard;
import org.geysermc.geyser.scoreboard.ScoreboardUpdater; import org.geysermc.geyser.scoreboard.ScoreboardUpdater;
import org.geysermc.geyser.scoreboard.Team; import org.geysermc.geyser.scoreboard.Team;
import org.geysermc.geyser.scoreboard.UpdateType;
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;
import org.geysermc.geyser.translator.text.MessageTranslator; import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamAction;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetPlayerTeamPacket;
import java.util.Arrays;
import java.util.Set;
@Translator(packet = ClientboundSetPlayerTeamPacket.class) @Translator(packet = ClientboundSetPlayerTeamPacket.class)
public class JavaSetPlayerTeamTranslator extends PacketTranslator<ClientboundSetPlayerTeamPacket> { public class JavaSetPlayerTeamTranslator extends PacketTranslator<ClientboundSetPlayerTeamPacket> {
@ -60,83 +54,45 @@ public class JavaSetPlayerTeamTranslator extends PacketTranslator<ClientboundSet
int pps = session.getWorldCache().increaseAndGetScoreboardPacketsPerSecond(); int pps = session.getWorldCache().increaseAndGetScoreboardPacketsPerSecond();
Scoreboard scoreboard = session.getWorldCache().getScoreboard(); Scoreboard scoreboard = session.getWorldCache().getScoreboard();
Team team = scoreboard.getTeam(packet.getTeamName());
switch (packet.getAction()) {
case CREATE -> {
team = scoreboard.registerNewTeam(packet.getTeamName(), packet.getPlayers())
.setName(MessageTranslator.convertMessage(packet.getDisplayName()))
.setColor(packet.getColor())
.setNameTagVisibility(packet.getNameTagVisibility())
.setPrefix(MessageTranslator.convertMessage(packet.getPrefix(), session.locale()))
.setSuffix(MessageTranslator.convertMessage(packet.getSuffix(), session.locale()));
if (packet.getPlayers().length != 0) { if (packet.getAction() == TeamAction.CREATE) {
if ((team.getNameTagVisibility() != NameTagVisibility.ALWAYS && !team.isVisibleFor(session.getPlayerEntity().getUsername())) scoreboard.registerNewTeam(
|| team.getColor() != TeamColor.RESET packet.getTeamName(),
|| !team.getCurrentData().getPrefix().isEmpty() packet.getPlayers(),
|| !team.getCurrentData().getSuffix().isEmpty()) { packet.getDisplayName(),
// Something is here that would modify entity names packet.getPrefix(),
scoreboard.updateEntityNames(team, true); packet.getSuffix(),
} packet.getNameTagVisibility(),
packet.getColor()
);
} else {
Team team = scoreboard.getTeam(packet.getTeamName());
if (team == null) {
if (logger.isDebug()) {
logger.debug("Error while translating Team Packet " + packet.getAction()
+ "! Scoreboard Team " + packet.getTeamName() + " is not registered."
);
} }
return;
} }
case UPDATE -> {
if (team == null) {
if (logger.isDebug()) {
logger.debug("Error while translating Team Packet " + packet.getAction()
+ "! Scoreboard Team " + packet.getTeamName() + " is not registered."
);
}
return;
}
TeamColor oldColor = team.getColor(); switch (packet.getAction()) {
NameTagVisibility oldVisibility = team.getNameTagVisibility(); case UPDATE -> {
String oldPrefix = team.getCurrentData().getPrefix(); team.updateProperties(
String oldSuffix = team.getCurrentData().getSuffix(); packet.getDisplayName(),
packet.getPrefix(),
team.setName(MessageTranslator.convertMessage(packet.getDisplayName())) packet.getSuffix(),
.setColor(packet.getColor()) packet.getNameTagVisibility(),
.setNameTagVisibility(packet.getNameTagVisibility()) packet.getColor()
.setPrefix(MessageTranslator.convertMessage(packet.getPrefix(), session.locale())) );
.setSuffix(MessageTranslator.convertMessage(packet.getSuffix(), session.locale()))
.setUpdateType(UpdateType.UPDATE);
if (oldVisibility != team.getNameTagVisibility()
|| oldColor != team.getColor()
|| !oldPrefix.equals(team.getCurrentData().getPrefix())
|| !oldSuffix.equals(team.getCurrentData().getSuffix())) {
// Update entities attached to this team as something about their nameplates have changed
scoreboard.updateEntityNames(team, false);
} }
case ADD_PLAYER -> team.addEntities(packet.getPlayers());
case REMOVE_PLAYER -> team.removeEntities(packet.getPlayers());
case REMOVE -> scoreboard.removeTeam(packet.getTeamName());
} }
case ADD_PLAYER -> {
if (team == null) {
if (logger.isDebug()) {
logger.debug("Error while translating Team Packet " + packet.getAction()
+ "! Scoreboard Team " + packet.getTeamName() + " is not registered."
);
}
return;
}
Set<String> added = team.addEntities(packet.getPlayers());
scoreboard.updateEntityNames(team, added, true);
}
case REMOVE_PLAYER -> {
if (team == null) {
if (logger.isDebug()) {
logger.debug("Error while translating Team Packet " + packet.getAction()
+ "! Scoreboard Team " + packet.getTeamName() + " is not registered."
);
}
return;
}
Set<String> removed = team.removeEntities(packet.getPlayers());
scoreboard.updateEntityNames(null, removed, true);
}
case REMOVE -> scoreboard.removeTeam(packet.getTeamName());
} }
// ScoreboardUpdater will handle it for us if the packets per second // ScoreboardUpdater will handle it for us if the packets per second
// (for score and team packets) is higher than the first threshold // (for score and team packets) is higher than the first threshold
if (pps < ScoreboardUpdater.FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD) { if (pps < ScoreboardUpdater.FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD) {

Datei anzeigen

@ -25,12 +25,8 @@
package org.geysermc.geyser.translator.protocol.java.scoreboard; package org.geysermc.geyser.translator.protocol.java.scoreboard;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetScorePacket;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.GeyserLogger; import org.geysermc.geyser.GeyserLogger;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.scoreboard.Objective; import org.geysermc.geyser.scoreboard.Objective;
import org.geysermc.geyser.scoreboard.Scoreboard; import org.geysermc.geyser.scoreboard.Scoreboard;
import org.geysermc.geyser.scoreboard.ScoreboardUpdater; import org.geysermc.geyser.scoreboard.ScoreboardUpdater;
@ -39,6 +35,7 @@ import org.geysermc.geyser.session.cache.WorldCache;
import org.geysermc.geyser.text.GeyserLocale; 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.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetScorePacket;
@Translator(packet = ClientboundSetScorePacket.class) @Translator(packet = ClientboundSetScorePacket.class)
public class JavaSetScoreTranslator extends PacketTranslator<ClientboundSetScorePacket> { public class JavaSetScoreTranslator extends PacketTranslator<ClientboundSetScorePacket> {
@ -63,16 +60,7 @@ public class JavaSetScoreTranslator extends PacketTranslator<ClientboundSetScore
} }
return; return;
} }
// If this is the objective that is in use to show the below name text, we need to update the player
// attached to this score.
boolean isBelowName = objective == scoreboard.getObjectiveSlots().get(ScoreboardPosition.BELOW_NAME);
objective.setScore(packet.getOwner(), packet.getValue(), packet.getDisplay(), packet.getNumberFormat()); objective.setScore(packet.getOwner(), packet.getValue(), packet.getDisplay(), packet.getNumberFormat());
if (isBelowName) {
// Update the below name score on this player
setBelowName(session, objective, packet.getOwner());
}
// ScoreboardUpdater will handle it for us if the packets per second // ScoreboardUpdater will handle it for us if the packets per second
// (for score and team packets) is higher than the first threshold // (for score and team packets) is higher than the first threshold
@ -80,36 +68,4 @@ public class JavaSetScoreTranslator extends PacketTranslator<ClientboundSetScore
scoreboard.onUpdate(); scoreboard.onUpdate();
} }
} }
/**
* @param objective the objective that currently resides on the below name display slot
*/
static void setBelowName(GeyserSession session, Objective objective, String username) {
PlayerEntity entity = getOtherPlayerEntity(session, username);
if (entity == null) {
return;
}
entity.setBelowNameText(objective);
}
private static @Nullable PlayerEntity getOtherPlayerEntity(GeyserSession session, String username) {
// We don't care about the session player, because... they're not going to be seeing their own score
if (session.getPlayerEntity().getUsername().equals(username)) {
return null;
}
for (PlayerEntity entity : session.getEntityCache().getAllPlayerEntities()) {
if (entity.getUsername().equals(username)) {
if (entity.isValid()) {
return entity;
} else {
// The below name text will be applied on spawn
return null;
}
}
}
return null;
}
} }

Datei anzeigen

@ -25,6 +25,8 @@
package org.geysermc.geyser.translator.text; package org.geysermc.geyser.translator.text;
import java.util.ArrayList;
import java.util.List;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.JoinConfiguration; import net.kyori.adventure.text.JoinConfiguration;
import net.kyori.adventure.text.ScoreComponent; import net.kyori.adventure.text.ScoreComponent;
@ -53,12 +55,6 @@ import org.geysermc.mcprotocollib.protocol.data.DefaultComponentSerializer;
import org.geysermc.mcprotocollib.protocol.data.game.Holder; import org.geysermc.mcprotocollib.protocol.data.game.Holder;
import org.geysermc.mcprotocollib.protocol.data.game.chat.ChatType; import org.geysermc.mcprotocollib.protocol.data.game.chat.ChatType;
import org.geysermc.mcprotocollib.protocol.data.game.chat.ChatTypeDecoration; import org.geysermc.mcprotocollib.protocol.data.game.chat.ChatTypeDecoration;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
public class MessageTranslator { public class MessageTranslator {
// These are used for handling the translations of the messages // These are used for handling the translations of the messages
@ -71,9 +67,6 @@ public class MessageTranslator {
private static final LegacyComponentSerializer BEDROCK_SERIALIZER; private static final LegacyComponentSerializer BEDROCK_SERIALIZER;
private static final String BEDROCK_COLORS; private static final String BEDROCK_COLORS;
// Store team colors for player names
private static final Map<TeamColor, String> TEAM_COLORS = new EnumMap<>(TeamColor.class);
// Legacy formatting character // Legacy formatting character
private static final String BASE = "\u00a7"; private static final String BASE = "\u00a7";
@ -81,31 +74,6 @@ public class MessageTranslator {
private static final String RESET = BASE + "r"; private static final String RESET = BASE + "r";
static { static {
TEAM_COLORS.put(TeamColor.RESET, RESET);
TEAM_COLORS.put(TeamColor.BLACK, BASE + "0");
TEAM_COLORS.put(TeamColor.DARK_BLUE, BASE + "1");
TEAM_COLORS.put(TeamColor.DARK_GREEN, BASE + "2");
TEAM_COLORS.put(TeamColor.DARK_AQUA, BASE + "3");
TEAM_COLORS.put(TeamColor.DARK_RED, BASE + "4");
TEAM_COLORS.put(TeamColor.DARK_PURPLE, BASE + "5");
TEAM_COLORS.put(TeamColor.GOLD, BASE + "6");
TEAM_COLORS.put(TeamColor.GRAY, BASE + "7");
TEAM_COLORS.put(TeamColor.DARK_GRAY, BASE + "8");
TEAM_COLORS.put(TeamColor.BLUE, BASE + "9");
TEAM_COLORS.put(TeamColor.GREEN, BASE + "a");
TEAM_COLORS.put(TeamColor.AQUA, BASE + "b");
TEAM_COLORS.put(TeamColor.RED, BASE + "c");
TEAM_COLORS.put(TeamColor.LIGHT_PURPLE, BASE + "d");
TEAM_COLORS.put(TeamColor.YELLOW, BASE + "e");
TEAM_COLORS.put(TeamColor.WHITE, BASE + "f");
// Formats, not colors
TEAM_COLORS.put(TeamColor.OBFUSCATED, BASE + "k");
TEAM_COLORS.put(TeamColor.BOLD, BASE + "l");
TEAM_COLORS.put(TeamColor.STRIKETHROUGH, BASE + "m");
TEAM_COLORS.put(TeamColor.ITALIC, BASE + "o");
// Temporary fix for https://github.com/KyoriPowered/adventure/issues/447 - TODO resolve properly // Temporary fix for https://github.com/KyoriPowered/adventure/issues/447 - TODO resolve properly
GsonComponentSerializer source = DefaultComponentSerializer.get() GsonComponentSerializer source = DefaultComponentSerializer.get()
.toBuilder() .toBuilder()
@ -157,13 +125,31 @@ public class MessageTranslator {
} }
/** /**
* Convert a Java message to the legacy format ready for bedrock * Convert a Java message to the legacy format ready for bedrock. Unlike
* {@link #convertMessageRaw(Component, String)} this adds a leading color reset. In Bedrock
* some places have build-in colors.
* *
* @param message Java message * @param message Java message
* @param locale Locale to use for translation strings * @param locale Locale to use for translation strings
* @return Parsed and formatted message for bedrock * @return Parsed and formatted message for bedrock
*/ */
public static String convertMessage(Component message, String locale) { public static String convertMessage(Component message, String locale) {
return convertMessage(message, locale, true);
}
/**
* Convert a Java message to the legacy format ready for bedrock. Unlike {@link #convertMessage(Component, String)}
* this version does not add a leading color reset. In Bedrock some places have build-in colors.
*
* @param message Java message
* @param locale Locale to use for translation strings
* @return Parsed and formatted message for bedrock
*/
public static String convertMessageRaw(Component message, String locale) {
return convertMessage(message, locale, false);
}
private static String convertMessage(Component message, String locale, boolean addLeadingResetFormat) {
try { try {
// Translate any components that require it // Translate any components that require it
message = RENDERER.render(message, locale); message = RENDERER.render(message, locale);
@ -172,7 +158,7 @@ public class MessageTranslator {
StringBuilder finalLegacy = new StringBuilder(); StringBuilder finalLegacy = new StringBuilder();
char[] legacyChars = legacy.toCharArray(); char[] legacyChars = legacy.toCharArray();
boolean lastFormatReset = false; boolean lastFormatReset = !addLeadingResetFormat;
for (int i = 0; i < legacyChars.length; i++) { for (int i = 0; i < legacyChars.length; i++) {
char legacyChar = legacyChars[i]; char legacyChar = legacyChars[i];
if (legacyChar != ChatColor.ESCAPE || i >= legacyChars.length - 1) { if (legacyChar != ChatColor.ESCAPE || i >= legacyChars.length - 1) {
@ -185,7 +171,7 @@ public class MessageTranslator {
char next = legacyChars[++i]; char next = legacyChars[++i];
if (BEDROCK_COLORS.indexOf(next) != -1) { if (BEDROCK_COLORS.indexOf(next) != -1) {
// Append this color code, as well as a necessary reset code // Unlike Java Edition, the ChatFormatting is not reset when a ChatColor is added
if (!lastFormatReset) { if (!lastFormatReset) {
finalLegacy.append(RESET); finalLegacy.append(RESET);
} }
@ -378,16 +364,6 @@ public class MessageTranslator {
session.sendUpstreamPacket(textPacket); session.sendUpstreamPacket(textPacket);
} }
/**
* Convert a team color to a chat color
*
* @param teamColor Color or format to convert
* @return The chat color character
*/
public static String toChatColor(TeamColor teamColor) {
return TEAM_COLORS.getOrDefault(teamColor, "");
}
/** /**
* Checks if the given message is over 256 characters (Java edition server chat limit) and sends a message to the user if it is * Checks if the given message is over 256 characters (Java edition server chat limit) and sends a message to the user if it is
* *

Datei anzeigen

@ -167,7 +167,7 @@ public class ChunkUtils {
byteBuf.readBytes(payload); byteBuf.readBytes(payload);
LevelChunkPacket data = new LevelChunkPacket(); LevelChunkPacket data = new LevelChunkPacket();
data.setDimension(DimensionUtils.javaToBedrock(session.getBedrockDimension())); data.setDimension(session.getBedrockDimension().bedrockId());
data.setChunkX(chunkX); data.setChunkX(chunkX);
data.setChunkZ(chunkZ); data.setChunkZ(chunkZ);
data.setSubChunksLength(0); data.setSubChunksLength(0);
@ -207,13 +207,6 @@ public class ChunkUtils {
int minY = dimension.minY(); int minY = dimension.minY();
int maxY = dimension.maxY(); int maxY = dimension.maxY();
if (minY % 16 != 0) {
throw new RuntimeException("Minimum Y must be a multiple of 16!");
}
if (maxY % 16 != 0) {
throw new RuntimeException("Maximum Y must be a multiple of 16!");
}
BedrockDimension bedrockDimension = session.getBedrockDimension(); BedrockDimension bedrockDimension = session.getBedrockDimension();
// Yell in the console if the world height is too height in the current scenario // Yell in the console if the world height is too height in the current scenario
// The constraints change depending on if the player is in the overworld or not, and if experimental height is enabled // The constraints change depending on if the player is in the overworld or not, and if experimental height is enabled

Datei anzeigen

@ -44,17 +44,8 @@ import java.util.Set;
public class DimensionUtils { public class DimensionUtils {
// Changes if the above-bedrock Nether building workaround is applied
private static int BEDROCK_NETHER_ID = 1;
public static final String BEDROCK_FOG_HELL = "minecraft:fog_hell"; public static final String BEDROCK_FOG_HELL = "minecraft:fog_hell";
public static final String NETHER_IDENTIFIER = "minecraft:the_nether";
private static final int BEDROCK_OVERWORLD_ID = 0;
private static final int BEDROCK_DEFAULT_NETHER_ID = 1;
private static final int BEDROCK_END_ID = 2;
public static void switchDimension(GeyserSession session, JavaDimension javaDimension) { public static void switchDimension(GeyserSession session, JavaDimension javaDimension) {
switchDimension(session, javaDimension, javaDimension.bedrockId()); switchDimension(session, javaDimension, javaDimension.bedrockId());
} }
@ -95,7 +86,7 @@ public class DimensionUtils {
// If the bedrock nether height workaround is enabled, meaning the client is told it's in the end dimension, // If the bedrock nether height workaround is enabled, meaning the client is told it's in the end dimension,
// we check if the player is entering the nether and apply the nether fog to fake the fact that the client // we check if the player is entering the nether and apply the nether fog to fake the fact that the client
// thinks they are in the end dimension. // thinks they are in the end dimension.
if (isCustomBedrockNetherId()) { if (BedrockDimension.isCustomBedrockNetherId()) {
if (javaDimension.isNetherLike()) { if (javaDimension.isNetherLike()) {
session.camera().sendFog(BEDROCK_FOG_HELL); session.camera().sendFog(BEDROCK_FOG_HELL);
} else if (previousDimension != null && previousDimension.isNetherLike()) { } else if (previousDimension != null && previousDimension.isNetherLike()) {
@ -168,22 +159,12 @@ public class DimensionUtils {
public static void setBedrockDimension(GeyserSession session, int bedrockDimension) { public static void setBedrockDimension(GeyserSession session, int bedrockDimension) {
session.setBedrockDimension(switch (bedrockDimension) { session.setBedrockDimension(switch (bedrockDimension) {
case BEDROCK_END_ID -> BedrockDimension.THE_END; case BedrockDimension.END_ID -> BedrockDimension.THE_END;
case BEDROCK_DEFAULT_NETHER_ID -> BedrockDimension.THE_NETHER; // JavaDimension *should* be set to BEDROCK_END_ID if the Nether workaround is enabled. case BedrockDimension.DEFAULT_NETHER_ID -> BedrockDimension.THE_NETHER; // JavaDimension *should* be set to BEDROCK_END_ID if the Nether workaround is enabled.
default -> BedrockDimension.OVERWORLD; default -> session.getBedrockOverworldDimension();
}); });
} }
public static int javaToBedrock(BedrockDimension dimension) {
if (dimension == BedrockDimension.THE_NETHER) {
return BEDROCK_NETHER_ID;
} else if (dimension == BedrockDimension.THE_END) {
return BEDROCK_END_ID;
} else {
return BEDROCK_OVERWORLD_ID;
}
}
/** /**
* Map the Java edition dimension IDs to Bedrock edition * Map the Java edition dimension IDs to Bedrock edition
* *
@ -192,9 +173,9 @@ public class DimensionUtils {
*/ */
public static int javaToBedrock(String javaDimension) { public static int javaToBedrock(String javaDimension) {
return switch (javaDimension) { return switch (javaDimension) {
case NETHER_IDENTIFIER -> BEDROCK_NETHER_ID; case BedrockDimension.NETHER_IDENTIFIER -> BedrockDimension.BEDROCK_NETHER_ID;
case "minecraft:the_end" -> 2; case "minecraft:the_end" -> BedrockDimension.END_ID;
default -> 0; default -> BedrockDimension.OVERWORLD_ID;
}; };
} }
@ -204,22 +185,11 @@ public class DimensionUtils {
public static int javaToBedrock(GeyserSession session) { public static int javaToBedrock(GeyserSession session) {
JavaDimension dimension = session.getDimensionType(); JavaDimension dimension = session.getDimensionType();
if (dimension == null) { if (dimension == null) {
return BEDROCK_OVERWORLD_ID; return BedrockDimension.OVERWORLD_ID;
} }
return dimension.bedrockId(); return dimension.bedrockId();
} }
/**
* The Nether dimension in Bedrock does not permit building above Y128 - the Bedrock above the dimension.
* This workaround sets the Nether as the End dimension to ignore this limit.
*
* @param isAboveNetherBedrockBuilding true if we should apply The End workaround
*/
public static void changeBedrockNetherId(boolean isAboveNetherBedrockBuilding) {
// Change dimension ID to the End to allow for building above Bedrock
BEDROCK_NETHER_ID = isAboveNetherBedrockBuilding ? BEDROCK_END_ID : BEDROCK_DEFAULT_NETHER_ID;
}
/** /**
* Gets the fake, temporary dimension we send clients to so we aren't switching to the same dimension without an additional * Gets the fake, temporary dimension we send clients to so we aren't switching to the same dimension without an additional
* dimension switch. * dimension switch.
@ -229,16 +199,13 @@ public class DimensionUtils {
* @return the Bedrock fake dimension to transfer to * @return the Bedrock fake dimension to transfer to
*/ */
public static int getTemporaryDimension(int currentBedrockDimension, int newBedrockDimension) { public static int getTemporaryDimension(int currentBedrockDimension, int newBedrockDimension) {
if (isCustomBedrockNetherId()) { if (BedrockDimension.isCustomBedrockNetherId()) {
// Prevents rare instances of Bedrock locking up // Prevents rare instances of Bedrock locking up
return newBedrockDimension == BEDROCK_END_ID ? BEDROCK_OVERWORLD_ID : BEDROCK_END_ID; return newBedrockDimension == BedrockDimension.END_ID ? BedrockDimension.OVERWORLD_ID : BedrockDimension.END_ID;
} }
// Check current Bedrock dimension and not just the Java dimension. // Check current Bedrock dimension and not just the Java dimension.
// Fixes rare instances like https://github.com/GeyserMC/Geyser/issues/3161 // Fixes rare instances like https://github.com/GeyserMC/Geyser/issues/3161
return currentBedrockDimension == BEDROCK_OVERWORLD_ID ? BEDROCK_DEFAULT_NETHER_ID : BEDROCK_OVERWORLD_ID; return currentBedrockDimension == BedrockDimension.OVERWORLD_ID ? BedrockDimension.DEFAULT_NETHER_ID : BedrockDimension.OVERWORLD_ID;
} }
public static boolean isCustomBedrockNetherId() {
return BEDROCK_NETHER_ID == BEDROCK_END_ID;
}
} }

Datei anzeigen

@ -25,6 +25,10 @@
package org.geysermc.geyser.util; package org.geysermc.geyser.util;
import java.util.Locale;
import net.kyori.adventure.key.Key;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.math.vector.Vector3f; import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.protocol.bedrock.data.GameType; import org.cloudburstmc.protocol.bedrock.data.GameType;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes; import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
@ -38,13 +42,13 @@ import org.geysermc.geyser.entity.type.living.animal.AnimalEntity;
import org.geysermc.geyser.entity.type.living.animal.horse.CamelEntity; import org.geysermc.geyser.entity.type.living.animal.horse.CamelEntity;
import org.geysermc.geyser.inventory.GeyserItemStack; import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.item.Items; import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.MinecraftLocale;
import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect; import org.geysermc.mcprotocollib.protocol.data.game.entity.Effect;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode;
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand; import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand;
import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType; import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType;
import java.util.Locale;
public final class EntityUtils { public final class EntityUtils {
/** /**
* A constant array of the two hands that a player can interact with an entity. * A constant array of the two hands that a player can interact with an entity.
@ -290,6 +294,32 @@ public final class EntityUtils {
}; };
} }
private static String translatedEntityName(@NonNull String namespace, @NonNull String name, @NonNull GeyserSession session) {
// MinecraftLocale would otherwise invoke getBootstrap (which doesn't exist) and create some folders,
// so use the default fallback value as used in Minecraft Java
if (EnvironmentUtils.isUnitTesting) {
return "entity." + namespace + "." + name;
}
return MinecraftLocale.getLocaleString("entity." + namespace + "." + name, session.locale());
}
public static String translatedEntityName(@NonNull Key type, @NonNull GeyserSession session) {
return translatedEntityName(type.namespace(), type.value(), session);
}
public static String translatedEntityName(@Nullable EntityType type, @NonNull GeyserSession session) {
if (type == EntityType.PLAYER) {
return "Player"; // the player's name is always shown instead
}
// default fallback value as used in Minecraft Java
if (type == null) {
return "entity.unregistered_sadface";
}
// this works at least with all 1.20.5 entities, except the killer bunny since that's not an entity type.
String typeName = type.name().toLowerCase(Locale.ROOT);
return translatedEntityName("minecraft", typeName, session);
}
private EntityUtils() { private EntityUtils() {
} }
} }

Datei anzeigen

@ -0,0 +1,41 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.util;
public final class EnvironmentUtils {
public static final boolean isUnitTesting = isUnitTesting();
private EnvironmentUtils() {}
private static boolean isUnitTesting() {
for (StackTraceElement element : Thread.currentThread().getStackTrace()) {
if (element.getClassName().startsWith("org.junit.")) {
return true;
}
}
return false;
}
}

Datei anzeigen

@ -0,0 +1,270 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.network;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNoNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockAndAddPlayerEntity;
import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockContextScoreboard;
import net.kyori.adventure.text.Component;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.packet.SetEntityDataPacket;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetPlayerTeamTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.CollisionRule;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.NameTagVisibility;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamAction;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetPlayerTeamPacket;
import org.junit.jupiter.api.Test;
public class NameVisibilityScoreboardTest {
@Test
void playerVisibilityNever() {
mockContextScoreboard(context -> {
var setPlayerTeamTranslator = new JavaSetPlayerTeamTranslator();
mockAndAddPlayerEntity(context, "player1", 2);
context.translate(
setPlayerTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"team1",
Component.text("displayName"),
Component.text("prefix"),
Component.text("suffix"),
false,
false,
NameTagVisibility.NEVER,
CollisionRule.NEVER,
TeamColor.DARK_RED,
new String[]{"player1"}
)
);
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.NAME, "");
return packet;
}, context);
});
}
@Test
void playerVisibilityHideForOtherTeam() {
mockContextScoreboard(context -> {
var setPlayerTeamTranslator = new JavaSetPlayerTeamTranslator();
mockAndAddPlayerEntity(context, "player1", 2);
context.translate(
setPlayerTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"team1",
Component.text("displayName"),
Component.text("prefix"),
Component.text("suffix"),
false,
false,
NameTagVisibility.HIDE_FOR_OTHER_TEAMS,
CollisionRule.NEVER,
TeamColor.DARK_RED,
new String[]{"player1"}
)
);
// only hidden if session player (Tim203) is in a team as well
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.NAME, "§4prefix§r§4player1§r§4suffix");
return packet;
}, context);
assertNoNextPacket(context);
// create another team and add Tim203 to it
context.translate(
setPlayerTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"team2",
Component.text("displayName"),
Component.text("prefix"),
Component.text("suffix"),
false,
false,
NameTagVisibility.NEVER,
CollisionRule.NEVER,
TeamColor.DARK_RED,
new String[]{"Tim203"}
)
);
// Tim203 is now in another team, so it should be hidden
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.NAME, "");
return packet;
}, context);
assertNoNextPacket(context);
// add Tim203 to same team as player1, score should be visible again
context.translate(
setPlayerTeamTranslator,
new ClientboundSetPlayerTeamPacket("team1", TeamAction.ADD_PLAYER, new String[]{"Tim203"})
);
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.NAME, "§4prefix§r§4player1§r§4suffix");
return packet;
}, context);
});
}
@Test
void playerVisibilityHideForOwnTeam() {
mockContextScoreboard(context -> {
var setPlayerTeamTranslator = new JavaSetPlayerTeamTranslator();
mockAndAddPlayerEntity(context, "player1", 2);
context.translate(
setPlayerTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"team1",
Component.text("displayName"),
Component.text("prefix"),
Component.text("suffix"),
false,
false,
NameTagVisibility.HIDE_FOR_OWN_TEAM,
CollisionRule.NEVER,
TeamColor.DARK_RED,
new String[]{"player1"}
)
);
// Tim203 is not in a team (let alone the same team), so should be visible
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.NAME, "§4prefix§r§4player1§r§4suffix");
return packet;
}, context);
assertNoNextPacket(context);
// Tim203 is now in the same team as player1, so should be hidden
context.translate(
setPlayerTeamTranslator,
new ClientboundSetPlayerTeamPacket("team1", TeamAction.ADD_PLAYER, new String[]{"Tim203"})
);
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.NAME, "");
return packet;
}, context);
assertNoNextPacket(context);
// create another team and add Tim203 to there, score should be visible again
context.translate(
setPlayerTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"team2",
Component.text("displayName"),
Component.text("prefix"),
Component.text("suffix"),
false,
false,
NameTagVisibility.NEVER,
CollisionRule.NEVER,
TeamColor.DARK_RED,
new String[]{"Tim203"}
)
);
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.NAME, "§4prefix§r§4player1§r§4suffix");
return packet;
}, context);
});
}
@Test
void playerVisibilityAlways() {
mockContextScoreboard(context -> {
var setPlayerTeamTranslator = new JavaSetPlayerTeamTranslator();
mockAndAddPlayerEntity(context, "player1", 2);
context.translate(
setPlayerTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"team1",
Component.text("displayName"),
Component.text("prefix"),
Component.text("suffix"),
false,
false,
NameTagVisibility.ALWAYS,
CollisionRule.NEVER,
TeamColor.DARK_RED,
new String[]{"player1"}
)
);
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.NAME, "§4prefix§r§4player1§r§4suffix");
return packet;
}, context);
// adding self to another team shouldn't make a difference
context.translate(
setPlayerTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"team2",
Component.text("displayName"),
Component.text("prefix"),
Component.text("suffix"),
false,
false,
NameTagVisibility.ALWAYS,
CollisionRule.NEVER,
TeamColor.DARK_RED,
new String[]{"Tim203"}
)
);
assertNoNextPacket(context);
// adding self to player1 team shouldn't matter
context.translate(
setPlayerTeamTranslator,
new ClientboundSetPlayerTeamPacket("team1", TeamAction.ADD_PLAYER, new String[]{"Tim203"})
);
assertNoNextPacket(context);
});
}
}

Datei anzeigen

@ -0,0 +1,93 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.network;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNextPacketType;
import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockContextScoreboard;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.cloudburstmc.protocol.bedrock.packet.AddEntityPacket;
import org.cloudburstmc.protocol.bedrock.packet.RemoveEntityPacket;
import org.geysermc.geyser.entity.type.living.monster.EnderDragonPartEntity;
import org.geysermc.geyser.session.cache.EntityCache;
import org.geysermc.geyser.translator.protocol.java.entity.JavaRemoveEntitiesTranslator;
import org.geysermc.geyser.translator.protocol.java.entity.spawn.JavaAddExperienceOrbTranslator;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.ClientboundRemoveEntitiesPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.spawn.ClientboundAddExperienceOrbPacket;
import org.junit.jupiter.api.Test;
/**
* Tests that don't fit in a larger system (e.g. sidebar objective) that were reported on GitHub
*/
public class ScoreboardIssueTests {
/**
* Test for <a href="https://github.com/GeyserMC/Geyser/issues/5075">#5075</a>
*/
@Test
void entityWithoutUuid() {
// experience orbs are the only known entities without an uuid, see Entity#teamIdentifier for more info
mockContextScoreboard(context -> {
var addExperienceOrbTranslator = new JavaAddExperienceOrbTranslator();
var removeEntitiesTranslator = new JavaRemoveEntitiesTranslator();
// Entity#teamIdentifier used to throw because it returned uuid.toString where uuid could be null.
// this would result in both EntityCache#spawnEntity and EntityCache#removeEntity throwing an exception,
// because the entity would be registered and deregistered to the scoreboard.
assertDoesNotThrow(() -> {
context.translate(addExperienceOrbTranslator, new ClientboundAddExperienceOrbPacket(2, 0, 0, 0, 1));
String displayName = context.mockOrSpy(EntityCache.class).getEntityByJavaId(2).getDisplayName();
assertEquals("entity.minecraft.experience_orb", displayName);
context.translate(removeEntitiesTranslator, new ClientboundRemoveEntitiesPacket(new int[] { 2 }));
});
// we know that spawning and removing the entity should be fine
assertNextPacketType(context, AddEntityPacket.class);
assertNextPacketType(context, RemoveEntityPacket.class);
});
}
/**
* Test for <a href="https://github.com/GeyserMC/Geyser/issues/5078">#5078</a>
*/
@Test
void entityWithoutType() {
// dragon entity parts are an entity in Geyser, but do not have an entity type
mockContextScoreboard(context -> {
// EntityUtils#translatedEntityName used to not take null EntityType's into account,
// so it used to throw an exception
assertDoesNotThrow(() -> {
// dragon entity parts are not spawned using a packet, so we manually create an instance
var dragonHeadPart = new EnderDragonPartEntity(context.session(), 2, 2, 1, 1);
String displayName = dragonHeadPart.getDisplayName();
assertEquals("entity.unregistered_sadface", displayName);
});
});
}
}

Datei anzeigen

@ -0,0 +1,227 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.network.belowname;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNoNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockAndAddPlayerEntity;
import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockContextScoreboard;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.packet.SetEntityDataPacket;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetDisplayObjectiveTranslator;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetObjectiveTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ObjectiveAction;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreType;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetDisplayObjectivePacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetObjectivePacket;
import org.junit.jupiter.api.Test;
public class BasicBelownameScoreboardTests {
@Test
void displayWithNoPlayersAndRemove() {
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective",
ObjectiveAction.ADD,
Component.text("objective"),
ScoreType.INTEGER,
null
)
);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.BELOW_NAME, "objective")
);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.BELOW_NAME, "")
);
assertNoNextPacket(context);
});
}
@Test
void displayColorWithOnePlayer() {
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
mockAndAddPlayerEntity(context, "player1", 2);
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective",
ObjectiveAction.ADD,
Component.text("objective", NamedTextColor.BLUE),
ScoreType.INTEGER,
null
)
);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.BELOW_NAME, "objective")
);
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.SCORE, "0 §r§9objective");
return packet;
}, context);
});
}
@Test
void displayWithOnePlayerAndRemove() {
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
mockAndAddPlayerEntity(context, "player1", 2);
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective",
ObjectiveAction.ADD,
Component.text("objective"),
ScoreType.INTEGER,
null
)
);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.BELOW_NAME, "objective")
);
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.SCORE, "0 §robjective");
return packet;
}, context);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.BELOW_NAME, "")
);
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.SCORE, "");
return packet;
}, context);
});
}
@Test
void overrideAndRemove() {
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
mockAndAddPlayerEntity(context, "player1", 2);
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective1",
ObjectiveAction.ADD,
Component.text("objective1"),
ScoreType.INTEGER,
null
)
);
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective2",
ObjectiveAction.ADD,
Component.text("objective2"),
ScoreType.INTEGER,
null
)
);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.BELOW_NAME, "objective2")
);
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.SCORE, "0 §robjective2");
return packet;
}, context);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.BELOW_NAME, "objective1")
);
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.SCORE, "");
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.SCORE, "0 §robjective1");
return packet;
}, context);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.BELOW_NAME, "")
);
assertNextPacket(() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.SCORE, "");
return packet;
}, context);
});
}
}

Datei anzeigen

@ -0,0 +1,204 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.network.playerlist;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNoNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockContextScoreboard;
import java.util.List;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextDecoration;
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
import org.cloudburstmc.protocol.bedrock.packet.RemoveObjectivePacket;
import org.cloudburstmc.protocol.bedrock.packet.SetDisplayObjectivePacket;
import org.cloudburstmc.protocol.bedrock.packet.SetScorePacket;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetDisplayObjectiveTranslator;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetObjectiveTranslator;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetScoreTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ObjectiveAction;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreType;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetDisplayObjectivePacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetObjectivePacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetScorePacket;
import org.junit.jupiter.api.Test;
/*
Identical to sidebar
*/
public class BasicPlayerlistScoreboardTests {
@Test
void display() {
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective",
ObjectiveAction.ADD,
Component.text("objective"),
ScoreType.INTEGER,
null
)
);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.PLAYER_LIST, "objective")
);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("0");
packet.setDisplayName("objective");
packet.setCriteria("dummy");
packet.setDisplaySlot("list");
packet.setSortOrder(1);
return packet;
}, context);
});
}
@Test
void displayNameColors() {
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective",
ObjectiveAction.ADD,
Component.text("objective", Style.style(NamedTextColor.AQUA, TextDecoration.BOLD)),
ScoreType.INTEGER,
null
)
);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.PLAYER_LIST, "objective")
);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("0");
packet.setDisplayName("§b§lobjective");
packet.setCriteria("dummy");
packet.setDisplaySlot("list");
packet.setSortOrder(1);
return packet;
}, context);
});
}
@Test
void overrideWithOneScore() {
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
var setScoreTranslator = new JavaSetScoreTranslator();
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective1",
ObjectiveAction.ADD,
Component.text("objective1"),
ScoreType.INTEGER,
null
)
);
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective2",
ObjectiveAction.ADD,
Component.text("objective2"),
ScoreType.INTEGER,
null
)
);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("Tim203", "objective1", 1));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("Tim203", "objective2", 2));
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.PLAYER_LIST, "objective2")
);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("0");
packet.setDisplayName("objective2");
packet.setCriteria("dummy");
packet.setDisplaySlot("list");
packet.setSortOrder(1);
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
// session player name is Tim203
packet.setInfos(List.of(new ScoreInfo(1, "0", 2, ScoreInfo.ScorerType.PLAYER, 1)));
return packet;
}, context);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.PLAYER_LIST, "objective1")
);
assertNextPacket(() -> {
var packet = new RemoveObjectivePacket();
packet.setObjectiveId("0");
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("2");
packet.setDisplayName("objective1");
packet.setCriteria("dummy");
packet.setDisplaySlot("list");
packet.setSortOrder(1);
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
// session player name is Tim203
packet.setInfos(List.of(new ScoreInfo(3, "2", 1, ScoreInfo.ScorerType.PLAYER, 1)));
return packet;
}, context);
});
}
}

Datei anzeigen

@ -0,0 +1,756 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.network.server;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNoNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockAndAddPlayerEntity;
import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockContextScoreboard;
import java.util.List;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
import org.cloudburstmc.protocol.bedrock.packet.RemoveObjectivePacket;
import org.cloudburstmc.protocol.bedrock.packet.SetDisplayObjectivePacket;
import org.cloudburstmc.protocol.bedrock.packet.SetEntityDataPacket;
import org.cloudburstmc.protocol.bedrock.packet.SetScorePacket;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetDisplayObjectiveTranslator;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetObjectiveTranslator;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetPlayerTeamTranslator;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetScoreTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.CollisionRule;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.NameTagVisibility;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ObjectiveAction;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreType;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamAction;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetDisplayObjectivePacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetObjectivePacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetPlayerTeamPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetScorePacket;
import org.junit.jupiter.api.Test;
public class CubecraftScoreboardTest {
@Test
void test() {
mockContextScoreboard(context -> {
var setTeamTranslator = new JavaSetPlayerTeamTranslator();
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
var setScoreTranslator = new JavaSetScoreTranslator();
// unused
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("SB_NoName", Component.text("SB_NoName"), Component.empty(), Component.empty(), true, true, NameTagVisibility.NEVER, CollisionRule.NEVER, TeamColor.RESET, new String[0]));
assertNoNextPacket(context);
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"sidebar",
ObjectiveAction.ADD,
Component.text("sidebar"),
ScoreType.INTEGER,
null
)
);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.SIDEBAR, "sidebar")
);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("0");
packet.setDisplayName("sidebar");
packet.setCriteria("dummy");
packet.setDisplaySlot("sidebar");
packet.setSortOrder(1);
return packet;
}, context);
// Now they're going to create a bunch of teams and add players to those teams in a very inefficient way.
// Presumably this is a leftover from an old system, as these don't seem to do anything but hide their nametags.
// For which you could just use a single team.
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", Component.text("2i|1"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.RESET, new String[0]));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", Component.text("2i|1"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.DARK_GRAY));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", Component.text("2i|1"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.DARK_GRAY));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", Component.text("2i|1"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.DARK_GRAY));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", Component.text("2i|1"), Component.empty(), Component.empty(), false, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.DARK_GRAY));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", Component.text("2i|1"), Component.empty(), Component.empty(), false, false, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.DARK_GRAY));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", Component.text("2i|1"), Component.empty(), Component.empty(), false, false, NameTagVisibility.NEVER, CollisionRule.ALWAYS, TeamColor.DARK_GRAY));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", Component.text("2i|1"), Component.empty(), Component.empty(), false, false, NameTagVisibility.NEVER, CollisionRule.NEVER, TeamColor.DARK_GRAY));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", TeamAction.ADD_PLAYER, new String[] { "A_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1y|11", Component.text("1y|11"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.RESET, new String[0]));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1y|11", Component.text("1y|11"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.LIGHT_PURPLE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1y|11", Component.text("1y|11"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.LIGHT_PURPLE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1y|11", Component.text("1y|11"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.LIGHT_PURPLE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1y|11", Component.text("1y|11"), Component.empty(), Component.empty(), false, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.LIGHT_PURPLE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1y|11", Component.text("1y|11"), Component.empty(), Component.empty(), false, false, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.LIGHT_PURPLE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1y|11", Component.text("1y|11"), Component.empty(), Component.empty(), false, false, NameTagVisibility.NEVER, CollisionRule.ALWAYS, TeamColor.LIGHT_PURPLE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1y|11", Component.text("1y|11"), Component.empty(), Component.empty(), false, false, NameTagVisibility.NEVER, CollisionRule.NEVER, TeamColor.LIGHT_PURPLE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1y|11", TeamAction.ADD_PLAYER, new String[] { "B_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", TeamAction.ADD_PLAYER, new String[] { "C_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", TeamAction.ADD_PLAYER, new String[] { "D_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1y|11", TeamAction.ADD_PLAYER, new String[] { "E_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", TeamAction.ADD_PLAYER, new String[] { "F_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", TeamAction.ADD_PLAYER, new String[] { "G_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2e|3", Component.text("2e|3"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.RESET, new String[0]));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2e|3", Component.text("2e|3"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.BLUE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2e|3", Component.text("2e|3"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.BLUE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2e|3", Component.text("2e|3"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.BLUE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2e|3", Component.text("2e|3"), Component.empty(), Component.empty(), false, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.BLUE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2e|3", Component.text("2e|3"), Component.empty(), Component.empty(), false, false, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.BLUE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2e|3", Component.text("2e|3"), Component.empty(), Component.empty(), false, false, NameTagVisibility.NEVER, CollisionRule.ALWAYS, TeamColor.BLUE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2e|3", Component.text("2e|3"), Component.empty(), Component.empty(), false, false, NameTagVisibility.NEVER, CollisionRule.NEVER, TeamColor.BLUE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2e|3", TeamAction.ADD_PLAYER, new String[] { "H_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", TeamAction.ADD_PLAYER, new String[] { "I_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("22|9", Component.text("22|9"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.RESET, new String[0]));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("22|9", Component.text("22|9"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.AQUA));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("22|9", Component.text("22|9"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.AQUA));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("22|9", Component.text("22|9"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.AQUA));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("22|9", Component.text("22|9"), Component.empty(), Component.empty(), false, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.AQUA));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("22|9", Component.text("22|9"), Component.empty(), Component.empty(), false, false, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.AQUA));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("22|9", Component.text("22|9"), Component.empty(), Component.empty(), false, false, NameTagVisibility.NEVER, CollisionRule.ALWAYS, TeamColor.AQUA));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("22|9", Component.text("22|9"), Component.empty(), Component.empty(), false, false, NameTagVisibility.NEVER, CollisionRule.NEVER, TeamColor.AQUA));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("22|9", TeamAction.ADD_PLAYER, new String[] { "J_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", TeamAction.ADD_PLAYER, new String[] { "K_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("26|7", Component.text("26|7"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.RESET, new String[0]));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("26|7", Component.text("26|7"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.AQUA));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("26|7", Component.text("26|7"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.AQUA));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("26|7", Component.text("26|7"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.AQUA));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("26|7", Component.text("26|7"), Component.empty(), Component.empty(), false, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.AQUA));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("26|7", Component.text("26|7"), Component.empty(), Component.empty(), false, false, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.AQUA));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("26|7", Component.text("26|7"), Component.empty(), Component.empty(), false, false, NameTagVisibility.NEVER, CollisionRule.ALWAYS, TeamColor.AQUA));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("26|7", Component.text("26|7"), Component.empty(), Component.empty(), false, false, NameTagVisibility.NEVER, CollisionRule.NEVER, TeamColor.AQUA));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("26|7", TeamAction.ADD_PLAYER, new String[] { "L_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2e|3", TeamAction.ADD_PLAYER, new String[] { "M_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", TeamAction.ADD_PLAYER, new String[] { "N_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1u|13", Component.text("1u|13"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.RESET, new String[0]));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1u|13", Component.text("1u|13"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.LIGHT_PURPLE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1u|13", Component.text("1u|13"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.LIGHT_PURPLE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1u|13", Component.text("1u|13"), Component.empty(), Component.empty(), true, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.LIGHT_PURPLE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1u|13", Component.text("1u|13"), Component.empty(), Component.empty(), false, true, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.LIGHT_PURPLE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1u|13", Component.text("1u|13"), Component.empty(), Component.empty(), false, false, NameTagVisibility.ALWAYS, CollisionRule.ALWAYS, TeamColor.LIGHT_PURPLE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1u|13", Component.text("1u|13"), Component.empty(), Component.empty(), false, false, NameTagVisibility.NEVER, CollisionRule.ALWAYS, TeamColor.LIGHT_PURPLE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1u|13", Component.text("1u|13"), Component.empty(), Component.empty(), false, false, NameTagVisibility.NEVER, CollisionRule.NEVER, TeamColor.LIGHT_PURPLE));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("1u|13", TeamAction.ADD_PLAYER, new String[] { "O_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", TeamAction.ADD_PLAYER, new String[] { "P_Player" }));
context.translate(setTeamTranslator, new ClientboundSetPlayerTeamPacket("2i|1", TeamAction.ADD_PLAYER, new String[] { "Q_Player" }));
assertNoNextPacket(context);
// Now that those teams are created and people added to it, they set the final sidebar name and add the lines to it.
// They're also not doing this efficiently, because they don't add the players when the team is created.
// Instead, they send an additional packet.
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"sidebar",
ObjectiveAction.UPDATE,
Component.empty()
.append(Component.text(
"CubeCraft", Style.style(NamedTextColor.WHITE, TextDecoration.BOLD))),
ScoreType.INTEGER,
null));
assertNextPacket(
() -> {
var packet = new RemoveObjectivePacket();
packet.setObjectiveId("0");
return packet;
},
context);
assertNextPacket(
() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("0");
packet.setDisplayName("§f§lCubeCraft");
packet.setCriteria("dummy");
packet.setDisplaySlot("sidebar");
packet.setSortOrder(1);
return packet;
},
context);
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-0",
Component.text("SB_l-0"),
Component.empty(),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET,
new String[0]));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket("SB_l-0", TeamAction.ADD_PLAYER, new String[] {"§0§0"}));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-0",
Component.text("SB_l-0"),
Component.empty().append(Component.text("", Style.style(NamedTextColor.BLACK))),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET));
assertNoNextPacket(context);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("§0§0", "sidebar", 10));
assertNextPacket(
() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(1, "0", 10, "§r§0§0§r")));
return packet;
},
context);
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-1",
Component.text("SB_l-1"),
Component.empty(),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET,
new String[0]));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket("SB_l-1", TeamAction.ADD_PLAYER, new String[] {"§0§1"}));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-1",
Component.text("SB_l-1"),
Component.empty()
.append(Component.textOfChildren(
Component.text("User: ", TextColor.color(0x3aa9ff)),
Component.text("Tim203", NamedTextColor.WHITE))),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET));
assertNoNextPacket(context);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("§0§1", "sidebar", 9));
assertNextPacket(
() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(2, "0", 9, "§bUser: §r§fTim203§r§0§1§r")));
return packet;
},
context);
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-2",
Component.text("SB_l-2"),
Component.empty(),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET,
new String[0]));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket("SB_l-2", TeamAction.ADD_PLAYER, new String[] {"§0§2"}));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-2",
Component.text("SB_l-2"),
Component.empty()
.append(Component.textOfChildren(
Component.text("Rank: ", TextColor.color(0x3aa9ff)),
Component.text("\uE1AB ", NamedTextColor.WHITE))),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET));
assertNoNextPacket(context);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("§0§2", "sidebar", 8));
assertNextPacket(
() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(3, "0", 8, "§bRank: §r§f\uE1AB §r§0§2§r")));
return packet;
},
context);
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-3",
Component.text("SB_l-3"),
Component.empty(),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET,
new String[0]));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket("SB_l-3", TeamAction.ADD_PLAYER, new String[] {"§0§3"}));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-3",
Component.text("SB_l-3"),
Component.empty(),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET));
assertNoNextPacket(context);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("§0§3", "sidebar", 7));
assertNextPacket(
() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(4, "0", 7, "§r§0§3§r")));
return packet;
},
context);
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-4",
Component.text("SB_l-4"),
Component.empty(),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET,
new String[0]));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket("SB_l-4", TeamAction.ADD_PLAYER, new String[] {"§0§4"}));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-4",
Component.text("SB_l-4"),
Component.empty(),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET));
assertNoNextPacket(context);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("§0§4", "sidebar", 6));
assertNextPacket(
() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(5, "0", 6, "§r§0§4§r")));
return packet;
},
context);
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-5",
Component.text("SB_l-5"),
Component.empty(),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET,
new String[0]));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket("SB_l-5", TeamAction.ADD_PLAYER, new String[] {"§0§5"}));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-5",
Component.text("SB_l-5"),
Component.empty().append(Component.text("", NamedTextColor.DARK_BLUE)),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET));
assertNoNextPacket(context);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("§0§5", "sidebar", 5));
assertNextPacket(
() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(6, "0", 5, "§r§0§5§r")));
return packet;
},
context);
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-6",
Component.text("SB_l-6"),
Component.empty(),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET,
new String[0]));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket("SB_l-6", TeamAction.ADD_PLAYER, new String[] {"§0§6"}));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-6",
Component.text("SB_l-6"),
Component.empty()
.append(Component.textOfChildren(
Component.text("Lobby: ", TextColor.color(0x3aa9ff)),
Component.text("EU #10", NamedTextColor.WHITE))),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET));
assertNoNextPacket(context);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("§0§6", "sidebar", 4));
assertNextPacket(
() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(7, "0", 4, "§bLobby: §r§fEU #10§r§0§6§r")));
return packet;
},
context);
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-7",
Component.text("SB_l-7"),
Component.empty(),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET,
new String[0]));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket("SB_l-7", TeamAction.ADD_PLAYER, new String[] {"§0§7"}));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-7",
Component.text("SB_l-7"),
Component.empty()
.append(Component.textOfChildren(
Component.text("Players: ", TextColor.color(0x3aa9ff)),
Component.text("783", NamedTextColor.WHITE))),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET));
assertNoNextPacket(context);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("§0§7", "sidebar", 3));
assertNextPacket(
() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(8, "0", 3, "§bPlayers: §r§f783§r§0§7§r")));
return packet;
},
context);
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-8",
Component.text("SB_l-8"),
Component.empty(),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET,
new String[0]));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket("SB_l-8", TeamAction.ADD_PLAYER, new String[] {"§0§8"}));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-8",
Component.text("SB_l-8"),
Component.empty().append(Component.text("", NamedTextColor.DARK_GREEN)),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET));
assertNoNextPacket(context);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("§0§8", "sidebar", 2));
assertNextPacket(
() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(9, "0", 2, "§r§0§8§r")));
return packet;
},
context);
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-9",
Component.text("SB_l-9"),
Component.empty(),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET,
new String[0]));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket("SB_l-9", TeamAction.ADD_PLAYER, new String[] {"§0§9"}));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-9",
Component.text("SB_l-9"),
Component.empty().append(Component.text("24/09/24 (g2208)", TextColor.color(0x777777))),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET));
assertNoNextPacket(context);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("§0§9", "sidebar", 1));
assertNextPacket(
() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(10, "0", 1, "§824/09/24 (g2208)§r§0§9§r")));
return packet;
},
context);
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-10",
Component.text("SB_l-10"),
Component.empty(),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET,
new String[0]));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket("SB_l-10", TeamAction.ADD_PLAYER, new String[] {"§0§a"}));
context.translate(
setTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"SB_l-10",
Component.text("SB_l-10"),
Component.empty().append(Component.text("play.cubecraft.net", NamedTextColor.GOLD)),
Component.empty(),
true,
true,
NameTagVisibility.ALWAYS,
CollisionRule.ALWAYS,
TeamColor.RESET));
assertNoNextPacket(context);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("§0§a", "sidebar", 0));
assertNextPacket(
() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(11, "0", 0, "§6play.cubecraft.net§r§0§a§r")));
return packet;
},
context);
// after this we get a ClientboundPlayerInfoUpdatePacket with the action UPDATE_DISPLAY_NAME,
// but that one is only shown in the tablist so we don't have to handle that.
// And after that we get each player's ClientboundPlayerInfoUpdatePacket with also a UPDATE_DISPLAY_NAME,
// which is also not interesting for us.
// CubeCraft seems to use two armor stands per player: 1 for the rank badge and 1 for the player name.
// So the only thing we have to verify is that the nametag is hidden
mockAndAddPlayerEntity(context, "A_Player", 2);
assertNextPacket(
() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(2);
packet.getMetadata().put(EntityDataTypes.NAME, "");
return packet;
},
context);
mockAndAddPlayerEntity(context, "B_Player", 3);
assertNextPacket(
() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(3);
packet.getMetadata().put(EntityDataTypes.NAME, "");
return packet;
},
context);
mockAndAddPlayerEntity(context, "E_Player", 4);
assertNextPacket(
() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(4);
packet.getMetadata().put(EntityDataTypes.NAME, "");
return packet;
},
context);
mockAndAddPlayerEntity(context, "H_Player", 5);
assertNextPacket(
() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(5);
packet.getMetadata().put(EntityDataTypes.NAME, "");
return packet;
},
context);
mockAndAddPlayerEntity(context, "J_Player", 6);
assertNextPacket(
() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(6);
packet.getMetadata().put(EntityDataTypes.NAME, "");
return packet;
},
context);
mockAndAddPlayerEntity(context, "K_Player", 7);
assertNextPacket(
() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(7);
packet.getMetadata().put(EntityDataTypes.NAME, "");
return packet;
},
context);
mockAndAddPlayerEntity(context, "L_Player", 8);
assertNextPacket(
() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(8);
packet.getMetadata().put(EntityDataTypes.NAME, "");
return packet;
},
context);
mockAndAddPlayerEntity(context, "O_Player", 9);
assertNextPacket(
() -> {
var packet = new SetEntityDataPacket();
packet.setRuntimeEntityId(9);
packet.getMetadata().put(EntityDataTypes.NAME, "");
return packet;
},
context);
});
}
}

Datei anzeigen

@ -0,0 +1,218 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.network.sidebar;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNoNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockContextScoreboard;
import java.util.List;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextDecoration;
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
import org.cloudburstmc.protocol.bedrock.packet.RemoveObjectivePacket;
import org.cloudburstmc.protocol.bedrock.packet.SetDisplayObjectivePacket;
import org.cloudburstmc.protocol.bedrock.packet.SetScorePacket;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetDisplayObjectiveTranslator;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetObjectiveTranslator;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetScoreTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ObjectiveAction;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreType;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetDisplayObjectivePacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetObjectivePacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetScorePacket;
import org.junit.jupiter.api.Test;
/*
Identical to playerlist
*/
public class BasicSidebarScoreboardTests {
@Test
void displayAndRemove() {
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective",
ObjectiveAction.ADD,
Component.text("objective"),
ScoreType.INTEGER,
null
)
);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.PLAYER_LIST, "objective")
);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("0");
packet.setDisplayName("objective");
packet.setCriteria("dummy");
packet.setDisplaySlot("list");
packet.setSortOrder(1);
return packet;
}, context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.PLAYER_LIST, "")
);
assertNextPacket(() -> {
var packet = new RemoveObjectivePacket();
packet.setObjectiveId("0");
return packet;
}, context);
});
}
@Test
void displayNameColors() {
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective",
ObjectiveAction.ADD,
Component.text("objective", Style.style(NamedTextColor.AQUA, TextDecoration.BOLD)),
ScoreType.INTEGER,
null
)
);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.SIDEBAR, "objective")
);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("0");
packet.setDisplayName("§b§lobjective");
packet.setCriteria("dummy");
packet.setDisplaySlot("sidebar");
packet.setSortOrder(1);
return packet;
}, context);
});
}
@Test
void override() {
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
var setScoreTranslator = new JavaSetScoreTranslator();
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective1",
ObjectiveAction.ADD,
Component.text("objective1"),
ScoreType.INTEGER,
null
)
);
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective2",
ObjectiveAction.ADD,
Component.text("objective2"),
ScoreType.INTEGER,
null
)
);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("Tim203", "objective1", 1));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("Tim203", "objective2", 2));
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.SIDEBAR, "objective2")
);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("0");
packet.setDisplayName("objective2");
packet.setCriteria("dummy");
packet.setDisplaySlot("sidebar");
packet.setSortOrder(1);
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(1, "0", 2, "Tim203")));
return packet;
}, context);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.SIDEBAR, "objective1")
);
assertNextPacket(() -> {
var packet = new RemoveObjectivePacket();
packet.setObjectiveId("0");
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("2");
packet.setDisplayName("objective1");
packet.setCriteria("dummy");
packet.setDisplaySlot("sidebar");
packet.setSortOrder(1);
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(3, "2", 1, "Tim203")));
return packet;
}, context);
});
}
}

Datei anzeigen

@ -0,0 +1,533 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.network.sidebar;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNoNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockContextScoreboard;
import java.util.List;
import net.kyori.adventure.text.Component;
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
import org.cloudburstmc.protocol.bedrock.packet.SetDisplayObjectivePacket;
import org.cloudburstmc.protocol.bedrock.packet.SetScorePacket;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaResetScorePacket;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetDisplayObjectiveTranslator;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetObjectiveTranslator;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetPlayerTeamTranslator;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetScoreTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.CollisionRule;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.NameTagVisibility;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ObjectiveAction;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreType;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundResetScorePacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetDisplayObjectivePacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetObjectivePacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetPlayerTeamPacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetScorePacket;
import org.junit.jupiter.api.Test;
public class OrderAndLimitSidebarScoreboardTests {
@Test
void aboveDisplayLimit() {
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
var setScoreTranslator = new JavaSetScoreTranslator();
var resetScoreTranslator = new JavaResetScorePacket();
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective",
ObjectiveAction.ADD,
Component.text("objective"),
ScoreType.INTEGER,
null
)
);
// some are in an odd order to make sure that there is no bias for which score is send first,
// and to make sure that the score value also doesn't influence the order
context.translate(setScoreTranslator, new ClientboundSetScorePacket("a", "objective", 1));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("b", "objective", 2));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("c", "objective", 3));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("d", "objective", 5));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("e", "objective", 4));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("f", "objective", 6));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("g", "objective", 9));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("h", "objective", 8));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("i", "objective", 7));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("p", "objective", 10));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("o", "objective", 11));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("n", "objective", 12));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("m", "objective", 13));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("k", "objective", 14));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("l", "objective", 15));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("j", "objective", 16));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("q", "objective", 17));
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.SIDEBAR, "objective")
);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("0");
packet.setDisplayName("objective");
packet.setCriteria("dummy");
packet.setDisplaySlot("sidebar");
packet.setSortOrder(1);
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(
new ScoreInfo(1, "0", 17, "q"),
new ScoreInfo(2, "0", 16, "j"),
new ScoreInfo(3, "0", 15, "l"),
new ScoreInfo(4, "0", 14, "k"),
new ScoreInfo(5, "0", 13, "m"),
new ScoreInfo(6, "0", 12, "n"),
new ScoreInfo(7, "0", 11, "o"),
new ScoreInfo(8, "0", 10, "p"),
new ScoreInfo(9, "0", 9, "g"),
new ScoreInfo(10, "0", 8, "h"),
new ScoreInfo(11, "0", 7, "i"),
new ScoreInfo(12, "0", 6, "f"),
new ScoreInfo(13, "0", 5, "d"),
new ScoreInfo(14, "0", 4, "e"),
new ScoreInfo(15, "0", 3, "c")
));
return packet;
}, context);
assertNoNextPacket(context);
// remove a score
context.translate(
resetScoreTranslator,
new ClientboundResetScorePacket("m", "objective")
);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(List.of(new ScoreInfo(5, "0", 13, "m")));
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(16, "0", 2, "b")));
return packet;
}, context);
// add a score
context.translate(
setScoreTranslator,
new ClientboundSetScorePacket("aa", "objective", 13)
);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(List.of(new ScoreInfo(16, "0", 2, "b")));
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(17, "0", 13, "aa")));
return packet;
}, context);
// add score with same score value (after)
context.translate(
setScoreTranslator,
new ClientboundSetScorePacket("ga", "objective", 9)
);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(List.of(
new ScoreInfo(15, "0", 3, "c"),
new ScoreInfo(9, "0", 9, "§0§rg")
));
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(
new ScoreInfo(9, "0", 9, "§0§rg"),
new ScoreInfo(18, "0", 9, "§1§rga")
));
return packet;
}, context);
// add another score with same score value (before all)
context.translate(
setScoreTranslator,
new ClientboundSetScorePacket("ag", "objective", 9)
);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(List.of(
new ScoreInfo(14, "0", 4, "e"),
new ScoreInfo(9, "0", 9, "§1§rg"),
new ScoreInfo(18, "0", 9, "§2§rga")
));
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(
new ScoreInfo(19, "0", 9, "§0§rag"),
new ScoreInfo(9, "0", 9, "§1§rg"),
new ScoreInfo(18, "0", 9, "§2§rga")
));
return packet;
}, context);
// remove score with same value
context.translate(
resetScoreTranslator,
new ClientboundResetScorePacket("g", "objective")
);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(List.of(
new ScoreInfo(9, "0", 9, "§1§rg"),
new ScoreInfo(18, "0", 9, "§1§rga")
));
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(
new ScoreInfo(18, "0", 9, "§1§rga"),
new ScoreInfo(20, "0", 4, "e")
));
return packet;
}, context);
// remove the other score with the same value
context.translate(
resetScoreTranslator,
new ClientboundResetScorePacket("ga", "objective")
);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(List.of(
new ScoreInfo(18, "0", 9, "§1§rga"),
new ScoreInfo(19, "0", 9, "ag")
));
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(
new ScoreInfo(19, "0", 9, "ag"),
new ScoreInfo(21, "0", 3, "c")
));
return packet;
}, context);
});
}
@Test
void aboveDisplayLimitWithTeam() {
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
var setScoreTranslator = new JavaSetScoreTranslator();
var resetScoreTranslator = new JavaResetScorePacket();
var setPlayerTeamTranslator = new JavaSetPlayerTeamTranslator();
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective",
ObjectiveAction.ADD,
Component.text("objective"),
ScoreType.INTEGER,
null
)
);
// some are in an odd order to make sure that there is no bias for which score is send first,
// and to make sure that the score value also doesn't influence the order
context.translate(setScoreTranslator, new ClientboundSetScorePacket("a", "objective", 1));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("b", "objective", 2));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("c", "objective", 3));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("d", "objective", 5));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("e", "objective", 4));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("f", "objective", 6));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("g", "objective", 9));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("h", "objective", 8));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("i", "objective", 7));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("p", "objective", 10));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("o", "objective", 11));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("n", "objective", 12));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("m", "objective", 13));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("k", "objective", 14));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("l", "objective", 15));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("j", "objective", 16));
context.translate(setScoreTranslator, new ClientboundSetScorePacket("q", "objective", 17));
context.translate(
setPlayerTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"team1",
Component.text("displayName"),
Component.text("prefix"),
Component.text("suffix"),
false,
false,
NameTagVisibility.ALWAYS,
CollisionRule.NEVER,
TeamColor.DARK_RED,
new String[]{ "f", "o" }
)
);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.SIDEBAR, "objective")
);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("0");
packet.setDisplayName("objective");
packet.setCriteria("dummy");
packet.setDisplaySlot("sidebar");
packet.setSortOrder(1);
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(
new ScoreInfo(1, "0", 17, "q"),
new ScoreInfo(2, "0", 16, "j"),
new ScoreInfo(3, "0", 15, "l"),
new ScoreInfo(4, "0", 14, "k"),
new ScoreInfo(5, "0", 13, "m"),
new ScoreInfo(6, "0", 12, "n"),
new ScoreInfo(7, "0", 11, "§4prefix§r§4o§r§4suffix"),
new ScoreInfo(8, "0", 10, "p"),
new ScoreInfo(9, "0", 9, "g"),
new ScoreInfo(10, "0", 8, "h"),
new ScoreInfo(11, "0", 7, "i"),
new ScoreInfo(12, "0", 6, "§4prefix§r§4f§r§4suffix"),
new ScoreInfo(13, "0", 5, "d"),
new ScoreInfo(14, "0", 4, "e"),
new ScoreInfo(15, "0", 3, "c")
));
return packet;
}, context);
assertNoNextPacket(context);
// remove a score
context.translate(
resetScoreTranslator,
new ClientboundResetScorePacket("m", "objective")
);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(List.of(new ScoreInfo(5, "0", 13, "m")));
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(16, "0", 2, "b")));
return packet;
}, context);
// add a score
context.translate(
setScoreTranslator,
new ClientboundSetScorePacket("aa", "objective", 13)
);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(List.of(new ScoreInfo(16, "0", 2, "b")));
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(17, "0", 13, "aa")));
return packet;
}, context);
// add some teams for the upcoming score adds
context.translate(
setPlayerTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"team2",
Component.text("displayName"),
Component.text("prefix"),
Component.text("suffix"),
false,
false,
NameTagVisibility.ALWAYS,
CollisionRule.NEVER,
TeamColor.DARK_AQUA,
new String[]{ "oa" }
)
);
context.translate(
setPlayerTeamTranslator,
new ClientboundSetPlayerTeamPacket(
"team3",
Component.text("displayName"),
Component.text("prefix"),
Component.text("suffix"),
false,
false,
NameTagVisibility.ALWAYS,
CollisionRule.NEVER,
TeamColor.DARK_PURPLE,
new String[]{ "ao" }
)
);
assertNoNextPacket(context);
// add a score that on Java should be after 'o', but would be before on Bedrock without manual order
// due to the team color
context.translate(
setScoreTranslator,
new ClientboundSetScorePacket("oa", "objective", 11)
);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(List.of(
new ScoreInfo(15, "0", 3, "c"),
new ScoreInfo(7, "0", 11, "§0§r§4prefix§r§4o§r§4suffix")
));
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(
new ScoreInfo(7, "0", 11, "§0§r§4prefix§r§4o§r§4suffix"),
new ScoreInfo(18, "0", 11, "§1§r§3prefix§r§3oa§r§3suffix")
));
return packet;
}, context);
// add a score that on Java should be before 'o', but would be after on Bedrock without manual order
// due to the team color
context.translate(
setScoreTranslator,
new ClientboundSetScorePacket("ao", "objective", 11)
);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(List.of(
new ScoreInfo(14, "0", 4, "e"),
new ScoreInfo(7, "0", 11, "§1§r§4prefix§r§4o§r§4suffix"),
new ScoreInfo(18, "0", 11, "§2§r§3prefix§r§3oa§r§3suffix")
));
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(
new ScoreInfo(19, "0", 11, "§0§r§5prefix§r§5ao§r§5suffix"),
new ScoreInfo(7, "0", 11, "§1§r§4prefix§r§4o§r§4suffix"),
new ScoreInfo(18, "0", 11, "§2§r§3prefix§r§3oa§r§3suffix")
));
return packet;
}, context);
// remove original 'o' score
context.translate(
resetScoreTranslator,
new ClientboundResetScorePacket("o", "objective")
);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(List.of(
new ScoreInfo(7, "0", 11, "§1§r§4prefix§r§4o§r§4suffix"),
new ScoreInfo(18, "0", 11, "§1§r§3prefix§r§3oa§r§3suffix")
));
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(
new ScoreInfo(18, "0", 11, "§1§r§3prefix§r§3oa§r§3suffix"),
new ScoreInfo(20, "0", 4, "e")
));
return packet;
}, context);
// remove the other score with the same value as 'o'
context.translate(
resetScoreTranslator,
new ClientboundResetScorePacket("oa", "objective")
);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(List.of(
new ScoreInfo(18, "0", 11, "§1§r§3prefix§r§3oa§r§3suffix"),
new ScoreInfo(19, "0", 11, "§5prefix§r§5ao§r§5suffix")
));
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(
new ScoreInfo(19, "0", 11, "§5prefix§r§5ao§r§5suffix"),
new ScoreInfo(21, "0", 3, "c")
));
return packet;
}, context);
});
}
}

Datei anzeigen

@ -0,0 +1,265 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.network.sidebar;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNoNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContextScoreboard.mockContextScoreboard;
import java.util.List;
import net.kyori.adventure.text.Component;
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
import org.cloudburstmc.protocol.bedrock.packet.SetDisplayObjectivePacket;
import org.cloudburstmc.protocol.bedrock.packet.SetScorePacket;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetDisplayObjectiveTranslator;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetObjectiveTranslator;
import org.geysermc.geyser.translator.protocol.java.scoreboard.JavaSetScoreTranslator;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ObjectiveAction;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreType;
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetDisplayObjectivePacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetObjectivePacket;
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetScorePacket;
import org.junit.jupiter.api.Test;
public class VanillaSidebarScoreboardTests {
@Test
void displayAndAddScore() {
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
var setScoreTranslator = new JavaSetScoreTranslator();
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective",
ObjectiveAction.ADD,
Component.text("objective"),
ScoreType.INTEGER,
null
)
);
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.SIDEBAR, "objective")
);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("0");
packet.setDisplayName("objective");
packet.setCriteria("dummy");
packet.setDisplaySlot("sidebar");
packet.setSortOrder(1);
return packet;
}, context);
assertNoNextPacket(context);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("owner", "objective", 1));
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(1, "0", 1, "owner")));
return packet;
}, context);
});
}
@Test
void displayAndChangeScoreValue() {
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
var setScoreTranslator = new JavaSetScoreTranslator();
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective",
ObjectiveAction.ADD,
Component.text("objective"),
ScoreType.INTEGER,
null
)
);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("owner", "objective", 1));
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.SIDEBAR, "objective")
);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("0");
packet.setDisplayName("objective");
packet.setCriteria("dummy");
packet.setDisplaySlot("sidebar");
packet.setSortOrder(1);
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(1, "0", 1, "owner")));
return packet;
}, context);
assertNoNextPacket(context);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("owner", "objective", 2));
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(1, "0", 2, "owner")));
return packet;
}, context);
});
}
@Test
void displayAndChangeScoreDisplayName() {
// this ensures that MCPE-143063 is properly handled
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
var setScoreTranslator = new JavaSetScoreTranslator();
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective",
ObjectiveAction.ADD,
Component.text("objective"),
ScoreType.INTEGER,
null
)
);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("owner", "objective", 1));
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.SIDEBAR, "objective")
);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("0");
packet.setDisplayName("objective");
packet.setCriteria("dummy");
packet.setDisplaySlot("sidebar");
packet.setSortOrder(1);
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(1, "0", 1, "owner")));
return packet;
}, context);
assertNoNextPacket(context);
context.translate(
setScoreTranslator,
new ClientboundSetScorePacket("owner", "objective", 1).withDisplay(Component.text("hi"))
);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(List.of(new ScoreInfo(1, "0", 1, "hi")));
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(1, "0", 1, "hi")));
return packet;
}, context);
});
}
@Test
void displayAndChangeScoreDisplayNameAndValue() {
// this ensures that MCPE-143063 is properly handled
mockContextScoreboard(context -> {
var setObjectiveTranslator = new JavaSetObjectiveTranslator();
var setDisplayObjectiveTranslator = new JavaSetDisplayObjectiveTranslator();
var setScoreTranslator = new JavaSetScoreTranslator();
context.translate(
setObjectiveTranslator,
new ClientboundSetObjectivePacket(
"objective",
ObjectiveAction.ADD,
Component.text("objective"),
ScoreType.INTEGER,
null
)
);
context.translate(setScoreTranslator, new ClientboundSetScorePacket("owner", "objective", 1));
assertNoNextPacket(context);
context.translate(
setDisplayObjectiveTranslator,
new ClientboundSetDisplayObjectivePacket(ScoreboardPosition.SIDEBAR, "objective")
);
assertNextPacket(() -> {
var packet = new SetDisplayObjectivePacket();
packet.setObjectiveId("0");
packet.setDisplayName("objective");
packet.setCriteria("dummy");
packet.setDisplaySlot("sidebar");
packet.setSortOrder(1);
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(1, "0", 1, "owner")));
return packet;
}, context);
assertNoNextPacket(context);
context.translate(
setScoreTranslator,
new ClientboundSetScorePacket("owner", "objective", 2).withDisplay(Component.text("hi"))
);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.REMOVE);
packet.setInfos(List.of(new ScoreInfo(1, "0", 2, "hi")));
return packet;
}, context);
assertNextPacket(() -> {
var packet = new SetScorePacket();
packet.setAction(SetScorePacket.Action.SET);
packet.setInfos(List.of(new ScoreInfo(1, "0", 2, "hi")));
return packet;
}, context);
});
}
}

Datei anzeigen

@ -0,0 +1,60 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.network.util;
import java.util.Collections;
import java.util.function.Supplier;
import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket;
import org.junit.jupiter.api.Assertions;
public class AssertUtils {
public static <T> void assertContextEquals(Supplier<? extends T> expected, T actual) {
if (actual == null) {
Assertions.fail("Expected another packet! " + expected.get());
}
Assertions.assertEquals(expected.get(), actual);
}
public static void assertNextPacket(Supplier<BedrockPacket> expected, GeyserMockContext context) {
assertContextEquals(expected, context.nextPacket());
}
public static void assertNextPacketType(GeyserMockContext context, Class<? extends BedrockPacket> type) {
var actual = context.nextPacket();
if (actual == null) {
Assertions.fail("Expected another packet! " + type);
}
Assertions.assertEquals(type, actual.getClass());
}
public static void assertNoNextPacket(GeyserMockContext context) {
Assertions.assertEquals(
Collections.emptyList(),
context.packets(),
"Expected no remaining packets, got " + context.packetCount()
);
}
}

Datei anzeigen

@ -0,0 +1,75 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.network.util;
import org.geysermc.geyser.GeyserLogger;
public class EmptyGeyserLogger implements GeyserLogger {
@Override
public void severe(String message) {
}
@Override
public void severe(String message, Throwable error) {
}
@Override
public void error(String message) {
}
@Override
public void error(String message, Throwable error) {
}
@Override
public void warning(String message) {
}
@Override
public void info(String message) {
}
@Override
public void debug(String message) {
}
@Override
public void setDebug(boolean debug) {
}
@Override
public boolean isDebug() {
return false;
}
}

Datei anzeigen

@ -0,0 +1,143 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.network.util;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.configuration.GeyserConfiguration;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
public class GeyserMockContext {
private final List<Object> mocksAndSpies = new ArrayList<>();
private final List<Object> storedObjects = new ArrayList<>();
private final List<BedrockPacket> packets = Collections.synchronizedList(new ArrayList<>());
private MockedStatic<GeyserImpl> geyserImplMock;
public static void mockContext(Consumer<GeyserMockContext> geyserContext) {
var context = new GeyserMockContext();
var geyserImpl = context.mock(GeyserImpl.class);
var config = context.mock(GeyserConfiguration.class);
when(config.getScoreboardPacketThreshold()).thenReturn(1_000);
when(geyserImpl.getConfig()).thenReturn(config);
var logger = context.storeObject(new EmptyGeyserLogger());
when(geyserImpl.getLogger()).thenReturn(logger);
try (var mocked = mockStatic(GeyserImpl.class)) {
mocked.when(GeyserImpl::getInstance).thenReturn(geyserImpl);
context.geyserImplMock = mocked;
geyserContext.accept(context);
}
}
public static void mockContext(Runnable runnable) {
mockContext(context -> runnable.run());
}
public <T> T mock(Class<T> type) {
return addMockOrSpy(Mockito.mock(type));
}
public <T> T spy(T object) {
return addMockOrSpy(Mockito.spy(object));
}
private <T> T addMockOrSpy(T mockOrSpy) {
mocksAndSpies.add(mockOrSpy);
return mockOrSpy;
}
public <T> T storeObject(T object) {
storedObjects.add(object);
return object;
}
/**
* Retries the mock or spy that is an instance of the specified type.
* This is only really intended for classes where you only need a single instance of.
*/
public <T> T mockOrSpy(Class<T> type) {
for (Object mock : mocksAndSpies) {
if (type.isInstance(mock)) {
return type.cast(mock);
}
}
return null;
}
public <T> T storedObject(Class<T> type) {
for (Object storedObject : storedObjects) {
if (type.isInstance(storedObject)) {
return type.cast(storedObject);
}
}
return null;
}
public GeyserSession session() {
return mockOrSpy(GeyserSession.class);
}
void addPacket(BedrockPacket packet) {
packets.add(packet);
}
public int packetCount() {
return packets.size();
}
public BedrockPacket nextPacket() {
if (packets.isEmpty()) {
return null;
}
return packets.remove(0);
}
public List<BedrockPacket> packets() {
return Collections.unmodifiableList(packets);
}
public <T> void translate(PacketTranslator<T> translator, T packet) {
translator.translate(session(), packet);
}
public MockedStatic<GeyserImpl> geyserImplMock() {
return geyserImplMock;
}
}

Datei anzeigen

@ -0,0 +1,96 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.scoreboard.network.util;
import static org.geysermc.geyser.scoreboard.network.util.AssertUtils.assertNoNextPacket;
import static org.geysermc.geyser.scoreboard.network.util.GeyserMockContext.mockContext;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import java.util.UUID;
import java.util.function.Consumer;
import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.entity.type.player.PlayerEntity;
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.EntityCache;
import org.geysermc.geyser.session.cache.WorldCache;
import org.mockito.stubbing.Answer;
public class GeyserMockContextScoreboard {
public static void mockContextScoreboard(Consumer<GeyserMockContext> geyserContext) {
mockContext(context -> {
createSessionSpy(context);
geyserContext.accept(context);
assertNoNextPacket(context);
});
}
private static void createSessionSpy(GeyserMockContext context) {
// GeyserSession has so many dependencies, it's easier to just mock it
var session = context.mock(GeyserSession.class);
when(session.getGeyser()).thenReturn(context.mockOrSpy(GeyserImpl.class));
when(session.locale()).thenReturn("en_US");
doAnswer((Answer<Void>) invocation -> {
context.addPacket(invocation.getArgument(0, BedrockPacket.class));
return null;
}).when(session).sendUpstreamPacket(any());
// SessionPlayerEntity loads stuff in like blocks, which is not what we want
var playerEntity = context.mock(SessionPlayerEntity.class);
when(playerEntity.getGeyserId()).thenReturn(1L);
when(playerEntity.getUsername()).thenReturn("Tim203");
when(session.getPlayerEntity()).thenReturn(playerEntity);
var entityCache = context.spy(new EntityCache(session));
when(session.getEntityCache()).thenReturn(entityCache);
var worldCache = context.spy(new WorldCache(session));
when(session.getWorldCache()).thenReturn(worldCache);
// disable global scoreboard updater
when(worldCache.increaseAndGetScoreboardPacketsPerSecond()).thenReturn(0);
}
public static PlayerEntity mockAndAddPlayerEntity(GeyserMockContext context, String username, long geyserId) {
var playerEntity = spy(new PlayerEntity(context.session(), geyserId, UUID.randomUUID(), username));
// fake the player being spawned
when(playerEntity.isValid()).thenReturn(true);
var entityCache = context.mockOrSpy(EntityCache.class);
entityCache.addPlayerEntity(playerEntity);
// called when the player spawns
entityCache.cacheEntity(playerEntity);
return playerEntity;
}
}

Datei anzeigen

@ -15,7 +15,7 @@ protocol-common = "3.0.0.Beta5-20240916.181041-6"
protocol-codec = "3.0.0.Beta5-20240916.181041-6" protocol-codec = "3.0.0.Beta5-20240916.181041-6"
raknet = "1.0.0.CR3-20240416.144209-1" raknet = "1.0.0.CR3-20240416.144209-1"
minecraftauth = "4.1.1" minecraftauth = "4.1.1"
mcprotocollib = "1.21-20240725.013034-16" mcprotocollib = "1.21-20241010.155958-24"
adventure = "4.14.0" adventure = "4.14.0"
adventure-platform = "4.3.0" adventure-platform = "4.3.0"
junit = "5.9.2" junit = "5.9.2"
@ -39,6 +39,7 @@ neoforge-minecraft = "21.1.1"
mixin = "0.8.5" mixin = "0.8.5"
mixinextras = "0.3.5" mixinextras = "0.3.5"
minecraft = "1.21.1" minecraft = "1.21.1"
mockito = "5.+"
# plugin versions # plugin versions
indra = "3.1.3" indra = "3.1.3"
@ -133,6 +134,8 @@ protocol-connection = { group = "org.cloudburstmc.protocol", name = "bedrock-con
math = { group = "org.cloudburstmc.math", name = "immutable", version = "2.0" } math = { group = "org.cloudburstmc.math", name = "immutable", version = "2.0" }
mockito = { module = "org.mockito:mockito-core", version.ref = "mockito" }
# plugins # plugins
lombok = { group = "io.freefair.gradle", name = "lombok-plugin", version.ref = "lombok" } lombok = { group = "io.freefair.gradle", name = "lombok-plugin", version.ref = "lombok" }
indra = { group = "net.kyori", name = "indra-common", version.ref = "indra" } indra = { group = "net.kyori", name = "indra-common", version.ref = "indra" }