Repository navigation
walkthrough_npc_portraits
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.
1. PortraitVariants.cs - Portrait discovery and variant management
- Recursively searches ALL folders under
PKCore/Textures/andPKCore/00-Mods/ - Searches both
Portraits/andNPCPortraits/subdirectories - Handles character name → portrait filename mappings
- Manages expression variants (angry, sad, happy, etc.)
- Supports both
.pngand high-performance pre-compressed.ddsformats (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
AssetLoaderwith 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
The system searches recursively through folders with the following priority:
Priority Order:
-
00-Mods Packages (highest priority) -
PKCore/00-Mods/<ModName>/Textures/ -
GSD1 folders (medium-high priority) - All folders under
Textures/GSD1/(Suikoden 1) -
GSD2 folders (medium priority) - All folders under
Textures/GSD2/(Suikoden 2) -
Root folders (lowest priority) - All folders directly under
Textures/
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.pngformats - ✅ If both
.ddsand.pngexist 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/andGSD2/directories
Map character names to portrait filenames, enabling:
- Use of game asset IDs (e.g.,
fp_053for Luc,fp_140for Luca) - Consistent naming across different character references
- Support for characters with multiple names/aliases
Example:
{
"Luca": "fp_140",
"Grady": "fp_grady",
"Durin": "fp_durin"
}-
Without mapping:
"Bonaparte"→ searches forbonaparte.dds/bonaparte.png -
With mapping:
"Luca"→ maps tofp_140→ searches forfp_140.dds/fp_140.png
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
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"
}
}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"
}Example: Loading portrait for "Luca|blood"
-
Parse expression:
"Luca|blood"→ character ="Luca", expression ="blood" -
Map name:
"Luca"→"fp_140"(viaPortraitMappings.json) -
Try variant:
"fp_140"+"blood"→"fp_140_blood.dds"/fp_140_blood.png(viaPortraitVariants.json) -
Search directories (priority order):
- 00-Mods packages
- GSD1 folders (for S1)
- GSD2 folders (for S2)
- Root
Textures/folders
-
If variant not found: Fall back to default
fp_140.dds/fp_140.png - If default not found: Fall back to native game portrait (preserves cutscene and Event Viewer expressions)
-
If no native portrait: Fall back to
fp_219.png(question mark)
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"
}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!!!!"
}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.
-
Find Dialogue Message IDs: Enable
LogTextIDs = trueinBepInEx/config/faospark.pkcore.cfg. -
Dump Dialogue Database: Enable
DumpTextDatabase = trueto generate cumulative JSON dumps inPKCore/Debug/TextDB_GSD1.jsonandTextDB_GSD2.json. -
Check Console Output:
[PortraitSystem] Found: GSD2/NPCPortraits/fp_140_blood.dds [PortraitVariants] Using variant: Luca (blood) -> fp_140_blood.dds