How It Works

When a dialogue opens, QuestLines resolves a portrait for the NPC using the following priority chain (first match wins):

  1. Page-level override — a Portrait field set directly on the dialogue page.
  2. NPC override — an explicit texture path stored per-NPC in npcs.json.
  3. Convention file — a PNG placed at QuestLines/portraits/{npcId}.png (or {citizenName}.png for HyCitizens NPCs).
  4. Skin API fallback — a headshot fetched asynchronously from api.hytl.skin using the citizen's skin recipe (HyCitizens only).

The resolved image is sent to the player as a DynamicImageAsset (for local PNG files and API fetches) or referenced directly as a resource-pack texture path (for override strings that don't match a local file).

The portrait is rendered as a 320×320 px panel anchored above the dialogue box, on whichever side is configured. It is automatically released when the dialogue closes.

Global Configuration

Three keys in QuestLines/QuestLines.conf control portrait behaviour globally:

Key Default Description
portrait_enabled false Master toggle. Portraits are not shown unless this is true.
portrait_side left Which side of the dialogue the portrait appears above. Accepts left or right.
portrait_skin_fallback true When true and no portrait file is found, QuestLines fetches a headshot from the skin API for HyCitizens NPCs. Set to false to disable all outbound API calls.

Example QuestLines.conf section:

portrait_enabled=true
portrait_side=left
portrait_skin_fallback=true

Changes take effect after /ql reload.

Convention Files

The simplest way to add a portrait is to drop a PNG into the plugin's portraits directory. No further configuration is needed.

  • Directory: QuestLines/portraits/
  • File name: {npcId}.png where npcId is the NPC's key in npcs.json (citizen UUID or entity UUID).
  • Spaces in the ID are replaced with underscores — e.g. an NPC with display name Bandit Captain maps to Bandit_Captain.png.
HyCitizens Name Fallback

For HyCitizens NPCs, if no file is found for the citizen's ID, QuestLines also checks for a file named after the citizen's display name (e.g. Guard.png). This makes it easy to share one portrait across multiple citizens that share the same name.

Portrait files are loaded once and cached in memory. Run /ql reload to clear the cache and pick up new or replaced files.

There is no enforced resolution requirement, but portraits are displayed at 256×256 px by the skin API and rendered at 320×320 px in the UI. Square images at 256×256 or larger look best.

NPC Portrait Override

You can pin a specific texture path to an NPC in npcs.json using the NpcPortraits map. The value can be either:

  • A local file name — QuestLines looks for QuestLines/portraits/{value}.png (spaces replaced with underscores). If found, it is delivered as a DynamicImageAsset, the same as a convention file.
  • A resource-pack texture path — if no matching local file exists, the value is passed directly as a TexturePath in the UI command. Use this to reference textures already bundled in your resource pack.
{
  "NpcPortraits": {
    "f48f7eaa-512e-415d-bfdb-f53d2ea0b991": "elder_mage",
    "b2c3d4e5-...": "textures/npcs/blacksmith"
  }
}

This override takes priority over convention files and the skin API, but is still overridden by a page-level Portrait field.

Per-NPC Portrait Toggle

You can disable portraits for a specific NPC even when they are globally enabled. Set NpcPortraitEnabled to false for that NPC's ID in npcs.json:

{
  "NpcPortraitEnabled": {
    "f48f7eaa-512e-415d-bfdb-f53d2ea0b991": false
  }
}

Only false values are stored. An absent entry means portraits are enabled for that NPC (the default). This flag is ignored if the global portrait_enabled setting is false.

Page-Level Portrait Override

Individual dialogue pages can override the portrait via the Portrait field in the page's JSON. This is the highest-priority portrait source and bypasses all NPC-level configuration.

{
  "PageId": "p1",
  "Npc": "Welcome, traveller!",
  "Portrait": "elder_mage_angry",
  ...
}

The value follows the same local-file-first logic as the NPC override: QuestLines checks for QuestLines/portraits/{value}.png first, and if not found, passes the value through as a resource-pack texture path.

Use page-level overrides to change the portrait mid-conversation — for example, to show a different expression on a key emotional beat.

Skin API Fallback

When portrait_skin_fallback=true and HyCitizens is active, QuestLines will automatically fetch a headshot for any citizen that has no portrait file configured. The headshot is a 256×256 px PNG generated from the citizen's skin data.

Two fetch paths are used depending on the citizen's skin configuration:

  • Live-skin player-model citizens — a simple GET request to api.hytl.skin/headshot/{username}.
  • Character-builder citizens and custom-skin player-models — a POST request to api.hytl.skin/headshot with the citizen's full skin recipe as a JSON body.

Fetches are asynchronous. On first open the portrait slot will be empty; once the fetch completes the dialogue is automatically refreshed to display the image. Subsequent opens for the same NPC use the cached result instantly.

Network Dependency

The skin API fallback makes outbound HTTP requests to api.hytl.skin. If your server has no internet access or you want to avoid the dependency, set portrait_skin_fallback=false in QuestLines.conf.

Cache & Reload

Portrait data (both file bytes and API responses) is held in memory after the first load. This means disk reads and API calls happen at most once per NPC per server session.

To force a cache clear — for example after replacing a portrait PNG on disk — run:

/ql reload

The reload also re-reads QuestLines.conf, so you can change portrait_enabled, portrait_side, or portrait_skin_fallback and have the changes take effect without restarting the server.

Quick Reference

Where What to set Priority
QuestLines.conf portrait_enabled=true Master toggle (lowest)
QuestLines/portraits/ Drop {npcId}.png or {citizenName}.png Convention file (3rd)
npcs.json NpcPortraits Map NPC ID → file name or texture path NPC override (2nd)
Page JSON Portrait field File name or texture path Page override (highest)
Automatic Skin API headshot (HyCitizens only, async) Fallback (4th)