479 lines
15 KiB
Java
479 lines
15 KiB
Java
package com.craftaro.epicanchors;
|
|
|
|
import com.craftaro.core.SongodaPlugin;
|
|
import com.craftaro.core.compatibility.CompatibleMaterial;
|
|
import com.craftaro.core.compatibility.CompatibleParticleHandler;
|
|
import com.craftaro.core.hooks.HologramManager;
|
|
import com.craftaro.core.third_party.de.tr7zw.nbtapi.NBTItem;
|
|
import com.craftaro.core.utils.TextUtils;
|
|
import com.craftaro.core.utils.TimeUtils;
|
|
import com.craftaro.epicanchors.api.Anchor;
|
|
import com.craftaro.epicanchors.api.AnchorAccessCheck;
|
|
import com.craftaro.epicanchors.api.AnchorManager;
|
|
import com.craftaro.epicanchors.files.AnchorsDataManager;
|
|
import com.craftaro.epicanchors.files.Settings;
|
|
import com.craftaro.epicanchors.utils.Callback;
|
|
import com.craftaro.epicanchors.utils.UpdateCallback;
|
|
import com.craftaro.epicanchors.utils.Utils;
|
|
import com.craftaro.third_party.com.cryptomorin.xseries.XMaterial;
|
|
import com.craftaro.third_party.com.cryptomorin.xseries.XSound;
|
|
import org.bukkit.Bukkit;
|
|
import org.bukkit.Chunk;
|
|
import org.bukkit.Location;
|
|
import org.bukkit.Material;
|
|
import org.bukkit.OfflinePlayer;
|
|
import org.bukkit.World;
|
|
import org.bukkit.block.Block;
|
|
import org.bukkit.entity.Player;
|
|
import org.bukkit.inventory.ItemStack;
|
|
import org.bukkit.inventory.meta.ItemMeta;
|
|
import org.jetbrains.annotations.NotNull;
|
|
import org.jetbrains.annotations.Nullable;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.LinkedList;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
import java.util.Set;
|
|
import java.util.UUID;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
public class AnchorManagerImpl implements AnchorManager {
|
|
private static final String ERR_WORLD_NOT_READY = "EpicAnchors has not finished initializing that world yet";
|
|
private static final String HOLOGRAM_PREFIX = "Anchor_";
|
|
|
|
private final SongodaPlugin plugin;
|
|
private final AnchorsDataManager dataManager;
|
|
|
|
private final Map<World, Set<Anchor>> anchors = new HashMap<>(3);
|
|
private final Set<Player> visualizedChunk = new HashSet<>();
|
|
private final List<AnchorAccessCheck> accessChecks = new LinkedList<>();
|
|
|
|
private boolean ready;
|
|
|
|
public AnchorManagerImpl(SongodaPlugin plugin, AnchorsDataManager dataManager) {
|
|
this.plugin = Objects.requireNonNull(plugin);
|
|
this.dataManager = Objects.requireNonNull(dataManager);
|
|
}
|
|
|
|
protected void saveAll() {
|
|
for (Set<Anchor> anchorSet : this.anchors.values()) {
|
|
this.dataManager.updateAnchors(anchorSet, null);
|
|
}
|
|
}
|
|
|
|
protected void deInitAll() {
|
|
for (World world : this.anchors.keySet().toArray(new World[0])) {
|
|
deInitAnchors(world);
|
|
}
|
|
}
|
|
|
|
protected void initAnchorsAsync(@NotNull World world, @Nullable UpdateCallback callback) {
|
|
if (this.anchors.containsKey(world)) {
|
|
if (callback != null) {
|
|
callback.accept(null);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
long start = System.nanoTime();
|
|
|
|
this.dataManager.getAnchors(world, (ex, result) -> {
|
|
if (ex == null) {
|
|
this.anchors.computeIfAbsent(world, key -> new HashSet<>());
|
|
|
|
for (Anchor anchor : result) {
|
|
((AnchorImpl) anchor).init(this.plugin);
|
|
|
|
this.anchors.computeIfAbsent(anchor.getWorld(), key -> new HashSet<>())
|
|
.add(anchor);
|
|
}
|
|
|
|
long end = System.nanoTime();
|
|
this.plugin.getLogger().info("Initialized " + this.anchors.get(world).size() + " anchors in world '" + world.getName() + "' " +
|
|
"(" + TimeUnit.NANOSECONDS.toMillis(end - start) + "ms)");
|
|
|
|
if (callback != null) {
|
|
callback.accept(null);
|
|
}
|
|
} else {
|
|
if (callback != null) {
|
|
callback.accept(ex);
|
|
} else {
|
|
Utils.logException(this.plugin, ex, "H2");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
protected void deInitAnchors(@NotNull World world) {
|
|
Set<Anchor> tmpAnchors = this.anchors.remove(world);
|
|
|
|
if (tmpAnchors != null) {
|
|
this.dataManager.updateAnchors(tmpAnchors, null);
|
|
|
|
for (Anchor anchor : tmpAnchors) {
|
|
((AnchorImpl) anchor).deInit(this.plugin);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected void setReady() {
|
|
this.ready = true;
|
|
|
|
Bukkit.getScheduler().runTaskTimer(this.plugin, this::saveAll, 20L * 60 * 5, 20L * 60 * 5);
|
|
}
|
|
|
|
@Override
|
|
public boolean isReady(World world) {
|
|
return this.ready && this.anchors.containsKey(world);
|
|
}
|
|
|
|
/* Getter */
|
|
|
|
@Override
|
|
public Anchor[] getAnchors(@NotNull World world) {
|
|
Set<Anchor> set = this.anchors.get(world);
|
|
|
|
if (set != null) {
|
|
return set.toArray(new Anchor[0]);
|
|
}
|
|
|
|
return new Anchor[0];
|
|
}
|
|
|
|
@Override
|
|
public @Nullable Anchor getAnchor(@NotNull Block block) {
|
|
if (!isReady(block.getWorld())) {
|
|
throw new IllegalStateException(ERR_WORLD_NOT_READY);
|
|
}
|
|
|
|
Location bLoc = block.getLocation();
|
|
Set<Anchor> set = this.anchors.get(block.getWorld());
|
|
|
|
if (set != null) {
|
|
for (Anchor anchor : set) {
|
|
if (anchor.getLocation().equals(bLoc)) {
|
|
return anchor;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public boolean isAnchor(@NotNull Block block) {
|
|
return getAnchor(block) != null;
|
|
}
|
|
|
|
@Override
|
|
public boolean hasAnchor(@NotNull Chunk chunk) {
|
|
if (!isReady(chunk.getWorld())) {
|
|
throw new IllegalStateException(ERR_WORLD_NOT_READY);
|
|
}
|
|
|
|
Set<Anchor> set = this.anchors.get(chunk.getWorld());
|
|
|
|
if (set != null) {
|
|
for (Anchor anchor : set) {
|
|
if (anchor.getChunk().equals(chunk)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
@SuppressWarnings("unused")
|
|
public List<Anchor> searchAnchors(Location center, double searchRadius) {
|
|
return searchAnchors(center, searchRadius, false);
|
|
}
|
|
|
|
@Override
|
|
public List<Anchor> searchAnchors(Location center, double searchRadius, boolean ignoreHeight) {
|
|
List<Anchor> result = new ArrayList<>();
|
|
|
|
if (ignoreHeight) {
|
|
center = center.clone();
|
|
center.setY(0);
|
|
}
|
|
|
|
for (Anchor anchor : getAnchors(center.getWorld())) {
|
|
Location loc = anchor.getLocation();
|
|
|
|
if (ignoreHeight) {
|
|
loc.setY(0);
|
|
}
|
|
|
|
if (center.distance(loc) <= searchRadius) {
|
|
result.add(anchor);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/* Create 'n Destroy */
|
|
|
|
@Override
|
|
public void createAnchor(@NotNull Location loc, @NotNull UUID owner, int ticks, @Nullable Callback<Anchor> callback) {
|
|
if (!isReady(loc.getWorld())) {
|
|
throw new IllegalStateException(ERR_WORLD_NOT_READY);
|
|
}
|
|
|
|
this.dataManager.insertAnchorAsync(loc, Objects.requireNonNull(owner), ticks, (ex, anchor) -> {
|
|
if (ex != null) {
|
|
if (callback != null) {
|
|
callback.accept(ex, null);
|
|
} else {
|
|
Utils.logException(this.plugin, ex, "H2");
|
|
}
|
|
} else {
|
|
Bukkit.getScheduler().runTask(this.plugin, () -> {
|
|
Block block = loc.getBlock();
|
|
block.setType(Settings.MATERIAL.getMaterial().parseMaterial());
|
|
|
|
this.anchors.computeIfAbsent(anchor.getWorld(), key -> new HashSet<>())
|
|
.add(anchor);
|
|
|
|
updateHologram(anchor);
|
|
|
|
if (callback != null) {
|
|
callback.accept(null, anchor);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
@Override
|
|
public void destroyAnchor(@NotNull Anchor anchor) {
|
|
destroyAnchor(anchor, false);
|
|
}
|
|
|
|
@Override
|
|
public void destroyAnchor(@NotNull Anchor anchor, boolean forceSkipItemDrop) {
|
|
if (!isReady(anchor.getWorld())) {
|
|
throw new IllegalStateException(ERR_WORLD_NOT_READY);
|
|
}
|
|
|
|
for (Set<Anchor> value : this.anchors.values()) {
|
|
value.remove(anchor);
|
|
}
|
|
|
|
removeHologram(anchor);
|
|
|
|
Location anchorLoc = anchor.getLocation();
|
|
Block anchorBlock = anchorLoc.getBlock();
|
|
Material anchorMaterial = anchorBlock.getType();
|
|
|
|
if (anchorBlock.getType() == Settings.MATERIAL.getMaterial().parseMaterial()) {
|
|
anchorBlock.setType(Material.AIR);
|
|
}
|
|
|
|
// Drop anchor as an item
|
|
if (!forceSkipItemDrop &&
|
|
Settings.ALLOW_ANCHOR_BREAKING.getBoolean() &&
|
|
(anchor.isInfinite() || anchor.getTicksLeft() >= 20)) {
|
|
anchor.getWorld().dropItemNaturally(anchorLoc, createAnchorItem(anchor.getTicksLeft(), anchorMaterial));
|
|
}
|
|
|
|
// Particles & Sound
|
|
XSound.ENTITY_GENERIC_EXPLODE.play(anchorLoc, 10, 10);
|
|
CompatibleParticleHandler.spawnParticles(CompatibleParticleHandler.ParticleType.getParticle(Settings.PARTICLE_DESTROY.getString()),
|
|
anchor.getLocation().add(.5, .5, .5), 100, .5, .5, .5);
|
|
|
|
((AnchorImpl) anchor).deInit(this.plugin);
|
|
this.dataManager.deleteAnchorAsync(anchor);
|
|
}
|
|
|
|
/* Anchor access */
|
|
|
|
@Override
|
|
public void registerAccessCheck(AnchorAccessCheck accessCheck) {
|
|
if (!this.accessChecks.contains(accessCheck)) {
|
|
// Adding at the start of the list makes sure the default check is
|
|
this.accessChecks.add(accessCheck);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean unregisterAccessCheck(AnchorAccessCheck accessCheck) {
|
|
return this.accessChecks.remove(accessCheck);
|
|
}
|
|
|
|
@Override
|
|
public boolean hasAccess(@NotNull Anchor anchor, @NotNull OfflinePlayer p) {
|
|
return hasAccess(anchor, p.getUniqueId());
|
|
}
|
|
|
|
@Override
|
|
public boolean hasAccess(@NotNull Anchor anchor, @NotNull UUID uuid) {
|
|
if (anchor.isLegacy() || anchor.getOwner().equals(uuid)) return true;
|
|
|
|
for (AnchorAccessCheck accessCheck : this.accessChecks) {
|
|
if (accessCheck.check(anchor, uuid)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/* Anchor item */
|
|
|
|
@Override
|
|
public ItemStack createAnchorItem(int ticks) {
|
|
return createAnchorItem(ticks, Settings.MATERIAL.getMaterial());
|
|
}
|
|
|
|
@Override
|
|
public ItemStack createAnchorItem(int ticks, Material material) {
|
|
return createAnchorItem(ticks, CompatibleMaterial.getMaterial(material).get());
|
|
}
|
|
|
|
@Override
|
|
public ItemStack createAnchorItem(int ticks, XMaterial material) {
|
|
if (ticks <= 0 && ticks != -1) throw new IllegalArgumentException();
|
|
|
|
ItemStack item = material.parseItem();
|
|
ItemMeta meta = item.getItemMeta();
|
|
|
|
assert meta != null;
|
|
meta.setDisplayName(formatAnchorText(ticks, false));
|
|
meta.setLore(TextUtils.formatText(Settings.LORE.getString().split("\r?\n")));
|
|
item.setItemMeta(meta);
|
|
|
|
NBTItem nbtItem = new NBTItem(item);
|
|
nbtItem.setInteger(NBT_TICKS_KEY, ticks);
|
|
|
|
return nbtItem.getItem();
|
|
}
|
|
|
|
/* Chunk visualization */
|
|
|
|
@Override
|
|
public boolean toggleChunkVisualized(Player p) {
|
|
boolean visualize = !hasChunksVisualized(p);
|
|
|
|
setChunksVisualized(p, visualize);
|
|
|
|
return visualize;
|
|
}
|
|
|
|
@Override
|
|
public void setChunksVisualized(Player p, boolean visualize) {
|
|
if (visualize) {
|
|
this.visualizedChunk.add(p);
|
|
} else {
|
|
this.visualizedChunk.remove(p);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
|
public boolean hasChunksVisualized(Player p) {
|
|
return this.visualizedChunk.contains(p);
|
|
}
|
|
|
|
/* Holograms */
|
|
|
|
@Override
|
|
public void updateHolograms(List<Anchor> anchors) {
|
|
// are holograms enabled?
|
|
if (!Settings.HOLOGRAMS.getBoolean() || !HologramManager.getManager().isEnabled()) return;
|
|
|
|
Map<String, List<String>> hologramData = new HashMap<>(anchors.size());
|
|
|
|
for (Anchor anchor : anchors) {
|
|
List<String> lines = Collections.singletonList(formatAnchorText(anchor.getTicksLeft(), true));
|
|
|
|
if (!HologramManager.isHologramLoaded(HOLOGRAM_PREFIX + anchor.getDbId())) {
|
|
HologramManager.createHologram(HOLOGRAM_PREFIX + anchor.getDbId(), anchor.getLocation(), lines);
|
|
continue;
|
|
}
|
|
|
|
hologramData.put(HOLOGRAM_PREFIX + anchor.getDbId(), lines);
|
|
}
|
|
|
|
// Create the holograms
|
|
HologramManager.bulkUpdateHolograms(hologramData);
|
|
}
|
|
|
|
private void updateHologram(Anchor anchor) {
|
|
updateHolograms(Collections.singletonList(anchor));
|
|
}
|
|
|
|
private String formatAnchorText(int ticks, boolean shorten) {
|
|
String remaining;
|
|
|
|
if (ticks < 0) {
|
|
remaining = this.plugin.getLocale().getMessage("Infinite").getMessage();
|
|
} else {
|
|
long millis = (ticks / 20L) * 1000L;
|
|
|
|
remaining = TimeUtils.makeReadable(millis);
|
|
|
|
if (shorten && millis > 60 * 5 * 1000 /* 5 minutes */ &&
|
|
remaining.charAt(remaining.length() - 1) == 's') {
|
|
int i = remaining.lastIndexOf(' ');
|
|
|
|
remaining = remaining.substring(0, i);
|
|
}
|
|
|
|
if (remaining.isEmpty()) {
|
|
remaining = "0s";
|
|
}
|
|
}
|
|
|
|
return TextUtils.formatText(Settings.NAME_TAG.getString().replace("{REMAINING}", remaining));
|
|
}
|
|
|
|
public static int getTicksFromItem(ItemStack item) {
|
|
if (item == null || item.getType() == Material.AIR) {
|
|
return 0;
|
|
}
|
|
|
|
NBTItem nbtItem = new NBTItem(item);
|
|
|
|
if (nbtItem.hasTag(NBT_TICKS_KEY)) {
|
|
return nbtItem.getInteger(NBT_TICKS_KEY);
|
|
}
|
|
|
|
// Legacy code (pre v2) to stay cross-version compatible
|
|
if (Settings.MATERIAL.getMaterial().parseMaterial() == item.getType()) {
|
|
if (nbtItem.hasTag("ticks")) {
|
|
int result = nbtItem.getInteger("ticks");
|
|
|
|
return result == -99 ? -1 : result;
|
|
}
|
|
|
|
// Tries to get the ticks remaining from hidden text
|
|
if (item.hasItemMeta() &&
|
|
item.getItemMeta().hasDisplayName() &&
|
|
item.getItemMeta().getDisplayName().contains(":")) {
|
|
try {
|
|
int result = Integer.parseInt(item.getItemMeta().getDisplayName().replace("§", "").split(":")[0]);
|
|
|
|
return result == -99 ? -1 : result;
|
|
} catch (NumberFormatException ignore) {
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private static void removeHologram(Anchor anchor) {
|
|
HologramManager.removeHologram(HOLOGRAM_PREFIX + anchor.getDbId());
|
|
}
|
|
}
|