Skip to content

walkthrough_npc_portraits

faospark edited this page Sep 16, 2026 · 2 revisions

NPC Portrait System - Full Documentation

Overview

The NPC Portrait System allows custom portrait injection for NPCs that don't have portraits in the base game, as well as expression switching for existing characters. It features a flexible directory search system, character name mapping, expression variants, DDS (BC1/BC3/BC7) texture acceleration, and automatic game-specific detection.

Note: Full support is active for both Suikoden I and Suikoden II.


Architecture

Core Components

1. PortraitVariants.cs - Portrait discovery and variant management

  • Recursively searches ALL folders under PKCore/Textures/ and PKCore/00-Mods/
  • Searches both Portraits/ and NPCPortraits/ subdirectories
  • Handles character name → portrait filename mappings
  • Manages expression variants (angry, sad, happy, etc.)
  • Supports both .png and high-performance pre-compressed .dds formats (BC1/BC3/BC7)
  • Provides unified API for portrait path resolution

2. PortraitSystemPatch.cs - Portrait injection and rendering

  • Patches dialogue and message window systems for Suikoden I and Suikoden II
  • Loads textures via AssetLoader with DDS BC7/BC3/BC1 support
  • Creates sprites on-demand with clean aspect ratios
  • Handles dialog text replacement and speaker name injection
  • Preserves native in-game portraits for Event Viewer and cutscenes: missing custom emotion variants gracefully fall back to original dialogue portraits rather than forcing question mark (fp_219) placeholders

Directory Structure & Search Priority

How It Works

The system searches recursively through folders with the following priority:

Priority Order:

  1. 00-Mods Packages (highest priority) - PKCore/00-Mods/<ModName>/Textures/
  2. GSD1 folders (medium-high priority) - All folders under Textures/GSD1/ (Suikoden 1)
  3. GSD2 folders (medium priority) - All folders under Textures/GSD2/ (Suikoden 2)
  4. Root folders (lowest priority) - All folders directly under Textures/

Example Layouts

Recommended Directory Layout:

PKCore/Textures/
├── GSD1/
│   └── NPCPortraits/          ← Suikoden I portraits (PNG / DDS)
│       ├── Grady.dds
│       ├── Iga.png
│       ├── Rosh.dds
│       └── Durin.png
├── GSD2/
│   └── NPCPortraits/          ← Suikoden II portraits (PNG / DDS)
│       ├── fp_140.dds         ← Luca Blight base portrait (DDS BC7)
│       ├── fp_140_laugh1.dds  ← Luca laughing
│       ├── fp_140_shout.dds   ← Luca shouting
│       ├── fp_140_blood.dds   ← Luca bloodied
│       └── Bonaparte.png
└── NPCPortraits/              ← Shared/fallback portraits
    └── fp_219.png             ← Generic "?" fallback

Key Points:

  • ✅ Supports both .dds (BC1, BC3, BC7) and .png formats
  • ✅ If both .dds and .png exist for the same name, DDS takes priority
  • ✅ Nest folders as deep as you want (e.g. Textures/GSD2/Portraits/Villagers/)
  • ✅ Game-specific isolation is managed via GSD1/ and GSD2/ directories

Character Name Mapping

Purpose

Map character names to portrait filenames, enabling:

  • Use of game asset IDs (e.g., fp_053 for Luc, fp_140 for Luca)
  • Consistent naming across different character references
  • Support for characters with multiple names/aliases

Configuration: PKCore/Config/PortraitMappings.json

Example:

{
  "Luca": "fp_140",
  "Grady": "fp_grady",
  "Durin": "fp_durin"
}

How It Works

  • Without mapping: "Bonaparte" → searches for bonaparte.dds / bonaparte.png
  • With mapping: "Luca" → maps to fp_140 → searches for fp_140.dds / fp_140.png

Expression Variants

Purpose

Support multiple emotional expressions for the same character:

  • fp_140_angry.dds, fp_140_laugh1.dds, fp_140_blood.dds
  • Switch portraits dynamically based on dialogue context

Configuration: PKCore/Config/PortraitVariants.json

Example (Luca Blight expressions):

{
  "fp_140": {
    "neutral": "fp_140_neutral.dds",
    "laugh1": "fp_140_laugh1.dds",
    "laugh2": "fp_140_laugh2.dds",
    "troll": "fp_140_troll.dds",
    "shout": "fp_140_shout.dds",
    "blood": "fp_140_blood.dds",
    "bloodfinal": "fp_140_bloodfinal.dds",
    "mad": "fp_140_mad.dds",
    "sneaky": "fp_140_sneaky.dds",
    "disgust1": "fp_140_disgust1.dds"
  }
}

Usage in Speaker Overrides

Use pipe syntax | to specify expressions:

S2SpeakerOverrides.json / S1SpeakerOverrides.json:

{
  "message:1002160039": "Luca|laugh1",
  "message:1002160048": "Luca|shout",
  "message:1004090030": "Luca|blood",
  "message:1004090048": "Luca|bloodfinal",
  "message:1010100035": "Luca|troll",
  "message:1003300033": "Nash",
  "message:1002070055": "Bonaparte"
}

Search Flow & Fallbacks

Complete Resolution Process

Example: Loading portrait for "Luca|blood"

  1. Parse expression: "Luca|blood" → character = "Luca", expression = "blood"
  2. Map name: "Luca" → "fp_140" (via PortraitMappings.json)
  3. Try variant: "fp_140" + "blood" → "fp_140_blood.dds" / fp_140_blood.png (via PortraitVariants.json)
  4. Search directories (priority order):
    • 00-Mods packages
    • GSD1 folders (for S1)
    • GSD2 folders (for S2)
    • Root Textures/ folders
  5. If variant not found: Fall back to default fp_140.dds / fp_140.png
  6. If default not found: Fall back to native game portrait (preserves cutscene and Event Viewer expressions)
  7. If no native portrait: Fall back to fp_219.png (question mark)

Integration with Dialog System

Speaker Overrides

Configured in PKCore/Config/S1SpeakerOverrides.json (Suikoden 1) and PKCore/Config/S2SpeakerOverrides.json (Suikoden 2):

{
  "message:1002160039": "Luca|laugh1",
  "message:1004090030": "Luca|blood",
  "message:1003300033": "Nash",
  "message:1002070055": "Bonaparte"
}

Dialog Text Overrides

Configured in PKCore/Config/DialogOverrides.json:

{
  "_comment": "Dialog text replacement by ID. Enable LogTextIDs in config to discover IDs.",
  "add_message:1120": "©2026 Konami Digital Entertainment",
  "message:1002070055": "Piip!",
  "message:1002070058": "GWHAAACK!!!!"
}

Save Slot Party Member Portraits

PKCore also extends portrait rendering to the Save / Load screen (ShowSaveSlotPartyPortraits = true):

  • Displays high-resolution mini portraits next to character levels (Txt_Lv) in save/load slots.
  • Fully integrated for both Suikoden I (UISaveLoad1) and Suikoden II (UISaveLoad2).
  • Automatically resolves character IDs and Face IDs with 0ms in-memory cache lookups.

Diagnostics & Troubleshooting

  1. Find Dialogue Message IDs: Enable LogTextIDs = true in BepInEx/config/faospark.pkcore.cfg.
  2. Dump Dialogue Database: Enable DumpTextDatabase = true to generate cumulative JSON dumps in PKCore/Debug/TextDB_GSD1.json and TextDB_GSD2.json.
  3. Check Console Output:
    [PortraitSystem] Found: GSD2/NPCPortraits/fp_140_blood.dds
    [PortraitVariants] Using variant: Luca (blood) -> fp_140_blood.dds
    

Clone this wiki locally