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:
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user