Getting the API

Everything lives on QuestLinesAPI. Obtain it once after the server has loaded plugins and store the reference in your plugin:

QuestLinesAPI api = ((QuestLinesPlugin) server.getPlugin("QuestLines")).getApi();

All integration — querying player data, checking requirements, executing actions, registering custom types, and querying NPCs — goes through this single object.

Register After Setup

Call api.registerRequirement, api.registerAction, and api.registerTextVariable after QuestLines has completed its setup() phase — i.e. from your own plugin's setup() or start(). Registering too early will throw a NullPointerException because the managers are not yet initialised.

Player Data

Querying Player Data

All query methods return safe, unmodifiable views. The player record is auto-created on first access.

// Quest state
boolean done    = api.hasCompletedQuest(playerRef, "intro_quest");
boolean started = api.hasStartedQuest(playerRef, "main_quest");
List<String> completed = api.getCompletedQuests(playerRef);
List<String> started  = api.getStartedQuests(playerRef);

// Tags
boolean vip  = api.hasTag(playerRef, "vip");
List<String> tags = api.getTags(playerRef);

// Progress counters (e.g. kill:zombie, break:dirt, talk:Elder Maren)
int kills = api.getProgress(playerRef, "kill:zombie");
Map<String, Integer> allProgress = api.getAllProgress(playerRef);

// Named variables (set by variable: action)
double score = api.getVariable(playerRef, "score");
Map<String, Double> allVars = api.getAllVariables(playerRef);

// Timestamps (epoch ms; set by setTimestamp: / timedStart: actions)
long ts = api.getTimestamp(playerRef, "last_trade");

// HUD-tracked quests
List<String> tracked = api.getTrackedQuests(playerRef);

Mutating Player Data

All mutators save automatically — no manual savePlayerData call needed.

// Tags
api.addTag(playerRef, "vip");
api.removeTag(playerRef, "vip");

// Variables
api.setVariable(playerRef, "score", 42.0);

// Progress counters (pass 0 to reset)
api.setProgress(playerRef, "kill:zombie", 5);
api.setProgress(playerRef, "kill:zombie", 0);           // resets counter

Checking Requirements

Accepts any colon-delimited requirement string that QuestLines understands. Unknown types pass by default, consistent with internal behavior.

// Single requirement
boolean passes = api.meetsRequirement(playerRef, "questCompleted:intro_quest");
boolean hasKills = api.meetsRequirement(playerRef, "kill:zombie:10");

// Multiple requirements — all must pass (short-circuits on first failure)
boolean eligible = api.meetsRequirements(playerRef,
    "hasTag:vip",
    "questCompleted:intro_quest",
    "variable:score:greater:100"
);

// List form
boolean ok = api.meetsRequirements(playerRef, List.of("hasTag:vip", "kill:zombie:10"));

Executing Actions

Accepts any colon-delimited action string that QuestLines understands, including all built-in types (questStarted:, addTag:, economy:give:, variable:, etc.) and any custom actions you have registered.

// Single action
api.executeAction(playerRef, "addTag:vip");
api.executeAction(playerRef, "questCompleted:bounty_quest");

// Multiple actions — executed in order
api.executeActions(playerRef,
    "questCompleted:bounty_quest",
    "economy:give:500",
    "variable:score:add:10"
);

// List form
api.executeActions(playerRef, List.of("track:zombie_kills:kill:zombie", "questStarted:hunt"));

Quest & HUD Control

Quest Queries

Check static quest configuration — does not depend on a specific player.

// Check if a quest can be completed more than once
boolean repeatable = api.isQuestRepeatable("daily_bounty");  // false if quest doesn't exist

// Questline membership
String qlId    = api.getQuestlineId("my_quest");    // "" if none
String qlTitle = api.getQuestlineTitle("my_quest"); // "" if none

// All quests in a questline (unmodifiable)
List<String> questIds = api.getQuestsInQuestline("main_story");

// All distinct questline IDs that have at least one quest
Set<String> questlines = api.getQuestlineIds();

Quest Mutations

Assign or remove a quest's questline. Both the questline ID and title are updated together and saved immediately. Changing the questline ID also changes the subdirectory the quest's JSON file is written to on the next save.

// Assign to a questline
api.setQuestline("my_quest", "main_story", "Main Story");

// Remove from questline (pass empty strings)
api.setQuestline("my_quest", "", "");

HUD Control

Programmatically suppress or restore the quest-goal HUD for a player. Use this during cutscenes, boss fights, or any context where the quest overlay would be distracting.

// Hide the HUD
api.setHudEnabled(playerRef, false);

// Restore it
api.setHudEnabled(playerRef, true);

// Query current state
boolean visible = api.isHudEnabled(playerRef);  // false only if explicitly suppressed
Runtime Only

HUD suppression is not persisted. If a player disconnects and reconnects, the HUD returns to its normal state. Your plugin is responsible for re-suppressing it if needed.

Custom Requirements

Implement the Requirement interface and register it. Your type becomes available in every requirement field — page requirements, response requirements, tracking tags, and any: / not: wrappers.

Interface

package net.evilcraft.questlines.requirements;

public interface Requirement {
    /** The colon-delimited type key, e.g. "mycheck". Case is normalised — lowercase is stored. */
    String getType();

    /**
     * @param playerRef  the player being evaluated
     * @param data       the player's quest data (never null — auto-created if missing)
     * @param parts      colon-split tokens of the full requirement string;
     *                   parts[0] is the type key, parts[1..] are the arguments
     * @param plugin     the QuestLines plugin instance
     * @param manager    the RequirementManager (use to evaluate sub-requirements)
     * @return true if the player satisfies this requirement
     */
    boolean test(PlayerRef playerRef, PlayerData data, String[] parts,
               QuestLinesPlugin plugin, RequirementManager manager);

    /** Optional — return true if this requirement depends on NPC interaction context
     *  (e.g. "which NPC opened dialogue"). Contextual requirements are skipped during
     *  HUD resolution. Default: false. */
    default boolean isContextual() { return false; }

    /** Optional — human-readable label shown in the HUD and journal objectives list.
     *  Default falls back to the raw colon-joined string.
     *  Prefer supplying this via RequirementDescriptor when registering through the API. */
    default String describe(String[] parts, PlayerRef playerRef, PlayerData data,
                             QuestLinesPlugin plugin) {
        return String.join(":", parts);
    }
}

Example Implementation

// Requirement: "myrank:vip" — true if the player has VIP rank in your system
public class MyRankRequirement implements Requirement {

    @Override
    public String getType() { return "myrank"; }

    @Override
    public boolean test(PlayerRef playerRef, PlayerData data, String[] parts,
                         QuestLinesPlugin plugin, RequirementManager manager) {
        if (parts.length < 2) return false;
        return MyRankSystem.hasRank(playerRef.getUuid(), parts[1]);
    }
}

Registration

Use the single-arg overload if your class already implements describe(), or the two-arg overload to supply an objective label as a lambda:

// Simple — no HUD label (falls back to raw string)
api.registerRequirement(new MyRankRequirement());

// With a RequirementDescriptor lambda — provides a label in the HUD / journal objectives
api.registerRequirement(new MyRankRequirement(),
    (parts, playerRef, data) -> "Rank required: " + parts[1]
);

// Descriptor with dynamic progress info
api.registerRequirement(new MyKillRequirement(),
    (parts, playerRef, data) -> {
        int current = data.getProgress("mykill:" + parts[1]);
        int target  = parts.length > 2 ? Integer.parseInt(parts[2]) : 1;
        return "Kill " + parts[1] + ": " + current + " / " + target;
    }
);

The descriptor receives the same parts[] array as test(), so argument values (entity IDs, counts, etc.) are accessible at the expected indices. Once registered, use the requirement in any quest JSON just like a built-in:

"Requirements": ["myrank:vip"]
Type Key Normalisation

The type key returned by getType() is lowercased before storage. At parse time the first colon-delimited token of the requirement string is also lowercased before lookup. This means "MyRank", "myrank", and "MYRANK" in JSON all resolve to the same requirement.

Custom Actions

Implement the Action interface and register it. Your type can then be used in any action list — response actions, load actions, and auto-trigger pages.

Interface

package net.evilcraft.questlines.actions;

public interface Action {
    /** The colon-delimited type key, e.g. "myreward". Case is normalised — lowercase is stored. */
    String getType();

    /**
     * @param playerRef     the player who triggered this action
     * @param data          the player's quest data (never null — auto-created if missing)
     * @param parts         colon-split tokens of the full action string;
     *                      parts[0] is the type key, parts[1..] are the arguments
     * @param plugin        the QuestLines plugin instance
     * @param questManager  the QuestManager (player-data helpers, page resolution, etc.)
     * @param actionManager the ActionManager (execute nested action strings if needed)
     */
    void execute(PlayerRef playerRef, PlayerData data, String[] parts,
               QuestLinesPlugin plugin, QuestManager questManager, ActionManager actionManager);
}

Example Implementation

// Action: "myreward:daily" — grants the player a daily reward from your system
public class MyRewardAction implements Action {

    @Override
    public String getType() { return "myreward"; }

    @Override
    public void execute(PlayerRef playerRef, PlayerData data, String[] parts,
                         QuestLinesPlugin plugin, QuestManager questManager,
                         ActionManager actionManager) {
        if (parts.length < 2) return;
        String rewardId = parts[1];
        MyRewardSystem.grant(playerRef.getUuid(), rewardId);
    }
}

Registration

api.registerAction(new MyRewardAction());

Use it in any action list in quest JSON:

"Actions": [
  "questCompleted:daily_quest",
  "myreward:daily"
]

Custom Text Variables

TextVariable is a @FunctionalInterface. Register a prefix and a lambda (or full class). The variable can then appear inside any Dialog or response Text field as {prefix} or {prefix:arg1:arg2:...}.

Interface

@FunctionalInterface
public interface TextVariable {
    /**
     * @param parts     colon-split contents of the placeholder;
     *                  parts[0] is the prefix itself
     *                  (e.g. {track:zombie_kills} → ["track", "zombie_kills"])
     * @param playerRef the player for whom text is being resolved
     * @param data      the player's quest data; may be null outside dialogue context
     * @param page      the current dialogue page; may be null outside dialogue context
     * @param plugin    the QuestLines plugin instance
     * @return the string to substitute in place of the placeholder; never null
     */
    String resolve(String[] parts, PlayerRef playerRef, PlayerData data,
               Page page, QuestLinesPlugin plugin);
}

Example — Simple Variable

// Registers {myguild} — shows the player's guild name
api.registerTextVariable("myguild", (parts, playerRef, data, page, p) ->
    MyGuildSystem.getGuildName(playerRef.getUuid()));

Example — Variable With Arguments

// Registers {mystat:statName} — e.g. {mystat:wins}, {mystat:playtime}
api.registerTextVariable("mystat", (parts, playerRef, data, page, p) -> {
    if (parts.length < 2) return "0";
    return MyStatsSystem.getStat(playerRef.getUuid(), parts[1]);
});

Then in dialogue JSON:

"Dialog": "Welcome back, {username}! Your guild is {myguild} and you have {mystat:wins} wins."

NPC Queries

These methods let you inspect which NPCs have quests assigned and which quests belong to a specific NPC. The NPC key is a HyCitizens citizen ID for HyCitizens NPCs, or an entity UUID string for standard Hytale NPCs — both live in the same map.

// All NPC IDs that have at least one direct quest assignment
Set<String> npcIds = api.getNpcsWithQuests();

// All NPC IDs that have at least one random quest group configured
Set<String> randomNpcIds = api.getNpcsWithRandomGroups();

// The ordered quest IDs directly assigned to a specific NPC
List<String> quests = api.getQuestsForNpc("citizen_123");

// All NPC IDs that have a specific quest directly assigned
List<String> npcsForQuest = api.getNpcsForQuest("intro_quest");
Direct vs. Random Assignments

getNpcsWithQuests() only returns NPCs from the direct assignment map. NPCs configured with only random quest groups will not appear there — use getNpcsWithRandomGroups() for those. Similarly, getQuestsForNpc and getNpcsForQuest only cover direct assignments.

Accessing Player Data (Low-Level)

For most use cases, prefer QuestLinesAPI (see above). If you need direct access to PlayerData — for example to read fields not exposed by the API — use QuestManager. It auto-creates a record for new players and keeps the username in sync.

Reading Data

QuestManager qm = ql.getQuestManager();
PlayerData data = qm.getPlayerData(playerRef);

// Quest state
boolean started   = data.getStartedQuests().contains("my_quest");
boolean completed = data.getCompletedQuests().contains("my_quest");

// Tags
boolean hasTag = data.getTags().contains("my_flag");

// Kill / break / place progress counters
int kills = data.getProgress().getOrDefault("kill:Goblin", 0);

// Named variables
double score = data.getVariables().getOrDefault("score", 0.0);

// Timestamps (epoch ms; used by cooldown requirements)
long ts = data.getTimestamps().getOrDefault("my_timer", 0L);

Mutating and Saving

Always call savePlayerData (or plugin.getPlayerConfig().save()) after mutating a player's data — changes are in-memory only until saved.

PlayerData data = qm.getPlayerData(playerRef);

data.getTags().add("my_custom_flag");
data.getVariables().put("score", 42.0);
data.getStartedQuests().add("my_quest");

qm.savePlayerData(playerRef, data);  // persists to players.json

PlayerData Fields

MethodTypeDescription
getStartedQuests()List<String>Quest IDs the player has started but not completed
getCompletedQuests()List<String>Quest IDs the player has completed
getTags()List<String>Arbitrary string flags; includes tracking tags
getProgress()Map<String, Integer>Counter values keyed by tracking key (e.g. "kill:Goblin")
getTimestamps()Map<String, Long>Epoch-ms timestamps keyed by arbitrary key; used by cooldown:
getVariables()Map<String, Double>Named numeric variables set by the variable: action
getTrackedQuests()List<String>Quest IDs currently shown on the player's Quest Goal HUD
getPreferredLanguage()StringLocale string, e.g. "en-US"
isAutoTrackQuests()booleanWhether new quests are auto-added to the HUD
getHudPosition()String"TopRight", "TopLeft", "BottomRight", or "BottomLeft"

QuestManager Utilities (Advanced)

QuestManager exposes lower-level helpers for advanced integrations — page resolution, HUD management, and more. For requirement checking and action execution, prefer QuestLinesAPI.

Page Resolution

QuestManager qm = ql.getQuestManager();

// Find the first page (across all quests for an NPC) whose requirements pass
QuestManager.PageResolution res = qm.resolvePageAndQuestForPlayer(playerRef, npcKey);
if (res != null) {
    String questId = res.questId;
    String pageId  = res.pageId;
    Page   page    = res.page;
}

// Find the player's current page in a specific quest (used by HUD)
Page current = qm.resolveCurrentPageForQuest(playerRef, "my_quest");

Quest Tracking (HUD)

// Add/remove a quest from the player's goal HUD and refresh it
qm.trackQuest(player, playerRef, "my_quest");
qm.untrackQuest(player, playerRef, "my_quest");

// Rebuild the HUD for a player (call after changing tracked quests or player data)
qm.refreshHud(playerRef);

Evaluating Requirements Programmatically

// Test a single requirement string for a player
RequirementManager rm   = qm.getRequirementManager();
PlayerData         data = qm.getPlayerData(playerRef);
boolean passes = rm.meetsRequirementPublic(playerRef, data, "questCompleted:my_quest");

Executing Actions Programmatically

// Execute an arbitrary list of action strings for a player
qm.executeActions(playerRef, List.of(
    "questStarted:my_quest",
    "track:goblin_kills:kill:Goblin",
    "item:IronSword:1"
));

Reading Quest & Page Configs

The in-memory quest and page maps are accessible if you need to inspect quest definitions at runtime.

// In-memory map of all quests, keyed by quest ID
Map<String, Quest> quests = ql.getQuestConfig().getQuestConfig().getQuests();

// In-memory map of all pages, keyed by page ID
Map<String, Page> pages = ql.getPageConfig().get().getPages();

// NPC → quest mappings
NpcConfig npcs = ql.getNpcConfig().get();
Read-Only Recommendation

These maps are the live in-memory state. Treat them as read-only unless you also call plugin.getQuestConfig().save() after any writes. Mutating them without saving leaves the disk state out of sync.

API Quick Reference

MethodDescription
QuestLinesAPI — ql.getApi()
api.hasCompletedQuest(playerRef, questId) True if the player has completed the quest
api.hasStartedQuest(playerRef, questId) True if the player has started but not completed the quest
api.hasTag(playerRef, tag) True if the player has the given tag
api.getProgress(playerRef, key) Progress counter value (e.g. "kill:zombie"); 0 if not set
api.getVariable(playerRef, name) Named variable value; 0.0 if not set
api.getTimestamp(playerRef, key) Epoch-ms timestamp; 0 if not set
api.getCompletedQuests / getStartedQuests / getTags / getTrackedQuests(playerRef) Unmodifiable list views of player quest state
api.getAllProgress / getAllVariables(playerRef) Unmodifiable map views of counters / variables
api.addTag(playerRef, tag) Add a tag and save
api.removeTag(playerRef, tag) Remove a tag and save
api.setVariable(playerRef, name, value) Set a named variable and save
api.setProgress(playerRef, key, value) Set a progress counter and save (0 resets the key)
api.meetsRequirement(playerRef, requirementString) Evaluate a single requirement string
api.meetsRequirements(playerRef, ...) Evaluate multiple requirements — all must pass; accepts List or varargs
api.executeAction(playerRef, actionString) Execute a single action string
api.executeActions(playerRef, ...) Execute multiple action strings in order; accepts List or varargs
api.isQuestRepeatable(questId) True if the quest is configured as repeatable; false if not found
api.getQuestlineId(questId) Questline ID the quest belongs to; empty string if none or quest not found
api.getQuestlineTitle(questId) Display title of the quest's questline; empty string if none or quest not found
api.getQuestsInQuestline(questlineId) Unmodifiable list of all quest IDs assigned to the given questline
api.getQuestlineIds() Unmodifiable set of all distinct questline IDs that have at least one quest
api.setQuestline(questId, questlineId, questlineTitle) Assign a quest to a questline and save; pass empty strings to clear
api.setHudEnabled(playerRef, enabled) Show or hide the quest-goal HUD for a player (runtime-only; cleared on disconnect)
api.isHudEnabled(playerRef) True unless the HUD has been explicitly suppressed via setHudEnabled
api.getNpcsWithQuests() Unmodifiable set of all NPC IDs with direct quest assignments
api.getNpcsWithRandomGroups() Unmodifiable set of all NPC IDs with random quest groups configured
api.getQuestsForNpc(npcId) Ordered list of quest IDs directly assigned to the given NPC
api.getNpcsForQuest(questId) All NPC IDs that have the given quest directly assigned
api.registerRequirement(r) Register a custom Requirement implementation
api.registerRequirement(r, descriptor) Register with a RequirementDescriptor lambda for HUD/journal objective labels
api.registerAction(a) Register a custom Action implementation
api.registerTextVariable(prefix, fn) Register a custom text variable substitution
Advanced — QuestManager & configs
ql.getQuestManager() Access the QuestManager facade
ql.getTextFormatter() Access the TextFormatter for manual variable substitution
ql.getQuestConfig() Access the directory-backed quest config (in-memory + I/O)
ql.getPageConfig() Access the page config (delegates to quest config)
ql.getPlayerConfig() Access the player data config
ql.getNpcConfig() Access the NPC → quest mapping config
qm.getPlayerData(playerRef) Get (or auto-create) a player's PlayerData
qm.savePlayerData(playerRef, data) Persist a player's PlayerData to disk
qm.resolvePageAndQuestForPlayer(playerRef, npcKey) Find the first passing page across all quests for an NPC; returns PageResolution or null
qm.resolveCurrentPageForQuest(playerRef, questId) Find the player's current page in a specific quest
qm.executeActions(playerRef, actions) Execute a list of action strings for a player
qm.getRequirementManager().meetsRequirementPublic(playerRef, data, req) Evaluate a single requirement string programmatically
qm.trackQuest(player, playerRef, questId) Add quest to player's HUD tracking list and refresh HUD
qm.untrackQuest(player, playerRef, questId) Remove quest from player's HUD tracking list and refresh HUD
qm.refreshHud(playerRef) Rebuild the Quest Goal HUD for a player
QuestLinesPlugin.getInstance() Static singleton accessor (use sparingly — prefer the injected instance)