Mirror von
https://github.com/GeyserMC/Geyser.git
synchronisiert 2025-01-11 15:41:08 +01:00
Merge remote-tracking branch 'origin/master' into feature/floodgate-merge
Dieser Commit ist enthalten in:
Commit
e4c1d4b022
@ -15,7 +15,7 @@ The ultimate goal of this project is to allow Minecraft: Bedrock Edition users t
|
||||
Special thanks to the DragonProxy project for being a trailblazer in protocol translation and for all the team members who have joined us here!
|
||||
|
||||
## Supported Versions
|
||||
Geyser is currently supporting Minecraft Bedrock 1.20.80 - 1.21.21 and Minecraft Java 1.21/1.21.1. For more information, please see [here](https://geysermc.org/wiki/geyser/supported-versions/).
|
||||
Geyser is currently supporting Minecraft Bedrock 1.20.80 - 1.21.30 and Minecraft Java 1.21/1.21.1. For more information, please see [here](https://geysermc.org/wiki/geyser/supported-versions/).
|
||||
|
||||
## Setting Up
|
||||
Take a look [here](https://geysermc.org/wiki/geyser/setup/) for how to set up Geyser.
|
||||
|
@ -63,6 +63,7 @@ dependencies {
|
||||
// Test
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(projects.isolation)
|
||||
testImplementation(libs.mockito)
|
||||
|
||||
// Annotation Processors
|
||||
compileOnly(projects.ap)
|
||||
|
@ -76,6 +76,7 @@ import org.geysermc.geyser.floodgate.IntegratedFloodgateProvider;
|
||||
import org.geysermc.geyser.floodgate.NoFloodgateProvider;
|
||||
import org.geysermc.geyser.floodgate.ProxyFloodgateProvider;
|
||||
import org.geysermc.geyser.impl.MinecraftVersionImpl;
|
||||
import org.geysermc.geyser.level.BedrockDimension;
|
||||
import org.geysermc.geyser.level.WorldManager;
|
||||
import org.geysermc.geyser.network.GameProtocol;
|
||||
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.util.AssetUtils;
|
||||
import org.geysermc.geyser.util.CooldownUtils;
|
||||
import org.geysermc.geyser.util.DimensionUtils;
|
||||
import org.geysermc.geyser.util.Metrics;
|
||||
import org.geysermc.geyser.util.MinecraftAuthLogger;
|
||||
import org.geysermc.geyser.util.VersionCheckUtils;
|
||||
@ -434,7 +434,7 @@ public class GeyserImpl implements GeyserApi, EventRegistrar {
|
||||
}
|
||||
|
||||
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");
|
||||
if (bedrockThreadCount == null) {
|
||||
|
@ -25,10 +25,10 @@
|
||||
|
||||
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 java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.function.BiConsumer;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.Accessors;
|
||||
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.registry.Registries;
|
||||
import org.geysermc.geyser.translator.entity.EntityMetadataTranslator;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.function.BiConsumer;
|
||||
import org.geysermc.geyser.util.EnvironmentUtils;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
return build(true);
|
||||
return build(!EnvironmentUtils.isUnitTesting);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -25,34 +25,131 @@
|
||||
|
||||
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.EntityFlag;
|
||||
import org.geysermc.geyser.entity.factory.EntityFactory;
|
||||
import org.geysermc.geyser.entity.properties.GeyserEntityProperties;
|
||||
import org.geysermc.geyser.entity.type.*;
|
||||
import org.geysermc.geyser.entity.type.living.*;
|
||||
import org.geysermc.geyser.entity.type.living.animal.*;
|
||||
import org.geysermc.geyser.entity.type.living.animal.horse.*;
|
||||
import org.geysermc.geyser.entity.type.AbstractArrowEntity;
|
||||
import org.geysermc.geyser.entity.type.AbstractWindChargeEntity;
|
||||
import org.geysermc.geyser.entity.type.AreaEffectCloudEntity;
|
||||
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.ParrotEntity;
|
||||
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.merchant.AbstractMerchantEntity;
|
||||
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.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.VindicatorEntity;
|
||||
import org.geysermc.geyser.entity.type.player.PlayerEntity;
|
||||
import org.geysermc.geyser.registry.Registries;
|
||||
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 static final EntityDefinition<AllayEntity> ALLAY;
|
||||
@ -1025,7 +1122,10 @@ public final class EntityDefinitions {
|
||||
.identifier("minecraft:armor_stand") // Emulated
|
||||
.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() {
|
||||
|
@ -54,7 +54,7 @@ public enum GeyserAttributeType {
|
||||
|
||||
// Bedrock Attributes
|
||||
ABSORPTION(null, "minecraft:absorption", 0f, 1024f, 0f),
|
||||
EXHAUSTION(null, "minecraft:player.exhaustion", 0f, 5f, 0f),
|
||||
EXHAUSTION(null, "minecraft:player.exhaustion", 0f, 20f, 0f),
|
||||
EXPERIENCE(null, "minecraft:player.experience", 0f, 1f, 0f),
|
||||
EXPERIENCE_LEVEL(null, "minecraft:player.level", 0f, 24791.00f, 0f),
|
||||
HEALTH(null, "minecraft:health", 0f, 1024f, 20f),
|
||||
|
@ -25,6 +25,12 @@
|
||||
|
||||
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.Getter;
|
||||
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.EntityEventType;
|
||||
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.entity.EntityDefinition;
|
||||
import org.geysermc.geyser.entity.GeyserDirtyMetadata;
|
||||
import org.geysermc.geyser.entity.properties.GeyserEntityPropertyManager;
|
||||
import org.geysermc.geyser.item.Items;
|
||||
import org.geysermc.geyser.scoreboard.Team;
|
||||
import org.geysermc.geyser.session.GeyserSession;
|
||||
import org.geysermc.geyser.translator.text.MessageTranslator;
|
||||
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.type.EntityType;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class Entity implements GeyserEntity {
|
||||
|
||||
private static final boolean PRINT_ENTITY_SPAWN_DEBUG = Boolean.parseBoolean(System.getProperty("Geyser.PrintEntitySpawnDebug", "false"));
|
||||
|
||||
protected final GeyserSession session;
|
||||
@ -68,6 +77,12 @@ public class Entity implements GeyserEntity {
|
||||
protected int entityId;
|
||||
protected final long geyserId;
|
||||
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 motion;
|
||||
@ -97,7 +112,7 @@ public class Entity implements GeyserEntity {
|
||||
@Setter(AccessLevel.NONE)
|
||||
private float boundingBoxWidth;
|
||||
@Setter(AccessLevel.NONE)
|
||||
protected String nametag = "";
|
||||
private String displayName;
|
||||
@Setter(AccessLevel.NONE)
|
||||
protected boolean silent = false;
|
||||
/* 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) {
|
||||
this.session = session;
|
||||
this.definition = definition;
|
||||
this.displayName = standardDisplayName();
|
||||
|
||||
this.entityId = entityId;
|
||||
this.geyserId = geyserId;
|
||||
this.uuid = uuid;
|
||||
this.definition = definition;
|
||||
this.motion = motion;
|
||||
this.yaw = yaw;
|
||||
this.pitch = pitch;
|
||||
@ -341,7 +357,7 @@ public class Entity implements GeyserEntity {
|
||||
* Sends the Bedrock metadata to the client
|
||||
*/
|
||||
public void updateBedrockMetadata() {
|
||||
if (!valid) {
|
||||
if (!isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -410,17 +426,84 @@ public class Entity implements GeyserEntity {
|
||||
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) {
|
||||
// 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();
|
||||
if (name.isPresent()) {
|
||||
nametag = MessageTranslator.convertMessage(name.get(), session.locale());
|
||||
dirtyMetadata.put(EntityDataTypes.NAME, nametag);
|
||||
} else if (!nametag.isEmpty()) {
|
||||
// Clear nametag
|
||||
dirtyMetadata.put(EntityDataTypes.NAME, "");
|
||||
String displayName = MessageTranslator.convertMessage(name.get(), session.locale());
|
||||
this.displayName = displayName;
|
||||
setNametag(displayName, true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
dirtyMetadata.put(EntityDataTypes.NAMETAG_ALWAYS_SHOW, (byte) (entityMetadata.getPrimitiveValue() ? 1 : 0));
|
||||
}
|
||||
|
@ -25,6 +25,11 @@
|
||||
|
||||
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.Getter;
|
||||
import lombok.Setter;
|
||||
@ -45,6 +50,7 @@ import org.geysermc.geyser.entity.vehicle.ClientVehicle;
|
||||
import org.geysermc.geyser.inventory.GeyserItemStack;
|
||||
import org.geysermc.geyser.item.Items;
|
||||
import org.geysermc.geyser.registry.type.ItemMapping;
|
||||
import org.geysermc.geyser.scoreboard.Team;
|
||||
import org.geysermc.geyser.session.GeyserSession;
|
||||
import org.geysermc.geyser.translator.item.ItemTranslator;
|
||||
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.ParticleType;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class LivingEntity extends Entity {
|
||||
|
||||
protected ItemData helmet = ItemData.AIR;
|
||||
protected ItemData chestplate = ItemData.AIR;
|
||||
protected ItemData leggings = ItemData.AIR;
|
||||
@ -150,6 +153,16 @@ public class LivingEntity extends Entity {
|
||||
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) {
|
||||
byte xd = entityMetadata.getPrimitiveValue();
|
||||
|
||||
|
@ -25,6 +25,7 @@
|
||||
|
||||
package org.geysermc.geyser.entity.type.living.animal;
|
||||
|
||||
import net.kyori.adventure.key.Key;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.cloudburstmc.math.vector.Vector3f;
|
||||
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.session.GeyserSession;
|
||||
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 java.util.UUID;
|
||||
|
||||
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) {
|
||||
super(session, entityId, geyserId, uuid, definition, position, motion, yaw, pitch, headYaw);
|
||||
@ -46,7 +49,7 @@ public class RabbitEntity extends AnimalEntity {
|
||||
int variant = entityMetadata.getPrimitiveValue();
|
||||
|
||||
// Change the killer bunny to display as white since it only exists on Java Edition
|
||||
boolean isKillerBunny = variant == 99;
|
||||
isKillerBunny = variant == 99;
|
||||
if (isKillerBunny) {
|
||||
variant = 1;
|
||||
}
|
||||
@ -56,6 +59,14 @@ public class RabbitEntity extends AnimalEntity {
|
||||
dirtyMetadata.put(EntityDataTypes.VARIANT, variant);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String standardDisplayName() {
|
||||
if (isKillerBunny) {
|
||||
return EntityUtils.translatedEntityName(Key.key("killer_bunny"), session);
|
||||
}
|
||||
return super.standardDisplayName();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected float getAdultSize() {
|
||||
return 0.55f;
|
||||
@ -71,4 +82,4 @@ public class RabbitEntity extends AnimalEntity {
|
||||
protected ItemTag getFoodTag() {
|
||||
return ItemTag.RABBIT_FOOD;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,6 +55,17 @@ public class TurtleEntity extends AnimalEntity {
|
||||
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
|
||||
public boolean canBeLeashed() {
|
||||
return false;
|
||||
|
@ -25,6 +25,12 @@
|
||||
|
||||
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.Setter;
|
||||
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.Vector3i;
|
||||
import org.cloudburstmc.nbt.NbtMap;
|
||||
import org.cloudburstmc.nbt.NbtMapBuilder;
|
||||
import org.cloudburstmc.protocol.bedrock.data.Ability;
|
||||
import org.cloudburstmc.protocol.bedrock.data.AbilityLayer;
|
||||
import org.cloudburstmc.protocol.bedrock.data.GameType;
|
||||
import org.cloudburstmc.protocol.bedrock.data.PlayerPermission;
|
||||
import org.cloudburstmc.protocol.bedrock.data.command.CommandPermission;
|
||||
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataMap;
|
||||
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
|
||||
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
|
||||
import org.cloudburstmc.protocol.bedrock.data.entity.EntityLinkData;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.AddPlayerPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.MovePlayerPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.SetEntityDataPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.SetEntityLinkPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.UpdateAttributesPacket;
|
||||
import org.geysermc.geyser.api.entity.type.player.GeyserPlayerEntity;
|
||||
@ -53,32 +58,13 @@ import org.geysermc.geyser.entity.attribute.GeyserAttributeType;
|
||||
import org.geysermc.geyser.entity.type.Entity;
|
||||
import org.geysermc.geyser.entity.type.LivingEntity;
|
||||
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.text.ChatColor;
|
||||
import org.geysermc.geyser.translator.text.MessageTranslator;
|
||||
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.Pose;
|
||||
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.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
|
||||
public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
|
||||
@ -96,6 +82,9 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
|
||||
|
||||
private String username;
|
||||
|
||||
private String cachedScore = "";
|
||||
private boolean scoreVisible = true;
|
||||
|
||||
/**
|
||||
* The textures property from the GameProfile.
|
||||
*/
|
||||
@ -123,6 +112,20 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
|
||||
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
|
||||
protected void initializeMetadata() {
|
||||
super.initializeMetadata();
|
||||
@ -132,17 +135,6 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
|
||||
|
||||
@Override
|
||||
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.setUuid(uuid);
|
||||
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
|
||||
this.resetMetadata();
|
||||
this.nametag = username;
|
||||
this.hand = ItemData.AIR;
|
||||
this.offhand = ItemData.AIR;
|
||||
this.boots = ItemData.AIR;
|
||||
@ -386,38 +379,30 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return username;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDisplayName(EntityMetadata<Optional<Component>, ?> entityMetadata) {
|
||||
// Doesn't do anything for players
|
||||
}
|
||||
|
||||
//todo this will become common entity logic once UUID support is implemented for them
|
||||
public void updateDisplayName(@Nullable Team team) {
|
||||
boolean needsUpdate;
|
||||
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;
|
||||
}
|
||||
@Override
|
||||
public String teamIdentifier() {
|
||||
return username;
|
||||
}
|
||||
|
||||
if (needsUpdate) {
|
||||
dirtyMetadata.put(EntityDataTypes.NAME, this.nametag);
|
||||
@Override
|
||||
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
|
||||
@ -425,6 +410,33 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
|
||||
// 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
|
||||
protected void setDimensions(Pose pose) {
|
||||
float height;
|
||||
@ -451,64 +463,6 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
|
||||
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.
|
||||
*/
|
||||
|
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.erosion;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
public class ErosionCancellationException extends CancellationException {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -42,7 +42,6 @@ public final class GeyserboundHandshakePacketHandler extends AbstractGeyserbound
|
||||
public void handleHandshake(GeyserboundHandshakePacket packet) {
|
||||
boolean useTcp = packet.getTransportType().getSocketAddress() == null;
|
||||
GeyserboundPacketHandlerImpl handler = new GeyserboundPacketHandlerImpl(session, useTcp ? new GeyserErosionPacketSender(session) : new NettyPacketSender<>());
|
||||
session.setErosionHandler(handler);
|
||||
if (!useTcp) {
|
||||
if (session.getGeyser().getErosionUnixListener() == null) {
|
||||
session.disconnect("Erosion configurations using Unix socket handling are not supported on this hardware!");
|
||||
@ -52,6 +51,7 @@ public final class GeyserboundHandshakePacketHandler extends AbstractGeyserbound
|
||||
} else {
|
||||
handler.onConnect();
|
||||
}
|
||||
session.setErosionHandler(handler);
|
||||
session.ensureInEventLoop(() -> session.getChunkCache().clear());
|
||||
}
|
||||
|
||||
|
@ -171,10 +171,10 @@ public final class GeyserboundPacketHandlerImpl extends AbstractGeyserboundPacke
|
||||
|
||||
@Override
|
||||
public void handleHandshake(GeyserboundHandshakePacket packet) {
|
||||
this.close();
|
||||
var handler = new GeyserboundHandshakePacketHandler(this.session);
|
||||
session.setErosionHandler(handler);
|
||||
handler.handleHandshake(packet);
|
||||
this.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -198,6 +198,17 @@ public final class GeyserboundPacketHandlerImpl extends AbstractGeyserboundPacke
|
||||
|
||||
public void close() {
|
||||
this.packetSender.close();
|
||||
|
||||
if (pendingLookup != null) {
|
||||
pendingLookup.completeExceptionally(new ErosionCancellationException());
|
||||
}
|
||||
if (pendingBatchLookup != null) {
|
||||
pendingBatchLookup.completeExceptionally(new ErosionCancellationException());
|
||||
}
|
||||
if (pickBlockLookup != null) {
|
||||
pickBlockLookup.completeExceptionally(new ErosionCancellationException());
|
||||
}
|
||||
asyncPendingLookups.forEach(($, future) -> future.completeExceptionally(new ErosionCancellationException()));
|
||||
}
|
||||
|
||||
public int getNextTransactionId() {
|
||||
|
@ -38,6 +38,7 @@ import org.geysermc.geyser.command.GeyserCommandSource;
|
||||
import org.geysermc.geyser.session.GeyserSession;
|
||||
import org.incendo.cloud.CommandManager;
|
||||
import org.incendo.cloud.context.CommandContext;
|
||||
import org.incendo.cloud.description.CommandDescription;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@ -193,11 +194,17 @@ public abstract class GeyserExtensionCommand extends GeyserCommand {
|
||||
.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")
|
||||
@Override
|
||||
public void execute(CommandContext<GeyserCommandSource> context) {
|
||||
GeyserCommandSource source = context.sender();
|
||||
String[] args = context.getOrDefault("args", "").split(" ");
|
||||
String[] args = context.getOrDefault("args", " ").split(" ");
|
||||
|
||||
if (sourceType.isInstance(source)) {
|
||||
executor.execute((T) source, this, args);
|
||||
|
@ -43,13 +43,13 @@ public class CameraDefinitions {
|
||||
|
||||
static {
|
||||
CAMERA_PRESETS = List.of(
|
||||
new CameraPreset(CameraPerspective.FIRST_PERSON.id(), "", null, null, null, null, null, null, OptionalBoolean.empty()),
|
||||
new CameraPreset(CameraPerspective.FREE.id(), "", null, null, null, null, null, null, OptionalBoolean.empty()),
|
||||
new CameraPreset(CameraPerspective.THIRD_PERSON.id(), "", null, null, null, null, null, null, OptionalBoolean.empty()),
|
||||
new CameraPreset(CameraPerspective.THIRD_PERSON_FRONT.id(), "", null, null, null, null, null, null, OptionalBoolean.empty()),
|
||||
new CameraPreset("geyser:free_audio", "minecraft:free", null, null, null, null, null, CameraAudioListener.PLAYER, OptionalBoolean.of(false)),
|
||||
new CameraPreset("geyser:free_effects", "minecraft:free", null, null, null, null, null, CameraAudioListener.CAMERA, OptionalBoolean.of(true)),
|
||||
new CameraPreset("geyser:free_audio_effects", "minecraft:free", null, null, null, null, null, CameraAudioListener.PLAYER, OptionalBoolean.of(true)));
|
||||
new CameraPreset(CameraPerspective.FIRST_PERSON.id(), "", null, null, null, null, null, null, OptionalBoolean.empty(), null, OptionalBoolean.empty(), null),
|
||||
new CameraPreset(CameraPerspective.FREE.id(), "", null, null, null, null, null, null, OptionalBoolean.empty(), null, OptionalBoolean.empty(), null),
|
||||
new CameraPreset(CameraPerspective.THIRD_PERSON.id(), "", null, null, null, null, null, null, OptionalBoolean.empty(), null, OptionalBoolean.empty(), null),
|
||||
new CameraPreset(CameraPerspective.THIRD_PERSON_FRONT.id(), "", null, null, null, null, null, null, OptionalBoolean.empty(), null, OptionalBoolean.empty(), null),
|
||||
new CameraPreset("geyser:free_audio", "minecraft:free", null, null, null, null, null, CameraAudioListener.PLAYER, OptionalBoolean.empty(), null, OptionalBoolean.of(false), null),
|
||||
new CameraPreset("geyser:free_effects", "minecraft:free", null, null, null, null, null, CameraAudioListener.CAMERA, OptionalBoolean.empty(), null, OptionalBoolean.of(true), null),
|
||||
new CameraPreset("geyser:free_audio_effects", "minecraft:free", null, null, null, null, null, CameraAudioListener.PLAYER, OptionalBoolean.empty(), null, OptionalBoolean.of(true), null));
|
||||
|
||||
SimpleDefinitionRegistry.Builder<NamedDefinition> builder = SimpleDefinitionRegistry.builder();
|
||||
for (int i = 0; i < CAMERA_PRESETS.size(); i++) {
|
||||
|
@ -32,6 +32,8 @@ import net.kyori.adventure.text.Component;
|
||||
import org.cloudburstmc.nbt.NbtMap;
|
||||
import org.cloudburstmc.nbt.NbtMapBuilder;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket;
|
||||
import org.geysermc.geyser.GeyserImpl;
|
||||
@ -78,6 +80,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater {
|
||||
slotPacket.setContainerId(ContainerId.UI);
|
||||
slotPacket.setSlot(bedrockSlot);
|
||||
slotPacket.setItem(inventory.getItem(i).getItemData(session));
|
||||
slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(slotPacket);
|
||||
}
|
||||
}
|
||||
@ -98,6 +101,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater {
|
||||
slotPacket.setContainerId(ContainerId.UI);
|
||||
slotPacket.setSlot(translator.javaSlotToBedrock(javaSlot));
|
||||
slotPacket.setItem(inventory.getItem(javaSlot).getItemData(session));
|
||||
slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(slotPacket);
|
||||
} else if (lastTargetSlot != javaSlot) {
|
||||
// Update the previous target slot to remove repair cost changes
|
||||
@ -105,6 +109,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater {
|
||||
slotPacket.setContainerId(ContainerId.UI);
|
||||
slotPacket.setSlot(translator.javaSlotToBedrock(lastTargetSlot));
|
||||
slotPacket.setItem(inventory.getItem(lastTargetSlot).getItemData(session));
|
||||
slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(slotPacket);
|
||||
}
|
||||
|
||||
@ -168,6 +173,7 @@ public class AnvilInventoryUpdater extends InventoryUpdater {
|
||||
slotPacket.setContainerId(ContainerId.UI);
|
||||
slotPacket.setSlot(translator.javaSlotToBedrock(slot));
|
||||
slotPacket.setItem(itemData);
|
||||
slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(slotPacket);
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,8 @@
|
||||
|
||||
package org.geysermc.geyser.inventory.updater;
|
||||
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.InventoryContentPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket;
|
||||
@ -61,6 +63,7 @@ public class ChestInventoryUpdater extends InventoryUpdater {
|
||||
InventoryContentPacket contentPacket = new InventoryContentPacket();
|
||||
contentPacket.setContainerId(inventory.getBedrockId());
|
||||
contentPacket.setContents(bedrockItems);
|
||||
contentPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(contentPacket);
|
||||
}
|
||||
|
||||
@ -73,6 +76,7 @@ public class ChestInventoryUpdater extends InventoryUpdater {
|
||||
slotPacket.setContainerId(inventory.getBedrockId());
|
||||
slotPacket.setSlot(translator.javaSlotToBedrock(javaSlot));
|
||||
slotPacket.setItem(inventory.getItem(javaSlot).getItemData(session));
|
||||
slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(slotPacket);
|
||||
return true;
|
||||
}
|
||||
|
@ -25,6 +25,8 @@
|
||||
|
||||
package org.geysermc.geyser.inventory.updater;
|
||||
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.InventoryContentPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket;
|
||||
@ -49,6 +51,7 @@ public class ContainerInventoryUpdater extends InventoryUpdater {
|
||||
InventoryContentPacket contentPacket = new InventoryContentPacket();
|
||||
contentPacket.setContainerId(inventory.getBedrockId());
|
||||
contentPacket.setContents(Arrays.asList(bedrockItems));
|
||||
contentPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(contentPacket);
|
||||
}
|
||||
|
||||
@ -61,6 +64,7 @@ public class ContainerInventoryUpdater extends InventoryUpdater {
|
||||
slotPacket.setContainerId(inventory.getBedrockId());
|
||||
slotPacket.setSlot(translator.javaSlotToBedrock(javaSlot));
|
||||
slotPacket.setItem(inventory.getItem(javaSlot).getItemData(session));
|
||||
slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(slotPacket);
|
||||
return true;
|
||||
}
|
||||
|
@ -26,6 +26,8 @@
|
||||
package org.geysermc.geyser.inventory.updater;
|
||||
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.InventoryContentPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket;
|
||||
@ -56,6 +58,7 @@ public class CrafterInventoryUpdater extends InventoryUpdater {
|
||||
contentPacket = new InventoryContentPacket();
|
||||
contentPacket.setContainerId(inventory.getBedrockId());
|
||||
contentPacket.setContents(Arrays.asList(bedrockItems));
|
||||
contentPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(contentPacket);
|
||||
|
||||
// inventory and hotbar
|
||||
@ -67,6 +70,7 @@ public class CrafterInventoryUpdater extends InventoryUpdater {
|
||||
contentPacket = new InventoryContentPacket();
|
||||
contentPacket.setContainerId(ContainerId.INVENTORY);
|
||||
contentPacket.setContents(Arrays.asList(bedrockItems));
|
||||
contentPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(contentPacket);
|
||||
|
||||
// Crafter result - it doesn't come after the grid, as explained elsewhere.
|
||||
@ -88,6 +92,7 @@ public class CrafterInventoryUpdater extends InventoryUpdater {
|
||||
packet.setContainerId(containerId);
|
||||
packet.setSlot(translator.javaSlotToBedrock(javaSlot));
|
||||
packet.setItem(inventory.getItem(javaSlot).getItemData(session));
|
||||
packet.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(packet);
|
||||
return true;
|
||||
}
|
||||
|
@ -25,6 +25,8 @@
|
||||
|
||||
package org.geysermc.geyser.inventory.updater;
|
||||
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.InventoryContentPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket;
|
||||
@ -49,6 +51,7 @@ public class HorseInventoryUpdater extends InventoryUpdater {
|
||||
InventoryContentPacket contentPacket = new InventoryContentPacket();
|
||||
contentPacket.setContainerId(inventory.getBedrockId());
|
||||
contentPacket.setContents(Arrays.asList(bedrockItems));
|
||||
contentPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(contentPacket);
|
||||
}
|
||||
|
||||
@ -61,6 +64,7 @@ public class HorseInventoryUpdater extends InventoryUpdater {
|
||||
slotPacket.setContainerId(4); // Horse GUI?
|
||||
slotPacket.setSlot(translator.javaSlotToBedrock(javaSlot));
|
||||
slotPacket.setItem(inventory.getItem(javaSlot).getItemData(session));
|
||||
slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(slotPacket);
|
||||
return true;
|
||||
}
|
||||
|
@ -26,6 +26,8 @@
|
||||
package org.geysermc.geyser.inventory.updater;
|
||||
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.InventoryContentPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket;
|
||||
@ -45,6 +47,7 @@ public class InventoryUpdater {
|
||||
InventoryContentPacket contentPacket = new InventoryContentPacket();
|
||||
contentPacket.setContainerId(ContainerId.INVENTORY);
|
||||
contentPacket.setContents(Arrays.asList(bedrockItems));
|
||||
contentPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(contentPacket);
|
||||
}
|
||||
|
||||
@ -54,6 +57,7 @@ public class InventoryUpdater {
|
||||
slotPacket.setContainerId(ContainerId.INVENTORY);
|
||||
slotPacket.setSlot(translator.javaSlotToBedrock(javaSlot));
|
||||
slotPacket.setItem(inventory.getItem(javaSlot).getItemData(session));
|
||||
slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(slotPacket);
|
||||
return true;
|
||||
}
|
||||
|
@ -26,6 +26,8 @@
|
||||
package org.geysermc.geyser.inventory.updater;
|
||||
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket;
|
||||
import org.geysermc.geyser.inventory.Inventory;
|
||||
import org.geysermc.geyser.session.GeyserSession;
|
||||
@ -46,6 +48,7 @@ public class UIInventoryUpdater extends InventoryUpdater {
|
||||
slotPacket.setContainerId(ContainerId.UI);
|
||||
slotPacket.setSlot(bedrockSlot);
|
||||
slotPacket.setItem(inventory.getItem(i).getItemData(session));
|
||||
slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(slotPacket);
|
||||
}
|
||||
}
|
||||
@ -59,6 +62,7 @@ public class UIInventoryUpdater extends InventoryUpdater {
|
||||
slotPacket.setContainerId(ContainerId.UI);
|
||||
slotPacket.setSlot(translator.javaSlotToBedrock(javaSlot));
|
||||
slotPacket.setItem(inventory.getItem(javaSlot).getItemData(session));
|
||||
slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(slotPacket);
|
||||
return true;
|
||||
}
|
||||
|
@ -25,8 +25,7 @@
|
||||
|
||||
package org.geysermc.geyser.item.enchantment;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrays;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.cloudburstmc.nbt.NbtMap;
|
||||
@ -35,11 +34,14 @@ import org.geysermc.geyser.item.Items;
|
||||
import org.geysermc.geyser.registry.Registries;
|
||||
import org.geysermc.geyser.session.cache.registry.RegistryEntryContext;
|
||||
import org.geysermc.geyser.translator.text.MessageTranslator;
|
||||
import org.geysermc.geyser.util.MinecraftKey;
|
||||
import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.geysermc.mcprotocollib.protocol.data.game.item.component.HolderSet;
|
||||
import java.util.function.ToIntFunction;
|
||||
|
||||
/**
|
||||
* @param description only populated if {@link #bedrockEnchantment()} is not null.
|
||||
@ -69,7 +71,7 @@ public record Enchantment(String identifier,
|
||||
|
||||
// TODO - description is a component. So if a hardcoded literal string is given, this will display normally on Java,
|
||||
// but Geyser will attempt to lookup the literal string as translation - and will fail, displaying an empty string as enchantment name.
|
||||
String description = bedrockEnchantment == null ? MessageTranslator.deserializeDescription(data) : null;
|
||||
String description = bedrockEnchantment == null ? MessageTranslator.deserializeDescription(context.session(), data) : null;
|
||||
|
||||
return new Enchantment(context.id().asString(), effects, supportedItems, maxLevel,
|
||||
description, anvilCost, exclusiveSet, bedrockEnchantment);
|
||||
@ -86,21 +88,21 @@ public record Enchantment(String identifier,
|
||||
}
|
||||
|
||||
// TODO holder set util?
|
||||
private static HolderSet readHolderSet(@Nullable Object holderSet, Function<Key, Integer> keyIdMapping) {
|
||||
private static HolderSet readHolderSet(@Nullable Object holderSet, ToIntFunction<Key> keyIdMapping) {
|
||||
if (holderSet == null) {
|
||||
return new HolderSet(new int[]{});
|
||||
return new HolderSet(IntArrays.EMPTY_ARRAY);
|
||||
}
|
||||
|
||||
if (holderSet instanceof String stringTag) {
|
||||
// Tag
|
||||
if (stringTag.startsWith("#")) {
|
||||
return new HolderSet(Key.key(stringTag.substring(1))); // Remove '#' at beginning that indicates tag
|
||||
return new HolderSet(MinecraftKey.key(stringTag.substring(1))); // Remove '#' at beginning that indicates tag
|
||||
} else {
|
||||
return new HolderSet(new int[]{keyIdMapping.apply(Key.key(stringTag))});
|
||||
return new HolderSet(new int[]{keyIdMapping.applyAsInt(MinecraftKey.key(stringTag))});
|
||||
}
|
||||
} else if (holderSet instanceof List<?> list) {
|
||||
// Assume the list is a list of strings
|
||||
return new HolderSet(list.stream().map(o -> (String) o).map(Key::key).map(keyIdMapping).mapToInt(Integer::intValue).toArray());
|
||||
return new HolderSet(list.stream().map(o -> (String) o).map(Key::key).mapToInt(keyIdMapping).toArray());
|
||||
}
|
||||
throw new IllegalArgumentException("Holder set must either be a tag, a string ID or a list of string IDs");
|
||||
}
|
||||
|
@ -25,17 +25,84 @@
|
||||
|
||||
package org.geysermc.geyser.level;
|
||||
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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) {
|
||||
public static final BedrockDimension OVERWORLD = new BedrockDimension(-64, 384, true);
|
||||
public static final BedrockDimension THE_NETHER = new BedrockDimension(0, 128, false);
|
||||
public static final BedrockDimension THE_END = new BedrockDimension(0, 256, true);
|
||||
@ToString
|
||||
public class BedrockDimension {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ import org.geysermc.erosion.packet.backendbound.BackendboundBatchBlockRequestPac
|
||||
import org.geysermc.erosion.packet.backendbound.BackendboundBlockRequestPacket;
|
||||
import org.geysermc.erosion.packet.backendbound.BackendboundPickBlockPacket;
|
||||
import org.geysermc.erosion.util.BlockPositionIterator;
|
||||
import org.geysermc.geyser.erosion.ErosionCancellationException;
|
||||
import org.geysermc.geyser.session.GeyserSession;
|
||||
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode;
|
||||
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents;
|
||||
@ -49,6 +50,8 @@ public class GeyserWorldManager extends WorldManager {
|
||||
var erosionHandler = session.getErosionHandler().getAsActive();
|
||||
if (erosionHandler == null) {
|
||||
return session.getChunkCache().getBlockAt(x, y, z);
|
||||
} else if (session.isClosed()) {
|
||||
throw new ErosionCancellationException();
|
||||
}
|
||||
CompletableFuture<Integer> future = new CompletableFuture<>(); // Boxes
|
||||
erosionHandler.setPendingLookup(future);
|
||||
@ -61,6 +64,8 @@ public class GeyserWorldManager extends WorldManager {
|
||||
var erosionHandler = session.getErosionHandler().getAsActive();
|
||||
if (erosionHandler == null) {
|
||||
return super.getBlockAtAsync(session, x, y, z);
|
||||
} else if (session.isClosed()) {
|
||||
return CompletableFuture.failedFuture(new ErosionCancellationException());
|
||||
}
|
||||
CompletableFuture<Integer> future = new CompletableFuture<>(); // Boxes
|
||||
int transactionId = erosionHandler.getNextTransactionId();
|
||||
@ -74,6 +79,8 @@ public class GeyserWorldManager extends WorldManager {
|
||||
var erosionHandler = session.getErosionHandler().getAsActive();
|
||||
if (erosionHandler == null) {
|
||||
return super.getBlocksAt(session, iter);
|
||||
} else if (session.isClosed()) {
|
||||
throw new ErosionCancellationException();
|
||||
}
|
||||
CompletableFuture<int[]> future = new CompletableFuture<>();
|
||||
erosionHandler.setPendingBatchLookup(future);
|
||||
@ -124,6 +131,8 @@ public class GeyserWorldManager extends WorldManager {
|
||||
var erosionHandler = session.getErosionHandler().getAsActive();
|
||||
if (erosionHandler == null) {
|
||||
return super.getPickItemComponents(session, x, y, z, addNbtData);
|
||||
} else if (session.isClosed()) {
|
||||
return CompletableFuture.failedFuture(new ErosionCancellationException());
|
||||
}
|
||||
CompletableFuture<Int2ObjectMap<byte[]>> future = new CompletableFuture<>();
|
||||
erosionHandler.setPickBlockLookup(future);
|
||||
|
@ -63,12 +63,19 @@ public record JavaDimension(int minY, int maxY, boolean piglinSafe, boolean ultr
|
||||
if ("minecraft".equals(id.namespace())) {
|
||||
String identifier = id.asString();
|
||||
bedrockId = DimensionUtils.javaToBedrock(identifier);
|
||||
isNetherLike = DimensionUtils.NETHER_IDENTIFIER.equals(identifier);
|
||||
isNetherLike = BedrockDimension.NETHER_IDENTIFIER.equals(identifier);
|
||||
} else {
|
||||
// Effects should give is a clue on how this (custom) dimension is supposed to look like
|
||||
String effects = dimension.getString("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);
|
||||
|
@ -44,7 +44,7 @@ public record JukeboxSong(String soundEvent, String description) {
|
||||
soundEvent = "";
|
||||
GeyserImpl.getInstance().getLogger().debug("Sound event for " + context.id() + " was of an unexpected type! Expected string or NBT map, got " + soundEventObject);
|
||||
}
|
||||
String description = MessageTranslator.deserializeDescription(data);
|
||||
String description = MessageTranslator.deserializeDescription(context.session(), data);
|
||||
return new JukeboxSong(soundEvent, description);
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,6 @@ import org.cloudburstmc.protocol.bedrock.codec.v291.serializer.MobArmorEquipment
|
||||
import org.cloudburstmc.protocol.bedrock.codec.v291.serializer.MobEquipmentSerializer_v291;
|
||||
import org.cloudburstmc.protocol.bedrock.codec.v291.serializer.PlayerHotbarSerializer_v291;
|
||||
import org.cloudburstmc.protocol.bedrock.codec.v291.serializer.SetEntityLinkSerializer_v291;
|
||||
import org.cloudburstmc.protocol.bedrock.codec.v291.serializer.SetEntityMotionSerializer_v291;
|
||||
import org.cloudburstmc.protocol.bedrock.codec.v390.serializer.PlayerSkinSerializer_v390;
|
||||
import org.cloudburstmc.protocol.bedrock.codec.v407.serializer.InventoryContentSerializer_v407;
|
||||
import org.cloudburstmc.protocol.bedrock.codec.v407.serializer.InventorySlotSerializer_v407;
|
||||
@ -43,6 +42,8 @@ import org.cloudburstmc.protocol.bedrock.codec.v662.serializer.SetEntityMotionSe
|
||||
import org.cloudburstmc.protocol.bedrock.codec.v712.serializer.InventoryContentSerializer_v712;
|
||||
import org.cloudburstmc.protocol.bedrock.codec.v712.serializer.InventorySlotSerializer_v712;
|
||||
import org.cloudburstmc.protocol.bedrock.codec.v712.serializer.MobArmorEquipmentSerializer_v712;
|
||||
import org.cloudburstmc.protocol.bedrock.codec.v729.serializer.InventoryContentSerializer_v729;
|
||||
import org.cloudburstmc.protocol.bedrock.codec.v729.serializer.InventorySlotSerializer_v729;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.AnvilDamagePacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.BossEventPacket;
|
||||
@ -139,6 +140,13 @@ class CodecProcessor {
|
||||
}
|
||||
};
|
||||
|
||||
private static final BedrockPacketSerializer<InventoryContentPacket> INVENTORY_CONTENT_SERIALIZER_V729 = new InventoryContentSerializer_v729() {
|
||||
@Override
|
||||
public void deserialize(ByteBuf buffer, BedrockCodecHelper helper, InventoryContentPacket packet) {
|
||||
throw new IllegalArgumentException("Client cannot send InventoryContentPacket in server-auth inventory environment!");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Serializer that throws an exception when trying to deserialize InventorySlotPacket since server-auth inventory is used.
|
||||
*/
|
||||
@ -159,6 +167,13 @@ class CodecProcessor {
|
||||
}
|
||||
};
|
||||
|
||||
private static final BedrockPacketSerializer<InventorySlotPacket> INVENTORY_SLOT_SERIALIZER_V729 = new InventorySlotSerializer_v729() {
|
||||
@Override
|
||||
public void deserialize(ByteBuf buffer, BedrockCodecHelper helper, InventorySlotPacket packet) {
|
||||
throw new IllegalArgumentException("Client cannot send InventorySlotPacket in server-auth inventory environment!");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Serializer that does nothing when trying to deserialize BossEventPacket since it is not used from the client.
|
||||
*/
|
||||
@ -214,16 +229,7 @@ class CodecProcessor {
|
||||
};
|
||||
|
||||
/**
|
||||
* Serializer that does nothing when trying to deserialize SetEntityMotionPacket since it is not used from the client for codec v291.
|
||||
*/
|
||||
private static final BedrockPacketSerializer<SetEntityMotionPacket> SET_ENTITY_MOTION_SERIALIZER_V291 = new SetEntityMotionSerializer_v291() {
|
||||
@Override
|
||||
public void deserialize(ByteBuf buffer, BedrockCodecHelper helper, SetEntityMotionPacket packet) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Serializer that does nothing when trying to deserialize SetEntityMotionPacket since it is not used from the client for codec v662.
|
||||
* Serializer that does nothing when trying to deserialize SetEntityMotionPacket since it is not used from the client.
|
||||
*/
|
||||
private static final BedrockPacketSerializer<SetEntityMotionPacket> SET_ENTITY_MOTION_SERIALIZER = new SetEntityMotionSerializer_v662() {
|
||||
@Override
|
||||
@ -256,8 +262,27 @@ class CodecProcessor {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static BedrockCodec processCodec(BedrockCodec codec) {
|
||||
boolean isPre712 = codec.getProtocolVersion() < 712;
|
||||
|
||||
boolean is729OrAbove = codec.getProtocolVersion() >= 729;
|
||||
boolean is712OrAbove = codec.getProtocolVersion() >= 712;
|
||||
|
||||
BedrockPacketSerializer<InventoryContentPacket> inventoryContentSerializer;
|
||||
if (is729OrAbove) {
|
||||
inventoryContentSerializer = INVENTORY_CONTENT_SERIALIZER_V729;
|
||||
} else if (is712OrAbove) {
|
||||
inventoryContentSerializer = INVENTORY_CONTENT_SERIALIZER_V712;
|
||||
} else {
|
||||
inventoryContentSerializer = INVENTORY_CONTENT_SERIALIZER_V407;
|
||||
}
|
||||
|
||||
BedrockPacketSerializer<InventorySlotPacket> inventorySlotSerializer;
|
||||
if (is729OrAbove) {
|
||||
inventorySlotSerializer = INVENTORY_SLOT_SERIALIZER_V729;
|
||||
} else if (is712OrAbove) {
|
||||
inventorySlotSerializer = INVENTORY_SLOT_SERIALIZER_V712;
|
||||
} else {
|
||||
inventorySlotSerializer = INVENTORY_SLOT_SERIALIZER_V407;
|
||||
}
|
||||
|
||||
BedrockCodec.Builder codecBuilder = codec.toBuilder()
|
||||
// Illegal unused serverbound EDU packets
|
||||
.updateSerializer(PhotoTransferPacket.class, ILLEGAL_SERIALIZER)
|
||||
@ -286,11 +311,11 @@ class CodecProcessor {
|
||||
.updateSerializer(AnvilDamagePacket.class, IGNORED_SERIALIZER)
|
||||
.updateSerializer(RefreshEntitlementsPacket.class, IGNORED_SERIALIZER)
|
||||
// Illegal when serverbound due to Geyser specific setup
|
||||
.updateSerializer(InventoryContentPacket.class, isPre712 ? INVENTORY_CONTENT_SERIALIZER_V407 : INVENTORY_CONTENT_SERIALIZER_V712)
|
||||
.updateSerializer(InventorySlotPacket.class, isPre712 ? INVENTORY_SLOT_SERIALIZER_V407 : INVENTORY_SLOT_SERIALIZER_V712)
|
||||
.updateSerializer(InventoryContentPacket.class, inventoryContentSerializer)
|
||||
.updateSerializer(InventorySlotPacket.class, inventorySlotSerializer)
|
||||
// Ignored only when serverbound
|
||||
.updateSerializer(BossEventPacket.class, BOSS_EVENT_SERIALIZER)
|
||||
.updateSerializer(MobArmorEquipmentPacket.class, isPre712 ? MOB_ARMOR_EQUIPMENT_SERIALIZER_V291 : MOB_ARMOR_EQUIPMENT_SERIALIZER_V712)
|
||||
.updateSerializer(MobArmorEquipmentPacket.class, is712OrAbove ? MOB_ARMOR_EQUIPMENT_SERIALIZER_V712 : MOB_ARMOR_EQUIPMENT_SERIALIZER_V291)
|
||||
.updateSerializer(PlayerHotbarPacket.class, PLAYER_HOTBAR_SERIALIZER)
|
||||
.updateSerializer(PlayerSkinPacket.class, PLAYER_SKIN_SERIALIZER)
|
||||
.updateSerializer(SetEntityDataPacket.class, SET_ENTITY_DATA_SERIALIZER)
|
||||
|
@ -31,6 +31,7 @@ import org.cloudburstmc.protocol.bedrock.codec.v671.Bedrock_v671;
|
||||
import org.cloudburstmc.protocol.bedrock.codec.v685.Bedrock_v685;
|
||||
import org.cloudburstmc.protocol.bedrock.codec.v686.Bedrock_v686;
|
||||
import org.cloudburstmc.protocol.bedrock.codec.v712.Bedrock_v712;
|
||||
import org.cloudburstmc.protocol.bedrock.codec.v729.Bedrock_v729;
|
||||
import org.cloudburstmc.protocol.bedrock.netty.codec.packet.BedrockPacketCodec;
|
||||
import org.geysermc.geyser.session.GeyserSession;
|
||||
import org.geysermc.mcprotocollib.protocol.codec.MinecraftCodec;
|
||||
@ -49,8 +50,8 @@ public final class GameProtocol {
|
||||
* Default Bedrock codec that should act as a fallback. Should represent the latest available
|
||||
* release of the game that Geyser supports.
|
||||
*/
|
||||
public static final BedrockCodec DEFAULT_BEDROCK_CODEC = CodecProcessor.processCodec(Bedrock_v712.CODEC.toBuilder()
|
||||
.minecraftVersion("1.21.20/1.21.21")
|
||||
public static final BedrockCodec DEFAULT_BEDROCK_CODEC = CodecProcessor.processCodec(Bedrock_v729.CODEC.toBuilder()
|
||||
.minecraftVersion("1.21.31")
|
||||
.build());
|
||||
|
||||
/**
|
||||
@ -74,7 +75,12 @@ public final class GameProtocol {
|
||||
SUPPORTED_BEDROCK_CODECS.add(CodecProcessor.processCodec(Bedrock_v686.CODEC.toBuilder()
|
||||
.minecraftVersion("1.21.2/1.21.3")
|
||||
.build()));
|
||||
SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC);
|
||||
SUPPORTED_BEDROCK_CODECS.add(CodecProcessor.processCodec(Bedrock_v712.CODEC.toBuilder()
|
||||
.minecraftVersion("1.21.20 - 1.21.23")
|
||||
.build()));
|
||||
SUPPORTED_BEDROCK_CODECS.add(DEFAULT_BEDROCK_CODEC.toBuilder()
|
||||
.minecraftVersion("1.21.30/1.21.31")
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -27,11 +27,28 @@ package org.geysermc.geyser.network.netty;
|
||||
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
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.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.Attribute;
|
||||
import io.netty.util.concurrent.DefaultThreadFactory;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.annotation.Nullable;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.geysermc.api.connection.Connection;
|
||||
import org.geysermc.geyser.GeyserImpl;
|
||||
@ -40,17 +57,15 @@ import org.geysermc.geyser.session.GeyserSession;
|
||||
import org.geysermc.mcprotocollib.network.BuiltinFlags;
|
||||
import org.geysermc.mcprotocollib.network.codec.PacketCodecHelper;
|
||||
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.TcpPacketCompression;
|
||||
import org.geysermc.mcprotocollib.network.tcp.TcpPacketEncryptor;
|
||||
import org.geysermc.mcprotocollib.network.tcp.TcpPacketSizer;
|
||||
import org.geysermc.mcprotocollib.network.tcp.TcpSession;
|
||||
import org.geysermc.mcprotocollib.protocol.codec.MinecraftCodecHelper;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Manages a Minecraft Java session over our LocalChannel implementations.
|
||||
*/
|
||||
@ -81,49 +96,58 @@ public final class LocalSession extends TcpSession {
|
||||
if (DEFAULT_EVENT_LOOP_GROUP == null) {
|
||||
DEFAULT_EVENT_LOOP_GROUP = new DefaultEventLoopGroup(new DefaultThreadFactory(this.getClass(), true));
|
||||
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();
|
||||
bootstrap.channel(LocalChannelWithRemoteAddress.class);
|
||||
bootstrap.handler(new ChannelInitializer<LocalChannelWithRemoteAddress>() {
|
||||
@Override
|
||||
public void initChannel(@NonNull LocalChannelWithRemoteAddress channel) {
|
||||
channel.spoofedRemoteAddress(new InetSocketAddress(clientIp, 0));
|
||||
PacketProtocol protocol = getPacketProtocol();
|
||||
protocol.newClientSession(LocalSession.this, transferring);
|
||||
final Bootstrap bootstrap = new Bootstrap();
|
||||
bootstrap.channel(LocalChannelWithRemoteAddress.class);
|
||||
bootstrap.handler(new ChannelInitializer<LocalChannelWithRemoteAddress>() {
|
||||
@Override
|
||||
public void initChannel(@NonNull LocalChannelWithRemoteAddress channel) {
|
||||
channel.spoofedRemoteAddress(new InetSocketAddress(clientIp, 0));
|
||||
PacketProtocol protocol = getPacketProtocol();
|
||||
protocol.newClientSession(LocalSession.this, transferring);
|
||||
|
||||
refreshReadTimeoutHandler(channel);
|
||||
refreshWriteTimeoutHandler(channel);
|
||||
ChannelPipeline pipeline = channel.pipeline();
|
||||
|
||||
ChannelPipeline pipeline = channel.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);
|
||||
|
||||
addHAProxySupport(pipeline);
|
||||
pipeline.addLast("read-timeout", new ReadTimeoutHandler(getFlag(BuiltinFlags.READ_TIMEOUT, 30)));
|
||||
pipeline.addLast("write-timeout", new WriteTimeoutHandler(getFlag(BuiltinFlags.WRITE_TIMEOUT, 0)));
|
||||
|
||||
if (GeyserImpl.getInstance().getFloodgateProvider() instanceof IntegratedFloodgateProvider) {
|
||||
Attribute<Connection> attribute = channel.attr(IntegratedFloodgateProvider.SESSION_KEY);
|
||||
attribute.set(session);
|
||||
}
|
||||
pipeline.addLast("encryption", new TcpPacketEncryptor());
|
||||
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);
|
||||
|
||||
if (GeyserImpl.getInstance().getFloodgateProvider() instanceof IntegratedFloodgateProvider) {
|
||||
Attribute<Connection> attribute = channel.attr(IntegratedFloodgateProvider.SESSION_KEY);
|
||||
attribute.set(session);
|
||||
}
|
||||
}).group(DEFAULT_EVENT_LOOP_GROUP).option(ChannelOption.CONNECT_TIMEOUT_MILLIS, getConnectTimeout() * 1000);
|
||||
}
|
||||
}).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);
|
||||
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 (!future.isSuccess()) {
|
||||
exceptionCaught(null, future.cause());
|
||||
}
|
||||
});
|
||||
} catch (Throwable t) {
|
||||
exceptionCaught(null, t);
|
||||
if (wait) {
|
||||
handleFuture.join();
|
||||
}
|
||||
}
|
||||
|
||||
@ -135,7 +159,7 @@ public final class LocalSession extends TcpSession {
|
||||
// TODO duplicate code
|
||||
private void addHAProxySupport(ChannelPipeline pipeline) {
|
||||
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() {
|
||||
@Override
|
||||
public void channelActive(@NonNull ChannelHandlerContext ctx) throws Exception {
|
||||
@ -147,9 +171,9 @@ public final class LocalSession extends TcpSession {
|
||||
remoteAddress = new InetSocketAddress(host, port);
|
||||
}
|
||||
ctx.channel().writeAndFlush(new HAProxyMessage(
|
||||
HAProxyProtocolVersion.V2, HAProxyCommand.PROXY, proxiedProtocol,
|
||||
clientAddress.getAddress().getHostAddress(), remoteAddress.getAddress().getHostAddress(),
|
||||
clientAddress.getPort(), remoteAddress.getPort()
|
||||
HAProxyProtocolVersion.V2, HAProxyCommand.PROXY, proxiedProtocol,
|
||||
clientAddress.getAddress().getHostAddress(), remoteAddress.getAddress().getHostAddress(),
|
||||
clientAddress.getPort(), remoteAddress.getPort()
|
||||
));
|
||||
ctx.pipeline().remove(this);
|
||||
ctx.pipeline().remove("proxy-protocol-encoder");
|
||||
@ -158,7 +182,7 @@ public final class LocalSession extends TcpSession {
|
||||
});
|
||||
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.
|
||||
|
@ -31,6 +31,7 @@ import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.level.Clien
|
||||
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.level.ClientboundLightUpdatePacket;
|
||||
import io.netty.channel.EventLoop;
|
||||
import org.geysermc.geyser.GeyserImpl;
|
||||
import org.geysermc.geyser.erosion.ErosionCancellationException;
|
||||
import org.geysermc.geyser.registry.loader.RegistryLoaders;
|
||||
import org.geysermc.geyser.session.GeyserSession;
|
||||
import org.geysermc.geyser.text.GeyserLocale;
|
||||
@ -87,6 +88,8 @@ public class PacketTranslatorRegistry<T> extends AbstractMappedRegistry<Class<?
|
||||
|
||||
try {
|
||||
translator.translate(session, packet);
|
||||
} catch (ErosionCancellationException ex) {
|
||||
GeyserImpl.getInstance().getLogger().debug("Caught ErosionCancellationException");
|
||||
} catch (Throwable ex) {
|
||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.network.translator.packet.failed", packet.getClass().getSimpleName()), ex);
|
||||
ex.printStackTrace();
|
||||
|
@ -34,11 +34,20 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
|
||||
import it.unimi.dsi.fastutil.ints.IntSet;
|
||||
import it.unimi.dsi.fastutil.objects.*;
|
||||
import org.cloudburstmc.nbt.*;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectMaps;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectIntPair;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
|
||||
import org.cloudburstmc.nbt.NBTInputStream;
|
||||
import org.cloudburstmc.nbt.NbtMap;
|
||||
import org.cloudburstmc.nbt.NbtMapBuilder;
|
||||
import org.cloudburstmc.nbt.NbtType;
|
||||
import org.cloudburstmc.nbt.NbtUtils;
|
||||
import org.cloudburstmc.protocol.bedrock.codec.v671.Bedrock_v671;
|
||||
import org.cloudburstmc.protocol.bedrock.codec.v685.Bedrock_v685;
|
||||
import org.cloudburstmc.protocol.bedrock.codec.v712.Bedrock_v712;
|
||||
import org.cloudburstmc.protocol.bedrock.codec.v729.Bedrock_v729;
|
||||
import org.cloudburstmc.protocol.bedrock.data.BlockPropertyData;
|
||||
import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition;
|
||||
import org.geysermc.geyser.GeyserImpl;
|
||||
@ -62,7 +71,15 @@ import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.BitSet;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
@ -110,7 +127,8 @@ public final class BlockRegistryPopulator {
|
||||
var blockMappers = ImmutableMap.<ObjectIntPair<String>, Remapper>builder()
|
||||
.put(ObjectIntPair.of("1_20_80", Bedrock_v671.CODEC.getProtocolVersion()), Conversion685_671::remapBlock)
|
||||
.put(ObjectIntPair.of("1_21_0", Bedrock_v685.CODEC.getProtocolVersion()), Conversion712_685::remapBlock)
|
||||
.put(ObjectIntPair.of("1_21_20", Bedrock_v712.CODEC.getProtocolVersion()), tag -> tag)
|
||||
.put(ObjectIntPair.of("1_21_20", Bedrock_v712.CODEC.getProtocolVersion()), Conversion729_712::remapBlock)
|
||||
.put(ObjectIntPair.of("1_21_30", Bedrock_v729.CODEC.getProtocolVersion()), tag -> tag)
|
||||
.build();
|
||||
|
||||
// We can keep this strong as nothing should be garbage collected
|
||||
|
@ -32,6 +32,8 @@ public class Conversion712_685 {
|
||||
private static final List<String> NEW_BLOCKS = Stream.of(NEW_STONE_BLOCK_SLABS_2, NEW_STONE_BLOCK_SLABS_3, NEW_STONE_BLOCK_SLABS_4, NEW_DOUBLE_STONE_BLOCK_SLABS, NEW_DOUBLE_STONE_BLOCK_SLABS_2, NEW_DOUBLE_STONE_BLOCK_SLABS_3, NEW_DOUBLE_STONE_BLOCK_SLABS_4, NEW_PRISMARINE_BLOCKS, NEW_CORAL_FAN_HANGS, NEW_CORAL_FAN_HANGS_2, NEW_CORAL_FAN_HANGS_3, NEW_MONSTER_EGGS, NEW_STONEBRICK_BLOCKS, NEW_LIGHT_BLOCKS, NEW_SANDSTONE_BLOCKS, NEW_QUARTZ_BLOCKS, NEW_RED_SANDSTONE_BLOCKS, NEW_SAND_BLOCKS, NEW_DIRT_BLOCKS, NEW_ANVILS, NEW_YELLOW_FLOWERS).flatMap(List::stream).toList();
|
||||
|
||||
static GeyserMappingItem remapItem(Item item, GeyserMappingItem mapping) {
|
||||
mapping = Conversion729_712.remapItem(item, mapping);
|
||||
|
||||
String identifer = mapping.getBedrockIdentifier();
|
||||
|
||||
if (!NEW_BLOCKS.contains(identifer)) {
|
||||
@ -153,6 +155,8 @@ public class Conversion712_685 {
|
||||
}
|
||||
|
||||
static NbtMap remapBlock(NbtMap tag) {
|
||||
tag = Conversion729_712.remapBlock(tag);
|
||||
|
||||
final String name = tag.getString("name");
|
||||
|
||||
if (!NEW_BLOCKS.contains(name)) {
|
||||
|
@ -0,0 +1,151 @@
|
||||
package org.geysermc.geyser.registry.populator;
|
||||
|
||||
import org.cloudburstmc.nbt.NbtMap;
|
||||
import org.geysermc.geyser.item.type.Item;
|
||||
import org.geysermc.geyser.registry.type.GeyserMappingItem;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class Conversion729_712 {
|
||||
private static final List<String> NEW_PURPUR_BLOCKS = List.of("minecraft:purpur_block", "minecraft:purpur_pillar");
|
||||
private static final List<String> NEW_WALL_BLOCKS = List.of("minecraft:cobblestone_wall", "minecraft:mossy_cobblestone_wall", "minecraft:granite_wall", "minecraft:diorite_wall", "minecraft:andesite_wall", "minecraft:sandstone_wall", "minecraft:brick_wall", "minecraft:stone_brick_wall", "minecraft:mossy_stone_brick_wall", "minecraft:nether_brick_wall", "minecraft:end_stone_brick_wall", "minecraft:prismarine_wall", "minecraft:red_sandstone_wall", "minecraft:red_nether_brick_wall");
|
||||
private static final List<String> NEW_SPONGE_BLOCKS = List.of("minecraft:sponge", "minecraft:wet_sponge");
|
||||
private static final List<String> NEW_TNT_BLOCKS = List.of("minecraft:tnt", "minecraft:underwater_tnt");
|
||||
private static final List<String> STRUCTURE_VOID = List.of("minecraft:structure_void");
|
||||
private static final List<String> NEW_BLOCKS = Stream.of(NEW_PURPUR_BLOCKS, NEW_WALL_BLOCKS, NEW_SPONGE_BLOCKS, NEW_TNT_BLOCKS, STRUCTURE_VOID).flatMap(List::stream).toList();
|
||||
|
||||
static GeyserMappingItem remapItem(Item item, GeyserMappingItem mapping) {
|
||||
String identifier = mapping.getBedrockIdentifier();
|
||||
|
||||
if (!NEW_BLOCKS.contains(identifier)) {
|
||||
return mapping;
|
||||
}
|
||||
|
||||
if (identifier.equals("minecraft:underwater_tnt")) {
|
||||
return mapping.withBedrockIdentifier("minecraft:tnt").withBedrockData(1);
|
||||
}
|
||||
|
||||
if (NEW_PURPUR_BLOCKS.contains(identifier)) {
|
||||
switch (identifier) {
|
||||
case "minecraft:purpur_block" -> { return mapping.withBedrockIdentifier("minecraft:purpur_block").withBedrockData(0); }
|
||||
case "minecraft:purpur_pillar" -> { return mapping.withBedrockIdentifier("minecraft:purpur_block").withBedrockData(1); }
|
||||
}
|
||||
}
|
||||
|
||||
if (NEW_WALL_BLOCKS.contains(identifier)) {
|
||||
switch (identifier) {
|
||||
case "minecraft:cobblestone_wall" -> { return mapping.withBedrockIdentifier("minecraft:cobblestone_wall").withBedrockData(0); }
|
||||
case "minecraft:mossy_cobblestone_wall" -> { return mapping.withBedrockIdentifier("minecraft:cobblestone_wall").withBedrockData(1); }
|
||||
case "minecraft:granite_wall" -> { return mapping.withBedrockIdentifier("minecraft:cobblestone_wall").withBedrockData(2); }
|
||||
case "minecraft:diorite_wall" -> { return mapping.withBedrockIdentifier("minecraft:cobblestone_wall").withBedrockData(3); }
|
||||
case "minecraft:andesite_wall" -> { return mapping.withBedrockIdentifier("minecraft:cobblestone_wall").withBedrockData(4); }
|
||||
case "minecraft:sandstone_wall" -> { return mapping.withBedrockIdentifier("minecraft:cobblestone_wall").withBedrockData(5); }
|
||||
case "minecraft:brick_wall" -> { return mapping.withBedrockIdentifier("minecraft:cobblestone_wall").withBedrockData(6); }
|
||||
case "minecraft:stone_brick_wall" -> { return mapping.withBedrockIdentifier("minecraft:cobblestone_wall").withBedrockData(7); }
|
||||
case "minecraft:mossy_stone_brick_wall" -> { return mapping.withBedrockIdentifier("minecraft:cobblestone_wall").withBedrockData(8); }
|
||||
case "minecraft:nether_brick_wall" -> { return mapping.withBedrockIdentifier("minecraft:cobblestone_wall").withBedrockData(9); }
|
||||
case "minecraft:end_stone_brick_wall" -> { return mapping.withBedrockIdentifier("minecraft:cobblestone_wall").withBedrockData(10); }
|
||||
case "minecraft:prismarine_wall" -> { return mapping.withBedrockIdentifier("minecraft:cobblestone_wall").withBedrockData(11); }
|
||||
case "minecraft:red_sandstone_wall" -> { return mapping.withBedrockIdentifier("minecraft:cobblestone_wall").withBedrockData(12); }
|
||||
case "minecraft:red_nether_brick_wall" -> { return mapping.withBedrockIdentifier("minecraft:cobblestone_wall").withBedrockData(13); }
|
||||
}
|
||||
}
|
||||
|
||||
if (NEW_SPONGE_BLOCKS.contains(identifier)) {
|
||||
switch (identifier) {
|
||||
case "minecraft:sponge" -> { return mapping.withBedrockIdentifier("minecraft:sponge").withBedrockData(0); }
|
||||
case "minecraft:wet_sponge" -> { return mapping.withBedrockIdentifier("minecraft:sponge").withBedrockData(1); }
|
||||
}
|
||||
}
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
static NbtMap remapBlock(NbtMap tag) {
|
||||
final String name = tag.getString("name");
|
||||
|
||||
if (!NEW_BLOCKS.contains(name)) {
|
||||
return tag;
|
||||
}
|
||||
|
||||
String replacement;
|
||||
|
||||
if (NEW_PURPUR_BLOCKS.contains(name)) {
|
||||
replacement = "minecraft:purpur_block";
|
||||
String purpurType = name.equals("minecraft:purpur_pillar") ? "lines" : "default";
|
||||
|
||||
NbtMap states = tag.getCompound("states")
|
||||
.toBuilder()
|
||||
.putString("chisel_type", purpurType)
|
||||
.build();
|
||||
|
||||
return tag.toBuilder().putString("name", replacement).putCompound("states", states).build();
|
||||
}
|
||||
|
||||
if (NEW_WALL_BLOCKS.contains(name)) {
|
||||
replacement = "minecraft:cobblestone_wall";
|
||||
String wallType;
|
||||
|
||||
switch (name) {
|
||||
case "minecraft:cobblestone_wall" -> wallType = "cobblestone";
|
||||
case "minecraft:mossy_cobblestone_wall" -> wallType = "mossy_cobblestone";
|
||||
case "minecraft:granite_wall" -> wallType = "granite";
|
||||
case "minecraft:diorite_wall" -> wallType = "diorite";
|
||||
case "minecraft:andesite_wall" -> wallType = "andesite";
|
||||
case "minecraft:sandstone_wall" -> wallType = "sandstone";
|
||||
case "minecraft:brick_wall" -> wallType = "brick";
|
||||
case "minecraft:stone_brick_wall" -> wallType = "stone_brick";
|
||||
case "minecraft:mossy_stone_brick_wall" -> wallType = "mossy_stone_brick";
|
||||
case "minecraft:nether_brick_wall" -> wallType = "nether_brick";
|
||||
case "minecraft:end_stone_brick_wall" -> wallType = "end_brick";
|
||||
case "minecraft:prismarine_wall" -> wallType = "prismarine";
|
||||
case "minecraft:red_sandstone_wall" -> wallType = "red_sandstone";
|
||||
case "minecraft:red_nether_brick_wall" -> wallType = "red_nether_brick";
|
||||
default -> throw new IllegalStateException("Unexpected value: " + name);
|
||||
}
|
||||
|
||||
NbtMap states = tag.getCompound("states")
|
||||
.toBuilder()
|
||||
.putString("wall_block_type", wallType)
|
||||
.build();
|
||||
|
||||
return tag.toBuilder().putString("name", replacement).putCompound("states", states).build();
|
||||
}
|
||||
|
||||
if (NEW_SPONGE_BLOCKS.contains(name)) {
|
||||
replacement = "minecraft:sponge";
|
||||
String spongeType = name.equals("minecraft:wet_sponge") ? "wet" : "dry";
|
||||
|
||||
NbtMap states = tag.getCompound("states")
|
||||
.toBuilder()
|
||||
.putString("sponge_type", spongeType)
|
||||
.build();
|
||||
|
||||
return tag.toBuilder().putString("name", replacement).putCompound("states", states).build();
|
||||
}
|
||||
|
||||
if (NEW_TNT_BLOCKS.contains(name)) {
|
||||
replacement = "minecraft:tnt";
|
||||
byte tntType = (byte) (name.equals("minecraft:underwater_tnt") ? 1 : 0);
|
||||
|
||||
NbtMap states = tag.getCompound("states")
|
||||
.toBuilder()
|
||||
.putByte("allow_underwater_bit", tntType)
|
||||
.build();
|
||||
|
||||
return tag.toBuilder().putString("name", replacement).putCompound("states", states).build();
|
||||
}
|
||||
|
||||
if (STRUCTURE_VOID.contains(name)) {
|
||||
NbtMap states = tag.getCompound("states")
|
||||
.toBuilder()
|
||||
.putString("structure_void_type", "air")
|
||||
.build();
|
||||
|
||||
return tag.toBuilder().putCompound("states", states).build();
|
||||
}
|
||||
|
||||
return tag;
|
||||
}
|
||||
}
|
@ -42,6 +42,7 @@ import org.cloudburstmc.nbt.NbtUtils;
|
||||
import org.cloudburstmc.protocol.bedrock.codec.v671.Bedrock_v671;
|
||||
import org.cloudburstmc.protocol.bedrock.codec.v685.Bedrock_v685;
|
||||
import org.cloudburstmc.protocol.bedrock.codec.v712.Bedrock_v712;
|
||||
import org.cloudburstmc.protocol.bedrock.codec.v729.Bedrock_v729;
|
||||
import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition;
|
||||
import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition;
|
||||
import org.cloudburstmc.protocol.bedrock.data.definitions.SimpleItemDefinition;
|
||||
@ -92,7 +93,8 @@ public class ItemRegistryPopulator {
|
||||
List<PaletteVersion> paletteVersions = new ArrayList<>(3);
|
||||
paletteVersions.add(new PaletteVersion("1_20_80", Bedrock_v671.CODEC.getProtocolVersion(), Collections.emptyMap(), Conversion685_671::remapItem));
|
||||
paletteVersions.add(new PaletteVersion("1_21_0", Bedrock_v685.CODEC.getProtocolVersion(), Collections.emptyMap(), Conversion712_685::remapItem));
|
||||
paletteVersions.add(new PaletteVersion("1_21_20", Bedrock_v712.CODEC.getProtocolVersion()));
|
||||
paletteVersions.add(new PaletteVersion("1_21_20", Bedrock_v712.CODEC.getProtocolVersion(), Collections.emptyMap(), Conversion729_712::remapItem));
|
||||
paletteVersions.add(new PaletteVersion("1_21_30", Bedrock_v729.CODEC.getProtocolVersion()));
|
||||
|
||||
GeyserBootstrap bootstrap = GeyserImpl.getInstance().getBootstrap();
|
||||
|
||||
|
@ -25,185 +25,100 @@
|
||||
|
||||
package org.geysermc.geyser.scoreboard;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
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.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
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
|
||||
public final class Objective {
|
||||
private final Scoreboard scoreboard;
|
||||
private final long id;
|
||||
private boolean active = true;
|
||||
private final List<DisplaySlot> activeSlots = new ArrayList<>();
|
||||
|
||||
@Setter
|
||||
private UpdateType updateType = UpdateType.ADD;
|
||||
private final String objectiveName;
|
||||
private final Map<String, ScoreReference> scores = new ConcurrentHashMap<>();
|
||||
|
||||
private String objectiveName;
|
||||
private ScoreboardPosition displaySlot;
|
||||
private String displaySlotName;
|
||||
private String displayName = "unknown";
|
||||
private String displayName;
|
||||
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) {
|
||||
this(scoreboard);
|
||||
this.scoreboard = scoreboard;
|
||||
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) {
|
||||
if (!scores.containsKey(id)) {
|
||||
long scoreId = scoreboard.getNextId().getAndIncrement();
|
||||
Score scoreObject = new Score(scoreId, id)
|
||||
.setScore(score)
|
||||
.setTeam(scoreboard.getTeamFor(id))
|
||||
.setDisplayName(displayName)
|
||||
.setNumberFormat(numberFormat)
|
||||
.setUpdateType(UpdateType.ADD);
|
||||
scores.put(id, scoreObject);
|
||||
if (scores.containsKey(id)) {
|
||||
return;
|
||||
}
|
||||
var reference = new ScoreReference(scoreboard, id, score, displayName, numberFormat);
|
||||
scores.put(id, reference);
|
||||
|
||||
for (var slot : activeSlots) {
|
||||
slot.addScore(reference);
|
||||
}
|
||||
}
|
||||
|
||||
public void setScore(String id, int score, Component displayName, NumberFormat numberFormat) {
|
||||
Score stored = scores.get(id);
|
||||
ScoreReference stored = scores.get(id);
|
||||
if (stored != null) {
|
||||
stored.setScore(score)
|
||||
.setDisplayName(displayName)
|
||||
.setNumberFormat(numberFormat)
|
||||
.setUpdateType(UpdateType.UPDATE);
|
||||
stored.updateProperties(scoreboard, score, displayName, numberFormat);
|
||||
return;
|
||||
}
|
||||
registerScore(id, score, displayName, numberFormat);
|
||||
}
|
||||
|
||||
public void removeScore(String id) {
|
||||
Score stored = scores.get(id);
|
||||
ScoreReference stored = scores.remove(id);
|
||||
if (stored != null) {
|
||||
stored.setUpdateType(UpdateType.REMOVE);
|
||||
stored.markDeleted();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used internally to remove a score from the score map
|
||||
*/
|
||||
public void removeScore0(String id) {
|
||||
scores.remove(id);
|
||||
}
|
||||
public void updateProperties(Component displayNameComponent, ScoreType type, NumberFormat format) {
|
||||
String displayName = MessageTranslator.convertMessageRaw(displayNameComponent, scoreboard.session().locale());
|
||||
boolean changed = !Objects.equals(this.displayName, displayName) || this.type != type;
|
||||
|
||||
public Objective setDisplayName(String displayName) {
|
||||
this.displayName = displayName;
|
||||
if (updateType == UpdateType.NOTHING) {
|
||||
updateType = UpdateType.UPDATE;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
this.type = type;
|
||||
|
||||
public Objective setNumberFormat(NumberFormat numberFormat) {
|
||||
if (Objects.equals(this.numberFormat, numberFormat)) {
|
||||
return this;
|
||||
}
|
||||
|
||||
this.numberFormat = numberFormat;
|
||||
if (updateType == UpdateType.NOTHING) {
|
||||
updateType = UpdateType.UPDATE;
|
||||
}
|
||||
|
||||
// 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);
|
||||
if (!Objects.equals(this.numberFormat, format)) {
|
||||
this.numberFormat = format;
|
||||
// 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
|
||||
if (!changed) {
|
||||
for (ScoreReference score : scores.values()) {
|
||||
if (score.numberFormat() == null) {
|
||||
score.markChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
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);
|
||||
if (changed) {
|
||||
for (DisplaySlot slot : activeSlots) {
|
||||
slot.markNeedsUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The objective will be removed on the next update
|
||||
*/
|
||||
public void pendingRemove() {
|
||||
updateType = UpdateType.REMOVE;
|
||||
public boolean hasDisplaySlot() {
|
||||
return !activeSlots.isEmpty();
|
||||
}
|
||||
|
||||
public @Nullable TeamColor getTeamColor() {
|
||||
return switch (displaySlot) {
|
||||
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 addDisplaySlot(DisplaySlot slot) {
|
||||
activeSlots.add(slot);
|
||||
}
|
||||
|
||||
public void removed() {
|
||||
active = false;
|
||||
updateType = UpdateType.REMOVE;
|
||||
scores = null;
|
||||
public void removeDisplaySlot(DisplaySlot slot) {
|
||||
activeSlots.remove(slot);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
132
core/src/main/java/org/geysermc/geyser/scoreboard/ScoreReference.java
Normale Datei
132
core/src/main/java/org/geysermc/geyser/scoreboard/ScoreReference.java
Normale Datei
@ -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;
|
||||
}
|
||||
}
|
@ -25,43 +25,72 @@
|
||||
|
||||
package org.geysermc.geyser.scoreboard;
|
||||
|
||||
import static org.geysermc.geyser.scoreboard.UpdateType.REMOVE;
|
||||
|
||||
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 net.kyori.adventure.text.Component;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
|
||||
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.geysermc.geyser.GeyserImpl;
|
||||
import org.geysermc.geyser.GeyserLogger;
|
||||
import org.geysermc.geyser.entity.type.Entity;
|
||||
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.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.TeamColor;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.geysermc.geyser.scoreboard.UpdateType.*;
|
||||
|
||||
/**
|
||||
* Here follows some information about how scoreboards work in Java Edition, that is related to the workings of this
|
||||
* class:
|
||||
* <p>
|
||||
* 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.
|
||||
* 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 {
|
||||
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 final GeyserSession session;
|
||||
private final GeyserLogger logger;
|
||||
@Getter
|
||||
private final AtomicLong nextId = new AtomicLong(0);
|
||||
|
||||
private final Map<String, Objective> objectives = new ConcurrentHashMap<>();
|
||||
@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
|
||||
/**
|
||||
* Required to preserve vanilla behavior, which also uses a map.
|
||||
@ -71,6 +100,7 @@ public final class Scoreboard {
|
||||
@Getter
|
||||
private final Map<String, Team> playerToTeam = new Object2ObjectOpenHashMap<>();
|
||||
|
||||
private final AtomicBoolean updateLockActive = new AtomicBoolean(false);
|
||||
private int lastAddScoreCount = 0;
|
||||
private int lastRemoveScoreCount = 0;
|
||||
|
||||
@ -80,24 +110,22 @@ public final class Scoreboard {
|
||||
}
|
||||
|
||||
public void removeScoreboard() {
|
||||
Iterator<Objective> iterator = objectives.values().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
Objective objective = iterator.next();
|
||||
iterator.remove();
|
||||
var copy = new HashMap<>(objectiveSlots);
|
||||
objectiveSlots.clear();
|
||||
|
||||
deleteObjective(objective, false);
|
||||
for (DisplaySlot slot : copy.values()) {
|
||||
slot.remove();
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable Objective registerNewObjective(String objectiveId) {
|
||||
Objective objective = objectives.get(objectiveId);
|
||||
if (objective != null) {
|
||||
// we have no other choice, or we have to make a new map?
|
||||
// if the objective hasn't been deleted, we have to force it
|
||||
if (objective.getUpdateType() != REMOVE) {
|
||||
return null;
|
||||
// matches vanilla behaviour
|
||||
if (SHOW_SCOREBOARD_LOGS) {
|
||||
logger.warning("An objective with the same name '" + objectiveId + "' already exists! Ignoring new objective!");
|
||||
}
|
||||
deleteObjective(objective, true);
|
||||
return null;
|
||||
}
|
||||
|
||||
objective = new Objective(this, objectiveId);
|
||||
@ -105,273 +133,162 @@ public final class Scoreboard {
|
||||
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);
|
||||
if (objective == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!objective.isActive()) {
|
||||
objective.setActive(displaySlot);
|
||||
// for reactivated objectives
|
||||
objective.setUpdateType(ADD);
|
||||
var display = objectiveSlots.get(slot);
|
||||
if (display != null && display.objective() != objective) {
|
||||
removedSlots.add(display);
|
||||
}
|
||||
|
||||
Objective storedObjective = objectiveSlots.get(displaySlot);
|
||||
if (storedObjective != null && storedObjective != objective) {
|
||||
storedObjective.pendingRemove();
|
||||
}
|
||||
objectiveSlots.put(displaySlot, objective);
|
||||
|
||||
if (displaySlot == ScoreboardPosition.BELOW_NAME) {
|
||||
// Display the below name score option to all players
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
display = switch (DisplaySlot.slotCategory(slot)) {
|
||||
case SIDEBAR -> new SidebarDisplaySlot(session, objective, slot);
|
||||
case BELOW_NAME -> new BelownameDisplaySlot(session, objective);
|
||||
case PLAYER_LIST -> new PlayerlistDisplaySlot(session, objective);
|
||||
default -> throw new IllegalStateException("Unexpected value: " + slot);
|
||||
};
|
||||
objectiveSlots.put(slot, display);
|
||||
objective.addDisplaySlot(display);
|
||||
}
|
||||
|
||||
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);
|
||||
if (team != null) {
|
||||
if (SHOW_SCOREBOARD_LOGS) {
|
||||
logger.info(GeyserLocale.getLocaleStringLog("geyser.network.translator.team.failed_overrides", teamName));
|
||||
}
|
||||
return team;
|
||||
return;
|
||||
}
|
||||
|
||||
team = new Team(this, teamName);
|
||||
team.addEntities(players);
|
||||
team = new Team(this, teamName, players, name, prefix, suffix, visibility, color);
|
||||
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)
|
||||
if (ADD_TEAM_SUGGESTIONS) {
|
||||
session.addCommandEnum("Geyser_Teams", team.getId());
|
||||
session.addCommandEnum("Geyser_Teams", team.id());
|
||||
}
|
||||
return team;
|
||||
}
|
||||
|
||||
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> removeScores = new ArrayList<>(lastRemoveScoreCount);
|
||||
List<Objective> removedObjectives = new ArrayList<>();
|
||||
|
||||
Team playerTeam = getTeamFor(session.getPlayerEntity().getUsername());
|
||||
Objective correctSidebar = null;
|
||||
DisplaySlot correctSidebarSlot = null;
|
||||
|
||||
for (Objective objective : objectives.values()) {
|
||||
// objective has been deleted
|
||||
if (objective.getUpdateType() == REMOVE) {
|
||||
removedObjectives.add(objective);
|
||||
for (DisplaySlot slot : objectiveSlots.values()) {
|
||||
// slot has been removed
|
||||
if (slot.updateType() == REMOVE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// there's nothing we can do with inactive objectives
|
||||
// after checking if the objective has been deleted,
|
||||
// except waiting for the objective to become activated (:
|
||||
if (!objective.isActive()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (playerTeam != null && playerTeam.getColor() == objective.getTeamColor()) {
|
||||
correctSidebar = objective;
|
||||
if (playerTeam != null && playerTeam.color() == slot.teamColor()) {
|
||||
correctSidebarSlot = slot;
|
||||
}
|
||||
}
|
||||
|
||||
if (correctSidebar == null) {
|
||||
correctSidebar = objectiveSlots.get(ScoreboardPosition.SIDEBAR);
|
||||
if (correctSidebarSlot == null) {
|
||||
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
|
||||
// scoreboard is removed, the client can crash
|
||||
deleteObjective(objective, true);
|
||||
slot.remove();
|
||||
}
|
||||
removedSlots.removeAll(actualRemovedSlots);
|
||||
|
||||
handleObjective(objectiveSlots.get(ScoreboardPosition.PLAYER_LIST), addScores, removeScores);
|
||||
handleObjective(correctSidebar, addScores, removeScores);
|
||||
handleObjective(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();
|
||||
}
|
||||
}
|
||||
handleDisplaySlot(objectiveSlots.get(ScoreboardPosition.PLAYER_LIST), addScores, removeScores);
|
||||
handleDisplaySlot(correctSidebarSlot, addScores, removeScores);
|
||||
handleDisplaySlot(objectiveSlots.get(ScoreboardPosition.BELOW_NAME), addScores, removeScores);
|
||||
|
||||
if (!removeScores.isEmpty()) {
|
||||
SetScorePacket setScorePacket = new SetScorePacket();
|
||||
setScorePacket.setAction(SetScorePacket.Action.REMOVE);
|
||||
setScorePacket.setInfos(removeScores);
|
||||
session.sendUpstreamPacket(setScorePacket);
|
||||
SetScorePacket packet = new SetScorePacket();
|
||||
packet.setAction(SetScorePacket.Action.REMOVE);
|
||||
packet.setInfos(removeScores);
|
||||
session.sendUpstreamPacket(packet);
|
||||
}
|
||||
|
||||
if (!addScores.isEmpty()) {
|
||||
SetScorePacket setScorePacket = new SetScorePacket();
|
||||
setScorePacket.setAction(SetScorePacket.Action.SET);
|
||||
setScorePacket.setInfos(addScores);
|
||||
session.sendUpstreamPacket(setScorePacket);
|
||||
SetScorePacket packet = new SetScorePacket();
|
||||
packet.setAction(SetScorePacket.Action.SET);
|
||||
packet.setInfos(addScores);
|
||||
session.sendUpstreamPacket(packet);
|
||||
}
|
||||
|
||||
lastAddScoreCount = addScores.size();
|
||||
lastRemoveScoreCount = removeScores.size();
|
||||
updateLockActive.set(false);
|
||||
}
|
||||
|
||||
private void handleObjective(Objective objective, List<ScoreInfo> addScores, List<ScoreInfo> removeScores) {
|
||||
if (objective == null || objective.getUpdateType() == REMOVE) {
|
||||
return;
|
||||
private void handleDisplaySlot(DisplaySlot slot, List<ScoreInfo> addScores, List<ScoreInfo> removeScores) {
|
||||
if (slot != null) {
|
||||
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) {
|
||||
return objectives.get(objectiveName);
|
||||
}
|
||||
|
||||
public Collection<Objective> getObjectives() {
|
||||
return objectives.values();
|
||||
}
|
||||
|
||||
public void unregisterObjective(String objectiveName) {
|
||||
Objective objective = getObjective(objectiveName);
|
||||
if (objective != null) {
|
||||
objective.pendingRemove();
|
||||
public void removeObjective(Objective objective) {
|
||||
objectives.remove(objective.getObjectiveName());
|
||||
for (DisplaySlot slot : objective.getActiveSlots()) {
|
||||
objectiveSlots.remove(slot.position(), slot);
|
||||
removedSlots.add(slot);
|
||||
}
|
||||
}
|
||||
|
||||
public Objective getSlot(ScoreboardPosition slot) {
|
||||
return objectiveSlots.get(slot);
|
||||
public void resetPlayerScores(String playerNameOrEntityUuid) {
|
||||
for (Objective objective : objectives.values()) {
|
||||
objective.removeScore(playerNameOrEntityUuid);
|
||||
}
|
||||
}
|
||||
|
||||
public Team getTeam(String teamName) {
|
||||
return teams.get(teamName);
|
||||
}
|
||||
|
||||
public Team getTeamFor(String entity) {
|
||||
return playerToTeam.get(entity);
|
||||
public Team getTeamFor(String playerNameOrEntityUuid) {
|
||||
return playerToTeam.get(playerNameOrEntityUuid);
|
||||
}
|
||||
|
||||
public void removeTeam(String teamName) {
|
||||
Team remove = teams.remove(teamName);
|
||||
if (remove != null) {
|
||||
remove.setUpdateType(REMOVE);
|
||||
// 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());
|
||||
if (remove == null) {
|
||||
return;
|
||||
}
|
||||
remove.remove();
|
||||
session.removeCommandEnum("Geyser_Teams", remove.id());
|
||||
}
|
||||
|
||||
@Contract("-> new")
|
||||
@ -381,48 +298,46 @@ public final class Scoreboard {
|
||||
(o1, o2) -> o1, LinkedHashMap::new));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the display names of all entities in a given team.
|
||||
* @param teamChange the players have either joined or left the team. Used for optimizations when just the display name updated.
|
||||
*/
|
||||
public void updateEntityNames(Team team, boolean teamChange) {
|
||||
Set<String> names = new HashSet<>(team.getEntities());
|
||||
updateEntityNames(team, names, teamChange);
|
||||
public void playerRegistered(PlayerEntity player) {
|
||||
for (DisplaySlot slot : objectiveSlots.values()) {
|
||||
slot.playerRegistered(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
|
||||
* from a team.
|
||||
*/
|
||||
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();
|
||||
public void playerRemoved(PlayerEntity player) {
|
||||
for (DisplaySlot slot : objectiveSlots.values()) {
|
||||
slot.playerRemoved(player);
|
||||
}
|
||||
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
|
||||
if (entity instanceof PlayerEntity player && names.remove(player.getUsername())) {
|
||||
player.updateDisplayName(team);
|
||||
player.updateBedrockMetadata();
|
||||
if (names.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void entityRegistered(Entity entity) {
|
||||
var team = getTeamFor(entity.teamIdentifier());
|
||||
if (team != null) {
|
||||
team.onEntitySpawn(entity);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the team's player was refreshed, then we need to go through every entity and check...
|
||||
*/
|
||||
private void refreshSessionPlayerDisplays() {
|
||||
for (Entity entity : session.getEntityCache().getEntities().values()) {
|
||||
if (entity instanceof PlayerEntity player) {
|
||||
Team playerTeam = session.getWorldCache().getScoreboard().getTeamFor(player.getUsername());
|
||||
player.updateDisplayName(playerTeam);
|
||||
player.updateBedrockMetadata();
|
||||
}
|
||||
}
|
||||
public long nextId() {
|
||||
return nextId.getAndIncrement();
|
||||
}
|
||||
|
||||
public GeyserSession session() {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
@ -173,7 +173,6 @@ public final class ScoreboardUpdater extends Thread {
|
||||
@Getter
|
||||
public static final class ScoreboardSession {
|
||||
private final GeyserSession session;
|
||||
@SuppressWarnings("WriteOnlyObject")
|
||||
private final AtomicInteger pendingPacketsPerSecond = new AtomicInteger(0);
|
||||
private int packetsPerSecond;
|
||||
private long lastUpdate;
|
||||
|
@ -25,48 +25,66 @@
|
||||
|
||||
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 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.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 static final long LAST_UPDATE_DEFAULT = -1;
|
||||
private static final long LAST_UPDATE_REMOVE = -2;
|
||||
|
||||
private final Scoreboard scoreboard;
|
||||
private final String id;
|
||||
|
||||
@Getter(AccessLevel.PACKAGE)
|
||||
private final Set<String> entities;
|
||||
private final Set<Entity> managedEntities;
|
||||
@NonNull private NameTagVisibility nameTagVisibility = NameTagVisibility.ALWAYS;
|
||||
@Setter private TeamColor color;
|
||||
private TeamColor color;
|
||||
|
||||
private final TeamData currentData;
|
||||
private TeamData cachedData;
|
||||
private String name;
|
||||
private String prefix;
|
||||
private String suffix;
|
||||
private long lastUpdate;
|
||||
|
||||
private boolean updating;
|
||||
|
||||
public Team(Scoreboard scoreboard, String id) {
|
||||
public Team(
|
||||
Scoreboard scoreboard,
|
||||
String id,
|
||||
String[] players,
|
||||
Component name,
|
||||
Component prefix,
|
||||
Component suffix,
|
||||
NameTagVisibility visibility,
|
||||
TeamColor color
|
||||
) {
|
||||
this.scoreboard = scoreboard;
|
||||
this.id = id;
|
||||
currentData = new TeamData();
|
||||
entities = new ObjectOpenHashSet<>();
|
||||
this.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<>();
|
||||
for (String name : names) {
|
||||
if (entities.add(name)) {
|
||||
added.add(name);
|
||||
// go to next score if score is already present
|
||||
if (!entities.add(name)) {
|
||||
continue;
|
||||
}
|
||||
added.add(name);
|
||||
scoreboard.getPlayerToTeam().compute(name, (player, oldTeam) -> {
|
||||
if (oldTeam != null) {
|
||||
// Remove old team from this map, and from the set of players of the old team.
|
||||
@ -78,26 +96,15 @@ public final class Team {
|
||||
}
|
||||
|
||||
if (added.isEmpty()) {
|
||||
return added;
|
||||
return;
|
||||
}
|
||||
// we don't have to change the updateType,
|
||||
// because the scores itself need updating, not the team
|
||||
for (Objective objective : scoreboard.getObjectives()) {
|
||||
for (String addedEntity : added) {
|
||||
Score score = objective.getScores().get(addedEntity);
|
||||
if (score != null) {
|
||||
score.setTeam(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return added;
|
||||
// we don't have to change our updateType,
|
||||
// because the scores themselves need updating, not the team
|
||||
scoreboard.setTeamFor(this, added);
|
||||
addAddedEntities(added);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return all removed entities from this team
|
||||
*/
|
||||
public Set<String> removeEntities(String... names) {
|
||||
public void removeEntities(String... names) {
|
||||
Set<String> removed = new HashSet<>();
|
||||
for (String name : names) {
|
||||
if (entities.remove(name)) {
|
||||
@ -105,87 +112,22 @@ public final class Team {
|
||||
}
|
||||
scoreboard.getPlayerToTeam().remove(name, this);
|
||||
}
|
||||
return removed;
|
||||
removeRemovedEntities(removed);
|
||||
}
|
||||
|
||||
public boolean hasEntity(String name) {
|
||||
return entities.contains(name);
|
||||
}
|
||||
|
||||
public Team setName(String name) {
|
||||
currentData.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Team setPrefix(String prefix) {
|
||||
// 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;
|
||||
public String displayName(String score) {
|
||||
String chatColor = ChatColor.chatColorFor(color);
|
||||
// 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)) {
|
||||
chatColor = "";
|
||||
}
|
||||
currentData.prefix = prefix;
|
||||
return this;
|
||||
}
|
||||
|
||||
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;
|
||||
// also add reset because setting the color does not reset the formatting, unlike Java
|
||||
return chatColor + prefix + ChatColor.RESET + chatColor + score + ChatColor.RESET + chatColor + suffix;
|
||||
}
|
||||
|
||||
public boolean isVisibleFor(String entity) {
|
||||
@ -201,34 +143,178 @@ public final class Team {
|
||||
};
|
||||
}
|
||||
|
||||
public Team setNameTagVisibility(@Nullable NameTagVisibility nameTagVisibility) {
|
||||
if (nameTagVisibility != null) {
|
||||
// Null check like this (and this.nameTagVisibility defaults to ALWAYS) as of Java 1.19.4
|
||||
this.nameTagVisibility = nameTagVisibility;
|
||||
public void updateProperties(Component name, Component prefix, Component suffix, NameTagVisibility visibility, TeamColor color) {
|
||||
// this shouldn't happen but hey!
|
||||
if (lastUpdate == LAST_UPDATE_REMOVE) {
|
||||
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
|
||||
public int 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
};
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -40,6 +40,7 @@ import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.Accessors;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.raphimc.minecraftauth.responsehandler.exception.MinecraftRequestException;
|
||||
import net.raphimc.minecraftauth.step.java.StepMCProfile;
|
||||
import net.raphimc.minecraftauth.step.java.StepMCToken;
|
||||
import net.raphimc.minecraftauth.step.java.session.StepFullJavaSession;
|
||||
@ -67,12 +68,14 @@ import org.cloudburstmc.protocol.bedrock.data.ExperimentData;
|
||||
import org.cloudburstmc.protocol.bedrock.data.GamePublishSetting;
|
||||
import org.cloudburstmc.protocol.bedrock.data.GameRuleData;
|
||||
import org.cloudburstmc.protocol.bedrock.data.GameType;
|
||||
import org.cloudburstmc.protocol.bedrock.data.LevelEvent;
|
||||
import org.cloudburstmc.protocol.bedrock.data.PlayerPermission;
|
||||
import org.cloudburstmc.protocol.bedrock.data.SoundEvent;
|
||||
import org.cloudburstmc.protocol.bedrock.data.SpawnBiomeType;
|
||||
import org.cloudburstmc.protocol.bedrock.data.command.CommandEnumData;
|
||||
import org.cloudburstmc.protocol.bedrock.data.command.CommandPermission;
|
||||
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.inventory.ItemData;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.AvailableEntityIdentifiersPacket;
|
||||
@ -83,9 +86,11 @@ import org.cloudburstmc.protocol.bedrock.packet.ChunkRadiusUpdatedPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.ClientboundCloseFormPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.CraftingDataPacket;
|
||||
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.GameRulesChangedPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.ItemComponentPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEvent2Packet;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.PlayStatusPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.SetTimePacket;
|
||||
@ -131,6 +136,7 @@ import org.geysermc.geyser.entity.type.Tickable;
|
||||
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
|
||||
import org.geysermc.geyser.entity.vehicle.ClientVehicle;
|
||||
import org.geysermc.geyser.erosion.AbstractGeyserboundPacketHandler;
|
||||
import org.geysermc.geyser.erosion.ErosionCancellationException;
|
||||
import org.geysermc.geyser.erosion.GeyserboundHandshakePacketHandler;
|
||||
import org.geysermc.geyser.floodgate.FloodgateProvider;
|
||||
import org.geysermc.geyser.impl.camera.CameraDefinitions;
|
||||
@ -172,7 +178,6 @@ import org.geysermc.geyser.text.MinecraftLocale;
|
||||
import org.geysermc.geyser.translator.inventory.InventoryTranslator;
|
||||
import org.geysermc.geyser.translator.text.MessageTranslator;
|
||||
import org.geysermc.geyser.util.ChunkUtils;
|
||||
import org.geysermc.geyser.util.DimensionUtils;
|
||||
import org.geysermc.geyser.util.EntityUtils;
|
||||
import org.geysermc.geyser.util.LoginEncryptionUtils;
|
||||
import org.geysermc.geyser.util.MinecraftAuthLogger;
|
||||
@ -229,6 +234,7 @@ import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@ -261,7 +267,7 @@ public class GeyserSession extends FloodgateConnection implements GeyserConnecti
|
||||
|
||||
@NonNull
|
||||
@Setter
|
||||
private AbstractGeyserboundPacketHandler erosionHandler;
|
||||
private volatile AbstractGeyserboundPacketHandler erosionHandler;
|
||||
|
||||
@Accessors(fluent = true)
|
||||
@Setter
|
||||
@ -386,6 +392,10 @@ public class GeyserSession extends FloodgateConnection implements GeyserConnecti
|
||||
@Setter
|
||||
private boolean sprinting;
|
||||
|
||||
/**
|
||||
* The overworld dimension which Bedrock Edition uses.
|
||||
*/
|
||||
private BedrockDimension bedrockOverworldDimension = BedrockDimension.OVERWORLD;
|
||||
/**
|
||||
* The dimension of the player.
|
||||
* As all entities are in the same world, this can be safely applied to all other entities.
|
||||
@ -399,7 +409,7 @@ public class GeyserSession extends FloodgateConnection implements GeyserConnecti
|
||||
* right before the StartGamePacket is sent.
|
||||
*/
|
||||
@Setter
|
||||
private BedrockDimension bedrockDimension = BedrockDimension.OVERWORLD;
|
||||
private BedrockDimension bedrockDimension = this.bedrockOverworldDimension;
|
||||
|
||||
@Setter
|
||||
private int breakingBlock;
|
||||
@ -570,16 +580,16 @@ public class GeyserSession extends FloodgateConnection implements GeyserConnecti
|
||||
private float walkSpeed;
|
||||
|
||||
/**
|
||||
* Caches current rain status.
|
||||
* Caches current rain strength.
|
||||
* Value between 0 and 1.
|
||||
*/
|
||||
@Setter
|
||||
private boolean raining = false;
|
||||
private float rainStrength = 0.0f;
|
||||
|
||||
/**
|
||||
* Caches current thunder status.
|
||||
* Caches current thunder strength.
|
||||
* Value between 0 and 1.
|
||||
*/
|
||||
@Setter
|
||||
private boolean thunder = false;
|
||||
private float thunderStrength = 0.0f;
|
||||
|
||||
/**
|
||||
* Stores a map of all statistics sent from the server.
|
||||
@ -709,6 +719,31 @@ public class GeyserSession extends FloodgateConnection implements GeyserConnecti
|
||||
* Send all necessary packets to load Bedrock into the server
|
||||
*/
|
||||
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();
|
||||
sentSpawnPacket = true;
|
||||
syncEntityProperties();
|
||||
@ -879,7 +914,14 @@ public class GeyserSession extends FloodgateConnection implements GeyserConnecti
|
||||
return task.getAuthentication().handle((result, ex) -> {
|
||||
if (ex != null) {
|
||||
geyser.getLogger().error("Failed to log in with Microsoft code!", ex);
|
||||
disconnect(ex.toString());
|
||||
if (ex instanceof CompletionException ce
|
||||
&& ce.getCause() instanceof MinecraftRequestException mre
|
||||
&& mre.getResponse().getStatusCode() == 404) {
|
||||
// Player is trying to join with a Microsoft account that doesn't have Java Edition purchased
|
||||
disconnect(GeyserLocale.getPlayerLocaleString("geyser.network.remote.invalid_account", locale()));
|
||||
} else {
|
||||
disconnect(ex.toString());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -924,8 +966,6 @@ public class GeyserSession extends FloodgateConnection implements GeyserConnecti
|
||||
// Start ticking
|
||||
tickThread = eventLoop.scheduleAtFixedRate(this::tick, 50, 50, TimeUnit.MILLISECONDS);
|
||||
|
||||
this.protocol.setUseDefaultListeners(false);
|
||||
|
||||
TcpSession downstream;
|
||||
if (geyser.getBootstrap().getSocketAddress() != null) {
|
||||
// We're going to connect through the JVM and not through TCP
|
||||
@ -951,7 +991,6 @@ public class GeyserSession extends FloodgateConnection implements GeyserConnecti
|
||||
this.downstream.getSession().setFlag(MinecraftConstants.FOLLOW_TRANSFERS, false);
|
||||
|
||||
if (geyser.getConfig().getRemote().isUseProxyProtocol()) {
|
||||
downstream.setFlag(BuiltinFlags.ENABLE_CLIENT_PROXY_PROTOCOL, true);
|
||||
downstream.setFlag(BuiltinFlags.CLIENT_PROXIED_ADDRESS, upstream.getAddress());
|
||||
}
|
||||
if (geyser.getConfig().isForwardPlayerPing()) {
|
||||
@ -961,22 +1000,6 @@ public class GeyserSession extends FloodgateConnection implements GeyserConnecti
|
||||
// We'll handle this since we have the registry data on hand
|
||||
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() {
|
||||
@Override
|
||||
public void packetSending(PacketSendingEvent event) {
|
||||
@ -1172,9 +1195,9 @@ public class GeyserSession extends FloodgateConnection implements GeyserConnecti
|
||||
tickThread.cancel(false);
|
||||
}
|
||||
|
||||
erosionHandler.close();
|
||||
|
||||
// Mark session as closed before cancelling erosion futures
|
||||
closed = true;
|
||||
erosionHandler.close();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1195,6 +1218,8 @@ public class GeyserSession extends FloodgateConnection implements GeyserConnecti
|
||||
eventLoop.execute(() -> {
|
||||
try {
|
||||
runnable.run();
|
||||
} catch (ErosionCancellationException e) {
|
||||
geyser.getLogger().debug("Caught ErosionCancellationException");
|
||||
} catch (Throwable e) {
|
||||
geyser.getLogger().error("Error thrown in " + this.bedrockUsername() + "'s event loop!", e);
|
||||
}
|
||||
@ -1212,6 +1237,8 @@ public class GeyserSession extends FloodgateConnection implements GeyserConnecti
|
||||
if (!closed) {
|
||||
runnable.run();
|
||||
}
|
||||
} catch (ErosionCancellationException e) {
|
||||
geyser.getLogger().debug("Caught ErosionCancellationException");
|
||||
} catch (Throwable e) {
|
||||
geyser.getLogger().error("Error thrown in " + this.bedrockUsername() + "'s event loop!", e);
|
||||
}
|
||||
@ -1540,7 +1567,7 @@ public class GeyserSession extends FloodgateConnection implements GeyserConnecti
|
||||
startGamePacket.setRotation(Vector2f.from(1, 1));
|
||||
|
||||
startGamePacket.setSeed(-1L);
|
||||
startGamePacket.setDimensionId(DimensionUtils.javaToBedrock(bedrockDimension));
|
||||
startGamePacket.setDimensionId(bedrockDimension.bedrockId());
|
||||
startGamePacket.setGeneratorId(1);
|
||||
startGamePacket.setLevelGameType(GameType.SURVIVAL);
|
||||
startGamePacket.setDifficulty(1);
|
||||
@ -1704,8 +1731,8 @@ public class GeyserSession extends FloodgateConnection implements GeyserConnecti
|
||||
return;
|
||||
}
|
||||
|
||||
if (protocol.getState() != intendedState) {
|
||||
geyser.getLogger().debug("Tried to send " + packet.getClass().getSimpleName() + " packet while not in " + intendedState.name() + " state");
|
||||
if (protocol.getOutboundState() != intendedState) {
|
||||
geyser.getLogger().debug("Tried to send " + packet.getClass().getSimpleName() + " packet while not in " + intendedState.name() + " outbound state");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1739,7 +1766,7 @@ public class GeyserSession extends FloodgateConnection implements GeyserConnecti
|
||||
}
|
||||
|
||||
private void sendDownstreamPacket0(Packet packet) {
|
||||
ProtocolState state = protocol.getState();
|
||||
ProtocolState state = protocol.getOutboundState();
|
||||
if (state == ProtocolState.GAME || state == ProtocolState.CONFIGURATION || packet.getClass() == ServerboundCustomQueryAnswerPacket.class) {
|
||||
downstream.sendPacket(packet);
|
||||
} else {
|
||||
@ -1964,6 +1991,71 @@ public class GeyserSession extends FloodgateConnection implements GeyserConnecti
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a packet to update rain strength.
|
||||
* Stops rain if strength is 0.
|
||||
*
|
||||
* @param strength value between 0 and 1
|
||||
*/
|
||||
public void updateRain(float strength) {
|
||||
boolean wasRaining = isRaining();
|
||||
this.rainStrength = strength;
|
||||
|
||||
LevelEventPacket rainPacket = new LevelEventPacket();
|
||||
rainPacket.setType(isRaining() ? LevelEvent.START_RAINING : LevelEvent.STOP_RAINING);
|
||||
rainPacket.setData((int) (strength * 65535));
|
||||
rainPacket.setPosition(Vector3f.ZERO);
|
||||
sendUpstreamPacket(rainPacket);
|
||||
|
||||
// Keep thunder in sync with rain when starting/stopping a storm
|
||||
if ((wasRaining != isRaining()) && isThunder()) {
|
||||
if (isRaining()) {
|
||||
LevelEventPacket thunderPacket = new LevelEventPacket();
|
||||
thunderPacket.setType(LevelEvent.START_THUNDERSTORM);
|
||||
thunderPacket.setData((int) (this.thunderStrength * 65535));
|
||||
thunderPacket.setPosition(Vector3f.ZERO);
|
||||
sendUpstreamPacket(thunderPacket);
|
||||
} else {
|
||||
LevelEventPacket thunderPacket = new LevelEventPacket();
|
||||
thunderPacket.setType(LevelEvent.STOP_THUNDERSTORM);
|
||||
thunderPacket.setData(0);
|
||||
thunderPacket.setPosition(Vector3f.ZERO);
|
||||
sendUpstreamPacket(thunderPacket);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a packet to update thunderstorm strength.
|
||||
* Stops thunderstorm if strength is 0.
|
||||
*
|
||||
* @param strength value between 0 and 1
|
||||
*/
|
||||
public void updateThunder(float strength) {
|
||||
this.thunderStrength = strength;
|
||||
|
||||
// Do not send thunder packet if not raining
|
||||
// The bedrock client will start raining automatically when updating thunder strength
|
||||
// https://github.com/GeyserMC/Geyser/issues/3679
|
||||
if (!isRaining()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LevelEventPacket thunderPacket = new LevelEventPacket();
|
||||
thunderPacket.setType(isThunder() ? LevelEvent.START_THUNDERSTORM : LevelEvent.STOP_THUNDERSTORM);
|
||||
thunderPacket.setData((int) (strength * 65535));
|
||||
thunderPacket.setPosition(Vector3f.ZERO);
|
||||
sendUpstreamPacket(thunderPacket);
|
||||
}
|
||||
|
||||
public boolean isRaining() {
|
||||
return this.rainStrength > 0;
|
||||
}
|
||||
|
||||
public boolean isThunder() {
|
||||
return this.thunderStrength > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String bedrockUsername() {
|
||||
return authData.name();
|
||||
|
@ -31,15 +31,18 @@ import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||
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 org.geysermc.geyser.entity.type.Entity;
|
||||
import org.geysermc.geyser.entity.type.Tickable;
|
||||
import org.geysermc.geyser.entity.type.player.PlayerEntity;
|
||||
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
|
||||
* for that player (e.g. seeing vanished players from /vanish)
|
||||
@ -68,6 +71,10 @@ public class EntityCache {
|
||||
if (cacheEntity(entity)) {
|
||||
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) {
|
||||
// Start ticking it
|
||||
tickableEntities.add((Tickable) entity);
|
||||
@ -86,21 +93,24 @@ public class EntityCache {
|
||||
}
|
||||
|
||||
public void removeEntity(Entity entity) {
|
||||
if (entity == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (entity instanceof PlayerEntity player) {
|
||||
session.getPlayerWithCustomHeads().remove(player.getUuid());
|
||||
}
|
||||
|
||||
if (entity != null) {
|
||||
if (entity.isValid()) {
|
||||
entity.despawnEntity();
|
||||
}
|
||||
if (entity.isValid()) {
|
||||
entity.despawnEntity();
|
||||
}
|
||||
entities.remove(entityIdTranslations.remove(entity.getEntityId()));
|
||||
|
||||
long geyserId = entityIdTranslations.remove(entity.getEntityId());
|
||||
entities.remove(geyserId);
|
||||
// don't track the entity anymore, now that it's removed
|
||||
session.getWorldCache().getScoreboard().entityRemoved(entity);
|
||||
|
||||
if (entity instanceof Tickable) {
|
||||
tickableEntities.remove(entity);
|
||||
}
|
||||
if (entity instanceof Tickable) {
|
||||
tickableEntities.remove(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,15 +136,39 @@ public class EntityCache {
|
||||
|
||||
public void addPlayerEntity(PlayerEntity entity) {
|
||||
// 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) {
|
||||
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) {
|
||||
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() {
|
||||
|
@ -49,7 +49,7 @@ import org.geysermc.geyser.session.GeyserSession;
|
||||
import org.geysermc.geyser.session.cache.registry.JavaRegistry;
|
||||
import org.geysermc.geyser.session.cache.registry.RegistryEntryContext;
|
||||
import org.geysermc.geyser.session.cache.registry.SimpleJavaRegistry;
|
||||
import org.geysermc.geyser.text.TextDecoration;
|
||||
import org.geysermc.geyser.text.ChatDecoration;
|
||||
import org.geysermc.geyser.translator.level.BiomeTranslator;
|
||||
import org.geysermc.geyser.util.MinecraftKey;
|
||||
import org.geysermc.mcprotocollib.protocol.MinecraftProtocol;
|
||||
@ -78,7 +78,7 @@ public final class RegistryCache {
|
||||
private static final Map<Key, BiConsumer<RegistryCache, List<RegistryEntry>>> REGISTRIES = new HashMap<>();
|
||||
|
||||
static {
|
||||
register("chat_type", cache -> cache.chatTypes, TextDecoration::readChatType);
|
||||
register("chat_type", cache -> cache.chatTypes, ChatDecoration::readChatType);
|
||||
register("dimension_type", cache -> cache.dimensions, JavaDimension::read);
|
||||
register("enchantment", cache -> cache.enchantments, Enchantment::read);
|
||||
register("jukebox_song", cache -> cache.jukeboxSongs, JukeboxSong::read);
|
||||
|
@ -31,9 +31,11 @@ import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.cloudburstmc.math.vector.Vector3i;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.SetTitlePacket;
|
||||
import org.geysermc.geyser.item.type.Item;
|
||||
import org.geysermc.geyser.scoreboard.Scoreboard;
|
||||
import org.geysermc.geyser.scoreboard.ScoreboardUpdater.ScoreboardSession;
|
||||
import org.geysermc.geyser.session.GeyserSession;
|
||||
@ -48,7 +50,7 @@ public final class WorldCache {
|
||||
@Getter
|
||||
private final ScoreboardSession scoreboardSession;
|
||||
@Getter
|
||||
private Scoreboard scoreboard;
|
||||
private @NonNull Scoreboard scoreboard;
|
||||
@Getter
|
||||
@Setter
|
||||
private Difficulty difficulty = Difficulty.EASY;
|
||||
@ -70,6 +72,8 @@ public final class WorldCache {
|
||||
@Setter
|
||||
private boolean editingSignOnFront;
|
||||
|
||||
private final Object2IntMap<Item> activeCooldowns = new Object2IntOpenHashMap<>(2);
|
||||
|
||||
public WorldCache(GeyserSession session) {
|
||||
this.session = session;
|
||||
this.scoreboard = new Scoreboard(session);
|
||||
@ -78,10 +82,8 @@ public final class WorldCache {
|
||||
}
|
||||
|
||||
public void removeScoreboard() {
|
||||
if (scoreboard != null) {
|
||||
scoreboard.removeScoreboard();
|
||||
scoreboard = new Scoreboard(session);
|
||||
}
|
||||
scoreboard.removeScoreboard();
|
||||
scoreboard = new Scoreboard(session);
|
||||
}
|
||||
|
||||
public int increaseAndGetScoreboardPacketsPerSecond() {
|
||||
@ -201,4 +203,32 @@ public final class WorldCache {
|
||||
public String removeActiveRecord(Vector3i pos) {
|
||||
return this.activeRecords.remove(pos);
|
||||
}
|
||||
|
||||
public void setCooldown(Item item, int ticks) {
|
||||
if (ticks == 0) {
|
||||
// As of Java 1.21
|
||||
this.activeCooldowns.removeInt(item);
|
||||
return;
|
||||
}
|
||||
this.activeCooldowns.put(item, session.getTicks() + ticks);
|
||||
}
|
||||
|
||||
public boolean hasCooldown(Item item) {
|
||||
return this.activeCooldowns.containsKey(item);
|
||||
}
|
||||
|
||||
public void tick() {
|
||||
// Implementation note: technically we could empty the field during hasCooldown checks,
|
||||
// but we don't want the cooldown field to balloon in size from overuse.
|
||||
if (!this.activeCooldowns.isEmpty()) {
|
||||
int ticks = session.getTicks();
|
||||
Iterator<Object2IntMap.Entry<Item>> it = Object2IntMaps.fastIterator(this.activeCooldowns);
|
||||
while (it.hasNext()) {
|
||||
Object2IntMap.Entry<Item> entry = it.next();
|
||||
if (entry.getIntValue() <= ticks) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,8 @@
|
||||
|
||||
package org.geysermc.geyser.text;
|
||||
|
||||
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
|
||||
|
||||
public class ChatColor {
|
||||
public static final String ANSI_RESET = (char) 0x1b + "[0m";
|
||||
|
||||
@ -84,4 +86,58 @@ public class ChatColor {
|
||||
string = string.replace(WHITE, (char) 0x1b + "[37;1m");
|
||||
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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -25,17 +25,19 @@
|
||||
|
||||
package org.geysermc.geyser.text;
|
||||
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.Style;
|
||||
import org.cloudburstmc.nbt.NbtMap;
|
||||
import org.cloudburstmc.nbt.NbtType;
|
||||
import org.geysermc.geyser.session.cache.registry.RegistryEntryContext;
|
||||
import org.geysermc.geyser.translator.text.MessageTranslator;
|
||||
import org.geysermc.mcprotocollib.protocol.data.game.chat.ChatType;
|
||||
import org.geysermc.mcprotocollib.protocol.data.game.chat.ChatTypeDecoration;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public record TextDecoration(String translationKey, List<Parameter> parameters, Style deserializedStyle) implements ChatTypeDecoration {
|
||||
public record ChatDecoration(String translationKey, List<Parameter> parameters, Style deserializedStyle) implements ChatTypeDecoration {
|
||||
|
||||
@Override
|
||||
public NbtMap style() {
|
||||
@ -53,38 +55,22 @@ public record TextDecoration(String translationKey, List<Parameter> parameters,
|
||||
String translationKey = chat.getString("translation_key");
|
||||
|
||||
NbtMap styleTag = chat.getCompound("style");
|
||||
Style style = deserializeStyle(styleTag);
|
||||
Style style = MessageTranslator.getStyleFromNbtMap(styleTag);
|
||||
|
||||
List<ChatTypeDecoration.Parameter> parameters = new ArrayList<>();
|
||||
List<String> parametersNbt = chat.getList("parameters", NbtType.STRING);
|
||||
for (String parameter : parametersNbt) {
|
||||
parameters.add(ChatTypeDecoration.Parameter.valueOf(parameter.toUpperCase(Locale.ROOT)));
|
||||
}
|
||||
return new ChatType(new TextDecoration(translationKey, parameters, style), null);
|
||||
return new ChatType(new ChatDecoration(translationKey, parameters, style), null);
|
||||
}
|
||||
return new ChatType(null, null);
|
||||
}
|
||||
|
||||
public static Style getStyle(ChatTypeDecoration decoration) {
|
||||
if (decoration instanceof TextDecoration textDecoration) {
|
||||
return textDecoration.deserializedStyle();
|
||||
if (decoration instanceof ChatDecoration chatDecoration) {
|
||||
return chatDecoration.deserializedStyle();
|
||||
}
|
||||
return deserializeStyle(decoration.style());
|
||||
}
|
||||
|
||||
private static Style deserializeStyle(NbtMap styleTag) {
|
||||
Style.Builder builder = Style.style();
|
||||
if (!styleTag.isEmpty()) {
|
||||
String color = styleTag.getString("color", null);
|
||||
if (color != null) {
|
||||
builder.color(NamedTextColor.NAMES.value(color));
|
||||
}
|
||||
//TODO implement the rest
|
||||
boolean italic = styleTag.getBoolean("italic");
|
||||
if (italic) {
|
||||
builder.decorate(net.kyori.adventure.text.format.TextDecoration.ITALIC);
|
||||
}
|
||||
}
|
||||
return builder.build();
|
||||
return MessageTranslator.getStyleFromNbtMap(decoration.style());
|
||||
}
|
||||
}
|
@ -981,11 +981,11 @@ public abstract class InventoryTranslator {
|
||||
|
||||
List<ItemStackResponseContainer> containerEntries = new ArrayList<>();
|
||||
for (Map.Entry<ContainerSlotType, List<ItemStackResponseSlot>> entry : containerMap.entrySet()) {
|
||||
containerEntries.add(new ItemStackResponseContainer(entry.getKey(), entry.getValue(), new FullContainerName(entry.getKey(), 0)));
|
||||
containerEntries.add(new ItemStackResponseContainer(entry.getKey(), entry.getValue(), new FullContainerName(entry.getKey(), null)));
|
||||
}
|
||||
|
||||
ItemStackResponseSlot cursorEntry = makeItemEntry(0, session.getPlayerInventory().getCursor());
|
||||
containerEntries.add(new ItemStackResponseContainer(ContainerSlotType.CURSOR, Collections.singletonList(cursorEntry), new FullContainerName(ContainerSlotType.CURSOR, 0)));
|
||||
containerEntries.add(new ItemStackResponseContainer(ContainerSlotType.CURSOR, Collections.singletonList(cursorEntry), new FullContainerName(ContainerSlotType.CURSOR, null)));
|
||||
|
||||
return containerEntries;
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ package org.geysermc.geyser.translator.inventory;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.ItemStackRequest;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.ItemStackRequestSlotData;
|
||||
@ -139,6 +140,7 @@ public class OldSmithingTableTranslator extends AbstractBlockInventoryTranslator
|
||||
slotPacket.setContainerId(ContainerId.UI);
|
||||
slotPacket.setSlot(53);
|
||||
slotPacket.setItem(UPGRADE_TEMPLATE.apply(session.getUpstream().getProtocolVersion()));
|
||||
slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(slotPacket);
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
|
||||
import it.unimi.dsi.fastutil.ints.IntSet;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.ItemStackRequest;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.ItemStackRequestSlotData;
|
||||
@ -83,6 +84,7 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
|
||||
contents[i - 36] = inventory.getItem(i).getItemData(session);
|
||||
}
|
||||
inventoryContentPacket.setContents(Arrays.asList(contents));
|
||||
inventoryContentPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(inventoryContentPacket);
|
||||
|
||||
// Armor
|
||||
@ -99,12 +101,14 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
|
||||
}
|
||||
}
|
||||
armorContentPacket.setContents(Arrays.asList(contents));
|
||||
armorContentPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(armorContentPacket);
|
||||
|
||||
// Offhand
|
||||
InventoryContentPacket offhandPacket = new InventoryContentPacket();
|
||||
offhandPacket.setContainerId(ContainerId.OFFHAND);
|
||||
offhandPacket.setContents(Collections.singletonList(inventory.getItem(45).getItemData(session)));
|
||||
offhandPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(offhandPacket);
|
||||
}
|
||||
|
||||
@ -126,6 +130,7 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
|
||||
slotPacket.setItem(inventory.getItem(i).getItemData(session));
|
||||
}
|
||||
|
||||
slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(slotPacket);
|
||||
}
|
||||
}
|
||||
@ -162,11 +167,13 @@ public class PlayerInventoryTranslator extends InventoryTranslator {
|
||||
slotPacket.setSlot(slot + 27);
|
||||
}
|
||||
slotPacket.setItem(bedrockItem);
|
||||
slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(slotPacket);
|
||||
} else if (slot == 45) {
|
||||
InventoryContentPacket offhandPacket = new InventoryContentPacket();
|
||||
offhandPacket.setContainerId(ContainerId.OFFHAND);
|
||||
offhandPacket.setContents(Collections.singletonList(bedrockItem));
|
||||
offhandPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(offhandPacket);
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ package org.geysermc.geyser.translator.inventory.horse;
|
||||
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.itemstack.request.ItemStackRequestSlotData;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.InventoryContentPacket;
|
||||
@ -94,6 +95,7 @@ public abstract class ChestedHorseInventoryTranslator extends AbstractHorseInven
|
||||
InventoryContentPacket contentPacket = new InventoryContentPacket();
|
||||
contentPacket.setContainerId(ContainerId.INVENTORY);
|
||||
contentPacket.setContents(Arrays.asList(bedrockItems));
|
||||
contentPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(contentPacket);
|
||||
|
||||
ItemData[] horseItems = new ItemData[chestSize + 1];
|
||||
@ -107,6 +109,7 @@ public abstract class ChestedHorseInventoryTranslator extends AbstractHorseInven
|
||||
InventoryContentPacket horseContentsPacket = new InventoryContentPacket();
|
||||
horseContentsPacket.setContainerId(inventory.getBedrockId());
|
||||
horseContentsPacket.setContents(Arrays.asList(horseItems));
|
||||
horseContentsPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(horseContentsPacket);
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ import org.cloudburstmc.math.vector.Vector3d;
|
||||
import org.cloudburstmc.math.vector.Vector3f;
|
||||
import org.cloudburstmc.math.vector.Vector3i;
|
||||
import org.cloudburstmc.protocol.bedrock.data.LevelEvent;
|
||||
import org.cloudburstmc.protocol.bedrock.data.SoundEvent;
|
||||
import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition;
|
||||
import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType;
|
||||
@ -42,6 +43,7 @@ import org.cloudburstmc.protocol.bedrock.data.inventory.transaction.LegacySetIte
|
||||
import org.cloudburstmc.protocol.bedrock.packet.ContainerOpenPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.InventoryTransactionPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.LevelSoundEventPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.UpdateBlockPacket;
|
||||
import org.geysermc.geyser.entity.EntityDefinitions;
|
||||
import org.geysermc.geyser.entity.type.Entity;
|
||||
@ -75,12 +77,15 @@ import org.geysermc.geyser.util.CooldownUtils;
|
||||
import org.geysermc.geyser.util.EntityUtils;
|
||||
import org.geysermc.geyser.util.InteractionResult;
|
||||
import org.geysermc.geyser.util.InventoryUtils;
|
||||
import org.geysermc.mcprotocollib.protocol.data.game.Holder;
|
||||
import org.geysermc.mcprotocollib.protocol.data.game.entity.object.Direction;
|
||||
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.InteractAction;
|
||||
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.PlayerAction;
|
||||
import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack;
|
||||
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType;
|
||||
import org.geysermc.mcprotocollib.protocol.data.game.item.component.Instrument;
|
||||
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.inventory.ServerboundContainerClickPacket;
|
||||
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundInteractPacket;
|
||||
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.player.ServerboundMovePlayerPosRotPacket;
|
||||
@ -373,6 +378,22 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
|
||||
break;
|
||||
} else if (packet.getItemInHand().getDefinition() == session.getItemMappings().getStoredItems().writtenBook().getBedrockDefinition()) {
|
||||
session.setCurrentBook(packet.getItemInHand());
|
||||
} else if (session.getPlayerInventory().getItemInHand().asItem() == Items.GOAT_HORN) {
|
||||
// Temporary workaround while we don't have full item/block use tracking.
|
||||
if (!session.getWorldCache().hasCooldown(Items.GOAT_HORN)) {
|
||||
Holder<Instrument> instrument = session.getPlayerInventory()
|
||||
.getItemInHand()
|
||||
.getComponent(DataComponentType.INSTRUMENT);
|
||||
if (instrument != null && instrument.isId()) {
|
||||
// BDS uses a LevelSoundEvent2Packet, but that doesn't work here... (as of 1.21.20)
|
||||
LevelSoundEventPacket soundPacket = new LevelSoundEventPacket();
|
||||
soundPacket.setSound(SoundEvent.valueOf("GOAT_CALL_" + instrument.id()));
|
||||
soundPacket.setPosition(session.getPlayerEntity().getPosition());
|
||||
soundPacket.setIdentifier("minecraft:player");
|
||||
soundPacket.setExtraData(-1);
|
||||
session.sendUpstreamPacket(soundPacket);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ import org.cloudburstmc.protocol.bedrock.packet.SetPlayerGameTypePacket;
|
||||
import org.geysermc.erosion.Constants;
|
||||
import org.geysermc.geyser.api.network.AuthType;
|
||||
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
|
||||
import org.geysermc.geyser.erosion.GeyserboundHandshakePacketHandler;
|
||||
import org.geysermc.geyser.level.BedrockDimension;
|
||||
import org.geysermc.geyser.level.JavaDimension;
|
||||
import org.geysermc.geyser.session.GeyserSession;
|
||||
import org.geysermc.geyser.translator.protocol.PacketTranslator;
|
||||
@ -56,18 +56,13 @@ public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket
|
||||
SessionPlayerEntity entity = session.getPlayerEntity();
|
||||
entity.setEntityId(packet.getEntityId());
|
||||
|
||||
if (session.getErosionHandler().isActive()) {
|
||||
session.getErosionHandler().close();
|
||||
session.setErosionHandler(new GeyserboundHandshakePacketHandler(session));
|
||||
}
|
||||
|
||||
PlayerSpawnInfo spawnInfo = packet.getCommonPlayerSpawnInfo();
|
||||
JavaDimension newDimension = session.getRegistryCache().dimensions().byId(spawnInfo.getDimension());
|
||||
|
||||
// If the player is already initialized and a join game packet is sent, they
|
||||
// are swapping servers
|
||||
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()) {
|
||||
// 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.
|
||||
@ -127,9 +122,9 @@ public class JavaLoginTranslator extends PacketTranslator<ClientboundLoginPacket
|
||||
}
|
||||
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);
|
||||
} 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
|
||||
session.camera().sendFog(DimensionUtils.BEDROCK_FOG_HELL);
|
||||
}
|
||||
|
@ -25,9 +25,6 @@
|
||||
|
||||
package org.geysermc.geyser.translator.protocol.java;
|
||||
|
||||
import org.cloudburstmc.math.vector.Vector3f;
|
||||
import org.cloudburstmc.protocol.bedrock.data.LevelEvent;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.SetPlayerGameTypePacket;
|
||||
import org.geysermc.geyser.entity.attribute.GeyserAttributeType;
|
||||
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
|
||||
@ -76,21 +73,11 @@ public class JavaRespawnTranslator extends PacketTranslator<ClientboundRespawnPa
|
||||
session.setGameMode(spawnInfo.getGameMode());
|
||||
|
||||
if (session.isRaining()) {
|
||||
LevelEventPacket stopRainPacket = new LevelEventPacket();
|
||||
stopRainPacket.setType(LevelEvent.STOP_RAINING);
|
||||
stopRainPacket.setData(0);
|
||||
stopRainPacket.setPosition(Vector3f.ZERO);
|
||||
session.sendUpstreamPacket(stopRainPacket);
|
||||
session.setRaining(false);
|
||||
session.updateRain(0);
|
||||
}
|
||||
|
||||
if (session.isThunder()) {
|
||||
LevelEventPacket stopThunderPacket = new LevelEventPacket();
|
||||
stopThunderPacket.setType(LevelEvent.STOP_THUNDERSTORM);
|
||||
stopThunderPacket.setData(0);
|
||||
stopThunderPacket.setPosition(Vector3f.ZERO);
|
||||
session.sendUpstreamPacket(stopThunderPacket);
|
||||
session.setThunder(false);
|
||||
session.updateThunder(0);
|
||||
}
|
||||
|
||||
JavaDimension newDimension = session.getRegistryCache().dimensions().byId(spawnInfo.getDimension());
|
||||
|
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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.translator.protocol.java;
|
||||
|
||||
import org.geysermc.geyser.erosion.GeyserboundHandshakePacketHandler;
|
||||
import org.geysermc.geyser.session.GeyserSession;
|
||||
import org.geysermc.geyser.translator.protocol.PacketTranslator;
|
||||
import org.geysermc.geyser.translator.protocol.Translator;
|
||||
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.ClientboundStartConfigurationPacket;
|
||||
|
||||
@Translator(packet = ClientboundStartConfigurationPacket.class)
|
||||
public class JavaStartConfigurationTranslator extends PacketTranslator<ClientboundStartConfigurationPacket> {
|
||||
|
||||
@Override
|
||||
public void translate(GeyserSession session, ClientboundStartConfigurationPacket packet) {
|
||||
var erosionHandler = session.getErosionHandler();
|
||||
if (erosionHandler.isActive()) {
|
||||
// Set new handler before closing
|
||||
session.setErosionHandler(new GeyserboundHandshakePacketHandler(session));
|
||||
erosionHandler.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldExecuteInEventLoop() {
|
||||
// Execute outside of event loop to cancel any pending erosion futures
|
||||
return false;
|
||||
}
|
||||
}
|
@ -25,7 +25,10 @@
|
||||
|
||||
package org.geysermc.geyser.translator.protocol.java;
|
||||
|
||||
import net.kyori.adventure.text.TextComponent;
|
||||
import net.kyori.adventure.text.TranslatableComponent;
|
||||
import net.kyori.adventure.text.TranslationArgument;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.cloudburstmc.nbt.NbtMap;
|
||||
import org.cloudburstmc.protocol.bedrock.data.LevelEvent;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.LevelEventGenericPacket;
|
||||
@ -55,16 +58,18 @@ public class JavaSystemChatTranslator extends PacketTranslator<ClientboundSystem
|
||||
if (component.arguments().size() == 2) {
|
||||
// Hack FYI, but it allows Bedrock players to easily understand this information
|
||||
// without it being covered up or saying the night is being slept through.
|
||||
int numPlayersSleeping = ((Number) component.arguments().get(0).value()).intValue();
|
||||
int totalPlayersNeeded = ((Number) component.arguments().get(1).value()).intValue();
|
||||
LevelEventGenericPacket sleepInfoPacket = new LevelEventGenericPacket();
|
||||
sleepInfoPacket.setType(LevelEvent.SLEEPING_PLAYERS);
|
||||
sleepInfoPacket.setTag(NbtMap.builder()
|
||||
.putInt("ableToSleep", totalPlayersNeeded)
|
||||
.putInt("overworldPlayerCount", totalPlayersNeeded)
|
||||
.putInt("sleepingPlayerCount", numPlayersSleeping)
|
||||
.build());
|
||||
session.sendUpstreamPacket(sleepInfoPacket);
|
||||
Integer numPlayersSleeping = convertToInt(component.arguments().get(0));
|
||||
Integer totalPlayersNeeded = convertToInt(component.arguments().get(1));
|
||||
if (numPlayersSleeping != null && totalPlayersNeeded != null) {
|
||||
LevelEventGenericPacket sleepInfoPacket = new LevelEventGenericPacket();
|
||||
sleepInfoPacket.setType(LevelEvent.SLEEPING_PLAYERS);
|
||||
sleepInfoPacket.setTag(NbtMap.builder()
|
||||
.putInt("ableToSleep", totalPlayersNeeded)
|
||||
.putInt("overworldPlayerCount", totalPlayersNeeded)
|
||||
.putInt("sleepingPlayerCount", numPlayersSleeping)
|
||||
.build());
|
||||
session.sendUpstreamPacket(sleepInfoPacket);
|
||||
}
|
||||
}
|
||||
} else if (component.key().equals("sleep.skipping_night")) {
|
||||
LevelEventGenericPacket sleepInfoPacket = new LevelEventGenericPacket();
|
||||
@ -97,4 +102,19 @@ public class JavaSystemChatTranslator extends PacketTranslator<ClientboundSystem
|
||||
session.getUpstream().queuePostStartGamePacket(textPacket);
|
||||
}
|
||||
}
|
||||
|
||||
private static @Nullable Integer convertToInt(TranslationArgument translationArgument) {
|
||||
Object value = translationArgument.value();
|
||||
if (value instanceof Number number) {
|
||||
return number.intValue();
|
||||
}
|
||||
if (value instanceof TextComponent textComponent) {
|
||||
try {
|
||||
return Integer.parseInt(textComponent.content());
|
||||
} catch (NumberFormatException e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,8 @@ import org.cloudburstmc.protocol.bedrock.data.SoundEvent;
|
||||
import org.cloudburstmc.protocol.bedrock.data.entity.EntityDataTypes;
|
||||
import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.EntityEventPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.InventoryContentPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket;
|
||||
@ -167,6 +169,7 @@ public class JavaEntityEventTranslator extends PacketTranslator<ClientboundEntit
|
||||
InventoryContentPacket offhandPacket = new InventoryContentPacket();
|
||||
offhandPacket.setContainerId(ContainerId.OFFHAND);
|
||||
offhandPacket.setContents(Collections.singletonList(InventoryUtils.getTotemOfUndying().apply(session.getUpstream().getProtocolVersion())));
|
||||
offhandPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(offhandPacket);
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,8 @@
|
||||
|
||||
package org.geysermc.geyser.translator.protocol.java.inventory;
|
||||
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName;
|
||||
import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack;
|
||||
import org.geysermc.mcprotocollib.protocol.data.game.recipe.Ingredient;
|
||||
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.inventory.ClientboundContainerSetSlotPacket;
|
||||
@ -180,6 +182,7 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator<Clientbound
|
||||
slotPacket.setContainerId(ContainerId.UI);
|
||||
slotPacket.setSlot(col + (row * gridDimensions) + offset);
|
||||
slotPacket.setItem(ItemData.AIR);
|
||||
slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(slotPacket);
|
||||
index++;
|
||||
}
|
||||
@ -212,6 +215,7 @@ public class JavaContainerSetSlotTranslator extends PacketTranslator<Clientbound
|
||||
slotPacket.setContainerId(ContainerId.UI);
|
||||
slotPacket.setSlot(col + (row * gridDimensions) + offset);
|
||||
slotPacket.setItem(ingredients[index]);
|
||||
slotPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(slotPacket);
|
||||
index++;
|
||||
}
|
||||
|
@ -57,5 +57,7 @@ public class JavaCooldownTranslator extends PacketTranslator<ClientboundCooldown
|
||||
bedrockPacket.setCooldownDuration(packet.getCooldownTicks());
|
||||
session.sendUpstreamPacket(bedrockPacket);
|
||||
}
|
||||
|
||||
session.getWorldCache().setCooldown(item, packet.getCooldownTicks());
|
||||
}
|
||||
}
|
||||
|
@ -33,9 +33,7 @@ import org.geysermc.mcprotocollib.protocol.data.game.level.notify.RespawnScreenV
|
||||
import org.geysermc.mcprotocollib.protocol.data.game.level.notify.ThunderStrengthValue;
|
||||
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.level.ClientboundGameEventPacket;
|
||||
import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.ServerboundClientCommandPacket;
|
||||
import org.cloudburstmc.math.vector.Vector3f;
|
||||
import org.cloudburstmc.protocol.bedrock.data.GameRuleData;
|
||||
import org.cloudburstmc.protocol.bedrock.data.LevelEvent;
|
||||
import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.*;
|
||||
import org.geysermc.geyser.entity.type.player.PlayerEntity;
|
||||
@ -48,9 +46,6 @@ import org.geysermc.geyser.util.EntityUtils;
|
||||
|
||||
@Translator(packet = ClientboundGameEventPacket.class)
|
||||
public class JavaGameEventTranslator extends PacketTranslator<ClientboundGameEventPacket> {
|
||||
// Strength of rainstorms and thunderstorms is a 0-1 float on Java, while on Bedrock it is a 0-65535 int
|
||||
private static final int MAX_STORM_STRENGTH = 65535;
|
||||
|
||||
@Override
|
||||
public void translate(GeyserSession session, ClientboundGameEventPacket packet) {
|
||||
PlayerEntity entity = session.getPlayerEntity();
|
||||
@ -65,42 +60,20 @@ public class JavaGameEventTranslator extends PacketTranslator<ClientboundGameEve
|
||||
// As a result many developers use these packets for the opposite of what their names implies
|
||||
// Behavior last verified with Java 1.19.4 and Bedrock 1.19.71
|
||||
case START_RAIN:
|
||||
LevelEventPacket stopRainPacket = new LevelEventPacket();
|
||||
stopRainPacket.setType(LevelEvent.STOP_RAINING);
|
||||
stopRainPacket.setData(0);
|
||||
stopRainPacket.setPosition(Vector3f.ZERO);
|
||||
session.sendUpstreamPacket(stopRainPacket);
|
||||
session.setRaining(false);
|
||||
session.updateRain(0);
|
||||
break;
|
||||
case STOP_RAIN:
|
||||
LevelEventPacket startRainPacket = new LevelEventPacket();
|
||||
startRainPacket.setType(LevelEvent.START_RAINING);
|
||||
startRainPacket.setData(MAX_STORM_STRENGTH);
|
||||
startRainPacket.setPosition(Vector3f.ZERO);
|
||||
session.sendUpstreamPacket(startRainPacket);
|
||||
session.setRaining(true);
|
||||
session.updateRain(1);
|
||||
break;
|
||||
case RAIN_STRENGTH:
|
||||
float rainStrength = ((RainStrengthValue) packet.getValue()).getStrength();
|
||||
boolean isCurrentlyRaining = rainStrength > 0f;
|
||||
LevelEventPacket changeRainPacket = new LevelEventPacket();
|
||||
changeRainPacket.setType(isCurrentlyRaining ? LevelEvent.START_RAINING : LevelEvent.STOP_RAINING);
|
||||
// This is the rain strength on LevelEventType.START_RAINING, but can be any value on LevelEventType.STOP_RAINING
|
||||
changeRainPacket.setData((int) (rainStrength * MAX_STORM_STRENGTH));
|
||||
changeRainPacket.setPosition(Vector3f.ZERO);
|
||||
session.sendUpstreamPacket(changeRainPacket);
|
||||
session.setRaining(isCurrentlyRaining);
|
||||
float rainStrength = ((RainStrengthValue) packet.getValue()).getStrength();
|
||||
session.updateRain(rainStrength);
|
||||
break;
|
||||
case THUNDER_STRENGTH:
|
||||
// See above, same process
|
||||
float thunderStrength = ((ThunderStrengthValue) packet.getValue()).getStrength();
|
||||
boolean isCurrentlyThundering = thunderStrength > 0f;
|
||||
LevelEventPacket changeThunderPacket = new LevelEventPacket();
|
||||
changeThunderPacket.setType(isCurrentlyThundering ? LevelEvent.START_THUNDERSTORM : LevelEvent.STOP_THUNDERSTORM);
|
||||
changeThunderPacket.setData((int) (thunderStrength * MAX_STORM_STRENGTH));
|
||||
changeThunderPacket.setPosition(Vector3f.ZERO);
|
||||
session.sendUpstreamPacket(changeThunderPacket);
|
||||
session.setThunder(isCurrentlyThundering);
|
||||
session.updateThunder(thunderStrength);
|
||||
break;
|
||||
case CHANGE_GAMEMODE:
|
||||
GameMode gameMode = (GameMode) packet.getValue();
|
||||
|
@ -29,7 +29,12 @@ import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufAllocator;
|
||||
import io.netty.buffer.ByteBufOutputStream;
|
||||
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 org.cloudburstmc.math.vector.Vector3i;
|
||||
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.util.BlockEntityUtils;
|
||||
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.ChunkSection;
|
||||
import org.geysermc.mcprotocollib.protocol.data.game.chunk.DataPalette;
|
||||
@ -509,7 +513,7 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
|
||||
levelChunkPacket.setChunkX(packet.getX());
|
||||
levelChunkPacket.setChunkZ(packet.getZ());
|
||||
levelChunkPacket.setData(Unpooled.wrappedBuffer(payload));
|
||||
levelChunkPacket.setDimension(DimensionUtils.javaToBedrock(session.getBedrockDimension()));
|
||||
levelChunkPacket.setDimension(session.getBedrockDimension().bedrockId());
|
||||
session.sendUpstreamPacket(levelChunkPacket);
|
||||
|
||||
for (Map.Entry<Vector3i, ItemFrameEntity> entry : session.getItemFrameCache().entrySet()) {
|
||||
|
@ -32,40 +32,22 @@ import org.geysermc.geyser.session.GeyserSession;
|
||||
import org.geysermc.geyser.session.cache.WorldCache;
|
||||
import org.geysermc.geyser.translator.protocol.PacketTranslator;
|
||||
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;
|
||||
|
||||
@Translator(packet = ClientboundResetScorePacket.class)
|
||||
public class JavaResetScorePacket extends PacketTranslator<ClientboundResetScorePacket> {
|
||||
|
||||
@Override
|
||||
public void translate(GeyserSession session, ClientboundResetScorePacket packet) {
|
||||
WorldCache worldCache = session.getWorldCache();
|
||||
Scoreboard scoreboard = worldCache.getScoreboard();
|
||||
int pps = worldCache.increaseAndGetScoreboardPacketsPerSecond();
|
||||
|
||||
Objective belowName = scoreboard.getObjectiveSlots().get(ScoreboardPosition.BELOW_NAME);
|
||||
|
||||
if (packet.getObjective() == null) {
|
||||
// No objective name means all scores are reset for that player (/scoreboard players reset PLAYERNAME)
|
||||
for (Objective otherObjective : scoreboard.getObjectives()) {
|
||||
otherObjective.removeScore(packet.getOwner());
|
||||
}
|
||||
|
||||
// as described below
|
||||
if (belowName != null) {
|
||||
JavaSetScoreTranslator.setBelowName(session, belowName, packet.getOwner());
|
||||
}
|
||||
scoreboard.resetPlayerScores(packet.getOwner());
|
||||
} else {
|
||||
Objective objective = scoreboard.getObjective(packet.getObjective());
|
||||
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
|
||||
|
@ -25,72 +25,45 @@
|
||||
|
||||
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.Scoreboard;
|
||||
import org.geysermc.geyser.scoreboard.ScoreboardUpdater;
|
||||
import org.geysermc.geyser.scoreboard.UpdateType;
|
||||
import org.geysermc.geyser.session.GeyserSession;
|
||||
import org.geysermc.geyser.session.cache.WorldCache;
|
||||
import org.geysermc.geyser.translator.protocol.PacketTranslator;
|
||||
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)
|
||||
public class JavaSetObjectiveTranslator extends PacketTranslator<ClientboundSetObjectivePacket> {
|
||||
private final GeyserLogger logger = GeyserImpl.getInstance().getLogger();
|
||||
|
||||
@Override
|
||||
public void translate(GeyserSession session, ClientboundSetObjectivePacket packet) {
|
||||
WorldCache worldCache = session.getWorldCache();
|
||||
Scoreboard scoreboard = worldCache.getScoreboard();
|
||||
int pps = worldCache.increaseAndGetScoreboardPacketsPerSecond();
|
||||
|
||||
Objective objective = scoreboard.getObjective(packet.getName());
|
||||
if (objective != null && objective.getUpdateType() != UpdateType.REMOVE && packet.getAction() == ObjectiveAction.ADD) {
|
||||
// matches vanilla behaviour
|
||||
logger.warning("An objective with the same name '" + packet.getName() + "' already exists! Ignoring packet");
|
||||
Objective objective;
|
||||
if (packet.getAction() == ObjectiveAction.ADD) {
|
||||
objective = scoreboard.registerNewObjective(packet.getName());
|
||||
} else {
|
||||
objective = scoreboard.getObjective(packet.getName());
|
||||
}
|
||||
|
||||
// matches vanilla
|
||||
if (objective == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((objective == null || objective.getUpdateType() == UpdateType.REMOVE) && packet.getAction() != ObjectiveAction.REMOVE) {
|
||||
objective = scoreboard.registerNewObjective(packet.getName());
|
||||
}
|
||||
|
||||
switch (packet.getAction()) {
|
||||
case ADD, UPDATE -> {
|
||||
objective.setDisplayName(MessageTranslator.convertMessage(packet.getDisplayName()))
|
||||
.setNumberFormat(packet.getNumberFormat())
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
case ADD, UPDATE ->
|
||||
objective.updateProperties(packet.getDisplayName(), packet.getType(), packet.getNumberFormat());
|
||||
case REMOVE -> scoreboard.removeObjective(objective);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -25,23 +25,17 @@
|
||||
|
||||
package org.geysermc.geyser.translator.protocol.java.scoreboard;
|
||||
|
||||
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 java.util.Arrays;
|
||||
import org.geysermc.geyser.GeyserImpl;
|
||||
import org.geysermc.geyser.GeyserLogger;
|
||||
import org.geysermc.geyser.scoreboard.Scoreboard;
|
||||
import org.geysermc.geyser.scoreboard.ScoreboardUpdater;
|
||||
import org.geysermc.geyser.scoreboard.Team;
|
||||
import org.geysermc.geyser.scoreboard.UpdateType;
|
||||
import org.geysermc.geyser.session.GeyserSession;
|
||||
import org.geysermc.geyser.translator.protocol.PacketTranslator;
|
||||
import org.geysermc.geyser.translator.protocol.Translator;
|
||||
import org.geysermc.geyser.translator.text.MessageTranslator;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamAction;
|
||||
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetPlayerTeamPacket;
|
||||
|
||||
@Translator(packet = ClientboundSetPlayerTeamPacket.class)
|
||||
public class JavaSetPlayerTeamTranslator extends PacketTranslator<ClientboundSetPlayerTeamPacket> {
|
||||
@ -60,83 +54,45 @@ public class JavaSetPlayerTeamTranslator extends PacketTranslator<ClientboundSet
|
||||
int pps = session.getWorldCache().increaseAndGetScoreboardPacketsPerSecond();
|
||||
|
||||
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 ((team.getNameTagVisibility() != NameTagVisibility.ALWAYS && !team.isVisibleFor(session.getPlayerEntity().getUsername()))
|
||||
|| team.getColor() != TeamColor.RESET
|
||||
|| !team.getCurrentData().getPrefix().isEmpty()
|
||||
|| !team.getCurrentData().getSuffix().isEmpty()) {
|
||||
// Something is here that would modify entity names
|
||||
scoreboard.updateEntityNames(team, true);
|
||||
}
|
||||
if (packet.getAction() == TeamAction.CREATE) {
|
||||
scoreboard.registerNewTeam(
|
||||
packet.getTeamName(),
|
||||
packet.getPlayers(),
|
||||
packet.getDisplayName(),
|
||||
packet.getPrefix(),
|
||||
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();
|
||||
NameTagVisibility oldVisibility = team.getNameTagVisibility();
|
||||
String oldPrefix = team.getCurrentData().getPrefix();
|
||||
String oldSuffix = team.getCurrentData().getSuffix();
|
||||
|
||||
team.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()))
|
||||
.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);
|
||||
switch (packet.getAction()) {
|
||||
case UPDATE -> {
|
||||
team.updateProperties(
|
||||
packet.getDisplayName(),
|
||||
packet.getPrefix(),
|
||||
packet.getSuffix(),
|
||||
packet.getNameTagVisibility(),
|
||||
packet.getColor()
|
||||
);
|
||||
}
|
||||
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
|
||||
// (for score and team packets) is higher than the first threshold
|
||||
if (pps < ScoreboardUpdater.FIRST_SCORE_PACKETS_PER_SECOND_THRESHOLD) {
|
||||
|
@ -25,12 +25,8 @@
|
||||
|
||||
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.GeyserLogger;
|
||||
import org.geysermc.geyser.entity.type.player.PlayerEntity;
|
||||
import org.geysermc.geyser.scoreboard.Objective;
|
||||
import org.geysermc.geyser.scoreboard.Scoreboard;
|
||||
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.translator.protocol.PacketTranslator;
|
||||
import org.geysermc.geyser.translator.protocol.Translator;
|
||||
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetScorePacket;
|
||||
|
||||
@Translator(packet = ClientboundSetScorePacket.class)
|
||||
public class JavaSetScoreTranslator extends PacketTranslator<ClientboundSetScorePacket> {
|
||||
@ -63,16 +60,7 @@ public class JavaSetScoreTranslator extends PacketTranslator<ClientboundSetScore
|
||||
}
|
||||
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());
|
||||
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
|
||||
// (for score and team packets) is higher than the first threshold
|
||||
@ -80,36 +68,4 @@ public class JavaSetScoreTranslator extends PacketTranslator<ClientboundSetScore
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -25,11 +25,17 @@
|
||||
|
||||
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.JoinConfiguration;
|
||||
import net.kyori.adventure.text.ScoreComponent;
|
||||
import net.kyori.adventure.text.TranslatableComponent;
|
||||
import net.kyori.adventure.text.flattener.ComponentFlattener;
|
||||
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 net.kyori.adventure.text.renderer.TranslatableComponentRenderer;
|
||||
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
|
||||
import net.kyori.adventure.text.serializer.legacy.CharacterAndFormat;
|
||||
@ -39,14 +45,16 @@ import org.cloudburstmc.nbt.NbtMap;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.TextPacket;
|
||||
import org.geysermc.geyser.GeyserImpl;
|
||||
import org.geysermc.geyser.session.GeyserSession;
|
||||
import org.geysermc.geyser.text.*;
|
||||
import org.geysermc.geyser.text.ChatColor;
|
||||
import org.geysermc.geyser.text.ChatDecoration;
|
||||
import org.geysermc.geyser.text.DummyLegacyHoverEventSerializer;
|
||||
import org.geysermc.geyser.text.GeyserLocale;
|
||||
import org.geysermc.geyser.text.GsonComponentSerializerWrapper;
|
||||
import org.geysermc.geyser.text.MinecraftTranslationRegistry;
|
||||
import org.geysermc.mcprotocollib.protocol.data.DefaultComponentSerializer;
|
||||
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.ChatTypeDecoration;
|
||||
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class MessageTranslator {
|
||||
// These are used for handling the translations of the messages
|
||||
@ -59,9 +67,6 @@ public class MessageTranslator {
|
||||
private static final LegacyComponentSerializer BEDROCK_SERIALIZER;
|
||||
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
|
||||
private static final String BASE = "\u00a7";
|
||||
|
||||
@ -69,31 +74,6 @@ public class MessageTranslator {
|
||||
private static final String RESET = BASE + "r";
|
||||
|
||||
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
|
||||
GsonComponentSerializer source = DefaultComponentSerializer.get()
|
||||
.toBuilder()
|
||||
@ -145,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 locale Locale to use for translation strings
|
||||
* @return Parsed and formatted message for bedrock
|
||||
*/
|
||||
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 {
|
||||
// Translate any components that require it
|
||||
message = RENDERER.render(message, locale);
|
||||
@ -160,7 +158,7 @@ public class MessageTranslator {
|
||||
|
||||
StringBuilder finalLegacy = new StringBuilder();
|
||||
char[] legacyChars = legacy.toCharArray();
|
||||
boolean lastFormatReset = false;
|
||||
boolean lastFormatReset = !addLeadingResetFormat;
|
||||
for (int i = 0; i < legacyChars.length; i++) {
|
||||
char legacyChar = legacyChars[i];
|
||||
if (legacyChar != ChatColor.ESCAPE || i >= legacyChars.length - 1) {
|
||||
@ -173,7 +171,7 @@ public class MessageTranslator {
|
||||
|
||||
char next = legacyChars[++i];
|
||||
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) {
|
||||
finalLegacy.append(RESET);
|
||||
}
|
||||
@ -341,16 +339,16 @@ public class MessageTranslator {
|
||||
// Though, Bedrock cannot care about the signed stuff.
|
||||
TranslatableComponent.Builder withDecoration = Component.translatable()
|
||||
.key(chat.translationKey())
|
||||
.style(TextDecoration.getStyle(chat));
|
||||
.style(ChatDecoration.getStyle(chat));
|
||||
List<ChatTypeDecoration.Parameter> parameters = chat.parameters();
|
||||
List<Component> args = new ArrayList<>(3);
|
||||
if (parameters.contains(TextDecoration.Parameter.TARGET)) {
|
||||
if (parameters.contains(ChatDecoration.Parameter.TARGET)) {
|
||||
args.add(targetName);
|
||||
}
|
||||
if (parameters.contains(TextDecoration.Parameter.SENDER)) {
|
||||
if (parameters.contains(ChatDecoration.Parameter.SENDER)) {
|
||||
args.add(sender);
|
||||
}
|
||||
if (parameters.contains(TextDecoration.Parameter.CONTENT)) {
|
||||
if (parameters.contains(ChatDecoration.Parameter.CONTENT)) {
|
||||
args.add(message);
|
||||
}
|
||||
withDecoration.arguments(args);
|
||||
@ -366,16 +364,6 @@ public class MessageTranslator {
|
||||
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
|
||||
*
|
||||
@ -426,17 +414,92 @@ public class MessageTranslator {
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize an NbtMap provided from a registry into a string.
|
||||
* Deserialize an NbtMap with a description text component (usually provided from a registry) into a Bedrock-formatted string.
|
||||
*/
|
||||
// This may be a Component in the future.
|
||||
public static String deserializeDescription(NbtMap tag) {
|
||||
NbtMap description = tag.getCompound("description");
|
||||
String translate = description.getString("translate", null);
|
||||
if (translate == null) {
|
||||
GeyserImpl.getInstance().getLogger().debug("Don't know how to read description! " + tag);
|
||||
return "";
|
||||
public static String deserializeDescription(GeyserSession session, NbtMap tag) {
|
||||
Object description = tag.get("description");
|
||||
Component parsed = componentFromNbtTag(description);
|
||||
return convertMessage(session, parsed);
|
||||
}
|
||||
|
||||
public static Component componentFromNbtTag(Object nbtTag) {
|
||||
return componentFromNbtTag(nbtTag, Style.empty());
|
||||
}
|
||||
|
||||
private static Component componentFromNbtTag(Object nbtTag, Style style) {
|
||||
if (nbtTag instanceof String literal) {
|
||||
return Component.text(literal).style(style);
|
||||
} else if (nbtTag instanceof List<?> list) {
|
||||
return Component.join(JoinConfiguration.noSeparators(), componentsFromNbtList(list, style));
|
||||
} else if (nbtTag instanceof NbtMap map) {
|
||||
Component component = null;
|
||||
String text = map.getString("text", null);
|
||||
if (text != null) {
|
||||
component = Component.text(text);
|
||||
} else {
|
||||
String translateKey = map.getString("translate", null);
|
||||
if (translateKey != null) {
|
||||
String fallback = map.getString("fallback", "");
|
||||
List<Component> args = new ArrayList<>();
|
||||
|
||||
Object with = map.get("with");
|
||||
if (with instanceof List<?> list) {
|
||||
args = componentsFromNbtList(list, style);
|
||||
} else if (with != null) {
|
||||
args.add(componentFromNbtTag(with, style));
|
||||
}
|
||||
component = Component.translatable(translateKey, fallback, args);
|
||||
}
|
||||
}
|
||||
|
||||
if (component != null) {
|
||||
Style newStyle = getStyleFromNbtMap(map, style);
|
||||
component = component.style(newStyle);
|
||||
|
||||
Object extra = map.get("extra");
|
||||
if (extra != null) {
|
||||
component = component.append(componentFromNbtTag(extra, newStyle));
|
||||
}
|
||||
|
||||
return component;
|
||||
}
|
||||
}
|
||||
return translate;
|
||||
|
||||
GeyserImpl.getInstance().getLogger().error("Expected tag to be a literal string, a list of components, or a component object with a text/translate key: " + nbtTag);
|
||||
return Component.empty();
|
||||
}
|
||||
|
||||
private static List<Component> componentsFromNbtList(List<?> list, Style style) {
|
||||
List<Component> components = new ArrayList<>();
|
||||
for (Object entry : list) {
|
||||
components.add(componentFromNbtTag(entry, style));
|
||||
}
|
||||
return components;
|
||||
}
|
||||
|
||||
public static Style getStyleFromNbtMap(NbtMap map) {
|
||||
Style.Builder style = Style.style();
|
||||
|
||||
String colorString = map.getString("color", null);
|
||||
if (colorString != null) {
|
||||
if (colorString.startsWith(TextColor.HEX_PREFIX)) {
|
||||
style.color(TextColor.fromHexString(colorString));
|
||||
} else {
|
||||
style.color(NamedTextColor.NAMES.value(colorString));
|
||||
}
|
||||
}
|
||||
|
||||
map.listenForBoolean("bold", value -> style.decoration(TextDecoration.BOLD, value));
|
||||
map.listenForBoolean("italic", value -> style.decoration(TextDecoration.ITALIC, value));
|
||||
map.listenForBoolean("underlined", value -> style.decoration(TextDecoration.UNDERLINED, value));
|
||||
map.listenForBoolean("strikethrough", value -> style.decoration(TextDecoration.STRIKETHROUGH, value));
|
||||
map.listenForBoolean("obfuscated", value -> style.decoration(TextDecoration.OBFUSCATED, value));
|
||||
|
||||
return style.build();
|
||||
}
|
||||
|
||||
public static Style getStyleFromNbtMap(NbtMap map, Style base) {
|
||||
return base.merge(getStyleFromNbtMap(map));
|
||||
}
|
||||
|
||||
public static void init() {
|
||||
|
@ -167,7 +167,7 @@ public class ChunkUtils {
|
||||
byteBuf.readBytes(payload);
|
||||
|
||||
LevelChunkPacket data = new LevelChunkPacket();
|
||||
data.setDimension(DimensionUtils.javaToBedrock(session.getBedrockDimension()));
|
||||
data.setDimension(session.getBedrockDimension().bedrockId());
|
||||
data.setChunkX(chunkX);
|
||||
data.setChunkZ(chunkZ);
|
||||
data.setSubChunksLength(0);
|
||||
@ -207,13 +207,6 @@ public class ChunkUtils {
|
||||
int minY = dimension.minY();
|
||||
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();
|
||||
// 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
|
||||
|
@ -28,11 +28,9 @@ package org.geysermc.geyser.util;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.cloudburstmc.math.vector.Vector3f;
|
||||
import org.cloudburstmc.math.vector.Vector3i;
|
||||
import org.cloudburstmc.protocol.bedrock.data.LevelEvent;
|
||||
import org.cloudburstmc.protocol.bedrock.data.PlayerActionType;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.ChangeDimensionPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.ChunkRadiusUpdatedPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.LevelEventPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.MobEffectPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.PlayerActionPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.StopSoundPacket;
|
||||
@ -46,17 +44,8 @@ import java.util.Set;
|
||||
|
||||
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 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) {
|
||||
switchDimension(session, javaDimension, javaDimension.bedrockId());
|
||||
}
|
||||
@ -89,25 +78,15 @@ public class DimensionUtils {
|
||||
entityEffects.clear();
|
||||
|
||||
// Always reset weather, as it sometimes suddenly starts raining. See https://github.com/GeyserMC/Geyser/issues/3679
|
||||
LevelEventPacket stopRainPacket = new LevelEventPacket();
|
||||
stopRainPacket.setType(LevelEvent.STOP_RAINING);
|
||||
stopRainPacket.setData(0);
|
||||
stopRainPacket.setPosition(Vector3f.ZERO);
|
||||
session.sendUpstreamPacket(stopRainPacket);
|
||||
session.setRaining(false);
|
||||
LevelEventPacket stopThunderPacket = new LevelEventPacket();
|
||||
stopThunderPacket.setType(LevelEvent.STOP_THUNDERSTORM);
|
||||
stopThunderPacket.setData(0);
|
||||
stopThunderPacket.setPosition(Vector3f.ZERO);
|
||||
session.sendUpstreamPacket(stopThunderPacket);
|
||||
session.setThunder(false);
|
||||
session.updateRain(0);
|
||||
session.updateThunder(0);
|
||||
|
||||
finalizeDimensionSwitch(session, player);
|
||||
|
||||
// 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
|
||||
// thinks they are in the end dimension.
|
||||
if (isCustomBedrockNetherId()) {
|
||||
if (BedrockDimension.isCustomBedrockNetherId()) {
|
||||
if (javaDimension.isNetherLike()) {
|
||||
session.camera().sendFog(BEDROCK_FOG_HELL);
|
||||
} else if (previousDimension != null && previousDimension.isNetherLike()) {
|
||||
@ -180,22 +159,12 @@ public class DimensionUtils {
|
||||
|
||||
public static void setBedrockDimension(GeyserSession session, int bedrockDimension) {
|
||||
session.setBedrockDimension(switch (bedrockDimension) {
|
||||
case BEDROCK_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.
|
||||
default -> BedrockDimension.OVERWORLD;
|
||||
case BedrockDimension.END_ID -> BedrockDimension.THE_END;
|
||||
case BedrockDimension.DEFAULT_NETHER_ID -> BedrockDimension.THE_NETHER; // JavaDimension *should* be set to BEDROCK_END_ID if the Nether workaround is enabled.
|
||||
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
|
||||
*
|
||||
@ -204,9 +173,9 @@ public class DimensionUtils {
|
||||
*/
|
||||
public static int javaToBedrock(String javaDimension) {
|
||||
return switch (javaDimension) {
|
||||
case NETHER_IDENTIFIER -> BEDROCK_NETHER_ID;
|
||||
case "minecraft:the_end" -> 2;
|
||||
default -> 0;
|
||||
case BedrockDimension.NETHER_IDENTIFIER -> BedrockDimension.BEDROCK_NETHER_ID;
|
||||
case "minecraft:the_end" -> BedrockDimension.END_ID;
|
||||
default -> BedrockDimension.OVERWORLD_ID;
|
||||
};
|
||||
}
|
||||
|
||||
@ -216,22 +185,11 @@ public class DimensionUtils {
|
||||
public static int javaToBedrock(GeyserSession session) {
|
||||
JavaDimension dimension = session.getDimensionType();
|
||||
if (dimension == null) {
|
||||
return BEDROCK_OVERWORLD_ID;
|
||||
return BedrockDimension.OVERWORLD_ID;
|
||||
}
|
||||
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
|
||||
* dimension switch.
|
||||
@ -241,16 +199,13 @@ public class DimensionUtils {
|
||||
* @return the Bedrock fake dimension to transfer to
|
||||
*/
|
||||
public static int getTemporaryDimension(int currentBedrockDimension, int newBedrockDimension) {
|
||||
if (isCustomBedrockNetherId()) {
|
||||
if (BedrockDimension.isCustomBedrockNetherId()) {
|
||||
// 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.
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,10 @@
|
||||
|
||||
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.protocol.bedrock.data.GameType;
|
||||
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.inventory.GeyserItemStack;
|
||||
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.player.GameMode;
|
||||
import org.geysermc.mcprotocollib.protocol.data.game.entity.player.Hand;
|
||||
import org.geysermc.mcprotocollib.protocol.data.game.entity.type.EntityType;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public final class EntityUtils {
|
||||
/**
|
||||
* 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() {
|
||||
}
|
||||
}
|
||||
|
41
core/src/main/java/org/geysermc/geyser/util/EnvironmentUtils.java
Normale Datei
41
core/src/main/java/org/geysermc/geyser/util/EnvironmentUtils.java
Normale Datei
@ -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;
|
||||
}
|
||||
}
|
@ -32,6 +32,8 @@ import org.cloudburstmc.nbt.NbtMapBuilder;
|
||||
import org.cloudburstmc.nbt.NbtType;
|
||||
import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerId;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerSlotType;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.FullContainerName;
|
||||
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.InventorySlotPacket;
|
||||
import org.cloudburstmc.protocol.bedrock.packet.PlayerHotbarPacket;
|
||||
@ -180,6 +182,7 @@ public class InventoryUtils {
|
||||
cursorPacket.setContainerId(ContainerId.UI);
|
||||
cursorPacket.setSlot(0);
|
||||
cursorPacket.setItem(session.getPlayerInventory().getCursor().getItemData(session));
|
||||
cursorPacket.setContainerNameData(new FullContainerName(ContainerSlotType.ANVIL_INPUT, null));
|
||||
session.sendUpstreamPacket(cursorPacket);
|
||||
}
|
||||
|
||||
|
BIN
core/src/main/resources/bedrock/block_palette.1_21_30.nbt
Normale Datei
BIN
core/src/main/resources/bedrock/block_palette.1_21_30.nbt
Normale Datei
Binäre Datei nicht angezeigt.
6214
core/src/main/resources/bedrock/creative_items.1_21_30.json
Normale Datei
6214
core/src/main/resources/bedrock/creative_items.1_21_30.json
Normale Datei
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
6898
core/src/main/resources/bedrock/runtime_item_states.1_21_30.json
Normale Datei
6898
core/src/main/resources/bedrock/runtime_item_states.1_21_30.json
Normale Datei
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
@ -1 +1 @@
|
||||
Subproject commit 698fd2b108a9e53f1e47b8cfdc122651b70d6059
|
||||
Subproject commit 93f207e7e9d73f58a7c8902f7deda9dcb0524c8e
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -11,12 +11,12 @@ netty-io-uring = "0.0.25.Final-SNAPSHOT"
|
||||
guava = "29.0-jre"
|
||||
gson = "2.3.1" # Provided by Spigot 1.8.8
|
||||
websocket = "1.5.1"
|
||||
protocol-connection = "3.0.0.Beta4-20240828.162251-1"
|
||||
protocol-common = "3.0.0.Beta4-20240828.162251-1"
|
||||
protocol-codec = "3.0.0.Beta4-20240828.162251-1"
|
||||
protocol-connection = "3.0.0.Beta5-20240916.181041-6"
|
||||
protocol-common = "3.0.0.Beta5-20240916.181041-6"
|
||||
protocol-codec = "3.0.0.Beta5-20240916.181041-6"
|
||||
raknet = "1.0.0.CR3-20240416.144209-1"
|
||||
minecraftauth = "4.1.1-20240806.235051-7"
|
||||
mcprotocollib = "1.21-20240725.013034-16"
|
||||
minecraftauth = "4.1.1"
|
||||
mcprotocollib = "1.21-20241010.155958-24"
|
||||
adventure = "4.14.0"
|
||||
adventure-platform = "4.3.0"
|
||||
junit = "5.9.2"
|
||||
@ -40,6 +40,7 @@ neoforge-minecraft = "21.1.1"
|
||||
mixin = "0.8.5"
|
||||
mixinextras = "0.3.5"
|
||||
minecraft = "1.21.1"
|
||||
mockito = "5.+"
|
||||
|
||||
# plugin versions
|
||||
indra = "3.1.3"
|
||||
@ -140,6 +141,8 @@ protocol-connection = { group = "org.cloudburstmc.protocol", name = "bedrock-con
|
||||
|
||||
math = { group = "org.cloudburstmc.math", name = "immutable", version = "2.0" }
|
||||
|
||||
mockito = { module = "org.mockito:mockito-core", version.ref = "mockito" }
|
||||
|
||||
# plugins
|
||||
lombok = { group = "io.freefair.gradle", name = "lombok-plugin", version.ref = "lombok" }
|
||||
indra = { group = "net.kyori", name = "indra-common", version.ref = "indra" }
|
||||
|
Laden…
x
In neuem Issue referenzieren
Einen Benutzer sperren