diff --git a/Core/src/main/java/com/craftaro/core/SongodaPlugin.java b/Core/src/main/java/com/craftaro/core/SongodaPlugin.java index 7506fa58..b7091ea4 100644 --- a/Core/src/main/java/com/craftaro/core/SongodaPlugin.java +++ b/Core/src/main/java/com/craftaro/core/SongodaPlugin.java @@ -7,6 +7,7 @@ import com.craftaro.core.dependency.Dependency; import com.craftaro.core.dependency.DependencyLoader; import com.craftaro.core.dependency.Relocation; +import com.craftaro.core.hooks.HookRegistryManager; import com.craftaro.core.locale.Locale; import com.craftaro.core.utils.Metrics; import com.craftaro.core.verification.CraftaroProductVerification; @@ -39,6 +40,8 @@ public abstract class SongodaPlugin extends JavaPlugin { private boolean licensePreventedPluginLoad = false; private boolean emergencyStop = false; + private final HookRegistryManager hookRegistryManager = new HookRegistryManager(this); + static { MinecraftVersion.getLogger().setLevel(Level.WARNING); MinecraftVersion.disableUpdateCheck(); @@ -221,6 +224,8 @@ public final void onDisable() { } catch (Exception ignored) { } + this.hookRegistryManager.deactivateAllActiveHooks(); + console.sendMessage(ChatColor.GREEN + "============================="); console.sendMessage(" "); // blank line to separate chatter } @@ -357,4 +362,8 @@ public void setDataManager(DataManager dataManager) { } this.dataManager = dataManager; } + + public HookRegistryManager getHookManager() { + return this.hookRegistryManager; + } } diff --git a/Core/src/main/java/com/craftaro/core/hooks/BaseHookRegistry.java b/Core/src/main/java/com/craftaro/core/hooks/BaseHookRegistry.java new file mode 100644 index 00000000..a60547ae --- /dev/null +++ b/Core/src/main/java/com/craftaro/core/hooks/BaseHookRegistry.java @@ -0,0 +1,145 @@ +package com.craftaro.core.hooks; + +import com.craftaro.core.hooks.holograms.Hook; +import org.bukkit.plugin.Plugin; +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.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * This hook registry makes use of priorities to automatically activate the highest priority hook that is available if no hook has been activated programmatically. + */ +public abstract class BaseHookRegistry extends HookRegistry { + private final Plugin plugin; + + private final Map hooksWithPriority = new HashMap<>(); + protected T activeHook = null; + + public BaseHookRegistry(Plugin plugin) { + this.plugin = plugin; + } + + public abstract void registerDefaultHooks(); + + public Optional getActive() { + if (this.activeHook == null) { + T hook = findFirstAvailableHook(); + if (hook != null) { + setActive(hook); + this.plugin.getLogger().info("Activated hook '" + hook.getName() + "'"); + } + + checkDependenciesOfAllHooksAndLogMissingOnes(); + } + return Optional.ofNullable(this.activeHook); + } + + public void setActive(@Nullable T hook) { + if (this.activeHook == hook) { + return; + } + + if (this.activeHook != null) { + this.activeHook.deactivate(); + } + + this.activeHook = hook; + if (this.activeHook != null) { + this.activeHook.activate(this.plugin); + } + } + + @Override + public @Nullable T get(String name) { + for (T hook : this.hooksWithPriority.keySet()) { + if (hook.getName().equalsIgnoreCase(name)) { + return hook; + } + } + return null; + } + + @Override + public @NotNull List getAll() { + // Use List.copyOf() when we upgrade to Java 10+ + return Collections.unmodifiableList(new ArrayList<>(this.hooksWithPriority.keySet())); + } + + @Override + public @NotNull List getAllNames() { + return this.hooksWithPriority + .keySet() + .stream() + .map(Hook::getName) + .sorted() + .collect(Collectors.toList()); + } + + @Override + public void register(@NotNull T hook) { + register(hook, 0); + } + + /** + * @see HookPriority + */ + public void register(@NotNull T hook, int priority) { + if (get(hook.getName()) != null) { + throw new IllegalArgumentException("Hook with name '" + hook.getName() + "' already registered"); + } + this.hooksWithPriority.put(hook, priority); + } + + @Override + public void unregister(@NotNull T hook) { + if (this.activeHook == hook) { + this.activeHook = null; + hook.deactivate(); + } + this.hooksWithPriority.remove(hook); + } + + @Override + public void clear() { + this.hooksWithPriority.clear(); + } + + protected @Nullable T findFirstAvailableHook() { + return this.hooksWithPriority + .entrySet() + .stream() + .sorted((o1, o2) -> o2.getValue().compareTo(o1.getValue())) + .filter((entry) -> entry.getKey().canBeActivated()) + .map(Map.Entry::getKey) + .findFirst() + .orElse(null); + } + + protected void checkDependenciesOfAllHooksAndLogMissingOnes() { + List missingDependencies = new ArrayList<>(0); + + for (T hook : getAll()) { + for (String pluginName : hook.getPluginDependencies()) { + if (this.plugin.getDescription().getDepend().contains(pluginName)) { + continue; + } + if (this.plugin.getDescription().getSoftDepend().contains(pluginName)) { + continue; + } + + missingDependencies.add(pluginName); + } + } + + if (!missingDependencies.isEmpty()) { + this.plugin.getLogger().warning("Nag author(s): Plugin accesses hooks that it does not declare dependance on: " + String.join(", ", missingDependencies)); + } + } +} diff --git a/Core/src/main/java/com/craftaro/core/hooks/HookPriority.java b/Core/src/main/java/com/craftaro/core/hooks/HookPriority.java new file mode 100644 index 00000000..371a8f96 --- /dev/null +++ b/Core/src/main/java/com/craftaro/core/hooks/HookPriority.java @@ -0,0 +1,17 @@ +package com.craftaro.core.hooks; + +/** + * Some handy constants for hook priorities intended to be used in + * {@link BaseHookRegistry#register(com.craftaro.core.hooks.holograms.Hook, int)} + */ +public final class HookPriority { + public static final int HIGHEST = 100; + public static final int HIGHER = 50; + public static final int HIGH = 10; + public static final int NORMAL = 0; + public static final int LOW = -10; + public static final int LOWER = -50; + + private HookPriority() { + } +} diff --git a/Core/src/main/java/com/craftaro/core/hooks/HookRegistry.java b/Core/src/main/java/com/craftaro/core/hooks/HookRegistry.java new file mode 100644 index 00000000..b57deea9 --- /dev/null +++ b/Core/src/main/java/com/craftaro/core/hooks/HookRegistry.java @@ -0,0 +1,29 @@ +package com.craftaro.core.hooks; + +import com.craftaro.core.hooks.holograms.Hook; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Optional; + +public abstract class HookRegistry { + public abstract Optional getActive(); + + public abstract void setActive(@Nullable T hook); + + public abstract @NotNull List getAllNames(); + + public abstract void register(@NotNull T hook); + + public abstract void unregister(@NotNull T hook); + + public abstract void clear(); + + @ApiStatus.Internal + public abstract @Nullable T get(String name); + + @ApiStatus.Internal + public abstract @NotNull List getAll(); +} diff --git a/Core/src/main/java/com/craftaro/core/hooks/HookRegistryManager.java b/Core/src/main/java/com/craftaro/core/hooks/HookRegistryManager.java new file mode 100644 index 00000000..b813149e --- /dev/null +++ b/Core/src/main/java/com/craftaro/core/hooks/HookRegistryManager.java @@ -0,0 +1,36 @@ +package com.craftaro.core.hooks; + +import com.craftaro.core.hooks.hologram.HologramHook; +import com.craftaro.core.hooks.hologram.HologramHookRegistry; +import org.bukkit.plugin.Plugin; + +import java.util.Optional; + +public class HookRegistryManager { + private final Plugin plugin; + + private HologramHookRegistry hologramRegistry; + + public HookRegistryManager(Plugin plugin) { + this.plugin = plugin; + } + + public Optional holograms() { + return getHologramRegistry().getActive(); + } + + public HologramHookRegistry getHologramRegistry() { + if (this.hologramRegistry == null) { + this.hologramRegistry = new HologramHookRegistry(this.plugin); + this.hologramRegistry.registerDefaultHooks(); + } + + return this.hologramRegistry; + } + + public void deactivateAllActiveHooks() { + if (this.hologramRegistry != null) { + this.hologramRegistry.setActive(null); + } + } +} diff --git a/Core/src/main/java/com/craftaro/core/hooks/hologram/HologramHook.java b/Core/src/main/java/com/craftaro/core/hooks/hologram/HologramHook.java new file mode 100644 index 00000000..894db781 --- /dev/null +++ b/Core/src/main/java/com/craftaro/core/hooks/hologram/HologramHook.java @@ -0,0 +1,71 @@ +package com.craftaro.core.hooks.hologram; + +import com.craftaro.core.hooks.holograms.Hook; +import com.craftaro.core.utils.LocationUtils; +import org.bukkit.Location; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public abstract class HologramHook implements Hook { + public abstract boolean exists(@NotNull String id); + + /** + * @throws IllegalStateException if the hologram already exists + * @see #createOrUpdateText(String, Location, List) + */ + public abstract void create(@NotNull String id, @NotNull Location location, @NotNull List lines); + + /** + * @throws IllegalStateException if the hologram does not exist + */ + public abstract void update(@NotNull String id, @NotNull List lines); + + public abstract void updateBulk(@NotNull Map> hologramData); + + public abstract void remove(@NotNull String id); + + public abstract void removeAll(); + + /** + * @see #create(String, Location, List) + */ + public void create(@NotNull String id, @NotNull Location location, @NotNull String text) { + create(id, location, Collections.singletonList(text)); + } + + public void createOrUpdateText(@NotNull String id, @NotNull Location location, @NotNull List lines) { + if (exists(id)) { + update(id, lines); + return; + } + + create(id, location, lines); + } + + /** + * @see #createOrUpdateText(String, Location, List) + */ + public void createOrUpdateText(@NotNull String id, @NotNull Location location, @NotNull String text) { + createOrUpdateText(id, location, Collections.singletonList(text)); + } + + /** + * @see #update(String, List) + */ + public void update(@NotNull String id, @NotNull String text) { + update(id, Collections.singletonList(text)); + } + + protected double getYOffset() { + return 1.5; + } + + protected @NotNull Location getNormalizedLocation(Location location) { + return LocationUtils + .getCenter(location) + .add(0, getYOffset(), 0); + } +} diff --git a/Core/src/main/java/com/craftaro/core/hooks/hologram/HologramHookRegistry.java b/Core/src/main/java/com/craftaro/core/hooks/hologram/HologramHookRegistry.java new file mode 100644 index 00000000..e1234f24 --- /dev/null +++ b/Core/src/main/java/com/craftaro/core/hooks/hologram/HologramHookRegistry.java @@ -0,0 +1,17 @@ +package com.craftaro.core.hooks.hologram; + +import com.craftaro.core.hooks.BaseHookRegistry; +import com.craftaro.core.hooks.HookPriority; +import com.craftaro.core.hooks.hologram.adapter.DecentHologramsHook; +import org.bukkit.plugin.Plugin; + +public class HologramHookRegistry extends BaseHookRegistry { + public HologramHookRegistry(Plugin plugin) { + super(plugin); + } + + @Override + public void registerDefaultHooks() { + register(new DecentHologramsHook(), HookPriority.HIGH); + } +} diff --git a/Core/src/main/java/com/craftaro/core/hooks/hologram/adapter/DecentHologramsHook.java b/Core/src/main/java/com/craftaro/core/hooks/hologram/adapter/DecentHologramsHook.java new file mode 100644 index 00000000..5a2e9ec1 --- /dev/null +++ b/Core/src/main/java/com/craftaro/core/hooks/hologram/adapter/DecentHologramsHook.java @@ -0,0 +1,95 @@ +package com.craftaro.core.hooks.hologram.adapter; + +import com.craftaro.core.hooks.hologram.HologramHook; +import eu.decentsoftware.holograms.api.DHAPI; +import eu.decentsoftware.holograms.api.holograms.Hologram; +import org.bukkit.Location; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class DecentHologramsHook extends HologramHook { + private static final String DECENT_HOLOGRAMS = "DecentHolograms"; + + private final ArrayList ourHologramIds = new ArrayList<>(0); + private String hologramNamePrefix; + + @Override + public String getName() { + return DECENT_HOLOGRAMS; + } + + @Override + public @NotNull String[] getPluginDependencies() { + return new String[] {DECENT_HOLOGRAMS}; + } + + @Override + public void activate(Plugin plugin) { + this.hologramNamePrefix = plugin.getClass().getName().replace('.', '_') + "-"; + } + + @Override + public void deactivate() { + removeAll(); + this.hologramNamePrefix = null; + } + + @Override + public boolean exists(@NotNull String id) { + return DHAPI.getHologram(getHologramName(id)) != null; + } + + @Override + public void create(@NotNull String id, @NotNull Location location, @NotNull List lines) { + if (exists(id)) { + throw new IllegalStateException("Cannot create hologram that already exists: " + getHologramName(id)); + } + + DHAPI.createHologram(getHologramName(id), getNormalizedLocation(location), lines); + this.ourHologramIds.add(id); + } + + @Override + public void update(@NotNull String id, @NotNull List lines) { + Hologram hologram = DHAPI.getHologram(getHologramName(id)); + if (hologram == null) { + throw new IllegalStateException("Cannot update hologram that does not exist: " + getHologramName(id)); + } + + DHAPI.setHologramLines(hologram, lines); + } + + @Override + public void updateBulk(@NotNull Map> hologramData) { + for (Map.Entry> entry : hologramData.entrySet()) { + update(entry.getKey(), entry.getValue()); + } + } + + @Override + public void remove(@Nullable String id) { + DHAPI.removeHologram(getHologramName(id)); + this.ourHologramIds.remove(id); + } + + @Override + public void removeAll() { + for (String id : this.ourHologramIds) { + DHAPI.removeHologram(getHologramName(id)); + } + this.ourHologramIds.clear(); + this.ourHologramIds.trimToSize(); + } + + private String getHologramName(String id) { + if (this.hologramNamePrefix == null) { + throw new IllegalStateException("Hook has not been activated yet"); + } + return this.hologramNamePrefix + id; + } +} diff --git a/Core/src/main/java/com/craftaro/core/hooks/holograms/Hook.java b/Core/src/main/java/com/craftaro/core/hooks/holograms/Hook.java new file mode 100644 index 00000000..2cc0ada8 --- /dev/null +++ b/Core/src/main/java/com/craftaro/core/hooks/holograms/Hook.java @@ -0,0 +1,24 @@ +package com.craftaro.core.hooks.holograms; + +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +public interface Hook { + String getName(); + + @NotNull String[] getPluginDependencies(); + + default boolean canBeActivated() { + for (String pluginName : getPluginDependencies()) { + if (!Bukkit.getPluginManager().isPluginEnabled(pluginName)) { + return false; + } + } + return true; + } + + void activate(Plugin plugin); + + void deactivate(); +} diff --git a/Core/src/main/java/com/craftaro/core/utils/LocationUtils.java b/Core/src/main/java/com/craftaro/core/utils/LocationUtils.java index 475c128d..9d6f35d5 100644 --- a/Core/src/main/java/com/craftaro/core/utils/LocationUtils.java +++ b/Core/src/main/java/com/craftaro/core/utils/LocationUtils.java @@ -25,4 +25,17 @@ public static boolean isInArea(Location location, Location pos1, Location pos2) location.getY() >= y1 && location.getY() <= y2 && location.getZ() >= z1 && location.getZ() <= z2; } + + public static Location getCenter(Location location) { + double xOffset = location.getBlockX() > 0 ? 0.5 : -0.5; + double zOffset = location.getBlockZ() > 0 ? 0.5 : -0.5; + return new Location( + location.getWorld(), + location.getBlockX() + xOffset, + location.getBlockY(), + location.getBlockZ() + zOffset, + 0, + 0 + ); + } }