Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Start Support addons loader #53

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package fr.euphyllia.skyllia.api.addons;

public enum AddonLoadPhase {
BEFORE,
AFTER
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package fr.euphyllia.skyllia.api.addons;

import org.bukkit.plugin.Plugin;

public interface SkylliaAddon {

/**
* Indicates the loading phase in which the addon wishes to be initialized.
*/
AddonLoadPhase getLoadPhase();

/**
* Called when the addon is detected, according to its loading phase (BEFORE/AFTER).
*/
void onLoad(Plugin plugin);

/**
* Called immediately after onLoad (within the same phase).
* Useful if you want to separate configuration/initialization (onLoad)
* from the actual activation (onEnable).
*/
void onEnable();

/**
* Called when the addon is deactivated.
*/
void onDisabled();
}
204 changes: 73 additions & 131 deletions plugin/src/main/java/fr/euphyllia/skyllia/Main.java
Original file line number Diff line number Diff line change
@@ -1,194 +1,136 @@
package fr.euphyllia.skyllia;

import fr.euphyllia.skyllia.addons.AddonLoader;
import fr.euphyllia.skyllia.api.InterneAPI;
import fr.euphyllia.skyllia.api.addons.AddonLoadPhase;
import fr.euphyllia.skyllia.api.commands.SubCommandRegistry;
import fr.euphyllia.skyllia.api.exceptions.UnsupportedMinecraftVersionException;
import fr.euphyllia.skyllia.api.utils.Metrics;
import fr.euphyllia.skyllia.api.utils.VersionUtils;
import fr.euphyllia.skyllia.commands.admin.SkylliaAdminCommand;
import fr.euphyllia.skyllia.commands.admin.SubAdminCommandImpl;
import fr.euphyllia.skyllia.commands.common.SkylliaCommand;
import fr.euphyllia.skyllia.commands.common.SubCommandImpl;
import fr.euphyllia.skyllia.cache.CacheScheduler;
import fr.euphyllia.skyllia.commands.CommandRegistrar;
import fr.euphyllia.skyllia.configuration.ConfigManager;
import fr.euphyllia.skyllia.configuration.ConfigToml;
import fr.euphyllia.skyllia.configuration.LanguageToml;
import fr.euphyllia.skyllia.configuration.PermissionsToml;
import fr.euphyllia.skyllia.listeners.bukkitevents.blocks.BlockEvent;
import fr.euphyllia.skyllia.listeners.bukkitevents.blocks.PistonEvent;
import fr.euphyllia.skyllia.listeners.bukkitevents.entity.DamageEvent;
import fr.euphyllia.skyllia.listeners.bukkitevents.folia.PortalAlternativeFoliaEvent;
import fr.euphyllia.skyllia.listeners.bukkitevents.gamerule.BlockGameRuleEvent;
import fr.euphyllia.skyllia.listeners.bukkitevents.gamerule.entity.ExplosionEvent;
import fr.euphyllia.skyllia.listeners.bukkitevents.gamerule.entity.GriefingEvent;
import fr.euphyllia.skyllia.listeners.bukkitevents.gamerule.entity.MobSpawnEvent;
import fr.euphyllia.skyllia.listeners.bukkitevents.gamerule.entity.PickupEvent;
import fr.euphyllia.skyllia.listeners.bukkitevents.paper.PortalAlternativePaperEvent;
import fr.euphyllia.skyllia.listeners.bukkitevents.player.*;
import fr.euphyllia.skyllia.listeners.skyblockevents.SkyblockEvent;
import fr.euphyllia.skyllia.managers.Managers;
import fr.euphyllia.skyllia.sgbd.exceptions.DatabaseException;
import io.papermc.paper.command.brigadier.Commands;
import io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager;
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
import fr.euphyllia.skyllia.listeners.ListenersRegistrar;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
* Main class of the plugin, coordinating initialization, enabling, and disabling.
*/
public class Main extends JavaPlugin {

private final Logger logger = LogManager.getLogger(this);
private static final Logger LOGGER = LogManager.getLogger(Main.class);

private InterneAPI interneAPI;

private SubCommandRegistry commandRegistry;
private SubCommandRegistry adminCommandRegistry;

@Override
public void onLoad() {
// Load addons which must be loaded before the plugin is fully enabled
new AddonLoader(this, LOGGER).loadAddons(AddonLoadPhase.BEFORE);
}

@Override
public void onEnable() {
// Initialize the internal API (checks server version compatibility)
if (!initializeInterneAPI()) {
return;
return; // Stop if version is not supported
}

if (!loadConfigurations()) {
// Load and check all configurations (config.toml, language.toml, permissions.toml)
ConfigManager configManager = new ConfigManager(this, LOGGER);
if (!configManager.loadConfigurations(interneAPI)) {
// If config load fails, the plugin is disabled (inside loadConfigurations)
return;
}

initializeCommandsDispatcher();
// Register commands via CommandRegistrar
CommandRegistrar commandRegistrar = new CommandRegistrar(this);
commandRegistrar.registerCommands();
// Récupérer les SubCommandRegistry si besoin
this.commandRegistry = commandRegistrar.getCommandRegistry();
this.adminCommandRegistry = commandRegistrar.getAdminCommandRegistry();

initializeManagers();
registerListeners();
scheduleCacheUpdate();
// Initialize managers
this.interneAPI.setManagers(new fr.euphyllia.skyllia.managers.Managers(interneAPI));
this.interneAPI.getManagers().init();

// Register listeners
new ListenersRegistrar(this, interneAPI, LOGGER).registerListeners();

// Schedule cache updates
new CacheScheduler(this, interneAPI, LOGGER).scheduleCacheUpdate();

// Check server configs for Nether/End warnings
checkDisabledConfig();

// Load addons which must be loaded after the plugin is fully enabled
new AddonLoader(this, LOGGER).loadAddons(AddonLoadPhase.AFTER);

// bStats metrics
new Metrics(this, 20874);
}

@Override
public void onDisable() {
// Disable all loaded addons
AddonLoader.disableAllAddons();

// Cancel scheduled tasks
Bukkit.getAsyncScheduler().cancelTasks(this);
Bukkit.getGlobalRegionScheduler().cancelTasks(this);

// Close DB if needed
if (this.interneAPI != null && this.interneAPI.getDatabaseLoader() != null) {
this.interneAPI.getDatabaseLoader().closeDatabase();
}
}

public InterneAPI getInterneAPI() {
return this.interneAPI;
}

public @NotNull SubCommandRegistry getCommandRegistry() {
return commandRegistry;
}

public @NotNull SubCommandRegistry getAdminCommandRegistry() {
return adminCommandRegistry;
}

/**
* Initializes the internal API, handling version compatibility.
*
* @return true if initialization succeeded, false otherwise
*/
private boolean initializeInterneAPI() {
try {
this.interneAPI = new InterneAPI(this);
this.interneAPI.loadAPI();
return true;
} catch (UnsupportedMinecraftVersionException e) {
logger.log(Level.FATAL, e.getMessage(), e);
LOGGER.log(Level.FATAL, e.getMessage(), e);
Bukkit.getPluginManager().disablePlugin(this);
return false;
}
}

private boolean loadConfigurations() {
try {
this.interneAPI.setupFirstSchematic(getDataFolder(), getResource("schematics/default.schem"));
if (!this.interneAPI.setupConfigs(getDataFolder(), "config.toml", ConfigToml::init) ||
!this.interneAPI.setupConfigs(getDataFolder(), "language.toml", LanguageToml::init) ||
!this.interneAPI.setupConfigs(getDataFolder(), "permissions.toml", PermissionsToml::init) ||
!this.interneAPI.setupSGBD()) {
Bukkit.getPluginManager().disablePlugin(this);
return false;
/**
* Checks if Nether/End are disabled, and logs a warning if they are not (based on config).
*/
private void checkDisabledConfig() {
if (fr.euphyllia.skyllia.api.utils.VersionUtils.IS_FOLIA && !ConfigToml.suppressWarningNetherEndEnabled) {
if (Bukkit.getAllowNether()) {
LOGGER.log(Level.WARN, "Disable nether in server.properties to disable nether portals!");
}
if (Bukkit.getAllowEnd()) {
LOGGER.log(Level.WARN, "Disable end in bukkit.yml to disable end portals!");
}
return true;
} catch (DatabaseException | IOException exception) {
logger.log(Level.FATAL, exception, exception);
Bukkit.getPluginManager().disablePlugin(this);
return false;
}
}

private void initializeManagers() {
this.interneAPI.setManagers(new Managers(interneAPI));
this.interneAPI.getManagers().init();
this.commandRegistry = new SubCommandImpl();
this.adminCommandRegistry = new SubAdminCommandImpl();
}

private void registerListeners() {
PluginManager pluginManager = getServer().getPluginManager();

// Bukkit Events
registerEvent(pluginManager, new JoinEvent(this.interneAPI));
registerEvent(pluginManager, new BlockEvent(this.interneAPI));
registerEvent(pluginManager, new InventoryEvent(this.interneAPI));
registerEvent(pluginManager, new PlayerEvent(this.interneAPI));
registerEvent(pluginManager, new DamageEvent(this.interneAPI));
registerEvent(pluginManager, new InteractEvent(this.interneAPI));
registerEvent(pluginManager, new TeleportEvent(this.interneAPI)); // TODO: Doesn't work with Folia 1.19.4-1.21.4
registerEvent(pluginManager, new PistonEvent(this.interneAPI));

if (VersionUtils.IS_FOLIA) {
registerEvent(pluginManager, new PortalAlternativeFoliaEvent(this.interneAPI));
}
if (VersionUtils.IS_PAPER) {
registerEvent(pluginManager, new PortalAlternativePaperEvent());
}

// GameRule Events
registerEvent(pluginManager, new BlockGameRuleEvent(this.interneAPI));
registerEvent(pluginManager, new ExplosionEvent(this.interneAPI));
registerEvent(pluginManager, new GriefingEvent(this.interneAPI));
registerEvent(pluginManager, new MobSpawnEvent(this.interneAPI));
registerEvent(pluginManager, new PickupEvent(this.interneAPI));

// Skyblock Event
registerEvent(pluginManager, new SkyblockEvent(this.interneAPI));
}

private void registerEvent(PluginManager pluginManager, Object listener) {
pluginManager.registerEvents((org.bukkit.event.Listener) listener, this);
}

private void scheduleCacheUpdate() {
Runnable cacheUpdateTask = () -> Bukkit.getOnlinePlayers().forEach(player -> this.interneAPI.updateCache(player));

Bukkit.getAsyncScheduler().runAtFixedRate(this, task -> cacheUpdateTask.run(), 1, ConfigToml.updateCacheTimer, TimeUnit.SECONDS);
public InterneAPI getInterneAPI() {
return this.interneAPI;
}

private void checkDisabledConfig() {
/* Since 1.20.3, there is a gamerule that allows you to increase the number of ticks between entering a portal and teleporting.
This makes the configuration possibly useless.
BUT just in case, I leave the message enabled by default.
*/
if (VersionUtils.IS_FOLIA && !ConfigToml.suppressWarningNetherEndEnabled) {
if (Bukkit.getAllowNether()) {
logger.log(Level.WARN, "Disable nether in server.properties to disable nether portals!");
}
if (Bukkit.getAllowEnd()) {
logger.log(Level.WARN, "Disable end in bukkit.yml to disable end portals!");
}
}
public SubCommandRegistry getCommandRegistry() {
return commandRegistry;
}

private void initializeCommandsDispatcher() {
LifecycleEventManager<Plugin> manager = this.getLifecycleManager();
manager.registerEventHandler(LifecycleEvents.COMMANDS, event -> {
final Commands commands = event.registrar();
commands.register("skyllia", "Islands commands", List.of("is"), new SkylliaCommand(this));
commands.register("skylliaadmin", "Administrator commands", List.of("isadmin"), new SkylliaAdminCommand(this));
});
public SubCommandRegistry getAdminCommandRegistry() {
return adminCommandRegistry;
}
}
87 changes: 87 additions & 0 deletions plugin/src/main/java/fr/euphyllia/skyllia/addons/AddonLoader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package fr.euphyllia.skyllia.addons;

import fr.euphyllia.skyllia.Main;
import fr.euphyllia.skyllia.api.addons.AddonLoadPhase;
import fr.euphyllia.skyllia.api.addons.SkylliaAddon;
import org.apache.logging.log4j.Logger;

import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;

/**
* This class scans and loads addons from the /addons folder.
*/
public class AddonLoader {

private static final List<SkylliaAddon> LOADED_ADDONS = new ArrayList<>();

private final Main plugin;
private final Logger logger;

/**
* Constructs an AddonLoader.
*
* @param plugin the main plugin instance
* @param logger a shared logger
*/
public AddonLoader(Main plugin, Logger logger) {
this.plugin = plugin;
this.logger = logger;
}

/**
* Searches for .jar files in the addons directory and loads them based on the specified phase.
*
* @param phase the AddonLoadPhase (BEFORE or AFTER plugin enable)
*/
public void loadAddons(AddonLoadPhase phase) {
File extensionsDir = new File(plugin.getDataFolder(), "addons");
if (!extensionsDir.exists()) {
extensionsDir.mkdirs();
logger.info("Addon directory created at {}", extensionsDir.getAbsolutePath());
return;
}

File[] files = extensionsDir.listFiles((dir, name) -> name.endsWith(".jar"));
if (files == null || files.length == 0) {
logger.warn("No addon files found in {}", extensionsDir.getAbsolutePath());
return;
}

for (File file : files) {
try {
URL jarUrl = file.toURI().toURL();
URLClassLoader classLoader = new URLClassLoader(new URL[]{jarUrl}, plugin.getClass().getClassLoader());

ServiceLoader<SkylliaAddon> serviceLoader = ServiceLoader.load(SkylliaAddon.class, classLoader);
for (SkylliaAddon addon : serviceLoader) {
if (addon.getLoadPhase() == phase) {
addon.onLoad(plugin);
addon.onEnable();
LOADED_ADDONS.add(addon);
logger.info("Loaded {} addon: {}", phase, addon.getClass().getName());
}
}
} catch (Exception e) {
logger.error("Failed to load addon: {}", file.getName(), e);
}
}
}

/**
* Disables all loaded addons, clearing the internal list.
*/
public static void disableAllAddons() {
for (SkylliaAddon addon : LOADED_ADDONS) {
try {
addon.onDisabled();
} catch (Exception ignored) {
}
}
LOADED_ADDONS.clear();
}
}
Loading