Developer Guide - Ubivis/AIDungeon-Wiki GitHub Wiki

Developer Guide

Welcome to the AIDungeon developer documentation. This section contains information for developers who want to extend, integrate with, or contribute to the AIDungeon plugin.

Quick Links

  • API Reference - Core classes and interfaces
  • Events - Custom events for plugin integration
  • Integration - How to integrate with AIDungeon
  • Contributing - Guidelines for contributors

Project Overview

AIDungeon is a Spigot plugin written in Java that integrates with Google's Gemini AI to generate dynamic dungeons in Minecraft. The plugin follows a modular architecture with these key components:

Core Components

  • AIDungeon.java: Main plugin class and entry point
  • DungeonManager: Central management of dungeon instances
  • DungeonGenerator: Handles the creation of dungeon structures
  • LightweightGeminiClient: Manages communication with the Gemini API
  • DatabaseManager: Handles all database operations

Data Models

  • Dungeon: Represents a dungeon instance
  • Room: Represents a room within a dungeon
  • PlayerProgress: Tracks player exploration data

Generation Pipeline

The generation process follows these steps:

  1. Location Selection: Finding suitable locations for dungeons
  2. API Integration: Calling Gemini API for dungeon design
  3. Structure Building: Converting the AI design into Minecraft structures
  4. Feature Population: Adding mobs, traps, and treasures
  5. Database Tracking: Recording dungeon data for persistence

Development Environment Setup

To set up a development environment for AIDungeon:

Prerequisites

  • Java JDK 21+
  • Gradle 8.5+
  • IDE (IntelliJ IDEA recommended)
  • Spigot BuildTools or Paper development environment

Building from Source

  1. Clone the repository:

    git clone https://github.com/UbivisMedia/aidungeon.git
    
  2. Navigate to the project directory:

    cd aidungeon
    
  3. Build with Gradle:

    ./gradlew build
    

The built JAR file will be in the build/libs directory.

IDE Setup

IntelliJ IDEA

  1. Open IntelliJ IDEA
  2. Select "Open or Import"
  3. Navigate to the cloned repository
  4. Select the build.gradle file and open it as a project
  5. Wait for the Gradle sync to complete

Eclipse

  1. Open Eclipse
  2. Select "File > Import"
  3. Choose "Gradle > Existing Gradle Project"
  4. Navigate to the cloned repository
  5. Complete the import wizard

Testing Environment

For testing during development:

  1. Set up a local Spigot/Paper test server
  2. Configure the build.gradle to copy the built JAR to your test server's plugins directory
  3. Use a Gemini API key for development (with usage limits)

Project Structure

src/main/java/com/ubivismedia/aidungeon/
├── AIDungeon.java                  # Main plugin class
├── api/                            # API integration
│   └── LightweightGeminiClient.java
├── commands/                       # Command handling
│   └── CommandHandler.java 
├── config/                         # Configuration
│   └── ConfigManager.java
├── database/                       # Database operations
│   └── DatabaseManager.java
├── dungeon/                        # Dungeon-related classes
│   ├── DungeonCollapser.java
│   ├── DungeonGenerator.java
│   ├── DungeonManager.java
│   ├── blocks/                     # Block handling
│   ├── boss/                       # Boss management
│   ├── features/                   # Dungeon features
│   ├── rooms/                      # Room generation
│   └── theme/                      # Theme handling
├── integration/                    # Third-party integrations
│   └── CustomMobResolver.java
├── listeners/                      # Event listeners
│   └── PlayerEventListener.java
├── model/                          # Data models
│   ├── Dungeon.java
│   ├── PlayerProgress.java
│   └── Room.java
└── scheduler/                      # Scheduled tasks
    └── DungeonScheduler.java

Adding Functionality

Creating an Extension

To extend AIDungeon's functionality without modifying the core code:

  1. Create a new plugin that depends on AIDungeon
  2. Access AIDungeon's API through AIDungeon.getInstance()
  3. Register listeners for AIDungeon's custom events
  4. Use the provided interfaces and classes for integration

Example plugin.yml for an extension:

name: MyAIDungeonExtension
version: 1.0.0
main: com.example.myextension.MyExtension
api-version: 1.21
depend: [AIDungeon]

Maven/Gradle Dependency

If AIDungeon is available in a Maven repository, add it as a dependency:

// Gradle
dependencies {
    compileOnly 'com.ubivismedia:aidungeon:0.2.0'
}
<!-- Maven -->
<dependency>
    <groupId>com.ubivismedia</groupId>
    <artifactId>aidungeon</artifactId>
    <version>0.2.0</version>
    <scope>provided</scope>
</dependency>

Key API Functionality

Accessing DungeonManager

The DungeonManager is the central API for dungeon interaction:

import com.ubivismedia.aidungeon.AIDungeon;
import com.ubivismedia.aidungeon.dungeon.DungeonManager;

// Get the DungeonManager instance
DungeonManager dungeonManager = AIDungeon.getInstance().getDungeonManager();

// Get all active dungeons
Map<Integer, Dungeon> dungeons = dungeonManager.getActiveDungeons();

// Get a specific dungeon
Dungeon dungeon = dungeonManager.getDungeon(dungeonId);

// Check for a dungeon at a location
Dungeon dungeonAtLocation = dungeonManager.getDungeonAtLocation(location);

Working with Dungeon Instances

Once you have a Dungeon instance, you can access its properties:

// Get dungeon properties
String theme = dungeon.getTheme();
String biomeType = dungeon.getBiomeType();
List<Room> rooms = dungeon.getRooms();
double explorationPercentage = dungeon.getExplorationPercentage();

// Get entrance location
Location entranceLocation = dungeon.getEntranceLocation(world);

// Check if fully explored
boolean isFullyExplored = dungeon.isFullyExplored();

Listening for Events

Register listeners for AIDungeon's custom events:

import com.ubivismedia.aidungeon.events.DungeonDiscoveredEvent;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;

public class MyDungeonListener implements Listener {
    
    @EventHandler
    public void onDungeonDiscovered(DungeonDiscoveredEvent event) {
        Dungeon dungeon = event.getDungeon();
        Player discoverer = event.getPlayer();
        
        // Your custom logic here
    }
}

Documentation Standards

When contributing to AIDungeon, please follow these documentation standards:

  1. JavaDoc: Add comprehensive JavaDoc comments to all public classes and methods
  2. Code Comments: Include inline comments for complex logic
  3. Method Naming: Use clear, descriptive method names
  4. Parameter Documentation: Document parameters and return values

Example of good JavaDoc:

/**
 * Creates a new dungeon at the specified location
 *
 * @param world The world where the dungeon will be created
 * @param x The x-coordinate for the dungeon entrance
 * @param y The y-coordinate for the dungeon entrance
 * @param z The z-coordinate for the dungeon entrance
 * @param biomeType The biome type string for theme selection
 * @return A CompletableFuture containing the created Dungeon instance
 * @throws IllegalArgumentException If the location is invalid
 */
public CompletableFuture<Dungeon> generateDungeon(World world, int x, int y, int z, String biomeType) {
    // Implementation
}

Advanced Development Topics

For more detailed information on specific development topics, refer to:

  • API Reference: Detailed API documentation
  • Events: Complete list of custom events
  • Integration: Advanced integration examples
  • Contributing: How to contribute to the project

We welcome contributions and extensions to the AIDungeon plugin. If you develop an extension or improvement, consider sharing it with the community!

API Reference

This document provides a detailed reference of the public API classes and methods available for developers who want to interact with the AIDungeon plugin.

Core Classes

AIDungeon

Main plugin class that serves as the entry point to the API.

public class AIDungeon extends JavaPlugin {
    // Static instance getter
    public static AIDungeon getInstance();
// Core component accessors
public ConfigManager getConfigManager();
public DatabaseManager getDatabaseManager();
public DungeonManager getDungeonManager();
public DungeonScheduler getDungeonScheduler();

// Logging helper
public void log(Level level, String message);

}

DungeonManager

Central manager for all dungeon-related operations.

public class DungeonManager {
    // Dungeon retrieval
    public Dungeon getDungeon(int dungeonId);
    public Map<Integer, Dungeon> getActiveDungeons();
    public List<Dungeon> getDungeonsInWorld(String worldName);
    public Dungeon getDungeonAtLocation(Location location);
    public Room getRoomAtLocation(Location location);
// Dungeon scanning
public void scanAroundPlayer(Player player);
public void processPendingLocations();

// Exploration tracking
public void trackPlayerExploration(Player player, Dungeon dungeon, Room room);

// Dungeon lifecycle
public void collapseDungeon(int dungeonId);
public void registerBossDefeat(int dungeonId, Player player);

}

ConfigManager

Handles configuration loading and provides access to config values.

public class ConfigManager {
    // Configuration operations
    public boolean loadConfiguration();
    public void saveConfiguration();
    public FileConfiguration getConfig();
// Database config getters
public String getDbType();
public String getDbHost();
public int getDbPort();
public String getDbName();
public String getDbUser();
public String getDbPassword();
public String getDbFile();

// Scheduler config getters
public int getPeriodicCheckMinutes();
public LocalTime getDailyScanTime();

// Generation config getters
public int getMinDistanceBetweenDungeons();
public int getPlayerSightRange();
public double getDungeonSpawnChance();
public int getMaxDungeonsPerRegion();
public int getMaxRoomsPerDungeon();

// Gemini API config getters
public String getGeminiApiKey();
public String getGeminiModel();
public String getGeminiPeakHoursModel();
public String getGeminiOffPeakModel();
public LocalTime getGeminiPeakHoursStart();
public LocalTime getGeminiPeakHoursEnd();
public String getGeminiLanguage();
public int getGeminiTimeout();

// Dynamic model selection
public boolean isWithinPeakHours();
public String getCurrentGeminiModel();

}

DatabaseManager

Handles database connections and operations.

public class DatabaseManager {
    // Database setup
    public boolean setupConnection();
    public Connection getConnection() throws SQLException;
    public void closeConnection();
    public boolean createTables();
}

Data Models

Dungeon

Represents a dungeon instance with all its properties and rooms.

public class Dungeon {
    // Enums
    public enum Status {
        GENERATING,
        ACTIVE,
        EXPLORED,
        COLLAPSING,
        COLLAPSED
    }
// Properties
private int id;
private String worldName;
private String biomeType;
private String theme;
private int xCoord;
private int yCoord;
private int zCoord;
private LocalDateTime createdAt;
private Status status;
private boolean bossDefeated;
private List&lt;Room&gt; rooms;

// Getters and setters for all properties (Lombok @Data annotation)

// Utility methods
public Location getEntranceLocation(World world);
public Room getBossRoom();
public double getExplorationPercentage();
public boolean isFullyExplored();

}

Room

Represents a room within a dungeon.

public class Room {
    // Enums
    public enum ExplorationStatus {
        UNEXPLORED,
        PARTIALLY_EXPLORED,
        EXPLORED
    }
// Properties
private int id;
private int dungeonId;
private String roomType;
private int xCoord;
private int yCoord;
private int zCoord;
private int width;
private int height;
private int length;
private ExplorationStatus explorationStatus;

// Getters and setters for all properties (Lombok @Data annotation)

// Utility methods
public Location getCenterLocation(World world);
public boolean containsLocation(Location location);
public Location getMinLocation(World world);
public Location getMaxLocation(World world);
public boolean isBossRoom();
public boolean isEntranceRoom();
public boolean isTreasureRoom();

}

PlayerProgress

Tracks a player's progress in exploring a dungeon.

public class PlayerProgress {
    // Properties
    private int id;
    private UUID playerUuid;
    private int dungeonId;
    private double explorationPercent;
    private LocalDateTime lastVisit;
// Getters and setters for all properties (Lombok @Data annotation)

// Utility methods
public boolean hasCompletedDungeon();

}

Integration Examples

Accessing the API

To access the AIDungeon API from your plugin:

import com.ubivismedia.aidungeon.AIDungeon;

public class MyPlugin extends JavaPlugin { private AIDungeon aidungeon;

@Override
public void onEnable() {
    // Get the AIDungeon plugin instance
    Plugin plugin = getServer().getPluginManager().getPlugin("AIDungeon");
    
    if (plugin instanceof AIDungeon) {
        aidungeon = (AIDungeon) plugin;
        getLogger().info("Successfully hooked into AIDungeon!");
    } else {
        getLogger().warning("Failed to hook into AIDungeon, disabling plugin.");
        getServer().getPluginManager().disablePlugin(this);
        return;
    }
    
    // Now you can access the API
    DungeonManager dungeonManager = aidungeon.getDungeonManager();
    // ...
}

}

Checking Dungeon Location

Example of checking if a player is in a dungeon:

import com.ubivismedia.aidungeon.AIDungeon;
import com.ubivismedia.aidungeon.dungeon.DungeonManager;
import com.ubivismedia.aidungeon.model.Dungeon;
import com.ubivismedia.aidungeon.model.Room;
import org.bukkit.entity.Player;
import org.bukkit.Location;

public boolean isPlayerInDungeon(Player player) { DungeonManager dungeonManager = AIDungeon.getInstance().getDungeonManager(); Location playerLocation = player.getLocation();

Dungeon dungeon = dungeonManager.getDungeonAtLocation(playerLocation);
return dungeon != null;

}

public boolean isPlayerInBossRoom(Player player) { DungeonManager dungeonManager = AIDungeon.getInstance().getDungeonManager(); Location playerLocation = player.getLocation();

Room room = dungeonManager.getRoomAtLocation(playerLocation);
return room != null &amp;&amp; room.isBossRoom();

}

Getting Dungeon Stats

Example of retrieving dungeon statistics:

import com.ubivismedia.aidungeon.AIDungeon;
import com.ubivismedia.aidungeon.dungeon.DungeonManager;
import com.ubivismedia.aidungeon.model.Dungeon;
import java.util.Map;

public void printDungeonStats() { DungeonManager dungeonManager = AIDungeon.getInstance().getDungeonManager(); Map<Integer, Dungeon> dungeons = dungeonManager.getActiveDungeons();

System.out.println("Total active dungeons: " + dungeons.size());

for (Dungeon dungeon : dungeons.values()) {
    System.out.println("Dungeon #" + dungeon.getId());
    System.out.println("  Theme: " + dungeon.getTheme());
    System.out.println("  Rooms: " + dungeon.getRooms().size());
    System.out.println("  Exploration: " + dungeon.getExplorationPercentage() + "%");
    System.out.println("  Boss Defeated: " + dungeon.isBossDefeated());
}

}

Database Schema

If you need to query the database directly, here's the schema structure:

dungeons Table

Column Type Description
id INTEGER Primary key
world_name VARCHAR(64) World where the dungeon is located
biome_type VARCHAR(32) Biome type for theme selection
theme VARCHAR(32) Dungeon theme
x_coord INTEGER X coordinate of entrance
y_coord INTEGER Y coordinate of entrance
z_coord INTEGER Z coordinate of entrance
created_at TIMESTAMP When the dungeon was created
status VARCHAR(16) Current status (GENERATING, ACTIVE, etc.)
boss_defeated BOOLEAN Whether the boss was defeated

Custom Components

DungeonGenerator

Handles the actual generation of dungeons in the world.

public class DungeonGenerator {
    public CompletableFuture<Dungeon> generateDungeon(World world, int x, int y, int z, String biomeType);
    public CompletableFuture<Void> collapseDungeon(Dungeon dungeon, World world);
}

LightweightGeminiClient

Manages communication with the Gemini API.

public class LightweightGeminiClient {
    public CompletableFuture<String> generateContent(String prompt);
}

DungeonCollapser

Handles the process of collapsing dungeons and replacing them with natural terrain.

public class DungeonCollapser {
    public static Material getReplacementMaterial(World world, Vector pos);
}

BlockPaletteManager

Manages block palettes for different biomes and themes.

public class BlockPaletteManager {
    public List<Material> getBlockPalette(String biomeType, String theme);
}

Event System

AIDungeon provides several custom events that you can listen for in your plugins:

// Fired when a dungeon is discovered
public class DungeonDiscoveredEvent extends Event {
    private final Dungeon dungeon;
    private final Player player;
public Dungeon getDungeon();
public Player getPlayer();

}

// Fired when a dungeon is generated public class DungeonGeneratedEvent extends Event { private final Dungeon dungeon;

public Dungeon getDungeon();

}

// Fired when a dungeon begins collapsing public class DungeonCollapseStartEvent extends Event { private final Dungeon dungeon;

public Dungeon getDungeon();

}

// Fired when a dungeon has fully collapsed public class DungeonCollapseEndEvent extends Event { private final int dungeonId; private final String worldName; private final int x, y, z;

public int getDungeonId();
public String getWorldName();
public int getX();
public int getY();
public int getZ();

}

// Fired when a boss is defeated public class BossDefeatedEvent extends Event { private final Dungeon dungeon; private final Player killer; private final LivingEntity boss;

public Dungeon getDungeon();
public Player getKiller();
public LivingEntity getBoss();

}

// Fired when a room is explored for the first time public class RoomExploredEvent extends Event { private final Room room; private final Dungeon dungeon; private final Player player;

public Room getRoom();
public Dungeon getDungeon();
public Player getPlayer();

}

Example: Custom Dungeon Extension

Here's a complete example of a plugin that extends AIDungeon functionality:

import com.ubivismedia.aidungeon.AIDungeon;
import com.ubivismedia.aidungeon.dungeon.DungeonManager;
import com.ubivismedia.aidungeon.events.BossDefeatedEvent;
import com.ubivismedia.aidungeon.events.DungeonDiscoveredEvent;
import com.ubivismedia.aidungeon.model.Dungeon;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.plugin.java.JavaPlugin;

public class DungeonRewardsExtension extends JavaPlugin implements Listener { private AIDungeon aidungeon;

@Override
public void onEnable() {
    // Get the AIDungeon plugin instance
    Plugin plugin = getServer().getPluginManager().getPlugin("AIDungeon");
    
    if (plugin instanceof AIDungeon) {
        aidungeon = (AIDungeon) plugin;
        getLogger().info("Successfully hooked into AIDungeon!");
        
        // Register our event listener
        getServer().getPluginManager().registerEvents(this, this);
        
        // Register custom command
        getCommand("dungeoninfo").setExecutor(new DungeonInfoCommand(this));
    } else {
        getLogger().warning("Failed to hook into AIDungeon, disabling plugin.");
        getServer().getPluginManager().disablePlugin(this);
    }
}

@EventHandler
public void onDungeonDiscovered(DungeonDiscoveredEvent event) {
    Player player = event.getPlayer();
    Dungeon dungeon = event.getDungeon();
    
    // Give discovery reward
    player.sendMessage("You received 100 XP for discovering a dungeon!");
    player.giveExp(100);
    
    // Broadcast discovery
    Bukkit.broadcastMessage(player.getName() + " discovered a " + 
            dungeonTypeName(dungeon.getTheme()) + " in the " + 
            dungeon.getBiomeType() + " biome!");
}

@EventHandler
public void onBossDefeated(BossDefeatedEvent event) {
    Player player = event.getKiller();
    Dungeon dungeon = event.getDungeon();
    
    // Give special rewards
    player.sendMessage("You received 500 XP for defeating the dungeon boss!");
    player.giveExp(500);
    
    // Economy integration example (requires Vault)
    if (getServer().getPluginManager().getPlugin("Vault") != null) {
        givePlayerMoney(player, 1000);
    }
    
    // Broadcast achievement
    Bukkit.broadcastMessage(player.getName() + " defeated the boss of the " + 
            dungeonTypeName(dungeon.getTheme()) + "!");
}

private String dungeonTypeName(String theme) {
    // Convert theme ID to display name
    return theme.replace("_", " ");
}

private void givePlayerMoney(Player player, int amount) {
    // Implementation would use Vault API
}

// Other utility methods and command handlers

}

Working with Database

If you need to perform custom database operations:

import com.ubivismedia.aidungeon.AIDungeon;
import com.ubivismedia.aidungeon.database.DatabaseManager;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.UUID;

public void getPlayerDungeonCount(UUID playerUuid) { DatabaseManager dbManager = AIDungeon.getInstance().getDatabaseManager();

try (Connection conn = dbManager.getConnection();
     PreparedStatement stmt = conn.prepareStatement(
             "SELECT COUNT(*) FROM player_progress WHERE player_uuid = ?")) {
    
    stmt.setString(1, playerUuid.toString());
    ResultSet rs = stmt.executeQuery();
    
    if (rs.next()) {
        int count = rs.getInt(1);
        System.out.println("Player has explored " + count + " dungeons.");
    }
} catch (SQLException e) {
    e.printStackTrace();
}

}

For more information on specific API components, see the additional developer documentation pages:

  • Events
  • Integration

Events

AIDungeon provides a comprehensive event system that allows other plugins to hook into various stages of the dungeon lifecycle. This page documents all available events and provides examples of how to use them.

Event Overview

AIDungeon events follow the standard Bukkit event system. All events are located in the com.ubivismedia.aidungeon.events package and can be listened to using the standard Bukkit @EventHandler annotation.

Dungeon Lifecycle Events

DungeonDiscoveredEvent

Fired when a player discovers a dungeon for the first time.

public class DungeonDiscoveredEvent extends Event implements Cancellable {
    private final Dungeon dungeon;
    private final Player player;
    private boolean cancelled;
    
    public Dungeon getDungeon();
    public Player getPlayer();
    
    @Override
    public boolean isCancelled();
    
    @Override
    public void setCancelled(boolean cancel);
}

Usage Example:

@EventHandler
public void onDungeonDiscovered(DungeonDiscoveredEvent event) {
    Player player = event.getPlayer();
    Dungeon dungeon = event.getDungeon();
    
    player.sendMessage("You discovered a " + dungeon.getTheme() + " dungeon!");
    
    // Give discovery rewards
    player.giveExp(100);
    
    // You can cancel the event to prevent the discovery message
    // event.setCancelled(true);
}

DungeonGeneratedEvent

Fired when a dungeon has been fully generated in the world.

public class DungeonGeneratedEvent extends Event {
    private final Dungeon dungeon;
    
    public Dungeon getDungeon();
    public World getWorld();
}

Usage Example:

@EventHandler
public void onDungeonGenerated(DungeonGeneratedEvent event) {
    Dungeon dungeon = event.getDungeon();
    World world = event.getWorld();
    
    // Log dungeon generation
    getLogger().info("Dungeon #" + dungeon.getId() + " generated at " + 
                     dungeon.getXCoord() + ", " + dungeon.getYCoord() + ", " + dungeon.getZCoord());
    
    // Maybe spawn special entities or add custom features
    Location entranceLocation = dungeon.getEntranceLocation(world);
    world.spawnParticle(Particle.PORTAL, entranceLocation, 100, 1, 1, 1, 0.5);
}

DungeonStatusChangedEvent

Fired when a dungeon's status changes (e.g., from ACTIVE to EXPLORED).

public class DungeonStatusChangedEvent extends Event {
    private final Dungeon dungeon;
    private final Dungeon.Status oldStatus;
    private final Dungeon.Status newStatus;
    
    public Dungeon getDungeon();
    public Dungeon.Status getOldStatus();
    public Dungeon.Status getNewStatus();
}

Usage Example:

@EventHandler
public void onDungeonStatusChanged(DungeonStatusChangedEvent event) {
    Dungeon dungeon = event.getDungeon();
    Dungeon.Status oldStatus = event.getOldStatus();
    Dungeon.Status newStatus = event.getNewStatus();
    
    if (oldStatus == Dungeon.Status.ACTIVE && newStatus == Dungeon.Status.EXPLORED) {
        // Dungeon has been fully explored
        getLogger().info("Dungeon #" + dungeon.getId() + " has been fully explored!");
        
        // Maybe give server-wide rewards or announcements
        Bukkit.broadcastMessage("A " + dungeon.getTheme() + " dungeon has been fully explored!");
    }
}

DungeonCollapseStartEvent

Fired when a dungeon begins its collapse sequence.

public class DungeonCollapseStartEvent extends Event implements Cancellable {
    private final Dungeon dungeon;
    private boolean cancelled;
    
    public Dungeon getDungeon();
    
    @Override
    public boolean isCancelled();
    
    @Override
    public void setCancelled(boolean cancel);
}

Usage Example:

@EventHandler
public void onDungeonCollapseStart(DungeonCollapseStartEvent event) {
    Dungeon dungeon = event.getDungeon();
    
    // Log collapse start
    getLogger().info("Dungeon #" + dungeon.getId() + " is starting to collapse");
    
    // You can cancel the collapse if needed
    // event.setCancelled(true);
    
    // Maybe teleport any players still in the dungeon
    World world = Bukkit.getWorld(dungeon.getWorldName());
    if (world != null) {
        Location surface = dungeon.getEntranceLocation(world).clone();
        surface.setY(world.getHighestBlockYAt(surface));
        
        for (Player player : world.getPlayers()) {
            for (Room room : dungeon.getRooms()) {
                if (room.containsLocation(player.getLocation())) {
                    player.teleport(surface);
                    player.sendMessage("You were teleported out of the collapsing dungeon!");
                    break;
                }
            }
        }
    }
}

DungeonCollapseEndEvent

Fired when a dungeon has fully collapsed and been removed from the world.

public class DungeonCollapseEndEvent extends Event {
    private final int dungeonId;
    private final String worldName;
    private final int x, y, z;
    
    public int getDungeonId();
    public String getWorldName();
    public int getX();
    public int getY();
    public int getZ();
}

Usage Example:

@EventHandler
public void onDungeonCollapseEnd(DungeonCollapseEndEvent event) {
    int dungeonId = event.getDungeonId();
    String worldName = event.getWorldName();
    int x = event.getX();
    int y = event.getY();
    int z = event.getZ();
    
    // Log collapse completion
    getLogger().info("Dungeon #" + dungeonId + " has fully collapsed at " + 
                     x + ", " + y + ", " + z + " in world " + worldName);
    
    // Maybe spawn some aftermath effects
    World world = Bukkit.getWorld(worldName);
    if (world != null) {
        Location location = new Location(world, x, y, z);
        world.spawnParticle(Particle.CLOUD, location, 200, 5, 3, 5, 0.1);
    }
}

Boss Events

BossSpawnedEvent

Fired when a dungeon boss is spawned.

public class BossSpawnedEvent extends Event {
    private final Dungeon dungeon;
    private final LivingEntity bossEntity;
    
    public Dungeon getDungeon();
    public LivingEntity getBossEntity();
}

Usage Example:

@EventHandler
public void onBossSpawned(BossSpawnedEvent event) {
    Dungeon dungeon = event.getDungeon();
    LivingEntity boss = event.getBossEntity();
    
    // Log boss spawn
    getLogger().info("Boss spawned in dungeon #" + dungeon.getId() + ": " + boss.getCustomName());
    
    // Maybe add custom effects or modify the boss
    boss.addPotionEffect(new PotionEffect(PotionEffectType.GLOWING, Integer.MAX_VALUE, 0));
    
    // Announce to nearby players
    World world = boss.getWorld();
    for (Player player : world.getPlayers()) {
        if (player.getLocation().distance(boss.getLocation()) < 50) {
            player.sendMessage("§c§lA powerful enemy has awakened in the dungeon!");
            player.playSound(player.getLocation(), Sound.ENTITY_WITHER_SPAWN, 1.0f, 0.5f);
        }
    }
}

BossDefeatedEvent

Fired when a dungeon boss is defeated.

public class BossDefeatedEvent extends Event {
    private final Dungeon dungeon;
    private final Player killer;
    private final LivingEntity boss;
    
    public Dungeon getDungeon();
    public Player getKiller();
    public LivingEntity getBoss();
    public List<Player> getNearbyPlayers();
}

Usage Example:

@EventHandler
public void onBossDefeated(BossDefeatedEvent event) {
    Dungeon dungeon = event.getDungeon();
    Player killer = event.getKiller();
    LivingEntity boss = event.getBoss();
    List<Player> nearbyPlayers = event.getNearbyPlayers();
    
    // Log boss defeat
    getLogger().info("Boss in dungeon #" + dungeon.getId() + " was defeated by " + killer.getName());
    
    // Give rewards to the killer
    killer.giveExp(500);
    killer.sendMessage("You have defeated the dungeon boss and earned 500 XP!");
    
    // Give rewards to nearby players who helped
    for (Player player : nearbyPlayers) {
        if (player != killer) {
            player.giveExp(250);
            player.sendMessage("You helped defeat the dungeon boss and earned 250 XP!");
        }
    }
    
    // Maybe spawn special rewards at the boss location
    Location bossLocation = boss.getLocation();
    bossLocation.getWorld().dropItem(bossLocation, new ItemStack(Material.DIAMOND, 5));
    
    // Create special effects
    bossLocation.getWorld().spawnParticle(Particle.EXPLOSION_HUGE, bossLocation, 10, 3, 3, 3, 0.1);
    bossLocation.getWorld().playSound(bossLocation, Sound.ENTITY_WITHER_DEATH, 1.0f, 1.0f);
}

Exploration Events

RoomDiscoveredEvent

Fired when a player discovers a room for the first time.

public class RoomDiscoveredEvent extends Event {
    private final Room room;
    private final Dungeon dungeon;
    private final Player player;
    
    public Room getRoom();
    public Dungeon getDungeon();
    public Player getPlayer();
}

Usage Example:

@EventHandler
public void onRoomDiscovered(RoomDiscoveredEvent event) {
    Player player = event.getPlayer();
    Room room = event.getRoom();
    Dungeon dungeon = event.getDungeon();
    
    // Different messages/rewards based on room type
    if (room.isBossRoom()) {
        player.sendMessage("§c§lYou have found the boss room! Prepare for battle!");
        player.playSound(player.getLocation(), Sound.BLOCK_BELL_USE, 1.0f, 0.5f);
    } else if (room.isTreasureRoom()) {
        player.sendMessage("§6§lYou have found a treasure room!");
        player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.5f);
    } else if (room.getRoomType().equals("TRAP")) {
        player.sendMessage("§e§lThis room looks dangerous. Watch your step!");
    }
    
    // Track player's room discoveries
    addRoomToPlayerDiscoveries(player.getUniqueId(), room.getId());
}

RoomExploredEvent

Fired when a room's exploration status changes to EXPLORED.

public class RoomExploredEvent extends Event {
    private final Room room;
    private final Dungeon dungeon;
    private final Player player;
    
    public Room getRoom();
    public Dungeon getDungeon();
    public Player getPlayer();
}

Usage Example:

@EventHandler
public void onRoomExplored(RoomExploredEvent event) {
    Player player = event.getPlayer();
    Room room = event.getRoom();
    Dungeon dungeon = event.getDungeon();
    
    // Give exploration XP based on room type
    int xpAmount = 10; // Base amount
    
    if (room.isBossRoom()) {
        xpAmount = 50;
    } else if (room.isTreasureRoom()) {
        xpAmount = 30;
    } else if (room.getRoomType().equals("TRAP")) {
        xpAmount = 20;
    }
    
    player.giveExp(xpAmount);
    
    // Check if this was the last unexplored room
    if (dungeon.isFullyExplored()) {
        player.sendMessage("§a§lCongratulations! You have fully explored this dungeon!");
        
        // Maybe give a special reward for full exploration
        ItemStack reward = new ItemStack(Material.ENCHANTED_BOOK);
        ItemMeta meta = reward.getItemMeta();
        meta.setDisplayName("§6Tome of Dungeon Mastery");
        reward.setItemMeta(meta);
        player.getInventory().addItem(reward);
    }
}

PlayerProgressUpdatedEvent

Fired when a player's dungeon exploration progress is updated.

public class PlayerProgressUpdatedEvent extends Event {
    private final UUID playerUuid;
    private final int dungeonId;
    private final double oldProgress;
    private final double newProgress;
    
    public UUID getPlayerUuid();
    public int getDungeonId();
    public double getOldProgress();
    public double getNewProgress();
    public boolean isCompleted();
}

Usage Example:

@EventHandler
public void onPlayerProgressUpdated(PlayerProgressUpdatedEvent event) {
    UUID playerUuid = event.getPlayerUuid();
    int dungeonId = event.getDungeonId();
    double oldProgress = event.getOldProgress();
    double newProgress = event.getNewProgress();
    
    // Check for milestone achievements
    if (oldProgress < 50 && newProgress >= 50) {
        // Player reached 50% exploration
        Player player = Bukkit.getPlayer(playerUuid);
        if (player != null) {
            player.sendMessage("§6You've explored half of the dungeon!");
            player.playSound(player.getLocation(), Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.0f);
        }
    }
    
    // Check for dungeon completion
    if (event.isCompleted()) {
        // Track completion in custom stats system
        addCompletedDungeon(playerUuid, dungeonId);
        
        // Maybe update global leaderboard
        updateDungeonCompletionLeaderboard(playerUuid);
    }
}

Integration Example: Custom Rewards System

Here's a complete example of how to integrate with AIDungeon events to create a custom rewards system:

import com.ubivismedia.aidungeon.events.*;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.plugin.java.JavaPlugin;

public class DungeonRewardsPlugin extends JavaPlugin implements Listener {
    
    private Map<UUID, Integer> playerPoints = new HashMap<>();
    
    @Override
    public void onEnable() {
        getServer().getPluginManager().registerEvents(this, this);
        
        // Register commands
        getCommand("dungeonpoints").setExecutor(new DungeonPointsCommand(this));
        
        // Load player points from database
        loadPlayerPoints();
    }
    
    @EventHandler
    public void onDungeonDiscovered(DungeonDiscoveredEvent event) {
        // Award points for discovering a dungeon
        addPlayerPoints(event.getPlayer().getUniqueId(), 10);
        event.getPlayer().sendMessage("§6+10 Dungeon Points for discovering a new dungeon!");
    }
    
    @EventHandler
    public void onRoomDiscovered(RoomDiscoveredEvent event) {
        // Award points based on room type
        int points = 0;
        Room room = event.getRoom();
        
        if (room.isBossRoom()) {
            points = 15;
        } else if (room.isTreasureRoom()) {
            points = 10;
        } else if (room.getRoomType().equals("TRAP")) {
            points = 8;
        } else {
            points = 5;
        }
        
        addPlayerPoints(event.getPlayer().getUniqueId(), points);
        event.getPlayer().sendMessage("§6+" + points + " Dungeon Points for discovering a " + 
                                    room.getRoomType().toLowerCase() + " room!");
    }
    
    @EventHandler
    public void onBossDefeated(BossDefeatedEvent event) {
        // Big reward for defeating a boss
        addPlayerPoints(event.getKiller().getUniqueId(), 50);
        event.getKiller().sendMessage("§6+50 Dungeon Points for defeating the dungeon boss!");
        
        // Smaller reward for helpers
        for (Player player : event.getNearbyPlayers()) {
            if (player != event.getKiller()) {
                addPlayerPoints(player.getUniqueId(), 25);
                player.sendMessage("§6+25 Dungeon Points for helping defeat the dungeon boss!");
            }
        }
    }
    
    @EventHandler
    public void onDungeonCollapseEnd(DungeonCollapseEndEvent event) {
        // Maybe some server-wide rewards or events when dungeons collapse
        getServer().broadcastMessage("§7A dungeon has collapsed and returned to nature.");
        
        // Spawn a new dungeon somewhere else to maintain dungeon count
        getServer().dispatchCommand(getServer().getConsoleSender(), "aid scan");
    }
    
    private void addPlayerPoints(UUID playerUuid, int points) {
        playerPoints.put(playerUuid, playerPoints.getOrDefault(playerUuid, 0) + points);
        savePlayerPoints(playerUuid);
        
        // Check for reward thresholds
        checkRewards(playerUuid);
    }
    
    private void checkRewards(UUID playerUuid) {
        int totalPoints = playerPoints.getOrDefault(playerUuid, 0);
        Player player = getServer().getPlayer(playerUuid);
        
        if (player == null) return;
        
        // Define reward tiers
        if (totalPoints >= 1000 && !hasReward(playerUuid, "tier3")) {
            giveReward(player, "tier3");
            player.sendMessage("§a§lYou've reached 1000 Dungeon Points and unlocked the Elite Explorer reward!");
        } else if (totalPoints >= 500 && !hasReward(playerUuid, "tier2")) {
            giveReward(player, "tier2");
            player.sendMessage("§a§lYou've reached 500 Dungeon Points and unlocked the Veteran Explorer reward!");
        } else if (totalPoints >= 100 && !hasReward(playerUuid, "tier1")) {
            giveReward(player, "tier1");
            player.sendMessage("§a§lYou've reached 100 Dungeon Points and unlocked the Novice Explorer reward!");
        }
    }
    
    // Database methods would be implemented here
    private void loadPlayerPoints() { /* Implementation */ }
    private void savePlayerPoints(UUID playerUuid) { /* Implementation */ }
    private boolean hasReward(UUID playerUuid, String tier) { /* Implementation */ }
    private void giveReward(Player player, String tier) { /* Implementation */ }
    
    // Getter for other classes
    public int getPlayerPoints(UUID playerUuid) {
        return playerPoints.getOrDefault(playerUuid, 0);
    }
}

Event Registration Best Practices

When registering event listeners for AIDungeon events, follow these best practices:

  1. Specify event priorities: Use the @EventHandler(priority = EventPriority.NORMAL) annotation to control the order of your event handlers.

  2. Handle exceptions: Wrap your event handling code in try-catch blocks to prevent your plugin from breaking AIDungeon functionality.

  3. Check for nulls: Always validate event data before using it, as some events may have optional fields.

  4. Respect cancellation: For cancellable events, check event.isCancelled() before taking action, and respect the wishes of other plugins.

  5. Use async tasks for heavy processing: If your event handler needs to perform database operations or other heavy processing, use Bukkit.getScheduler().runTaskAsynchronously().

Event Firing Order

AIDungeon events follow this general firing order:

  1. Generation phase:

    • DungeonGeneratedEvent
  2. Discovery phase:

    • DungeonDiscoveredEvent
    • RoomDiscoveredEvent (as players find rooms)
  3. Exploration phase:

    • RoomExploredEvent (as rooms are fully explored)
    • PlayerProgressUpdatedEvent (as exploration percentages change)
    • BossSpawnedEvent (when entering the boss room)
    • BossDefeatedEvent (when the boss is killed)
  4. Completion phase:

    • DungeonStatusChangedEvent (ACTIVE → EXPLORED)
    • DungeonCollapseStartEvent (after collapse delay)
    • DungeonCollapseEndEvent (when collapse completes)

Understanding this order helps you plan your event handlers to properly integrate with the dungeon lifecycle.

Integration

This guide explains how to integrate your plugin with AIDungeon, including examples for common integration scenarios, best practices, and solutions to potential challenges.

Basic Integration

Setting Up Your Plugin

First, add AIDungeon as a dependency in your plugin.yml:

name: YourPlugin
version: 1.0.0
main: com.yourname.yourplugin.YourPlugin
api-version: '1.21'
depend: [AIDungeon]  # Hard dependency - your plugin won't load without AIDungeon
# OR
softdepend: [AIDungeon]  # Soft dependency - your plugin will load even if AIDungeon is missing

Accessing the API

In your main plugin class, get the AIDungeon instance:

import com.ubivismedia.aidungeon.AIDungeon;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;

public class YourPlugin extends JavaPlugin {
    private AIDungeon aidungeon;
    private boolean aidungeonHooked = false;
    
    @Override
    public void onEnable() {
        // Get the AIDungeon plugin instance
        Plugin plugin = getServer().getPluginManager().getPlugin("AIDungeon");
        
        if (plugin instanceof AIDungeon) {
            aidungeon = (AIDungeon) plugin;
            aidungeonHooked = true;
            getLogger().info("Successfully hooked into AIDungeon!");
        } else {
            getLogger().warning("AIDungeon not found or not enabled!");
            if (plugin == null) {
                getLogger().warning("Make sure AIDungeon is installed!");
            } else {
                getLogger().warning("Incompatible version of AIDungeon detected!");
            }
        }
        
        // Register event listeners, commands, etc.
        if (aidungeonHooked) {
            getServer().getPluginManager().registerEvents(new YourDungeonListener(this), this);
            getCommand("yourdungeoncommand").setExecutor(new YourDungeonCommand(this));
        }
    }
    
    public AIDungeon getAIDungeon() {
        return aidungeon;
    }
    
    public boolean isAIDungeonHooked() {
        return aidungeonHooked;
    }
}

Common Integration Scenarios

Custom Rewards System

This example shows how to create a custom rewards system based on dungeon exploration:

import com.ubivismedia.aidungeon.events.BossDefeatedEvent;
import com.ubivismedia.aidungeon.events.DungeonDiscoveredEvent;
import com.ubivismedia.aidungeon.events.RoomExploredEvent;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;

public class CustomRewardsListener implements Listener {
    private final YourPlugin plugin;
    
    public CustomRewardsListener(YourPlugin plugin) {
        this.plugin = plugin;
    }
    
    @EventHandler
    public void onDungeonDiscovered(DungeonDiscoveredEvent event) {
        Player player = event.getPlayer();
        
        // Give custom currency
        plugin.getEconomyManager().addCurrency(player, "dungeon_tokens", 5);
        player.sendMessage("§a+5 Dungeon Tokens for discovering a new dungeon!");
    }
    
    @EventHandler
    public void onRoomExplored(RoomExploredEvent event) {
        Player player = event.getPlayer();
        
        // Give rewards based on room type
        if (event.getRoom().isBossRoom()) {
            plugin.getEconomyManager().addCurrency(player, "dungeon_tokens", 10);
            player.sendMessage("§a+10 Dungeon Tokens for exploring the boss room!");
        } else if (event.getRoom().isTreasureRoom()) {
            plugin.getEconomyManager().addCurrency(player, "dungeon_tokens", 7);
            player.sendMessage("§a+7 Dungeon Tokens for exploring a treasure room!");
        } else {
            plugin.getEconomyManager().addCurrency(player, "dungeon_tokens", 2);
            player.sendMessage("§a+2 Dungeon Tokens for exploring a room!");
        }
    }
    
    @EventHandler
    public void onBossDefeated(BossDefeatedEvent event) {
        Player player = event.getKiller();
        
        // Big reward for boss defeat
        plugin.getEconomyManager().addCurrency(player, "dungeon_tokens", 25);
        player.sendMessage("§a+25 Dungeon Tokens for defeating the dungeon boss!");
        
        // Add to player stats
        plugin.getStatsManager().incrementStat(player, "bosses_defeated", 1);
    }
}

Custom Dungeon Commands

Add your own commands that interact with AIDungeon's data:

import com.ubivismedia.aidungeon.AIDungeon;
import com.ubivismedia.aidungeon.dungeon.DungeonManager;
import com.ubivismedia.aidungeon.model.Dungeon;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;

public class DungeonStatsCommand implements CommandExecutor {
    private final YourPlugin plugin;
    
    public DungeonStatsCommand(YourPlugin plugin) {
        this.plugin = plugin;
    }
    
    @Override
    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
        if (!(sender instanceof Player)) {
            sender.sendMessage("This command can only be used by players.");
            return true;
        }
        
        Player player = (Player) sender;
        
        if (!plugin.isAIDungeonHooked()) {
            player.sendMessage("§cAIDungeon is not currently available.");
            return true;
        }
        
        AIDungeon aidungeon = plugin.getAIDungeon();
        DungeonManager dungeonManager = aidungeon.getDungeonManager();
        
        // Show the player their dungeon exploration statistics
        int totalDungeons = countPlayerDungeons(player);
        int completedDungeons = countPlayerCompletedDungeons(player);
        int defeatedBosses = plugin.getStatsManager().getStat(player, "bosses_defeated");
        
        player.sendMessage("§6=== Your Dungeon Statistics ===");
        player.sendMessage("§eDungeons Explored: §f" + totalDungeons);
        player.sendMessage("§eDungeons Completed: §f" + completedDungeons);
        player.sendMessage("§eBosses Defeated: §f" + defeatedBosses);
        player.sendMessage("§eDungeon Tokens: §f" + plugin.getEconomyManager().getCurrency(player, "dungeon_tokens"));
        
        // Show currently active dungeon if player is in one
        Dungeon currentDungeon = dungeonManager.getDungeonAtLocation(player.getLocation());
        if (currentDungeon != null) {
            player.sendMessage("§eCurrently in: §f" + formatDungeonName(currentDungeon.getTheme()));
            player.sendMessage("§eExploration: §f" + String.format("%.1f%%", currentDungeon.getExplorationPercentage()));
            player.sendMessage("§eBoss Defeated: §f" + (currentDungeon.isBossDefeated() ? "Yes" : "No"));
        }
        
        return true;
    }
    
    private String formatDungeonName(String theme) {
        // Convert theme_name to Title Case
        String[] words = theme.split("_");
        StringBuilder result = new StringBuilder();
        
        for (String word : words) {
            if (word.length() > 0) {
                result.append(Character.toUpperCase(word.charAt(0)))
                      .append(word.substring(1))
                      .append(" ");
            }
        }
        
        return result.toString().trim();
    }
    
    private int countPlayerDungeons(Player player) {
        // Implementation to count dungeons from your plugin's database
        return plugin.getStatsManager().getPlayerDungeonCount(player);
    }
    
    private int countPlayerCompletedDungeons(Player player) {
        // Implementation to count completed dungeons from your plugin's database
        return plugin.getStatsManager().getPlayerCompletedDungeonCount(player);
    }
}

Dungeon Enhancements

Add custom features to dungeons:

import com.ubivismedia.aidungeon.events.DungeonGeneratedEvent;
import com.ubivismedia.aidungeon.events.RoomDiscoveredEvent;
import com.ubivismedia.aidungeon.model.Dungeon;
import com.ubivismedia.aidungeon.model.Room;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.EntityType;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.scheduler.BukkitRunnable;

public class DungeonEnhancementsListener implements Listener {
    private final YourPlugin plugin;
    
    public DungeonEnhancementsListener(YourPlugin plugin) {
        this.plugin = plugin;
    }
    
    @EventHandler
    public void onDungeonGenerated(DungeonGeneratedEvent event) {
        Dungeon dungeon = event.getDungeon();
        World world = event.getWorld();
        
        // Add custom particle effects at the dungeon entrance
        Location entranceLocation = dungeon.getEntranceLocation(world);
        
        // Schedule repeating particle effects
        new BukkitRunnable() {
            @Override
            public void run() {
                // Check if dungeon still exists and is active
                if (plugin.getAIDungeon().getDungeonManager().getDungeon(dungeon.getId()) == null) {
                    this.cancel();
                    return;
                }
                
                // Spawn particles
                world.spawnParticle(org.bukkit.Particle.DRAGON_BREATH, 
                        entranceLocation.clone().add(0, 1.5, 0), 
                        20, 0.5, 0.5, 0.5, 0.02);
            }
        }.runTaskTimer(plugin, 20L, 40L); // Run every 2 seconds
        
        // Add custom entities to the boss room
        Room bossRoom = dungeon.getBossRoom();
        if (bossRoom != null) {
            Location bossRoomCenter = bossRoom.getCenterLocation(world);
            
            // Spawn minions in the boss room
            for (int i = 0; i < 4; i++) {
                double offsetX = (Math.random() - 0.5) * (bossRoom.getWidth() * 0.8);
                double offsetZ = (Math.random() - 0.5) * (bossRoom.getLength() * 0.8);
                
                Location spawnLoc = bossRoomCenter.clone().add(offsetX, 0, offsetZ);
                world.spawnEntity(spawnLoc, EntityType.SKELETON);
            }
        }
    }
    
    @EventHandler
    public void onRoomDiscovered(RoomDiscoveredEvent event) {
        Room room = event.getRoom();
        Player player = event.getPlayer();
        World world = player.getWorld();
        
        // Add special effects when a player discovers a room
        Location roomCenter = room.getCenterLocation(world);
        
        // Play sound effect
        world.playSound(roomCenter, org.bukkit.Sound.BLOCK_ENCHANTMENT_TABLE_USE, 1.0f, 0.5f);
        
        // Spawn ambient particles
        world.spawnParticle(org.bukkit.Particle.ENCHANTMENT_TABLE, 
                roomCenter, 50, room.getWidth()/2.0, room.getHeight()/2.0, room.getLength()/2.0, 0.1);
    }
}

Economy Integration

Integrate with Vault for economic rewards:

import com.ubivismedia.aidungeon.events.BossDefeatedEvent;
import com.ubivismedia.aidungeon.events.DungeonDiscoveredEvent;
import net.milkbowl.vault.economy.Economy;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.plugin.RegisteredServiceProvider;

public class VaultIntegrationListener implements Listener {
    private final YourPlugin plugin;
    private Economy economy;
    
    public VaultIntegrationListener(YourPlugin plugin) {
        this.plugin = plugin;
        setupEconomy();
    }
    
    private boolean setupEconomy() {
        if (plugin.getServer().getPluginManager().getPlugin("Vault") == null) {
            return false;
        }
        
        RegisteredServiceProvider<Economy> rsp = plugin.getServer().getServicesManager().getRegistration(Economy.class);
        if (rsp == null) {
            return false;
        }
        
        economy = rsp.getProvider();
        return economy != null;
    }
    
    @EventHandler
    public void onDungeonDiscovered(DungeonDiscoveredEvent event) {
        if (economy == null) return;
        
        Player player = event.getPlayer();
        
        // Give money reward for discovering a dungeon
        double reward = 50.0;
        economy.depositPlayer(player, reward);
        player.sendMessage("§a+" + reward + " " + economy.currencyNamePlural() + " for discovering a dungeon!");
    }
    
    @EventHandler
    public void onBossDefeated(BossDefeatedEvent event) {
        if (economy == null) return;
        
        Player player = event.getKiller();
        
        // Give money reward for defeating a boss
        double reward = 250.0;
        economy.depositPlayer(player, reward);
        player.sendMessage("§a+" + reward + " " + economy.currencyNamePlural() + " for defeating a dungeon boss!");
        
        // Give smaller rewards to helpers
        double helperReward = 100.0;
        for (Player helper : event.getNearbyPlayers()) {
            if (helper != player) {
                economy.depositPlayer(helper, helperReward);
                helper.sendMessage("§a+" + helperReward + " " + economy.currencyNamePlural() + " for helping defeat a dungeon boss!");
            }
        }
    }
}

Dungeon Data Access

Querying Dungeon Information

Access and query dungeon data for your plugin's features:

import com.ubivismedia.aidungeon.AIDungeon;
import com.ubivismedia.aidungeon.dungeon.DungeonManager;
import com.ubivismedia.aidungeon.model.Dungeon;
import com.ubivismedia.aidungeon.model.Room;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class DungeonDataAnalyzer {
    private final YourPlugin plugin;
    
    public DungeonDataAnalyzer(YourPlugin plugin) {
        this.plugin = plugin;
    }
    
    public Map<String, Integer> getThemeDistribution() {
        if (!plugin.isAIDungeonHooked()) {
            return new HashMap<>();
        }
        
        DungeonManager dungeonManager = plugin.getAIDungeon().getDungeonManager();
        Map<Integer, Dungeon> dungeons = dungeonManager.getActiveDungeons();
        
        // Count dungeons by theme
        Map<String, Integer> themeCount = new HashMap<>();
        
        for (Dungeon dungeon : dungeons.values()) {
            String theme = dungeon.getTheme();
            themeCount.put(theme, themeCount.getOrDefault(theme, 0) + 1);
        }
        
        return themeCount;
    }
    
    public Map<String, Integer> getRoomTypeDistribution() {
        if (!plugin.isAIDungeonHooked()) {
            return new HashMap<>();
        }
        
        DungeonManager dungeonManager = plugin.getAIDungeon().getDungeonManager();
        Map<Integer, Dungeon> dungeons = dungeonManager.getActiveDungeons();
        
        // Count rooms by type
        Map<String, Integer> roomTypeCount = new HashMap<>();
        
        for (Dungeon dungeon : dungeons.values()) {
            for (Room room : dungeon.getRooms()) {
                String roomType = room.getRoomType();
                roomTypeCount.put(roomType, roomTypeCount.getOrDefault(roomType, 0) + 1);
            }
        }
        
        return roomTypeCount;
    }
    
    public double getAverageExplorationPercentage() {
        if (!plugin.isAIDungeonHooked()) {
            return 0.0;
        }
        
        DungeonManager dungeonManager = plugin.getAIDungeon().getDungeonManager();
        Map<Integer, Dungeon> dungeons = dungeonManager.getActiveDungeons();
        
        if (dungeons.isEmpty()) {
            return 0.0;
        }
        
        // Calculate average exploration percentage
        double totalPercentage = 0.0;
        
        for (Dungeon dungeon : dungeons.values()) {
            totalPercentage += dungeon.getExplorationPercentage();
        }
        
        return totalPercentage / dungeons.size();
    }
    
    public List<Dungeon> getNewestDungeons(int count) {
        if (!plugin.isAIDungeonHooked()) {
            return List.of();
        }
        
        DungeonManager dungeonManager = plugin.getAIDungeon().getDungeonManager();
        Map<Integer, Dungeon> dungeons = dungeonManager.getActiveDungeons();
        
        // Sort dungeons by creation date (newest first) and take the first 'count'
        return dungeons.values().stream()
                .sorted((d1, d2) -> d2.getCreatedAt().compareTo(d1.getCreatedAt()))
                .limit(count)
                .toList();
    }
}

Custom Database Queries

Perform direct database queries for complex data retrieval:

import com.ubivismedia.aidungeon.AIDungeon;
import com.ubivismedia.aidungeon.database.DatabaseManager;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class DungeonDatabaseQueries {
    private final YourPlugin plugin;
    
    public DungeonDatabaseQueries(YourPlugin plugin) {
        this.plugin = plugin;
    }
    
    public List<Integer> getPlayerExploredDungeons(UUID playerUuid) {
        if (!plugin.isAIDungeonHooked()) {
            return List.of();
        }
        
        List<Integer> exploredDungeons = new ArrayList<>();
        DatabaseManager dbManager = plugin.getAIDungeon().getDatabaseManager();
        
        try (Connection conn = dbManager.getConnection();
             PreparedStatement stmt = conn.prepareStatement(
                     "SELECT dungeon_id FROM player_progress WHERE player_uuid = ? AND exploration_percent > 0")) {
            
            stmt.setString(1, playerUuid.toString());
            ResultSet rs = stmt.executeQuery();
            
            while (rs.next()) {
                exploredDungeons.add(rs.getInt("dungeon_id"));
            }
            
        } catch (SQLException e) {
            plugin.getLogger().severe("Error querying player explored dungeons: " + e.getMessage());
            e.printStackTrace();
        }
        
        return exploredDungeons;
    }
    
    public Map<String, Integer> getTopDungeonExplorers(int limit) {
        if (!plugin.isAIDungeonHooked()) {
            return Map.of();
        }
        
        Map<String, Integer> topExplorers = new LinkedHashMap<>();
        DatabaseManager dbManager = plugin.getAIDungeon().getDatabaseManager();
        
        try (Connection conn = dbManager.getConnection();
             PreparedStatement stmt = conn.prepareStatement(
                     "SELECT player_uuid, COUNT(*) as dungeon_count FROM player_progress " +
                     "WHERE exploration_percent = 100 " +
                     "GROUP BY player_uuid ORDER BY dungeon_count DESC LIMIT ?")) {
            
            stmt.setInt(1, limit);
            ResultSet rs = stmt.executeQuery();
            
            while (rs.next()) {
                String playerUuid = rs.getString("player_uuid");
                int dungeonCount = rs.getInt("dungeon_count");
                
                // Convert UUID to player name if possible
                String playerName = Bukkit.getOfflinePlayer(UUID.fromString(playerUuid)).getName();
                if (playerName == null) {
                    playerName = playerUuid;
                }
                
                topExplorers.put(playerName, dungeonCount);
            }
            
        } catch (SQLException e) {
            plugin.getLogger().severe("Error querying top dungeon explorers: " + e.getMessage());
            e.printStackTrace();
        }
        
        return topExplorers;
    }
}

Plugin Interoperability

PlaceholderAPI Integration

Provide AIDungeon-related placeholders for other plugins:

import com.ubivismedia.aidungeon.AIDungeon;
import com.ubivismedia.aidungeon.dungeon.DungeonManager;
import com.ubivismedia.aidungeon.model.Dungeon;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;

public class AIDungeonPlaceholders extends PlaceholderExpansion {
    private final YourPlugin plugin;
    
    public AIDungeonPlaceholders(YourPlugin plugin) {
        this.plugin = plugin;
    }
    
    @Override
    public String getIdentifier() {
        return "yourdungeon";
    }
    
    @Override
    public String getAuthor() {
        return plugin.getDescription().getAuthors().get(0);
    }
    
    @Override
    public String getVersion() {
        return plugin.getDescription().getVersion();
    }
    
    @Override
    public boolean persist() {
        return true;
    }
    
    @Override
    public String onRequest(OfflinePlayer offlinePlayer, String identifier) {
        if (offlinePlayer == null || !offlinePlayer.isOnline() || !plugin.isAIDungeonHooked()) {
            return null;
        }
        
        Player player = offlinePlayer.getPlayer();
        AIDungeon aidungeon = plugin.getAIDungeon();
        DungeonManager dungeonManager = aidungeon.getDungeonManager();
        
        // Current dungeon placeholders
        if (identifier.equals("current_dungeon")) {
            Dungeon dungeon = dungeonManager.getDungeonAtLocation(player.getLocation());
            return dungeon != null ? formatDungeonName(dungeon.getTheme()) : "None";
        }
        
        if (identifier.equals("current_dungeon_exploration")) {
            Dungeon dungeon = dungeonManager.getDungeonAtLocation(player.getLocation());
            return dungeon != null ? String.format("%.1f%%", dungeon.getExplorationPercentage()) : "0.0%";
        }
        
        if (identifier.equals("current_dungeon_boss_defeated")) {
            Dungeon dungeon = dungeonManager.getDungeonAtLocation(player.getLocation());
            return dungeon != null ? (dungeon.isBossDefeated() ? "Yes" : "No") : "N/A";
        }
        
        // Player stats placeholders
        if (identifier.equals("total_dungeons")) {
            return String.valueOf(plugin.getDungeonDataAnalyzer().getPlayerDungeonCount(player.getUniqueId()));
        }
        
        if (identifier.equals("completed_dungeons")) {
            return String.valueOf(plugin.getDungeonDataAnalyzer().getPlayerCompletedDungeonCount(player.getUniqueId()));
        }
        
        if (identifier.equals("boss_kills")) {
            return String.valueOf(plugin.getStatsManager().getStat(player, "bosses_defeated"));
        }
        
        if (identifier.equals("dungeon_tokens")) {
            return String.valueOf(plugin.getEconomyManager().getCurrency(player, "dungeon_tokens"));
        }
        
        return null;
    }
    
    private String formatDungeonName(String theme) {
        String[] words = theme.split("_");
        StringBuilder result = new StringBuilder();
        
        for (String word : words) {
            if (word.length() > 0) {
                result.append(Character.toUpperCase(word.charAt(0)))
                      .append(word.substring(1))
                      .append(" ");
            }
        }
        
        return result.toString().trim();
    }
}

Dungeon Progression System

Create a dungeon progression system that builds on top of AIDungeon:

import com.ubivismedia.aidungeon.events.BossDefeatedEvent;
import com.ubivismedia.aidungeon.events.DungeonDiscoveredEvent;
import com.ubivismedia.aidungeon.model.Dungeon;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;

public class DungeonProgressionSystem implements Listener {
    private final YourPlugin plugin;
    
    // Define progression tiers
    private final String[] PROGRESSION_TIERS = {
        "Novice Explorer",
        "Dungeon Delver",
        "Tomb Raider",
        "Dungeon Master",
        "Legendary Conqueror"
    };
    
    // Dungeon completions required for each tier
    private final int[] TIER_REQUIREMENTS = {1, 5, 15, 30, 50};
    
    // Special abilities unlocked at each tier
    private final String[] TIER_ABILITIES = {
        "dungeonheal",      // Regenerate health in dungeons
        "dungeoncompass",   // Compass pointing to nearest dungeon
        "dungeonrescue",    // Teleport to dungeon entrance when health is low
        "dungeonloot",      // Improved loot chances
        "dungeonblink"      // Short-range teleport within dungeons
    };
    
    public DungeonProgressionSystem(YourPlugin plugin) {
        this.plugin = plugin;
    }
    
    @EventHandler
    public void onBossDefeated(BossDefeatedEvent event) {
        Player player = event.getKiller();
        
        // Update player's completed dungeon count
        int completedDungeons = plugin.getDungeonDataAnalyzer().getPlayerCompletedDungeonCount(player.getUniqueId());
        completedDungeons++;
        
        // Save the updated count
        plugin.getDungeonDataAnalyzer().setPlayerCompletedDungeonCount(player.getUniqueId(), completedDungeons);
        
        // Check if player leveled up in the progression system
        int oldTier = getCurrentTier(completedDungeons - 1);
        int newTier = getCurrentTier(completedDungeons);
        
        if (newTier > oldTier) {
            // Player has reached a new tier!
            String tierName = PROGRESSION_TIERS[newTier];
            String ability = TIER_ABILITIES[newTier];
            
            player.sendMessage("§a§l=============================");
            player.sendMessage("§6§lDUNGEON TIER REACHED: §e" + tierName);
            player.sendMessage("§6§lNew Ability Unlocked: §e" + formatAbilityName(ability));
            player.sendMessage("§a§l=============================");
            
            // Give player the new ability
            grantAbility(player, ability);
        }
    }
    
    private int getCurrentTier(int completedDungeons) {
        for (int i = TIER_REQUIREMENTS.length - 1; i >= 0; i--) {
            if (completedDungeons >= TIER_REQUIREMENTS[i]) {
                return i;
            }
        }
        return -1; // No tier yet
    }
    
    private String formatAbilityName(String ability) {
        // Remove "dungeon" prefix and capitalize
        String name = ability.replace("dungeon", "");
        return Character.toUpperCase(name.charAt(0)) + name.substring(1);
    }
    
    private void grantAbility(Player player, String ability) {
        // Implementation to give player the ability
        plugin.getAbilityManager().unlockAbility(player.getUniqueId(), ability);
    }
    
    // Command to use abilities would be implemented separately
}

Best Practices

Error Handling and Graceful Degradation

public class SafeDungeonAccess {
    private final YourPlugin plugin;
    
    public SafeDungeonAccess(YourPlugin plugin) {
        this.plugin = plugin;
    }
    
    public Optional<Dungeon> getCurrentDungeon(Player player) {
        try {
            if (!plugin.isAIDungeonHooked()) {
                return Optional.empty();
            }
            
            DungeonManager dungeonManager = plugin.getAIDungeon().getDungeonManager();
            Dungeon dungeon = dungeonManager.getDungeonAtLocation(player.getLocation());
            
            return Optional.ofNullable(dungeon);
        } catch (Exception e) {
            plugin.getLogger().warning("Error getting current dungeon: " + e.getMessage());
            return Optional.empty();
        }
    }
    
    public void safelyTrackExploration(Player player) {
        try {
            if (!plugin.isAIDungeonHooked()) {
                return;
            }
            
            DungeonManager dungeonManager = plugin.getAIDungeon().getDungeonManager();
            Dungeon dungeon = dungeonManager.getDungeonAtLocation(player.getLocation());
            
            if (dungeon != null) {
                Room room = dungeonManager.getRoomAtLocation(player.getLocation());
                if (room != null) {
                    dungeonManager.trackPlayerExploration(player, dungeon, room);
                }
            }
        } catch (Exception e) {
            plugin.getLogger().warning("Error tracking exploration: " + e.getMessage());
        }
    }
}

Version Compatibility

Handle potential API changes across different versions of AIDungeon:

import com.ubivismedia.aidungeon.AIDungeon;
import org.bukkit.plugin.Plugin;

public class VersionCompatibilityHandler {
    private final YourPlugin plugin;
    private String aidungeonVersion;
    private boolean isCompatible = false;
    
    public VersionCompatibilityHandler(YourPlugin plugin) {
        this.plugin = plugin;
        checkCompatibility();
    }
    
    private void checkCompatibility() {
        Plugin aidungeonPlugin = plugin.getServer().getPluginManager().getPlugin("AIDungeon");
        
        if (aidungeonPlugin == null || !(aidungeonPlugin instanceof AIDungeon)) {
            plugin.getLogger().warning("AIDungeon plugin not found or is not the expected type.");
            return;
        }
        
        // Get version string
        aidungeonVersion = aidungeonPlugin.getDescription().getVersion();
        
        // Parse version (assuming format like "0.2.0")
        String[] versionParts = aidungeonVersion.split("\\.");
        if (versionParts.length < 2) {
            plugin.getLogger().warning("Unable to parse AIDungeon version: " + aidungeonVersion);
            return;
        }
        
        try {
            int major = Integer.parseInt(versionParts[0]);
            int minor = Integer.parseInt(versionParts[1]);
            
            // Check if version is compatible (e.g., 0.2.0 or newer)
            if (major > 0 || (major == 0 && minor >= 2)) {
                isCompatible = true;
                plugin.getLogger().info("Compatible AIDungeon version detected: " + aidungeonVersion);
            } else {
                plugin.getLogger().warning("Incompatible AIDungeon version: " + aidungeonVersion + 
                                         ". Requires 0.2.0 or newer.");
            }
        } catch (NumberFormatException e) {
            plugin.getLogger().warning("Error parsing AIDungeon version: " + e.getMessage());
        }
    }
    
    public boolean isCompatible() {
        return isCompatible;
    }
    
    public String getVersion() {
        return aidungeonVersion;
    }
}

## Performance Considerations

When integrating with AIDungeon, keep these performance factors in mind:

### Efficient Event Handling

```java
import com.ubivismedia.aidungeon.events.DungeonDiscoveredEvent;
import com.ubivismedia.aidungeon.events.RoomDiscoveredEvent;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;

public class OptimizedEventHandler implements Listener {
    private final YourPlugin plugin;
    private long lastRoomProcessTime = 0;
    private static final long ROOM_PROCESS_COOLDOWN = 1000; // 1 second cooldown
    
    public OptimizedEventHandler(YourPlugin plugin) {
        this.plugin = plugin;
    }
    
    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void onDungeonDiscovered(DungeonDiscoveredEvent event) {
        // Use MONITOR priority to run after AIDungeon's own handlers
        
        // Handle dungeon discovery (this event is infrequent, so no cooldown needed)
        processDiscovery(event);
    }
    
    @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
    public void onRoomDiscovered(RoomDiscoveredEvent event) {
        // Add rate limiting for frequent events
        long currentTime = System.currentTimeMillis();
        if (currentTime - lastRoomProcessTime < ROOM_PROCESS_COOLDOWN) {
            return; // Skip if processed recently
        }
        
        // Process room discovery
        processRoomDiscovery(event);
        lastRoomProcessTime = currentTime;
    }
    
    // Use async processing for database operations
    private void processDiscovery(DungeonDiscoveredEvent event) {
        // Quick synchronous operations
        event.getPlayer().sendMessage("Dungeon discovered!");
        
        // Move heavy processing to async task
        plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
            // Database operations
            plugin.getPlayerDatabase().recordDungeonDiscovery(
                event.getPlayer().getUniqueId(), 
                event.getDungeon().getId()
            );
            
            // Any other heavy processing
            processStatistics(event.getDungeon());
        });
    }
    
    private void processRoomDiscovery(RoomDiscoveredEvent event) {
        // Implementation here
    }
    
    private void processStatistics(Dungeon dungeon) {
        // Heavy statistical processing
    }
}

Caching Dungeon Data

import com.ubivismedia.aidungeon.model.Dungeon;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

public class DungeonDataCache {
    private final Map<Integer, Dungeon> dungeonCache = new HashMap<>();
    private final Map<UUID, Map<Integer, Double>> playerProgressCache = new HashMap<>();
    private final Map<Integer, Long> cacheTimes = new HashMap<>();
    
    private static final long CACHE_EXPIRY = TimeUnit.MINUTES.toMillis(5); // 5 minute cache
    
    // Cache a dungeon
    public void cacheDungeon(Dungeon dungeon) {
        dungeonCache.put(dungeon.getId(), dungeon);
        cacheTimes.put(dungeon.getId(), System.currentTimeMillis());
    }
    
    // Get a dungeon from cache
    public Dungeon getDungeon(int dungeonId) {
        // Check if cache is valid
        Long cacheTime = cacheTimes.get(dungeonId);
        if (cacheTime == null || System.currentTimeMillis() - cacheTime > CACHE_EXPIRY) {
            dungeonCache.remove(dungeonId);
            cacheTimes.remove(dungeonId);
            return null;
        }
        
        return dungeonCache.get(dungeonId);
    }
    
    // Cache player progress
    public void cachePlayerProgress(UUID playerUuid, int dungeonId, double progress) {
        playerProgressCache.computeIfAbsent(playerUuid, k -> new HashMap<>())
            .put(dungeonId, progress);
    }
    
    // Get player progress
    public Double getPlayerProgress(UUID playerUuid, int dungeonId) {
        Map<Integer, Double> progressMap = playerProgressCache.get(playerUuid);
        return progressMap != null ? progressMap.get(dungeonId) : null;
    }
    
    // Clear expired cache entries
    public void cleanupCache() {
        long currentTime = System.currentTimeMillis();
        
        // Remove expired dungeon cache entries
        cacheTimes.entrySet().removeIf(entry -> 
            currentTime - entry.getValue() > CACHE_EXPIRY
        );
        
        dungeonCache.keySet().removeIf(id -> !cacheTimes.containsKey(id));
    }
}

Minimizing Synchronous Operations

When interacting with dungeons, especially on player movement events, keep synchronous operations light:

import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.scheduler.BukkitRunnable;

public class DungeonPlayerTracker implements Listener {
    private final YourPlugin plugin;
    private final Map<UUID, Location> lastProcessedLocations = new HashMap<>();
    
    public DungeonPlayerTracker(YourPlugin plugin) {
        this.plugin = plugin;
        
        // Schedule periodic cleanup of location cache
        new BukkitRunnable() {
            @Override
            public void run() {
                // Remove entries for offline players
                for (Player player : plugin.getServer().getOnlinePlayers()) {
                    lastProcessedLocations.remove(player.getUniqueId());
                }
            }
        }.runTaskTimer(plugin, 1200L, 1200L); // Run every minute
    }
    
    @EventHandler(ignoreCancelled = true)
    public void onPlayerMove(PlayerMoveEvent event) {
        // Skip if player only rotated but didn't change position
        if (event.getFrom().getBlockX() == event.getTo().getBlockX() &&
            event.getFrom().getBlockY() == event.getTo().getBlockY() &&
            event.getFrom().getBlockZ() == event.getTo().getBlockZ()) {
            return;
        }
        
        Player player = event.getPlayer();
        Location currentLocation = player.getLocation();
        
        // Only process every 10 blocks moved to reduce overhead
        Location lastLocation = lastProcessedLocations.get(player.getUniqueId());
        if (lastLocation != null && lastLocation.distance(currentLocation) < 10) {
            return;
        }
        
        // Update last processed location
        lastProcessedLocations.put(player.getUniqueId(), currentLocation.clone());
        
        // Schedule async task for non-critical operations
        plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
            // Do database lookups or other heavy operations
            int nearbyDungeons = countNearbyDungeons(currentLocation);
            
            // Schedule sync task for any operations that need to be sync
            if (nearbyDungeons > 0) {
                plugin.getServer().getScheduler().runTask(plugin, () -> {
                    // Update player's HUD or other sync operations
                    updatePlayerDungeonHUD(player, nearbyDungeons);
                });
            }
        });
    }
    
    private int countNearbyDungeons(Location location) {
        // Implementation to count nearby dungeons
        return 0;
    }
    
    private void updatePlayerDungeonHUD(Player player, int dungeonCount) {
        // Implementation to update player's HUD
    }
}

Common Integration Challenges

Handling Plugin Load Order

If your plugin needs AIDungeon to be loaded first:

@Override
public void onEnable() {
    // Check if AIDungeon is loaded
    if (getServer().getPluginManager().getPlugin("AIDungeon") == null) {
        getLogger().severe("AIDungeon plugin not found! Disabling plugin.");
        getServer().getPluginManager().disablePlugin(this);
        return;
    }
    
    // Wait for AIDungeon to be fully enabled
    if (!getServer().getPluginManager().isPluginEnabled("AIDungeon")) {
        getLogger().info("Waiting for AIDungeon to be fully enabled...");
        getServer().getPluginManager().registerEvents(new PluginEnableListener(), this);
    } else {
        initializePlugin();
    }
}

private class PluginEnableListener implements Listener {
    @EventHandler
    public void onPluginEnable(PluginEnableEvent event) {
        if (event.getPlugin().getName().equals("AIDungeon")) {
            getLogger().info("AIDungeon enabled, initializing plugin now.");
            initializePlugin();
            // Unregister this listener
            HandlerList.unregisterAll(this);
        }
    }
}

private void initializePlugin() {
    // Initialize your plugin's features
}

Database Concurrency

When interacting with AIDungeon's database:

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.concurrent.CompletableFuture;

public class SafeDatabaseOperations {
    private final YourPlugin plugin;
    
    public SafeDatabaseOperations(YourPlugin plugin) {
        this.plugin = plugin;
    }
    
    public CompletableFuture<List<Integer>> getCompletedDungeons(UUID playerUuid) {
        CompletableFuture<List<Integer>> future = new CompletableFuture<>();
        
        // Run database operation asynchronously
        plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
            List<Integer> dungeonIds = new ArrayList<>();
            
            try {
                Connection conn = plugin.getAIDungeon().getDatabaseManager().getConnection();
                
                // Important: Use try-with-resources to ensure connection is returned to pool
                try (conn;
                     PreparedStatement stmt = conn.prepareStatement(
                         "SELECT dungeon_id FROM player_progress " +
                         "WHERE player_uuid = ? AND exploration_percent = 100")) {
                    
                    stmt.setString(1, playerUuid.toString());
                    ResultSet rs = stmt.executeQuery();
                    
                    while (rs.next()) {
                        dungeonIds.add(rs.getInt("dungeon_id"));
                    }
                }
                
                future.complete(dungeonIds);
            } catch (SQLException e) {
                plugin.getLogger().severe("Database error: " + e.getMessage());
                future.completeExceptionally(e);
            }
        });
        
        return future;
    }
}

Conclusion

Integrating with AIDungeon allows you to extend the dungeon experience with custom features, rewards systems, progression mechanics, and more. By following the best practices in this guide, you can create robust integrations that enhance player enjoyment while maintaining server performance.

For additional assistance:

  • Check the API Reference for detailed information about available classes and methods
  • Review the Events documentation to understand all available event hooks
  • Refer to the Contributing guide if you want to contribute to the AIDungeon project itself

Contributing

Thank you for your interest in contributing to the AIDungeon plugin! This document provides guidelines for contributing to the project, including coding standards, submission process, and development workflows.

Getting Started

Prerequisites

To contribute to AIDungeon, you will need:

  • Java JDK 21 or higher
  • Git
  • Gradle 8.5+
  • IDE (IntelliJ IDEA recommended)
  • A testing Minecraft server (Paper/Spigot 1.21.5+)
  • Basic understanding of Bukkit/Spigot plugin development

Setting Up the Development Environment

  1. Fork the repository on GitHub (You will need to ask, to get access to the private repository)
  2. Clone your fork:
    git clone https://github.com/YOUR-USERNAME/aidungeon.git
    
  3. Configure upstream remote:
    git remote add upstream https://github.com/Ubivis/AIDungeon/aidungeon.git
    
  4. Create a branch for your changes:
    git checkout -b feature/your-feature-name
    
  5. Open the project in your IDE
  6. Build the project with Gradle:
    ./gradlew build
    

Coding Standards

AIDungeon follows these coding standards:

Code Style

  • Use 4 spaces for indentation (no tabs)
  • Maximum line length: 120 characters
  • Follow Java naming conventions:
    • camelCase for variables and methods
    • PascalCase for class names
    • UPPER_SNAKE_CASE for constants
  • Package names should be all lowercase
  • Use meaningful names that describe what the variable/method/class does

Documentation

  • All public classes and methods must have JavaDoc comments
  • Include @param, @return, and @throws tags where applicable
  • Explain non-obvious code with inline comments
  • Keep comments up-to-date with code changes

Example:

/**
 * Calculates the exploration percentage for a dungeon
 *
 * @param dungeonId The ID of the dungeon
 * @return Percentage of the dungeon that has been explored (0-100)
 * @throws IllegalArgumentException If the dungeon ID is invalid
 */
public double calculateExplorationPercentage(int dungeonId) {
    // Implementation
}

Performance Considerations

  • Avoid heavy operations on the main thread
  • Use async tasks for database operations
  • Cache data when appropriate
  • Consider memory usage when storing large objects

Best Practices

  • Follow SOLID principles:
    • Single Responsibility Principle
    • Open/Closed Principle
    • Liskov Substitution Principle
    • Interface Segregation Principle
    • Dependency Inversion Principle
  • Write unit tests for core functionality
  • Handle exceptions appropriately
  • Log errors and important information
  • Make your code thread-safe where needed
  • Use nullable annotations (@Nullable, @NotNull) for clarity

Submitting Changes

Pull Request Process

  1. Ensure you've tested your changes thoroughly
  2. Update documentation if you've changed behavior
  3. Update the README.md or wiki pages if necessary
  4. Push your changes to your fork:
    git push origin feature/your-feature-name
    
  5. Create a Pull Request from your branch to the main repository's develop branch
  6. Fill out the PR template with details about your changes
  7. Wait for code review and address any feedback

Code Review

All submissions require review before being merged. During code review:

  • Be responsive to feedback
  • Explain your design decisions
  • Be willing to make changes to improve your code
  • Thank reviewers for their time and input

Development Workflow

Branching Strategy

  • main: Production-ready code, stable releases
  • develop: Integration branch for features, target for PRs
  • feature/*: New features and non-emergency bug fixes
  • hotfix/*: Urgent fixes for production issues
  • release/*: Preparing for a new release

Testing Your Changes

  1. Build the plugin:
    ./gradlew build
    
  2. Copy the JAR from build/libs/AIDungeon-x.x.x.jar to your test server's plugins directory
  3. Test your changes in a realistic environment
  4. Verify there are no errors in the server console
  5. Run unit tests:
    ./gradlew test
    

Creating a New Feature

  1. Check existing issues and PRs to ensure your feature isn't already being worked on
  2. Create an issue describing your proposed feature
  3. Wait for feedback from maintainers
  4. Once approved, implement the feature
  5. Submit a PR following the guidelines above

API Changes

When making changes to the public API:

  1. Avoid breaking changes when possible
  2. If breaking changes are necessary:
    • Document the changes thoroughly
    • Update the version number appropriately
    • Provide migration guidance in the PR
  3. Add @since tags to new API elements
  4. Consider backwards compatibility

Versioning

AIDungeon follows Semantic Versioning (SemVer):

  • MAJOR version for incompatible API changes
  • MINOR version for backwards-compatible functionality additions
  • PATCH version for backwards-compatible bug fixes

Example: 0.2.00.3.0 for new features

Bug Reports and Feature Requests

Reporting Bugs

  1. Check existing issues to avoid duplicates
  2. Use the Bug Report template
  3. Include:
    • Description of the bug
    • Steps to reproduce
    • Expected behavior
    • Screenshots if applicable
    • Server information (version, plugins, etc.)
    • AIDungeon version
    • Logs or error messages

Requesting Features

  1. Check existing issues and PRs to avoid duplicates
  2. Use the Feature Request template
  3. Explain the feature clearly
  4. Describe how it would benefit users
  5. Outline expected behavior
  6. Suggest implementation approach if possible

Community

Communication Channels

  • GitHub Issues: For bug reports and feature discussions
  • Discord Server: For real-time communication
  • Wiki: For documentation and guides

Code of Conduct

  • Be respectful and inclusive
  • Provide constructive feedback
  • Help others learn and grow
  • Focus on the code, not the person
  • Follow the Golden Rule

License

By contributing to AIDungeon, you agree that your contributions will be licensed under the same license as the project (see LICENSE file).

Recognition

Contributors are recognized in:

  • The Contributors section of the README
  • Release notes for significant contributions
  • The @author tag in JavaDoc for significant code contributions

Additional Resources

Thank you for contributing to AIDungeon! Your efforts help make this plugin better for everyone.

⚠️ **GitHub.com Fallback** ⚠️