Introduce a new hook system (right now only with Hologram support)

My plans with this hook system are to make it easier to understand and maintain – Hopefully it is also more flexible

Hooks now have a dedicated activation-life-cycle – I think this allows it to contain all the logic to do what
it needs to do but also provide some helper methods like `#canBeActivated`.
The activation should be used by the hooks to reduce the memory and performancec impact when not used.
The de-activation also allows hooks to clean up themselves and not rely on the plugins used by the hook or
the plugin using the hook to clean up.

For example:
You can see that the `DecentHologramsHook` has few class variables and they are kept as
small as possible and reasonable when not activated.
In `#deactivate` I call `#removeAll` to remove all holograms that still exist and I call `ArrayList#trimToSize` to reduce it's size again.
This has a similar effect to setting the class varaible null or to a new List but with this I can make that field *final*.

The exact implementation details and capabilities vary on the third-party plugins being supported by hooks,
so it needs to be flexible and easy to understand, so we can easily add support for more plugins and especially
new plugin categories like Maps (dynmap, BlueMap, PlexMap, ...).
This commit is contained in:
Christian Koop 2024-01-30 17:49:45 +01:00
parent d8564d1c1c
commit 638b793040
No known key found for this signature in database
GPG Key ID: 89A8181384E010A3
10 changed files with 454 additions and 0 deletions

View File

@ -7,6 +7,7 @@ import com.craftaro.core.database.DatabaseType;
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 abstract class SongodaPlugin extends JavaPlugin {
} catch (Exception ignored) {
}
this.hookRegistryManager.deactivateAllActiveHooks();
console.sendMessage(ChatColor.GREEN + "=============================");
console.sendMessage(" "); // blank line to separate chatter
}
@ -357,4 +362,8 @@ public abstract class SongodaPlugin extends JavaPlugin {
}
this.dataManager = dataManager;
}
public HookRegistryManager getHookManager() {
return this.hookRegistryManager;
}
}

View File

@ -0,0 +1,144 @@
package com.craftaro.core.hooks;
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<T extends Hook> extends HookRegistry<T> {
private final Plugin plugin;
private final Map<T, Integer> hooksWithPriority = new HashMap<>();
protected T activeHook = null;
public BaseHookRegistry(Plugin plugin) {
this.plugin = plugin;
}
public abstract void registerDefaultHooks();
public Optional<T> 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<T> getAll() {
// Use List.copyOf() when we upgrade to Java 10+
return Collections.unmodifiableList(new ArrayList<>(this.hooksWithPriority.keySet()));
}
@Override
public @NotNull List<String> 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<String> 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));
}
}
}

View File

@ -0,0 +1,24 @@
package com.craftaro.core.hooks;
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();
}

View File

@ -0,0 +1,17 @@
package com.craftaro.core.hooks;
/**
* Some handy constants for hook priorities intended to be used in
* {@link BaseHookRegistry#register(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() {
}
}

View File

@ -0,0 +1,28 @@
package com.craftaro.core.hooks;
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<T extends Hook> {
public abstract Optional<T> getActive();
public abstract void setActive(@Nullable T hook);
public abstract @NotNull List<String> 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<T> getAll();
}

View File

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

View File

@ -0,0 +1,71 @@
package com.craftaro.core.hooks.hologram;
import com.craftaro.core.hooks.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<String> lines);
/**
* @throws IllegalStateException if the hologram does not exist
*/
public abstract void update(@NotNull String id, @NotNull List<String> lines);
public abstract void updateBulk(@NotNull Map<String, List<String>> 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<String> 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);
}
}

View File

@ -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<HologramHook> {
public HologramHookRegistry(Plugin plugin) {
super(plugin);
}
@Override
public void registerDefaultHooks() {
register(new DecentHologramsHook(), HookPriority.HIGH);
}
}

View File

@ -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<String> 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<String> 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<String> 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<String, List<String>> hologramData) {
for (Map.Entry<String, List<String>> 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;
}
}

View File

@ -25,4 +25,17 @@ public class LocationUtils {
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
);
}
}