This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Build the entire project:
gradle buildThis compiles all modules (API, NMS versions, Hooks, Missions) and creates fat JARs with shadow JAR plugin. Artifacts are placed in /target.
Build API only:
gradle :API:buildBuild without debug checks:
gradle build -x checkDebugClean build artifacts:
gradle cleanNote: The gradlew wrapper (gradle-wrapper.properties) specifies Gradle 9.4.1. The project uses Java 8 as baseline, with support up to Java 21 for newer NMS versions.
Debug check (automatic on build):
The checkDebug task scans for Bukkit.broadcastMessage debug statements and fails the build if found. This is part of the standard build process. To bypass (not recommended): gradle build -x checkDebug.
No automated linting: The project has no static analysis configured (no spotbugs, checkstyle, etc.). Code review and manual inspection are the primary quality gates.
No test suite: This codebase has no automated tests. Validation is manual and would require a running Spigot/Paper server.
SuperiorSkyblock2 is a Bukkit/Spigot/Paper plugin providing a complete Skyblock gamemode implementation. The codebase uses a Manager pattern with a clean API/implementation separation.
SuperiorSkyblockPlugin (extends JavaPlugin, implements SuperiorSkyblock interface) initializes and orchestrates all systems:
- Loads configuration, database, NMS handlers
- Initializes managers for grids, players, islands, roles, upgrades, missions
- Registers event listeners and commands
- Bootstraps the module system and services
All major systems extend Manager (abstract base with loadData() contract):
- GridManager: Island creation, placement, and global island tracking. Uses
IslandBuilderImpl(builder pattern) to construct islands with all dependencies. - PlayersManager: Maps Bukkit Players to
SuperiorPlayerwrappers. Handles offline players and NPC support via metadata checking. - RolesManager: Defines island member roles and their permission hierarchies.
- UpgradesManager: Manages upgradeable island properties (size, team limits, bank limits, etc.) with level-based costing.
- MissionsManager: Tracks island missions and mission holders. Integrated with mission modules.
- SchematicManager: Loads island schematics from YAML files for initial island generation.
- BlockValuesManager: Maintains block worth calculations for island level scoring.
- ModulesManager: Dynamically loads and manages plugin modules (builtin: Generators, Missions, Bank, Upgrades; external: custom JAR modules).
Each manager owns a Container (IslandsContainer, PlayersContainer, etc.) that stores and retrieves cached objects, with database bridges handling persistence.
Island objects represent player islands and are the central domain model:
- Island hierarchy: Owner, members (with roles and privileges), co-op players
- Island properties: Upgrades, flags, warps, bank (money storage), settings, ratings
- Island data: Block counts (cached), world locations, schematics, permissions
- Island operations: Invite/kick, promote/demote, disband, recalculate worth
Related packages:
island.builder:IslandBuilderImpl- fluent island construction from database/scratchisland.privilege: Permission nodes and role-based privilege checkingisland.flag: Toggleable island behavior flagsisland.upgrade: Upgrade leveling system with costs and effectsisland.cache: Caches block counts, levels, worth to avoid recalculationisland.chunk: Island chunk ownership trackingisland.warp: Island warp managementisland.bank: Island money storage with transactions
SuperiorPlayer wrappers (subclass SSuperiorPlayer) wrap Bukkit Players and offline data:
- Permission checks for island actions
- Personal statistics and missions
- Island membership state
- Chat and respawn preferences
Players use builders (SuperiorPlayerBuilderImpl) for construction. NPC players handled separately via SuperiorNPCPlayer for compatibility with NPC plugins.
DatabaseBridge interface abstracts persistence:
- Modes: Read-only, read-write, write-only (specified per database instance)
- Operations:
insertObject(),updateObject(),deleteObject(),getObject() - Bridges: Dedicated bridge classes (GridDatabaseBridge, IslandsDatabaseBridge, PlayersDatabaseBridge) handle specific entity persistence
- SQL implementation:
SQLDatabaseBridgebacked by HikariCP connection pooling
ObjectsPools.DB_COLUMN_BATCH provides pooled reuse of DBColumn objects for allocation efficiency (see Object Pooling under Coding Conventions).
Supports 18 Minecraft versions (v1_8_R3 through v26_2, including Paper variants):
- Strategy: Each version gets its own Gradle subproject under
/NMS/v*/ - Code generation: Templates in
/NMS/src/main/templates/are processed with version-specific properties to generate implementation classes (AbstractNMSAlgorithms, AbstractNMSChunks, etc.) - Version registration: NMSLoader (external common library) dynamically selects correct NMS handler at runtime based on server version
- NMS facades (in
/src/main/java/com/bgsoftware/superiorskyblock/nms/):- NMSAlgorithms, NMSChunks, NMSEntities, NMSWorld, NMSPlayers, NMSTags, NMSHolograms, NMSDragonFight
- These route to version-specific implementations and hide Minecraft internals
Version-specific modules use PaperWeight (for 1.17+) or Spigot mappings for compilation. Conditional compilation controlled via gradle.properties (e.g., nms.compile_v1_21=true).
Shared NMS code is written once as .template files and stamped into each version's source tree at build time, avoiding copy-paste across 18+ versions.
How it works:
- Templates (
NMS/src/main/templates/**/*.template): Java source files with${KEY}placeholders for anything that differs between Minecraft versions — package names, class paths, method signatures, etc. - Properties files (
NMS/<version>/properties): Each NMS subproject contains a plain-textpropertiesfile with key=value pairs that fill in the placeholders for that version. generateNmsSourcestask (NMS/build.gradle): Defined on every NMS subproject. At build time it:- Reads the subproject's
propertiesfile - Iterates all
*.templatefiles underNMS/src/main/templates/ - Replaces every
${KEY}occurrence with the matching value - Writes the result to
$buildDir/generated/sources/nms/(stripping the.templateextension, preserving directory structure)
- Reads the subproject's
- Source set wiring: Generated sources are added to the
mainJava source set alongside hand-written sources insrc/main/java/.compileJavadepends ongenerateNmsSources, so generation always runs first.
Common template variables (defined per version in properties):
NMS_VERSION— version identifier used in package names (e.g.v1_21_7)CRAFTBUKKIT_PACKAGE— CraftBukkit package, versioned pre-1.20.5, unversioned afterCOMMON_NMS_PACKAGE— base NMS package shared by a version family (e.g.v1_20_3code reused byv1_21_7)REGISTRY_CLASS,CHUNK_STATUS_CLASS,SERVER_LEVEL_RANDOM_TYPE— NMS class paths that moved between versionsABSTRACT_MINECART_SUBPACKAGE,STAR_LIGHT_INTERFACE_CLASS, etc. — other version-specific class references
When to edit:
- To fix a bug that affects all NMS versions: edit the
.templatefile. - To fix a bug that is version-specific: edit the hand-written source in that subproject's
src/main/java/, and if it should propagate, add a new placeholder + property entry. - To add a new NMS version: create a new subproject directory with a
propertiesfile, abuild.gradle, and asrc/main/java/for any hand-written overrides.
40+ optional plugin hooks in /Hooks use a provider pattern:
- Each hook detects the target plugin's presence at runtime
- Implements a provider interface (e.g., EconomyProvider for Vault)
- ProvidersManager dynamically loads compatible providers
- Seamless fallback if plugin is absent
Examples: PlaceholderAPI, Vault economy, LuckPerms permissions, WorldEdit, Slimefun, various spawner plugins.
Builtin modules (loaded automatically):
- Generators: Custom block generators (e.g., cobblestone with custom rates)
- Missions: Repeatable island challenges with rewards
- Bank: Island-wide money storage separate from player accounts
- Upgrades: Island stat upgrades with leveling
External modules can be placed in /plugins/SuperiorSkyblock2/modules/ as JAR files. ModulesManager dynamically loads and registers them via reflection.
ServicesHandler registers and retrieves internal services that other components depend on:
- PlaceholdersService, HologramsService, DragonBattleService, BossBarsService
- MessagesService, PortalsService, RegionManagerService, StackedBlocksService, WorldRecordService
Services implement IService interface and are registered with Bukkit's ServiceManager (ServicePriority.Normal).
Custom plugin events (in core.events.plugin.PluginEventsFactory) abstract Bukkit/Paper events:
- PluginEventsDispatcher fires domain events (IslandJoinEvent, IslandUpgradeEvent, etc.)
- EventCallbacks allow plugins to hook into lifecycle events
- Platform abstraction: GameEventsDispatcher bridges to Bukkit events
- Event args passed through PluginEventArgs utility
- Manager: Base Manager + concrete implementations (GridManagerImpl, PlayersManagerImpl, etc.)
- Builder: IslandBuilder, PlayerBuilder, SchematicBuilder, ItemBuilder for complex object construction
- Factory: DefaultIslandsFactory, DefaultPlayersFactory, etc. for object creation
- Strategy: IslandCreationAlgorithm implementations for pluggable generation logic
- Bridge: DatabaseBridge abstraction layer for persistence
- Template Method: NMS abstract classes with template generation for version-specific code
- Container: IslandsContainer, PlayersContainer for data storage and retrieval
- Service Locator: ServicesHandler for central service registry
- Observer: Custom event system with callback registrations
- Adapter: Provider wrappers for external plugin integration (PlaceholderAPI, Vault, etc.)
Many code paths here run per-tick or per-event (move, block place/break, chunk load), so short-lived allocations add up. ObjectsPool<T> (core/ObjectsPool.java) is a thread-safe free-list of reusable objects, and ObjectsPools (core/ObjectsPools.java) holds the shared pools:
ObjectsPools.LOCATION— reusable BukkitLocationsObjectsPools.LAZY_LOCATION— reusableLazyWorldLocationsObjectsPools.DB_COLUMN_BATCH— a batch ofDBColumns for a single database operation
Locations are the main case. Bukkit's no-arg getLocation() on Player, Entity and Block allocates a new Location on every call. Use the overload that fills an existing one (getLocation(Location)), and get that Location from the pool. The plugin's own types expose the same overload, e.g. SuperiorPlayer#getLocation(Location) and IslandWarp#getLocation(Location). Always use try-with-resources so the object goes back to the pool:
Island island;
try (ObjectsPools.Wrapper<Location> wrapper = ObjectsPools.LOCATION.obtain()) {
island = plugin.getGrid().getIslandAt(player.getLocation(wrapper.getHandle()));
}Database operations follow the same pattern with a batch:
try (ObjectsPools.Batch<DBColumn> pool = ObjectsPools.DB_COLUMN_BATCH.obtain()) {
databaseBridge.insertObject("players_missions",
pool.obtain().withNameAndValue("player", uuid.toString()),
pool.obtain().withNameAndValue("name", missionName));
}Rules:
- A pooled object must not escape its try block. Don't store it in a field or collection, return it, hand it to an async/scheduled task, or keep a reference after the block. Once it's released, another caller can obtain and overwrite it. Pull out what you need (the island, block coords, a world) inside the block. If you need to keep the
Location,clone()it first. - Never release a pooled object twice, and never use it after
close(). - Some classes pool themselves: they implement
ObjectsPool.Releasable+AutoCloseableand have a private staticPOOL(ChunkPosition,ButtonClickContextImpl,WorldGeneratorOptionsImpl,SuperiorNPCPlayer).ChunkPosition.of(...)returns a pooled instance by default. Use it in try-with-resources, and callcopy()(or thefromPool = falseoverload) when the position will be stored, e.g. as a map key. - To add a pool for a new wrapped type, use
ObjectsPools.createNewPool(() -> new Foo(...)), asNMSUtils.java.templatedoes forBlockPos.MutableBlockPos.
Pick the list type by how it will be accessed:
LinkedList: lists that are only appended to and then iterated (or used as a queue): tab-completion results, collected search results, batches of objects to process, temporary aliases. This is the default in the codebase.ArrayList: lists that need random access by index (get(i),set(i, ...), binary search, sorting then indexing), such as sorted top-islands lists, menu page/slot lookups and pattern rows. Also use it when the final size is known up front, so it can be pre-sized:new ArrayList<>(size).
Never call get(i) in a loop on a LinkedList. Each call is O(n), so the loop becomes O(n²). If a list needs both cheap appends and indexed reads, use ArrayList.
build.gradle orchestrates subprojects:
- Configures shadow JAR relocation (BStats, common libraries)
- Defines collectArtifacts task (final JAR placement to
/target) - Includes subproject builds in shadowJar (NMS modules embedded in nms/ subdirectory, mission modules in modules/missions/)
Published separately as com.bgsoftware:SuperiorSkyblockAPI to Maven repo (https://repo.bg-software.com/repository/api/).
291 Java files defining:
- Core interfaces: SuperiorSkyblock, Island, SuperiorPlayer, GridManager, etc.
- Event types: 30+ plugin-specific events extending Bukkit events
- Data layer: DatabaseBridge, DatabaseFilter abstractions
- Handlers: Interface contracts for all managers
- Enums and domain objects
Backwards compatibility is mandatory. Third-party plugins depend on this API, and breaking changes silently break those plugins at runtime. Rules:
- Never remove or rename an existing API method, class, or enum constant.
- Never change a method signature (parameter types, parameter count, return type).
- Never change observable behaviour of an existing API method — if the semantics must change, add a new method instead.
- Extend, don't replace: add new methods alongside the old ones. Have the old method delegate to the new one so existing callers continue to work unchanged.
@Deprecatedis not removal — mark old methods@Deprecatedwhen a better alternative exists, but keep them functional and delegating.- Adding methods to interfaces that have a
Delegate*variant is allowed. Third-party plugins don't implement these interfaces directly. They extend the matchingDelegate*class, which wraps ahandleand forwards every call to it (Island→DelegateIsland,SuperiorPlayer→DelegateSuperiorPlayer,IslandBank→DelegateIslandBank,IslandWarp→DelegateIslandWarp,IslandsFactory→DelegateIslandsFactory, the algorithm and container interfaces, etc.). When you add a method to one of these interfaces, you must also add a forwarding override to itsDelegate*class (return this.handle.newMethod(...);) and implement it in the plugin's concrete classes. For interfaces without aDelegate*variant (e.g. thehooks/*Providerinterfaces), a new method must be adefaultmethod so existing third-party implementations still load.
Example pattern when behaviour needs to change:
// Old method — keep it, delegate to new
@Deprecated
Island getIsland(UUID uuid);
// New method with improved/extended behaviour
Island getIsland(UUID uuid, boolean includeArchived);Prefer Enumerable classes over Java enum for extensible constants. Java enums are sealed — third-party plugins cannot add new values. When a concept needs to be extendable through the API (new privileges, new flags, new types), use the Enumerable pattern instead.
The pattern (IslandPrivilege, IslandFlag, etc.):
- The class implements
com.bgsoftware.superiorskyblock.api.objects.Enumerable(providesordinal()for efficient array-based lookups). - A private static
Map<String, T>holds all registered instances; a privatestatic int ordinalCounterassigns sequential ordinals. - The constructor is private — callers cannot instantiate directly.
- A public static
register(String name, ...)method creates and stores a new instance, enforcing uniqueness. - Public static
values()andgetByName(String)mirror the standard enum API.
public class IslandPrivilege implements Enumerable {
private static final Map<String, IslandPrivilege> registry = new HashMap<>();
private static int ordinalCounter = 0;
private final String name;
private final int ordinal;
private IslandPrivilege(String name) {
this.name = name.toUpperCase(Locale.ENGLISH);
this.ordinal = ordinalCounter++;
}
@Override public int ordinal() { return ordinal; }
public String getName() { return name; }
public static void register(String name) {
name = name.toUpperCase(Locale.ENGLISH);
Preconditions.checkState(!registry.containsKey(name), "Already registered: " + name);
registry.put(name, new IslandPrivilege(name));
}
public static IslandPrivilege getByName(String name) { /* lookup */ return registry.get(name.toUpperCase(Locale.ENGLISH)); }
public static Collection<IslandPrivilege> values() { return registry.values(); }
}Use this pattern (not enum) whenever you add a new extensible constant type to the API.
Use EnumerateMap and EnumerateSet for collections keyed by Enumerable types. Both classes live in core.collections and replace HashMap/HashSet when the key is an Enumerable. They back storage with a plain array indexed by ordinal(), giving O(1) get/contains with no hashing overhead.
EnumerateMap<K extends Enumerable, V>— array-backed map;get(key)readsvalues[key.ordinal()]directly.EnumerateSet<K extends Enumerable>— array-backed set backed byboolean[];contains(key)readsvalues[key.ordinal()]directly.- Both auto-expand their backing array when a newly registered
Enumerablehas a higher ordinal than the current capacity. - Construct with the full
Collection<K>of known instances so the array is pre-sized correctly:new EnumerateMap<>(IslandPrivilege.values()).
Use these instead of HashMap/HashSet anywhere the key is an Enumerable (privileges, flags, upgrades, dimensions, etc.).
798 Java files under /src/main/java/com/bgsoftware/superiorskyblock/:
Key packages:
config/: SettingsManagerImpl, configuration loaders, section-based config structurecore/: Manager base, utilities (ObjectsPools, LazyReference, Counter, etc.), error handlingdatabase/: SQL bridge implementation, DBColumn, result mappingisland/: Island implementation, builders, privilege system, upgrades, warps, cacheplayer/: Player wrappers, permissions, respawn logic, inventory managementcommands/: Command system with admin/player command hierarchylistener/: Event listeners for Bukkit eventsexternal/: Provider implementations for external pluginsservice/: Internal service implementationsmodule/: Module system and builtin module implementationsworld/: World generation, chunk management, schematicsnms/: NMS facades routing to version-specific implementations
- gradle.properties: Controls which NMS and hook modules compile. Set to
falseto skip expensive version compilations (e.g.,nms.compile_v1_21=false). - plugin.yml: Contains 50+ permission nodes for commands; API version 1.13+; soft dependencies on 25+ common plugins
- Database configuration: SQLite by default, but supports any JDBC database via config
- Block values: Loaded from YAML, updatable at runtime
- Schematics: Binary format stored in
/src/main/resources/schematics/
- Current version: 2026.1 (active development)
- License: GNU GPL v3.0
- Author: Ome_R (BG Software)
- Project type: Bukkit/Spigot/Paper plugin (Java)
- Target audience: Skyblock server administrators and developers
The plugin is designed as an API-first plugin. Other plugins should depend on SuperiorSkyblockAPI (not the implementation JAR) to avoid breaking changes. API methods are stable and documented; implementation changes occur without API modifications.
Access the plugin instance via: SuperiorSkyblockAPI.getSuperiorSkyblock() after checking plugin availability.