diff --git a/Movecraft/build.gradle.kts b/Movecraft/build.gradle.kts index 683722e23..14950e7b7 100644 --- a/Movecraft/build.gradle.kts +++ b/Movecraft/build.gradle.kts @@ -13,6 +13,11 @@ dependencies { runtimeOnly(project(":movecraft-v1_21", "reobf")) implementation(project(":movecraft-api")) compileOnly("org.yaml:snakeyaml:2.0") + testImplementation(libs.org.junit.jupiter.junit.jupiter.api) + testImplementation(libs.junit.junit) + testImplementation(libs.org.hamcrest.hamcrest.library) + testImplementation("org.mockito:mockito-core:5.13.0") + testImplementation("com.github.seeseemelk:MockBukkit-v1.18:2.85.2") } tasks.shadowJar { diff --git a/Movecraft/src/main/java/net/countercraft/movecraft/Movecraft.java b/Movecraft/src/main/java/net/countercraft/movecraft/Movecraft.java index fe69859c1..b882d8b9a 100644 --- a/Movecraft/src/main/java/net/countercraft/movecraft/Movecraft.java +++ b/Movecraft/src/main/java/net/countercraft/movecraft/Movecraft.java @@ -17,48 +17,41 @@ package net.countercraft.movecraft; -import io.papermc.paper.datapack.Datapack; import net.countercraft.movecraft.async.AsyncManager; -import net.countercraft.movecraft.commands.*; -import net.countercraft.movecraft.config.Settings; -import net.countercraft.movecraft.craft.ChunkManager; -import net.countercraft.movecraft.craft.CraftManager; -import net.countercraft.movecraft.craft.datatag.CraftDataTagRegistry; +import net.countercraft.movecraft.commands.CraftInfoCommand; +import net.countercraft.movecraft.commands.CraftReportCommand; +import net.countercraft.movecraft.commands.CraftTypeCommand; +import net.countercraft.movecraft.commands.CruiseCommand; +import net.countercraft.movecraft.commands.ManOverboardCommand; +import net.countercraft.movecraft.commands.MovecraftCommand; +import net.countercraft.movecraft.commands.PilotCommand; +import net.countercraft.movecraft.commands.ReleaseCommand; +import net.countercraft.movecraft.commands.RotateCommand; +import net.countercraft.movecraft.commands.ScuttleCommand; import net.countercraft.movecraft.features.contacts.ContactsCommand; -import net.countercraft.movecraft.features.contacts.ContactsManager; -import net.countercraft.movecraft.features.contacts.ContactsSign; import net.countercraft.movecraft.features.fading.WreckManager; -import net.countercraft.movecraft.features.status.StatusManager; -import net.countercraft.movecraft.features.status.StatusSign; -import net.countercraft.movecraft.listener.*; -import net.countercraft.movecraft.localisation.I18nSupport; -import net.countercraft.movecraft.mapUpdater.MapUpdateManager; -import net.countercraft.movecraft.processing.WorldManager; -import net.countercraft.movecraft.sign.*; -import net.countercraft.movecraft.util.BukkitTeleport; -import net.countercraft.movecraft.util.Tags; -import org.bukkit.Bukkit; -import org.bukkit.Material; +import net.countercraft.movecraft.lifecycle.PluginBuilder; +import net.countercraft.movecraft.sign.AscendSign; +import net.countercraft.movecraft.sign.CraftSign; +import net.countercraft.movecraft.sign.CruiseSign; +import net.countercraft.movecraft.sign.DescendSign; +import net.countercraft.movecraft.sign.HelmSign; +import net.countercraft.movecraft.sign.MoveSign; +import net.countercraft.movecraft.sign.NameSign; +import net.countercraft.movecraft.sign.PilotSign; +import net.countercraft.movecraft.sign.RelativeMoveSign; +import net.countercraft.movecraft.sign.ReleaseSign; +import net.countercraft.movecraft.sign.RemoteSign; +import net.countercraft.movecraft.sign.ScuttleSign; +import net.countercraft.movecraft.sign.SpeedSign; +import net.countercraft.movecraft.sign.SubcraftRotateSign; +import net.countercraft.movecraft.sign.TeleportSign; import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.NotNull; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.logging.Logger; - public class Movecraft extends JavaPlugin { private static Movecraft instance; - - private Logger logger; - private boolean shuttingDown; - private WorldHandler worldHandler; - private SmoothTeleport smoothTeleport; - private AsyncManager asyncManager; - private WreckManager wreckManager; + private PluginBuilder.Application application; public static synchronized Movecraft getInstance() { return instance; @@ -66,138 +59,16 @@ public static synchronized Movecraft getInstance() { @Override public void onDisable() { - shuttingDown = true; + application.host().stopAll(); + application = null; } @Override public void onEnable() { - // Read in config - Settings.LOCALE = getConfig().getString("Locale"); - Settings.Debug = getConfig().getBoolean("Debug", false); - Settings.DisableNMSCompatibilityCheck = getConfig().getBoolean("IReallyKnowWhatIAmDoing", false); - Settings.DisableSpillProtection = getConfig().getBoolean("DisableSpillProtection", false); - Settings.DisableIceForm = getConfig().getBoolean("DisableIceForm", true); - Settings.ReleaseOnDeath = getConfig().getBoolean("ReleaseOnDeath", false); - - String[] localisations = {"en", "cz", "nl", "fr"}; - for (String s : localisations) { - if (!new File(getDataFolder() - + "/localisation/movecraftlang_" + s + ".properties").exists()) { - saveResource("localisation/movecraftlang_" + s + ".properties", false); - } - } - I18nSupport.init(); - - - // if the PilotTool is specified in the config.yml file, use it - String pilotTool = getConfig().getString("PilotTool"); - if (pilotTool != null) { - Material material = Material.getMaterial(pilotTool); - if (material != null) { - logger.info("Recognized PilotTool setting of: " + pilotTool); - Settings.PilotTool = material; - } - else { - logger.info("No PilotTool setting, using default of stick"); - } - } - else { - logger.info("No PilotTool setting, using default of stick"); - } - - String minecraftVersion = getServer().getMinecraftVersion(); - getLogger().info("Loading support for " + minecraftVersion); - try { - final Class worldHandlerClazz = Class.forName("net.countercraft.movecraft.compat." + WorldHandler.getPackageName(minecraftVersion) + ".IWorldHandler"); - // Check if we have a NMSHandler class at that location. - if (WorldHandler.class.isAssignableFrom(worldHandlerClazz)) { // Make sure it actually implements NMS - worldHandler = (WorldHandler) worldHandlerClazz.getConstructor().newInstance(); // Set our handler - - // Try to setup the smooth teleport handler - try { - final Class smoothTeleportClazz = Class.forName("net.countercraft.movecraft.support." + WorldHandler.getPackageName(minecraftVersion) + ".ISmoothTeleport"); - if (SmoothTeleport.class.isAssignableFrom(smoothTeleportClazz)) { - smoothTeleport = (SmoothTeleport) smoothTeleportClazz.getConstructor().newInstance(); - } - else { - smoothTeleport = new BukkitTeleport(); // Fall back to bukkit teleportation - getLogger().warning("Did not find smooth teleport, falling back to bukkit teleportation provider."); - } - } - catch (final ReflectiveOperationException e) { - if (Settings.Debug) { - e.printStackTrace(); - } - smoothTeleport = new BukkitTeleport(); // Fall back to bukkit teleportation - getLogger().warning("Falling back to bukkit teleportation provider."); - } - } - } - catch (final Exception e) { - e.printStackTrace(); - getLogger().severe("Could not find support for this version."); - if (!Settings.DisableNMSCompatibilityCheck) { - // Disable ourselves and exit - setEnabled(false); - return; - } - else { - // Server owner claims to know what they are doing, warn them of the possible consequences - getLogger().severe("WARNING!\n\t" - + "Running Movecraft on an incompatible version can corrupt your world and break EVERYTHING!\n\t" - + "We provide no support for any issues."); - } - } - - - Settings.SinkCheckTicks = getConfig().getDouble("SinkCheckTicks", 100.0); - Settings.ManOverboardTimeout = getConfig().getInt("ManOverboardTimeout", 30); - Settings.ManOverboardDistSquared = Math.pow(getConfig().getDouble("ManOverboardDistance", 1000), 2); - Settings.SilhouetteViewDistance = getConfig().getInt("SilhouetteViewDistance", 200); - Settings.SilhouetteBlockCount = getConfig().getInt("SilhouetteBlockCount", 20); - Settings.ProtectPilotedCrafts = getConfig().getBoolean("ProtectPilotedCrafts", false); - Settings.MaxRemoteSigns = getConfig().getInt("MaxRemoteSigns", -1); - Settings.CraftsUseNetherPortals = getConfig().getBoolean("CraftsUseNetherPortals", false); - Settings.RequireCreatePerm = getConfig().getBoolean("RequireCreatePerm", false); - Settings.RequireNamePerm = getConfig().getBoolean("RequireNamePerm", true); - Settings.FadeWrecksAfter = getConfig().getInt("FadeWrecksAfter", 0); - Settings.FadeTickCooldown = getConfig().getInt("FadeTickCooldown", 20); - Settings.FadePercentageOfWreckPerCycle = getConfig().getDouble("FadePercentageOfWreckPerCycle", 10.0); - if (getConfig().contains("ExtraFadeTimePerBlock")) { - Map temp = getConfig().getConfigurationSection("ExtraFadeTimePerBlock").getValues(false); - for (String str : temp.keySet()) { - Set materials = Tags.parseMaterials(str); - for (Material m : materials) { - Settings.ExtraFadeTimePerBlock.put(m, (Integer) temp.get(str)); - } - } - } - - Settings.ForbiddenRemoteSigns = new HashSet<>(); - for(String s : getConfig().getStringList("ForbiddenRemoteSigns")) { - Settings.ForbiddenRemoteSigns.add(s.toLowerCase()); - } - - if(shuttingDown && Settings.IGNORE_RESET) { - logger.severe("Movecraft is incompatible with the reload command. Movecraft has shut down and will restart when the server is restarted."); - logger.severe("If you wish to use the reload command and Movecraft, you may disable this check inside the config.yml by setting 'safeReload: false'"); - getPluginLoader().disablePlugin(this); - return; - } - - // Startup procedure - boolean datapackInitialized = isDatapackEnabled() || initializeDatapack(); - asyncManager = new AsyncManager(); - asyncManager.runTaskTimer(this, 0, 1); - MapUpdateManager.getInstance().runTaskTimer(this, 0, 1); - - - CraftManager.initialize(datapackInitialized); - Bukkit.getScheduler().runTaskTimer(this, WorldManager.INSTANCE::run, 0,1); - wreckManager = new WreckManager(WorldManager.INSTANCE); - - getServer().getPluginManager().registerEvents(new InteractListener(), this); + var injector = PluginBuilder.createFor(this); + Startup.registerServices(injector); + //TODO: migrate to aikar or brigadier commands, left in place for now getCommand("movecraft").setExecutor(new MovecraftCommand()); getCommand("release").setExecutor(new ReleaseCommand()); getCommand("pilot").setExecutor(new PilotCommand()); @@ -208,10 +79,9 @@ public void onEnable() { getCommand("scuttle").setExecutor(new ScuttleCommand()); getCommand("crafttype").setExecutor(new CraftTypeCommand()); getCommand("craftinfo").setExecutor(new CraftInfoCommand()); + getCommand("contacts").setExecutor(new ContactsCommand()); - getServer().getPluginManager().registerEvents(new BlockListener(), this); - getServer().getPluginManager().registerEvents(new PlayerListener(), this); - getServer().getPluginManager().registerEvents(new ChunkManager(), this); + //TODO: Sign rework getServer().getPluginManager().registerEvents(new AscendSign(), this); getServer().getPluginManager().registerEvents(new CraftSign(), this); getServer().getPluginManager().registerEvents(new CruiseSign(), this); @@ -227,117 +97,33 @@ public void onEnable() { getServer().getPluginManager().registerEvents(new SubcraftRotateSign(), this); getServer().getPluginManager().registerEvents(new TeleportSign(), this); getServer().getPluginManager().registerEvents(new ScuttleSign(), this); - getServer().getPluginManager().registerEvents(new CraftPilotListener(), this); - getServer().getPluginManager().registerEvents(new CraftReleaseListener(), this); - - var contactsManager = new ContactsManager(); - contactsManager.runTaskTimerAsynchronously(this, 0, 20); - getServer().getPluginManager().registerEvents(contactsManager, this); - getServer().getPluginManager().registerEvents(new ContactsSign(), this); - getCommand("contacts").setExecutor(new ContactsCommand()); - - var statusManager = new StatusManager(); - statusManager.runTaskTimerAsynchronously(this, 0, 1); - getServer().getPluginManager().registerEvents(statusManager, this); - getServer().getPluginManager().registerEvents(new StatusSign(), this); - logger.info("[V " + getDescription().getVersion() + "] has been enabled."); + // Startup + application = injector.build(); + application.host().startAll(); + getLogger().info("[V %s] has been enabled.".formatted(getDescription().getVersion())); } @Override public void onLoad() { super.onLoad(); instance = this; - logger = getLogger(); saveDefaultConfig(); } - private boolean initializeDatapack() { - File datapackDirectory = null; - for(var world : getServer().getWorlds()) { - datapackDirectory = new File(world.getWorldFolder(), "datapacks"); - if(datapackDirectory.exists()) - break; - } - if(datapackDirectory == null) { - logger.severe("Failed to initialize Movecraft data pack due to first time world initialization."); - return false; - } - if(!datapackDirectory.exists()) { - logger.info("Creating a datapack directory at " + datapackDirectory.getPath()); - if(!datapackDirectory.mkdir()) { - logger.severe("Failed to create datapack directory!"); - return false; - } - } - else if(new File(datapackDirectory, "movecraft-data.zip").exists()) { - logger.warning("Conflicting datapack already exists in " + datapackDirectory.getPath() + ". If you would like to regenerate the datapack, delete the existing one."); - return false; - } - if(!datapackDirectory.canWrite()) { - logger.warning("Missing permissions to write to world directory."); - return false; - } - - try(var stream = new FileOutputStream(new File(datapackDirectory, "movecraft-data.zip")); - var pack = getResource("movecraft-data.zip")) { - if(pack == null) { - logger.severe("No internal datapack found, report this."); - return false; - } - pack.transferTo(stream); - } - catch(IOException e) { - e.printStackTrace(); - return false; - } - logger.info("Saved default Movecraft datapack."); - - getServer().dispatchCommand(getServer().createCommandSender(response -> {}), "datapack list"); // list datapacks to trigger the server to check - for (Datapack datapack : getServer().getDatapackManager().getPacks()) { - if (!datapack.getName().equals("file/movecraft-data.zip")) - continue; - - if (!datapack.isEnabled()) { - datapack.setEnabled(true); - logger.info("Datapack enabled."); - } - break; - } - - if (!isDatapackEnabled()) { - logger.severe("Failed to automatically load movecraft datapack. Check if it exists."); - setEnabled(false); - return false; - } - return true; - } - - private boolean isDatapackEnabled() { - getServer().dispatchCommand(getServer().createCommandSender(response -> {}), "datapack list"); // list datapacks to trigger the server to check - for (Datapack datapack : getServer().getDatapackManager().getPacks()) { - if (!datapack.getName().equals("file/movecraft-data.zip")) - continue; - - return datapack.isEnabled(); - } - return false; - } - - - public WorldHandler getWorldHandler(){ - return worldHandler; + public @NotNull WorldHandler getWorldHandler(){ + return application.container().getService(WorldHandler.class); } - public SmoothTeleport getSmoothTeleport() { - return smoothTeleport; + public @NotNull SmoothTeleport getSmoothTeleport() { + return application.container().getService(SmoothTeleport.class); } - public AsyncManager getAsyncManager() { - return asyncManager; + public @NotNull AsyncManager getAsyncManager() { + return application.container().getService(AsyncManager.class); } public @NotNull WreckManager getWreckManager(){ - return wreckManager; + return application.container().getService(WreckManager.class); } } diff --git a/Movecraft/src/main/java/net/countercraft/movecraft/Startup.java b/Movecraft/src/main/java/net/countercraft/movecraft/Startup.java new file mode 100644 index 000000000..d1f058b41 --- /dev/null +++ b/Movecraft/src/main/java/net/countercraft/movecraft/Startup.java @@ -0,0 +1,59 @@ +package net.countercraft.movecraft; + +import net.countercraft.movecraft.async.AsyncManager; +import net.countercraft.movecraft.config.DataPackHostedService; +import net.countercraft.movecraft.config.SettingsHostedService; +import net.countercraft.movecraft.craft.ChunkManager; +import net.countercraft.movecraft.craft.CraftManager; +import net.countercraft.movecraft.features.contacts.ContactsManager; +import net.countercraft.movecraft.features.contacts.ContactsSign; +import net.countercraft.movecraft.features.fading.WreckManager; +import net.countercraft.movecraft.features.status.StatusManager; +import net.countercraft.movecraft.features.status.StatusSign; +import net.countercraft.movecraft.lifecycle.PluginBuilder; +import net.countercraft.movecraft.listener.BlockListener; +import net.countercraft.movecraft.listener.CraftPilotListener; +import net.countercraft.movecraft.listener.CraftReleaseListener; +import net.countercraft.movecraft.listener.InteractListener; +import net.countercraft.movecraft.listener.PlayerListener; +import net.countercraft.movecraft.localisation.I18nSupport; +import net.countercraft.movecraft.mapUpdater.MapUpdateManager; +import net.countercraft.movecraft.processing.WorldManager; +import net.countercraft.movecraft.support.SmoothTeleportFactory; +import net.countercraft.movecraft.support.VersionProvider; +import net.countercraft.movecraft.support.WorldHandlerFactory; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +public class Startup { + @Contract("_->param1") + public static @NotNull PluginBuilder registerServices(@NotNull PluginBuilder injector){ + injector + .register(MapUpdateManager.class) + .register(AsyncManager.class) + .register(VersionProvider.class) + .register(SettingsHostedService.class) + .register(SmoothTeleportFactory.class) + .register(WorldHandlerFactory.class) + .registerInstance(WorldManager.INSTANCE) + .register(WreckManager.class) + .register(I18nSupport.class) + .register(DataPackHostedService.class) + .register(CraftManager.class) + .register(InteractListener.class) + .register(BlockListener.class) + .register(PlayerListener.class) + .register(ChunkManager.class) + .register(CraftPilotListener.class) + .register(CraftReleaseListener.class) + .register(ContactsManager.class) + .register(StatusManager.class); + + // Signs + injector + .register(StatusSign.class) + .register(ContactsSign.class); + + return injector; + } +} diff --git a/Movecraft/src/main/java/net/countercraft/movecraft/async/AsyncManager.java b/Movecraft/src/main/java/net/countercraft/movecraft/async/AsyncManager.java index 7825b86e2..fe995b768 100644 --- a/Movecraft/src/main/java/net/countercraft/movecraft/async/AsyncManager.java +++ b/Movecraft/src/main/java/net/countercraft/movecraft/async/AsyncManager.java @@ -18,6 +18,7 @@ package net.countercraft.movecraft.async; import com.google.common.collect.Lists; +import jakarta.inject.Inject; import net.countercraft.movecraft.CruiseDirection; import net.countercraft.movecraft.Movecraft; import net.countercraft.movecraft.MovecraftLocation; @@ -30,11 +31,11 @@ import net.countercraft.movecraft.craft.SinkingCraft; import net.countercraft.movecraft.craft.type.CraftType; import net.countercraft.movecraft.events.CraftReleaseEvent; +import net.countercraft.movecraft.lifecycle.Worker; import net.countercraft.movecraft.mapUpdater.MapUpdateManager; import net.kyori.adventure.text.Component; import org.bukkit.World; import org.bukkit.entity.Player; -import org.bukkit.scheduler.BukkitRunnable; import org.jetbrains.annotations.NotNull; import java.util.HashMap; @@ -47,13 +48,21 @@ import java.util.concurrent.LinkedBlockingQueue; @Deprecated -public class AsyncManager extends BukkitRunnable { - private final Map ownershipMap = new HashMap<>(); - private final BlockingQueue finishedAlgorithms = new LinkedBlockingQueue<>(); - private final Set clearanceSet = new HashSet<>(); - private final Map cooldownCache = new WeakHashMap<>(); - - public AsyncManager() {} +public class AsyncManager implements Worker { + private final Map ownershipMap; + private final BlockingQueue finishedAlgorithms; + private final Set clearanceSet; + private final Map cooldownCache; + private final @NotNull MapUpdateManager mapUpdateManager; + + @Inject + public AsyncManager(@NotNull MapUpdateManager mapUpdateManager) { + this.mapUpdateManager = mapUpdateManager; + ownershipMap = new HashMap<>(); + finishedAlgorithms = new LinkedBlockingQueue<>(); + clearanceSet = new HashSet<>(); + cooldownCache = new WeakHashMap<>(); + } public void submitTask(AsyncTask task, Craft c) { if (c.isNotProcessing()) { @@ -120,14 +129,14 @@ private boolean processTranslation(@NotNull final TranslationTask task, @NotNull if (task.isCollisionExplosion()) { c.setHitBox(task.getNewHitBox()); c.setFluidLocations(task.getNewFluidList()); - MapUpdateManager.getInstance().scheduleUpdates(task.getUpdates()); + mapUpdateManager.scheduleUpdates(task.getUpdates()); CraftManager.getInstance().addReleaseTask(c); return true; } return false; } // The craft is clear to move, perform the block updates - MapUpdateManager.getInstance().scheduleUpdates(task.getUpdates()); + mapUpdateManager.scheduleUpdates(task.getUpdates()); c.setHitBox(task.getNewHitBox()); c.setFluidLocations(task.getNewFluidList()); @@ -153,7 +162,7 @@ private boolean processRotation(@NotNull final RotationTask task, @NotNull final } - MapUpdateManager.getInstance().scheduleUpdates(task.getUpdates()); + mapUpdateManager.scheduleUpdates(task.getUpdates()); c.setHitBox(task.getNewHitBox()); c.setFluidLocations(task.getNewFluidList()); @@ -312,6 +321,16 @@ private void processSinking() { } } + @Override + public boolean isAsync() { + return false; + } + + @Override + public int getPeriod() { + return 1; + } + public void run() { clearAll(); diff --git a/Movecraft/src/main/java/net/countercraft/movecraft/craft/ChunkManager.java b/Movecraft/src/main/java/net/countercraft/movecraft/craft/ChunkManager.java index 65d0497b9..1d8c3a2d9 100644 --- a/Movecraft/src/main/java/net/countercraft/movecraft/craft/ChunkManager.java +++ b/Movecraft/src/main/java/net/countercraft/movecraft/craft/ChunkManager.java @@ -1,5 +1,6 @@ package net.countercraft.movecraft.craft; +import jakarta.inject.Inject; import net.countercraft.movecraft.Movecraft; import net.countercraft.movecraft.MovecraftChunk; import net.countercraft.movecraft.MovecraftLocation; @@ -19,7 +20,10 @@ @Deprecated public class ChunkManager implements Listener { - + + @Inject + public ChunkManager(){} + private static final Set chunks = new HashSet<>(); public static void addChunksToLoad(Iterable list) { diff --git a/Movecraft/src/main/java/net/countercraft/movecraft/craft/CraftManager.java b/Movecraft/src/main/java/net/countercraft/movecraft/craft/CraftManager.java index 1db3db008..37477791e 100644 --- a/Movecraft/src/main/java/net/countercraft/movecraft/craft/CraftManager.java +++ b/Movecraft/src/main/java/net/countercraft/movecraft/craft/CraftManager.java @@ -17,16 +17,18 @@ package net.countercraft.movecraft.craft; +import jakarta.inject.Inject; import net.countercraft.movecraft.Movecraft; import net.countercraft.movecraft.MovecraftLocation; +import net.countercraft.movecraft.config.DataPackHostedService; import net.countercraft.movecraft.craft.type.CraftType; import net.countercraft.movecraft.events.CraftReleaseEvent; import net.countercraft.movecraft.events.CraftSinkEvent; import net.countercraft.movecraft.events.TypesReloadedEvent; import net.countercraft.movecraft.exception.NonCancellableReleaseException; +import net.countercraft.movecraft.lifecycle.HostedService; import net.countercraft.movecraft.localisation.I18nSupport; import net.countercraft.movecraft.processing.CachedMovecraftWorld; -import net.countercraft.movecraft.processing.MovecraftWorld; import net.countercraft.movecraft.processing.WorldManager; import net.countercraft.movecraft.processing.effects.Effect; import net.countercraft.movecraft.processing.functions.CraftSupplier; @@ -59,19 +61,17 @@ import static net.countercraft.movecraft.util.ChatUtils.ERROR_PREFIX; -public class CraftManager implements Iterable{ +public class CraftManager implements Iterable, HostedService { private static CraftManager instance; + /** + * @deprecated Prefer DI + */ + @Deprecated public static CraftManager getInstance() { return instance; } - public static void initialize(boolean loadCraftTypes) { - instance = new CraftManager(loadCraftTypes); - } - - - /** * Set of all crafts on the server, weakly ordered by their hashcode. * Note: Crafts are added in detection via the addCraft method, and removed in the removeCraft method. @@ -90,14 +90,20 @@ public static void initialize(boolean loadCraftTypes) { */ @NotNull private Set craftTypes; - - private CraftManager(boolean loadCraftTypes) { - if(loadCraftTypes) + @Inject + public CraftManager(@NotNull DataPackHostedService dataPackService) { + if(dataPackService.isDatapackInitialized()) craftTypes = loadCraftTypes(); else craftTypes = new HashSet<>(); } + + @Override + public void start() { + instance = this; + } + @NotNull private Set loadCraftTypes() { File craftsFile = new File(Movecraft.getInstance().getDataFolder().getAbsolutePath() + "/types"); diff --git a/Movecraft/src/main/java/net/countercraft/movecraft/features/contacts/ContactsManager.java b/Movecraft/src/main/java/net/countercraft/movecraft/features/contacts/ContactsManager.java index 2647e6f00..d1c9ce06b 100644 --- a/Movecraft/src/main/java/net/countercraft/movecraft/features/contacts/ContactsManager.java +++ b/Movecraft/src/main/java/net/countercraft/movecraft/features/contacts/ContactsManager.java @@ -1,8 +1,8 @@ package net.countercraft.movecraft.features.contacts; +import jakarta.inject.Inject; import net.countercraft.movecraft.MovecraftLocation; import net.countercraft.movecraft.craft.*; -import net.countercraft.movecraft.craft.datatag.CraftDataTagContainer; import net.countercraft.movecraft.craft.datatag.CraftDataTagKey; import net.countercraft.movecraft.craft.datatag.CraftDataTagRegistry; import net.countercraft.movecraft.craft.type.CraftType; @@ -10,6 +10,7 @@ import net.countercraft.movecraft.exception.EmptyHitBoxException; import net.countercraft.movecraft.features.contacts.events.LostContactEvent; import net.countercraft.movecraft.features.contacts.events.NewContactEvent; +import net.countercraft.movecraft.lifecycle.Worker; import net.countercraft.movecraft.localisation.I18nSupport; import net.kyori.adventure.sound.Sound; import net.kyori.adventure.text.Component; @@ -21,14 +22,28 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; -import org.bukkit.scheduler.BukkitRunnable; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; -public class ContactsManager extends BukkitRunnable implements Listener { +public class ContactsManager implements Listener, Worker { private static final CraftDataTagKey> RECENT_CONTACTS = CraftDataTagRegistry.INSTANCE.registerTagKey(new NamespacedKey("movecraft", "recent-contacts"), craft -> new WeakHashMap<>()); + private final @NotNull CraftManager craftManager; + @Inject + public ContactsManager(@NotNull CraftManager craftManager){ + this.craftManager = craftManager; + } + + @Override + public boolean isAsync() { + return true; + } + + @Override + public int getPeriod() { + return 20; + } @Override public void run() { @@ -41,7 +56,7 @@ private void runContacts() { if (w == null) continue; - Set craftsInWorld = CraftManager.getInstance().getCraftsInWorld(w); + Set craftsInWorld = craftManager.getCraftsInWorld(w); for (Craft base : craftsInWorld) { if (base instanceof SinkingCraft || base instanceof SubCraft) continue; @@ -122,7 +137,7 @@ private void runRecentContacts() { if (w == null) continue; - for (PlayerCraft base : CraftManager.getInstance().getPlayerCraftsInWorld(w)) { + for (PlayerCraft base : craftManager.getPlayerCraftsInWorld(w)) { if (base.getHitBox().isEmpty()) continue; @@ -234,7 +249,7 @@ public void onCraftSink(@NotNull CraftSinkEvent e) { } private void remove(Craft base) { - for (Craft other : CraftManager.getInstance().getCrafts()) { + for (Craft other : craftManager.getCrafts()) { List contacts = other.getDataTag(Craft.CONTACTS); if (contacts.contains(base)) continue; @@ -243,7 +258,7 @@ private void remove(Craft base) { other.setDataTag(Craft.CONTACTS, contacts); } - for (Craft other : CraftManager.getInstance().getCrafts()) { + for (Craft other : craftManager.getCrafts()) { Map recentContacts = other.getDataTag(RECENT_CONTACTS); if (!recentContacts.containsKey(other)) continue; diff --git a/Movecraft/src/main/java/net/countercraft/movecraft/features/contacts/ContactsSign.java b/Movecraft/src/main/java/net/countercraft/movecraft/features/contacts/ContactsSign.java index 83f52d845..e565b20ef 100644 --- a/Movecraft/src/main/java/net/countercraft/movecraft/features/contacts/ContactsSign.java +++ b/Movecraft/src/main/java/net/countercraft/movecraft/features/contacts/ContactsSign.java @@ -1,5 +1,6 @@ package net.countercraft.movecraft.features.contacts; +import jakarta.inject.Inject; import net.countercraft.movecraft.MovecraftLocation; import net.countercraft.movecraft.craft.Craft; import net.countercraft.movecraft.craft.type.CraftType; @@ -21,6 +22,9 @@ public class ContactsSign implements Listener { private static final String HEADER = "Contacts:"; + @Inject + public ContactsSign(){} + @EventHandler public void onCraftDetect(@NotNull CraftDetectEvent event) { World world = event.getCraft().getWorld(); diff --git a/Movecraft/src/main/java/net/countercraft/movecraft/features/fading/WreckManager.java b/Movecraft/src/main/java/net/countercraft/movecraft/features/fading/WreckManager.java index ee12262df..fd8f487ea 100644 --- a/Movecraft/src/main/java/net/countercraft/movecraft/features/fading/WreckManager.java +++ b/Movecraft/src/main/java/net/countercraft/movecraft/features/fading/WreckManager.java @@ -1,5 +1,6 @@ package net.countercraft.movecraft.features.fading; +import jakarta.inject.Inject; import net.countercraft.movecraft.config.Settings; import net.countercraft.movecraft.craft.Craft; import net.countercraft.movecraft.processing.WorldManager; @@ -16,6 +17,7 @@ public class WreckManager { private final @NotNull WorldManager worldManager; + @Inject public WreckManager(@NotNull WorldManager worldManager){ this.worldManager = Objects.requireNonNull(worldManager); } diff --git a/Movecraft/src/main/java/net/countercraft/movecraft/features/status/StatusManager.java b/Movecraft/src/main/java/net/countercraft/movecraft/features/status/StatusManager.java index 5a9068f01..7555458b9 100644 --- a/Movecraft/src/main/java/net/countercraft/movecraft/features/status/StatusManager.java +++ b/Movecraft/src/main/java/net/countercraft/movecraft/features/status/StatusManager.java @@ -1,5 +1,6 @@ package net.countercraft.movecraft.features.status; +import jakarta.inject.Inject; import net.countercraft.movecraft.MovecraftLocation; import net.countercraft.movecraft.config.Settings; import net.countercraft.movecraft.craft.Craft; @@ -10,6 +11,7 @@ import net.countercraft.movecraft.craft.type.CraftType; import net.countercraft.movecraft.craft.type.RequiredBlockEntry; import net.countercraft.movecraft.features.status.events.CraftStatusUpdateEvent; +import net.countercraft.movecraft.lifecycle.Worker; import net.countercraft.movecraft.localisation.I18nSupport; import net.countercraft.movecraft.processing.WorldManager; import net.countercraft.movecraft.processing.effects.Effect; @@ -31,12 +33,28 @@ import java.util.Map; import java.util.function.Supplier; -public class StatusManager extends BukkitRunnable implements Listener { +public class StatusManager implements Listener, Worker { private static final CraftDataTagKey LAST_STATUS_CHECK = CraftDataTagRegistry.INSTANCE.registerTagKey(new NamespacedKey("movecraft", "last-status-check"), craft -> System.currentTimeMillis()); + private final @NotNull CraftManager craftManager; + + @Inject + public StatusManager(@NotNull CraftManager craftManager){ + this.craftManager = craftManager; + } + + @Override + public boolean isAsync() { + return true; + } + + @Override + public int getPeriod() { + return 1; + } @Override public void run() { - for (Craft c : CraftManager.getInstance().getCrafts()) { + for (Craft c : craftManager.getCrafts()) { long ticksElapsed = (System.currentTimeMillis() - c.getDataTag(LAST_STATUS_CHECK)) / 50; if (ticksElapsed <= Settings.SinkCheckTicks) continue; @@ -189,7 +207,7 @@ public void onCraftStatusUpdate(@NotNull CraftStatusUpdateEvent e) { if (sinking) { craft.getAudience().sendMessage(I18nSupport.getInternationalisedComponent("Player - Craft is sinking")); craft.setCruising(false); - CraftManager.getInstance().sink(craft); + craftManager.sink(craft); } } } diff --git a/Movecraft/src/main/java/net/countercraft/movecraft/features/status/StatusSign.java b/Movecraft/src/main/java/net/countercraft/movecraft/features/status/StatusSign.java index d8a488576..c3fd3c7fc 100644 --- a/Movecraft/src/main/java/net/countercraft/movecraft/features/status/StatusSign.java +++ b/Movecraft/src/main/java/net/countercraft/movecraft/features/status/StatusSign.java @@ -1,5 +1,6 @@ package net.countercraft.movecraft.features.status; +import jakarta.inject.Inject; import net.countercraft.movecraft.MovecraftLocation; import net.countercraft.movecraft.craft.Craft; import net.countercraft.movecraft.craft.type.CraftType; @@ -24,6 +25,9 @@ public final class StatusSign implements Listener { + @Inject + public StatusSign(){} + @EventHandler public void onCraftDetect(CraftDetectEvent event) { World world = event.getCraft().getWorld(); diff --git a/Movecraft/src/main/java/net/countercraft/movecraft/listener/BlockListener.java b/Movecraft/src/main/java/net/countercraft/movecraft/listener/BlockListener.java index 0ce37d5ef..b4ed2427c 100644 --- a/Movecraft/src/main/java/net/countercraft/movecraft/listener/BlockListener.java +++ b/Movecraft/src/main/java/net/countercraft/movecraft/listener/BlockListener.java @@ -17,6 +17,7 @@ package net.countercraft.movecraft.listener; +import jakarta.inject.Inject; import net.countercraft.movecraft.MovecraftLocation; import net.countercraft.movecraft.config.Settings; import net.countercraft.movecraft.craft.Craft; @@ -52,6 +53,9 @@ import org.jetbrains.annotations.NotNull; public class BlockListener implements Listener { + @Inject + public BlockListener(){} + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) public void onBlockBreak(@NotNull BlockBreakEvent e) { if (!Settings.ProtectPilotedCrafts) diff --git a/Movecraft/src/main/java/net/countercraft/movecraft/listener/CraftPilotListener.java b/Movecraft/src/main/java/net/countercraft/movecraft/listener/CraftPilotListener.java index 5901cdace..55fb4131d 100644 --- a/Movecraft/src/main/java/net/countercraft/movecraft/listener/CraftPilotListener.java +++ b/Movecraft/src/main/java/net/countercraft/movecraft/listener/CraftPilotListener.java @@ -1,5 +1,6 @@ package net.countercraft.movecraft.listener; +import jakarta.inject.Inject; import net.countercraft.movecraft.MovecraftLocation; import net.countercraft.movecraft.craft.Craft; import net.countercraft.movecraft.events.CraftPilotEvent; @@ -10,6 +11,8 @@ import org.jetbrains.annotations.NotNull; public class CraftPilotListener implements Listener { + @Inject + public CraftPilotListener(){} @EventHandler(ignoreCancelled = true) public void onCraftPilot(@NotNull CraftPilotEvent event) { diff --git a/Movecraft/src/main/java/net/countercraft/movecraft/listener/CraftReleaseListener.java b/Movecraft/src/main/java/net/countercraft/movecraft/listener/CraftReleaseListener.java index 26c933cdd..bcccacdca 100644 --- a/Movecraft/src/main/java/net/countercraft/movecraft/listener/CraftReleaseListener.java +++ b/Movecraft/src/main/java/net/countercraft/movecraft/listener/CraftReleaseListener.java @@ -1,5 +1,6 @@ package net.countercraft.movecraft.listener; +import jakarta.inject.Inject; import net.countercraft.movecraft.MovecraftLocation; import net.countercraft.movecraft.craft.Craft; import net.countercraft.movecraft.events.CraftReleaseEvent; @@ -10,6 +11,8 @@ import org.jetbrains.annotations.NotNull; public class CraftReleaseListener implements Listener { + @Inject + public CraftReleaseListener(){} @EventHandler public void onDisassembly(@NotNull CraftReleaseEvent event) { diff --git a/Movecraft/src/main/java/net/countercraft/movecraft/listener/InteractListener.java b/Movecraft/src/main/java/net/countercraft/movecraft/listener/InteractListener.java index b12233858..332cf361a 100644 --- a/Movecraft/src/main/java/net/countercraft/movecraft/listener/InteractListener.java +++ b/Movecraft/src/main/java/net/countercraft/movecraft/listener/InteractListener.java @@ -17,6 +17,7 @@ package net.countercraft.movecraft.listener; +import jakarta.inject.Inject; import net.countercraft.movecraft.config.Settings; import net.countercraft.movecraft.craft.CraftManager; import net.countercraft.movecraft.craft.PlayerCraft; @@ -37,7 +38,12 @@ import java.util.WeakHashMap; public final class InteractListener implements Listener { - private final Map timeMap = new WeakHashMap<>(); + private final Map timeMap; + + @Inject + public InteractListener() { + timeMap = new WeakHashMap<>(); + } @EventHandler(priority = EventPriority.LOWEST) // LOWEST so that it runs before the other events public void onPlayerInteract(@NotNull PlayerInteractEvent e) { diff --git a/Movecraft/src/main/java/net/countercraft/movecraft/listener/PlayerListener.java b/Movecraft/src/main/java/net/countercraft/movecraft/listener/PlayerListener.java index b9f829be1..efe47b202 100644 --- a/Movecraft/src/main/java/net/countercraft/movecraft/listener/PlayerListener.java +++ b/Movecraft/src/main/java/net/countercraft/movecraft/listener/PlayerListener.java @@ -17,6 +17,7 @@ package net.countercraft.movecraft.listener; +import jakarta.inject.Inject; import net.countercraft.movecraft.MovecraftLocation; import net.countercraft.movecraft.config.Settings; import net.countercraft.movecraft.craft.Craft; @@ -42,7 +43,12 @@ import java.util.WeakHashMap; public class PlayerListener implements Listener { - private final Map timeToReleaseAfter = new WeakHashMap<>(); + private final Map timeToReleaseAfter; + + @Inject + public PlayerListener() { + timeToReleaseAfter = new WeakHashMap<>(); + } private Set checkCraftBorders(Craft craft) { Set mergePoints = new HashSet<>(); diff --git a/Movecraft/src/main/java/net/countercraft/movecraft/localisation/I18nSupport.java b/Movecraft/src/main/java/net/countercraft/movecraft/localisation/I18nSupport.java index 1a324425b..2167bb794 100644 --- a/Movecraft/src/main/java/net/countercraft/movecraft/localisation/I18nSupport.java +++ b/Movecraft/src/main/java/net/countercraft/movecraft/localisation/I18nSupport.java @@ -17,10 +17,12 @@ package net.countercraft.movecraft.localisation; -import net.countercraft.movecraft.Movecraft; +import jakarta.inject.Inject; import net.countercraft.movecraft.config.Settings; +import net.countercraft.movecraft.lifecycle.HostedService; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; +import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; @@ -31,32 +33,53 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Properties; -import java.util.logging.Level; +import java.util.logging.Logger; -public class I18nSupport { +public class I18nSupport implements HostedService { private static Properties languageFile; + private final @NotNull Plugin plugin; + private final @NotNull Logger logger; - public static void init() { - languageFile = new Properties(); + @Inject + public I18nSupport(@NotNull Plugin plugin, @NotNull Logger logger){ + this.plugin = plugin; + this.logger = logger; + } - File localisationDirectory = new File(Movecraft.getInstance().getDataFolder().getAbsolutePath() + "/localisation"); - if (!localisationDirectory.exists()) { - localisationDirectory.mkdirs(); + + @Override + public void start() { + String[] localisations = {"en", "cz", "nl", "fr"}; + for (String locale : localisations) { + var file = new File("%s/localisation/movecraftlang_%s.properties".formatted(plugin.getDataFolder(), locale)); + if (!file.exists()) { + plugin.saveResource("localisation/movecraftlang_%s.properties".formatted(locale), false); + } } - InputStream inputStream = null; + init(); + } + + private void init() { + languageFile = new Properties(); + + Path localisationDirectory = plugin.getDataFolder().toPath().resolve("localisation"); + try { - inputStream = new FileInputStream(localisationDirectory.getAbsolutePath() + "/movecraftlang" + "_" + Settings.LOCALE + ".properties"); - } catch (FileNotFoundException e) { - e.printStackTrace(); + Files.createDirectories(localisationDirectory); + } catch (IOException e) { + throw new IllegalStateException("Critical Error in Localisation System", e); } - if (inputStream == null) { - Movecraft.getInstance().getLogger().log(Level.SEVERE, "Critical Error in Localisation System"); - Movecraft.getInstance().getServer().shutdown(); - return; + InputStream inputStream; + try { + inputStream = new FileInputStream(localisationDirectory.resolve("movecraftlang_" + Settings.LOCALE + ".properties").toFile()); + } catch (FileNotFoundException e) { + throw new IllegalStateException("Critical Error in Localisation System", e); } try { diff --git a/Movecraft/src/main/java/net/countercraft/movecraft/mapUpdater/MapUpdateManager.java b/Movecraft/src/main/java/net/countercraft/movecraft/mapUpdater/MapUpdateManager.java index a590212a3..d12a22bc6 100644 --- a/Movecraft/src/main/java/net/countercraft/movecraft/mapUpdater/MapUpdateManager.java +++ b/Movecraft/src/main/java/net/countercraft/movecraft/mapUpdater/MapUpdateManager.java @@ -17,10 +17,11 @@ package net.countercraft.movecraft.mapUpdater; +import jakarta.inject.Inject; import net.countercraft.movecraft.Movecraft; import net.countercraft.movecraft.config.Settings; +import net.countercraft.movecraft.lifecycle.Worker; import net.countercraft.movecraft.mapUpdater.update.UpdateCommand; -import org.bukkit.scheduler.BukkitRunnable; import org.jetbrains.annotations.NotNull; import java.util.Collection; @@ -30,19 +31,22 @@ import java.util.logging.Logger; @Deprecated -public class MapUpdateManager extends BukkitRunnable { +public class MapUpdateManager implements Worker { + private final Queue updates; - private final Queue updates = new ConcurrentLinkedQueue<>(); -// private final Queue updates = new LinkedBlockingQueue<>(); - //private PriorityQueue updateQueue = new PriorityQueue<>(); - - //@Deprecated - //public HashMap blockUpdatesPerCraft = new HashMap<>(); + @Inject + public MapUpdateManager() { + this.updates = new ConcurrentLinkedQueue<>(); + } - private MapUpdateManager() { } + @Override + public boolean isAsync() { + return false; + } - public static MapUpdateManager getInstance() { - return MapUpdateManagerHolder.INSTANCE; + @Override + public int getPeriod() { + return 1; } public void run() { @@ -96,9 +100,4 @@ public void scheduleUpdates(@NotNull UpdateCommand... updates){ public void scheduleUpdates(@NotNull Collection updates){ this.updates.addAll(updates); } - - private static class MapUpdateManagerHolder { - private static final MapUpdateManager INSTANCE = new MapUpdateManager(); - } - } diff --git a/Movecraft/src/main/java/net/countercraft/movecraft/support/SmoothTeleportFactory.java b/Movecraft/src/main/java/net/countercraft/movecraft/support/SmoothTeleportFactory.java new file mode 100644 index 000000000..c810566f5 --- /dev/null +++ b/Movecraft/src/main/java/net/countercraft/movecraft/support/SmoothTeleportFactory.java @@ -0,0 +1,44 @@ +package net.countercraft.movecraft.support; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import net.countercraft.movecraft.SmoothTeleport; +import net.countercraft.movecraft.config.Settings; +import net.countercraft.movecraft.util.BukkitTeleport; +import org.jetbrains.annotations.NotNull; + +import java.util.logging.Logger; + +public class SmoothTeleportFactory implements Provider { + private final @NotNull Logger logger; + private final @NotNull VersionInfo versionInfo; + + @Inject + public SmoothTeleportFactory(@NotNull Logger logger, @NotNull VersionInfo versionInfo) { + this.logger = logger; + this.versionInfo = versionInfo; + } + + public SmoothTeleport get(){ + try { + // Try to set up the smooth teleport handler + final Class smoothTeleportClazz = Class.forName("net.countercraft.movecraft.support." + versionInfo.getPackageName() + ".ISmoothTeleport"); + if (SmoothTeleport.class.isAssignableFrom(smoothTeleportClazz)) { + return (SmoothTeleport) smoothTeleportClazz.getConstructor().newInstance(); + } + + // Fall back to bukkit teleportation + logger.warning("Did not find smooth teleport, falling back to bukkit teleportation provider."); + + return new BukkitTeleport(); + } catch (final ReflectiveOperationException e) { + // Fall back to bukkit teleportation + logger.warning("Falling back to bukkit teleportation provider."); + if (Settings.Debug) { + e.printStackTrace(); + } + + return new BukkitTeleport(); + } + } +} diff --git a/Movecraft/src/main/java/net/countercraft/movecraft/support/WorldHandlerFactory.java b/Movecraft/src/main/java/net/countercraft/movecraft/support/WorldHandlerFactory.java new file mode 100644 index 000000000..aaf9c4308 --- /dev/null +++ b/Movecraft/src/main/java/net/countercraft/movecraft/support/WorldHandlerFactory.java @@ -0,0 +1,43 @@ +package net.countercraft.movecraft.support; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import net.countercraft.movecraft.WorldHandler; +import net.countercraft.movecraft.config.Settings; +import org.jetbrains.annotations.NotNull; + +import java.util.logging.Logger; + +public class WorldHandlerFactory implements Provider { + private final @NotNull Logger logger; + private final @NotNull VersionInfo versionInfo; + + @Inject + public WorldHandlerFactory(@NotNull Logger logger, @NotNull VersionInfo versionInfo) { + this.logger = logger; + this.versionInfo = versionInfo; + } + + @Override + public WorldHandler get() { + try { + final Class worldHandlerClazz = Class.forName("net.countercraft.movecraft.compat." + versionInfo.getPackageName() + ".IWorldHandler"); + // Check if we have a NMSHandler class at that location. + if (WorldHandler.class.isAssignableFrom(worldHandlerClazz)) { // Make sure it actually implements NMS + return (WorldHandler) worldHandlerClazz.getConstructor().newInstance(); // Set our handler + } + } catch (final Exception e) { + if (!Settings.DisableNMSCompatibilityCheck) { + throw new IllegalStateException("Could not find support for version %s.".formatted(versionInfo.version())); + } + } + + // Server owner claims to know what they are doing, warn them of the possible consequences + logger.severe(""" + WARNING! + Running Movecraft on an incompatible version can corrupt your world and break EVERYTHING! + We provide no support for any issues."""); + + return null; + } +} diff --git a/Movecraft/src/test/java/net/countercraft/movecraft/StartupTest.java b/Movecraft/src/test/java/net/countercraft/movecraft/StartupTest.java new file mode 100644 index 000000000..9c5cc7349 --- /dev/null +++ b/Movecraft/src/test/java/net/countercraft/movecraft/StartupTest.java @@ -0,0 +1,68 @@ +package net.countercraft.movecraft; + +import be.seeseemelk.mockbukkit.scheduler.BukkitSchedulerMock; +import io.papermc.paper.datapack.DatapackManager; +import net.countercraft.movecraft.lifecycle.PluginBuilder; +import org.bukkit.Server; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.PluginManager; +import org.junit.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.UUID; +import java.util.logging.Logger; + +import static org.mockito.Mockito.*; + +public class StartupTest { + @Test + public void testInjection(){ + var host = buildTestApplication(); + } + + @Test + public void testStartStop(){ + var host = buildTestApplication(); + + host.host().startAll(); + host.host().stopAll(); + } + + private static PluginBuilder.Application buildTestApplication(){ + var pluginManager = mock(PluginManager.class); + var scheduler = new BukkitSchedulerMock(); + var dataPackManager = mock(DatapackManager.class); + + var server = mock(Server.class); + when(server.getScheduler()).thenReturn(scheduler); + when(server.getPluginManager()).thenReturn(pluginManager); + when(server.getDatapackManager()).thenReturn(dataPackManager); + + Path directory; + try { + directory = Files.createTempDirectory(UUID.randomUUID().toString()); + directory.resolve("localisation").toFile().mkdir(); + directory.resolve("localisation").resolve("movecraftlang_null.properties").toFile().createNewFile(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + var plugin = mock(Plugin.class); + when(plugin.getLogger()).thenReturn(Logger.getLogger("movecraft-unit-test")); + when(plugin.getServer()).thenReturn(server); + when(plugin.getDataFolder()).thenReturn(directory.toFile()); + when(plugin.getConfig()).thenReturn(mock(FileConfiguration.class)); + + // Create builder + var builder = PluginBuilder.createFor(plugin); + + // Register plugin services + Startup.registerServices(builder); + + // Build + return builder.build(); + } +} diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 9c72f6524..49f01cd96 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { api(libs.it.unimi.dsi.fastutil) api(libs.net.kyori.adventure.api) api(libs.net.kyori.adventure.platform.bukkit) + api(libs.org.int4j.dirk.dirk.di) testImplementation(libs.org.junit.jupiter.junit.jupiter.api) testImplementation(libs.junit.junit) testImplementation(libs.org.hamcrest.hamcrest.library) diff --git a/api/src/main/java/net/countercraft/movecraft/WorldHandler.java b/api/src/main/java/net/countercraft/movecraft/WorldHandler.java index 3b72e569f..d4e629cc3 100644 --- a/api/src/main/java/net/countercraft/movecraft/WorldHandler.java +++ b/api/src/main/java/net/countercraft/movecraft/WorldHandler.java @@ -20,6 +20,7 @@ public abstract class WorldHandler { @Deprecated(forRemoval = true) public abstract void setAccessLocation(@NotNull InventoryView inventoryView, @NotNull Location location); // Not needed for 1.20+, remove when dropping support for 1.18.2 + @Deprecated public static @NotNull String getPackageName(@NotNull String minecraftVersion) { String[] parts = minecraftVersion.split("\\."); if (parts.length < 2) diff --git a/api/src/main/java/net/countercraft/movecraft/config/DataPackHostedService.java b/api/src/main/java/net/countercraft/movecraft/config/DataPackHostedService.java new file mode 100644 index 000000000..800c6be8b --- /dev/null +++ b/api/src/main/java/net/countercraft/movecraft/config/DataPackHostedService.java @@ -0,0 +1,124 @@ +package net.countercraft.movecraft.config; + +import io.papermc.paper.datapack.Datapack; +import jakarta.inject.Inject; +import net.countercraft.movecraft.lifecycle.HostedService; +import org.bukkit.Server; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.logging.Logger; + +public class DataPackHostedService implements HostedService { + private final @NotNull Plugin plugin; + private final @NotNull Logger logger; + private boolean isInitialized; + + @Inject + public DataPackHostedService(@NotNull Plugin plugin, @NotNull Logger logger) { + this.plugin = plugin; + this.logger = logger; + } + + @Override + public void start() { + isInitialized = isDatapackEnabled() || initializeDatapack(); + } + + public boolean isDatapackInitialized(){ + // TODO: Async + return isInitialized; + } + + private boolean initializeDatapack() { + File datapackDirectory = null; + Server server = plugin.getServer(); + + for(var world : server.getWorlds()) { + datapackDirectory = new File(world.getWorldFolder(), "datapacks"); + if(datapackDirectory.exists()) { + break; + } + } + + if(datapackDirectory == null) { + logger.severe("Failed to initialize Movecraft data pack due to first time world initialization."); + + return false; + } + + if(!datapackDirectory.exists()) { + logger.info("Creating a datapack directory at " + datapackDirectory.getPath()); + if(!datapackDirectory.mkdir()) { + logger.severe("Failed to create datapack directory!"); + + return false; + } + } else if(new File(datapackDirectory, "movecraft-data.zip").exists()) { + logger.warning("Conflicting datapack already exists in " + datapackDirectory.getPath() + ". If you would like to regenerate the datapack, delete the existing one."); + + return false; + } + + if(!datapackDirectory.canWrite()) { + logger.warning("Missing permissions to write to world directory."); + + return false; + } + + try(var stream = new FileOutputStream(new File(datapackDirectory, "movecraft-data.zip")); + var pack = plugin.getResource("movecraft-data.zip")) { + if(pack == null) { + logger.severe("No internal datapack found, report this."); + + return false; + } + + pack.transferTo(stream); + } + catch(IOException e) { + e.printStackTrace(); + + return false; + } + + logger.info("Saved default Movecraft datapack."); + server.dispatchCommand(server.createCommandSender(response -> {}), "datapack list"); // list datapacks to trigger the server to check + for (Datapack datapack : server.getDatapackManager().getPacks()) { + if (!datapack.getName().equals("file/movecraft-data.zip")) { + continue; + } + + if (!datapack.isEnabled()) { + datapack.setEnabled(true); + logger.info("Datapack enabled."); + } + + break; + } + + if (!isDatapackEnabled()) { + throw new IllegalStateException("Failed to automatically load movecraft datapack. Check if it exists."); + } + + return true; + } + + private boolean isDatapackEnabled() { + Server server = plugin.getServer(); + server.dispatchCommand(server.createCommandSender(response -> {}), "datapack list"); // list datapacks to trigger the server to check + for (Datapack datapack : server.getDatapackManager().getPacks()) { + if (!datapack.getName().equals("file/movecraft-data.zip")) { + continue; + } + + return datapack.isEnabled(); + } + + return false; + } + +} diff --git a/api/src/main/java/net/countercraft/movecraft/config/Settings.java b/api/src/main/java/net/countercraft/movecraft/config/Settings.java index 122d0b5a5..d69a94d76 100644 --- a/api/src/main/java/net/countercraft/movecraft/config/Settings.java +++ b/api/src/main/java/net/countercraft/movecraft/config/Settings.java @@ -23,6 +23,7 @@ import java.util.HashSet; import java.util.Map; +// TODO: De-static public class Settings { public static boolean IGNORE_RESET = false; public static boolean Debug = false; diff --git a/api/src/main/java/net/countercraft/movecraft/config/SettingsHostedService.java b/api/src/main/java/net/countercraft/movecraft/config/SettingsHostedService.java new file mode 100644 index 000000000..6ea4fbb8a --- /dev/null +++ b/api/src/main/java/net/countercraft/movecraft/config/SettingsHostedService.java @@ -0,0 +1,79 @@ +package net.countercraft.movecraft.config; + +import jakarta.inject.Inject; +import net.countercraft.movecraft.lifecycle.HostedService; +import net.countercraft.movecraft.util.Tags; +import org.bukkit.Material; +import org.bukkit.configuration.Configuration; +import org.bukkit.plugin.Plugin; +import org.int4.dirk.annotations.Opt; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; + +public class SettingsHostedService implements HostedService { + private final @NotNull Configuration configuration; + private final @NotNull Logger logger; + + @Inject + public SettingsHostedService(@NotNull Plugin plugin, @NotNull Logger logger) { + this.configuration = plugin.getConfig(); + this.logger = logger; + } + + @Override + public void start() { + Settings.LOCALE = configuration.getString("Locale"); + Settings.Debug = configuration.getBoolean("Debug", false); + Settings.DisableNMSCompatibilityCheck = configuration.getBoolean("IReallyKnowWhatIAmDoing", false); + Settings.DisableSpillProtection = configuration.getBoolean("DisableSpillProtection", false); + Settings.DisableIceForm = configuration.getBoolean("DisableIceForm", true); + Settings.ReleaseOnDeath = configuration.getBoolean("ReleaseOnDeath", false); + Settings.SinkCheckTicks = configuration.getDouble("SinkCheckTicks", 100.0); + Settings.ManOverboardTimeout = configuration.getInt("ManOverboardTimeout", 30); + Settings.ManOverboardDistSquared = Math.pow(configuration.getDouble("ManOverboardDistance", 1000), 2); + Settings.SilhouetteViewDistance = configuration.getInt("SilhouetteViewDistance", 200); + Settings.SilhouetteBlockCount = configuration.getInt("SilhouetteBlockCount", 20); + Settings.ProtectPilotedCrafts = configuration.getBoolean("ProtectPilotedCrafts", false); + Settings.MaxRemoteSigns = configuration.getInt("MaxRemoteSigns", -1); + Settings.CraftsUseNetherPortals = configuration.getBoolean("CraftsUseNetherPortals", false); + Settings.RequireCreatePerm = configuration.getBoolean("RequireCreatePerm", false); + Settings.RequireNamePerm = configuration.getBoolean("RequireNamePerm", true); + Settings.FadeWrecksAfter = configuration.getInt("FadeWrecksAfter", 0); + Settings.FadeTickCooldown = configuration.getInt("FadeTickCooldown", 20); + Settings.FadePercentageOfWreckPerCycle = configuration.getDouble("FadePercentageOfWreckPerCycle", 10.0); + + // if the PilotTool is specified in the config.yml file, use it + String pilotTool = configuration.getString("PilotTool"); + if (pilotTool != null) { + Material material = Material.getMaterial(pilotTool); + if (material != null) { + logger.info("Recognized PilotTool setting of: " + pilotTool); + Settings.PilotTool = material; + } + else { + logger.info("No PilotTool setting, using default of stick"); + } + } + else { + logger.info("No PilotTool setting, using default of stick"); + } + + if (configuration.contains("ExtraFadeTimePerBlock")) { + Map temp = configuration.getConfigurationSection("ExtraFadeTimePerBlock").getValues(false); + for (String str : temp.keySet()) { + Set materials = Tags.parseMaterials(str); + for (Material m : materials) { + Settings.ExtraFadeTimePerBlock.put(m, (Integer) temp.get(str)); + } + } + } + + Settings.ForbiddenRemoteSigns = new HashSet<>(configuration.getStringList("ForbiddenRemoteSigns")); + } + +} diff --git a/api/src/main/java/net/countercraft/movecraft/lifecycle/HostedService.java b/api/src/main/java/net/countercraft/movecraft/lifecycle/HostedService.java new file mode 100644 index 000000000..1bc22730e --- /dev/null +++ b/api/src/main/java/net/countercraft/movecraft/lifecycle/HostedService.java @@ -0,0 +1,6 @@ +package net.countercraft.movecraft.lifecycle; + +public interface HostedService { + default void start(){} + default void stop(){} +} diff --git a/api/src/main/java/net/countercraft/movecraft/lifecycle/ListenerHostedService.java b/api/src/main/java/net/countercraft/movecraft/lifecycle/ListenerHostedService.java new file mode 100644 index 000000000..db17198cb --- /dev/null +++ b/api/src/main/java/net/countercraft/movecraft/lifecycle/ListenerHostedService.java @@ -0,0 +1,24 @@ +package net.countercraft.movecraft.lifecycle; + +import jakarta.inject.Inject; +import org.bukkit.event.Listener; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class ListenerHostedService implements HostedService { + private final @NotNull List listeners; + private final @NotNull Plugin plugin; + + @Inject + public ListenerHostedService(@NotNull List listeners, @NotNull Plugin plugin){ + this.listeners = listeners; + this.plugin = plugin; + } + + @Override + public void start() { + listeners.forEach(listener -> plugin.getServer().getPluginManager().registerEvents(listener, plugin)); + } +} diff --git a/api/src/main/java/net/countercraft/movecraft/lifecycle/PluginBuilder.java b/api/src/main/java/net/countercraft/movecraft/lifecycle/PluginBuilder.java new file mode 100644 index 000000000..1084b09dd --- /dev/null +++ b/api/src/main/java/net/countercraft/movecraft/lifecycle/PluginBuilder.java @@ -0,0 +1,76 @@ +package net.countercraft.movecraft.lifecycle; + +import org.bukkit.plugin.Plugin; +import org.int4.dirk.api.Injector; +import org.int4.dirk.di.Injectors; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.logging.Logger; + +public final class PluginBuilder { + private final Injector injector; + + private PluginBuilder() { + injector = Injectors.manual(); + } + + @Contract("_->new") + public static @NotNull PluginBuilder createFor(@NotNull Plugin plugin){ + var builder = new PluginBuilder(); + builder.injector.registerInstance(plugin.getLogger()); + builder.injector.registerInstance(plugin); + + return builder; + } + + @SuppressWarnings("UnusedReturnValue") + @Contract("_->this") + public @NotNull PluginBuilder register(Type type){ + injector.register(type); + + return this; + } + + @Contract("_->this") + public @NotNull PluginBuilder register(Collection types){ + injector.register(types); + + return this; + } + + @SuppressWarnings("UnusedReturnValue") + @Contract("_,_->this") + public @NotNull PluginBuilder registerInstance(Object instance, Annotation... qualifiers){ + injector.registerInstance(instance, qualifiers); + + return this; + } + + @Contract("->new") + public @NotNull Application build(){ + // Lifecycle management + injector.register(WorkerHost.class); + injector.register(ListenerHostedService.class); + injector.register(ServiceHost.class); + + return new Application(injector.getInstance(ServiceHost.class), new ServiceProvider(injector)); + } + + public record Application(ServiceHost host, ServiceProvider container){} + + public static class ServiceProvider { + private final @NotNull Injector injector; + + private ServiceProvider(@NotNull Injector injector) { + this.injector = injector; + } + + public T getService(@NotNull Class cls){ + return injector.getInstance(cls); + } + } +} diff --git a/api/src/main/java/net/countercraft/movecraft/lifecycle/ServiceHost.java b/api/src/main/java/net/countercraft/movecraft/lifecycle/ServiceHost.java new file mode 100644 index 000000000..e9c8e2c53 --- /dev/null +++ b/api/src/main/java/net/countercraft/movecraft/lifecycle/ServiceHost.java @@ -0,0 +1,23 @@ +package net.countercraft.movecraft.lifecycle; + +import jakarta.inject.Inject; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class ServiceHost { + private final @NotNull List hostedServices; + + @Inject + public ServiceHost(@NotNull List hostedServices) { + this.hostedServices = hostedServices; + } + + public void startAll(){ + hostedServices.forEach(HostedService::start); + } + + public void stopAll(){ + hostedServices.forEach(HostedService::stop); + } +} diff --git a/api/src/main/java/net/countercraft/movecraft/lifecycle/Worker.java b/api/src/main/java/net/countercraft/movecraft/lifecycle/Worker.java new file mode 100644 index 000000000..a1b89cba5 --- /dev/null +++ b/api/src/main/java/net/countercraft/movecraft/lifecycle/Worker.java @@ -0,0 +1,13 @@ +package net.countercraft.movecraft.lifecycle; + +public interface Worker { + default int getDelay(){ + return 1; + } + + boolean isAsync(); + + int getPeriod(); + + void run(); +} diff --git a/api/src/main/java/net/countercraft/movecraft/lifecycle/WorkerHost.java b/api/src/main/java/net/countercraft/movecraft/lifecycle/WorkerHost.java new file mode 100644 index 000000000..48c62f625 --- /dev/null +++ b/api/src/main/java/net/countercraft/movecraft/lifecycle/WorkerHost.java @@ -0,0 +1,40 @@ +package net.countercraft.movecraft.lifecycle; + +import jakarta.inject.Inject; +import org.bukkit.plugin.Plugin; +import org.bukkit.scheduler.BukkitTask; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.stream.Collectors; + +public class WorkerHost implements HostedService { + private final @NotNull Plugin plugin; + private final @NotNull List workers; + private @NotNull List tasks; + + @Inject + public WorkerHost(@NotNull Plugin plugin, @NotNull List workers) { + this.plugin = plugin; + this.workers = workers; + tasks = List.of(); + } + + @Override + public void start() { + tasks = workers.stream().map(worker -> { + if(worker.isAsync()){ + return plugin.getServer().getScheduler().runTaskTimerAsynchronously(plugin, worker::run, worker.getDelay(), worker.getPeriod()); + } else { + return plugin.getServer().getScheduler().runTaskTimer(plugin, worker::run, worker.getDelay(), worker.getPeriod()); + } + }).collect(Collectors.toList()); + } + + @Override + public void stop() { + var oldTasks = tasks; + tasks = List.of(); + oldTasks.forEach(BukkitTask::cancel); + } +} diff --git a/api/src/main/java/net/countercraft/movecraft/processing/WorldManager.java b/api/src/main/java/net/countercraft/movecraft/processing/WorldManager.java index 74378240d..c48fc7fa4 100644 --- a/api/src/main/java/net/countercraft/movecraft/processing/WorldManager.java +++ b/api/src/main/java/net/countercraft/movecraft/processing/WorldManager.java @@ -1,5 +1,6 @@ package net.countercraft.movecraft.processing; +import net.countercraft.movecraft.lifecycle.Worker; import net.countercraft.movecraft.processing.effects.Effect; import net.countercraft.movecraft.util.CompletableFutureTask; import org.bukkit.Bukkit; @@ -20,9 +21,13 @@ /** * */ -public final class WorldManager implements Executor { - +public final class WorldManager implements Executor, Worker { + /** + * @deprecated Prefer dependency injection over static accessors + */ + @Deprecated public static final WorldManager INSTANCE = new WorldManager(); + private static final Runnable POISON = new Runnable() { @Override public void run() {/* No-op */} @@ -39,6 +44,16 @@ public String toString(){ private WorldManager(){} + @Override + public boolean isAsync() { + return false; + } + + @Override + public int getPeriod() { + return 1; + } + public void run() { if(!Bukkit.isPrimaryThread()){ throw new RuntimeException("WorldManager must be executed on the main thread."); diff --git a/api/src/main/java/net/countercraft/movecraft/support/VersionInfo.java b/api/src/main/java/net/countercraft/movecraft/support/VersionInfo.java new file mode 100644 index 000000000..a03412dea --- /dev/null +++ b/api/src/main/java/net/countercraft/movecraft/support/VersionInfo.java @@ -0,0 +1,13 @@ +package net.countercraft.movecraft.support; + +import org.jetbrains.annotations.NotNull; + +public record VersionInfo(String version) { + public @NotNull String getPackageName() { + String[] parts = version.split("\\."); + if (parts.length < 2) + throw new IllegalArgumentException(); + + return "v1_" + parts[1]; + } +} diff --git a/api/src/main/java/net/countercraft/movecraft/support/VersionProvider.java b/api/src/main/java/net/countercraft/movecraft/support/VersionProvider.java new file mode 100644 index 000000000..e1246ace6 --- /dev/null +++ b/api/src/main/java/net/countercraft/movecraft/support/VersionProvider.java @@ -0,0 +1,26 @@ +package net.countercraft.movecraft.support; + +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import net.countercraft.movecraft.WorldHandler; +import net.countercraft.movecraft.config.Settings; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +import java.util.logging.Logger; + +public class VersionProvider implements Provider { + private final @NotNull Plugin plugin; + + @Inject + public VersionProvider(@NotNull Plugin plugin) { + this.plugin = plugin; + } + + @Override + public VersionInfo get() { + String minecraftVersion = plugin.getServer().getMinecraftVersion(); + + return new VersionInfo(minecraftVersion); + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8ab52af61..541dfbd79 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,7 @@ junit-junit = "4.13.2" net-kyori-adventure-api = "4.17.0" net-kyori-adventure-platform-bukkit = "4.3.2" org-hamcrest-hamcrest-library = "1.3" +org-int4j-dirk-dirk-di = "1.0.0-beta1" org-jetbrains-annotations = "24.1.0" org-junit-jupiter-junit-jupiter-api = "5.10.2" org-openjdk-jmh-jmh-core = "1.37" @@ -22,6 +23,7 @@ junit-junit = { module = "junit:junit", version.ref = "junit-junit" } net-kyori-adventure-api = { module = "net.kyori:adventure-api", version.ref = "net-kyori-adventure-api" } net-kyori-adventure-platform-bukkit = { module = "net.kyori:adventure-platform-bukkit", version.ref = "net-kyori-adventure-platform-bukkit" } org-hamcrest-hamcrest-library = { module = "org.hamcrest:hamcrest-library", version.ref = "org-hamcrest-hamcrest-library" } +org-int4j-dirk-dirk-di = { module = "org.int4.dirk:dirk-di", version.ref = "org-int4j-dirk-dirk-di" } org-jetbrains-annotations = { module = "org.jetbrains:annotations", version.ref = "org-jetbrains-annotations" } org-junit-jupiter-junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "org-junit-jupiter-junit-jupiter-api" } org-openjdk-jmh-jmh-core = { module = "org.openjdk.jmh:jmh-core", version.ref = "org-openjdk-jmh-jmh-core" }