-
Notifications
You must be signed in to change notification settings - Fork 0
Game Data Integration Guide
How to bootstrap, configure, and use IGameDataService in Radoub tools.
- Overview
- Bootstrap
- Settings Integration
- IsConfigured Guard Pattern
- Module Context Switching
- Caching
- Common Usage Recipes
- Error Handling
- Testing
GameDataService provides unified access to NWN game data (2DA tables, TLK strings, resources, soundsets, palettes). All Radoub tools use the same patterns for initialization and access.
Key characteristics:
- Direct instantiation (not registered in DI containers)
- Settings-driven configuration via
RadoubSettings.Instance - Lazy initialization - created in background tasks, null until ready
-
IDisposable- must be disposed when the tool shuts down
Most tools use the parameterless constructor, which reads from RadoubSettings.Instance:
_gameDataService = new GameDataService();Since construction involves file I/O (scanning KEY/BIF archives, loading TLK), initialize on a background thread:
private IGameDataService? _gameDataService;
private async Task InitializeGameDataServiceAsync()
{
try
{
_gameDataService = await Task.Run(() => new GameDataService());
if (_gameDataService.IsConfigured)
UnifiedLogger.LogApplication(LogLevel.INFO, "GameDataService initialized");
else
UnifiedLogger.LogApplication(LogLevel.WARN, "GameDataService created but not configured");
}
catch (Exception ex)
{
UnifiedLogger.LogApplication(LogLevel.WARN, $"GameDataService init failed: {ex.Message}");
_gameDataService = null;
}
}For unit tests or tools that need custom paths:
var config = new GameResourceConfig
{
GameDataPath = @"C:\Games\NWN\data",
KeyFilePath = @"C:\Games\NWN\data\nwn_base.key",
OverridePath = @"C:\Games\NWN\override",
TlkPath = @"C:\Games\NWN\data\dialog.tlk",
CacheArchives = true
};
using var service = new GameDataService(config);GameDataService internally calls BuildConfig(RadoubSettings) during construction:
RadoubSettings.BaseGameInstallPath → GameResourceConfig.GameDataPath + "/data"
→ GameResourceConfig.KeyFilePath + "/data/nwn_base.key"
RadoubSettings.NeverwinterNightsPath → GameResourceConfig.OverridePath + "/override"
RadoubSettings.HakSearchPaths → GameResourceConfig.HakPaths (scans *.hak)
RadoubSettings.CustomTlkPath → GameResourceConfig.CustomTlkPath
RadoubSettings.GetTlkPath() → GameResourceConfig.TlkPath
RadoubSettings is a shared singleton persisted to ~/Radoub/RadoubSettings.json. When Trebuchet's "Override Sub-Tools" is enabled, all tools share the same paths and TLK settings.
When the user changes game paths in the Settings dialog:
// Save settings, then reload the service
RadoubSettings.Instance.Save();
_gameDataService?.ReloadConfiguration();ReloadConfiguration() disposes the old resolver, clears all caches, and reinitializes from the current RadoubSettings.Instance.
Always check before use. IsConfigured is false when game paths are missing or invalid.
private IGameDataService GameData => _gameDataService
?? throw new InvalidOperationException("GameDataService not initialized");public List<BaseItemType> LoadBaseItems()
{
if (_gameDataService == null || !_gameDataService.IsConfigured)
{
UnifiedLogger.LogParser(LogLevel.WARN, "GameDataService not configured");
return new List<BaseItemType>();
}
// Safe to use _gameDataService here
}if (_gameDataService == null || !_gameDataService.IsConfigured)
{
PaletteStatusText = "Game paths not configured. Set paths in Settings to browse game items.";
return;
}public bool GameResourcesAvailable => _gameDataService?.IsConfigured ?? false;When the user opens a different module, the custom TLK may change.
Trebuchet sets the custom TLK path on RadoubSettings.Instance when loading a module:
private void SyncToRadoubSettings()
{
var settings = RadoubSettings.Instance;
if (!string.IsNullOrEmpty(CustomTlk))
{
var tlkPath = ResolveTlkPath(CustomTlk);
settings.CustomTlkPath = tlkPath ?? "";
}
else
{
settings.CustomTlkPath = "";
}
}Other tools pick up the change via ReloadConfiguration() or by creating a new GameDataService instance.
When a tool opens a module, call ConfigureModuleHaks() to load only the HAK files referenced by that module's IFO HakList. This avoids the performance penalty of scanning all HAK files (80+ files, 15+ seconds):
// After opening a module directory
_gameDataService.ConfigureModuleHaks(moduleDirectory);This reads module.ifo from the directory, extracts the HakList, resolves HAK file paths, and configures the resolver. All caches (2DA, SSF, palette) are cleared since resource resolution order changes.
For tools that need immediate TLK switching without full reload:
service.SetCustomTlk(@"C:\NWN\modules\mymod\customtlk.tlk"); // Load new TLK
service.SetCustomTlk(null); // Clear custom TLKGameDataService caches three data types internally:
| Cache | Key | Behavior |
|---|---|---|
| 2DA files | Case-insensitive name | Thread-safe, negative caching |
| SSF soundsets | Case-insensitive ResRef | Thread-safe, negative caching |
| Palette categories | Resource type (ushort) | Cached per resource type |
Negative caching: When a resource is not found, null is cached to avoid repeated lookups for the same missing resource. This is important for performance when many items reference non-existent 2DA files.
All cache access is lock-protected:
lock (_lock)
{
if (_twoDACache.TryGetValue(name, out var cached))
return cached; // Returns null if previously not found
var twoDA = LoadTwoDA(name);
_twoDACache[name] = twoDA;
return twoDA;
}| Scenario | Method |
|---|---|
| User changes game paths | ReloadConfiguration() |
| User clicks "Clear and Reload" in Settings | ClearCache() |
| Switching files/contexts |
ClearCache() on wrapper services |
| Test cleanup | ClearCache() |
Tools often add their own cache layer on top of GameDataService:
// Tool-level item resolution with its own cache
private readonly Dictionary<string, UtiFile?> _utiCache = new();
public UtiFile? GetItem(string resRef)
{
if (_utiCache.TryGetValue(resRef, out var cached))
return cached;
var data = _gameDataService.FindResource(resRef, ResourceTypes.Uti);
var uti = data != null ? UtiReader.Read(data) : null;
_utiCache[resRef] = uti;
return uti;
}// Pattern: 2DA cell contains a StrRef → resolve via TLK
string? nameStrRef = service.Get2DAValue("baseitems", baseItemIndex, "Name");
if (!string.IsNullOrEmpty(nameStrRef))
{
string? name = service.GetString(nameStrRef);
if (!string.IsNullOrEmpty(name))
return name;
}
// Fallback to label column
string? label = service.Get2DAValue("baseitems", baseItemIndex, "label");
return label ?? $"BaseItem_{baseItemIndex}";var utiData = service.FindResource("nw_wswdg001", ResourceTypes.Uti);
if (utiData != null)
{
var item = UtiReader.Read(utiData);
// Use item
}Use FindBaseResource() when you need the original base game version of a resource, bypassing HAK overrides. This checks Override and BIF only:
// Get the standard race skeleton, even if a CEP HAK replaces it
var skelData = service.FindBaseResource("c_human", ResourceTypes.Mdl);var scripts = service.ListResources(ResourceTypes.Ncs);
foreach (var info in scripts)
{
Console.WriteLine($"{info.ResRef} [{info.Source}]");
}var categories = service.GetPaletteCategories(ResourceTypes.Utc);
foreach (var cat in categories)
{
Console.WriteLine($"[{cat.Id}] {cat.FullPath}");
}string? resRef = service.GetSoundsetResRef(42);
if (resRef != null)
{
SsfFile? ssf = service.GetSoundsetByResRef(resRef);
// Use soundset
}Wrap construction in try/catch. The service can fail if KEY/BIF archives are corrupted or paths are inaccessible:
try
{
_gameDataService = new GameDataService();
}
catch (Exception ex)
{
UnifiedLogger.LogApplication(LogLevel.WARN, $"GameDataService init failed: {ex.Message}");
_gameDataService = null;
}All resource lookups return null for missing data. Never assume a resource exists:
var twoDA = service.Get2DA("racialtypes");
if (twoDA == null)
{
// 2DA file not found in any source - handle gracefully
return;
}GetString(string?) handles "****" and non-numeric values by returning null:
string? text = service.GetString("****"); // null
string? bad = service.GetString("abc"); // null
string? ok = service.GetString("12345"); // Resolved text or nullAlways dispose in tool shutdown:
protected override void OnClosed(EventArgs e)
{
(_gameDataService as IDisposable)?.Dispose();
base.OnClosed(e);
}[Fact]
public void Get2DA_BaseItems_ReturnsData()
{
var config = new GameResourceConfig
{
GameDataPath = TestPaths.GameData,
KeyFilePath = TestPaths.KeyFile,
CacheArchives = true
};
using var service = new GameDataService(config);
var baseItems = service.Get2DA("baseitems");
Assert.NotNull(baseItems);
}If game data isn't available in the test environment, guard tests:
[Fact]
public void GetString_InvalidRef_ReturnsNull()
{
using var service = new GameDataService();
if (!service.IsConfigured)
return; // Skip if no game data
var result = service.GetString("****");
Assert.Null(result);
}Since IGameDataService is an interface, create mock implementations for testing services that depend on it:
public class MockGameDataService : IGameDataService
{
public bool IsConfigured => true;
private readonly Dictionary<string, TwoDAFile> _twoDAs = new();
public TwoDAFile? Get2DA(string name) =>
_twoDAs.TryGetValue(name, out var twoDA) ? twoDA : null;
// Minimal implementations for other methods...
}- IGameDataService-API-Reference - Complete method reference
- Radoub-Formats - Format library overview
- Radoub-UI-Developer - Shared UI components using IGameDataService
Page freshness: 2026-04-18
Getting Started
User Guide
Features
Help
- Manifest - Journal Editor
- Quartermaster - Creature/Inventory Editor
- Relique - Item Editor
- Reliquary - Placeable Editor (Alpha)
- Fence - Merchant/Store Editor
- Trebuchet - Radoub Launcher
- Marlinspike - Search and Replace
- Spell Check - Dictionary-based spell checking
- Token System - Dialog tokens and custom colors
Parley Internals
Manifest Internals
Quartermaster Internals
Relique Internals
Reliquary Internals
Fence Internals
Marlinspike (Search Engine)
Trebuchet Internals
Radoub.UI
Library
Low-Level Formats
High-Level Parsers
- JRL Format (.jrl)
- UTI Format (.uti) - Item blueprints
- UTC Format (.utc) - Creature blueprints
- UTM Format (.utm) - Store blueprints
- UTP Format (.utp) - Placeable blueprints
- UTD Format (.utd) - Door blueprints
- ARE Format (.are) - Area properties
- BIC Format (.bic) - Player characters
Original BioWare Aurora Engine file format specifications.
Core Formats
- GFF Format - Generic File Format
- KEY/BIF Format - Resource archives
- ERF Format - Encapsulated resources
- TLK Format - Talk tables
- 2DA Format - Data tables
- Localized Strings
- Common GFF Structs
Object Blueprints
- Creature Format (.utc)
- Item Format (.uti)
- Store Format (.utm)
- Door/Placeable (.utd/.utp)
- Encounter Format (.ute)
- Sound Object (.uts)
- Trigger Format (.utt)
- Waypoint Format (.utw)
Module/Area Files
- Conversation Format (.dlg)
- Journal Format (.jrl)
- Area File Format (.are/.git/.gic)
- Module Info (.ifo)
- Faction Format (.fac)
- Palette/ITP Format (.itp)
- SSF Format - Sound sets
Reference
Page freshness: 2026-05-24