mirror of
https://github.com/GeyserMC/Geyser.git
synced 2024-08-14 23:57:35 +00:00
Merge a5345b37fb
into 8f7d512073
This commit is contained in:
commit
9adab9c349
24 changed files with 1784 additions and 1020 deletions
|
@ -60,7 +60,6 @@ import java.util.*;
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
public class Entity implements GeyserEntity {
|
public class Entity implements GeyserEntity {
|
||||||
|
|
||||||
private static final boolean PRINT_ENTITY_SPAWN_DEBUG = Boolean.parseBoolean(System.getProperty("Geyser.PrintEntitySpawnDebug", "false"));
|
private static final boolean PRINT_ENTITY_SPAWN_DEBUG = Boolean.parseBoolean(System.getProperty("Geyser.PrintEntitySpawnDebug", "false"));
|
||||||
|
|
||||||
protected final GeyserSession session;
|
protected final GeyserSession session;
|
||||||
|
@ -68,6 +67,12 @@ public class Entity implements GeyserEntity {
|
||||||
protected int entityId;
|
protected int entityId;
|
||||||
protected final long geyserId;
|
protected final long geyserId;
|
||||||
protected UUID uuid;
|
protected UUID uuid;
|
||||||
|
/**
|
||||||
|
* Do not call this setter directly!
|
||||||
|
* This will bypass the scoreboard and setting the metadata
|
||||||
|
*/
|
||||||
|
@Setter(AccessLevel.NONE)
|
||||||
|
protected String nametag = "";
|
||||||
|
|
||||||
protected Vector3f position;
|
protected Vector3f position;
|
||||||
protected Vector3f motion;
|
protected Vector3f motion;
|
||||||
|
@ -97,7 +102,7 @@ public class Entity implements GeyserEntity {
|
||||||
@Setter(AccessLevel.NONE)
|
@Setter(AccessLevel.NONE)
|
||||||
private float boundingBoxWidth;
|
private float boundingBoxWidth;
|
||||||
@Setter(AccessLevel.NONE)
|
@Setter(AccessLevel.NONE)
|
||||||
protected String nametag = "";
|
private String displayName = "";
|
||||||
@Setter(AccessLevel.NONE)
|
@Setter(AccessLevel.NONE)
|
||||||
protected boolean silent = false;
|
protected boolean silent = false;
|
||||||
/* Metadata end */
|
/* Metadata end */
|
||||||
|
@ -411,16 +416,39 @@ public class Entity implements GeyserEntity {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDisplayName(EntityMetadata<Optional<Component>, ?> entityMetadata) {
|
public void setDisplayName(EntityMetadata<Optional<Component>, ?> entityMetadata) {
|
||||||
|
// the difference between displayName and nametag aren't important for non-living entities and for players,
|
||||||
|
// but the displayName is needed for living entities that are part of a scoreboard team.
|
||||||
|
// For them the nametag is prefix + displayName + suffix
|
||||||
Optional<Component> name = entityMetadata.getValue();
|
Optional<Component> name = entityMetadata.getValue();
|
||||||
if (name.isPresent()) {
|
if (name.isPresent()) {
|
||||||
nametag = MessageTranslator.convertMessage(name.get(), session.locale());
|
var displayName = MessageTranslator.convertMessage(name.get(), session.locale());
|
||||||
|
this.displayName = displayName;
|
||||||
|
setNametag(displayName, true);
|
||||||
|
} else {
|
||||||
|
this.displayName = "";
|
||||||
|
setNametag(null, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setNametag(@Nullable String nametag, boolean fromDisplayName) {
|
||||||
|
var hide = nametag == null;
|
||||||
|
if (hide) {
|
||||||
|
nametag = "";
|
||||||
|
}
|
||||||
|
var 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);
|
dirtyMetadata.put(EntityDataTypes.NAME, nametag);
|
||||||
} else if (!nametag.isEmpty()) {
|
// if nametag (player with team) is hidden for player, so should the score (belowname)
|
||||||
// Clear nametag
|
scoreVisibility(!hide);
|
||||||
dirtyMetadata.put(EntityDataTypes.NAME, "");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void scoreVisibility(boolean show) {}
|
||||||
|
|
||||||
public void setDisplayNameVisible(BooleanEntityMetadata entityMetadata) {
|
public void setDisplayNameVisible(BooleanEntityMetadata entityMetadata) {
|
||||||
dirtyMetadata.put(EntityDataTypes.NAMETAG_ALWAYS_SHOW, (byte) (entityMetadata.getPrimitiveValue() ? 1 : 0));
|
dirtyMetadata.put(EntityDataTypes.NAMETAG_ALWAYS_SHOW, (byte) (entityMetadata.getPrimitiveValue() ? 1 : 0));
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,8 +44,10 @@ import org.geysermc.geyser.entity.attribute.GeyserAttributeType;
|
||||||
import org.geysermc.geyser.inventory.GeyserItemStack;
|
import org.geysermc.geyser.inventory.GeyserItemStack;
|
||||||
import org.geysermc.geyser.item.Items;
|
import org.geysermc.geyser.item.Items;
|
||||||
import org.geysermc.geyser.registry.type.ItemMapping;
|
import org.geysermc.geyser.registry.type.ItemMapping;
|
||||||
|
import org.geysermc.geyser.scoreboard.Team;
|
||||||
import org.geysermc.geyser.session.GeyserSession;
|
import org.geysermc.geyser.session.GeyserSession;
|
||||||
import org.geysermc.geyser.translator.item.ItemTranslator;
|
import org.geysermc.geyser.translator.item.ItemTranslator;
|
||||||
|
import org.geysermc.geyser.translator.text.MessageTranslator;
|
||||||
import org.geysermc.geyser.util.AttributeUtils;
|
import org.geysermc.geyser.util.AttributeUtils;
|
||||||
import org.geysermc.geyser.util.InteractionResult;
|
import org.geysermc.geyser.util.InteractionResult;
|
||||||
import org.geysermc.geyser.util.MathUtils;
|
import org.geysermc.geyser.util.MathUtils;
|
||||||
|
@ -65,11 +67,11 @@ import org.geysermc.mcprotocollib.protocol.data.game.level.particle.Particle;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.level.particle.ParticleType;
|
import org.geysermc.mcprotocollib.protocol.data.game.level.particle.ParticleType;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
public class LivingEntity extends Entity {
|
public class LivingEntity extends Entity {
|
||||||
|
|
||||||
protected ItemData helmet = ItemData.AIR;
|
protected ItemData helmet = ItemData.AIR;
|
||||||
protected ItemData chestplate = ItemData.AIR;
|
protected ItemData chestplate = ItemData.AIR;
|
||||||
protected ItemData leggings = ItemData.AIR;
|
protected ItemData leggings = ItemData.AIR;
|
||||||
|
@ -149,6 +151,45 @@ public class LivingEntity extends Entity {
|
||||||
dirtyMetadata.put(EntityDataTypes.STRUCTURAL_INTEGRITY, 1);
|
dirtyMetadata.put(EntityDataTypes.STRUCTURAL_INTEGRITY, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void updateNametag(@Nullable Team team) {
|
||||||
|
if (team != null) {
|
||||||
|
String newNametag;
|
||||||
|
if (team.isVisibleFor(session.getPlayerEntity().getUsername())) {
|
||||||
|
TeamColor color = team.color();
|
||||||
|
String chatColor = MessageTranslator.toChatColor(color);
|
||||||
|
// We have to emulate what modern Java text already does for us and add the color to each section
|
||||||
|
newNametag = chatColor + team.prefix() + chatColor + getDisplayName() + chatColor + team.suffix();
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void hideNametag() {
|
||||||
|
setNametag("", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String teamIdentifier() {
|
||||||
|
return uuid.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setNametag(@Nullable String nametag, boolean fromDisplayName) {
|
||||||
|
if (nametag != null && fromDisplayName) {
|
||||||
|
var team = session.getWorldCache().getScoreboard().getTeamFor(teamIdentifier());
|
||||||
|
if (team != null) {
|
||||||
|
updateNametag(team);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.setNametag(nametag, fromDisplayName);
|
||||||
|
}
|
||||||
|
|
||||||
public void setLivingEntityFlags(ByteEntityMetadata entityMetadata) {
|
public void setLivingEntityFlags(ByteEntityMetadata entityMetadata) {
|
||||||
byte xd = entityMetadata.getPrimitiveValue();
|
byte xd = entityMetadata.getPrimitiveValue();
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,6 @@ import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
import org.cloudburstmc.math.vector.Vector3f;
|
import org.cloudburstmc.math.vector.Vector3f;
|
||||||
import org.cloudburstmc.math.vector.Vector3i;
|
import org.cloudburstmc.math.vector.Vector3i;
|
||||||
import org.cloudburstmc.nbt.NbtMap;
|
import org.cloudburstmc.nbt.NbtMap;
|
||||||
import org.cloudburstmc.nbt.NbtMapBuilder;
|
|
||||||
import org.cloudburstmc.protocol.bedrock.data.Ability;
|
import org.cloudburstmc.protocol.bedrock.data.Ability;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.AbilityLayer;
|
import org.cloudburstmc.protocol.bedrock.data.AbilityLayer;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.GameType;
|
import org.cloudburstmc.protocol.bedrock.data.GameType;
|
||||||
|
@ -49,26 +48,13 @@ import org.geysermc.geyser.entity.attribute.GeyserAttributeType;
|
||||||
import org.geysermc.geyser.entity.type.Entity;
|
import org.geysermc.geyser.entity.type.Entity;
|
||||||
import org.geysermc.geyser.entity.type.LivingEntity;
|
import org.geysermc.geyser.entity.type.LivingEntity;
|
||||||
import org.geysermc.geyser.entity.type.living.animal.tameable.ParrotEntity;
|
import org.geysermc.geyser.entity.type.living.animal.tameable.ParrotEntity;
|
||||||
import org.geysermc.geyser.scoreboard.Objective;
|
|
||||||
import org.geysermc.geyser.scoreboard.Score;
|
|
||||||
import org.geysermc.geyser.scoreboard.Team;
|
|
||||||
import org.geysermc.geyser.scoreboard.UpdateType;
|
|
||||||
import org.geysermc.geyser.session.GeyserSession;
|
import org.geysermc.geyser.session.GeyserSession;
|
||||||
import org.geysermc.geyser.text.ChatColor;
|
|
||||||
import org.geysermc.geyser.translator.text.MessageTranslator;
|
|
||||||
import org.geysermc.geyser.util.ChunkUtils;
|
import org.geysermc.geyser.util.ChunkUtils;
|
||||||
import org.geysermc.mcprotocollib.protocol.codec.NbtComponentSerializer;
|
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.BlankFormat;
|
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.FixedFormat;
|
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.NumberFormat;
|
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.StyledFormat;
|
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata;
|
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.EntityMetadata;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose;
|
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.Pose;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
|
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.BooleanEntityMetadata;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
|
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.ByteEntityMetadata;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.FloatEntityMetadata;
|
import org.geysermc.mcprotocollib.protocol.data.game.entity.metadata.type.FloatEntityMetadata;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
|
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -92,6 +78,9 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
|
||||||
|
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
|
private String cachedScore;
|
||||||
|
private boolean scoreVisible = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The textures property from the GameProfile.
|
* The textures property from the GameProfile.
|
||||||
*/
|
*/
|
||||||
|
@ -128,17 +117,6 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void spawnEntity() {
|
public void spawnEntity() {
|
||||||
// Check to see if the player should have a belowname counterpart added
|
|
||||||
Objective objective = session.getWorldCache().getScoreboard().getObjectiveSlots().get(ScoreboardPosition.BELOW_NAME);
|
|
||||||
if (objective != null) {
|
|
||||||
setBelowNameText(objective);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update in case this entity has been despawned, then respawned
|
|
||||||
this.nametag = this.username;
|
|
||||||
// The name can't be updated later (the entity metadata for it is ignored), so we need to check for this now
|
|
||||||
updateDisplayName(session.getWorldCache().getScoreboard().getTeamFor(username));
|
|
||||||
|
|
||||||
AddPlayerPacket addPlayerPacket = new AddPlayerPacket();
|
AddPlayerPacket addPlayerPacket = new AddPlayerPacket();
|
||||||
addPlayerPacket.setUuid(uuid);
|
addPlayerPacket.setUuid(uuid);
|
||||||
addPlayerPacket.setUsername(username);
|
addPlayerPacket.setUsername(username);
|
||||||
|
@ -173,6 +151,7 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
|
||||||
|
|
||||||
// Since we re-use player entities: Clear flags, held item, etc
|
// Since we re-use player entities: Clear flags, held item, etc
|
||||||
this.resetMetadata();
|
this.resetMetadata();
|
||||||
|
this.nametag = username;
|
||||||
this.hand = ItemData.AIR;
|
this.hand = ItemData.AIR;
|
||||||
this.offhand = ItemData.AIR;
|
this.offhand = ItemData.AIR;
|
||||||
this.boots = ItemData.AIR;
|
this.boots = ItemData.AIR;
|
||||||
|
@ -376,38 +355,30 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDisplayName() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setDisplayName(EntityMetadata<Optional<Component>, ?> entityMetadata) {
|
public void setDisplayName(EntityMetadata<Optional<Component>, ?> entityMetadata) {
|
||||||
// Doesn't do anything for players
|
// Doesn't do anything for players
|
||||||
}
|
}
|
||||||
|
|
||||||
//todo this will become common entity logic once UUID support is implemented for them
|
@Override
|
||||||
public void updateDisplayName(@Nullable Team team) {
|
public String teamIdentifier() {
|
||||||
boolean needsUpdate;
|
return username;
|
||||||
if (team != null) {
|
|
||||||
String newDisplayName;
|
|
||||||
if (team.isVisibleFor(session.getPlayerEntity().getUsername())) {
|
|
||||||
TeamColor color = team.getColor();
|
|
||||||
String chatColor = MessageTranslator.toChatColor(color);
|
|
||||||
// We have to emulate what modern Java text already does for us and add the color to each section
|
|
||||||
String prefix = team.getCurrentData().getPrefix();
|
|
||||||
String suffix = team.getCurrentData().getSuffix();
|
|
||||||
newDisplayName = chatColor + prefix + chatColor + this.username + chatColor + suffix;
|
|
||||||
} else {
|
|
||||||
// The name is not visible to the session player; clear name
|
|
||||||
newDisplayName = "";
|
|
||||||
}
|
|
||||||
needsUpdate = !newDisplayName.equals(this.nametag);
|
|
||||||
this.nametag = newDisplayName;
|
|
||||||
} else {
|
|
||||||
// The name has reset, if it was previously something else
|
|
||||||
needsUpdate = !this.nametag.equals(this.username);
|
|
||||||
this.nametag = this.username;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needsUpdate) {
|
@Override
|
||||||
dirtyMetadata.put(EntityDataTypes.NAME, this.nametag);
|
protected void setNametag(@Nullable String nametag, boolean fromDisplayName) {
|
||||||
|
// when fromDisplayName, LivingEntity will call scoreboard code. After that
|
||||||
|
// setNametag is called again with fromDisplayName on false
|
||||||
|
if (nametag == null && !fromDisplayName) {
|
||||||
|
// nametag = null means reset, so reset it back to username
|
||||||
|
nametag = username;
|
||||||
}
|
}
|
||||||
|
super.setNametag(nametag, fromDisplayName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -415,6 +386,26 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
|
||||||
// Doesn't do anything for players
|
// Doesn't do anything for players
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setBelowNameText(String text) {
|
||||||
|
if (text == null) {
|
||||||
|
text = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedScore = text;
|
||||||
|
if (scoreVisible) {
|
||||||
|
dirtyMetadata.put(EntityDataTypes.SCORE, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void scoreVisibility(boolean show) {
|
||||||
|
var changed = scoreVisible != show;
|
||||||
|
scoreVisible = show;
|
||||||
|
if (changed) {
|
||||||
|
dirtyMetadata.put(EntityDataTypes.SCORE, show ? cachedScore : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void setDimensions(Pose pose) {
|
protected void setDimensions(Pose pose) {
|
||||||
float height;
|
float height;
|
||||||
|
@ -441,64 +432,6 @@ public class PlayerEntity extends LivingEntity implements GeyserPlayerEntity {
|
||||||
setBoundingBoxHeight(height);
|
setBoundingBoxHeight(height);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setBelowNameText(Objective objective) {
|
|
||||||
if (objective != null && objective.getUpdateType() != UpdateType.REMOVE) {
|
|
||||||
Score score = objective.getScores().get(username);
|
|
||||||
String numberString;
|
|
||||||
NumberFormat numberFormat;
|
|
||||||
int amount;
|
|
||||||
if (score != null) {
|
|
||||||
amount = score.getScore();
|
|
||||||
numberFormat = score.getNumberFormat();
|
|
||||||
if (numberFormat == null) {
|
|
||||||
numberFormat = objective.getNumberFormat();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
amount = 0;
|
|
||||||
numberFormat = objective.getNumberFormat();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (numberFormat instanceof BlankFormat) {
|
|
||||||
numberString = "";
|
|
||||||
} else if (numberFormat instanceof FixedFormat fixedFormat) {
|
|
||||||
numberString = MessageTranslator.convertMessage(fixedFormat.getValue());
|
|
||||||
} else if (numberFormat instanceof StyledFormat styledFormat) {
|
|
||||||
NbtMapBuilder styledAmount = styledFormat.getStyle().toBuilder();
|
|
||||||
styledAmount.putString("text", String.valueOf(amount));
|
|
||||||
|
|
||||||
numberString = MessageTranslator.convertJsonMessage(
|
|
||||||
NbtComponentSerializer.tagComponentToJson(styledAmount.build()).toString(), session.locale());
|
|
||||||
} else {
|
|
||||||
numberString = String.valueOf(amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
String displayString = numberString + " " + ChatColor.RESET + objective.getDisplayName();
|
|
||||||
|
|
||||||
if (valid) {
|
|
||||||
// Already spawned - we still need to run the rest of this code because the spawn packet will be
|
|
||||||
// providing the information
|
|
||||||
SetEntityDataPacket packet = new SetEntityDataPacket();
|
|
||||||
packet.setRuntimeEntityId(geyserId);
|
|
||||||
packet.getMetadata().put(EntityDataTypes.SCORE, displayString);
|
|
||||||
session.sendUpstreamPacket(packet);
|
|
||||||
} else {
|
|
||||||
// Not spawned yet, store score value in dirtyMetadata to be picked up by #spawnEntity
|
|
||||||
dirtyMetadata.put(EntityDataTypes.SCORE, displayString);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (valid) {
|
|
||||||
SetEntityDataPacket packet = new SetEntityDataPacket();
|
|
||||||
packet.setRuntimeEntityId(geyserId);
|
|
||||||
packet.getMetadata().put(EntityDataTypes.SCORE, "");
|
|
||||||
session.sendUpstreamPacket(packet);
|
|
||||||
} else {
|
|
||||||
// Not spawned yet, store score value in dirtyMetadata to be picked up by #spawnEntity
|
|
||||||
dirtyMetadata.put(EntityDataTypes.SCORE, "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the UUID that should be used when dealing with Bedrock's tab list.
|
* @return the UUID that should be used when dealing with Bedrock's tab list.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -25,185 +25,100 @@
|
||||||
|
|
||||||
package org.geysermc.geyser.scoreboard;
|
package org.geysermc.geyser.scoreboard;
|
||||||
|
|
||||||
import lombok.Getter;
|
import java.util.ArrayList;
|
||||||
import lombok.Setter;
|
import java.util.List;
|
||||||
import net.kyori.adventure.text.Component;
|
|
||||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.NumberFormat;
|
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
|
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
|
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import lombok.Getter;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import org.geysermc.geyser.scoreboard.display.slot.DisplaySlot;
|
||||||
|
import org.geysermc.geyser.translator.text.MessageTranslator;
|
||||||
|
import org.geysermc.mcprotocollib.protocol.data.game.chat.numbers.NumberFormat;
|
||||||
|
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreType;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
public final class Objective {
|
public final class Objective {
|
||||||
private final Scoreboard scoreboard;
|
private final Scoreboard scoreboard;
|
||||||
private final long id;
|
private final List<DisplaySlot> activeSlots = new ArrayList<>();
|
||||||
private boolean active = true;
|
|
||||||
|
|
||||||
@Setter
|
private final String objectiveName;
|
||||||
private UpdateType updateType = UpdateType.ADD;
|
private final Map<String, ScoreReference> scores = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private String objectiveName;
|
private String displayName;
|
||||||
private ScoreboardPosition displaySlot;
|
|
||||||
private String displaySlotName;
|
|
||||||
private String displayName = "unknown";
|
|
||||||
private NumberFormat numberFormat;
|
private NumberFormat numberFormat;
|
||||||
private int type = 0; // 0 = integer, 1 = heart
|
private ScoreType type;
|
||||||
|
|
||||||
private Map<String, Score> scores = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
private Objective(Scoreboard scoreboard) {
|
|
||||||
this.id = scoreboard.getNextId().getAndIncrement();
|
|
||||||
this.scoreboard = scoreboard;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* /!\ This method is made for temporary objectives until the real objective is received
|
|
||||||
*
|
|
||||||
* @param scoreboard the scoreboard
|
|
||||||
* @param objectiveName the name of the objective
|
|
||||||
*/
|
|
||||||
public Objective(Scoreboard scoreboard, String objectiveName) {
|
public Objective(Scoreboard scoreboard, String objectiveName) {
|
||||||
this(scoreboard);
|
this.scoreboard = scoreboard;
|
||||||
this.objectiveName = objectiveName;
|
this.objectiveName = objectiveName;
|
||||||
this.active = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Objective(Scoreboard scoreboard, String objectiveName, ScoreboardPosition displaySlot, String displayName, int type) {
|
|
||||||
this(scoreboard);
|
|
||||||
this.objectiveName = objectiveName;
|
|
||||||
this.displaySlot = displaySlot;
|
|
||||||
this.displaySlotName = translateDisplaySlot(displaySlot);
|
|
||||||
this.displayName = displayName;
|
|
||||||
this.type = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String translateDisplaySlot(ScoreboardPosition displaySlot) {
|
|
||||||
return switch (displaySlot) {
|
|
||||||
case BELOW_NAME -> "belowname";
|
|
||||||
case PLAYER_LIST -> "list";
|
|
||||||
default -> "sidebar";
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void registerScore(String id, int score, Component displayName, NumberFormat numberFormat) {
|
public void registerScore(String id, int score, Component displayName, NumberFormat numberFormat) {
|
||||||
if (!scores.containsKey(id)) {
|
if (scores.containsKey(id)) {
|
||||||
long scoreId = scoreboard.getNextId().getAndIncrement();
|
return;
|
||||||
Score scoreObject = new Score(scoreId, id)
|
}
|
||||||
.setScore(score)
|
var reference = new ScoreReference(scoreboard, id, score, displayName, numberFormat);
|
||||||
.setTeam(scoreboard.getTeamFor(id))
|
scores.put(id, reference);
|
||||||
.setDisplayName(displayName)
|
|
||||||
.setNumberFormat(numberFormat)
|
for (var slot : activeSlots) {
|
||||||
.setUpdateType(UpdateType.ADD);
|
slot.addScore(reference);
|
||||||
scores.put(id, scoreObject);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setScore(String id, int score, Component displayName, NumberFormat numberFormat) {
|
public void setScore(String id, int score, Component displayName, NumberFormat numberFormat) {
|
||||||
Score stored = scores.get(id);
|
ScoreReference stored = scores.get(id);
|
||||||
if (stored != null) {
|
if (stored != null) {
|
||||||
stored.setScore(score)
|
stored.updateProperties(scoreboard, score, displayName, numberFormat);
|
||||||
.setDisplayName(displayName)
|
|
||||||
.setNumberFormat(numberFormat)
|
|
||||||
.setUpdateType(UpdateType.UPDATE);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
registerScore(id, score, displayName, numberFormat);
|
registerScore(id, score, displayName, numberFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeScore(String id) {
|
public void removeScore(String id) {
|
||||||
Score stored = scores.get(id);
|
ScoreReference stored = scores.remove(id);
|
||||||
if (stored != null) {
|
if (stored != null) {
|
||||||
stored.setUpdateType(UpdateType.REMOVE);
|
stored.markDeleted();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public void updateProperties(Component displayNameComponent, ScoreType type, NumberFormat format) {
|
||||||
* Used internally to remove a score from the score map
|
var displayName = MessageTranslator.convertMessage(displayNameComponent, scoreboard.session().locale());
|
||||||
*/
|
var changed = !Objects.equals(this.displayName, displayName) || this.type != type;
|
||||||
public void removeScore0(String id) {
|
|
||||||
scores.remove(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Objective setDisplayName(String displayName) {
|
|
||||||
this.displayName = displayName;
|
this.displayName = displayName;
|
||||||
if (updateType == UpdateType.NOTHING) {
|
|
||||||
updateType = UpdateType.UPDATE;
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Objective setType(int type) {
|
|
||||||
this.type = type;
|
this.type = type;
|
||||||
if (updateType == UpdateType.NOTHING) {
|
|
||||||
updateType = UpdateType.UPDATE;
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setActive(ScoreboardPosition displaySlot) {
|
if (!Objects.equals(this.numberFormat, format)) {
|
||||||
if (!active) {
|
this.numberFormat = format;
|
||||||
active = true;
|
// update the number format for scores that are following this objective's number format,
|
||||||
this.displaySlot = displaySlot;
|
// but only if the objective itself doesn't need to be updated.
|
||||||
displaySlotName = translateDisplaySlot(displaySlot);
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
if (changed) {
|
||||||
* The objective will be removed on the next update
|
for (DisplaySlot slot : activeSlots) {
|
||||||
*/
|
slot.markNeedsUpdate();
|
||||||
public void pendingRemove() {
|
}
|
||||||
updateType = UpdateType.REMOVE;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public @Nullable TeamColor getTeamColor() {
|
public boolean hasDisplaySlot() {
|
||||||
return switch (displaySlot) {
|
return !activeSlots.isEmpty();
|
||||||
case SIDEBAR_TEAM_RED -> TeamColor.RED;
|
|
||||||
case SIDEBAR_TEAM_AQUA -> TeamColor.AQUA;
|
|
||||||
case SIDEBAR_TEAM_BLUE -> TeamColor.BLUE;
|
|
||||||
case SIDEBAR_TEAM_GOLD -> TeamColor.GOLD;
|
|
||||||
case SIDEBAR_TEAM_GRAY -> TeamColor.GRAY;
|
|
||||||
case SIDEBAR_TEAM_BLACK -> TeamColor.BLACK;
|
|
||||||
case SIDEBAR_TEAM_GREEN -> TeamColor.GREEN;
|
|
||||||
case SIDEBAR_TEAM_WHITE -> TeamColor.WHITE;
|
|
||||||
case SIDEBAR_TEAM_YELLOW -> TeamColor.YELLOW;
|
|
||||||
case SIDEBAR_TEAM_DARK_RED -> TeamColor.DARK_RED;
|
|
||||||
case SIDEBAR_TEAM_DARK_AQUA -> TeamColor.DARK_AQUA;
|
|
||||||
case SIDEBAR_TEAM_DARK_BLUE -> TeamColor.DARK_BLUE;
|
|
||||||
case SIDEBAR_TEAM_DARK_GRAY -> TeamColor.DARK_GRAY;
|
|
||||||
case SIDEBAR_TEAM_DARK_GREEN -> TeamColor.DARK_GREEN;
|
|
||||||
case SIDEBAR_TEAM_DARK_PURPLE -> TeamColor.DARK_PURPLE;
|
|
||||||
case SIDEBAR_TEAM_LIGHT_PURPLE -> TeamColor.LIGHT_PURPLE;
|
|
||||||
default -> null;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removed() {
|
public void addDisplaySlot(DisplaySlot slot) {
|
||||||
active = false;
|
activeSlots.add(slot);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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) {
|
||||||
|
var 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,20 +26,25 @@
|
||||||
package org.geysermc.geyser.scoreboard;
|
package org.geysermc.geyser.scoreboard;
|
||||||
|
|
||||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
|
import org.cloudburstmc.protocol.bedrock.data.ScoreInfo;
|
||||||
import org.cloudburstmc.protocol.bedrock.data.command.CommandEnumConstraint;
|
import org.cloudburstmc.protocol.bedrock.data.command.CommandEnumConstraint;
|
||||||
import org.cloudburstmc.protocol.bedrock.packet.RemoveObjectivePacket;
|
|
||||||
import org.cloudburstmc.protocol.bedrock.packet.SetDisplayObjectivePacket;
|
|
||||||
import org.cloudburstmc.protocol.bedrock.packet.SetScorePacket;
|
import org.cloudburstmc.protocol.bedrock.packet.SetScorePacket;
|
||||||
import org.geysermc.geyser.GeyserImpl;
|
import org.geysermc.geyser.GeyserImpl;
|
||||||
import org.geysermc.geyser.GeyserLogger;
|
import org.geysermc.geyser.GeyserLogger;
|
||||||
import org.geysermc.geyser.entity.type.Entity;
|
|
||||||
import org.geysermc.geyser.entity.type.player.PlayerEntity;
|
import org.geysermc.geyser.entity.type.player.PlayerEntity;
|
||||||
|
import org.geysermc.geyser.scoreboard.display.slot.BelownameDisplaySlot;
|
||||||
|
import org.geysermc.geyser.scoreboard.display.slot.DisplaySlot;
|
||||||
|
import org.geysermc.geyser.scoreboard.display.slot.PlayerlistDisplaySlot;
|
||||||
|
import org.geysermc.geyser.scoreboard.display.slot.SidebarDisplaySlot;
|
||||||
import org.geysermc.geyser.session.GeyserSession;
|
import org.geysermc.geyser.session.GeyserSession;
|
||||||
import org.geysermc.geyser.text.GeyserLocale;
|
import org.geysermc.geyser.text.GeyserLocale;
|
||||||
|
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.NameTagVisibility;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
|
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
|
||||||
|
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
|
||||||
import org.jetbrains.annotations.Contract;
|
import org.jetbrains.annotations.Contract;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
@ -50,18 +55,34 @@ import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.geysermc.geyser.scoreboard.UpdateType.*;
|
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 {
|
public final class Scoreboard {
|
||||||
private static final boolean SHOW_SCOREBOARD_LOGS = Boolean.parseBoolean(System.getProperty("Geyser.ShowScoreboardLogs", "true"));
|
private static final boolean SHOW_SCOREBOARD_LOGS = Boolean.parseBoolean(System.getProperty("Geyser.ShowScoreboardLogs", "true"));
|
||||||
private static final boolean ADD_TEAM_SUGGESTIONS = Boolean.parseBoolean(System.getProperty("Geyser.AddTeamSuggestions", "true"));
|
private static final boolean ADD_TEAM_SUGGESTIONS = Boolean.parseBoolean(System.getProperty("Geyser.AddTeamSuggestions", "true"));
|
||||||
|
|
||||||
private final GeyserSession session;
|
private final GeyserSession session;
|
||||||
private final GeyserLogger logger;
|
private final GeyserLogger logger;
|
||||||
@Getter
|
|
||||||
private final AtomicLong nextId = new AtomicLong(0);
|
private final AtomicLong nextId = new AtomicLong(0);
|
||||||
|
|
||||||
private final Map<String, Objective> objectives = new ConcurrentHashMap<>();
|
private final Map<String, Objective> objectives = new ConcurrentHashMap<>();
|
||||||
@Getter
|
@Getter
|
||||||
private final Map<ScoreboardPosition, Objective> objectiveSlots = new EnumMap<>(ScoreboardPosition.class);
|
private final Map<ScoreboardPosition, DisplaySlot> objectiveSlots = Collections.synchronizedMap(new EnumMap<>(ScoreboardPosition.class));
|
||||||
|
private final List<DisplaySlot> removedSlots = Collections.synchronizedList(new ArrayList<>());
|
||||||
|
|
||||||
private final Map<String, Team> teams = new ConcurrentHashMap<>(); // updated on multiple threads
|
private final Map<String, Team> teams = new ConcurrentHashMap<>(); // updated on multiple threads
|
||||||
/**
|
/**
|
||||||
* Required to preserve vanilla behavior, which also uses a map.
|
* Required to preserve vanilla behavior, which also uses a map.
|
||||||
|
@ -71,6 +92,7 @@ public final class Scoreboard {
|
||||||
@Getter
|
@Getter
|
||||||
private final Map<String, Team> playerToTeam = new Object2ObjectOpenHashMap<>();
|
private final Map<String, Team> playerToTeam = new Object2ObjectOpenHashMap<>();
|
||||||
|
|
||||||
|
private final AtomicBoolean updateLockActive = new AtomicBoolean(false);
|
||||||
private int lastAddScoreCount = 0;
|
private int lastAddScoreCount = 0;
|
||||||
private int lastRemoveScoreCount = 0;
|
private int lastRemoveScoreCount = 0;
|
||||||
|
|
||||||
|
@ -80,24 +102,22 @@ public final class Scoreboard {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeScoreboard() {
|
public void removeScoreboard() {
|
||||||
Iterator<Objective> iterator = objectives.values().iterator();
|
var copy = new HashMap<>(objectiveSlots);
|
||||||
while (iterator.hasNext()) {
|
objectiveSlots.clear();
|
||||||
Objective objective = iterator.next();
|
|
||||||
iterator.remove();
|
|
||||||
|
|
||||||
deleteObjective(objective, false);
|
for (DisplaySlot slot : copy.values()) {
|
||||||
|
slot.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public @Nullable Objective registerNewObjective(String objectiveId) {
|
public @Nullable Objective registerNewObjective(String objectiveId) {
|
||||||
Objective objective = objectives.get(objectiveId);
|
Objective objective = objectives.get(objectiveId);
|
||||||
if (objective != null) {
|
if (objective != null) {
|
||||||
// we have no other choice, or we have to make a new map?
|
// matches vanilla behaviour
|
||||||
// if the objective hasn't been deleted, we have to force it
|
if (SHOW_SCOREBOARD_LOGS) {
|
||||||
if (objective.getUpdateType() != REMOVE) {
|
logger.warning("An objective with the same name '" + objectiveId + "' already exists! Ignoring new objective!");
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
deleteObjective(objective, true);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
objective = new Objective(this, objectiveId);
|
objective = new Objective(this, objectiveId);
|
||||||
|
@ -105,230 +125,127 @@ public final class Scoreboard {
|
||||||
return objective;
|
return objective;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void displayObjective(String objectiveId, ScoreboardPosition displaySlot) {
|
public void displayObjective(String objectiveId, ScoreboardPosition slot) {
|
||||||
|
if (objectiveId.isEmpty()) {
|
||||||
|
// matches vanilla behaviour
|
||||||
|
var display = objectiveSlots.get(slot);
|
||||||
|
if (display != null) {
|
||||||
|
removedSlots.add(display);
|
||||||
|
objectiveSlots.remove(slot, display);
|
||||||
|
var objective = display.objective();
|
||||||
|
objective.removeDisplaySlot(display);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Objective objective = objectives.get(objectiveId);
|
Objective objective = objectives.get(objectiveId);
|
||||||
if (objective == null) {
|
if (objective == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!objective.isActive()) {
|
var display = objectiveSlots.get(slot);
|
||||||
objective.setActive(displaySlot);
|
if (display != null && display.objective() != objective) {
|
||||||
// for reactivated objectives
|
removedSlots.add(display);
|
||||||
objective.setUpdateType(ADD);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Objective storedObjective = objectiveSlots.get(displaySlot);
|
display = switch (DisplaySlot.slotCategory(slot)) {
|
||||||
if (storedObjective != null && storedObjective != objective) {
|
case SIDEBAR -> new SidebarDisplaySlot(session, objective, slot);
|
||||||
storedObjective.pendingRemove();
|
case BELOW_NAME -> new BelownameDisplaySlot(session, objective);
|
||||||
}
|
case PLAYER_LIST -> new PlayerlistDisplaySlot(session, objective);
|
||||||
objectiveSlots.put(displaySlot, objective);
|
default -> throw new IllegalStateException("Unexpected value: " + slot);
|
||||||
|
};
|
||||||
if (displaySlot == ScoreboardPosition.BELOW_NAME) {
|
objectiveSlots.put(slot, display);
|
||||||
// Display the below name score option to all players
|
objective.addDisplaySlot(display);
|
||||||
// Of note: unlike Bedrock, if there is an objective in the below name slot, everyone has a display
|
|
||||||
for (PlayerEntity entity : session.getEntityCache().getAllPlayerEntities()) {
|
|
||||||
if (!entity.isValid()) {
|
|
||||||
// Player hasn't spawned yet - don't bother, it'll be done then
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
entity.setBelowNameText(objective);
|
public void registerNewTeam(
|
||||||
}
|
String teamName,
|
||||||
}
|
String[] players,
|
||||||
}
|
Component name,
|
||||||
|
Component prefix,
|
||||||
public Team registerNewTeam(String teamName, String[] players) {
|
Component suffix,
|
||||||
|
NameTagVisibility visibility,
|
||||||
|
TeamColor color
|
||||||
|
) {
|
||||||
Team team = teams.get(teamName);
|
Team team = teams.get(teamName);
|
||||||
if (team != null) {
|
if (team != null) {
|
||||||
if (SHOW_SCOREBOARD_LOGS) {
|
if (SHOW_SCOREBOARD_LOGS) {
|
||||||
logger.info(GeyserLocale.getLocaleStringLog("geyser.network.translator.team.failed_overrides", teamName));
|
logger.info(GeyserLocale.getLocaleStringLog("geyser.network.translator.team.failed_overrides", teamName));
|
||||||
}
|
}
|
||||||
return team;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
team = new Team(this, teamName);
|
team = new Team(this, teamName, players, name, prefix, suffix, visibility, color);
|
||||||
team.addEntities(players);
|
|
||||||
teams.put(teamName, team);
|
teams.put(teamName, team);
|
||||||
|
|
||||||
// Update command parameters - is safe to send even if the command enum doesn't exist on the client (as of 1.19.51)
|
// Update command parameters - is safe to send even if the command enum doesn't exist on the client (as of 1.19.51)
|
||||||
if (ADD_TEAM_SUGGESTIONS) {
|
if (ADD_TEAM_SUGGESTIONS) {
|
||||||
session.addCommandEnum("Geyser_Teams", team.getId());
|
session.addCommandEnum("Geyser_Teams", team.id());
|
||||||
}
|
}
|
||||||
return team;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onUpdate() {
|
public void onUpdate() {
|
||||||
|
// if an update is already running, let it finish
|
||||||
|
if (updateLockActive.getAndSet(true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
List<ScoreInfo> addScores = new ArrayList<>(lastAddScoreCount);
|
List<ScoreInfo> addScores = new ArrayList<>(lastAddScoreCount);
|
||||||
List<ScoreInfo> removeScores = new ArrayList<>(lastRemoveScoreCount);
|
List<ScoreInfo> removeScores = new ArrayList<>(lastRemoveScoreCount);
|
||||||
List<Objective> removedObjectives = new ArrayList<>();
|
|
||||||
|
|
||||||
Team playerTeam = getTeamFor(session.getPlayerEntity().getUsername());
|
Team playerTeam = getTeamFor(session.getPlayerEntity().getUsername());
|
||||||
Objective correctSidebar = null;
|
DisplaySlot correctSidebarSlot = null;
|
||||||
|
|
||||||
for (Objective objective : objectives.values()) {
|
for (DisplaySlot slot : objectiveSlots.values()) {
|
||||||
// objective has been deleted
|
// slot has been removed
|
||||||
if (objective.getUpdateType() == REMOVE) {
|
if (slot.updateType() == REMOVE) {
|
||||||
removedObjectives.add(objective);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// there's nothing we can do with inactive objectives
|
if (playerTeam != null && playerTeam.color() == slot.teamColor()) {
|
||||||
// after checking if the objective has been deleted,
|
correctSidebarSlot = slot;
|
||||||
// except waiting for the objective to become activated (:
|
|
||||||
if (!objective.isActive()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (playerTeam != null && playerTeam.getColor() == objective.getTeamColor()) {
|
|
||||||
correctSidebar = objective;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (correctSidebar == null) {
|
if (correctSidebarSlot == null) {
|
||||||
correctSidebar = objectiveSlots.get(ScoreboardPosition.SIDEBAR);
|
correctSidebarSlot = objectiveSlots.get(ScoreboardPosition.SIDEBAR);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Objective objective : removedObjectives) {
|
var actualRemovedSlots = new ArrayList<>(removedSlots);
|
||||||
|
for (var slot : actualRemovedSlots) {
|
||||||
// Deletion must be handled before the active objectives are handled - otherwise if a scoreboard display is changed before the current
|
// Deletion must be handled before the active objectives are handled - otherwise if a scoreboard display is changed before the current
|
||||||
// scoreboard is removed, the client can crash
|
// scoreboard is removed, the client can crash
|
||||||
deleteObjective(objective, true);
|
slot.remove();
|
||||||
}
|
}
|
||||||
|
removedSlots.removeAll(actualRemovedSlots);
|
||||||
|
|
||||||
handleObjective(objectiveSlots.get(ScoreboardPosition.PLAYER_LIST), addScores, removeScores);
|
handleDisplaySlot(objectiveSlots.get(ScoreboardPosition.PLAYER_LIST), addScores, removeScores);
|
||||||
handleObjective(correctSidebar, addScores, removeScores);
|
handleDisplaySlot(correctSidebarSlot, addScores, removeScores);
|
||||||
handleObjective(objectiveSlots.get(ScoreboardPosition.BELOW_NAME), addScores, removeScores);
|
handleDisplaySlot(objectiveSlots.get(ScoreboardPosition.BELOW_NAME), addScores, removeScores);
|
||||||
|
|
||||||
Iterator<Team> teamIterator = teams.values().iterator();
|
|
||||||
while (teamIterator.hasNext()) {
|
|
||||||
Team current = teamIterator.next();
|
|
||||||
|
|
||||||
switch (current.getCachedUpdateType()) {
|
|
||||||
case ADD, UPDATE -> current.markUpdated();
|
|
||||||
case REMOVE -> teamIterator.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!removeScores.isEmpty()) {
|
if (!removeScores.isEmpty()) {
|
||||||
SetScorePacket setScorePacket = new SetScorePacket();
|
SetScorePacket packet = new SetScorePacket();
|
||||||
setScorePacket.setAction(SetScorePacket.Action.REMOVE);
|
packet.setAction(SetScorePacket.Action.REMOVE);
|
||||||
setScorePacket.setInfos(removeScores);
|
packet.setInfos(removeScores);
|
||||||
session.sendUpstreamPacket(setScorePacket);
|
session.sendUpstreamPacket(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!addScores.isEmpty()) {
|
if (!addScores.isEmpty()) {
|
||||||
SetScorePacket setScorePacket = new SetScorePacket();
|
SetScorePacket packet = new SetScorePacket();
|
||||||
setScorePacket.setAction(SetScorePacket.Action.SET);
|
packet.setAction(SetScorePacket.Action.SET);
|
||||||
setScorePacket.setInfos(addScores);
|
packet.setInfos(addScores);
|
||||||
session.sendUpstreamPacket(setScorePacket);
|
session.sendUpstreamPacket(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
lastAddScoreCount = addScores.size();
|
lastAddScoreCount = addScores.size();
|
||||||
lastRemoveScoreCount = removeScores.size();
|
lastRemoveScoreCount = removeScores.size();
|
||||||
|
updateLockActive.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleObjective(Objective objective, List<ScoreInfo> addScores, List<ScoreInfo> removeScores) {
|
private void handleDisplaySlot(DisplaySlot slot, List<ScoreInfo> addScores, List<ScoreInfo> removeScores) {
|
||||||
if (objective == null || objective.getUpdateType() == REMOVE) {
|
if (slot != null) {
|
||||||
return;
|
slot.render(addScores, removeScores);
|
||||||
}
|
}
|
||||||
|
|
||||||
// hearts can't hold teams, so we treat them differently
|
|
||||||
if (objective.getType() == 1) {
|
|
||||||
for (Score score : objective.getScores().values()) {
|
|
||||||
boolean update = score.shouldUpdate();
|
|
||||||
|
|
||||||
if (update) {
|
|
||||||
score.update(objective);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (score.getUpdateType() != REMOVE && update) {
|
|
||||||
addScores.add(score.getCachedInfo());
|
|
||||||
}
|
|
||||||
if (score.getUpdateType() != ADD && update) {
|
|
||||||
removeScores.add(score.getCachedInfo());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean objectiveAdd = objective.getUpdateType() == ADD;
|
|
||||||
boolean objectiveUpdate = objective.getUpdateType() == UPDATE;
|
|
||||||
|
|
||||||
for (Score score : objective.getScores().values()) {
|
|
||||||
if (score.getUpdateType() == REMOVE) {
|
|
||||||
ScoreInfo cachedInfo = score.getCachedInfo();
|
|
||||||
// cachedInfo can be null here when ScoreboardUpdater is being used and a score is added and
|
|
||||||
// removed before a single update cycle is performed
|
|
||||||
if (cachedInfo != null) {
|
|
||||||
removeScores.add(cachedInfo);
|
|
||||||
}
|
|
||||||
// score is pending to be removed, so we can remove it from the objective
|
|
||||||
objective.removeScore0(score.getName());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Team team = score.getTeam();
|
|
||||||
|
|
||||||
boolean add = objectiveAdd || objectiveUpdate;
|
|
||||||
|
|
||||||
if (team != null) {
|
|
||||||
if (team.getUpdateType() == REMOVE || !team.hasEntity(score.getName())) {
|
|
||||||
score.setTeam(null);
|
|
||||||
add = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (score.shouldUpdate()) {
|
|
||||||
score.update(objective);
|
|
||||||
add = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (add) {
|
|
||||||
addScores.add(score.getCachedInfo());
|
|
||||||
}
|
|
||||||
|
|
||||||
// we need this as long as MCPE-143063 hasn't been fixed.
|
|
||||||
// the checks after 'add' are there to prevent removing scores that
|
|
||||||
// are going to be removed anyway / don't need to be removed
|
|
||||||
if (add && score.getUpdateType() != ADD && !(objectiveUpdate || objectiveAdd)) {
|
|
||||||
removeScores.add(score.getCachedInfo());
|
|
||||||
}
|
|
||||||
|
|
||||||
score.setUpdateType(NOTHING);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (objectiveUpdate) {
|
|
||||||
RemoveObjectivePacket removeObjectivePacket = new RemoveObjectivePacket();
|
|
||||||
removeObjectivePacket.setObjectiveId(objective.getObjectiveName());
|
|
||||||
session.sendUpstreamPacket(removeObjectivePacket);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (objectiveAdd || objectiveUpdate) {
|
|
||||||
SetDisplayObjectivePacket displayObjectivePacket = new SetDisplayObjectivePacket();
|
|
||||||
displayObjectivePacket.setObjectiveId(objective.getObjectiveName());
|
|
||||||
displayObjectivePacket.setDisplayName(objective.getDisplayName());
|
|
||||||
displayObjectivePacket.setCriteria("dummy");
|
|
||||||
displayObjectivePacket.setDisplaySlot(objective.getDisplaySlotName());
|
|
||||||
displayObjectivePacket.setSortOrder(1); // 0 = ascending, 1 = descending
|
|
||||||
session.sendUpstreamPacket(displayObjectivePacket);
|
|
||||||
}
|
|
||||||
|
|
||||||
objective.setUpdateType(NOTHING);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param remove if we should remove the objective from the objectives map.
|
|
||||||
*/
|
|
||||||
public void deleteObjective(Objective objective, boolean remove) {
|
|
||||||
if (remove) {
|
|
||||||
objectives.remove(objective.getObjectiveName());
|
|
||||||
}
|
|
||||||
objectiveSlots.remove(objective.getDisplaySlot(), objective);
|
|
||||||
|
|
||||||
objective.removed();
|
|
||||||
|
|
||||||
RemoveObjectivePacket removeObjectivePacket = new RemoveObjectivePacket();
|
|
||||||
removeObjectivePacket.setObjectiveId(objective.getObjectiveName());
|
|
||||||
session.sendUpstreamPacket(removeObjectivePacket);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Objective getObjective(String objectiveName) {
|
public Objective getObjective(String objectiveName) {
|
||||||
|
@ -339,39 +256,35 @@ public final class Scoreboard {
|
||||||
return objectives.values();
|
return objectives.values();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void unregisterObjective(String objectiveName) {
|
public void removeObjective(Objective objective) {
|
||||||
Objective objective = getObjective(objectiveName);
|
objectives.remove(objective.getObjectiveName());
|
||||||
if (objective != null) {
|
for (DisplaySlot slot : objective.getActiveSlots()) {
|
||||||
objective.pendingRemove();
|
objectiveSlots.remove(slot.position(), slot);
|
||||||
|
removedSlots.add(slot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Objective getSlot(ScoreboardPosition slot) {
|
public void resetPlayerScores(String playerNameOrEntityUuid) {
|
||||||
return objectiveSlots.get(slot);
|
for (Objective objective : objectives.values()) {
|
||||||
|
objective.removeScore(playerNameOrEntityUuid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Team getTeam(String teamName) {
|
public Team getTeam(String teamName) {
|
||||||
return teams.get(teamName);
|
return teams.get(teamName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Team getTeamFor(String entity) {
|
public Team getTeamFor(String playerNameOrEntityUuid) {
|
||||||
return playerToTeam.get(entity);
|
return playerToTeam.get(playerNameOrEntityUuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeTeam(String teamName) {
|
public void removeTeam(String teamName) {
|
||||||
Team remove = teams.remove(teamName);
|
Team remove = teams.remove(teamName);
|
||||||
if (remove != null) {
|
if (remove == null) {
|
||||||
remove.setUpdateType(REMOVE);
|
return;
|
||||||
// We need to use the direct entities list here, so #refreshSessionPlayerDisplays also updates accordingly
|
|
||||||
// With the player's lack of a team in visibility checks
|
|
||||||
updateEntityNames(remove, remove.getEntities(), true);
|
|
||||||
for (String name : remove.getEntities()) {
|
|
||||||
// 1.19.3 Mojmap Scoreboard#removePlayerTeam(PlayerTeam)
|
|
||||||
playerToTeam.remove(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
session.removeCommandEnum("Geyser_Teams", remove.getId());
|
|
||||||
}
|
}
|
||||||
|
remove.remove();
|
||||||
|
session.removeCommandEnum("Geyser_Teams", remove.id());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Contract("-> new")
|
@Contract("-> new")
|
||||||
|
@ -381,48 +294,32 @@ public final class Scoreboard {
|
||||||
(o1, o2) -> o1, LinkedHashMap::new));
|
(o1, o2) -> o1, LinkedHashMap::new));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public void playerRegistered(PlayerEntity player) {
|
||||||
* Updates the display names of all entities in a given team.
|
for (DisplaySlot slot : objectiveSlots.values()) {
|
||||||
* @param teamChange the players have either joined or left the team. Used for optimizations when just the display name updated.
|
slot.playerRegistered(player);
|
||||||
*/
|
}
|
||||||
public void updateEntityNames(Team team, boolean teamChange) {
|
|
||||||
Set<String> names = new HashSet<>(team.getEntities());
|
|
||||||
updateEntityNames(team, names, teamChange);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public void playerRemoved(PlayerEntity player) {
|
||||||
* Updates the display name of a set of entities within a given team. The team may also be null if the set is being removed
|
for (DisplaySlot slot : objectiveSlots.values()) {
|
||||||
* from a team.
|
slot.playerRemoved(player);
|
||||||
*/
|
|
||||||
public void updateEntityNames(@Nullable Team team, Set<String> names, boolean teamChange) {
|
|
||||||
if (names.remove(session.getPlayerEntity().getUsername()) && teamChange) {
|
|
||||||
// If the player's team changed, then other entities' teams may modify their visibility based on team status
|
|
||||||
refreshSessionPlayerDisplays();
|
|
||||||
}
|
|
||||||
if (!names.isEmpty()) {
|
|
||||||
for (Entity entity : session.getEntityCache().getEntities().values()) {
|
|
||||||
// This more complex logic is for the future to iterate over all entities, not just players
|
|
||||||
if (entity instanceof PlayerEntity player && names.remove(player.getUsername())) {
|
|
||||||
player.updateDisplayName(team);
|
|
||||||
player.updateBedrockMetadata();
|
|
||||||
if (names.isEmpty()) {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setTeamFor(Team team, Set<String> entities) {
|
||||||
|
for (DisplaySlot slot : objectiveSlots.values()) {
|
||||||
|
// only sidebar slots use teams
|
||||||
|
if (slot instanceof SidebarDisplaySlot sidebar) {
|
||||||
|
sidebar.setTeamFor(team, entities);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public long nextId() {
|
||||||
* If the team's player was refreshed, then we need to go through every entity and check...
|
return nextId.getAndIncrement();
|
||||||
*/
|
}
|
||||||
private void refreshSessionPlayerDisplays() {
|
|
||||||
for (Entity entity : session.getEntityCache().getEntities().values()) {
|
public GeyserSession session() {
|
||||||
if (entity instanceof PlayerEntity player) {
|
return session;
|
||||||
Team playerTeam = session.getWorldCache().getScoreboard().getTeamFor(player.getUsername());
|
|
||||||
player.updateDisplayName(playerTeam);
|
|
||||||
player.updateBedrockMetadata();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -173,7 +173,6 @@ public final class ScoreboardUpdater extends Thread {
|
||||||
@Getter
|
@Getter
|
||||||
public static final class ScoreboardSession {
|
public static final class ScoreboardSession {
|
||||||
private final GeyserSession session;
|
private final GeyserSession session;
|
||||||
@SuppressWarnings("WriteOnlyObject")
|
|
||||||
private final AtomicInteger pendingPacketsPerSecond = new AtomicInteger(0);
|
private final AtomicInteger pendingPacketsPerSecond = new AtomicInteger(0);
|
||||||
private int packetsPerSecond;
|
private int packetsPerSecond;
|
||||||
private long lastUpdate;
|
private long lastUpdate;
|
||||||
|
|
|
@ -25,43 +25,61 @@
|
||||||
|
|
||||||
package org.geysermc.geyser.scoreboard;
|
package org.geysermc.geyser.scoreboard;
|
||||||
|
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.NameTagVisibility;
|
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
|
|
||||||
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
|
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.experimental.Accessors;
|
|
||||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
|
||||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
|
||||||
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||||
|
import org.geysermc.geyser.entity.type.Entity;
|
||||||
|
import org.geysermc.geyser.entity.type.LivingEntity;
|
||||||
|
import org.geysermc.geyser.session.GeyserSession;
|
||||||
|
import org.geysermc.geyser.translator.text.MessageTranslator;
|
||||||
|
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.NameTagVisibility;
|
||||||
|
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Accessors(chain = true)
|
|
||||||
public final class Team {
|
public final class Team {
|
||||||
|
public static final long LAST_UPDATE_DEFAULT = -1;
|
||||||
|
private static final long LAST_UPDATE_REMOVE = -2;
|
||||||
|
|
||||||
private final Scoreboard scoreboard;
|
private final Scoreboard scoreboard;
|
||||||
private final String id;
|
private final String id;
|
||||||
|
|
||||||
@Getter(AccessLevel.PACKAGE)
|
|
||||||
private final Set<String> entities;
|
private final Set<String> entities;
|
||||||
|
private final Set<LivingEntity> managedEntities;
|
||||||
@NonNull private NameTagVisibility nameTagVisibility = NameTagVisibility.ALWAYS;
|
@NonNull private NameTagVisibility nameTagVisibility = NameTagVisibility.ALWAYS;
|
||||||
@Setter private TeamColor color;
|
private TeamColor color;
|
||||||
|
|
||||||
private final TeamData currentData;
|
private String name;
|
||||||
private TeamData cachedData;
|
private String prefix;
|
||||||
|
private String suffix;
|
||||||
|
private long lastUpdate;
|
||||||
|
|
||||||
private boolean updating;
|
public Team(
|
||||||
|
Scoreboard scoreboard,
|
||||||
public Team(Scoreboard scoreboard, String id) {
|
String id,
|
||||||
|
String[] players,
|
||||||
|
Component name,
|
||||||
|
Component prefix,
|
||||||
|
Component suffix,
|
||||||
|
NameTagVisibility visibility,
|
||||||
|
TeamColor color
|
||||||
|
) {
|
||||||
this.scoreboard = scoreboard;
|
this.scoreboard = scoreboard;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
currentData = new TeamData();
|
this.entities = new ObjectOpenHashSet<>();
|
||||||
entities = new ObjectOpenHashSet<>();
|
this.managedEntities = new ObjectOpenHashSet<>();
|
||||||
|
|
||||||
|
addEntitiesNoUpdate(players);
|
||||||
|
// this calls the update
|
||||||
|
updateProperties(name, prefix, suffix, visibility, color);
|
||||||
|
lastUpdate = LAST_UPDATE_DEFAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<String> addEntities(String... names) {
|
public void addEntities(String... names) {
|
||||||
|
addAddedEntities(addEntitiesNoUpdate(names));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> addEntitiesNoUpdate(String... names) {
|
||||||
Set<String> added = new HashSet<>();
|
Set<String> added = new HashSet<>();
|
||||||
for (String name : names) {
|
for (String name : names) {
|
||||||
if (entities.add(name)) {
|
if (entities.add(name)) {
|
||||||
|
@ -80,24 +98,13 @@ public final class Team {
|
||||||
if (added.isEmpty()) {
|
if (added.isEmpty()) {
|
||||||
return added;
|
return added;
|
||||||
}
|
}
|
||||||
// we don't have to change the updateType,
|
// we don't have to change our updateType,
|
||||||
// because the scores itself need updating, not the team
|
// because the scores itself need updating, not the team
|
||||||
for (Objective objective : scoreboard.getObjectives()) {
|
scoreboard.setTeamFor(this, added);
|
||||||
for (String addedEntity : added) {
|
|
||||||
Score score = objective.getScores().get(addedEntity);
|
|
||||||
if (score != null) {
|
|
||||||
score.setTeam(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return added;
|
return added;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public void removeEntities(String... names) {
|
||||||
* @return all removed entities from this team
|
|
||||||
*/
|
|
||||||
public Set<String> removeEntities(String... names) {
|
|
||||||
Set<String> removed = new HashSet<>();
|
Set<String> removed = new HashSet<>();
|
||||||
for (String name : names) {
|
for (String name : names) {
|
||||||
if (entities.remove(name)) {
|
if (entities.remove(name)) {
|
||||||
|
@ -105,87 +112,15 @@ public final class Team {
|
||||||
}
|
}
|
||||||
scoreboard.getPlayerToTeam().remove(name, this);
|
scoreboard.getPlayerToTeam().remove(name, this);
|
||||||
}
|
}
|
||||||
return removed;
|
removeRemovedEntities(removed);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasEntity(String name) {
|
public boolean hasEntity(String name) {
|
||||||
return entities.contains(name);
|
return entities.contains(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Team setName(String name) {
|
public String displayName(String score) {
|
||||||
currentData.name = name;
|
return prefix + score + suffix;
|
||||||
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;
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isVisibleFor(String entity) {
|
public boolean isVisibleFor(String entity) {
|
||||||
|
@ -201,34 +136,193 @@ public final class Team {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public Team setNameTagVisibility(@Nullable NameTagVisibility nameTagVisibility) {
|
public void updateProperties(Component name, Component prefix, Component suffix, NameTagVisibility visibility, TeamColor color) {
|
||||||
if (nameTagVisibility != null) {
|
// this shouldn't happen but hey!
|
||||||
// Null check like this (and this.nameTagVisibility defaults to ALWAYS) as of Java 1.19.4
|
if (lastUpdate == LAST_UPDATE_REMOVE) {
|
||||||
this.nameTagVisibility = nameTagVisibility;
|
return;
|
||||||
}
|
}
|
||||||
return this;
|
|
||||||
|
var oldName = this.name;
|
||||||
|
var oldPrefix = this.prefix;
|
||||||
|
var oldSuffix = this.suffix;
|
||||||
|
var oldVisible = isVisibleFor(playerName());
|
||||||
|
var oldColor = this.color;
|
||||||
|
|
||||||
|
this.name = MessageTranslator.convertMessage(name, session().locale());
|
||||||
|
this.prefix = MessageTranslator.convertMessage(prefix, session().locale());
|
||||||
|
this.suffix = MessageTranslator.convertMessage(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) {
|
||||||
|
if (entities.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hidden = false;
|
||||||
|
if (nameTagVisibility != NameTagVisibility.ALWAYS && !isVisibleFor(playerName())) {
|
||||||
|
// while the team has technically changed, we don't mark it as changed because the visibility
|
||||||
|
// doesn't influence any of the display slots
|
||||||
|
hideEntities();
|
||||||
|
hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.color != TeamColor.RESET || !this.prefix.isEmpty() || !this.suffix.isEmpty()) {
|
||||||
|
markChanged();
|
||||||
|
// we've already hidden the entities, so we don't have to update them again
|
||||||
|
if (!hidden) {
|
||||||
|
updateEntities();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 (LivingEntity entity : managedEntities) {
|
||||||
|
entity.updateNametag(null);
|
||||||
|
entity.updateBedrockMetadata();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideEntities() {
|
||||||
|
for (LivingEntity entity : managedEntities) {
|
||||||
|
entity.hideNametag();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateEntities() {
|
||||||
|
for (LivingEntity entity : managedEntities) {
|
||||||
|
entity.updateNametag(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addAddedEntities(Set<String> names) {
|
||||||
|
var containsSelf = names.contains(playerName());
|
||||||
|
|
||||||
|
for (Entity entity : session().getEntityCache().getEntities().values()) {
|
||||||
|
if (!(entity instanceof LivingEntity living)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (names.contains(living.teamIdentifier())) {
|
||||||
|
managedEntities.add(living);
|
||||||
|
if (!containsSelf) {
|
||||||
|
living.updateNametag(this);
|
||||||
|
living.updateBedrockMetadata();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (containsSelf) {
|
||||||
|
refreshAllEntities();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeRemovedEntities(Set<String> names) {
|
||||||
|
var 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()) {
|
||||||
|
if (!(entity instanceof LivingEntity living)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
living.updateNametag(scoreboard.getTeamFor(living.teamIdentifier()));
|
||||||
|
living.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 String prefix() {
|
||||||
|
return prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String suffix() {
|
||||||
|
return suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long lastUpdate() {
|
||||||
|
return lastUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> entities() {
|
||||||
|
return entities;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return id.hashCode();
|
return id.hashCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Getter
|
|
||||||
public static final class TeamData {
|
|
||||||
private UpdateType updateType;
|
|
||||||
private boolean changed;
|
|
||||||
|
|
||||||
private String name;
|
|
||||||
private String prefix;
|
|
||||||
private String suffix;
|
|
||||||
|
|
||||||
private TeamData() {
|
|
||||||
updateType = UpdateType.ADD;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDisplayName(String score) {
|
|
||||||
return prefix + score + suffix;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,131 @@
|
||||||
|
/*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
var finalName = reference.name();
|
||||||
|
var 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (order != null) {
|
||||||
|
finalName = order + ChatColor.RESET + finalName;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,184 @@
|
||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
setAndAddBelownameForExisting();
|
||||||
|
updateType = UpdateType.NOTHING;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
// add is handled in the constructor and remove is handled in #remove()
|
||||||
|
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 setAndAddBelownameForExisting() {
|
||||||
|
for (PlayerEntity player : session.getEntityCache().getAllPlayerEntities()) {
|
||||||
|
playerRegistered(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
} 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,190 @@
|
||||||
|
/*
|
||||||
|
* 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,
|
||||||
|
// 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.
|
||||||
|
// Note that
|
||||||
|
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)) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -126,15 +126,35 @@ public class EntityCache {
|
||||||
|
|
||||||
public void addPlayerEntity(PlayerEntity entity) {
|
public void addPlayerEntity(PlayerEntity entity) {
|
||||||
// putIfAbsent matches the behavior of playerInfoMap in Java as of 1.19.3
|
// putIfAbsent matches the behavior of playerInfoMap in Java as of 1.19.3
|
||||||
playerEntities.putIfAbsent(entity.getUuid(), entity);
|
var exists = playerEntities.putIfAbsent(entity.getUuid(), entity) != null;
|
||||||
|
if (exists) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// notify scoreboard for new entity
|
||||||
|
session.getWorldCache().getScoreboard().playerRegistered(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
public PlayerEntity getPlayerEntity(UUID uuid) {
|
public PlayerEntity getPlayerEntity(UUID uuid) {
|
||||||
return playerEntities.get(uuid);
|
return playerEntities.get(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<PlayerEntity> getPlayersByName(String name) {
|
||||||
|
var list = new ArrayList<PlayerEntity>();
|
||||||
|
for (PlayerEntity player : playerEntities.values()) {
|
||||||
|
if (name.equals(player.getUsername())) {
|
||||||
|
list.add(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
public PlayerEntity removePlayerEntity(UUID uuid) {
|
public PlayerEntity removePlayerEntity(UUID uuid) {
|
||||||
return playerEntities.remove(uuid);
|
var player = playerEntities.remove(uuid);
|
||||||
|
if (player != null) {
|
||||||
|
// notify scoreboard
|
||||||
|
session.getWorldCache().getScoreboard().playerRemoved(player);
|
||||||
|
}
|
||||||
|
return player;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Collection<PlayerEntity> getAllPlayerEntities() {
|
public Collection<PlayerEntity> getAllPlayerEntities() {
|
||||||
|
|
|
@ -31,6 +31,7 @@ import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
|
||||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
import org.cloudburstmc.math.vector.Vector3i;
|
import org.cloudburstmc.math.vector.Vector3i;
|
||||||
import org.cloudburstmc.protocol.bedrock.packet.SetTitlePacket;
|
import org.cloudburstmc.protocol.bedrock.packet.SetTitlePacket;
|
||||||
|
@ -48,7 +49,7 @@ public final class WorldCache {
|
||||||
@Getter
|
@Getter
|
||||||
private final ScoreboardSession scoreboardSession;
|
private final ScoreboardSession scoreboardSession;
|
||||||
@Getter
|
@Getter
|
||||||
private Scoreboard scoreboard;
|
private @NonNull Scoreboard scoreboard;
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
private Difficulty difficulty = Difficulty.EASY;
|
private Difficulty difficulty = Difficulty.EASY;
|
||||||
|
@ -78,11 +79,9 @@ public final class WorldCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeScoreboard() {
|
public void removeScoreboard() {
|
||||||
if (scoreboard != null) {
|
|
||||||
scoreboard.removeScoreboard();
|
scoreboard.removeScoreboard();
|
||||||
scoreboard = new Scoreboard(session);
|
scoreboard = new Scoreboard(session);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public int increaseAndGetScoreboardPacketsPerSecond() {
|
public int increaseAndGetScoreboardPacketsPerSecond() {
|
||||||
int pendingPps = scoreboardSession.getPendingPacketsPerSecond().incrementAndGet();
|
int pendingPps = scoreboardSession.getPendingPacketsPerSecond().incrementAndGet();
|
||||||
|
|
|
@ -84,4 +84,30 @@ public class ChatColor {
|
||||||
string = string.replace(WHITE, (char) 0x1b + "[37;1m");
|
string = string.replace(WHITE, (char) 0x1b + "[37;1m");
|
||||||
return string;
|
return string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String styleOrder(int index) {
|
||||||
|
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;
|
||||||
|
case 18 -> STRIKETHROUGH;
|
||||||
|
case 19 -> UNDERLINE;
|
||||||
|
default -> ITALIC;
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -32,40 +32,22 @@ import org.geysermc.geyser.session.GeyserSession;
|
||||||
import org.geysermc.geyser.session.cache.WorldCache;
|
import org.geysermc.geyser.session.cache.WorldCache;
|
||||||
import org.geysermc.geyser.translator.protocol.PacketTranslator;
|
import org.geysermc.geyser.translator.protocol.PacketTranslator;
|
||||||
import org.geysermc.geyser.translator.protocol.Translator;
|
import org.geysermc.geyser.translator.protocol.Translator;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
|
|
||||||
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundResetScorePacket;
|
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundResetScorePacket;
|
||||||
|
|
||||||
@Translator(packet = ClientboundResetScorePacket.class)
|
@Translator(packet = ClientboundResetScorePacket.class)
|
||||||
public class JavaResetScorePacket extends PacketTranslator<ClientboundResetScorePacket> {
|
public class JavaResetScorePacket extends PacketTranslator<ClientboundResetScorePacket> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void translate(GeyserSession session, ClientboundResetScorePacket packet) {
|
public void translate(GeyserSession session, ClientboundResetScorePacket packet) {
|
||||||
WorldCache worldCache = session.getWorldCache();
|
WorldCache worldCache = session.getWorldCache();
|
||||||
Scoreboard scoreboard = worldCache.getScoreboard();
|
Scoreboard scoreboard = worldCache.getScoreboard();
|
||||||
int pps = worldCache.increaseAndGetScoreboardPacketsPerSecond();
|
int pps = worldCache.increaseAndGetScoreboardPacketsPerSecond();
|
||||||
|
|
||||||
Objective belowName = scoreboard.getObjectiveSlots().get(ScoreboardPosition.BELOW_NAME);
|
|
||||||
|
|
||||||
if (packet.getObjective() == null) {
|
if (packet.getObjective() == null) {
|
||||||
// No objective name means all scores are reset for that player (/scoreboard players reset PLAYERNAME)
|
// No objective name means all scores are reset for that player (/scoreboard players reset PLAYERNAME)
|
||||||
for (Objective otherObjective : scoreboard.getObjectives()) {
|
scoreboard.resetPlayerScores(packet.getOwner());
|
||||||
otherObjective.removeScore(packet.getOwner());
|
|
||||||
}
|
|
||||||
|
|
||||||
// as described below
|
|
||||||
if (belowName != null) {
|
|
||||||
JavaSetScoreTranslator.setBelowName(session, belowName, packet.getOwner());
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Objective objective = scoreboard.getObjective(packet.getObjective());
|
Objective objective = scoreboard.getObjective(packet.getObjective());
|
||||||
objective.removeScore(packet.getOwner());
|
objective.removeScore(packet.getOwner());
|
||||||
|
|
||||||
// If this is the objective that is in use to show the below name text, we need to update the player
|
|
||||||
// attached to this score.
|
|
||||||
if (objective == belowName) {
|
|
||||||
// Update the score on this player to now reflect 0
|
|
||||||
JavaSetScoreTranslator.setBelowName(session, objective, packet.getOwner());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ScoreboardUpdater will handle it for us if the packets per second
|
// ScoreboardUpdater will handle it for us if the packets per second
|
||||||
|
|
|
@ -25,72 +25,45 @@
|
||||||
|
|
||||||
package org.geysermc.geyser.translator.protocol.java.scoreboard;
|
package org.geysermc.geyser.translator.protocol.java.scoreboard;
|
||||||
|
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ObjectiveAction;
|
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
|
|
||||||
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetObjectivePacket;
|
|
||||||
import org.geysermc.geyser.GeyserImpl;
|
|
||||||
import org.geysermc.geyser.GeyserLogger;
|
|
||||||
import org.geysermc.geyser.entity.type.player.PlayerEntity;
|
|
||||||
import org.geysermc.geyser.scoreboard.Objective;
|
import org.geysermc.geyser.scoreboard.Objective;
|
||||||
import org.geysermc.geyser.scoreboard.Scoreboard;
|
import org.geysermc.geyser.scoreboard.Scoreboard;
|
||||||
import org.geysermc.geyser.scoreboard.ScoreboardUpdater;
|
import org.geysermc.geyser.scoreboard.ScoreboardUpdater;
|
||||||
import org.geysermc.geyser.scoreboard.UpdateType;
|
|
||||||
import org.geysermc.geyser.session.GeyserSession;
|
import org.geysermc.geyser.session.GeyserSession;
|
||||||
import org.geysermc.geyser.session.cache.WorldCache;
|
import org.geysermc.geyser.session.cache.WorldCache;
|
||||||
import org.geysermc.geyser.translator.protocol.PacketTranslator;
|
import org.geysermc.geyser.translator.protocol.PacketTranslator;
|
||||||
import org.geysermc.geyser.translator.protocol.Translator;
|
import org.geysermc.geyser.translator.protocol.Translator;
|
||||||
import org.geysermc.geyser.translator.text.MessageTranslator;
|
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ObjectiveAction;
|
||||||
|
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetObjectivePacket;
|
||||||
|
|
||||||
@Translator(packet = ClientboundSetObjectivePacket.class)
|
@Translator(packet = ClientboundSetObjectivePacket.class)
|
||||||
public class JavaSetObjectiveTranslator extends PacketTranslator<ClientboundSetObjectivePacket> {
|
public class JavaSetObjectiveTranslator extends PacketTranslator<ClientboundSetObjectivePacket> {
|
||||||
private final GeyserLogger logger = GeyserImpl.getInstance().getLogger();
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void translate(GeyserSession session, ClientboundSetObjectivePacket packet) {
|
public void translate(GeyserSession session, ClientboundSetObjectivePacket packet) {
|
||||||
WorldCache worldCache = session.getWorldCache();
|
WorldCache worldCache = session.getWorldCache();
|
||||||
Scoreboard scoreboard = worldCache.getScoreboard();
|
Scoreboard scoreboard = worldCache.getScoreboard();
|
||||||
int pps = worldCache.increaseAndGetScoreboardPacketsPerSecond();
|
int pps = worldCache.increaseAndGetScoreboardPacketsPerSecond();
|
||||||
|
|
||||||
Objective objective = scoreboard.getObjective(packet.getName());
|
Objective objective;
|
||||||
if (objective != null && objective.getUpdateType() != UpdateType.REMOVE && packet.getAction() == ObjectiveAction.ADD) {
|
if (packet.getAction() == ObjectiveAction.ADD) {
|
||||||
// matches vanilla behaviour
|
objective = scoreboard.registerNewObjective(packet.getName());
|
||||||
logger.warning("An objective with the same name '" + packet.getName() + "' already exists! Ignoring packet");
|
} else {
|
||||||
|
objective = scoreboard.getObjective(packet.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// matches vanilla
|
||||||
|
if (objective == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((objective == null || objective.getUpdateType() == UpdateType.REMOVE) && packet.getAction() != ObjectiveAction.REMOVE) {
|
|
||||||
objective = scoreboard.registerNewObjective(packet.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (packet.getAction()) {
|
switch (packet.getAction()) {
|
||||||
case ADD, UPDATE -> {
|
case ADD, UPDATE ->
|
||||||
objective.setDisplayName(MessageTranslator.convertMessage(packet.getDisplayName()))
|
objective.updateProperties(packet.getDisplayName(), packet.getType(), packet.getNumberFormat());
|
||||||
.setNumberFormat(packet.getNumberFormat())
|
case REMOVE -> scoreboard.removeObjective(objective);
|
||||||
.setType(packet.getType().ordinal());
|
|
||||||
if (objective == scoreboard.getObjectiveSlots().get(ScoreboardPosition.BELOW_NAME)) {
|
|
||||||
// Update the score tag of all players
|
|
||||||
for (PlayerEntity entity : session.getEntityCache().getAllPlayerEntities()) {
|
|
||||||
if (entity.isValid()) {
|
|
||||||
entity.setBelowNameText(objective);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case REMOVE -> {
|
|
||||||
scoreboard.unregisterObjective(packet.getName());
|
|
||||||
if (objective != null && objective == scoreboard.getObjectiveSlots().get(ScoreboardPosition.BELOW_NAME)) {
|
|
||||||
// Clear the score tag from all players
|
|
||||||
for (PlayerEntity entity : session.getEntityCache().getAllPlayerEntities()) {
|
|
||||||
// Other places we check for the entity being valid,
|
|
||||||
// but we must set the below name text as null for all players
|
|
||||||
// or else PlayerEntity#spawnEntity will find a null objective and not touch EntityData#SCORE_TAG
|
|
||||||
entity.setBelowNameText(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (objective == null || !objective.isActive()) {
|
// Scoreboard#removeObjective doesn't touch the display slot(s) that were attached to it.
|
||||||
|
// So Objective#hasDisplaySlot will be true as long as it's currently present on the Bedrock client
|
||||||
|
if (!objective.hasDisplaySlot()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,23 +25,17 @@
|
||||||
|
|
||||||
package org.geysermc.geyser.translator.protocol.java.scoreboard;
|
package org.geysermc.geyser.translator.protocol.java.scoreboard;
|
||||||
|
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.NameTagVisibility;
|
import java.util.Arrays;
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamAction;
|
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamColor;
|
|
||||||
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetPlayerTeamPacket;
|
|
||||||
import org.geysermc.geyser.GeyserImpl;
|
import org.geysermc.geyser.GeyserImpl;
|
||||||
import org.geysermc.geyser.GeyserLogger;
|
import org.geysermc.geyser.GeyserLogger;
|
||||||
import org.geysermc.geyser.scoreboard.Scoreboard;
|
import org.geysermc.geyser.scoreboard.Scoreboard;
|
||||||
import org.geysermc.geyser.scoreboard.ScoreboardUpdater;
|
import org.geysermc.geyser.scoreboard.ScoreboardUpdater;
|
||||||
import org.geysermc.geyser.scoreboard.Team;
|
import org.geysermc.geyser.scoreboard.Team;
|
||||||
import org.geysermc.geyser.scoreboard.UpdateType;
|
|
||||||
import org.geysermc.geyser.session.GeyserSession;
|
import org.geysermc.geyser.session.GeyserSession;
|
||||||
import org.geysermc.geyser.translator.protocol.PacketTranslator;
|
import org.geysermc.geyser.translator.protocol.PacketTranslator;
|
||||||
import org.geysermc.geyser.translator.protocol.Translator;
|
import org.geysermc.geyser.translator.protocol.Translator;
|
||||||
import org.geysermc.geyser.translator.text.MessageTranslator;
|
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.TeamAction;
|
||||||
|
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetPlayerTeamPacket;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
@Translator(packet = ClientboundSetPlayerTeamPacket.class)
|
@Translator(packet = ClientboundSetPlayerTeamPacket.class)
|
||||||
public class JavaSetPlayerTeamTranslator extends PacketTranslator<ClientboundSetPlayerTeamPacket> {
|
public class JavaSetPlayerTeamTranslator extends PacketTranslator<ClientboundSetPlayerTeamPacket> {
|
||||||
|
@ -60,82 +54,44 @@ public class JavaSetPlayerTeamTranslator extends PacketTranslator<ClientboundSet
|
||||||
int pps = session.getWorldCache().increaseAndGetScoreboardPacketsPerSecond();
|
int pps = session.getWorldCache().increaseAndGetScoreboardPacketsPerSecond();
|
||||||
|
|
||||||
Scoreboard scoreboard = session.getWorldCache().getScoreboard();
|
Scoreboard scoreboard = session.getWorldCache().getScoreboard();
|
||||||
|
|
||||||
|
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());
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
switch (packet.getAction()) {
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case UPDATE -> {
|
case UPDATE -> {
|
||||||
if (team == null) {
|
team.updateProperties(
|
||||||
if (logger.isDebug()) {
|
packet.getDisplayName(),
|
||||||
logger.debug("Error while translating Team Packet " + packet.getAction()
|
packet.getPrefix(),
|
||||||
+ "! Scoreboard Team " + packet.getTeamName() + " is not registered."
|
packet.getSuffix(),
|
||||||
|
packet.getNameTagVisibility(),
|
||||||
|
packet.getColor()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return;
|
case ADD_PLAYER -> team.addEntities(packet.getPlayers());
|
||||||
}
|
case REMOVE_PLAYER -> team.removeEntities(packet.getPlayers());
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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());
|
case REMOVE -> scoreboard.removeTeam(packet.getTeamName());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ScoreboardUpdater will handle it for us if the packets per second
|
// ScoreboardUpdater will handle it for us if the packets per second
|
||||||
// (for score and team packets) is higher than the first threshold
|
// (for score and team packets) is higher than the first threshold
|
||||||
|
|
|
@ -25,12 +25,8 @@
|
||||||
|
|
||||||
package org.geysermc.geyser.translator.protocol.java.scoreboard;
|
package org.geysermc.geyser.translator.protocol.java.scoreboard;
|
||||||
|
|
||||||
import org.geysermc.mcprotocollib.protocol.data.game.scoreboard.ScoreboardPosition;
|
|
||||||
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetScorePacket;
|
|
||||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
|
||||||
import org.geysermc.geyser.GeyserImpl;
|
import org.geysermc.geyser.GeyserImpl;
|
||||||
import org.geysermc.geyser.GeyserLogger;
|
import org.geysermc.geyser.GeyserLogger;
|
||||||
import org.geysermc.geyser.entity.type.player.PlayerEntity;
|
|
||||||
import org.geysermc.geyser.scoreboard.Objective;
|
import org.geysermc.geyser.scoreboard.Objective;
|
||||||
import org.geysermc.geyser.scoreboard.Scoreboard;
|
import org.geysermc.geyser.scoreboard.Scoreboard;
|
||||||
import org.geysermc.geyser.scoreboard.ScoreboardUpdater;
|
import org.geysermc.geyser.scoreboard.ScoreboardUpdater;
|
||||||
|
@ -39,6 +35,7 @@ import org.geysermc.geyser.session.cache.WorldCache;
|
||||||
import org.geysermc.geyser.text.GeyserLocale;
|
import org.geysermc.geyser.text.GeyserLocale;
|
||||||
import org.geysermc.geyser.translator.protocol.PacketTranslator;
|
import org.geysermc.geyser.translator.protocol.PacketTranslator;
|
||||||
import org.geysermc.geyser.translator.protocol.Translator;
|
import org.geysermc.geyser.translator.protocol.Translator;
|
||||||
|
import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.scoreboard.ClientboundSetScorePacket;
|
||||||
|
|
||||||
@Translator(packet = ClientboundSetScorePacket.class)
|
@Translator(packet = ClientboundSetScorePacket.class)
|
||||||
public class JavaSetScoreTranslator extends PacketTranslator<ClientboundSetScorePacket> {
|
public class JavaSetScoreTranslator extends PacketTranslator<ClientboundSetScorePacket> {
|
||||||
|
@ -63,16 +60,7 @@ public class JavaSetScoreTranslator extends PacketTranslator<ClientboundSetScore
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this is the objective that is in use to show the below name text, we need to update the player
|
|
||||||
// attached to this score.
|
|
||||||
boolean isBelowName = objective == scoreboard.getObjectiveSlots().get(ScoreboardPosition.BELOW_NAME);
|
|
||||||
|
|
||||||
objective.setScore(packet.getOwner(), packet.getValue(), packet.getDisplay(), packet.getNumberFormat());
|
objective.setScore(packet.getOwner(), packet.getValue(), packet.getDisplay(), packet.getNumberFormat());
|
||||||
if (isBelowName) {
|
|
||||||
// Update the below name score on this player
|
|
||||||
setBelowName(session, objective, packet.getOwner());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ScoreboardUpdater will handle it for us if the packets per second
|
// ScoreboardUpdater will handle it for us if the packets per second
|
||||||
// (for score and team packets) is higher than the first threshold
|
// (for score and team packets) is higher than the first threshold
|
||||||
|
@ -80,36 +68,4 @@ public class JavaSetScoreTranslator extends PacketTranslator<ClientboundSetScore
|
||||||
scoreboard.onUpdate();
|
scoreboard.onUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param objective the objective that currently resides on the below name display slot
|
|
||||||
*/
|
|
||||||
static void setBelowName(GeyserSession session, Objective objective, String username) {
|
|
||||||
PlayerEntity entity = getOtherPlayerEntity(session, username);
|
|
||||||
if (entity == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
entity.setBelowNameText(objective);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static @Nullable PlayerEntity getOtherPlayerEntity(GeyserSession session, String username) {
|
|
||||||
// We don't care about the session player, because... they're not going to be seeing their own score
|
|
||||||
if (session.getPlayerEntity().getUsername().equals(username)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (PlayerEntity entity : session.getEntityCache().getAllPlayerEntities()) {
|
|
||||||
if (entity.getUsername().equals(username)) {
|
|
||||||
if (entity.isValid()) {
|
|
||||||
return entity;
|
|
||||||
} else {
|
|
||||||
// The below name text will be applied on spawn
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue