-
Notifications
You must be signed in to change notification settings - Fork 0
Game Data Integration Guide
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, and palettes. Every Radoub tool follows the same initialization and access pattern.
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— dispose on tool shutdown
Most tools use the parameterless constructor, which reads from RadoubSettings.Instance:
_gameDataService = new GameDataService();Construction does file I/O (scanning KEY/BIF archives, loading TLK), so 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, every tool shares the same paths and TLK settings.
When the user changes game paths:
// 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 IsConfigured before use. It 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;The custom TLK may change when the user opens a different module.
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 the module's IFO HakList. This avoids scanning every HAK file on disk (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 skip repeat lookups. This matters 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 a 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() for the original base-game version, bypassing HAK overrides. 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 fails 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);
}When game data is unavailable 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);
}IGameDataService is an interface, so create mock implementations for 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-05-24
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