Developer API
CoreJobs fires a cancellable Bukkit event before every gameplay payout. Another plugin listens, reads what CoreJobs is about to pay, and either multiplies it, replaces it, or cancels it. That is the supported way to give a rank, an armour set, a pet, or a crystal its own job bonus.
Older builds have no api package at all. If your plugin hooks by reflection, a ClassNotFoundException is your signal to tell the owner to update.
The three events
All three live in cc.arrowtan.corejobs.api.event, extend org.bukkit.event.Event, and implement org.bukkit.event.Cancellable.
| Event | Fires when | Cancelling means |
|---|---|---|
CoreJobsXpGainEvent |
Job xp is about to be banked | No xp and no level check. Money and farmpoints from the same action still pay |
CoreJobsMoneyGainEvent |
Job money is about to be deposited | No money. Xp and farmpoints still pay |
CoreJobsFarmpointsGainEvent |
Farmpoints from a drop are about to be banked | No balance, no ranking points, no action bar line |
What each one carries
Same shape on all three, only the value name changes.
| Method | Returns |
|---|---|
getPlayer() |
The player being paid |
getJobId() |
The job id as written in jobs.yml. all on a farmpoints drop no job claimed |
getBaseXp() / getBaseMoney() / getBaseAmount() |
The value before any multiplier, straight off the config |
getBoostMultiplier() |
The multiplier CoreJobs worked out from its own boosts. 1.0 when none runs |
getXp() / getMoney() / getAmount() |
What is about to be paid. Starts at base times the boost multiplier |
setXp() / setMoney() / setAmount() |
Replaces it. Negative values clamp to 0 |
isAdminGrant() |
Always false. Admin commands do not fire these events |
isCancelled() / setCancelled(boolean) |
Standard Bukkit cancel |
Whatever the getter reads when the event returns is what gets paid. For xp that means the level-up check, the {gained} placeholder and the {bar} fill on the action bar all read your number, not the one CoreJobs started with.
Where they fire
Every event fires after the CoreJobs boost was already applied, so a x1.25 set bonus on top of a running x2 boost pays x2.5, not x1.25.
Xp and money both fire in JobManager.pay(...), the one place every award path goes through, so block breaks, harvests, kills, fishing and enchanting are all covered:
xp *= xpBoost; // the CoreJobs BoostType.XP multiplier
// CoreJobsXpGainEvent fires here
xp = event.isCancelled() ? 0 : event.getXp();
// the xp is banked and the level check runs
Farmpoints fire in FarmpointsManager.earn(...), after the farmpoints boost and before the balance and the ranking points are written.
Rules the events follow
- Main thread, synchronous. Every award path already runs on the main thread, so reading worn armour, a held item or a permission inside the handler is safe.
- Nothing is fired for 0. A payout of 0 or less never fires, so a block that pays money but no xp only fires the money event.
- Admin paths are silent.
/corejobs xp,setxp,setlevel,addlevelsand the farmpointsgive,takeandsetfire nothing. Only gameplay does. - No re-entry. CoreJobs reads the value once and uses it once. It does not re-read its boosts or re-run the payout because a handler changed something.
- Names are a frozen contract. The package, the class names and every method above stay as they are. A rename means a minor version bump and a note in the changelog.
Hooking by reflection
The route to take if you do not want a compile-time dependency. Put CoreJobs under softdepend in your plugin.yml and register the class by name:
public void hookCoreJobs(Plugin plugin) {
Class<?> eventClass;
try {
eventClass = Class.forName("cc.arrowtan.corejobs.api.event.CoreJobsXpGainEvent");
} catch (ClassNotFoundException e) {
plugin.getLogger().warning("CoreJobs is too old for the xp bonus, update it to 1.4.0 or newer.");
return;
}
EventExecutor executor = (listener, event) -> {
double multiplier = multiplierFor((Player) event.getClass().getMethod("getPlayer").invoke(event));
if (multiplier == 1.0) {
return;
}
double xp = (double) event.getClass().getMethod("getXp").invoke(event);
event.getClass().getMethod("setXp", double.class).invoke(event, xp * multiplier);
};
Bukkit.getPluginManager().registerEvent(
eventClass.asSubclass(Event.class), this, EventPriority.NORMAL, executor, plugin, true);
}
Leave the value alone when your multiplier is 1.0. That way you never fight another plugin over a payout you were not going to change.
Hooking at compile time
CoreJobs is not on Maven Central and has no jitpack build, so add the jar as a system scope dependency or install it into your own local repository, then write an ordinary listener:
@EventHandler(priority = EventPriority.NORMAL)
public void onJobXp(CoreJobsXpGainEvent event) {
if (!wearsFullSet(event.getPlayer())) {
return;
}
event.setXp(event.getXp() * 1.25);
}
Keep the dependency provided so the classes are not shaded into your jar.
Things to know
With boosts.xp-also-money: true in config.yml the xp boost is folded into the money boost before the event fires, because that is what the payout actually used. getBoostMultiplier() reports the combined figure and there is no way to split it back apart.
- A cancelled xp gain that still paid money leaves the action bar reading
0gained. The player gets no in-game hint that another plugin cancelled it. - The
%corejobs_boost_<type>_multiplier%placeholders report CoreJobs boosts only, on purpose: a scoreboard reading one of those is asking about the boost that was bought. Register a provider, below, to have your bonus show up as well.
Showing your bonus in the menus
The events pay your bonus. They do not tell CoreJobs about it, so the boosts menu and the placeholders have nothing to show. cc.arrowtan.corejobs.api.ExternalMultiplierProvider is the other half: your plugin reports what it is giving a player right now, and CoreJobs prints it.
public interface ExternalMultiplierProvider {
double multiplier(Player player, BoostType type, String jobId);
String name();
}
Register it once, and drop it in your own onDisable:
CoreJobs core = (CoreJobs) Bukkit.getPluginManager().getPlugin("CoreJobs");
core.registerMultiplierProvider(this, myProvider);
core.unregisterMultiplierProvider(this);
One provider per plugin. Registering again replaces what that plugin had.
| Rule | Detail |
|---|---|
| Display only | Nothing a provider returns reaches a payout. The gain events do that, and counting it twice would double it |
| Main thread only | multiplier is never called off it, so worn armour, a held item or a permission are all safe to read |
| Called per key asked for | One player, one category, one job. jobId is a job id from jobs.yml, or all when the question is not about one job |
| Keep it cheap | It runs once per boosts.external-refresh-ticks for each key a placeholder or the menu asked for |
| A throw counts as 1.0 | The exception is logged with your plugin name and the other providers still answer |
| Frozen contract | The package, the interface name and both method names stay as they are |
What a player sees once a provider is registered:
%corejobs_external_<type>_multiplier%,_amount%and_source%, where the source is yourname().%corejobs_total_<type>_multiplier%and_amount%, the CoreJobs boost times every provider, which is what the player is really earning.%corejobs_total_<type>_source%, yourname()alongside the CoreJobs boost when both are above 1.0. Point admins at this one for a scoreboard:external_..._sourceon its own prints the no-source wording next to a running CoreJobs boost, which reads as no boost at all.- A line in the boosts menu per category you are boosting, from
boosts-menu.externalinmenus.yml.
A key nobody has asked for yet reads 1.0 off the main thread and is filled in on the next refresh, so a scoreboard can be one refresh behind right after a join. The payout is right from the first block either way.