From d97ce19f495bd8c08c6be0cd194e538c6a24d815 Mon Sep 17 00:00:00 2001
From: MiniDigger | Martin <admin@minidigger.dev>
Date: Fri, 17 Apr 2020 06:10:38 +0200
Subject: [PATCH] Add Player Client Options API (#2883)

---
 .../Add-Player-Client-Options-API.patch       | 203 ++++++++++++++++++
 .../Implement-Player-Client-Options-API.patch | 202 +++++++++++++++++
 2 files changed, 405 insertions(+)
 create mode 100644 Spigot-API-Patches/Add-Player-Client-Options-API.patch
 create mode 100644 Spigot-Server-Patches/Implement-Player-Client-Options-API.patch

diff --git a/Spigot-API-Patches/Add-Player-Client-Options-API.patch b/Spigot-API-Patches/Add-Player-Client-Options-API.patch
new file mode 100644
index 0000000000..c76d72cf80
--- /dev/null
+++ b/Spigot-API-Patches/Add-Player-Client-Options-API.patch
@@ -0,0 +1,203 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: MiniDigger | Martin <admin@minidigger.dev>
+Date: Mon, 20 Jan 2020 21:38:34 +0100
+Subject: [PATCH] Add Player Client Options API
+
+
+diff --git a/src/main/java/com/destroystokyo/paper/ClientOption.java b/src/main/java/com/destroystokyo/paper/ClientOption.java
+new file mode 100644
+index 00000000..9dad814c
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/ClientOption.java
+@@ -0,0 +0,0 @@
++package com.destroystokyo.paper;
++
++import org.jetbrains.annotations.NotNull;
++
++import org.bukkit.inventory.MainHand;
++
++public final class ClientOption<T> {
++
++    public static final ClientOption<SkinParts> SKIN_PARTS = new ClientOption<>(SkinParts.class);
++    public static final ClientOption<Boolean> CHAT_COLORS_ENABLED = new ClientOption<>(Boolean.class);
++    public static final ClientOption<ChatVisibility> CHAT_VISIBILITY = new ClientOption<>(ChatVisibility.class);
++    public static final ClientOption<String> LOCALE = new ClientOption<>(String.class);
++    public static final ClientOption<MainHand> MAIN_HAND = new ClientOption<>(MainHand.class);
++    public static final ClientOption<Integer> VIEW_DISTANCE = new ClientOption<>(Integer.class);
++
++    private final Class<T> type;
++
++    private ClientOption(@NotNull Class<T> type) {
++        this.type = type;
++    }
++
++    @NotNull
++    public Class<T> getType() {
++        return type;
++    }
++
++    public enum ChatVisibility {
++        FULL,
++        SYSTEM,
++        HIDDEN,
++        UNKNOWN
++    }
++}
+diff --git a/src/main/java/com/destroystokyo/paper/SkinParts.java b/src/main/java/com/destroystokyo/paper/SkinParts.java
+new file mode 100644
+index 00000000..4a0c3940
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/SkinParts.java
+@@ -0,0 +0,0 @@
++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/src/main/java/com/destroystokyo/paper/event/player/PlayerClientOptionsChangeEvent.java b/src/main/java/com/destroystokyo/paper/event/player/PlayerClientOptionsChangeEvent.java
+new file mode 100644
+index 00000000..f7f171c4
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/event/player/PlayerClientOptionsChangeEvent.java
+@@ -0,0 +0,0 @@
++package com.destroystokyo.paper.event.player;
++
++import com.destroystokyo.paper.ClientOption;
++import com.destroystokyo.paper.ClientOption.ChatVisibility;
++import com.destroystokyo.paper.SkinParts;
++
++import org.jetbrains.annotations.NotNull;
++
++import org.bukkit.entity.Player;
++import org.bukkit.event.HandlerList;
++import org.bukkit.event.player.PlayerEvent;
++import org.bukkit.inventory.MainHand;
++
++/**
++ * Called when the player changes his client settings
++ */
++public class PlayerClientOptionsChangeEvent extends PlayerEvent {
++
++    private static final HandlerList handlers = 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;
++
++    public PlayerClientOptionsChangeEvent(@NotNull Player player, @NotNull String locale, int viewDistance, @NotNull ChatVisibility chatVisibility, boolean chatColors, @NotNull SkinParts skinParts, @NotNull MainHand mainHand) {
++        super(player);
++        this.locale = locale;
++        this.viewDistance = viewDistance;
++        this.chatVisibility = chatVisibility;
++        this.chatColors = chatColors;
++        this.skinparts = skinParts;
++        this.mainHand = mainHand;
++    }
++
++    @NotNull
++    public String getLocale() {
++        return locale;
++    }
++
++    public boolean hasLocaleChanged() {
++        return !locale.equals(player.getClientOption(ClientOption.LOCALE));
++    }
++
++    public int getViewDistance() {
++        return viewDistance;
++    }
++
++    public boolean hasViewDistanceChanged() {
++        return viewDistance != player.getClientOption(ClientOption.VIEW_DISTANCE);
++    }
++
++    @NotNull
++    public ChatVisibility getChatVisibility() {
++        return chatVisibility;
++    }
++
++    public boolean hasChatVisibilityChanged() {
++        return chatVisibility != player.getClientOption(ClientOption.CHAT_VISIBILITY);
++    }
++
++    public boolean hasChatColorsEnabled() {
++        return chatColors;
++    }
++
++    public boolean hasChatColorsEnabledChanged() {
++        return chatColors != player.getClientOption(ClientOption.CHAT_COLORS_ENABLED);
++    }
++
++    @NotNull
++    public SkinParts getSkinParts() {
++        return skinparts;
++    }
++
++    public boolean hasSkinPartsChanged() {
++        return skinparts.getRaw() != player.getClientOption(ClientOption.SKIN_PARTS).getRaw();
++    }
++
++    @NotNull
++    public MainHand getMainHand() {
++        return mainHand;
++    }
++
++    public boolean hasMainHandChanged() {
++        return mainHand != player.getClientOption(ClientOption.MAIN_HAND);
++    }
++
++    @Override
++    @NotNull
++    public HandlerList getHandlers() {
++        return handlers;
++    }
++
++    @NotNull
++    public static HandlerList getHandlerList() {
++        return handlers;
++    }
++}
+diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java
+index 3600b4c8..787bb144 100644
+--- a/src/main/java/org/bukkit/entity/Player.java
++++ b/src/main/java/org/bukkit/entity/Player.java
+@@ -0,0 +0,0 @@
+ package org.bukkit.entity;
+ 
+ import java.net.InetSocketAddress;
++import com.destroystokyo.paper.ClientOption; // Paper
+ import com.destroystokyo.paper.Title; // Paper
+ import com.destroystokyo.paper.profile.PlayerProfile; // Paper
+ import java.util.Date; // Paper
+@@ -0,0 +0,0 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM
+      * Reset the cooldown counter to 0, effectively starting the cooldown period.
+      */
+     void resetCooldown();
++
++    /**
++     * @return the client option value of the player
++     */
++    @NotNull
++    <T> T getClientOption(@NotNull ClientOption<T> option);
+     // Paper end
+ 
+     // Spigot start
+--
\ No newline at end of file
diff --git a/Spigot-Server-Patches/Implement-Player-Client-Options-API.patch b/Spigot-Server-Patches/Implement-Player-Client-Options-API.patch
new file mode 100644
index 0000000000..be6830d40f
--- /dev/null
+++ b/Spigot-Server-Patches/Implement-Player-Client-Options-API.patch
@@ -0,0 +1,202 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: MiniDigger | Martin <admin@minidigger.dev>
+Date: Mon, 20 Jan 2020 21:38:15 +0100
+Subject: [PATCH] Implement Player Client Options API
+
+
+diff --git a/src/main/java/com/destroystokyo/paper/PaperSkinParts.java b/src/main/java/com/destroystokyo/paper/PaperSkinParts.java
+new file mode 100644
+index 000000000..b6f4400df
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/PaperSkinParts.java
+@@ -0,0 +0,0 @@
++package com.destroystokyo.paper;
++
++import com.google.common.base.Objects;
++
++import java.util.StringJoiner;
++
++public class PaperSkinParts implements SkinParts {
++
++    private final int raw;
++
++    public PaperSkinParts(int raw) {
++        this.raw = raw;
++    }
++
++    public boolean hasCapeEnabled() {
++        return (raw & 1) == 1;
++    }
++
++    public boolean hasJacketEnabled() {
++        return (raw >> 1 & 1) == 1;
++    }
++
++    public boolean hasLeftSleeveEnabled() {
++        return (raw >> 2 & 1) == 1;
++    }
++
++    public boolean hasRightSleeveEnabled() {
++        return (raw >> 3 & 1) == 1;
++    }
++
++    public boolean hasLeftPantsEnabled() {
++        return (raw >> 4 & 1) == 1;
++    }
++
++    public boolean hasRightPantsEnabled() {
++        return (raw >> 5 & 1) == 1;
++    }
++
++    public boolean hasHatsEnabled() {
++        return (raw >> 6 & 1) == 1;
++    }
++
++    @Override
++    public int getRaw() {
++        return raw;
++    }
++
++    @Override
++    public boolean equals(Object o) {
++        if (this == o) return true;
++        if (o == null || getClass() != o.getClass()) return false;
++        PaperSkinParts that = (PaperSkinParts) o;
++        return raw == that.raw;
++    }
++
++    @Override
++    public int hashCode() {
++        return Objects.hashCode(raw);
++    }
++
++    @Override
++    public String toString() {
++        return new StringJoiner(", ", PaperSkinParts.class.getSimpleName() + "[", "]")
++            .add("raw=" + raw)
++            .add("cape=" + hasCapeEnabled())
++            .add("jacket=" + hasJacketEnabled())
++            .add("leftSleeve=" + hasLeftSleeveEnabled())
++            .add("rightSleeve=" + hasRightSleeveEnabled())
++            .add("leftPants=" + hasLeftPantsEnabled())
++            .add("rightPants=" + hasRightPantsEnabled())
++            .add("hats=" + hasHatsEnabled())
++            .toString();
++    }
++}
+diff --git a/src/main/java/net/minecraft/server/EntityHuman.java b/src/main/java/net/minecraft/server/EntityHuman.java
+index 7afcde608..8a3f2b5e4 100644
+--- a/src/main/java/net/minecraft/server/EntityHuman.java
++++ b/src/main/java/net/minecraft/server/EntityHuman.java
+@@ -0,0 +0,0 @@ public abstract class EntityHuman extends EntityLiving {
+     private static final Map<EntityPose, EntitySize> b = ImmutableMap.<EntityPose, EntitySize>builder().put(EntityPose.STANDING, EntityHuman.bp).put(EntityPose.SLEEPING, EntityHuman.ap).put(EntityPose.FALL_FLYING, EntitySize.b(0.6F, 0.6F)).put(EntityPose.SWIMMING, EntitySize.b(0.6F, 0.6F)).put(EntityPose.SPIN_ATTACK, EntitySize.b(0.6F, 0.6F)).put(EntityPose.CROUCHING, EntitySize.b(0.6F, 1.5F)).put(EntityPose.DYING, EntitySize.c(0.2F, 0.2F)).build();
+     private static final DataWatcherObject<Float> c = DataWatcher.a(EntityHuman.class, DataWatcherRegistry.c);
+     private static final DataWatcherObject<Integer> d = DataWatcher.a(EntityHuman.class, DataWatcherRegistry.b);
+-    protected static final DataWatcherObject<Byte> bq = DataWatcher.a(EntityHuman.class, DataWatcherRegistry.a);
++    protected static final DataWatcherObject<Byte> bq = DataWatcher.a(EntityHuman.class, DataWatcherRegistry.a); public static DataWatcherObject<Byte> getSkinPartsWatcher() { return bq; } // Paper - OBFHELPER
+     protected static final DataWatcherObject<Byte> br = DataWatcher.a(EntityHuman.class, DataWatcherRegistry.a);
+     protected static final DataWatcherObject<NBTTagCompound> bs = DataWatcher.a(EntityHuman.class, DataWatcherRegistry.p);
+     protected static final DataWatcherObject<NBTTagCompound> bt = DataWatcher.a(EntityHuman.class, DataWatcherRegistry.p);
+diff --git a/src/main/java/net/minecraft/server/EntityPlayer.java b/src/main/java/net/minecraft/server/EntityPlayer.java
+index cd8df622f..202068314 100644
+--- a/src/main/java/net/minecraft/server/EntityPlayer.java
++++ b/src/main/java/net/minecraft/server/EntityPlayer.java
+@@ -0,0 +0,0 @@ package net.minecraft.server;
+ 
+ import com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent;
+ import com.google.common.collect.Lists;
++import com.destroystokyo.paper.event.player.PlayerClientOptionsChangeEvent; // Paper
+ import com.mojang.authlib.GameProfile;
+ import com.mojang.datafixers.util.Either;
+ import io.netty.util.concurrent.Future;
+@@ -0,0 +0,0 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
+     public int lastSentExp = -99999999;
+     public int invulnerableTicks = 60;
+     private EnumChatVisibility ch;
+-    private boolean ci = true;
++    private boolean ci = true; public boolean hasChatColorsEnabled() { return this.ci; } // Paper - OBFHELPER
+     private long cj = SystemUtils.getMonotonicMillis();
+     private Entity spectatedEntity; private void setSpectatorTargetField(Entity e) { this.spectatedEntity = e; } // Paper - OBFHELPER
+     public boolean worldChangeInvuln;
+@@ -0,0 +0,0 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
+     }
+ 
+     public void a(PacketPlayInSettings packetplayinsettings) {
++        new PlayerClientOptionsChangeEvent(getBukkitEntity(), packetplayinsettings.getLocale(), packetplayinsettings.viewDistance, com.destroystokyo.paper.ClientOption.ChatVisibility.valueOf(packetplayinsettings.getChatVisibility().name()), packetplayinsettings.hasChatColorsEnabled(), new com.destroystokyo.paper.PaperSkinParts(packetplayinsettings.getSkinParts()), packetplayinsettings.getMainHand() == EnumMainHand.LEFT ? MainHand.LEFT : MainHand.RIGHT).callEvent(); // Paper - settings event
+         // CraftBukkit start
+         if (getMainHand() != packetplayinsettings.getMainHand()) {
+             PlayerChangedMainHandEvent event = new PlayerChangedMainHandEvent(getBukkitEntity(), getMainHand() == EnumMainHand.LEFT ? MainHand.LEFT : MainHand.RIGHT);
+diff --git a/src/main/java/net/minecraft/server/PacketPlayInSettings.java b/src/main/java/net/minecraft/server/PacketPlayInSettings.java
+index 8faebf9ef..4da637138 100644
+--- a/src/main/java/net/minecraft/server/PacketPlayInSettings.java
++++ b/src/main/java/net/minecraft/server/PacketPlayInSettings.java
+@@ -0,0 +0,0 @@ public class PacketPlayInSettings implements Packet<PacketListenerPlayIn> {
+         packetlistenerplayin.a(this);
+     }
+ 
++    public String getLocale() { return b(); } // Paper - OBFHELPER
+     public String b() {
+         return this.a;
+     }
+ 
++    public EnumChatVisibility getChatVisibility() { return d(); } // Paper - OBFHELPER
+     public EnumChatVisibility d() {
+         return this.c;
+     }
+ 
++    public boolean hasChatColorsEnabled() { return e(); } // Paper - OBFHELPER
+     public boolean e() {
+         return this.d;
+     }
+ 
++    public int getSkinParts() { return f(); } // Paper - OBFHELPER
+     public int f() {
+         return this.e;
+     }
+diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+index 33cac5fcb..b76379a17 100644
+--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java
+@@ -0,0 +0,0 @@
+ package org.bukkit.craftbukkit.entity;
+ 
++import com.destroystokyo.paper.ClientOption.ChatVisibility;
++import com.destroystokyo.paper.PaperSkinParts;
++import com.destroystokyo.paper.ClientOption;
+ import com.destroystokyo.paper.Title;
+ import com.destroystokyo.paper.profile.CraftPlayerProfile;
+ import com.destroystokyo.paper.profile.PlayerProfile;
+@@ -0,0 +0,0 @@ import net.minecraft.server.BlockPosition;
+ import net.minecraft.server.ChatComponentText;
+ import net.minecraft.server.Container;
+ import net.minecraft.server.Entity;
++import net.minecraft.server.EntityHuman;
+ import net.minecraft.server.EntityLiving;
+ import net.minecraft.server.EntityPlayer;
+ import net.minecraft.server.EnumChatFormat;
+@@ -0,0 +0,0 @@ public class CraftPlayer extends CraftHumanEntity implements Player {
+     public void setViewDistance(int viewDistance) {
+         throw new NotImplementedException("Per-Player View Distance APIs need further understanding to properly implement"); // TODO
+     }
++
++    @Override
++    public <T> T getClientOption(ClientOption<T> type) {
++        if(ClientOption.SKIN_PARTS.equals(type)) {
++            return type.getType().cast(new PaperSkinParts(getHandle().getDataWatcher().get(EntityHuman.getSkinPartsWatcher())));
++        } else if(ClientOption.CHAT_COLORS_ENABLED.equals(type)) {
++            return type.getType().cast(getHandle().hasChatColorsEnabled());
++        } else if(ClientOption.CHAT_VISIBILITY.equals(type)) {
++            return type.getType().cast(getHandle().getChatFlags() == null ? ChatVisibility.UNKNOWN : ChatVisibility.valueOf(getHandle().getChatFlags().name()));
++        } else if(ClientOption.LOCALE.equals(type)) {
++            return type.getType().cast(getLocale());
++        } else if(ClientOption.MAIN_HAND.equals(type)) {
++            return type.getType().cast(getMainHand());
++        } else if(ClientOption.VIEW_DISTANCE.equals(type)) {
++            return type.getType().cast(getClientViewDistance());
++        }
++        throw new RuntimeException("Unknown settings type");
++    }
+     // Paper end
+ 
+     // Spigot start
+--
\ No newline at end of file