-
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.
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
}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-02-13
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