9
0
mirror of https://github.com/Samsuik/Sakura.git synced 2025-12-28 19:29:07 +00:00

Start on feature patches

This commit is contained in:
Samsuik
2025-01-16 21:54:41 +00:00
parent 39bba67ec9
commit 1e345a18b5
46 changed files with 2407 additions and 4 deletions

View File

@@ -2,6 +2,9 @@ package me.samsuik.sakura.command;
import me.samsuik.sakura.command.subcommands.ConfigCommand;
import me.samsuik.sakura.command.subcommands.TPSCommand;
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;
@@ -14,6 +17,9 @@ public final class SakuraCommands {
COMMANDS.put("sakura", new SakuraCommand("sakura"));
COMMANDS.put("config", new ConfigCommand("config"));
COMMANDS.put("tps", new TPSCommand("tps"));
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) {

View File

@@ -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);
}
}
}

View File

@@ -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)
);
}
}

View File

@@ -0,0 +1,41 @@
package me.samsuik.sakura.entity;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import org.jspecify.annotations.NullMarked;
import java.util.Optional;
@NullMarked
public record EntityState(Vec3 position, Vec3 momentum, AABB bb, Vec3 stuckSpeed, Optional<BlockPos> supportingPos, boolean onGround, float fallDistance) {
public static EntityState of(Entity entity) {
return new EntityState(
entity.position(), entity.getDeltaMovement(), entity.getBoundingBox(),
entity.stuckSpeedMultiplier(), entity.mainSupportingBlockPos,
entity.onGround(), entity.fallDistance
);
}
public void apply(Entity entity) {
entity.setPos(this.position);
entity.setDeltaMovement(this.momentum);
entity.setBoundingBox(this.bb);
entity.makeStuckInBlock(Blocks.AIR.defaultBlockState(), this.stuckSpeed);
entity.onGround = this.onGround;
entity.mainSupportingBlockPos = this.supportingPos;
entity.fallDistance = this.fallDistance;
}
public void applyEntityPosition(Entity entity) {
entity.setPos(this.position);
entity.setBoundingBox(this.bb);
}
public boolean comparePositionAndMotion(Entity entity) {
return entity.position().equals(this.position)
&& entity.getDeltaMovement().equals(this.momentum);
}
}

View File

@@ -0,0 +1,76 @@
package me.samsuik.sakura.entity.merge;
import net.minecraft.world.entity.Entity;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public final class EntityMergeHandler {
private final TrackedMergeHistory trackedHistory = new TrackedMergeHistory();
/**
* Tries to merge the provided entities using the {@link MergeStrategy}.
*
* @param previous the last entity to tick
* @param entity the entity being merged
* @return success
*/
public boolean tryMerge(@Nullable Entity entity, @Nullable Entity previous) {
if (entity instanceof MergeableEntity mergeEntity && previous instanceof MergeableEntity) {
MergeEntityData mergeEntityData = mergeEntity.getMergeEntityData();
MergeStrategy strategy = MergeStrategy.from(mergeEntityData.getMergeLevel());
Entity into = strategy.mergeEntity(entity, previous, this.trackedHistory);
if (into instanceof MergeableEntity intoEntity && !into.isRemoved() && mergeEntity.isSafeToMergeInto(intoEntity, strategy.trackHistory())) {
return this.mergeEntity(mergeEntity, intoEntity);
}
}
return false;
}
/**
* Stores the merged data of the provided entities if the {@link MergeStrategy} requires it.
*
* @param entity provided entity
*/
public void removeEntity(@Nullable Entity entity) {
if (entity instanceof MergeableEntity mergeEntity) {
MergeEntityData mergeEntityData = mergeEntity.getMergeEntityData();
MergeStrategy strategy = MergeStrategy.from(mergeEntityData.getMergeLevel());
if (mergeEntityData.hasMerged() && strategy.trackHistory()) {
this.trackedHistory.trackHistory(entity, mergeEntityData);
}
}
}
/**
* Called every tick and provided the current server tick to remove any unneeded merge history.
*
* @param tick server tick
*/
public void expire(int tick) {
if (tick % 200 == 0) {
this.trackedHistory.expire(tick);
}
}
/**
* Merges the first entity into the second. The entity merge count can be retrieved through the {@link MergeEntityData}.
* <p>
* This method also updates the bukkit handle so that plugins reference the first entity after the second entity has been removed.
*
* @param mergeEntity the first entity
* @param into the entity to merge into
* @return if successful
*/
public boolean mergeEntity(@NotNull MergeableEntity mergeEntity, @NotNull MergeableEntity into) {
MergeEntityData entities = mergeEntity.getMergeEntityData();
MergeEntityData mergeInto = into.getMergeEntityData();
mergeInto.mergeWith(entities); // merge entities together
// discard the entity and update the bukkit handle
Entity nmsEntity = (Entity) mergeEntity;
nmsEntity.discard();
nmsEntity.updateBukkitHandle((Entity) into);
return true;
}
}

View File

@@ -0,0 +1,16 @@
package me.samsuik.sakura.entity.merge;
import net.minecraft.world.entity.Entity;
import org.jetbrains.annotations.NotNull;
public interface MergeCondition {
default MergeCondition and(@NotNull MergeCondition condition) {
return (e,c,t) -> this.accept(e,c,t) && condition.accept(e,c,t);
}
default MergeCondition or(@NotNull MergeCondition condition) {
return (e,c,t) -> this.accept(e,c,t) || condition.accept(e,c,t);
}
boolean accept(@NotNull Entity entity, int attempts, long sinceCreation);
}

View File

@@ -0,0 +1,52 @@
package me.samsuik.sakura.entity.merge;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import net.minecraft.world.entity.Entity;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public final class MergeEntityData {
private final Entity entity;
private List<MergeEntityData> connected = new ObjectArrayList<>();
private int count = 1;
private MergeLevel mergeLevel = MergeLevel.NONE;
public MergeEntityData(Entity entity) {
this.entity = entity;
}
public void mergeWith(@NotNull MergeEntityData mergeEntityData) {
this.connected.add(mergeEntityData);
this.connected.addAll(mergeEntityData.connected);
this.count += mergeEntityData.getCount();
mergeEntityData.setCount(0);
}
public LongOpenHashSet getOriginPositions() {
LongOpenHashSet positions = new LongOpenHashSet();
this.connected.forEach(entityData -> positions.add(entityData.entity.getPackedOriginPosition()));
return positions;
}
public boolean hasMerged() {
return !this.connected.isEmpty() && this.count != 0;
}
public void setMergeLevel(MergeLevel mergeLevel) {
this.mergeLevel = mergeLevel;
}
public MergeLevel getMergeLevel() {
return mergeLevel;
}
public void setCount(int count) {
this.count = count;
}
public int getCount() {
return this.count;
}
}

View File

@@ -0,0 +1,118 @@
package me.samsuik.sakura.entity.merge;
import me.samsuik.sakura.utils.collections.FixedSizeCustomObjectTable;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.entity.EntityTickList;
import org.jetbrains.annotations.NotNull;
public interface MergeStrategy {
/**
* If this merge strategy requires the merge history to be tracked.
*
* @return should track history
*/
boolean trackHistory();
/**
* Tries to merge the first entity into another entity.
* <p>
* The first entity should always be positioned right after the second entity in the
* {@link EntityTickList}. This method should only
* be called before the first entity and after the second entity has ticked.
*
* @param entity current entity
* @param previous last entity to tick
* @return success
*/
Entity mergeEntity(@NotNull Entity entity, @NotNull Entity previous, @NotNull TrackedMergeHistory mergeHistory);
/**
* Gets the {@link MergeStrategy} for the {@link MergeLevel}.
*
* @param level provided level
* @return strategy
*/
static MergeStrategy from(MergeLevel level) {
return switch (level) {
case NONE -> None.INSTANCE;
case STRICT -> Strict.INSTANCE;
case LENIENT -> Lenient.INSTANCE;
case SPAWN -> Spawn.INSTANCE;
};
}
final class None implements MergeStrategy {
private static final None INSTANCE = new None();
@Override
public boolean trackHistory() {
return false;
}
@Override
public Entity mergeEntity(@NotNull Entity entity, @NotNull Entity previous, @NotNull TrackedMergeHistory mergeHistory) {
return null;
}
}
final class Strict implements MergeStrategy {
private static final Strict INSTANCE = new Strict();
@Override
public boolean trackHistory() {
return false;
}
@Override
public Entity mergeEntity(@NotNull Entity entity, @NotNull Entity previous, @NotNull TrackedMergeHistory mergeHistory) {
return entity.compareState(previous) ? previous : null;
}
}
final class Lenient implements MergeStrategy {
private static final Lenient INSTANCE = new Lenient();
private final FixedSizeCustomObjectTable<Entity> entityTable = new FixedSizeCustomObjectTable<>(512, entity -> {
return entity.blockPosition().hashCode();
});
@Override
public boolean trackHistory() {
return true;
}
@Override
public Entity mergeEntity(@NotNull Entity entity, @NotNull Entity previous, @NotNull TrackedMergeHistory mergeHistory) {
if (entity.compareState(previous)) {
return previous;
}
Entity nextEntity = this.entityTable.getAndWrite(entity);
if (nextEntity == null || !nextEntity.level().equals(entity.level())) {
return null;
}
return mergeHistory.hasPreviousMerged(entity, nextEntity) && entity.compareState(nextEntity) ? nextEntity : null;
}
}
final class Spawn implements MergeStrategy {
private static final Spawn INSTANCE = new Spawn();
private static final MergeCondition CONDITION = (e, shots, time) -> (shots > 16 || time >= 200);
@Override
public boolean trackHistory() {
return true;
}
@Override
public Entity mergeEntity(@NotNull Entity entity, @NotNull Entity previous, @NotNull TrackedMergeHistory mergeHistory) {
final Entity mergeInto;
if (entity.tickCount == 1 && mergeHistory.hasPreviousMerged(entity, previous) && mergeHistory.hasMetCondition(previous, CONDITION)) {
mergeInto = previous;
} else {
mergeInto = entity.compareState(previous) ? previous : null;
}
return mergeInto;
}
}
}

View File

@@ -0,0 +1,22 @@
package me.samsuik.sakura.entity.merge;
import org.jetbrains.annotations.NotNull;
public interface MergeableEntity {
@NotNull MergeEntityData getMergeEntityData();
boolean isSafeToMergeInto(@NotNull MergeableEntity entity, boolean ticksLived);
default boolean respawnEntity() {
MergeEntityData mergeData = this.getMergeEntityData();
int count = mergeData.getCount();
if (count > 1) {
mergeData.setCount(0);
this.respawnEntity(count);
return true;
}
return false;
}
void respawnEntity(int count);
}

View File

@@ -0,0 +1,83 @@
package me.samsuik.sakura.entity.merge;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import me.samsuik.sakura.configuration.WorldConfiguration.Cannons.Mechanics.TNTSpread;
import me.samsuik.sakura.utils.TickExpiry;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.item.FallingBlockEntity;
import org.jetbrains.annotations.NotNull;
public final class TrackedMergeHistory {
private final Long2ObjectMap<PositionHistory> historyMap = new Long2ObjectOpenHashMap<>();
public boolean hasPreviousMerged(@NotNull Entity entity, @NotNull Entity into) {
PositionHistory positions = this.getHistory(into);
return positions != null && positions.has(entity.getPackedOriginPosition());
}
public void trackHistory(@NotNull Entity entity, @NotNull MergeEntityData mergeEntityData) {
long originPosition = entity.getPackedOriginPosition();
PositionHistory positions = this.historyMap.computeIfAbsent(originPosition, p -> new PositionHistory());
LongOpenHashSet originPositions = mergeEntityData.getOriginPositions();
boolean createHistory = positions.hasTimePassed(160);
if (createHistory && (entity instanceof FallingBlockEntity || entity.level().sakuraConfig().cannons.mechanics.tntSpread == TNTSpread.ALL)) {
originPositions.forEach(pos -> this.historyMap.put(pos, positions));
}
positions.trackPositions(originPositions, !createHistory);
}
public boolean hasMetCondition(@NotNull Entity entity, MergeCondition condition) {
PositionHistory positions = this.getHistory(entity);
return positions != null && positions.hasMetConditions(entity, condition);
}
private PositionHistory getHistory(Entity entity) {
long originPosition = entity.getPackedOriginPosition();
return this.historyMap.get(originPosition);
}
public void expire(int tick) {
this.historyMap.values().removeIf(p -> p.expiry().isExpired(tick));
}
private static final class PositionHistory {
private final LongOpenHashSet positions = new LongOpenHashSet();
private final TickExpiry expiry = new TickExpiry(MinecraftServer.currentTick, 200);
private final long created = MinecraftServer.currentTick;
private int cycles = 0;
public TickExpiry expiry() {
return this.expiry;
}
public boolean has(long position) {
this.expiry.refresh(MinecraftServer.currentTick);
return this.positions.contains(position);
}
public void trackPositions(LongOpenHashSet positions, boolean retain) {
if (retain) {
this.positions.retainAll(positions);
} else {
this.positions.addAll(positions);
}
this.cycles++;
}
public boolean hasMetConditions(@NotNull Entity entity, @NotNull MergeCondition condition) {
this.expiry.refresh(MinecraftServer.currentTick);
return condition.accept(entity, this.cycles, this.timeSinceCreation());
}
public boolean hasTimePassed(int ticks) {
return this.timeSinceCreation() > ticks;
}
private long timeSinceCreation() {
return MinecraftServer.currentTick - this.created;
}
}
}

View File

@@ -23,7 +23,7 @@ import java.util.function.LongConsumer;
public final class LocalConfigManager implements LocalStorageHandler {
private static final int SMALL_REGION_SIZE = 12;
private static final int CONFIG_CACHE_EXPIRATION = 600;
private final Map<LocalRegion, LocalValueStorage> storageMap = new Object2ObjectOpenHashMap<>();
private final List<LocalRegion> largeRegions = new ObjectArrayList<>();
private final Long2ObjectMap<List<LocalRegion>> smallRegions = new Long2ObjectOpenHashMap<>();
@@ -41,7 +41,7 @@ public final class LocalConfigManager implements LocalStorageHandler {
int regionX = x >> this.regionExponent;
int regionZ = z >> this.regionExponent;
long regionPos = ChunkPos.asLong(regionX, regionZ);
List<LocalRegion> regions = this.smallRegions.get(regionPos);
List<LocalRegion> regions = this.smallRegions.getOrDefault(regionPos, List.of());
for (LocalRegion region : Iterables.concat(regions, this.largeRegions)) {
if (region.contains(x, z)) {
return Optional.of(region);

View File

@@ -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;
}
}
}
}
}

View File

@@ -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<NamespacedKey, GuiComponent> componentsUnderKey = HashMultimap.create();
private final Object2ObjectMap<GuiComponent, NamespacedKey> 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<GuiComponent> getComponents() {
return ImmutableList.copyOf(this.componentKeys.keySet());
}
public ImmutableList<GuiComponent> findComponents(NamespacedKey key) {
return ImmutableList.copyOf(this.componentsUnderKey.get(key));
}
public Optional<GuiComponent> findFirst(NamespacedKey key) {
Collection<GuiComponent> components = this.componentsUnderKey.get(key);
return components.stream().findFirst();
}
public void removeComponents(NamespacedKey key) {
Collection<GuiComponent> 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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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<ItemStack> items;
private final int slot;
private final int selected;
private final GuiClickEvent whenClicked;
public ItemSwitch(List<ItemStack> 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));
}
}

View File

@@ -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<VisibilityType, VisibilityState> 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);
}
}

View File

@@ -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;
}
}

View File

@@ -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<VisibilityType, ImmutableList<ItemStack>> GUI_ITEMS = new Reference2ObjectOpenHashMap<>();
static final Reference2ObjectMap<VisibilityState, ItemStack> TOGGLE_BUTTON_ITEMS = new Reference2ObjectOpenHashMap<>();
static {
Reference2ObjectMap<VisibilityType, ItemStack> 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<ItemStack> 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;
}
}