A core, platform-agnostic framework for making Bukkit plugins and Hytale mods.
Write commands, configs, messages and player data once, against an API that has no idea which server it is running on. The same command class compiles and runs on Bukkit/Spigot/Paper and on Hytale; the same player data can live in flat files, MySQL, PostgreSQL, H2 or MongoDB, chosen by the server owner in a config file rather than by you in code.
This page is a tour. The full developer guide - every API, every contract, every trap - is the EverNifeCore Wiki.
| Start here | |
|---|---|
| Installation | the repository and the one compileOnly dependency |
| Quick Start | a complete minimal plugin: command, config, localized message |
| Architecture Overview | common with zero Bukkit imports, the two platforms, the startup sequence |
| Gotchas & Pitfalls | the behavioral traps, before you hit them |
- Download the latest JAR from Releases
- Drop it in
plugins/ - Start the server (do not reload it)
repositories {
maven { url = 'https://maven.petrus.dev/public' }
}
dependencies {
// Bukkit/Spigot/Paper - the fat jar already carries the common API.
compileOnly 'br.com.finalcraft:evernifecore-minecraft:3.0.1'
}Hytale, the thin common artifact and the relocated view are on Installation.
A command is a plain class. Parameters are injected and parsed: <arg> is required, [arg] is
optional, and @Arg.Flag binds a --name value flag that may appear anywhere on the line.
@FinalCMD(
aliases = {"greet", "hi"},
permission = "myplugin.greet"
)
public class GreetCommand {
@FinalCMD.SubCMD(
subcmd = "bonus",
permission = "myplugin.greet.bonus"
)
public void setBonus(FCommandSender sender,
@Arg("<player>") FPlayer target,
@Arg("<amount>") Integer amount,
@Arg.Flag(value = "--silent", aliases = "-s", def = "false") Boolean silent) {
// parsed, permission-checked, tab-completed and help-generated for you
}
}
FinalCMDManager.registerCommand(ecPluginData, GreetCommand.class);Registration builds a command tree: nodes are walked and dispatched in ordered phases, permission and validation are evaluated over the whole path, and tab-completion is answered from the tree.
馃摉 Command Framework 路 Argument Parsing 路 Flags
YAML (or TOML/JSON) with inline comments generated from your code - you never ship a bundled default
file. getOrSetValueIfAbsent reads the key, or seeds it with its comment the first time.
Config config = ConfigFactory.open(ecPluginData, "config.yml");
int joinBonus = config.getOrSetValueIfAbsent(
"settings.join-bonus",
100,
"Bonus shown to a player when they run /greet me."
);Teaching the config a new type is one registration, and it then works everywhere - solo value, POJO field, map value, list element:
ConfigFactory.register(MyData.class).asMap(MyData::toMap, MyData::fromMap); // object-shaped
ConfigFactory.register(MyId.class).asString(MyId::toString, MyId::parse); // scalar, usable as a keyIf your type is already an POJO there is no need to even register it at all. Use normal Jackson Annotations to ignore some fields and that's it.
A PDSection is a plain Jackson POJO. Mutate the fields, call markDirty(), and the flush pipeline
persists it. That is the whole persistence API.
public class JobsSection extends PDSection {
public int level;
public String job = "none";
public void levelUp() {
level++;
markDirty();
}
}
// once, at enable - the id is the stable storage identity, so the class can be renamed freely
PlayerController.registerPDSectionCfg(ecPluginData, JobsSection.class, "jobs");
// reads are 100% async; on a cached online player the future is already completed
PlayerController.getPDSection(uuid, JobsSection.class).thenAccept(JobsSection::levelUp);馃摉 PlayerData & PDSections 路 Accounts 路 Cooldowns
Where that data physically lives is the admin's decision, in storage.yml. The developer advises
a default; nobody has to recompile anything to move a collection to MySQL.
storage-backends:
playerdata: # what belongs to ONE player on THIS server
enabled: true
type: groupedfile
path: plugins/EverNifeCore/StorageData/PlayerData
networkdata: # what the whole network must agree on
enabled: true
type: groupedfile
path: plugins/EverNifeCore/StorageData/NetworkData
mysql:
enabled: false
type: sql
url: "jdbc:mysql://localhost:3306/minecraft"Backends are named by role, not by technology: a single server is a network of one, and when a
second server joins you point networkdata at a shared database and nothing else changes. Backends
supported: grouped files, local files, MySQL/MariaDB, PostgreSQL, H2, MongoDB, in-memory. Moving live
data between them is a runtime command (/ecstorage transfer), not a migration script.
馃摉 Storage Backends 路 Inline Backends for Plugins 路 Legacy Data Migration
Every message declares its translations inline and is synced to an editable language file per plugin,
so a server owner rewords it without touching your jar. ${key} placeholders are declared bare and
cited with their delimiters.
@FCLocale(lang = LocaleType.EN_US, text = "&aWelcome, ${player}&a! Your bonus is &6${bonus}&a.")
@FCLocale(lang = LocaleType.PT_BR, text = "&aBem-vindo, ${player}&a! Seu b么nus 茅 &6${bonus}&a.")
public static LocaleMessage WELCOME;
WELCOME.addPlaceholder("player", player.getName())
.addPlaceholder("bonus", joinBonus)
.send(player);FancyText is the rich-text interface: FancySegment is one styled piece, FancyFormatter an
ordered chain of pieces.
FancyText.of("&aClick here to teleport!")
.setHover("&7Teleports you to spawn")
.setClickCommand("/spawn")
.send(player);
FancyText.of("&6Legendary Sword")
.setHoverItem("minecraft:diamond_sword")
.send(player);馃摉 Localization 路 FancyText 路 Placeholders
ItemStack reward = FCItemFactory.from(Material.DIAMOND_SWORD)
.displayName("&bExcalibur") // color codes translated for you
.lore("&7A legendary blade", "&7+10 damage")
.addEnchant(Enchantment.DAMAGE_ALL, 5)
.setGlow()
.setUnbreakable()
.build();
builder.asGuiItem(); // straight into the GUI layer
builder.asLayout(); // a LayoutIcon for the config-driven layout systemThe builder clones its source, so the original stack is never mutated, and it falls back to raw NBT on legacy versions where the modern API does not exist.
馃摉 Items & NBT 路 GUI Framework
common has zero Bukkit imports. Platform behaviour reaches it two ways: runtime providers
registered by the platform entry point, and compile-time stubs that each platform replaces with a real
class of the same fully-qualified name. Your code takes FPlayer and FCommandSender and never asks
which server it is on.
| Platform | Module | Deployable artifact |
|---|---|---|
| Bukkit / Spigot / Paper (1.7.10 - 1.21) | minecraft |
minecraft:shadowJar |
| Hytale | hytale |
hytale:shadowJar (classifier Hytale) |
One JDK 25 toolchain compiles everything; the Bukkit side emits Java 8 bytecode against a Java 8 API floor, so one jar covers 1.7.10 through 1.21.
馃摉 Platform Abstraction 路 Hytale Platform 路 Version Compatibility 路 Java Versions & Toolchains
Sync and async scheduling on virtual threads where the runtime has them, per-server and per-player
cooldowns that can be network-wide, accounts that link a player's data across alt logins, paginated
chat views, and a headless test engine published as evernifecore-common-tests so a plugin can test
against platform doubles instead of a running server.
馃摉 Scheduler & Threading 路 Cooldowns 路 Accounts 路 full wiki index
Each one is optional and degrades quietly when its plugin is absent.
| Integration | What you get |
|---|---|
| Vault / VaultUnlocked | one economy API over whatever the server runs - Economy |
| PlaceholderAPI | resolve placeholders and register your own namespace |
| LuckPerms | read a player's meta values |
| WorldEdit | schematic pasting |
| WorldGuard / GriefDefender / GriefPreventionPlus | one canBuild/canBreak/canInteract answer over every installed protection plugin |
| BossShopPro | shop items carrying EverNifeCore NBT data parts |
馃摉 Integrations
- EveryDatabase - the storage engine: entities, codecs, caching, references, backend drivers.
- EveryConfig - the Jackson-first YAML layer: typed reads/writes, inline comments, async saves.
- EveryLibs - reflection, executor and collection helpers.
All three are api dependencies of common, so their types are part of its public API.
Building the whole project needs a few jars that cannot be fetched from a
public repository (paid or unobtainable plugins used by the integration modules); common,
api-contracts and libby build without them.
馃摉 Building from Source 路 Project Layout 路 CHANGELOG
- Discord: Join our community
- SpigotMC: Plugin page
- Issues: GitHub Issues
Developed with 鉂わ笍 by EverNife
Empowering Minecraft plugin development since 2016