Skip to content

Game Data Integration Guide

LordOfMyatar edited this page May 24, 2026 · 4 revisions

Game Data Integration Guide

Bootstrap, configure, and use IGameDataService in Radoub tools.


Table of Contents


Overview

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

Bootstrap

Standard pattern (settings-driven)

Most tools use the parameterless constructor, which reads from RadoubSettings.Instance:

_gameDataService = new GameDataService();

Async background initialization

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;
    }
}

Explicit configuration (tests)

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);

Settings integration

How RadoubSettings maps to GameResourceConfig

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

Settings persistence

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.

After a settings change

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.


IsConfigured guard pattern

Always check IsConfigured before use. It is false when game paths are missing or invalid.

Property guard

private IGameDataService GameData => _gameDataService
    ?? throw new InvalidOperationException("GameDataService not initialized");

Method-level guard

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
}

UI state binding

if (_gameDataService == null || !_gameDataService.IsConfigured)
{
    PaletteStatusText = "Game paths not configured. Set paths in Settings to browse game items.";
    return;
}

Null-conditional for optional features

public bool GameResourcesAvailable => _gameDataService?.IsConfigured ?? false;

Module context switching

The custom TLK may change when the user opens a different module.

Trebuchet pattern

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.

Module-aware HAK scanning

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.

Direct SetCustomTlk

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 TLK

Caching

Internal cache behavior

GameDataService 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.

Thread safety

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;
}

When to clear cache

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()

Wrapper service caching

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;
}

Common usage recipes

Resolve a name from 2DA + TLK

// 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}";

Load a resource by ResRef

var utiData = service.FindResource("nw_wswdg001", ResourceTypes.Uti);
if (utiData != null)
{
    var item = UtiReader.Read(utiData);
    // Use item
}

Load a base-game resource (skip HAKs)

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);

List all resources of a type

var scripts = service.ListResources(ResourceTypes.Ncs);
foreach (var info in scripts)
{
    Console.WriteLine($"{info.ResRef} [{info.Source}]");
}

Get palette categories for a type

var categories = service.GetPaletteCategories(ResourceTypes.Utc);
foreach (var cat in categories)
{
    Console.WriteLine($"[{cat.Id}] {cat.FullPath}");
}

Resolve a soundset

string? resRef = service.GetSoundsetResRef(42);
if (resRef != null)
{
    SsfFile? ssf = service.GetSoundsetByResRef(resRef);
    // Use soundset
}

Error handling

Construction failures

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;
}

Missing resources

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;
}

Invalid StrRef values

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 null

Disposal

Always dispose in tool shutdown:

protected override void OnClosed(EventArgs e)
{
    (_gameDataService as IDisposable)?.Dispose();
    base.OnClosed(e);
}

Testing

Unit tests with explicit config

[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);
}

Testing without game data

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);
}

Mocking for service tests

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

See also


Home | Index


Page freshness: 2026-05-24


Parley

Getting Started

User Guide

Features

Help


Manifest


Quartermaster


Relique


Reliquary


Fence

  • Fence - Merchant/Store Editor

Trebuchet


Shared Features


Developers

Parley Internals

Manifest Internals

Quartermaster Internals

Relique Internals

Reliquary Internals

Fence Internals

Marlinspike (Search Engine)

Trebuchet Internals

Radoub.UI


Radoub.Formats

Library

Low-Level Formats

High-Level Parsers


Legacy Bioware Docs

Original BioWare Aurora Engine file format specifications.

Core Formats

Object Blueprints

Module/Area Files

Reference


Page freshness: 2026-05-24

Index

Clone this wiki locally