> For the complete documentation index, see [llms.txt](https://docs.mongenscave.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.mongenscave.com/premium-products/quests/developer-api.md).

# Developer API

The plugin exposes a small API for other plugins: you can add your own objective triggers, reward types, register quests from code, listen to leveling events and modify per-player limits. This page assumes you're comfortable writing a Bukkit plugin.

### Setup <a href="#setup" id="setup"></a>

<div align="left"><figure><img src="https://img.shields.io/badge/dynamic/xml?url=https%3A%2F%2Frepo.mongenscave.com%2Freleases%2Fcom%2Fmongenscave%2Fmc-QuestAPII%2Fmaven-metadata.xml&#x26;query=%2Fmetadata%2Fversioning%2Flatest&#x26;style=for-the-badge&#x26;logoColor=%23F7556B&#x26;label=mc-QuestAPI" alt=""><figcaption></figcaption></figure></div>

{% tabs %}
{% tab title="Maven" %}

```xml
<repository>
  <id>MonGens-Cave</id>
  <url>https://repo.mongenscave.com/releases<repository></url>
</repository>

<dependency>
  <groupId>com.mongenscave</groupId>
  <artifactId>mc-QuestAPI</artifactId>
  <version>[VERSION]</version>
</dependency>
```

{% endtab %}

{% tab title="Gradle" %}

```groovy
maven { url "https://repo.mongenscave.com/releases" }

implementation "com.mongenscave:mc-QuestAPI:[VERSION]"
```

{% endtab %}
{% endtabs %}

And depend on the plugin in your `plugin.yml` so it loads first:

```yaml
depend: [mc-Quests]
```

The entry point is a static accessor, available from `onLoad()` onward:

```java
QuestAPI api = McQuests.getApi();
```

### The API at a glance <a href="#the-api-at-a-glance" id="the-api-at-a-glance"></a>

```java
public interface IQuestAPI {
    void registerObjective(ObjectiveHandler handler);
    void registerCondition(ConditionHandler handler);
    void registerReward(RewardHandler handler);
    void registerQuest(QuestDefinition quest);
    void giveQuest(Player player, String questId);
    void trigger(Player player, ObjectiveTrigger trigger, Object context);
}
```

### Custom objective triggers <a href="#custom-objective-triggers" id="custom-objective-triggers"></a>

This is the most useful extension point: it lets quest creators use *your* plugin's actions in `quests.yml`, just like the built-in triggers.

A trigger has two halves — an `ObjectiveHandler` that knows how to match and count an action, and a call to `api.trigger(...)` that fires when the action happens.

#### 1. Implement the handler <a href="#id-1-implement-the-handler" id="id-1-implement-the-handler"></a>

```java
public final class VoteObjective implements ObjectiveHandler {

    public static final ObjectiveTrigger VOTE = new ObjectiveTrigger("vote");

    @Override
    public ObjectiveTrigger trigger() {
        return VOTE;
    }

    @Override
    public boolean matches(String target, Object context) {
        if (!(context instanceof VoteContext vote)) return false;
        if (target == null || target.equalsIgnoreCase("any")) return true;

        return vote.serviceName().equalsIgnoreCase(target);
    }

    @Override
    public int progress(Object context) {
        return 1;
    }

    public record VoteContext(String serviceName) {}
}
```

* **`trigger()`** — the trigger's ID. It's what quest creators write in the `trigger:` field (IDs are lowercased automatically).
* **`matches(target, context)`** — decides whether this event counts for an objective with the given `target`. The `context` is whatever object you pass to `api.trigger(...)`; a small record is the usual pattern.
* **`progress(context)`** — how much progress the event is worth. Return `1` for count-based objectives, or an amount (damage, money, items) for total-based ones.

#### 2. Register it and fire it <a href="#id-2-register-it-and-fire-it" id="id-2-register-it-and-fire-it"></a>

```java
@Override
public void onEnable() {
    McQuests.getApi().registerObjective(new VoteObjective());
}

@EventHandler
public void onVote(VotifierEvent event) {
    Player player = Bukkit.getPlayerExact(event.getVote().getUsername());
    if (player == null) return;

    McQuests.getApi().trigger(player, VoteObjective.VOTE,
            new VoteObjective.VoteContext(event.getVote().getServiceName()));
}
```

#### 3. Use it in quests.yml <a href="#id-3-use-it-in-questsyml" id="id-3-use-it-in-questsyml"></a>

```yaml
objectives:
  voter:
    name: "&#B8FFF2&lSupporter"
    trigger: vote
    target: any
    required: 3
```

#### How a trigger call flows <a href="#how-a-trigger-call-flows" id="how-a-trigger-call-flows"></a>

`api.trigger(player, trigger, context)` only affects the given player, and only their **active** quests. For each active quest, every objective whose `trigger:` matches the trigger ID is tested with `matches(...)`; on a match, `progress(...)` is added (capped at `required`). Progress messages, objective/quest completion and rewards are all handled for you.

### Custom rewards <a href="#custom-rewards" id="custom-rewards"></a>

A `RewardHandler` adds a new `type:` for the `rewards:` section:

```java
public final class LootboxReward implements RewardHandler {

    @Override
    public String type() {
        return "lootbox";
    }

    @Override
    public void give(Player player, String value) {
        MyLootboxPlugin.give(player, value);
    }
}
```

```java
McQuests.getApi().registerReward(new LootboxReward());
```

```yaml
rewards:
  - type: lootbox
    display-name: "&#FFD86E1x Rare Lootbox"
    value: rare
```

The `value` arrives as the raw string from the config — parsing (amounts, `:` separators) is up to you.

### Custom conditions <a href="#custom-conditions" id="custom-conditions"></a>

The `ConditionHandler` interface mirrors rewards — `type()` plus a `check(player, value)` that returns whether the player may accept the quest:

```java
public final class TownyCondition implements ConditionHandler {

    @Override
    public String type() {
        return "town";
    }

    @Override
    public boolean check(Player player, String value) {
        return TownyAPI.getInstance().getTown(value).hasResident(player.getName());
    }
}
```

{% hint style="warning" %}
Condition registration is currently a stub: quest-accept checks only evaluate the built-in `permission` and `world` conditions, so handlers registered through `registerCondition(...)` are not consulted yet. Register them for forward compatibility, but don't rely on them gating quests today.
{% endhint %}

### Registering quests from code <a href="#registering-quests-from-code" id="registering-quests-from-code"></a>

`registerQuest(...)` adds a quest built with `QuestDefinition.builder()` — the same model the YAML loader produces:

```java
QuestDefinition quest = QuestDefinition.builder()
        .id("event_boss_hunt")
        .level(10)
        .daily(false)
        .objectives(List.of(
                ObjectiveDefinition.builder()
                        .id("kill_boss")
                        .trigger("kill_mythic_mob")
                        .target("EventBoss")
                        .requiredAmount(1)
                        .name("&#FF6B6BSlay the Event Boss")
                        .description(List.of("&7Defeat the boss before it escapes."))
                        .build()))
        .conditions(List.of())
        .rewards(List.of(
                RewardDefinition.builder()
                        .type("quest_xp")
                        .value("500")
                        .build()))
        .acceptExpire(-1)
        .activeExpire(30 * 60 * 1000L)
        .build();

McQuests.getApi().registerQuest(quest);
```

Note that `acceptExpire`/`activeExpire` are **milliseconds** here (`-1` = no limit), unlike the seconds used in `quests.yml`.

{% hint style="warning" %}
`/quests reload` rebuilds the quest list from `quests.yml`, which wipes code-registered quests. If your quests must survive reloads, re-register them — or ship them as YAML and only use the API for the trigger/reward logic.
{% endhint %}

To hand a quest to a player programmatically:

```java
McQuests.getApi().giveQuest(player, "event_boss_hunt");
```

This goes through the same pipeline as clicking the quest board — limits and conditions are checked, so it can fail silently. For unconditional grants, mirror the `/quests force give` admin command instead.

### Events <a href="#events" id="events"></a>

Two Bukkit events cover the leveling system:

#### `PlayerXpGainEvent` <a href="#playerxpgainevent" id="playerxpgainevent"></a>

Fires before quest XP is applied. Cancellable, and the amount is mutable — this is the hook for custom XP boosters:

```java
@EventHandler
public void onXpGain(PlayerXpGainEvent event) {
    if (event.getPlayer().hasPermission("myserver.booster")) {
        event.setAmount(event.getAmount() * 2);
    }
}
```

Getters: `getPlayer()`, `getAmount()` / `setAmount(double)`, `getOriginalAmount()`, `getLevel()`, `getCurrentXp()`.

#### `PlayerLevelUpEvent` <a href="#playerlevelupevent" id="playerlevelupevent"></a>

Fires once per level gained (a big XP grant can fire it several times in a row). Not cancellable — it's a notification:

```java
@EventHandler
public void onLevelUp(PlayerLevelUpEvent event) {
    broadcastMilestone(event.getPlayer(), event.getNewLevel());
}
```

Getters: `getPlayer()`, `getOldLevel()`, `getNewLevel()`.

### Per-player stat modifiers <a href="#per-player-stat-modifiers" id="per-player-stat-modifiers"></a>

A `QuestUserModifier` lets you adjust a player's computed limits and multipliers — the same numbers the level bonuses and premium pass feed into:

```java
McQuests.getInstance().getModifiers().add((player, user) -> {
    if (player.hasPermission("myserver.mvp")) {
        user.setDailyBoardSize(user.getDailyBoardSize() + 1);
        user.setXpMultiplier(user.getXpMultiplier() * 1.1);
    }
});
```

The `MutableQuestUser` exposes `maxAccepted`, `maxActive`, `dailyBoardSize`, `xpMultiplier` and `rewardMultiplier`. Modifiers run after the base + per-level values are computed; premium bonuses are applied on top of the result.

{% hint style="info" %}
The computed user object is cached. If your modifier's inputs change at runtime (a rank purchase, a toggled booster), call `McQuests.getInstance().getQuestUserService().invalidate(player)` to force a rebuild.
{% endhint %}

### Built-in trigger constants <a href="#built-in-trigger-constants" id="built-in-trigger-constants"></a>

All built-in trigger IDs are available as constants in `com.mongenscave.mcquests.api.trigger.Triggers` (`Triggers.BLOCK_BREAK`, `Triggers.KILL_MYTHIC_MOB`, ...). Use them instead of constructing `ObjectiveTrigger` by hand when firing or comparing built-in triggers — see [How to Create a Quest](/premium-products/quests/features/create-a-quest.md) for the full list and their semantics.
