Plugin API
QuestLines exposes a full extensibility API for other Hytale plugins: query player quest state, evaluate requirements, execute actions, and register custom requirement types, action types, and text variables.
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.
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
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"]
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");
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
| Method | Type | Description |
|---|---|---|
| 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() | String | Locale string, e.g. "en-US" |
| isAutoTrackQuests() | boolean | Whether 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();
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
| Method | Description |
|---|---|
| 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) |