VR-Engage  2.2
Loading...
Searching...
No Matches
Player Station Framework

The Player Station Framework provides the core infrastructure for VR-Engage frontend development. It manages the application lifecycle, coordinates component interactions, and provides the foundation for customizing and extending the user experience.

This page describes how to implement frontend plugins and components for VR-Engage.

Unless otherwise noted, the types described here are defined in the makVre namespace and are part of the VR-Engage frontend libraries.

The Player Station Framework organizes frontend functionality into a hierarchy of cooperating classes. At the top, DtPlayerStationApp manages the overall application lifecycle and global services. Below that, DtPlayerStation instances represent individual players, each with their own role, components, and state. Components derived from DtPlayerComponent implement specific functionality like vehicle control, weapon systems, or sensor displays.

Key concepts

  • Player Station App (DtPlayerStationApp): Top-level application controller and customization entry point.
  • Player Station (DtPlayerStation): Represents a single player's session, role, and components.
  • Player Component (DtPlayerComponent): Base class for all frontend functionality units.
  • Role (role Lua files and entity configuration): Defines which components are instantiated for a given player role.
  • Attribute Store (DtAttributeHandle via playerAttributeStore()): Shared key/value store for component interaction and UI binding.

This architecture promotes modularity through composition. Rather than creating monolithic vehicle classes, developers assemble vehicles from discrete components. A tank role might combine DtDriverControlLogic for movement, DtGunnerControlLogic for the main gun, DtCommanderControlLogic for situational awareness, and various sensor and radio components. This approach allows reusing components across vehicle types and testing components in isolation.

Typical workflow

  • The application starts and DtPlayerStationApp initializes global services and loads plugins.
  • Plugins register component types with the component factory.
  • The user connects to a simulation backend and enters the role-selection flow.
  • When a role is selected, a DtPlayerStation is created (or updated) and the configured components are instantiated.
  • Components are initialized (initialize() / postInitialize()) and then updated every frame via tick() while the user is engaged in the role.
  • When the application shuts down or the role is disengaged, player stations and components are shut down and cleaned up.

Core framework classes

DtPlayerStationApp

DtPlayerStationApp serves as the top-level application controller and entry point for frontend customization. It manages global services, plugin loading, and the main update loop. Most developers interact with this class during plugin initialization to register component factories.

// filepath: src/myPlugin.cxx
using namespace makVre;
{
// Register component types with the application-wide factory.
factory.addCreator<DtMyComponent>("DtMyComponent");
factory.addCreator<DtMyOtherComponent>("DtMyOtherComponent");
return true;
}
void addCreator(std::string name="")
Registers a creator for a specific type.
Definition factory.h:112
Top-level class representing the VR-Engage application.
Definition playerStationApp.h:163
virtual DtComponentFactory & componentFactory()
Gets the component factory.
VRECOMMONCOMPONENTS_DLL bool initPlayerStationModule(makVre::DtPlayerStationApp *app)
Module initialization function declarations using C linkage.
Include export definitions for this library.
Definition glsVreMessageUtil.h:49
DtFactory< DtPlayerComponent > DtComponentFactory
Type definitions for factory classes.
Definition playerStationApp.h:120
Defines the DtPlayerStationApp class for the VR-Engage application.

Advanced customization can subclass DtPlayerStationApp to override initialization behavior, add custom services, or modify the update loop. However, most extensions are better implemented as components registered through the standard plugin mechanism.

DtPlayerStation

DtPlayerStation represents a single player's session within the application. It manages the player's current role, instantiated components, and player-specific state. When a user selects a role (such as "Tank Commander"), the player station instantiates the components specified in that role's configuration.

The player station provides access to player-specific services:

// filepath: src/myComponent.cxx
void DtMyComponent::someMethod()
{
// Access the player station.
DtPlayerStation* station = player();
// Get the current role name.
std::string currentRoleName = station->stationName();
// Access the composed role definition if needed.
DtInitTable roleDef = station->definition();
// Access the player-specific attribute store.
auto& attrs = station->playerAttributeStore();
// Find a specific component by type (requires including that component's header).
if (auto* controlLogic = station->findComponent<DtEntityControlLogic>())
{
// Interact with the control logic component.
}
}
Component that provides entity control functionality.
Definition entityControlLogic.h:60
Table-based access to Lua state for configuration data.
Definition initializer.h:38
Class for managing the user interface for engaged roles.
Definition playerStation.h:51
COMP_TYPE * findComponent(const std::string &name="")
Finds a component by type and optional name.
Definition playerStation.h:329
virtual DtAttributeHandle & playerAttributeStore()
Gets the player attribute store.
virtual DtInitTable definition() const
virtual std::string stationName() const
Gets the name of the role.

DtPlayerStationStateManager

The state manager controls high-level application flow through a state machine. These states determine when components are active and what services are available.

stateDiagram-v2
    [*] --> Disconnected
    Disconnected --> Connecting: Connect requested
    Connecting --> Disconnected: Connection failed
    Connecting --> RoleSelection: Connected
    RoleSelection --> Engaged: Role selected
    Engaged --> RoleSelection: Role exited
    RoleSelection --> Disconnected: Disconnect
    Engaged --> Disconnected: Connection lost
  • Disconnected: Initial state with no network connection. Components are not instantiated.
  • Connecting: Establishing connection to the simulation backend. UI shows connection progress.
  • RoleSelection: Connected and displaying available roles. The user can browse and select roles.
  • Engaged: The user is actively controlling an entity. Role components are instantiated and active.

Components specified in a role configuration are instantiated when entering the Engaged state and destroyed when leaving it. Resources are therefore allocated only when needed and properly cleaned up during role transitions.

Role management and loading

Role definitions are discovered and loaded collaboratively by the Lua role loader script and the C++ player creation infrastructure. The loader script (data/factory/scripts/loadDefinition.lua) composes role templates from role files, component groups, and inheritance chains. The DtPlayerCreationPalette class catalogs available entities and roles from simulation model sets, uses the loader to build complete role configurations, and returns a merged DtInitTable when users select roles. For a detailed walkthrough of this composition pipeline, see the "How composition is implemented" section on the Role Configuration page.

Role definition files are Lua scripts that specify which component groups to instantiate and how to configure them. They live under vre-roles-dir, which by default is data/simulationModelSets/VR-Engage/roles:

-- filepath: data/simulationModelSets/VR-Engage/roles/commander.lua
-- Command position in a main battle tank (excerpt)
appModes = {"Ground"};
roles = {"Commander"};
inherits = "@(vre-roles-dir)/platform.lua";
components = {
["controlLogic"] = {
componentType = "DtCommanderControlLogic";
inputConfigFile = "commanderInput.lua";
joystickFunctionGroups = {"Commander View"};
mouseControlSupport = true;
};
["observerUpdater"] = {
componentType = "DtCommanderObserverUpdater";
};
["extendedStateLogic"] = {
componentType = "DtExtendedStateLogic";
};
};

Each component entry maps to a C++ class registered with the component factory (see Role Configuration for full details of the role system and file structure). Keys in the components table (such as configuration parameters) become fields in the DtInitTable passed to the component's initialize() method. For a catalog of configuration parameters supported by built-in toolkit components, see the Doxygen group Role Configuration Parameters.

Player components

DtPlayerComponent is the base class for all frontend components. Custom components inherit from this class and override lifecycle methods to implement their functionality.

Component lifecycle

Components follow a deterministic lifecycle managed by the framework. This lifecycle governs proper resource management and helps avoid common bugs.

stateDiagram-v2
    [*] --> Constructed: Factory creates
    Constructed --> Initialized: initialize() called
    Initialized --> PostInitialized: postInitialize() called
    PostInitialized --> Ticking: tick() called each frame
    Ticking --> PostInitialized: tick() returns
    PostInitialized --> Destroyed: shutdown() called
    Destroyed --> [*]

    note right of Initialized
        Read configuration
        Initialize members
        Validate parameters
    end note

    note right of PostInitialized
        Locate other components
        Acquire shared resources
        Subscribe to messages
    end note

    note right of Destroyed
        Unsubscribe messages
        Release resources
        Save state if needed
    end note
  1. Construction: The component factory creates the instance using the default constructor. Do not perform significant initialization here; the component does not yet have access to configuration or the player station.
  2. Initialization: The framework calls initialize(DtPlayerStation*, DtInitTable&) when creating the component. Use the DtInitTable to read configuration parameters from the role definition and initialize member variables. Return false to indicate failure, which prevents the component from being used.
  3. Post-initialization: After all components have been initialized, the framework calls postInitialize() on each component. Use this phase to locate other components, look up shared attributes in the player attribute store, and subscribe to messages.
  4. Ticking: The framework calls tick(double dt) every frame with the time delta since the last frame. Perform per-frame updates here, such as smoothing input values, updating animations, or refreshing UI state.
  5. Destruction: The framework calls shutdown() before destroying the component or when the player station is shut down. Unsubscribe from messages, release resources, and perform final cleanup.

Implementing a component

A complete component implementation demonstrates the lifecycle methods and common patterns:

// filepath: include/myComponent.h
#pragma once
namespace MyPlugin
{
//! Custom component demonstrating lifecycle patterns.
//! This component computes a simple engine warning flag for the UI.
class DtEngineMonitor : public makVre::DtPlayerComponent
{
public:
DtEngineMonitor();
~DtEngineMonitor() override;
bool initialize(makVre::DtPlayerStation* player, makVre::DtInitTable& config) override;
bool postInitialize() override;
void tick(double dt) override;
void shutdown() override;
const char* type() const override;
private:
void updateWarningState();
// Configuration parameters.
double myWarningRpmThreshold;
// Runtime state.
bool myWarningActive;
};
} // namespace MyPlugin
Base class for all role components in VR-Engage.
Definition playerComponent.h:78
Defines the DtPlayerComponent base class for VR-Engage role components.
// filepath: src/myComponent.cxx
#include "myComponent.h"
#include "vreUtil/logger.h"
using namespace makVre;
namespace MyPlugin
{
namespace
{
// Component type string used for registration and in role configuration.
const char* DtEngineMonitorType = "DtEngineMonitor";
}
DtEngineMonitor::DtEngineMonitor()
, myWarningRpmThreshold(1500.0)
, myWarningActive(false)
{
}
DtEngineMonitor::~DtEngineMonitor() = default;
bool DtEngineMonitor::initialize(DtPlayerStation* player, DtInitTable& config)
{
DtPROFILE("DtEngineMonitor::initialize");
if (!DtPlayerComponent::initialize(player, config))
{
return false;
}
// Read configuration with a default.
myWarningRpmThreshold = config.findDataOr<double>("warningRpmThreshold", myWarningRpmThreshold);
if (myWarningRpmThreshold <= 0.0)
{
LOG_WARN("EngineMonitor") << "warningRpmThreshold must be positive" << std::endl;
myWarningRpmThreshold = 1500.0;
}
// Initialize attributes used by the UI.
playerAttributeStore().setAttribute<int>("engine/rpm", 0);
playerAttributeStore().setAttribute<bool>("engine/warning", false);
return true;
}
bool DtEngineMonitor::postInitialize()
{
// Nothing to resolve from other components in this simple example.
}
void DtEngineMonitor::tick(double dt)
{
DtPROFILE("DtEngineMonitor::tick");
// Read current RPM from the attribute store (for example, set by another component).
int rpm = playerAttributeStore().getAttributeOr<int>("engine/rpm", 0);
myWarningActive = (rpm >= myWarningRpmThreshold);
// Publish the warning flag for the UI.
playerAttributeStore().setAttribute<bool>("engine/warning", myWarningActive);
}
void DtEngineMonitor::shutdown()
{
// No special cleanup required in this example.
}
const char* DtEngineMonitor::type() const
{
return DtEngineMonitorType;
}
} // namespace MyPlugin
RT findDataOr(const std::string &name, RT defaultValue, bool silent=false, bool resolveDots=true)
Definition initializer.h:215
virtual bool postInitialize()
Performs post-initialization setup.
virtual bool initialize(DtPlayerStation *player, DtInitTable &config)
Initializes the component.
virtual void shutdown()
Shuts down the component.
Provides configuration initialization for VREngage components.
Provides logging functionality with various severity levels and channels.
#define LOG_WARN(channel)
Macro to log a warning message to log files.
Definition logger.h:69
Defines the DtPlayerStation class for managing engaged roles.

Configuration sources

Component configuration originates from multiple sources, with later sources overriding earlier ones (the merge process is described in more detail on the Role Configuration page):

  1. Hardcoded defaults: Default values in the C++ code provide baseline behavior.
  2. Component group parameters: Parameters in component-group Lua files customize component types.
  3. Role parameters: Parameters in role definitions customize specific roles.
  4. User preferences: Runtime user settings can override configuration values.
-- filepath: data/simulationModelSets/VR-Engage/roles/sampleVehicle.lua
-- Example role overriding the engine monitor configuration
components = {
["engineMonitor"] = {
componentType = "DtEngineMonitor";
-- Override for a vehicle with a higher normal RPM range.
warningRpmThreshold = 2200.0;
};
};

Fields in the Lua components tables become entries in the DtInitTable passed to the component's initialize() method. For example, warningRpmThreshold in the Lua configuration is read via config.findDataOr<double>("warningRpmThreshold", 1500.0) in the C++ component.

Registration and configuration recap

Putting it together, the typical flow for a custom component is:

  1. C++ type identifier: The component implements const char* type() const and returns a stable type string (for example, "DtEngineMonitor").
  2. Plugin registration: The plugin's initPlayerStationModule() function registers the component with the application-wide factory using the same type string:

    factory.addCreator<DtEngineMonitor>("DtEngineMonitor");
  3. Role configuration: Role and component-group Lua files use that type string in the componentType field:

    components = {
    ["engineMonitor"] = {
    componentType = "DtEngineMonitor";
    warningRpmThreshold = 1500.0;
    };
    };
  4. Initialization: When the role is engaged, the framework creates a DtEngineMonitor instance, calls initialize(DtPlayerStation*, DtInitTable&) with a merged DtInitTable containing values from component defaults, component groups, role files, and entity overrides, and then calls postInitialize() before beginning regular tick() updates.

Component design

Design components with single, well-defined responsibilities. A component that combines unrelated functionality becomes difficult to test, configure, and reuse. If a component requires conditional behavior based on role, consider splitting it into separate components composed through configuration. For example, avoid a single component that implements both complex HUD rendering and weapon control; instead, separate these concerns into independent components.

Minimize dependencies between components. Components that directly reference other components create coupling that complicates testing and limits reusability. Use message passing and the player attribute store for component interaction instead of direct references.

Threading considerations

The frontend process uses two primary threads that affect component implementation:

Main thread executes rendering, component updates, and UI event processing. Per-frame component tick() methods, input handlers, and most message callbacks run in the main thread. Components that block this thread cause frame rate drops and input latency. Long-running operations should execute asynchronously with results delivered via message callbacks.

Network thread handles communication with the backend process. Network message reception, serialization, and initial processing occur on this thread. The message manager automatically marshals messages to the main thread for component delivery, ensuring thread-safe callback execution. Components rarely interact directly with the network thread.

Component developers work primarily with the main thread. Message subscriptions, attribute store access, and framework service calls execute on the main thread unless explicitly documented otherwise. Avoid blocking operations in message callbacks and tick() methods.

Shared state and synchronization

Components that create worker threads must implement explicit synchronization when accessing shared state. The framework provides no automatic thread safety. Use standard C++ synchronization primitives (such as mutexes and condition variables) to protect shared data structures accessed from multiple threads. Document thread-safety requirements clearly in component interfaces.

Performance considerations

Frontend components execute on the main rendering thread, so unnecessary work directly impacts frame rate and input latency. Keep tick() implementations lightweight and avoid per-frame allocations, complex container operations, or blocking I/O. Prefer incremental updates and caching over recomputing expensive results every frame. Minimize logging in hot paths and use debug-only logging guards for verbose output. Avoid long-running work in message callbacks or QML property notifications; offload heavy computation to worker threads and return results via messages or attributes.

Player attribute store

The Player Attribute Store provides frontend-local hierarchical state storage shared by all components and UI for a single player station. Unlike messages that represent discrete events, attributes represent persistent state that components can query at any time. The store supports change notifications, allowing reactive programming patterns where components respond to state changes rather than polling.

Attribute basics

Attribute hierarchy

Within a player station, attributes are organized in a tree structure similar to a file system. Navigation uses the bracket operator ([]) to traverse the hierarchy, with each bracket accessing a child node by name. This organization groups related attributes logically and supports efficient subtree operations like iteration.

Root (per-player)
├── position
│ ├── x
│ ├── y
│ └── z
├── orientation
│ ├── heading
│ ├── pitch
│ └── roll
├── systems
│ ├── engine
│ │ ├── rpm
│ │ ├── temperature
│ │ └── fuelFlow
│ ├── transmission
│ │ ├── gear
│ │ └── clutchEngaged
│ └── weapons
│ ├── selectedIndex
│ ├── armed
│ └── safetyOn
└── controls
├── throttle
├── brake
└── steering

Path-based access supports both specific attribute queries and subtree iteration. Components navigate the hierarchy using chained bracket operators (for example, attrs["systems"]["engine"]["rpm"] or attrs["controls"]["throttle"]).

Basic attribute operations

The attribute store provides type-safe accessors for reading and writing values. The API supports common types including booleans, integers, floating-point numbers, strings, and vectors.

// filepath: src/engineComponent.cxx
void DtEngineComponent::updateEngineState(double deltaTime)
{
DtAttributeHandle attrs = playerAttributeStore();
// Read control inputs
double throttle = attrs["controls"]["throttle"]->getOr<double>(0.0);
bool ignitionOn = attrs["systems"]["engine"]["ignitionOn"]->getOr<bool>(false);
// Calculate engine state
if (ignitionOn && myEngineRunning)
{
myCurrentRpm = calculateRpm(throttle, myCurrentRpm, deltaTime);
myFuelFlow = calculateFuelFlow(myCurrentRpm);
myTemperature = calculateTemperature(myCurrentRpm, myTemperature, deltaTime);
}
else
{
myCurrentRpm = 0.0;
myFuelFlow = 0.0;
myTemperature = coolDown(myTemperature, deltaTime);
}
// Write updated state
attrs["systems"]["engine"]["rpm"]->set<double>(myCurrentRpm);
attrs["systems"]["engine"]["fuelFlow"]->set<double>(myFuelFlow);
attrs["systems"]["engine"]["temperature"]->set<double>(myTemperature);
attrs["systems"]["engine"]["running"]->set<bool>(myEngineRunning);
}
Provides attribute handling system for hierarchical data storage and manipulation.
Handle class for safe access to attributes.
Definition attributeHandle.h:30
bool set(const T &value)
Sets the value of the attribute.
T getOr(T orValue) const
Gets the value of the attribute or a default value if undefined.

The getOr<T>() method provides default values when attributes are unset, simplifying initialization and handling missing data gracefully. For attributes that must exist, the get<T>() method asserts if the attribute is not set or if the type does not match.

Advanced access patterns

Typed handles for performance

String-based path lookups are convenient but incur overhead from string parsing and tree traversal. For attributes accessed frequently (such as every frame), typed handles provide cached direct access that bypasses the path resolution.

// filepath: src/engineComponent.h
#pragma once
class DtEngineComponent : public makVre::DtPlayerComponent
{
public:
bool initialize(DtPlayerStation* player, DtInitTable& config) override;
void tick(double deltaTime) override;
private:
void updateEngineState(double deltaTime);
double calculateRpm(double throttle, double currentRpm, double dt);
// Typed handles for high-frequency access
DtAttributeHandleType<double> myTemperatureHandle;
DtAttributeHandleType<bool> myRunningHandle;
// State
double myCurrentRpm = 0.0;
double myFuelFlow = 0.0;
double myTemperature = 20.0;
bool myEngineRunning = false;
};
Provides handle classes for safe access to attributes.
Type-specific handle class for attributes.
Definition attributeHandle.h:181
virtual void tick(double dt)=0
Updates the component.
// filepath: src/engineComponent.cxx
bool DtEngineComponent::initialize(DtPlayerStation* player, DtInitTable& config)
{
if (!DtPlayerComponent::initialize(player, config))
{
return false;
}
DtAttributeHandle attrs = playerAttributeStore();
// Initialize handles once during setup - use [] to navigate hierarchy
// and .as<T>() to create typed handles for frequent access
myThrottleHandle = attrs["controls"]["throttle"].as<double>();
myRpmHandle = attrs["systems"]["engine"]["rpm"].as<double>();
myFuelFlowHandle = attrs["systems"]["engine"]["fuelFlow"].as<double>();
myTemperatureHandle = attrs["systems"]["engine"]["temperature"].as<double>();
myRunningHandle = attrs["systems"]["engine"]["running"].as<bool>();
// Set initial values using typed handle assignment
myRpmHandle = 0.0;
myFuelFlowHandle = 0.0;
myTemperatureHandle = 20.0;
myRunningHandle = false;
return true;
}
void DtEngineComponent::tick(double deltaTime)
{
// Read using typed handles - no path parsing overhead
// Typed handles support direct conversion to their value type
double throttle = myThrottleHandle;
// Calculate state
if (myEngineRunning)
{
myCurrentRpm = calculateRpm(throttle, myCurrentRpm, deltaTime);
// ... other calculations
}
// Write using typed handles - direct assignment
myRpmHandle = myCurrentRpm;
myFuelFlowHandle = myFuelFlow;
myTemperatureHandle = myTemperature;
}
DtAttributeHandleType< T > as()
Converts this handle to a typed handle.

Handle-based access is significantly faster than path-based access for high-frequency operations. The initial handle creation performs the path resolution once, caching the result for subsequent access. Handles remain valid as long as the attribute exists in the store.

Change notifications

Components can register callbacks to receive notifications when specific attributes change. This allows reactive patterns where components respond to state changes rather than polling for updates.

// filepath: src/temperatureGauge.h
#pragma once
class DtTemperatureGaugeComponent : public makVre::DtPlayerComponent
{
public:
bool initialize(DtPlayerStation* player, DtInitTable& config) override;
void shutdown() override;
private:
void onTemperatureChanged(double newTemperature);
void onWarningThresholdChanged(double newThreshold);
void updateWarningState();
// Attribute handles for monitored values
DtAttributeHandle myTemperatureAttr;
DtAttributeHandle myWarningThresholdAttr;
double myCurrentTemperature = 0.0;
double myWarningThreshold = 90.0;
bool myWarningActive = false;
};
Provides a callback mechanism for attribute changes in the VREngage system.
// filepath: src/temperatureGauge.cxx
#include "temperatureGauge.h"
bool DtTemperatureGaugeComponent::initialize(DtPlayerStation* player, DtInitTable& config)
{
if (!DtPlayerComponent::initialize(player, config))
{
return false;
}
// Get handles to the attributes we want to monitor
myTemperatureAttr = playerAttributeStore()["systems"]["engine"]["temperature"];
myWarningThresholdAttr = playerAttributeStore()["settings"]["temperatureWarningThreshold"];
// Initialize threshold from current value or default
myWarningThreshold = myWarningThresholdAttr->getOr<double>(90.0);
// Register callbacks using the callback manager
// The manager tracks all connections for automatic cleanup
myAttributeCallbacks.connect(
myTemperatureAttr,
this,
&DtTemperatureGaugeComponent::onTemperatureChanged);
myAttributeCallbacks.connect(
myWarningThresholdAttr,
this,
&DtTemperatureGaugeComponent::onWarningThresholdChanged);
return true;
}
void DtTemperatureGaugeComponent::onTemperatureChanged(double newTemperature)
{
myCurrentTemperature = newTemperature;
updateGaugeDisplay(newTemperature);
updateWarningState();
}
void DtTemperatureGaugeComponent::onWarningThresholdChanged(double newThreshold)
{
myWarningThreshold = newThreshold;
updateWarningState();
}
void DtTemperatureGaugeComponent::updateWarningState()
{
bool shouldWarn = myCurrentTemperature > myWarningThreshold;
if (shouldWarn != myWarningActive)
{
myWarningActive = shouldWarn;
if (shouldWarn)
{
activateWarningIndicator();
}
else
{
deactivateWarningIndicator();
}
}
}
void DtTemperatureGaugeComponent::shutdown()
{
// Disconnect all callbacks to prevent dangling pointers
// The callback manager handles bulk cleanup automatically
myAttributeCallbacks.disconnectAll();
}

Change callbacks fire synchronously when attributes are modified, enabling immediate response to state changes. For attributes that change frequently, consider batching updates or using rate limiting to avoid excessive callback invocations.

Usage guidelines

UI and QML integration

The player attribute store is the primary bridge between simulation logic and UI. HUD elements and QML overlays bind to attribute paths (for example, attrs["systems"]["engine"]["rpm"] or attrs["systems"]["weapons"]["armed"]) and update automatically when those attributes change. Component code is responsible for publishing clean, stable attribute names and values; UI code should avoid performing complex simulation logic and instead react to attributes maintained by components.

Subtree operations

The attribute store supports operations on entire subtrees, enabling efficient enumeration and bulk operations.

// filepath: src/diagnosticsComponent.cxx
void DtDiagnosticsComponent::logSystemState()
{
DtAttributeHandle systems = playerAttributeStore()["systems"];
LOG_INFO("Diagnostics") << "=== System State ===" << std::endl;
// Iterate all direct children of the systems attribute
systems->forEach(
{
LOG_INFO("Diagnostics") << attr->path() << " = " << attr->getAsString() << std::endl;
});
LOG_INFO("Diagnostics") << "===================" << std::endl;
}
void DtDiagnosticsComponent::resetAllSystems()
{
DtAttributeHandle systems = playerAttributeStore()["systems"];
// Unset all attributes under systems/ recursively and set defaults
systems->unsetRecursive();
// Re-initialize default values
systems["engine"]["rpm"]->set<double>(0.0);
systems["engine"]["temperature"]->set<double>(20.0);
systems["engine"]["running"]->set<bool>(false);
systems["transmission"]["gear"]->set<int>(0);
systems["weapons"]["armed"]->set<bool>(false);
}
std::string getAsString() const
Gets string representation of the attribute value.
virtual std::string path() const
Returns the full path of this attribute in the hierarchy.
bool unsetRecursive()
Unsets this attribute and all child attributes, recursively.
void forEach(std::function< void(DtAttributeHandle)> forEachFun)
#define LOG_INFO(channel)
Macro to log an informational message to log files.
Definition logger.h:74

Attribute naming conventions

When defining attributes for a player station, follow these guidelines to keep the store predictable and easy to consume from UI and other components:

  • Use hierarchical organization that groups related data (e.g., ["systems"]["engine"]["rpm"], ["controls"]["throttle"], ["ui"]["warnings"]["engine"]).
  • Prefer stable, lowerCamelCase leaf names so bindings remain valid across releases.
  • Keep attribute semantics simple: one well-defined value per path, rather than overloading a single path with multiple meanings.
  • Publish derived or display-ready values (such as percentages or clamped status flags) when the UI needs them, instead of duplicating the same calculations in QML.

Lifetime and threading

Each DtPlayerStation owns a single player attribute store instance that is created when the station is created and cleared when the station shuts down or the role changes. The store is a frontend-only data structure; it does not perform any network synchronization or conflict resolution on its own.

Attribute access and change callbacks occur on the main thread where components run. Components that create worker threads must use explicit synchronization when they read or write attributes from those threads. The framework does not provide automatic thread safety for attribute operations.

See also