Developer Guide - Ubivis/AIDungeon-Wiki GitHub Wiki
Welcome to the AIDungeon developer documentation. This section contains information for developers who want to extend, integrate with, or contribute to the AIDungeon plugin.
- API Reference - Core classes and interfaces
- Events - Custom events for plugin integration
- Integration - How to integrate with AIDungeon
- Contributing - Guidelines for contributors
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:
- 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
- Dungeon: Represents a dungeon instance
- Room: Represents a room within a dungeon
- PlayerProgress: Tracks player exploration data
The generation process follows these steps:
- Location Selection: Finding suitable locations for dungeons
- API Integration: Calling Gemini API for dungeon design
- Structure Building: Converting the AI design into Minecraft structures
- Feature Population: Adding mobs, traps, and treasures
- Database Tracking: Recording dungeon data for persistence
To set up a development environment for AIDungeon:
- Java JDK 21+
- Gradle 8.5+
- IDE (IntelliJ IDEA recommended)
- Spigot BuildTools or Paper development environment
-
Clone the repository:
git clone https://github.com/UbivisMedia/aidungeon.git -
Navigate to the project directory:
cd aidungeon -
Build with Gradle:
./gradlew build
The built JAR file will be in the build/libs directory.
- Open IntelliJ IDEA
- Select "Open or Import"
- Navigate to the cloned repository
- Select the
build.gradlefile and open it as a project - Wait for the Gradle sync to complete
- Open Eclipse
- Select "File > Import"
- Choose "Gradle > Existing Gradle Project"
- Navigate to the cloned repository
- Complete the import wizard
For testing during development:
- Set up a local Spigot/Paper test server
- Configure the
build.gradleto copy the built JAR to your test server's plugins directory - Use a Gemini API key for development (with usage limits)
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
To extend AIDungeon's functionality without modifying the core code:
- Create a new plugin that depends on AIDungeon
- Access AIDungeon's API through
AIDungeon.getInstance() - Register listeners for AIDungeon's custom events
- 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]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>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);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();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
}
}When contributing to AIDungeon, please follow these documentation standards:
- JavaDoc: Add comprehensive JavaDoc comments to all public classes and methods
- Code Comments: Include inline comments for complex logic
- Method Naming: Use clear, descriptive method names
- 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
}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!
This document provides a detailed reference of the public API classes and methods available for developers who want to interact with the AIDungeon plugin.
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);
}
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);
}
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();
}
Handles database connections and operations.
public class DatabaseManager {
// Database setup
public boolean setupConnection();
public Connection getConnection() throws SQLException;
public void closeConnection();
public boolean createTables();
}
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<Room> 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();
}
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();
}
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();
}
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();
// ...
}
}
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 && room.isBossRoom();
}
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());
}
}
If you need to query the database directly, here's the schema structure:
| 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 |
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);
}
Manages communication with the Gemini API.
public class LightweightGeminiClient {
public CompletableFuture<String> generateContent(String prompt);
}
Handles the process of collapsing dungeons and replacing them with natural terrain.
public class DungeonCollapser {
public static Material getReplacementMaterial(World world, Vector pos);
}
Manages block palettes for different biomes and themes.
public class BlockPaletteManager {
public List<Material> getBlockPalette(String biomeType, String theme);
}
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();
}
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
}
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
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.
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.
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);
}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);
}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!");
}
}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;
}
}
}
}
}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);
}
}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);
}
}
}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);
}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());
}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);
}
}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);
}
}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);
}
}When registering event listeners for AIDungeon events, follow these best practices:
-
Specify event priorities: Use the
@EventHandler(priority = EventPriority.NORMAL)annotation to control the order of your event handlers. -
Handle exceptions: Wrap your event handling code in try-catch blocks to prevent your plugin from breaking AIDungeon functionality.
-
Check for nulls: Always validate event data before using it, as some events may have optional fields.
-
Respect cancellation: For cancellable events, check
event.isCancelled()before taking action, and respect the wishes of other plugins. -
Use async tasks for heavy processing: If your event handler needs to perform database operations or other heavy processing, use
Bukkit.getScheduler().runTaskAsynchronously().
AIDungeon events follow this general firing order:
-
Generation phase:
DungeonGeneratedEvent
-
Discovery phase:
DungeonDiscoveredEvent-
RoomDiscoveredEvent(as players find rooms)
-
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)
-
-
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.
This guide explains how to integrate your plugin with AIDungeon, including examples for common integration scenarios, best practices, and solutions to potential challenges.
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 missingIn 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;
}
}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);
}
}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);
}
}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);
}
}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!");
}
}
}
}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();
}
}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;
}
}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();
}
}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
}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());
}
}
}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
}
}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));
}
}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
}
}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
}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;
}
}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
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.
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
- Fork the repository on GitHub (You will need to ask, to get access to the private repository)
- Clone your fork:
git clone https://github.com/YOUR-USERNAME/aidungeon.git - Configure upstream remote:
git remote add upstream https://github.com/Ubivis/AIDungeon/aidungeon.git - Create a branch for your changes:
git checkout -b feature/your-feature-name - Open the project in your IDE
- Build the project with Gradle:
./gradlew build
AIDungeon follows these coding standards:
- Use 4 spaces for indentation (no tabs)
- Maximum line length: 120 characters
- Follow Java naming conventions:
-
camelCasefor variables and methods -
PascalCasefor class names -
UPPER_SNAKE_CASEfor constants
-
- Package names should be all lowercase
- Use meaningful names that describe what the variable/method/class does
- All public classes and methods must have JavaDoc comments
- Include
@param,@return, and@throwstags 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
}- Avoid heavy operations on the main thread
- Use async tasks for database operations
- Cache data when appropriate
- Consider memory usage when storing large objects
- 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
- Ensure you've tested your changes thoroughly
- Update documentation if you've changed behavior
- Update the README.md or wiki pages if necessary
- Push your changes to your fork:
git push origin feature/your-feature-name - Create a Pull Request from your branch to the main repository's
developbranch - Fill out the PR template with details about your changes
- Wait for code review and address any feedback
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
-
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
- Build the plugin:
./gradlew build - Copy the JAR from
build/libs/AIDungeon-x.x.x.jarto your test server's plugins directory - Test your changes in a realistic environment
- Verify there are no errors in the server console
- Run unit tests:
./gradlew test
- Check existing issues and PRs to ensure your feature isn't already being worked on
- Create an issue describing your proposed feature
- Wait for feedback from maintainers
- Once approved, implement the feature
- Submit a PR following the guidelines above
When making changes to the public API:
- Avoid breaking changes when possible
- If breaking changes are necessary:
- Document the changes thoroughly
- Update the version number appropriately
- Provide migration guidance in the PR
- Add
@sincetags to new API elements - Consider backwards compatibility
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.0 → 0.3.0 for new features
- Check existing issues to avoid duplicates
- Use the Bug Report template
- Include:
- Description of the bug
- Steps to reproduce
- Expected behavior
- Screenshots if applicable
- Server information (version, plugins, etc.)
- AIDungeon version
- Logs or error messages
- Check existing issues and PRs to avoid duplicates
- Use the Feature Request template
- Explain the feature clearly
- Describe how it would benefit users
- Outline expected behavior
- Suggest implementation approach if possible
- GitHub Issues: For bug reports and feature discussions
- Discord Server: For real-time communication
- Wiki: For documentation and guides
- Be respectful and inclusive
- Provide constructive feedback
- Help others learn and grow
- Focus on the code, not the person
- Follow the Golden Rule
By contributing to AIDungeon, you agree that your contributions will be licensed under the same license as the project (see LICENSE file).
Contributors are recognized in:
- The Contributors section of the README
- Release notes for significant contributions
- The
@authortag in JavaDoc for significant code contributions
- [GitHub Flow Guide](https://guides.github.com/introduction/flow/)
- [Spigot Plugin Development Guide](https://www.spigotmc.org/wiki/spigot-plugin-development/)
- [Effective Java by Joshua Bloch](https://www.oreilly.com/library/view/effective-java-3rd/9780134686097/)
Thank you for contributing to AIDungeon! Your efforts help make this plugin better for everyone.