diff --git a/paper-api/src/main/java/com/destroystokyo/paper/ClientOption.java b/paper-api/src/main/java/com/destroystokyo/paper/ClientOption.java new file mode 100644 index 0000000000..290bde7050 --- /dev/null +++ b/paper-api/src/main/java/com/destroystokyo/paper/ClientOption.java @@ -0,0 +1,76 @@ +package com.destroystokyo.paper; + +import net.kyori.adventure.translation.Translatable; +import net.kyori.adventure.util.Index; +import org.bukkit.inventory.MainHand; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public final class ClientOption { + + public static final ClientOption SKIN_PARTS = new ClientOption<>(SkinParts.class); + public static final ClientOption CHAT_COLORS_ENABLED = new ClientOption<>(Boolean.class); + public static final ClientOption CHAT_VISIBILITY = new ClientOption<>(ChatVisibility.class); + public static final ClientOption LOCALE = new ClientOption<>(String.class); + public static final ClientOption MAIN_HAND = new ClientOption<>(MainHand.class); + public static final ClientOption VIEW_DISTANCE = new ClientOption<>(Integer.class); + public static final ClientOption TEXT_FILTERING_ENABLED = new ClientOption<>(Boolean.class); + public static final ClientOption ALLOW_SERVER_LISTINGS = new ClientOption<>(Boolean.class); + public static final ClientOption PARTICLE_VISIBILITY = new ClientOption<>(ParticleVisibility.class); + + private final Class type; + + private ClientOption(final Class type) { + this.type = type; + } + + public Class getType() { + return this.type; + } + + public enum ChatVisibility implements Translatable { + FULL("full"), + SYSTEM("system"), + HIDDEN("hidden"), + /** + * @deprecated no longer used anymore since 1.15.2, the value fallback + * to the default value of the setting when unknown on the server. + * In this case {@link #FULL} will be returned. + */ + @Deprecated(since = "1.15.2", forRemoval = true) + UNKNOWN("unknown"); + + public static final Index NAMES = Index.create(ChatVisibility.class, chatVisibility -> chatVisibility.name); + private final String name; + + ChatVisibility(final String name) { + this.name = name; + } + + @Override + public String translationKey() { + if (this == UNKNOWN) { + throw new UnsupportedOperationException(this.name + " doesn't have a translation key"); + } + return "options.chat.visibility." + this.name; + } + } + + public enum ParticleVisibility implements Translatable { + ALL("all"), + DECREASED("decreased"), + MINIMAL("minimal"); + + public static final Index NAMES = Index.create(ParticleVisibility.class, particleVisibility -> particleVisibility.name); + private final String name; + + ParticleVisibility(final String name) { + this.name = name; + } + + @Override + public String translationKey() { + return "options.particles." + this.name; + } + } +} diff --git a/paper-api/src/main/java/com/destroystokyo/paper/SkinParts.java b/paper-api/src/main/java/com/destroystokyo/paper/SkinParts.java new file mode 100644 index 0000000000..4a0c39405d --- /dev/null +++ b/paper-api/src/main/java/com/destroystokyo/paper/SkinParts.java @@ -0,0 +1,20 @@ +package com.destroystokyo.paper; + +public interface SkinParts { + + boolean hasCapeEnabled(); + + boolean hasJacketEnabled(); + + boolean hasLeftSleeveEnabled(); + + boolean hasRightSleeveEnabled(); + + boolean hasLeftPantsEnabled(); + + boolean hasRightPantsEnabled(); + + boolean hasHatsEnabled(); + + int getRaw(); +} diff --git a/paper-api/src/main/java/com/destroystokyo/paper/event/player/PlayerClientOptionsChangeEvent.java b/paper-api/src/main/java/com/destroystokyo/paper/event/player/PlayerClientOptionsChangeEvent.java new file mode 100644 index 0000000000..5245955fb3 --- /dev/null +++ b/paper-api/src/main/java/com/destroystokyo/paper/event/player/PlayerClientOptionsChangeEvent.java @@ -0,0 +1,142 @@ +package com.destroystokyo.paper.event.player; + +import com.destroystokyo.paper.ClientOption; +import com.destroystokyo.paper.ClientOption.ChatVisibility; +import com.destroystokyo.paper.ClientOption.ParticleVisibility; +import com.destroystokyo.paper.SkinParts; +import java.util.Map; +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; +import org.bukkit.event.player.PlayerEvent; +import org.bukkit.inventory.MainHand; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; + +/** + * Called when the player changes their client settings + */ +@NullMarked +public class PlayerClientOptionsChangeEvent extends PlayerEvent { + + private static final HandlerList HANDLER_LIST = new HandlerList(); + + private final String locale; + private final int viewDistance; + private final ChatVisibility chatVisibility; + private final boolean chatColors; + private final SkinParts skinparts; + private final MainHand mainHand; + private final boolean allowsServerListings; + private final boolean textFilteringEnabled; + private final ParticleVisibility particleVisibility; + + @Deprecated + public PlayerClientOptionsChangeEvent(final Player player, final String locale, final int viewDistance, final ChatVisibility chatVisibility, final boolean chatColors, final SkinParts skinParts, final MainHand mainHand) { + super(player); + this.locale = locale; + this.viewDistance = viewDistance; + this.chatVisibility = chatVisibility; + this.chatColors = chatColors; + this.skinparts = skinParts; + this.mainHand = mainHand; + this.allowsServerListings = false; + this.textFilteringEnabled = false; + this.particleVisibility = ParticleVisibility.ALL; + } + + @ApiStatus.Internal + public PlayerClientOptionsChangeEvent(final Player player, final Map, ?> options) { + super(player); + + this.locale = (String) options.get(ClientOption.LOCALE); + this.viewDistance = (int) options.get(ClientOption.VIEW_DISTANCE); + this.chatVisibility = (ChatVisibility) options.get(ClientOption.CHAT_VISIBILITY); + this.chatColors = (boolean) options.get(ClientOption.CHAT_COLORS_ENABLED); + this.skinparts = (SkinParts) options.get(ClientOption.SKIN_PARTS); + this.mainHand = (MainHand) options.get(ClientOption.MAIN_HAND); + this.allowsServerListings = (boolean) options.get(ClientOption.ALLOW_SERVER_LISTINGS); + this.textFilteringEnabled = (boolean) options.get(ClientOption.TEXT_FILTERING_ENABLED); + this.particleVisibility = (ParticleVisibility) options.get(ClientOption.PARTICLE_VISIBILITY); + } + + public String getLocale() { + return this.locale; + } + + public boolean hasLocaleChanged() { + return !this.locale.equals(this.player.getClientOption(ClientOption.LOCALE)); + } + + public int getViewDistance() { + return this.viewDistance; + } + + public boolean hasViewDistanceChanged() { + return this.viewDistance != this.player.getClientOption(ClientOption.VIEW_DISTANCE); + } + + public ChatVisibility getChatVisibility() { + return this.chatVisibility; + } + + public boolean hasChatVisibilityChanged() { + return this.chatVisibility != this.player.getClientOption(ClientOption.CHAT_VISIBILITY); + } + + public boolean hasChatColorsEnabled() { + return this.chatColors; + } + + public boolean hasChatColorsEnabledChanged() { + return this.chatColors != this.player.getClientOption(ClientOption.CHAT_COLORS_ENABLED); + } + + public SkinParts getSkinParts() { + return this.skinparts; + } + + public boolean hasSkinPartsChanged() { + return this.skinparts.getRaw() != this.player.getClientOption(ClientOption.SKIN_PARTS).getRaw(); + } + + public MainHand getMainHand() { + return this.mainHand; + } + + public boolean hasMainHandChanged() { + return this.mainHand != this.player.getClientOption(ClientOption.MAIN_HAND); + } + + public boolean hasTextFilteringEnabled() { + return this.textFilteringEnabled; + } + + public boolean hasTextFilteringChanged() { + return this.textFilteringEnabled != this.player.getClientOption(ClientOption.TEXT_FILTERING_ENABLED); + } + + public boolean allowsServerListings() { + return this.allowsServerListings; + } + + public boolean hasAllowServerListingsChanged() { + return this.allowsServerListings != this.player.getClientOption(ClientOption.ALLOW_SERVER_LISTINGS); + } + + public ParticleVisibility getParticleVisibility() { + return this.particleVisibility; + } + + public boolean hasParticleVisibilityChanged() { + return this.particleVisibility != this.player.getClientOption(ClientOption.PARTICLE_VISIBILITY); + } + + @Override + public HandlerList getHandlers() { + return HANDLER_LIST; + } + + public static HandlerList getHandlerList() { + return HANDLER_LIST; + } +} diff --git a/paper-api/src/main/java/org/bukkit/entity/Player.java b/paper-api/src/main/java/org/bukkit/entity/Player.java index 70fef7c72b..1a94a91489 100644 --- a/paper-api/src/main/java/org/bukkit/entity/Player.java +++ b/paper-api/src/main/java/org/bukkit/entity/Player.java @@ -3375,6 +3375,13 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM void resetCooldown(); // Paper end - attack cooldown API + // Paper start - client option API + /** + * @return the client option value of the player + */ + @NotNull T getClientOption(com.destroystokyo.paper.@NotNull ClientOption option); + // Paper end - client option API + // Spigot start public class Spigot extends Entity.Spigot {