Skip to content

Game Data Integration Guide

LordOfMyatar edited this page Feb 14, 2026 · 4 revisions

Game Data Integration Guide

How to 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, 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

Bootstrap

Standard Pattern (Settings-Driven)

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

_gameDataService = new GameDataService();

Async Background Initialization

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

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, all tools share the same paths and TLK settings.

After Settings Change

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.


IsConfigured Guard Pattern

Always check before use. IsConfigured 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

When the user opens a different module, the custom TLK may change.

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.

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 avoid repeated lookups for the same missing resource. This is important for performance 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 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;
}

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
}

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

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

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

Mocking for Service Tests

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

See Also


Home | Index


Page freshness: 2026-02-13


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