1. Introduction

Disorder is a practical and modern C++ implementation of the IEEE 1278.1 Standard for Distributed Interactive Simulation (DIS) Application Protocols.

1.1. DIS is dead! Long live DIS!

The NATO Standardization Agency (NSA) retired DIS in favor of High Level Architecture (HLA) in 1998 and officially canceled DIS in 2010. Why then would any sane individual or company bother making a DIS library?

The truth, as anyone in the simulation industry will tell you, is that HLA has a dirty little secret. Its specification is so intentionally nebulous regarding what information is communicated between simulations, that the vast majority of HLA applications wind up using the precisely specified DIS PDUs to communicate state information.

This is probably why the DIS standard continues to evolve past its official death. A new version of the application protocol standard was released in 2012, two years after it died.

Disorder does not directly support HLA. However, Disorder’s PDU transport mechanism can be extended, without significant suffering, to send and receive DIS PDUs via the desired HLA implementation.

1.2. Motivation

There is a distinct lack of industrial strength C++ open source DIS libraries in the world. This thing attempts to fill that void with a well documented, C++ only, robust library with a very liberal licensing model.

The library is developed using a test driven model where repeatable and extensive tests are developed along side the library. The test suite comes with the library and can be run against the library at will.

The library was developed and is actively maintained by a small business called Squall Line Software, LLC. In many ways, it is an experiment with an alternative business model. Squall Line Software does not intend to produce closed source extensions or a "better" version of the software that requires payment. The product, in its entirety, is what you get when you download it.

What Squall Line Software sells is expert consulting services for customer specific applications of the library. Squall Line Software can help integrate disorder into your simulation product, or use disorder to produce a custom simulator for you. Squall Line Software also provides support contracts for disorder to ensure that customer needs are addressed in a timely manner should questions or problems arise.

Please direct all inquires related to disorder to disorder@squalllinesoftware.com.

2. Legalities

2.1. Disorder Library

The Disorder library is free, open source, and released under the comprehensible and liberal terms of the MIT License.

Yes, you can use Disorder in your overpriced commercial product that brings you wealth beyond belief without offering monetary or other reciprocation of any kind. You aren’t even required to feel guilty about doing such things.

2.2. Book of Disorder

This book is licensed under the terms of the Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc-nd/3.0/ or send a letter to Creative Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA.

3. Intended Audience

This library is intended for use in commercial simulation products as well as open-source simulation environments. It may also be of use to hobbyists, tinkerers, and educators.

This book is intended for software developers interested in using Disorder in their project. It is assumed that readers have a basic general knowledge DIS. It is also assumed that readers are well versed in C++ including the 2011 standard. This book provides a general overview of what the library can do and how to compile and install it. For a more precise and detailed description of library’s API, consult the Disorder API Reference Manual.

4. Features

Wouldn’t you rather invest time and effort on actually making a simulator instead of creating infrastructure to allow it to play well with others?

This library is not a cute experiment with XML or multi-language code generation. It is an industrial strength, practical, and modern C++ only library built for performance, stability, convenience, extensibility, and configurability.

Some of the main features of Disorder are:

  • full support for 1278.1-1995 PDUs

  • built-in simulation entity management

  • built-in dead reckoning

  • geospatial coordinate conversion

  • extensible framework for logging, PDU manipulation and transmission, defining and handling custom PDUs, etc.

  • optionally uses multiple threads to take advantage of modern multi-core CPUs

  • included SISO compliant enumerations and bit fields headers

  • leverages the goodness provided by other open source projects including ASIO, SEDRIS, Eigen, GeographicLib, Google Test/Mock, and Meson Build.

  • extensive test suite to prove correctness and robustness across versions and platforms

  • good documentation

5. Installation

6. Getting Started

Enough nonsense, let the hacking begin!

6.1. Disordered Hello World

Let’s start with the obligatory tradition of questionable usefulness. This stupid example sends a single Comment PDU containing the traditional text and then exits. Doing such a thing is not exactly productive, but hopefully you’ll learn something from this example anyway.

6.1.1. Show me the code!

hello_world.cpp
// Copyright (c) 2011-2018 Squall Line Software, LLC
// Distributed under the terms of the MIT License
// See accompanying LICENSE.txt file or
// http://www.opensource.org/licenses/mit-license.php

#include <disorder/exercise.hpp>

#include <disorder/pdu/transport/make_udp.hpp>

#include <disorder/log/log.hpp>
#include <disorder/log/synchronous_file_scribe.hpp>

#include <disorder/pdu/family/simulation_management/comment.hpp>

#include <cstdlib>

//----------------------------------------------------------------------------
int main(int argc, char** argv)
{
    disorder::log::Log::instance().threshold(
        disorder::log::Significance::DEBUG); (1)

    disorder::log::Log::instance().scribe(
        new disorder::log::SynchronousFileScribe("hello_world_log.txt")); (2)

    disorder::Exercise::instance().add(
        disorder::pdu::transport::make_udp(
            "localhost", "3000",
            "localhost", "3000")); (3)

    int result = EXIT_SUCCESS;

    if (disorder::Exercise::instance().initialize(
            disorder::SimulationAddress(2, 3))) (4)
    {
        disorder::pdu::Comment(
            siso::VariableRecordType::EXERCISE_DESCRIPTION,
            "Hello world!").send(); (5)

        disorder::Exercise::instance().update(); (6)
    }
    else
    {
        DISORDER_LOG_E("Failed to initialize Disorder"); (7)
        result = EXIT_FAILURE;
    }

    disorder::Exercise::destroy(); (8)

    return result;
}
1 Tell disorder to log everything with a significance of DEBUG or greater (all logging enabled) (default is WARNING)
2 Tell disorder to log using a synchronous file scribe writing to hello_world_log.txt in the present working directory
3 Tell disorder to send/receive PDUs using a UDP transport on port 3000 of the localhost loopback device (Oh, what masterful irony!)
4 Initialize disorder with a DIS site identifier of 2 and a DIS application identifier of 3. Note that a DIS exercise identifier of 1 is implied here. If another exercise identifier is desired, there’s another overload of Exercise::initialize that accepts an exercise identifier.
5 Construct and send a DIS Comment PDU containing Hello world!. The siso::DatumId::EXERCISE_DESCRIPTION parameter is the datum identifier and it isn’t significant except that it specifies a variable datum of unspecified length. Many other options are available for this PDU, see the Disorder API Reference Manual or the SISO-REF-010 for more details.
6 Tell disorder to perform periodic processing.
7 This macro sends an error message to disorder’s logging infrastructure indicating failure to initialize disorder
8 Orderly shutdown the disorder library.

This example can be found in the source tree and is compiled with the library. The executable is hiding in bin/<variant>/examples/hello_world within a compiled disorder source tree.

6.1.2. What just happened?

The PDU sent by this stupid example can be captured using the awesome and infinitely useful wireshark tool. The specifics of how to do that are outside the scope of this document, but such an experiment may yield something similar to the following:

Wiresharked Hello World

As clearly shown, the Comment PDU was sent and it contains the obligatory text with the specified datum identifier. Perhaps more interestingly, Disorder filled out many of the PDU fields automatically and assumed a few things along the way that may not be correct.

  • All of the header fields were filled out automatically by the library.

    • The protocol version and exercise identifier come from the configuration kept by disorder::Exercise and are set when the PDU is sent.

    • The PDU type and family are set automatically when the PDU is constructed.

    • The timestamp comes from disorder::time::Clock and is applied when the PDU is sent.

    • The PDU length is computed automatically even for variable length PDUs such as this one when the PDU is sent.

  • If not specifically set by the user, the library fills out the originating entity identifier for all simulation management PDUs when they are sent. It does not specify the entity identifier portion however, so it may be advisable to explicitly set this field to a specific entity where appropriate.

  • It’s probably an error to not specify the receiving entity identifier explicitly. Disorder will log a warning about this, and then cheerfully set the field such that the message is sent to all entities. Telling everyone is probably better than telling nobody as default behavior, but one should explicitly set this field.

  • Less impressively, the length of the variable datum field was automatically inferred from the specified content.

Also of note here is that Disorder automatically padded the PDU’s variable datum field to achieve the 64-bit alignment required by the DIS standard. The actual value is 12 bytes (96 bits), but the field was padded to occupy 16 bytes (128 bits) to satisfy the standard.

One can also see that IPv6 was used for the transmission. This is a result of localhost resolving to an IPv6 address and an IPv4 address on the test machine. While IPv6 addresses are preferred when something ambiguous is specified, Disorder happily accepts IPv4 addresses also.

7. General Specifics

7.1. Logging

Disorder provides a configurable and extensible logging infrastructure. The library makes heavy use of this infrastructure internally. When something goes wrong, the log is your best friend in figuring out what happened.

The disorder log can be extended without much effort to route log messages to a simulation application’s existing logging mechanism. See the Log Scribes section for information on how to get that done.

Several significance levels are employed to allow for only as much detailed information to be logged as desired. The logging threshold is defaulted to WARNING which causes everything with WARNING significance or greater to be logged. Everything with a lesser significance is discarded. The log threshold can be adjusted on the fly.

To set the logging threshold, just call threshold on the disorder::log::Log instance supplying the desired threshold. For example, to set the threshold to DEBUG:

disorder::log::Log::instance().threshold(
        disorder::log::Significance::DEBUG);

The possible set of significances are defined in the Significance enum within the disorder::log namespace. Disorder currently supports the following log significances:

/// possible criticalities for log messages.  The elements of this enum
/// are defined in ascending order of significance.
enum class Significance: std::uint8_t
{
    DEBUG,
    INFORMATION,
    WARNING,
    ERROR
};

7.1.1. Macros

The preferred way to send stuff to Disorder’s logging system is via the provided set of macros for logging stuff with different significance levels. The macros supply the logger with file name and line number information along with the significance level and message.

The most common logging macros are DISORDER_LOG_D, DISORDER_LOG_I, DISORDER_LOG_W, and DISORDER_LOG_E. The only difference between them is the significance level which is denoted by the last character. These macros only take one argument, the message to log, and work with a variety of types of things. Some examples include:

// The logging macros accept string literals.
DISORDER_LOG_I("I shot the sheriff, but I did not shoot the deputy.");

// The macros also accept std::string variables.
std::string some_message;
...
DISORDER_LOG_D(some_message);

// The macros even allow streaming things to construct messages on the fly.
DISORDER_LOG_E(
    "Oh, no!  The "
    << that_which_is_ruined
    << " is ruined!  Why did you do that?");

// It's safe to use the macros in a dangling conditional but you are dirty if you do this kind of thing:
if (something_bad_happened)
        DISORDER_LOG_E("Something bad happened!");
The logging macros don’t evaluate the message expression unless the message is actually going to be logged. That means the overhead of string insertion and conversion of data types to string is completely avoided when the logging significance threshold is above the log level of the log statement.

Logging macros are also provided for less common use cases. DISORDER_LOG can be used to log a message of any significance where one must supply said significance. Even though disorder prepends the proper namespace scoping to the significance value, it’s probably more convenient to use the aforementioned set of macros that include the significance in their name. Nevertheless, DISORDER_LOG can be employed thusly:

DISORDER_LOG(DEBUG, "How did that happen?");
DISORDER_LOG(ERROR, "The " << that_which_exploded << " just exploded.  Run for your lives!");

DISORDER_NAKED_LOG is provided for cases where Disorder’s automatic significance namespace scope prefixing is undesirable. For example, this is useful if the significance happens to be kept in a variable:

disorder::log::Significance give_a_shit_level;
...
DISORDER_NAKED_LOG(give_a_shit_level, "She just broke up with me.");

7.1.2. Reading the Logs

Here is some example content from a Disorder log file:

[2013-Dec-28 22:15:54.140916]<I>(139789475452736): Timestamp mode is RELATIVE with an epoch of 2013-Dec-29 03:15:54.140876. (../../src/disorder/time/clock.cpp:184)
[2013-Dec-28 22:15:54.140963]<D>(139789475452736): No spatial reference frame convertor was assigned to disorder::spatial::ReferenceFrame prior to calling disorder::Exercise::initialize.  Using the default SEDRIS SRM based convertor. (../../src/disorder/exercise.cpp:155)
[2013-Dec-28 22:15:54.140990]<I>(139789475452736): No flattened coordinate system is configured.  This is not a problem, but using it in this state is. (../../src/disorder/geospatial/reference_frame.cpp:86)
[2013-Dec-28 22:15:54.141045]<D>(139789475452736): Using the default active PDU disseminator. (../../src/disorder/exercise.cpp:243)
[2013-Dec-28 22:15:54.141127]<I>(139789475452736): UDP transport's local endpoint is bound to [::1]:3000 (../../src/disorder/pdu/transport/udp_transport.cpp:141)
[2013-Dec-28 22:15:54.141174]<D>(139789475452736): UDP transport's receive buffer is 140000 bytes (../../src/disorder/pdu/transport/udp_transport.cpp:193)
[2013-Dec-28 22:15:54.141213]<I>(139789475452736): UDP transport's remote endpoint is [::1]:3000 (../../src/disorder/pdu/transport/udp_transport.cpp:211)
[2013-Dec-28 22:15:54.141312]<D>(139789475452736): Using the default 1995 entity publisher (../../src/disorder/entity/manager.cpp:184)
[2013-Dec-28 22:15:54.141366]<W>(139789475452736): A PDU of type 22 is being sent with an UNKNOWN receiving_entity_id field.  This would cause the PDU to go nowhere fast, so disorder is helping you by setting it to ALL.  You really should fix the thing that is sending this abomination because this switcharoo may not be what you intended. (../../src/disorder/pdu/simulation_management.cpp:46)
[2013-Dec-28 22:15:54.141648]<I>(139789475452736): Shutting down (../../src/disorder/log/log.cpp:57)

Each message is composed of various parts from left to right:

  • The first part within square brackets [] is the timestamp of the message in GMT including fractional seconds.

  • The second part within angled brackets <> is the significance of the message where the letter is the first character of the significance’s name.

  • The third part within parenthesis () is the identifier of the thread that sent the message.

  • The fourth part is after the colon : and is the actual log message.

  • Last, but certainly not least, the fifth part is after the message within parenthesis () and shows the source file and line number where the message came from.

7.2. PDU Transmission

The actual work of sending and receiving PDUs is performed by one or more configurable and extendible PDU transports that are registered with disorder::Exercise. All of the UDP transports that are shipped with disorder perform their work asynchronously. That is, the caller’s thread of execution is not blocked to transmit or receive a PDU.

7.2.1. Configuring PDU Transports

In order to transmit and receive PDUs, one or more PDU transports have to be registered with disorder::Exercise. One or more of the transports that come with disorder can be employed and/or user defined transports can be used. PDUs are sent and received using all registered transports unless the transports themselves employ some kind of filtering.

There is no default PDU transport. If at least one PDU transport is not registered with disorder::Exercise no PDUs will be transmitted or received. A log message will be generated in this case, but it isn’t treated as a fatal error so disorder::Exercise::initialize will not fail as a result of not registering a PDU transport.

It’s usually a one-liner to register a PDU transport. Here’s an example:

disorder::Exercise::instance().add( (1)
    new disorder::pdu::NetworkTransport( (2)
        new disorder::network::UDPBroadcastTransport( (3)
            "0.0.0.0", "3000", (4)
            "192.168.1.255", "3000", (5)
            disorder::network::Transport::ProcessingModel::PASSIVE))); (6)
1 Transports are added by calling add on the disorder::Exercise instance.
2 Start by making a disorder::pdu::NetworkTransport which requires a disorder::network::Transport to perform the actual network transmission work.
3 In this case, the built-in UDP broadcast network transport is used.
4 The first two parameters are the local host and port to bind to.
5 The next two parameters are the remote host and port to send to.
6 This transport is configured to use the PASSIVE processing model which makes it only receive and send PDUs when disorder::Exercise::update is called.
Network transports that come with disorder all provide a processing model constructor parameter and default to PASSIVE. An ACTIVE model is also available in which the transport spawns its own thread that is dedicated to performing the I/O to send and receive network packets.

The processing model of the transport itself does not control which thread invokes registered PDU receivers. That is the job of the PDU disseminator. It has similar processing model configuration options which are described in the Configuring the PDU Disseminator section.

See PDU Transports for information on making custom transports.

7.2.2. Configuring the PDU Disseminator

All received PDUs are routed from PDU transports to the PDU disseminator. The disseminator is responsible distributing received PDUs to the rest of the simulation application. As described in the Receiving PDUs section, a simulation application registers receivers for PDUs it is interested in with the disseminator and the disseminator then invokes appropriate registered receivers when a PDU is received.

Like PDU transports, the PDU disseminator supports passive and active processing models. By default, the PDU disseminator is active causing it to spawn its own dedicated thread which invokes registered PDU receivers. That makes all simulation applications that use Disorder multi-threaded by default. While that is generally useful, some closed minded people may consider this an abomination so disorder allows the disseminator to easily be switched to a passive processing model where it only invokes registered PDU receivers when disorder::Exercise::update is called using the thread that calls disorder::Exercise::update. Here’s how to do that:

#include <disorder/pdu/disseminator.hpp> (1)
#include <disorder/exercise.hpp> (1)
...
disorder::Exercise::instance().pdu_disseminator(
    new disorder::pdu::PassiveDisseminator()); (2)
...
disorder::Exercise::instance().initialize(123, 456, 78); (3)
1 required #includes
2 tell disorder to use the passive PDU disseminator (this must be done prior to calling disorder::Exercise::initialize)
3 initialize disorder
Ownership of the memory allocated to the disseminator is assumed by disorder::Exercise and is cleaned up by disorder when disorder::Exercise::destroy() is called by the simulation application upon termination.
It is possible to implement a custom PDU disseminator and have disorder use that instead, but that is of questionable usefulness so it isn’t covered in this book.

7.2.3. Sending PDUs

Sending a PDU is a matter of instantiating the desired PDU, filling it out as desired, and then calling send on it.

Here’s an example:

disorder::pdu::Fire fire; (1)

fire.firing_entity_id = disorder::entity::Id(12, 83, 94); (2)
fire.target_entity_id = disorder::entity::Id(12, 83, 57); (2)
fire.munition_id = disorder::pdu::record::EntityId::UNKNOWN; (2)
fire.fire_mission_index = disorder::pdu::Fire::NO_FIRE_MISSION; (2)

const disorder::geospatial::GeodeticLocation LOCATION(
    disorder::degrees_to_radians(37.19751842118354),
    disorder::degrees_to_radians(-112.98065185546875),
    0);

fire.location = LOCATION; (2)

fire.burst_descriptor.munition_type =
    disorder::pdu::record::EntityType(
        siso::EntityKind::MUNITION,
        siso::MunitionDomain::ANTI_AIR,
        siso::Country::AUSTRIA,
        siso::MunitionCategory::BALLISTIC); (2)

fire.burst_descriptor.warhead = siso::Warhead::KINETIC; (2)
fire.burst_descriptor.fuse = siso::Fuse::INERT; (2)
fire.burst_descriptor.quantity = 1; (2)
fire.burst_descriptor.rate = 0; (2)

fire.velocity = disorder::geospatial::GeodeticVector(20, 15, 10, LOCATION); (2)

fire.send(); (3)
1 instantiate the desired PDU
2 fill out the PDU fields
3 call send on the PDU to start the process of transmitting it
All of the PDU header fields are filled out automatically by disorder.
When a particular PDU is published periodically, it’s perfectly acceptable to keep the same PDU instance around and call send on it multiple times. Once send has completed and returned execution back to the caller, the PDU can be modified at will without affecting the requested transmission without regard for whether or not it has actually been transmitted completely yet.

7.2.4. Receiving PDUs

Registering Receivers

In order to receive PDUs, receivers must be registered with the PDU disseminator. Receivers of many flavors can be registered with the disseminator. This flexibility comes with significant type safety risk, so many compile time checks are made on receiver registrations to ensure that receiver method signatures handle an appropriate type of PDU for the requested PDU type. Let’s look at some examples:

// free functions can be used (in this case to receive any type of PDU)
void receive_pdu(const disorder::pdu::PDU& pdu)
{
    // Do something productive with pdu here.
}

disorder::Exercise::instance().pdu_disseminator().register_receiver<
    siso::DISPDUType::FIRE>(receive_pdu);

disorder::Exercise::instance().pdu_disseminator().register_receiver<
    siso::DISPDUType::ELECTROMAGNETIC_EMISSION>(receive_pdu);

// class member functions can be used (in this case to receive any simulation management PDU)
class Handler
{
public:
    Handler()
    {
        disorder::Exercise::instance().pdu_disseminator().register_receiver<
            siso::DISPDUType::STOP_FREEZE>(
                std::bind(
                    &Handler::handle_simulation_management_pdu,
                    this,
                    std::placeholders::_1);

        disorder::Exercise::instance().pdu_disseminator().register_receiver<
            siso::DISPDUType::START_RESUME>(
                std::bind(
                    &Handler::handle_simulation_management_pdu,
                    this,
                    std::placeholders::_1);
    }

private:
    void handle_simulation_management_pdu(const disorder::pdu::SimulationManagement& pdu);
};

// lambdas can be used, but they must be wrapped with std::function
// (in this case to receive only designator PDUs)
disorder::Exercise::instance().pdu_disseminator().register_receiver<siso::DISPDUType::DESIGNATOR>(
    std::function<void (const disorder::pdu::Designator&)>(
        [](const disorder::pdu::Designator& pdu)
        {
            // Do something productive with pdu here.
        }));

// boost::bind can be used instead of std::bind
class ImportantStuffDoer
{
public:
    ImportantStuffDoer()
    {
        disorder::Exercise::instance().pdu_disseminator().register_receiver<
            siso::DISPDUType::FIRE>(
                boost::bind(&ImportantStuffDoer::do_important_stuff, this, _1));
    }

private:
    void do_important_stuff(const disorder::pdu::Fire& pdu);
};
Keeping a Copy of a Received PDU

A copy of a received PDU can be made and stored by a simulation application, but the most efficient way to keep a received PDU beyond the scope of the function that received the PDU is to use std::shared_ptr. Disorder manages all received PDUs with std::shared_ptr providing a mechanism for simulation applications to keep received PDUs as desired without having to copy them. Here’s a trivial example showing how one might store the last detected collision in the simulation:

std::shared_ptr<disorder::pdu::Collision> last_collision;

void receive_collision(const std::shared_ptr<disorder::pdu::Collision>& pdu)
{
    last_collision = pdu;
}

...
disorder::Exercise::instance().pdu_disseminator().register_receiver<
    siso::DISPDUType::COLLISION>(receive_collision);
...
Deregistering Receivers

When an object that has registered a PDU receiver is destroyed, it must deregister its PDU receiver to prevent disorder from attempting to still disseminate PDUs to it if it isn’t desirable for your simulation to crash. Deregistering receivers may also be useful for non-transient objects that only care about certain PDUs for a limited time.

To deregister a receiver, just save the receiver identifier returned by disorder::pdu::Disseminator::register_receiver and then pass it to disorder::pdu::Disseminator::deregister_receiver when appropriate. For example:

class EventReportSpy
{
public:
    EventReportSpy():
        RECEIVER_ID_(
            disorder::Exercise::instance().pdu_disseminator().register_receiver<
                siso::DISPDUType::EVENT_REPORT>(
                    std::bind(
                        &EventReportSpy::handle_event_report,
                        this,
                        std::placeholders::_1)))
    {
    }

    ~EventReportSpy()
    {
        disorder::Exercise::instance().pdu_disseminator().deregister_receiver(
            RECEIVER_ID_);
    }

private:
    const disorder::pdu::Disseminator::ReceiverId RECEIVER_ID_;

    void handle_event_report(const disorder::pdu::EventReport& event_report)
    {
        // Do something productive with event_report here.
    }
};
There is no race condition between deregistering a PDU receiver and the disseminator attempting to deliver a newly received pertinent PDU. Once the deregister_receiver call returns, the handler that was deregistered is guaranteed to never be invoked again by the disseminator even when using the active flavor of PDU disseminator.

7.3. Entity Management

Entity stuff is contained within the disorder::entity namespace. The disorder::entity::Manager singleton is responsible for entity management.

7.3.1. Internal Entities

An internal entity is managed by the simulation application. The simulation application is responsible for maintaining its state.

Internal Entity Creation

Internal entities are added to the simulation via disorder::entity::Manager::new_internal_entity. To make a new internal entity with an identifier chosen by Disorder:

disorder::entity::Entity* entity(disorder::entity::Manager::instance().new_internal_entity());

Often times, simulations have preset entity identifiers. To get Disorder to make an entity for a given entity identifier, use the other overload:

disorder::entity::Id fantastic_id;
...
fantastic_id = some value from a configuration file or something;
...
disorder::entity::Entity* entity(disorder::entity::Manager::instance().new_internal_entity(fantastic_id));

if (entity)
{
    // Yay!  It worked fine.  Rejoice and then do something productive with entity.
}
else
{
    // In this case, Disorder couldn't make the entity because the identifier is
    // already in use.  Panic may be appropriate.
}
Internal Entity Manipulation

To get internal entity state published for other simulation applications to consume, one just has to update disorder’s internal Entity with the state from the simulation by calling the appropriate mutator methods on the Entity class.

Internal Entity Destruction

Internal entities are removed from the simulation by passing an appropriate entity identifier to disorder::entity::Manager::delete_internal_entity. For example, to delete the 23.78.91 internal entity:

disorder::entity::Manager::instance().delete_internal_entity(
    disorder::entity::Id(23, 78, 91));

7.3.2. External Entities

An external entity is managed by another simulation application. External entities may be managed by a different application on the same computer or they may be managed by an application on another computer on a local or wide area network.

  • External entities are automatically created and destroyed by Disorder.

  • External entity state is updated automatically within Disorder whenever said state is received from the other application.

  • External entities are automatically dead reckoned by Disorder when appropriate.

  • Applications can subscribe to entity events to get notified when something interesting happens.

7.3.3. Entity Events

Applications can subscribe for events from internal and external entities within Disorder, however external entity events are probably much more useful.

Entity Event Filters

Entity event subscriptions use disorder::entity::event::Filter instances to specify which entity events are of interest. Two kinds of filters are provided by the library and the mechanism can be extended arbitrarily.

TypeFilter

The type filter can be used to register for entity events based on event type. Only one type of event is allowed to pass through the filter. Possible event types are:

disorder/entity/events.hpp
/// possible entity event types
enum class Type
{
    CREATED, ///< a new entity entered the simulation
    DELETED, ///< an entity was removed from the simulation
    MODIFIED ///< an existing entity changed
};

One might register for external entity created and deleted events like this:

class BigBrother
{
public:
    BigBrother():
        ENTITY_CREATED_EVENT_HANDLER_ID_(
            disorder::entity::Manager::instance().register_event_handler(
                new disorder::entity::event::TypeFilter(
                    disorder::entity::event::Type::CREATED),
                std::bind(
                    &BigBrother::handle_entity_event,
                    this,
                    std::placeholders::_1,
                    std::placeholders::_2))),
        ENTITY_DELETED_EVENT_HANDLER_ID_(
            disorder::entity::Manager::instance().register_event_handler(
                new disorder::entity::event::TypeFilter(
                    disorder::entity::event::Type::DELETED),
                std::bind(
                    &BigBrother::handle_entity_event,
                    this,
                    std::placeholders::_1,
                    std::placeholders::_2)))
    {
    }

    ~BigBrother()
    {
        disorder::entity::Manager::instance().deregister_event_handler(
            ENTITY_CREATED_EVENT_HANDLER_ID_);

        disorder::entity::Manager::instance().deregister_event_handler(
            ENTITY_DELETED_EVENT_HANDLER_ID_);
    }

private:
    const disorder::entity::event::HandlerId ENTITY_CREATED_EVENT_HANDLER_ID_;
    const disorder::entity::event::HandlerId ENTITY_DELETED_EVENT_HANDLER_ID_;

    void handle_entity_event(
        disorder::entity::event::Type type,
        const disorder::entity::Entity& entity)
    {
        switch (type)
        {
            case disorder::entity::event::Type::CREATED:
                // Do something productive here... entity was just created...
                break;
            case disorder::entity::event::Type::DELETED:
                // Do something productive here... entity is going to be deleted...
                break;
            default:
                DISORDER_LOG_E(
                    "Disorder sucks!  It called my damn handler with an "
                    "entity event of type "
                    << static_cast<int>(type)
                    << " that I didn't register for.");
        }
    }
};
The above handle_entity_event method is only called on external entity creation or deletion because disorder::entity::event::TypeFilter has a second constructor parameter that defaults to only allowing external entities through the filter.
ModifiedFilter

The modified filter can be used to register for events that are triggered when one or more of a specified set of changes occurs to an entity. An example might help:

class ExpensiveImageGenerator
{
public:
    ExpensiveImageGenerator():
        ENTITY_MODIFIED_EVENT_HANDLER_ID_(
            disorder::entity::Manager::instance().register_event_handler(
                new disorder::entity::event::ModifiedFilter(
                      disorder::entity::EXTRAPOLATED_LOCATION_CHANGED
                    | disorder::entity::EXTRAPOLATED_ORIENTATION_CHANGED), (1)
                std::bind(
                    &ExpensiveImageGenerator::update_entity,
                    this,
                    std::placeholders::_2))) (2)
    {
    }

    ~ExpensiveImageGenerator()
    {
        disorder::entity::Manager::instance().deregister_event_handler(
            ENTITY_MODIFIED_EVENT_HANDLER_ID_);
    }

private:
    const disorder::entity::event::HandlerId ENTITY_MODIFIED_EVENT_HANDLER_ID_;

    void update_entity(const disorder::entity::Entity& entity) (2)
    {
        // Do something productive here with entity...
    }
};
1 This filter causes the handler to be invoked when the dead reckoned position or orientation of an external entity changes. This only applies to external entities because the second ModifiedFilter constructor parameter for entity ownership defaults to external only.
2 The second parameter is the a constant reference to the entity. In this example, the first parameter to the event handler, the event type, is discarded like an old smelly hat. The assumption is that disorder works properly and only calls the handler with applicable entity modified events. Depending on one’s level of paranoia, one may not wish to make such assumptions.
The full list of entity modifications that trigger events can be found at the top of disorder/entity/entity.hpp.
Custom Filters

If the provided simple set of filters is not sophisticated enough for a particular need, don’t panic or curse the developers. Just make your own damn filter! It’s not as difficult as landing humans on Mars and returning them to Earth safely. Just derive something from disorder::entity::event::Filter and override the percolate method as desired. Here’s a contrived example that will allow all events for a particular entity through the filter:

struct IdFilter: public disorder::entity::event::Filter
{
    IdFilter(const disorder::entity::Id& id):
        ID_(id)
    {
    }

    bool percolate(disorder::entity::event::Type type,
                   const disorder::entity::Entity& entity)
    {
        return entity.id() == ID_; (1)
    }

private:
    const disorder::entity::Id ID_;
};
1 The percolate method returns true when the event is allowed through the filter meaning that event handlers associated with it should be invoked.

That’s it. Just make an instance of the custom filter and pass it to disorder::entity::Manager::register_event_handler with a handler and start getting only the events of interest.

7.3.4. Entity Manager

disorder::entity::Manager is passive. It only manipulates internal and external entities during the exercise update process that happens when disorder::Exercise::instance().update() is called. When exactly is a good time to manipulate simulation entities depends on whether or not your simulation application is multi-threaded with respect to the parts that interact with disorder.

Single Threaded Entity Interaction

For single threaded simulation applications, it is safe to update disorder’s internal entities and extract state information from external entities at any time. Generally, the easiest thing to do for internal entities may be to save a pointer to disorder’s Entity class within the simulation application’s class that models that entity and then just update the fields of the disorder Entity when ever is desirable.

Multi-threaded Entity Interaction

Multi-threaded simulation applications have to take care to borrow the entity manager’s entities before modifying internal entities or extracting state from external entities to avoid threading problems. For example, disorder may be in the middle of publishing an internal entity on the thread running an exercise update while the simulation application is updating the same entity on a different thread.

The disorder::entity::Thief provides a synchronization mechanism that can be used to take exclusive access of the entity manager’s entities. It behaves mainly as a scoped lock in that it acquires exclusive access to the entities upon creation and releases control back to disorder::entity::Manager upon destruction, but it can also be used to gain and release exclusive access to the entities at will. Here is an example:

// Take exclusive access to the simulation entities (external and internal)
// and iterate over them.  Exclusive access is returned to the entity
// manager when the loop finishes because the entity thief created by the
// entities() call on the entity manager goes out of scope with the loop.
for (auto& pair: disorder::entity::Manager::instance().entities())
{
    // the entities container is a map of entity identifier to Entity*
    disorder::entity::Entity& entity = *pair.second;

    if (entity.internal())
    {
        // update the entity with the latest state from the simulation
    }
    else // entity is external
    {
        // update the simulation with the latest state from the entity
    }
}

7.4. Geospatial Ramblings

8. Performance Tuning

8.1. Efficient Logging

By default, disorder’s logging mechanism is backed by a synchronous file writer. That is, all logging calls with significance above the current logging threshold block until the log message is actually written to disk. Yeah, okay, so that’s not exactly true because many filesystems have a mind of their own these days, but the point here is that the filesystem operation may take an undesirable amount of time. This may slow time critical things down if lots of logging is desired and raising the logging threshold to mitigate it is undesirable. A more efficient alternative may be to use an asynchronous logging scribe to offload the filesystem operations to another thread. The upside of this approach is that log statements will return quickly.

To get an asynchronous scribe instead of a synchronous one, just wrap the synchronous one in a disorder::log::AsynchronousScribe like so:

// Tell disorder to use an asynchronous file scribe.  This must be done prior
// to calling disorder::Exercise::initialize.
disorder::log::Log::instance().scribe(
    new disorder::log::AsynchronousScribe(
        new disorder::log::SynchronousFileScribe("fantastic_log.txt")));
Using an asynchronous log scribe can hamper crash debugging efforts. If a simulation application crashes, it is likely that all logging information up to the point of the crash will not make it to the log because the asynchronous mechanism may not have had a chance to write recent messages when the crash occurs.
Consider using a high logging threshold in a production environment like disorder’s default of WARNING. Using a low threshold where lots of logging is occurring can have a significant negative impact on performance.

8.2. UDP Socket Receive Buffer Size

Disorder chooses an arbitrary default for all UDP socket receive buffers. This default may not work well for all simulation applications. The receive buffer size of all disorder’s UDP transports can be explicity set by calling the transport’s receive_buffer_size method prior to initializing the disorder exercise.

8.3. Multi-threading

9. Extending Things

You can hack the source code of Disorder directly to suit your needs, but doing so is an avenue of last resort. Several parts of disorder have been built to allow customization without having to muck with the innards of the library.

If you create a wonderful extension that has practical applications in a general use case beyond your specific application, consider contributing it back to Disorder. Making the library better is in everyone’s best interest.

9.1. Log Scribes

Disorder’s logging mechanism can be rerouted at will. Just derive a thing from disorder::log::Scribe, implement the trivial interface, and then pass an instance of the thing to disorder::log::Log::scribe prior to initializing disorder.

Any synchronous scribe can be turned into an asynchronous scribe by wrapping it in a disorder::log::AsynchronousScribe. See Efficient Logging for more details on that.

While this extensibility provides limitless possibility, here’s a frivolous and completely boring example that sends all disorder logging to syslog on a system with such a capability.

syslog_scribe.cpp
// Copyright (c) 2011-2018 Squall Line Software, LLC
// Distributed under the terms of the MIT License
// See accompanying LICENSE.txt file or
// http://www.opensource.org/licenses/mit-license.php

#include <disorder/exercise.hpp>

#include <disorder/log/log.hpp>
#include <disorder/log/scribe.hpp>

#include <syslog.h>

//----------------------------------------------------------------------------
class SyslogScribe: public disorder::log::Scribe (1)
{
public:
    bool initialize()  (2)
    {
        ::openlog("syslog_scribe", LOG_PID, LOG_USER);
        return true;
    }

    void terminate() (3)
    {
        ::closelog();
    }

    void write(disorder::log::Significance significance,
               const std::string& thread,
               const std::string& timestamp,
               const char* file_name,
               int line_number,
               const std::string& message) (4)
    {
        static const int SIG_TO_PRI[] =
            {
                LOG_DEBUG,
                LOG_INFO,
                LOG_WARNING,
                LOG_ERR
            };

        const bool SIGNIFICANCE_IS_VALID =
               (int(significance) >= 0)
            && (int(significance) <  (  int(sizeof(SIG_TO_PRI)
                                      / int(sizeof(SIG_TO_PRI[0])))));

        ::syslog(
            SIGNIFICANCE_IS_VALID ? SIG_TO_PRI[int(significance)] : LOG_NOTICE,
            "<%c> %s [%s:%d] (thread: %s) (timestamp: %s)",
            SIGNIFICANCE_IS_VALID
                ? disorder::log::significance_abbreviation(significance)
                : 'N',
            message.c_str(),
            file_name,
            line_number,
            thread.c_str(),
            timestamp.c_str());
    }
};

//----------------------------------------------------------------------------
int main(int argc, char** argv)
{
    disorder::log::Log::instance().threshold(
        disorder::log::Significance::DEBUG); (5)

    disorder::log::Log::instance().scribe(new SyslogScribe()); (6)

    int result = EXIT_SUCCESS;

    if (disorder::Exercise::instance().initialize(
            disorder::SimulationAddress(2, 3)))
    {
        DISORDER_LOG_D("Debug this!"); (7)
        DISORDER_LOG_I("Don't look directly into the sun."); (7)
        DISORDER_LOG_W("That's probably not good."); (7)
        DISORDER_LOG_E("The core will melt down in 3 minutes!"); (7)
        DISORDER_NAKED_LOG(
            disorder::log::Significance(-12378),
            "I get filtered by disorder because I'm below the threshold!"); (8)
        DISORDER_NAKED_LOG(
            disorder::log::Significance(23487516),
            "I'm so important I inflated my importance to obscene levels!"); (8)
    }
    else
    {
        DISORDER_LOG_E("Failed to initialize Disorder"); (7)
        result = EXIT_FAILURE;
    }

    disorder::Exercise::destroy();

    return result;
}
1 Derive a custom scribe from the disorder::log::Scribe interface.
2 Implement the initialize function to perform one time initialization. If this method returns false, it will bubble up to cause disorder’s Exercise::initialize method to return false.
3 Implement the terminate function to orderly shutdown the log when disorder::Exercise::destroy is called.
4 Implement the write function to actually send something to the log.
5 Default the logging threshold to DEBUG. This is arbitrary just to get all the significance levels to appear in syslog upon attempt later.
6 Tell disorder to use the custom log scribe. This must be done prior to calling disorder::Exercise::initialize. Disorder will delete the specified instance after the terminate method is called on it.
7 Send some messages to the log for demonstration purposes.
8 Fail to confuse disorder with ridiculousness. One should not do this sort of thing. It’s here to show that the library can handle nonsense. That’s doesn’t mean it should be fed nonsense, however.

In case your hankering for minutia knows no bounds, here’s some syslog output generated by this example:

Jun 13 21:01:04 localhost syslog_scribe[6241]: <D> Debug this! [../../examples/syslog_scribe/syslog_scribe.cpp:76] (thread: 140486812428096) (timestamp: 2014-Jun-14 01:01:04.086968093)
Jun 13 21:01:04 localhost syslog_scribe[6241]: <I> Don't look directly into the sun. [../../examples/syslog_scribe/syslog_scribe.cpp:77] (thread: 140486812428096) (timestamp: 2014-Jun-14 01:01:04.086991170)
Jun 13 21:01:04 localhost syslog_scribe[6241]: <W> That's probably not good. [../../examples/syslog_scribe/syslog_scribe.cpp:78] (thread: 140486812428096) (timestamp: 2014-Jun-14 01:01:04.087014055)
Jun 13 21:01:04 localhost syslog_scribe[6241]: <E> The core will melt down in 3 minutes! [../../examples/syslog_scribe/syslog_scribe.cpp:79] (thread: 140486812428096) (timestamp: 2014-Jun-14 01:01:04.087037610)
Jun 13 21:01:04 localhost syslog_scribe[6241]: <N> I'm so important I inflated my importance to obscene levels! [../../examples/syslog_scribe/syslog_scribe.cpp:85] (thread: 140486812428096) (timestamp: 2014-Jun-14 01:01:04.087062333)

9.2. Network Transports

Network transports are how disorder sends and receives things over the network. disorder::network::Transport is the abstract base class for a generic network transport layer that is capable of sending and receiving any sort of data. This tree of transports can also be extended by the user to provide new and wonderful mechanisms to send PDUs and other goodness over a network.

All transports derive from the disorder::network::Transport base class. Let’s have a look at that:

transport.hpp
// Copyright (c) 2011-2018 Squall Line Software, LLC
// Distributed under the terms of the MIT License
// See accompanying LICENSE.txt file or
// http://www.opensource.org/licenses/mit-license.php

#ifndef DISORDER_NETWORK_TRANSPORT_HPP
#define DISORDER_NETWORK_TRANSPORT_HPP

#include <system_error>

#include <cstdint>

namespace disorder
{

namespace network
{

class Receiver;

/**
* Transport is the abstract base class for all mechanisms that send data to
* and receive data from other simulation components.
*/
class Transport
{
public:
    /// possible processing models a transport may employ
    enum class ProcessingModel
    {
        /// periodic processing occurs during disorder's main update loop
        PASSIVE,

        /// event driven processing occurs on a thread dedicated to this
        /// transport
        ACTIVE,
    };

    /// Destructor.
    virtual ~Transport();

    /**
    * Initializes the communication channels this transport uses.
    *
    * @return true if successful, false otherwise
    */
    virtual bool initialize();

    /**
    * Registers a handler to receive the data that comes in on this transport.
    *
    * Only one receiver is allowed.
    *
    * @param receiver receiver that handles all the incoming data.  Memory is
    *        owned by the caller and it is the caller's responsibility to
    *        ensure that the lifetime of the \p receiver is longer than this
    *        transport.
    */
    void register_receiver(Receiver* receiver);

    /**
    * Sends a prepared packet asynchronously across the network using this
    * transport.
    *
    * Note that this function assumes ownership of the memory allocated to
    * packet and it will be deleted with delete[] when it is no longer of use.
    *
    * @param packet pointer to the packet to send
    * @param byte_size byte size of the packet to send
    */
    virtual void async_transmit(
        const uint8_t* packet,
        std::size_t byte_size) = 0;

    /**
    * Sends a prepared packet synchronously across the network using this
    * transport.  This method blocks until the data has been transmitted
    * successfully or an error occurs.
    *
    * Note that ownership of the memory allocated to packet is retained by
    * the caller.
    *
    * @param packet pointer to the packet to send
    * @param byte_size byte size of the packet to send
    * @param error_code if something goes wrong, what will be indicated here
    * @return number of bytes sent
    */
    virtual std::size_t sync_transmit(
        const uint8_t* packet,
        std::size_t byte_size,
        std::error_code& error_code) = 0;

protected:
    /// desired processing model
    const ProcessingModel PROCESSING_MODEL_;

    /// pointer to the thing that handles received data
    Receiver* receiver_;

    /**
    * Constructor.
    *
    * @param processing_model determines how this transport performs periodic
    *                         processing
    */
    Transport(ProcessingModel processing_model);
};

}

}

#endif

The interface is fairly straightforward. Derived classes only have to implement two abstract transmit functions, async_transmit and sync_transmit, but it’s likely the initialize method will be overridden too. It is strongly suggested that all transports support both the active and passive processing models.

9.2.1. Implementing the Transport Processing Models

The disorder::network::Transport base class constructor requires a processing model to be specified. While one can opt to cheerfully ignore this parameter and specify something that isn’t exactly true and then do whatever the hell one wants, it is generally suggested that one implement both processing models and then see which works best for a particular situation. In the PASSIVE processing model, the transport only transports when the update method is called by disorder’s main processing loop. When using the ACTIVE model, the transport transports whenever there is something to do via a dedicated thread.

9.2.2. Examples

Two custom transport examples are included in the source distribution in addition to disorder’s provided transports. These are simply for demonstration purposes as they may be completely illogical to use in practice.

Unix Domain Sockets

If your set of simulation applications happens to run on the same Unix-ish box, transmitting DIS over a Unix Domain Socket may be optimal. Disorder can do that leveraging the standalone asio library or as part of boost. See examples/uds_pdu_transport within the source tree for details.

If it works, this output should happen when the example is executed:

Got a comment:
        Do you think the rain will hurt the rhubarb?
        Not if it's in cans!
0MQ

Wouldn’t it be fun to transmit DIS over 0MQ? Disorder can do that. See examples/zmq_pdu_transport within the source tree for details.

If it works, this output should happen when the example is executed:

Got a comment:
        DIS over 0MQ!

9.3. PDU Transports

How disorder sends and receives PDUs is only limited by the imagination of crafty keyboard jockeys. As discussed in PDU Transmission, PDUs are sent and received using transports. Like other aspects of disorder, an opportunity is afforded to define and insert custom transports into the mix.

The disorder::pdu::transport::Transport abstract base class provides the interface for all PDU transports. The disorder library provides one concrete disorder::pdu::transport::Transport implementation called disorder::pdu::transport::NetworkTransport which uses a disorder::network::Transport that is required to be supplied upon construction. If you’re sure you don’t want your extended transport interacting with a network, keep reading, otherwise consider perusing the Network Transports section.

If you want to make a new PDU transport that doesn’t send PDUs over a network (What fun is that?), disorder::pdu::transport::Transport might be a good place to start. Let’s have a look at that.

transport.hpp
// Copyright (c) 2011-2018 Squall Line Software, LLC
// Distributed under the terms of the MIT License
// See accompanying LICENSE.txt file or
// http://www.opensource.org/licenses/mit-license.php

#ifndef DISORDER_PDU_TRANSPORT_TRANSPORT_HPP
#define DISORDER_PDU_TRANSPORT_TRANSPORT_HPP

namespace disorder
{

namespace pdu
{

namespace dissemination { class DisseminationInterface; }
struct PDU;

namespace transport
{

class Filter;

/**
* Transport provides an abstract interface for a mechanism for sending and
* receiving PDUs.
*/
class Transport
{
public:
    /// Constructor.
    Transport();

    /// Destructor.
    virtual ~Transport();

    /**
    * Initializes the communication channels this transport uses.
    *
    * @return true if successful, false otherwise
    */
    virtual bool initialize();

    /**
    * Sends a PDU via this transport.
    *
    * @param pdu the PDU to send
    */
    virtual void send(PDU& pdu) = 0;

    /**
    * Registers a handler to receive PDUs.  Only one disseminator is allowed.
    *
    * @param disseminator pointer to the thing that gets each incoming PDU.
    *        Memory is owned by the caller and it is the caller's
    *        responsibility to ensure that the lifetime of the \p disseminator
    *        is longer than this transport.
    */
    void register_disseminator(
        dissemination::DisseminationInterface* disseminator);

    /**
    * This method sets the incoming PDU filter.  All PDUs that don't make it
    * through the filter are dropped.  Only one incoming filter can be applied
    * at any given time.
    *
    * This class assumes ownership of the supplied filter.
    *
    * @param filter desired incoming PDU filter, or nullptr to remove an
    *        existing filter
    */
    void incoming_filter(Filter* filter);

    /**
    * This method sets the outgoing PDU filter.  All PDUs that don't make it
    * through the filter are not sent across the transport.  Only one outgoing
    * filter can be applied at any given time.
    *
    * This class assumes ownership of the supplied filter.
    *
    * @param filter desired outgoing PDU filter, or nullptr to remove an
    *        existing filter
    */
    void outgoing_filter(Filter* filter);

protected:
    /// thing that handles received PDUs
    dissemination::DisseminationInterface* disseminator_;

    /// filter for outgoing PDUs
    Filter* send_filter_;

    /// filter for incoming PDUs
    Filter* receive_filter_;
};

}

}

}

#endif

The interface is deceptively simplistic. Derived classes only have to implement the abstract send function but initialize will generally also be overridden. Receiving PDUs is intentionally mostly unbounded by this interface.

9.3.1. Implementing the disorder::pdu::transport::Transport Interface

  • The initialize function is where the communication channels the transport uses are established. If they cannot be established for some reason, this function should return false. Returning false from disorder::pdu::transport::Transport::initialize will bubble up to cause disorder::Exercise::initialize to return false. If this method is overridden in a derived class, it should always call this base class version which ensures that a receiver has been registered.

  • The send function should serialize the supplied PDU into a format suitable for transmission and then start an asynchronous send operation that will be serviced at some point later depending on the processing model of the transport. It might be okay to just send the darn PDU here depending on the transport as long as it doesn’t block the caller for excessive amounts of time where excessive is simulation application specific.

9.3.2. Deflating and Inflating PDUs

Converting between the user PDU model (disorder::pdu::PDU derived classes) and whatever transmission protocol dependent PDU representation is required for PDU transmission and reception is the job of PDU transforms in disorder. PDU transforms implement a visitor pattern on PDU fields. Deflators serialize user model PDUs into another format while inflators go the other direction. Both transforms derive from the disorder::pdu::field::Visitor base class.

Disorder provides the disorder::pdu::transform::IPPacketEncoder to transform a user model PDU into the transmission representation specified in the IEEE-1278.1 standard. The disorder::pdu::transform::IPPacketDecoder performs the opposite transformation.

9.3.3. Where to Stick Received PDUs

When a PDU is received, it should be inflated into the appropriate disorder::pdu::PDU instance. PDU transforms and the disorder::pdu::Factory are useful for those purposes respectively. Once the user model PDU instance is obtained, it is generally forwarded to the disseminator registered with the transport. Said disseminator is kept in the disseminator_ protected member of the Transport base class. Generally, this disseminator is an instance of the built-in disorder::pdu::dissemination::Disseminator. It requires the dynamically allocated PDU to be wrapped in a std::shared_ptr and forked over to the disseminate method. That’s all there is to it. For other parts of your simulation application to handle the received PDUs, register interest in them as described in the Receiving PDUs section.

Custom implementations of disorder::pdu::dissemination::DisseminationInterface can be used to avoid the built-in dissemination mechanism for a specific PDU transport.

9.4. Custom/Experimental PDUs

So, you want to make your own damn PDU and transmit and receive it using disorder? Oh, good for you! Uhm…​ I mean, sure, no problem.

There are some ground rules:

  • All PDUs must derive from disorder::pdu::PDU

  • All PDUs disorder is expected to receive must be registered with the disorder::pdu::Factory

Yet another example is in order:

#include <disorder/pdu/factory.hpp>
#include <disorder/pdu/pdu.hpp>
#include <disorder/pdu/field/visitor.hpp>

struct AwesomePDU: public disorder::pdu::PDU (1)
{
    disorder::pdu::record::EntityId entity_id; (2)

    uint32_t awesomeness; (2)

    AwesomePDU():
        disorder::pdu::PDU(
            siso::DISPDUType(237),
            siso::DISProtocolFamily::OTHER): (3)
        awesomeness(0)
    {
    }

    void accept_without_header(disorder::pdu::field::Visitor& visitor) override (4)
    {
        visitor.visit(entity_id);
        visitor.visit(awesomeness);
    }
};

...

DISORDER_REGISTER_EXPERIMENTAL_PDU(237, AwesomePDU) (5)
1 Ground rule #1. Check! Look at the damn disorder::pdu::PDU header in the source tree to get a clue.
2 Declare some content within the ridiculous custom PDU. Notice that the PDU header is missing here because it is declared in the base class.
3 Pass some important things to the base class constructor. All PDUs require a type and a protocol family. According to the standard, PDU types between 129 and 255 are reserved for experimental PDUs so picking a number within that range would be wise. OTHER is probably a good choice for the protocol family of custom/experimental PDUs.
4 The accept_without_header method is abstract in the base class so it is required to be implemented in all concrete derived PDUs. It accepts a field visitor that needs to visit all the fields that comprise your PDU except the header. In this case, both fields are things disorder knows how to visit already so they are just visited directly. This is the magic that is used to inflate and deflate PDUs.
5 Ground rule #2. Check! This registration macro defined disorder/pdu/factory.hpp allows disorder to inflate instances of this new PDU when they are received by a transport. It is not necessary to register custom/experimental PDUs that are only intended to be sent. This macro is best placed in the global scope in some implementation file.
If you expect something else to receive and comprehend your new PDU, that something else must also know about it.

9.5. The Datum Variable Record Situation

Unfortunately, the SISO-REF-010 identifies over 1,100 variable record types but does not provide any guidance on exactly how those records map to data structures transmitted within PDUs. That makes using them in a situation where not all simulation components may interpret them the same fraught with peril. That makes developing a library that interacts with them problematic and perilous.

Disorder tries to deal with this situation by defining a very small subset of the known variable record types that seem to map to obvious data structures. The rest of the known variable record types as well as non-standard custom variable record types can be registered by simulation applications. The disorder::pdu::record::VariableRecordFactory serves this purpose.

9.6. Geospatial Converters

10. Platform Specific Notes

10.1. Windows

10.1.1. Global Namespace Pollution

Global namespace pollution is a giant problem with the horrendously offensive windows APIs. Several symbols used in disorder are haphazardly #defined in the windows API. To counter this, disorder will #undef the offending symbols. However, this might piss off your software if it relies on that stupidity, so disorder will re-pollute the global namespace if you #define DISORDER_PLEASE_RESTORE_GLOBAL_NAMESPACE_POLLUTION before including disorder stuff. Doing such a thing is probably not a good solution because it will make your life difficult if you use the trampled symbols from the disorder library in your code. To resolve any such issues, you can use disorder/utility/global_namespace_pollution_countermeasures.hpp similarly to how it is done in the library itself. Something like this might help:

#include <windows.h>

#define DISORDER_PLEASE_RESTORE_GLOBAL_NAMESPACE_POLLUTION (1)
#include <disorder/log/log.hpp>
...
// Do whatever with windows global namespace pollution in effect.
...
#include <disorder/utility/global_namespace_pollution_countermeasures.hpp> (2)
...
// Do whatever with windows global namespace pollution removed.
DISORDER_LOG_E("Something failed.");
...
#define DISORDER_RESTORE_GLOBAL_NAMESPACE_POLLUTION (3)
#include <disorder/utility/global_namespace_pollution_countermeasures.hpp> (4)
...
// Do whatever with windows global namespace pollution in effect.
1 This #define instructs disorder to restore the global namespace pollution after it has removed it for its own evil purposes.
2 Including this special header will remove the offending symbols from the global namespace.
3 This macro makes the subsequent inclusion of the special header turn into a restore operation because that similar macro with the PLEASE is defined.
4 This #include restores the global namespace pollution.
There may not be a best practice approach to this awful problem, but it may be wise to include all the windows crap first and then disorder headers to attempt to avoid the situation where windows crap is looking for symbols that disorder undefined. However, that is certainly not going to be as easy as it sounds with a large complex code base.

Good luck. This problem sucks. Send all fan mail and thanks to Microsoft for this situation.