From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Samsuik Date: Tue, 21 Sep 2021 23:54:25 +0100 Subject: [PATCH] Client Visibility Settings diff --git a/src/main/java/me/samsuik/sakura/command/SakuraCommands.java b/src/main/java/me/samsuik/sakura/command/SakuraCommands.java index 3e85c19db0655034c203eaab8d2e6b5504da5da8..cbb2e57f9ab3b48a6e5f792711c4c6fd2d34d445 100644 --- a/src/main/java/me/samsuik/sakura/command/SakuraCommands.java +++ b/src/main/java/me/samsuik/sakura/command/SakuraCommands.java @@ -1,6 +1,9 @@ package me.samsuik.sakura.command; import me.samsuik.sakura.command.subcommands.ConfigCommand; +import me.samsuik.sakura.command.subcommands.FPSCommand; +import me.samsuik.sakura.command.subcommands.VisualCommand; +import me.samsuik.sakura.player.visibility.VisibilityTypes; import net.minecraft.server.MinecraftServer; import org.bukkit.command.Command; @@ -12,6 +15,9 @@ public final class SakuraCommands { static { COMMANDS.put("sakura", new SakuraCommand("sakura")); COMMANDS.put("config", new ConfigCommand("config")); + COMMANDS.put("fps", new FPSCommand("fps")); + COMMANDS.put("tntvisibility", new VisualCommand(VisibilityTypes.TNT, "tnttoggle")); + COMMANDS.put("sandvisibility", new VisualCommand(VisibilityTypes.SAND, "sandtoggle")); } public static void registerCommands(MinecraftServer server) { diff --git a/src/main/java/me/samsuik/sakura/command/subcommands/FPSCommand.java b/src/main/java/me/samsuik/sakura/command/subcommands/FPSCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..a85f84ebfef4ed3f3dc2a337a057d541f64c9d0e --- /dev/null +++ b/src/main/java/me/samsuik/sakura/command/subcommands/FPSCommand.java @@ -0,0 +1,24 @@ +package me.samsuik.sakura.command.subcommands; + +import me.samsuik.sakura.command.BaseSubCommand; +import me.samsuik.sakura.player.visibility.VisibilityGui; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; + +@DefaultQualifier(NonNull.class) +public final class FPSCommand extends BaseSubCommand { + private final VisibilityGui visibilityGui = new VisibilityGui(); + + public FPSCommand(String name) { + super(name); + } + + @Override + public void execute(CommandSender sender, String[] args) { + if (sender instanceof Player player) { + this.visibilityGui.showTo(player); + } + } +} diff --git a/src/main/java/me/samsuik/sakura/command/subcommands/VisualCommand.java b/src/main/java/me/samsuik/sakura/command/subcommands/VisualCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..cd9ebbb7bbb90c87faf75fcc4b487e93573f1387 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/command/subcommands/VisualCommand.java @@ -0,0 +1,41 @@ +package me.samsuik.sakura.command.subcommands; + +import me.samsuik.sakura.command.BaseSubCommand; +import me.samsuik.sakura.configuration.GlobalConfiguration; +import me.samsuik.sakura.player.visibility.VisibilitySettings; +import me.samsuik.sakura.player.visibility.VisibilityState; +import me.samsuik.sakura.player.visibility.VisibilityType; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; + +import java.util.Arrays; + +@DefaultQualifier(NonNull.class) +public final class VisualCommand extends BaseSubCommand { + private final VisibilityType type; + + public VisualCommand(VisibilityType type, String... aliases) { + super(type.key() + "visibility"); + this.setAliases(Arrays.asList(aliases)); + this.type = type; + } + + @Override + public void execute(CommandSender sender, String[] args) { + if (!(sender instanceof Player player)) { + return; + } + + VisibilitySettings settings = player.getVisibility(); + VisibilityState state = settings.toggle(type); + + String stateName = (state == VisibilityState.ON) ? "Enabled" : "Disabled"; + player.sendRichMessage(GlobalConfiguration.get().messages.fpsSettingChange, + Placeholder.unparsed("name", this.type.key()), + Placeholder.unparsed("state", stateName) + ); + } +} diff --git a/src/main/java/me/samsuik/sakura/player/gui/FeatureGui.java b/src/main/java/me/samsuik/sakura/player/gui/FeatureGui.java new file mode 100644 index 0000000000000000000000000000000000000000..49f53c01d61cbe551aa68a91f53782e8f01d9c67 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/player/gui/FeatureGui.java @@ -0,0 +1,44 @@ +package me.samsuik.sakura.player.gui; + +import me.samsuik.sakura.player.gui.components.GuiComponent; +import net.kyori.adventure.text.Component; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public abstract class FeatureGui { + private final int size; + private final Component title; + + public FeatureGui(int size, Component title) { + this.size = size; + this.title = title; + } + + protected abstract void fillInventory(Inventory inventory); + + protected abstract void afterFill(Player player, FeatureGuiInventory inventory); + + public final void showTo(Player bukkitPlayer) { + FeatureGuiInventory featureInventory = new FeatureGuiInventory(this, this.size, this.title); + this.fillInventory(featureInventory.getInventory()); + this.afterFill(bukkitPlayer, featureInventory); + bukkitPlayer.openInventory(featureInventory.getInventory()); + } + + @ApiStatus.Internal + public static void clickEvent(InventoryClickEvent event) { + Inventory clicked = event.getClickedInventory(); + if (clicked != null && clicked.getHolder(false) instanceof FeatureGuiInventory featureInventory) { + event.setCancelled(true); + for (GuiComponent component : featureInventory.getComponents().reversed()) { + if (component.interaction(event, featureInventory)) { + break; + } + } + } + } +} diff --git a/src/main/java/me/samsuik/sakura/player/gui/FeatureGuiInventory.java b/src/main/java/me/samsuik/sakura/player/gui/FeatureGuiInventory.java new file mode 100644 index 0000000000000000000000000000000000000000..6c86501b8a392ed04c46646bfac0cad9b2ca33bc --- /dev/null +++ b/src/main/java/me/samsuik/sakura/player/gui/FeatureGuiInventory.java @@ -0,0 +1,90 @@ +package me.samsuik.sakura.player.gui; + +import com.google.common.base.Preconditions; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Multimap; +import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import me.samsuik.sakura.player.gui.components.GuiComponent; +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.jspecify.annotations.NullMarked; + +import java.util.Collection; +import java.util.Optional; + +@NullMarked +public final class FeatureGuiInventory implements InventoryHolder { + private final Inventory inventory; + private final FeatureGui gui; + private final Multimap componentsUnderKey = HashMultimap.create(); + private final Object2ObjectMap componentKeys = new Object2ObjectLinkedOpenHashMap<>(); + + public FeatureGuiInventory(FeatureGui gui, int size, Component component) { + this.inventory = Bukkit.createInventory(this, size, component); + this.gui = gui; + } + + @Override + public Inventory getInventory() { + return this.inventory; + } + + public FeatureGui getGui() { + return this.gui; + } + + public ImmutableList getComponents() { + return ImmutableList.copyOf(this.componentKeys.keySet()); + } + + public ImmutableList findComponents(NamespacedKey key) { + return ImmutableList.copyOf(this.componentsUnderKey.get(key)); + } + + public Optional findFirst(NamespacedKey key) { + Collection components = this.componentsUnderKey.get(key); + return components.stream().findFirst(); + } + + public void removeComponents(NamespacedKey key) { + Collection removed = this.componentsUnderKey.removeAll(key); + for (GuiComponent component : removed) { + this.componentKeys.remove(component); + } + } + + public void addComponent(GuiComponent component, NamespacedKey key) { + Preconditions.checkArgument(!this.componentKeys.containsKey(component), "component has already been added"); + this.componentKeys.put(component, key); + this.componentsUnderKey.put(key, component); + this.inventoryUpdate(component); + } + + public void removeComponent(GuiComponent component) { + NamespacedKey key = this.componentKeys.remove(component); + this.componentsUnderKey.remove(key, component); + } + + public void replaceComponent(GuiComponent component, GuiComponent replacement) { + NamespacedKey key = this.componentKeys.remove(component); + Preconditions.checkNotNull(key, "component does not exist"); + this.componentKeys.put(replacement, key); + this.componentsUnderKey.remove(key, component); + this.componentsUnderKey.put(key, replacement); + this.inventoryUpdate(replacement); + } + + public void removeAllComponents() { + this.componentKeys.clear(); + this.componentsUnderKey.clear(); + } + + private void inventoryUpdate(GuiComponent component) { + component.creation(this.inventory); + } +} diff --git a/src/main/java/me/samsuik/sakura/player/gui/ItemStackUtil.java b/src/main/java/me/samsuik/sakura/player/gui/ItemStackUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..eddf7632f194142c48f4012734128a10e422eea6 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/player/gui/ItemStackUtil.java @@ -0,0 +1,19 @@ +package me.samsuik.sakura.player.gui; + +import net.kyori.adventure.text.Component; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public class ItemStackUtil { + public static ItemStack itemWithBlankName(Material material) { + return itemWithName(material, Component.empty()); + } + + public static ItemStack itemWithName(Material material, Component component) { + ItemStack item = new ItemStack(material); + item.editMeta(m -> m.itemName(component)); + return item; + } +} diff --git a/src/main/java/me/samsuik/sakura/player/gui/components/GuiClickEvent.java b/src/main/java/me/samsuik/sakura/player/gui/components/GuiClickEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..39065d5003bfa60e7453079eb3202bea1b605d4a --- /dev/null +++ b/src/main/java/me/samsuik/sakura/player/gui/components/GuiClickEvent.java @@ -0,0 +1,10 @@ +package me.samsuik.sakura.player.gui.components; + +import me.samsuik.sakura.player.gui.FeatureGuiInventory; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public interface GuiClickEvent { + void doSomething(InventoryClickEvent event, FeatureGuiInventory inventory); +} diff --git a/src/main/java/me/samsuik/sakura/player/gui/components/GuiComponent.java b/src/main/java/me/samsuik/sakura/player/gui/components/GuiComponent.java new file mode 100644 index 0000000000000000000000000000000000000000..5a2ff0ca3462254bc4e01192b48c321c6f34e909 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/player/gui/components/GuiComponent.java @@ -0,0 +1,13 @@ +package me.samsuik.sakura.player.gui.components; + +import me.samsuik.sakura.player.gui.FeatureGuiInventory; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public interface GuiComponent { + boolean interaction(InventoryClickEvent event, FeatureGuiInventory featureInventory); + + void creation(Inventory inventory); +} diff --git a/src/main/java/me/samsuik/sakura/player/gui/components/ItemButton.java b/src/main/java/me/samsuik/sakura/player/gui/components/ItemButton.java new file mode 100644 index 0000000000000000000000000000000000000000..6d49d3390b2e01fcffd638abdde569d1788f3256 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/player/gui/components/ItemButton.java @@ -0,0 +1,34 @@ +package me.samsuik.sakura.player.gui.components; + +import me.samsuik.sakura.player.gui.FeatureGuiInventory; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public final class ItemButton implements GuiComponent { + private final ItemStack bukkitItem; + private final int slot; + private final GuiClickEvent whenClicked; + + public ItemButton(ItemStack bukkitItem, int slot, GuiClickEvent whenClicked) { + this.bukkitItem = bukkitItem; + this.slot = slot; + this.whenClicked = whenClicked; + } + + @Override + public boolean interaction(InventoryClickEvent event, FeatureGuiInventory featureInventory) { + if (event.getSlot() == this.slot) { + this.whenClicked.doSomething(event, featureInventory); + return true; + } + return false; + } + + @Override + public void creation(Inventory inventory) { + inventory.setItem(this.slot, this.bukkitItem); + } +} diff --git a/src/main/java/me/samsuik/sakura/player/gui/components/ItemSwitch.java b/src/main/java/me/samsuik/sakura/player/gui/components/ItemSwitch.java new file mode 100644 index 0000000000000000000000000000000000000000..243f1cbabf7054d7d5092f21e923e4dea5085123 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/player/gui/components/ItemSwitch.java @@ -0,0 +1,44 @@ +package me.samsuik.sakura.player.gui.components; + +import com.google.common.base.Preconditions; +import me.samsuik.sakura.player.gui.FeatureGuiInventory; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.jspecify.annotations.NullMarked; + +import java.util.Collections; +import java.util.List; + +@NullMarked +public final class ItemSwitch implements GuiComponent { + private final List items; + private final int slot; + private final int selected; + private final GuiClickEvent whenClicked; + + public ItemSwitch(List items, int slot, int selected, GuiClickEvent whenClicked) { + Preconditions.checkArgument(!items.isEmpty()); + this.items = Collections.unmodifiableList(items); + this.slot = slot; + this.selected = selected; + this.whenClicked = whenClicked; + } + + @Override + public boolean interaction(InventoryClickEvent event, FeatureGuiInventory featureInventory) { + if (this.slot == event.getSlot()) { + int next = (this.selected + 1) % this.items.size(); + ItemSwitch itemSwitch = new ItemSwitch(this.items, this.slot, next, this.whenClicked); + featureInventory.replaceComponent(this, itemSwitch); + this.whenClicked.doSomething(event, featureInventory); + return true; + } + return false; + } + + @Override + public void creation(Inventory inventory) { + inventory.setItem(this.slot, this.items.get(this.selected)); + } +} diff --git a/src/main/java/me/samsuik/sakura/player/visibility/PlayerVisibilitySettings.java b/src/main/java/me/samsuik/sakura/player/visibility/PlayerVisibilitySettings.java new file mode 100644 index 0000000000000000000000000000000000000000..938fe2143d0db2a0760022433c951155b5e088c3 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/player/visibility/PlayerVisibilitySettings.java @@ -0,0 +1,65 @@ +package me.samsuik.sakura.player.visibility; + +import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; +import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; +import net.minecraft.nbt.CompoundTag; +import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NonNull; + +public final class PlayerVisibilitySettings implements VisibilitySettings { + private static final String SETTINGS_COMPOUND_TAG = "clientVisibilitySettings"; + private final Reference2ObjectMap visibilityStates = new Reference2ObjectOpenHashMap<>(); + + @Override + public @NonNull VisibilityState get(@NonNull VisibilityType type) { + VisibilityState state = this.visibilityStates.get(type); + return state != null ? state : type.getDefault(); + } + + @Override + public @NotNull VisibilityState set(@NonNull VisibilityType type, @NonNull VisibilityState state) { + if (type.isDefault(state)) { + this.visibilityStates.remove(type); + } else { + this.visibilityStates.put(type, state); + } + return state; + } + + @Override + public @NonNull VisibilityState currentState() { + int modifiedCount = this.visibilityStates.size(); + if (modifiedCount == 0) { + return VisibilityState.ON; + } else if (modifiedCount != VisibilityTypes.types().size()) { + return VisibilityState.MODIFIED; + } else { + return VisibilityState.OFF; + } + } + + @Override + public boolean playerModified() { + return !this.visibilityStates.isEmpty(); + } + + public void loadData(@NonNull CompoundTag tag) { + if (!tag.contains(SETTINGS_COMPOUND_TAG, CompoundTag.TAG_COMPOUND)) { + return; + } + + CompoundTag settingsTag = tag.getCompound(SETTINGS_COMPOUND_TAG); + for (VisibilityType type : VisibilityTypes.types()) { + if (settingsTag.contains(type.key(), CompoundTag.TAG_STRING)) { + VisibilityState state = VisibilityState.valueOf(settingsTag.getString(type.key())); + this.visibilityStates.put(type, state); + } + } + } + + public void saveData(@NonNull CompoundTag tag) { + CompoundTag settingsTag = new CompoundTag(); + this.visibilityStates.forEach((t, s) -> settingsTag.putString(t.key(), s.name())); + tag.put(SETTINGS_COMPOUND_TAG, settingsTag); + } +} diff --git a/src/main/java/me/samsuik/sakura/player/visibility/VisibilityGui.java b/src/main/java/me/samsuik/sakura/player/visibility/VisibilityGui.java new file mode 100644 index 0000000000000000000000000000000000000000..67b821b7a614704cb736afe286e967e1c955a289 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/player/visibility/VisibilityGui.java @@ -0,0 +1,96 @@ +package me.samsuik.sakura.player.visibility; + +import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue; +import me.samsuik.sakura.configuration.GlobalConfiguration; +import me.samsuik.sakura.player.gui.FeatureGui; +import me.samsuik.sakura.player.gui.FeatureGuiInventory; +import me.samsuik.sakura.player.gui.components.ItemButton; +import me.samsuik.sakura.player.gui.components.ItemSwitch; +import net.kyori.adventure.text.Component; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.jspecify.annotations.NullMarked; + +import static me.samsuik.sakura.player.gui.ItemStackUtil.itemWithBlankName; + +@NullMarked +public final class VisibilityGui extends FeatureGui { + private static final NamespacedKey TOGGLE_BUTTON_KEY = new NamespacedKey("sakura", "toggle_button"); + private static final NamespacedKey MENU_ITEMS_KEY = new NamespacedKey("sakura", "menu_items"); + + public VisibilityGui() { + super(45, Component.text("FPS Settings")); + } + + @Override + protected void fillInventory(Inventory inventory) { + for (int slot = 0; slot < inventory.getSize(); ++slot) { + // x, y from top left of the inventory + int x = slot % 9; + int y = slot / 9; + // from center + int rx = x - 4; + int ry = y - 2; + double d = Math.sqrt(rx * rx + ry * ry); + if (d <= 3.25) { + inventory.setItem(slot, itemWithBlankName(GlobalConfiguration.get().fps.material)); + } else if (x % 8 == 0) { + inventory.setItem(slot, itemWithBlankName(Material.BLACK_STAINED_GLASS_PANE)); + } else { + inventory.setItem(slot, itemWithBlankName(Material.WHITE_STAINED_GLASS_PANE)); + } + } + } + + @Override + protected void afterFill(Player player, FeatureGuiInventory inventory) { + VisibilitySettings settings = player.getVisibility(); + IntArrayFIFOQueue slots = this.availableSlots(); + this.updateToggleButton(settings, player, inventory); + for (VisibilityType type : VisibilityTypes.types()) { + VisibilityState state = settings.get(type); + int index = type.states().indexOf(state); + int slot = slots.dequeueInt(); + + ItemSwitch itemSwitch = new ItemSwitch( + VisibilityGuiItems.GUI_ITEMS.get(type), + slot, index, + (e, inv) -> { + settings.cycle(type); + this.updateToggleButton(settings, player, inv); + } + ); + + inventory.addComponent(itemSwitch, MENU_ITEMS_KEY); + } + } + + private void updateToggleButton(VisibilitySettings settings, Player player, FeatureGuiInventory inventory) { + inventory.removeComponents(TOGGLE_BUTTON_KEY); + VisibilityState settingsState = settings.currentState(); + ItemButton button = new ItemButton( + VisibilityGuiItems.TOGGLE_BUTTON_ITEMS.get(settingsState), + (2 * 9) + 8, + (e, inv) -> { + settings.toggleAll(); + inventory.removeAllComponents(); + this.afterFill(player, inv); + } + ); + inventory.addComponent(button, TOGGLE_BUTTON_KEY); + } + + private IntArrayFIFOQueue availableSlots() { + IntArrayFIFOQueue slots = new IntArrayFIFOQueue(); + for (int row = 1; row < 4; ++row) { + for (int column = 3; column < 6; ++column) { + if ((column + row) % 2 == 0) { + slots.enqueue((row * 9) + column); + } + } + } + return slots; + } +} diff --git a/src/main/java/me/samsuik/sakura/player/visibility/VisibilityGuiItems.java b/src/main/java/me/samsuik/sakura/player/visibility/VisibilityGuiItems.java new file mode 100644 index 0000000000000000000000000000000000000000..ca26410b394602f3aa0e50efc8109ad9c6ec0611 --- /dev/null +++ b/src/main/java/me/samsuik/sakura/player/visibility/VisibilityGuiItems.java @@ -0,0 +1,55 @@ +package me.samsuik.sakura.player.visibility; + +import com.google.common.collect.ImmutableList; +import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; +import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; +import me.samsuik.sakura.player.gui.ItemStackUtil; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.apache.commons.lang.StringUtils; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +import java.util.Locale; + +public final class VisibilityGuiItems { + static final Reference2ObjectMap> GUI_ITEMS = new Reference2ObjectOpenHashMap<>(); + static final Reference2ObjectMap TOGGLE_BUTTON_ITEMS = new Reference2ObjectOpenHashMap<>(); + + static { + Reference2ObjectMap items = new Reference2ObjectOpenHashMap<>(); + + items.put(VisibilityTypes.TNT, ItemStackUtil.itemWithName(Material.TNT, Component.text("Tnt", NamedTextColor.RED))); + items.put(VisibilityTypes.SAND, ItemStackUtil.itemWithName(Material.SAND, Component.text("Sand", NamedTextColor.YELLOW))); + items.put(VisibilityTypes.EXPLOSIONS, ItemStackUtil.itemWithName(Material.COBWEB, Component.text("Explosions", NamedTextColor.WHITE))); + items.put(VisibilityTypes.SPAWNERS, ItemStackUtil.itemWithName(Material.SPAWNER, Component.text("Spawners", NamedTextColor.DARK_GRAY))); + items.put(VisibilityTypes.PISTONS, ItemStackUtil.itemWithName(Material.PISTON, Component.text("Pistons", NamedTextColor.GOLD))); + + for (VisibilityType type : VisibilityTypes.types()) { + ItemStack item = items.get(type); + + ImmutableList stateItems = type.states().stream() + .map(s -> createItemForState(item, s)) + .collect(ImmutableList.toImmutableList()); + + GUI_ITEMS.put(type, stateItems); + } + + TOGGLE_BUTTON_ITEMS.put(VisibilityState.ON, ItemStackUtil.itemWithName(Material.GREEN_STAINED_GLASS_PANE, Component.text("Enabled", NamedTextColor.GREEN))); + TOGGLE_BUTTON_ITEMS.put(VisibilityState.MODIFIED, ItemStackUtil.itemWithName(Material.MAGENTA_STAINED_GLASS_PANE, Component.text("Modified", NamedTextColor.LIGHT_PURPLE))); + TOGGLE_BUTTON_ITEMS.put(VisibilityState.OFF, ItemStackUtil.itemWithName(Material.RED_STAINED_GLASS_PANE, Component.text("Disabled", NamedTextColor.RED))); + } + + private static String lowercaseThenCapitalise(String name) { + String lowercaseName = name.toLowerCase(Locale.ENGLISH); + return StringUtils.capitalize(lowercaseName); + } + + private static ItemStack createItemForState(ItemStack in, VisibilityState state) { + String capitalisedName = lowercaseThenCapitalise(state.name()); + Component component = Component.text(" | " + capitalisedName, NamedTextColor.GRAY); + ItemStack itemCopy = in.clone(); + itemCopy.editMeta(m -> m.itemName(m.itemName().append(component))); + return itemCopy; + } +} diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket.java index 1a37654aff9a9c86c9f7af10a1cf721371f0c5ec..82644b34a77dc5e5af38260b7b07b3ec9aceae33 100644 --- a/src/main/java/net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket.java +++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket.java @@ -19,7 +19,7 @@ public class ClientboundSectionBlocksUpdatePacket implements Packet public public ClientboundSectionBlocksUpdatePacket(SectionPos sectionPos, ShortSet positions, LevelChunkSection section) { this.sectionPos = sectionPos; diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java index 1f9f023a43c16cc472deea5285b01c19136e019f..cf1e4cb1cdbda7dd1668ca40a788173942572a9e 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -1891,6 +1891,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier persistentStateManagerFactory, int viewDistance, boolean dsync) { super(new RegionStorageInfo(session.getLevelId(), world.dimension(), "chunk"), session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync); @@ -222,6 +223,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider this.poiManager = new PoiManager(new RegionStorageInfo(session.getLevelId(), world.dimension(), "poi"), path.resolve("poi"), dataFixer, dsync, iregistrycustom, world.getServer(), world); this.setServerViewDistance(viewDistance); this.worldGenContext = new WorldGenContext(world, chunkGenerator, structureTemplateManager, this.lightEngine, null, this::setChunkUnsaved); // Paper - rewrite chunk system + // Sakura start - client visibility settings; minimal tnt/sand + this.minimalEntities = new it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap(); + this.minimalEntities.defaultReturnValue(Integer.MIN_VALUE); + // Sakura end - client visibility settings; minimal tnt/sand } private void setChunkUnsaved(ChunkPos pos) { @@ -973,6 +978,8 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider tracker.serverEntity.sendChanges(); } } + + this.minimalEntities.clear(); // Sakura - client visibility settings; minimal tnt/sand } // Paper end - optimise entity tracker @@ -1210,6 +1217,32 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider return !this.seenBy.isEmpty(); } // Paper end - optimise entity tracker + // Sakura start - client visibility settings; entity visibility + private boolean checkEntityVisibility(final ServerPlayer player) { + final Entity entity = this.entity; + final me.samsuik.sakura.player.visibility.VisibilitySettings settings = player.visibilitySettings; + if (!settings.playerModified() || !(entity.isPrimedTNT || entity.isFallingBlock)) { + return true; + } + final me.samsuik.sakura.player.visibility.VisibilityType type; + if (entity.isPrimedTNT) { + type = me.samsuik.sakura.player.visibility.VisibilityTypes.TNT; + } else { + type = me.samsuik.sakura.player.visibility.VisibilityTypes.SAND; + } + final me.samsuik.sakura.player.visibility.VisibilityState state = settings.get(type); + if (state == me.samsuik.sakura.player.visibility.VisibilityState.MINIMAL) { + final long key = entity.blockPosition().asLong() ^ entity.getType().hashCode(); + final long visibleEntity = ChunkMap.this.minimalEntities.get(key); + if (visibleEntity != Integer.MIN_VALUE) { + return entity.getId() == visibleEntity; + } else { + ChunkMap.this.minimalEntities.put(key, entity.getId()); + } + } + return state != me.samsuik.sakura.player.visibility.VisibilityState.OFF; + } + // Sakura end - client visibility settings; entity visibility public TrackedEntity(final Entity entity, final int i, final int j, final boolean flag) { this.serverEntity = new ServerEntity(ChunkMap.this.level, entity, j, flag, this::broadcast, this.seenBy); // CraftBukkit @@ -1287,6 +1320,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider } flag = flag && this.entity.broadcastToPlayer(player) && ChunkMap.this.isChunkTracked(player, this.entity.chunkPosition().x, this.entity.chunkPosition().z); // Paper end - Configurable entity tracking range by Y + flag = flag && this.checkEntityVisibility(player); // Sakura start - client visibility settings; entity visibility // CraftBukkit start - respect vanish API if (flag && !player.getBukkitEntity().canSee(this.entity.getBukkitEntity())) { // Paper - only consider hits diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java index 876dc65f91bf09865563c8d583b78e4fdafd82b7..9ff9f5a5d9d156b3a2a939e67c412470b2631d8d 100644 --- a/src/main/java/net/minecraft/server/level/ServerLevel.java +++ b/src/main/java/net/minecraft/server/level/ServerLevel.java @@ -595,6 +595,21 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe this.lagCompensationTick = (System.nanoTime() - net.minecraft.server.MinecraftServer.SERVER_INIT) / (java.util.concurrent.TimeUnit.MILLISECONDS.toNanos(50L)); } // Paper end - lag compensation + // Sakura start - client visibility settings + public final LongSet explosionPositions = new it.unimi.dsi.fastutil.longs.LongOpenHashSet(); + + public final boolean checkExplosionVisibility(final Vec3 position, final ServerPlayer player) { + final me.samsuik.sakura.player.visibility.VisibilitySettings settings = player.visibilitySettings; + if (settings.isDisabled(me.samsuik.sakura.player.visibility.VisibilityTypes.EXPLOSIONS)) { + return false; + } else if (settings.isToggled(me.samsuik.sakura.player.visibility.VisibilityTypes.EXPLOSIONS)) { + final BlockPos blockPosition = BlockPos.containing(position); + final long encodedPosition = blockPosition.asLong(); + return this.explosionPositions.add(encodedPosition); + } + return true; + } + // Sakura end - client visibility settings // Add env and gen to constructor, IWorldDataServer -> WorldDataServer public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List list, boolean flag1, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) { @@ -1906,7 +1921,18 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe if (entityplayer.distanceToSqr(vec3d) < 4096.0D) { Optional optional = Optional.ofNullable((Vec3) serverexplosion.getHitPlayers().get(entityplayer)); - entityplayer.connection.send(new ClientboundExplodePacket(vec3d, optional, particleparam2, holder)); + // Sakura start - client visibility settings; let players toggle explosion particles + ParticleOptions particle = particleparam2; + Vec3 position = vec3d; + // In 1.22 and later this should be replaced with sending the motion through a PlayerPositionPacket. + // The problem here is SetEntityMotion is capped to 3.9 b/pt and the only other alternate mean was + // implemented in 1.21.3. I believe it's best to just wait on this issue and deal with this hack. + if (!this.checkExplosionVisibility(vec3d, entityplayer)) { + position = new Vec3(0.0, -1024.0, 0.0); + particle = net.minecraft.core.particles.ParticleTypes.SMOKE; + } + entityplayer.connection.send(new ClientboundExplodePacket(position, optional, particle, holder)); + // Sakura end - client visibility settings; let players toggle explosion particles } } diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java index fc7f7a34babd095a51b5321f600aef65a2a9d123..2a9659158d39d4d5505328afd7a2d8dc9ecf456f 100644 --- a/src/main/java/net/minecraft/server/level/ServerPlayer.java +++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java @@ -357,6 +357,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple return this.viewDistanceHolder; } // Paper end - rewrite chunk system + public final me.samsuik.sakura.player.visibility.PlayerVisibilitySettings visibilitySettings = new me.samsuik.sakura.player.visibility.PlayerVisibilitySettings(); // Sakura - client visibility settings public ServerPlayer(MinecraftServer server, ServerLevel world, GameProfile profile, ClientInformation clientOptions) { super(world, world.getSharedSpawnPos(), world.getSharedSpawnAngle(), profile); diff --git a/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java index b0bc66dc7248aae691dcab68b925b52a1695e63f..2f9413b9442cc7f95e1974a772812fe397d4c4bd 100644 --- a/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java +++ b/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java @@ -101,6 +101,22 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack protected final org.bukkit.craftbukkit.CraftServer cserver; public boolean processedDisconnect; + // Sakura start - client visibility settings + private @Nullable Packet recreatePacket(final Packet packet) { + final me.samsuik.sakura.player.visibility.VisibilitySettings settings = this.player.visibilitySettings; + if (packet instanceof net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket bedPacket) { + if (settings.isToggled(me.samsuik.sakura.player.visibility.VisibilityTypes.SPAWNERS) && bedPacket.getType() == net.minecraft.world.level.block.entity.BlockEntityType.MOB_SPAWNER) { + return null; + } + } else if (packet instanceof net.minecraft.network.protocol.game.ClientboundBlockEventPacket bePacket) { + if (settings.isToggled(me.samsuik.sakura.player.visibility.VisibilityTypes.PISTONS) && bePacket.getBlock() instanceof net.minecraft.world.level.block.piston.PistonBaseBlock) { + return null; + } + } + return packet; + } + // Sakura end - client visibility settings + public CraftPlayer getCraftPlayer() { return (this.player == null) ? null : (CraftPlayer) this.player.getBukkitEntity(); // CraftBukkit end @@ -309,6 +325,12 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack ClientboundSetDefaultSpawnPositionPacket packet6 = (ClientboundSetDefaultSpawnPositionPacket) packet; this.player.compassTarget = CraftLocation.toBukkit(packet6.pos, this.getCraftPlayer().getWorld()); } + // Sakura start - client visibility settings + if (this.player.visibilitySettings.playerModified()) { + packet = this.recreatePacket(packet); + if (packet == null) return; + } + // Sakura end - client visibility settings // CraftBukkit end if (packet.isTerminal()) { this.close(); @@ -322,8 +344,11 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack CrashReport crashreport = CrashReport.forThrowable(throwable, "Sending packet"); CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Packet being sent"); + // Sakura start - this has to be effectively final as we're modifying the packet above + var packetFinal = packet; crashreportsystemdetails.setDetail("Packet class", () -> { - return packet.getClass().getCanonicalName(); + return packetFinal.getClass().getCanonicalName(); + // Sakura end }); throw new ReportedException(crashreport); } diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java index 84fa24880d02dc7ba1ec8bda3575be38447fd4b2..52f79a8d1b7890ffba3495ca9390f0edc27e6f99 100644 --- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java @@ -3314,6 +3314,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl event.setCancelled(cancelled); AbstractContainerMenu oldContainer = this.player.containerMenu; // SPIGOT-1224 + me.samsuik.sakura.player.gui.FeatureGui.clickEvent(event); // Sakura - client visibility settings this.cserver.getPluginManager().callEvent(event); if (this.player.containerMenu != oldContainer) { return; diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java index 1b547be0fe97119edf4f29666cfe0037e0c778e0..29045a4857aadbc7f9ae0c612555743ad404682d 100644 --- a/src/main/java/net/minecraft/world/entity/Entity.java +++ b/src/main/java/net/minecraft/world/entity/Entity.java @@ -568,6 +568,10 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess } } // Paper end - optimise entity tracker + // Sakura start - visibility api and command + public boolean isPrimedTNT; + public boolean isFallingBlock; + // Sakura end - visibility api and command public Entity(EntityType type, Level world) { this.id = Entity.ENTITY_COUNTER.incrementAndGet(); diff --git a/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java b/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java index 410c4e4a42640ebe8a9c233eb2064aad76e45a27..e270eaec13afb16ed80e7c3fd8ea35ee58291263 100644 --- a/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java +++ b/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java @@ -78,6 +78,7 @@ public class FallingBlockEntity extends Entity { this.blockState = Blocks.SAND.defaultBlockState(); this.dropItem = true; this.fallDamageMax = 40; + this.isFallingBlock = true; // Sakura } public FallingBlockEntity(Level world, double x, double y, double z, BlockState block) { diff --git a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java index 809f5e847e2f5bb594c130cebd2cb897ea768d82..eebd53ac889da265cb259ba3cb8c1ce4ef34d931 100644 --- a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java +++ b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java @@ -63,6 +63,7 @@ public class PrimedTnt extends Entity implements TraceableEntity { super(type, world); this.explosionPower = 4.0F; this.blocksBuilding = true; + this.isPrimedTNT = true; // Sakura } public PrimedTnt(Level world, double x, double y, double z, @Nullable LivingEntity igniter) { diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java index 20f64850cb42d986358f01ffcdb42187e1684bca..f3f4b4df097e1e593ac5028f7d3db92fc407d831 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java @@ -2395,6 +2395,13 @@ public class CraftPlayer extends CraftHumanEntity implements Player { handle.keepLevel = data.getBoolean("keepLevel"); } } + + // Sakura start - client visibility settings; load from nbt + if (nbttagcompound.contains("sakura", 10)) { + CompoundTag sakuraTag = nbttagcompound.getCompound("sakura"); + this.getHandle().visibilitySettings.loadData(sakuraTag); + } + // Sakura end - client visibility settings; load from nbt } public void setExtraData(CompoundTag nbttagcompound) { @@ -2424,6 +2431,11 @@ public class CraftPlayer extends CraftHumanEntity implements Player { paper.putLong("LastLogin", handle.loginTime); paper.putLong("LastSeen", System.currentTimeMillis()); // Paper end + // Sakura start - client visibility settings; save to nbt + CompoundTag sakuraTag = nbttagcompound.getCompound("sakura"); + this.getHandle().visibilitySettings.saveData(sakuraTag); + nbttagcompound.put("sakura", sakuraTag); + // Sakura end - client visibility settings; save to nbt } @Override @@ -3083,6 +3095,13 @@ public class CraftPlayer extends CraftHumanEntity implements Player { return this.getHandle().allowsListing(); } + // Sakura start - client visibility settings; api + @Override + public final me.samsuik.sakura.player.visibility.VisibilitySettings getVisibility() { + return this.getHandle().visibilitySettings; + } + // Sakura end - client visibility settings; api + // Paper start @Override public net.kyori.adventure.text.Component displayName() {