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.

Available from CoreJobs 1.4.0

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

You multiply on top of the boosts, not underneath them

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, addlevels and the farmpoints give, take and set fire 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

The money multiplier reads as one number

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 0 gained. 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 your name().
  • %corejobs_total_<type>_multiplier% and _amount%, the CoreJobs boost times every provider, which is what the player is really earning.
  • %corejobs_total_<type>_source%, your name() alongside the CoreJobs boost when both are above 1.0. Point admins at this one for a scoreboard: external_..._source on 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.external in menus.yml.
The first read after a login shows nothing

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.