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?

Many commercial defense simulators still choose DIS to communicate with one another despite the NSA’s directive on HLA. SISO continues to evolve the DIS standard. A new version of the DIS application protocol standard was released in 2012 by SISO, two years after the NSA killed it. A newer version beyond that has been rumored to be in work for many years and documentation from SISO appears to indicate it will be a major overhaul.

1.2. Motivation

There was a distinct lack of industrial strength C++ open source DIS libraries in the world. This thing fills the 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

  • partial support for 1278.1-1998 PDUs (see Limitations for details)

  • partial support for 1278.1-2012 PDUs (see Limitations for details)

  • simulation entity, transmitter, designator, IFF system, and synthetic environment object management

  • entity and articulated part dead reckoning (all 9 algorithms)

  • geospatial coordinate conversion

  • IFF system interactive interrogations

  • 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

  • basic support for working with DI Guy custom PDUs

  • 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. Limitations

The following things from the IEEE Standard for Distributed Interactive Simulation-Application Protocols (IEEE Std 1278.1-2012) are not yet implemented in Disorder:

  • Identification Friend or Foe (IFF) PDU Layer 2 described in sections 5.7.6 and 7.6.5 of the 1278.1-2012 standard

  • Many of the IFF Data Records identified in section B.2 of the 1278.1-2012

  • IFF Civilian Mode S or Military Mode 5 Squitter capability as described in B.1.3.3.i of the 1278.1-2012 standard

  • Non-Real-Time Protocol as described in section 8 of the 1278.1-2012 standard

  • Live Entity (LE) Information/Interaction Protocol as described in section 9 of the 1278.1-2012 standard

  • Some of the optional parts of the Transfer Ownership function as described in section 5.9.4 and Annex H of the 1278.1-2012 standard

There is no technical reason why these features cannot be added to the library. Patches to support these features are welcome. The library author can also be contracted to implement missing features in a timely manner if desired.

5.1. Why doesn’t this damn thing support HLA?

High Level Architecture (HLA) does not have a standard on-the-wire protocol. Having no standard on-the-wire protocol locks all simulations that support HLA into only interoperating with other things that speak their specific HLA implementation’s on-the-wire protocol. HLA simulations do not generally work across HLA vendor implementations so all possible players that a simulation ever hopes to interact with must use the same vendor’s HLA implementation in order to ensure interoperability. Not only that, but often different versions of HLA implementations from the same vendor are not compatible with one another because their proprietary on-the-wire protocol evolves in incompatible ways.

No, the RPR FOM does not provide any assistance with the HLA vendor interoperability problem. The RPR FOM can be used to translate between the standard on-the-wire protocol of DIS and a non-standard HLA on-the-wire protocol allowing any DIS compliant simulation application to interact with a specific HLA implementation. A simulation exercise would require such a translation for each HLA implementation that participating simulation applications use.

If you find this difficult to believe because HLA vendor websites and marketing materials for expensive HLA middleware products don’t mention this interoperability limitation, do some research and ask the vendor pointed questions about interoperability. Most people read marketing statements such as "HLA 1516-2010" or "HLA 1.3" standards compliant to imply interoperability with other implementations of the same standard. That simply is not true for HLA due to the lack of an on-the-wire protocol standard. Often times, the first inkling of this limitation happens after a company has purchased an expensive HLA middleware, expended a lot of effort to develop a simulation with it, and then tries to use that simulation product with another simulation developed with a different vendor’s HLA much later. Then the two simulation developers argue over who will switch to the other’s HLA implementation so their simulations can talk to each other.

Some vendors offer bridges to translate from one HLA implementation to another. This type of bridge is just another ridiculous layer of unnecessary complexity that slows everything down and adds confusion. In many ways HLA itself is a ridiculous unnecessary complexity that slows things down and adds confusion. It has demonstrably resulted in less interoperability across the simulation industry.

Disorder does not and most likely will not support HLA unless and until there is some reasonable way to avoid this interoperability problem. Without such an interoperability standard, implementing HLA in a library like this would result in yet another vendor specific HLA island. What’s the tangible usefulness of that? Open source HLA implementations have been attempted a few times (see Open HLA, CERTI, and others). As of early 2022, all of these projects seem abandoned.

As of early 2023, the in-development IEEE 1516-202X HLA 4 standard is said to finally have a standard on-the-wire protocol. Once a version of that standard is available, it will be evaluated for library inclusion. Some information on the HLA 4 protocol is available here, here, here, and here.

6. Installation

Information on downloading, compiling, and installing the library is available on the Disorder wiki.

7. Getting Started

Enough nonsense, let the hacking begin!

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

7.1.1. Show me the code!

hello_world.cpp
// Copyright (c) 2011-2024 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/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::FileScribe("hello_world_log.txt")); (2)

    disorder::Exercise::instance().add(
        disorder::pdu::transport::make_udp(
            disorder::network::udp::Options()
                .local_host("localhost")
                .remote_endpoint("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 <build>/examples/hello_world within a compiled disorder source tree.

7.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::ReferenceClock 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 by default when something ambiguous is specified, Disorder happily accepts IPv4 addresses and can be made to prefer IPv4 over IPv6 via disorder::network::udp::Options::ip_version in ambiguous situations if desired.

8. General Specifics

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

8.1.1. Macros

Standard

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. The log message can contain anything stream-able via the stream insertion operator (operator<<). 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.
Once

Disorder also provides DISORDER_LOG_D_ONCE, DISORDER_LOG_I_ONCE, DISORDER_LOG_W_ONCE, and DISORDER_LOG_E_ONCE macros for situations when the message should only ever be logged once. This is helpful to avoid flooding the log with messages repeatedly reporting the same condition from a part of a simulation application that is executed frequently. These macros otherwise work exactly like the ones without the _ONCE suffix.

No matter how often this specific log statement is encountered, it can only result in one instance of the specified message being logged. If the significance is below the logging threshold, the message will not be logged and another attempt will be made when the statement is encountered later.

The once guarantee is enforced across threads such that if multiple threads encounter the same log once statement simultaneously and the significance is above the threshold, one of the threads will log the message and the others will not.

These messages are logged at most once per execution of the simulation application. These log statements are not reactivated if disorder is terminated and reinitialized.

The once guarantee is per log statement. That is, if the exact same log statement appears more than once in an application, the message will be logged once per statement. For example, this will generate 3 "Hello!" log messages:

DISORDER_LOG_E_ONCE("Hello!");
DISORDER_LOG_E_ONCE("Hello!");
DISORDER_LOG_E_ONCE("Hello!");
Once Each

Additionally, Disorder provides DISORDER_LOG_D_ONCE_EACH, DISORDER_LOG_I_ONCE_EACH, DISORDER_LOG_W_ONCE_EACH, and DISORDER_LOG_E_ONCE_EACH macros for situations when the message should only ever be logged once for each unique value of some key. These macros take an additional key argument to control this behavior.

This set of macros is helpful for situations where it is desirable to log some condition only once, but that condition involves some variable and it is desirable to log an occurrence of each different value of that variable. Consider an error that can have multiple causes where it is desirable to know which different cause(s) of the error condition occur. A DIS situation where this is useful occurs when processing received PDUs that can contain custom/non-standard records and a record of unknown type is encountered. In that circumstance, it is desirable to log each unknown record type once. Because such PDUs will likely be received periodically, logging the same unknown record repeatedly would fill up logs without providing significantly more useful information than the first occurrence of each unknown record type.

A key must be supplied as the first parameter to the _ONCE_EACH macros. The key can be of any type that can be used as a key to an std::unordered_set.

Consider the following concrete example:

enum Problem { GREMLINS, QUANTUM_FLUCTUATION, SPILLED_COFFEE };

std::ostream& operator<<(std::ostream& stream, Problem problem)  (2)
{
    switch (problem)
    {
        case GREMLINS: stream << "GREMLINS"; break;
        case QUANTUM_FLUCTUATION: stream << "QUANTUM_FLUCTUATION"; break;
        case SPILLED_COFFEE: stream << "SPILLED_COFFEE"; break;
        default: stream << "unknown tomfoolery"; break;
    }

    return stream;
}

void it_broke(Problem problem)
{
    DISORDER_LOG_E_ONCE_EACH(problem, "It broke due to " << problem << '!'); (1)
}
1 A value of the Problem enumeration is used as the key for the once guarantee. The message is only logged once for each unique problem value no matter how many times it_broke is called with a unique problem value.
2 This stream insertion operator is only necessary because the problem variable of the Problem enumerated type is streamed into the log message. It is not strictly necessary to satisfy the log macro otherwise and it can be avoided even in that circumstance by casting the enumerated value to an integer while constructing the log message at the cost of producing a less human-readable log message.
In the dumb example above, a log message corresponding to the 3 explicit enumerated values can only ever be logged once per value, but it’s entirely possible that the "It broke due to unknown tomfoolery!" log message can be generated more than once by that single DISORDER_LOG_E_ONCE_EACH. Why? Because multiple values of the problem key correspond to that log message. For example it_broke(Problem(-123)); and it_broke(Problem(98)); would result in that same log message being generated twice. The once guarantee is based entirely upon unique values of key. The log message may be exactly the same for different values of key.
Less Common Use Cases

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

There are similar flavors of DISORDER_LOG_ONCE and DISORDER_LOG_ONCE_EACH should one become mired in a situation where they might be useful.

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

8.1.2. Reading the Logs

Here is some example content from a Disorder log file:

I 2013-Dec-28 22:15:54.140916621: Timestamp mode is RELATIVE with an epoch of 2013-Dec-29 03:15:54.140876. [main](../../src/disorder/time/clock.cpp:184)
D 2013-Dec-28 22:15:54.140963742: No spatial reference frame convertor was assigned to disorder::spatial::ReferenceFrame prior to calling disorder::Exercise::initialize.  Using the default SEDRIS SRM based convertor. [main](../../src/disorder/exercise.cpp:155)
I 2013-Dec-28 22:15:54.140990501: No flattened coordinate system is configured.  This is not a problem, but using it in this state is. [main](../../src/disorder/geospatial/reference_frame.cpp:86)
D 2013-Dec-28 22:15:54.141045571: Using the default active PDU disseminator. [main](../../src/disorder/exercise.cpp:243)
I 2013-Dec-28 22:15:54.141127525: UDP transport's local endpoint is bound to [::1]:3000 [main](../../src/disorder/pdu/transport/udp_transport.cpp:141)
D 2013-Dec-28 22:15:54.141174992: UDP transport's receive buffer is 140000 bytes [main](../../src/disorder/pdu/transport/udp_transport.cpp:193)
I 2013-Dec-28 22:15:54.141213311: UDP transport's remote endpoint is [::1]:3000 [main](../../src/disorder/pdu/transport/udp_transport.cpp:211)
D 2013-Dec-28 22:15:54.141312412: Using the default 1995 entity publisher [main](../../src/disorder/entity/manager.cpp:184)
W 2013-Dec-28 22:15:54.141366523: A PDU of type 22 is being sent with the receiving_entity_id field set to NONE.  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. [main](../../src/disorder/pdu/simulation_management.cpp:46)
I 2013-Dec-28 22:15:54.141648546: Shutting down [main](../../src/disorder/log/log.cpp:57)

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

  • The first character is the significance of the message where the letter is the first character of the significance’s name.

  • The second part is the timestamp of the message in UTC/GMT including fractional seconds down to nanosecond precision.

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

  • The fourth part is after the log message and enclosed within square brackets [] containing the symbolic name or identifier of the thread that sent the message.

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

The format of the timestamp is adjustable via the disorder::log::Log::instance().timestamp_format() method. This method can also be used to configure the output timestamps to use the local timezone instead of UTC if desired.
By default, the thread identifier part of the log message line contains the textual representation of a std::thread::id produced by operator<<. One can provide an arbitrary symbolic name to be used instead as shown above by calling disorder::log::Log::instance().thread_name().
The source file and line number can be excluded from log messages if desired via a parameter to the FileScribe constructor.

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

8.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 the simplest possible example:

disorder::Exercise::instance().add(disorder::pdu::transport::make_udp()); (1)
1 Transports are added by calling add on the disorder::Exercise instance. The UDP PDU transport factory, disorder::pdu::transport::make_udp, is used to construct UDP PDU transports. In this case, the default IPv4 UDP broadcast transport on port 3000 is used. Disorder will choose the first local network interface that is active and capable of IPv4 broadcasting. This behavior may not be deterministic if the host machine has multiple network interfaces. The default behavior may not be desirable. More control is afforded. See below for more complex examples.

Here’s a slightly more complex example showing how to make an IPv4 UDP multicast transport on port 3000 with multicast loopback disabled using the local interface on the 192.168.1 subnetwork:

disorder::Exercise::instance().add( (1)
    disorder::pdu::transport::make_udp( (2)
        disorder::network::udp::Options() (3)
            .remote_host("224.0.0.101") (4)
            .multicast_loopback(false) (5)
            .local_netmask("192.168.1.0/24")); (6)
1 Transports are added by calling add on the disorder::Exercise instance.
2 The UDP PDU transport factory, disorder::pdu::transport::make_udp, is used to construct UDP PDU transports. It will make the appropriate broadcast or multicast transport based on the specified options.
3 The disorder::network::udp::Options object controls the UDP network transport configuration. It allows calls to be chained so many options can be set inline. Look at the API reference manual or the disorder/network/udp/options.hpp header file for full details.
4 Set the remote host to a multicast address to make a multicast transport. The default port of 3000 is used in this case.
5 Disable multicast loopback to prevent all applications on the local host, including the sending simulation application, from receiving sent PDUs. Multicast loopback is enabled by default.
6 Select the local interface on the 192.168.1 subnetwork. This is useful if the host machine has multiple local network interfaces and a specific interface for DIS is desirable without explicitly identifying it with a complete IP address. This is helpful if local IP addresses for simulation application hosts are assigned via DHCP, for example. Netmasks are specified in CIDR notation.
Disorder will attempt to resolve symbolic local and remote host names. If resolution of a name is ambiguous, IPv6 is preferred by default. The ip_version option can be used to select the IPv4 address instead.
Disorder has a built-in mechanism to efficiently drop received PDUs that were sent by the simulation application receiving them, so simulation applications need not defend against reception of their own PDUs when using broadcast or multicast with loopback enabled.
Network transports that come with disorder all provide a processing model option 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.
Filtering Inbound and Outbound PDUs

Disorder allows for filtering of both inbound and outbound PDUs at the transport level. So, for example, different types of PDUs can be sent and/or received via different transports in the same application. This is all done transparently to the logic in the simulation application that sends or receives the PDUs. This capability affords added flexibility to simulation exercise and network designers to do things like separate high volume low latency PDUs from the rest of the DIS traffic. For example, voice data in Signal PDUs from simulated radios can be easily separated from the rest of the PDUs of a simulation exercise.

Filters can be applied to any PDU transport via the disorder::pdu::transport::Options object similar to how UDP transport options are applied as discussed above.

In the below example, all simulated radio PDUs are sent to a different multicast channel than the rest of the PDUs published by the simulation application.

disorder::Exercise::instance().add(
    disorder::pdu::transport::make_udp(
        disorder::network::udp::Options().remote_host("224.0.0.200"),
        disorder::pdu::transport::Options()
            .outgoing_filter(
                new disorder::pdu::transport::FamilyExclusionFilter(
                    siso::DISProtocolFamily::RADIO_COMMUNICATIONS)))); (1)

disorder::Exercise::instance().add(
    disorder::pdu::transport::make_udp(
        disorder::network::udp::Options().remote_endpoint("224.0.0.201", "3001"),
        disorder::pdu::transport::Options()
            .outgoing_filter(
                new disorder::pdu::transport::FamilyFilter(
                    siso::DISProtocolFamily::RADIO_COMMUNICATIONS)))); (2)
1 Add a UDP multicast transport on 224.0.0.200 port 3000 that is used for all PDUs published by this simulation application except for those within the radio communications family.
2 Add a UDP multicast transport on 224.0.0.201 port 3001 that all PDUs within the radio communications family published by this simulation application will be sent over.
In the above example, only outbound PDUs are filtered. All PDUs will be received from both transports. Incoming PDUs can just as easily be filtered by applying an incoming_filter as desired.
Ownership of the memory allocated to a filter is assumed by the PDU transport it gets applied to and will be cleaned up when the PDU transport is destroyed.
Other types of filters are provided by disorder. See disorder/pdu/transport/filter.hpp for more details.
Custom filters can be created by simulation application authors by deriving from disorder::pdu::transport::Filter and implementing the abstract percolate method.
Common Problems Configuring Transports

Specifying a local_endpoint is often problematic. Disorder binds its outbound socket to this endpoint. The library has an internal mechanism to prevent simulation applications from receiving their own outbound PDUs even when multicast_loopback is enabled. That mechanism relies on the outbound socket having a unique local address and port combination, so the outbound socket is not configured for address reuse. This situation leads to the following errors when an explicitly configured local_endpoint causes the outbound socket to bind to the same local address and port combination the inbound socket uses:

Linux
Failed to bind inbound local endpoint for UDP transport on ###.###.###.###:#### with error Address already in use (asio.system:98)
Windows
Failed to bind inbound local endpoint for UDP transport on ###.###.###.###:#### with error An attempt was made to access a socket in a way forbidden by its access permissions. (asio.system:10013)
It’s generally best to avoid specifying a local_endpoint. The library will almost always chose an appropriate local_endpoint automatically. To explicitly control the network interface that gets used, specify only the local_host or the local_netmask and leave the local_port at the default value to allow the library to select a free port to bind the outbound socket to.

8.2.2. Configuring the PDU Disseminator

All received PDUs are routed from PDU transports to the PDU disseminator. The disseminator is responsible for 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.

8.2.3. Sending PDUs

Sending a PDU is a matter of instantiating an object of the desired disorder::pdu::PDU derived class, populating its fields with data, and then calling send on it.

The network transports shipped with disorder allow different PDU instances to be sent concurrently from multiple threads.

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::NONE; (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 or even destroyed at will without affecting the requested transmission without regard for whether or not it has actually been transmitted completely yet.

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

8.3. Entity Management

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

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

Precise Dead Reckoning

The default behavior of the library is to timestamp outbound PDUs when they are sent. Because Entity State and Entity State Update PDUs are published by the library automatically when necessary, there can be some unpredictable latency between when location, orientation, velocity, etc. values are updated in an Entity by a simulation application and when the corresponding Entity State or Entity State Update PDU is sent. The Entity State and Entity State Update PDU timestamp is used as the basis for dead reckoning calculations by receiving simulation applications, so it is imperative, especially for fast movers, that the timestamp closely align with the values in the PDU.

There are different strategies for overcoming this problem. When an exercise uses relative timestamps, simulation applications can take control over the reference clock and manually advance it from one frame to the next in precise time intervals in which the dynamic entity state is also computed. In this way, the PDU timestamps will correspond directly to the entity state information. Information on taking control over the reference clock can be found in the Reference Clock section of this document.

Another strategy to get precise Entity State and Entity State Update PDU timestamps is for simulation applications to directly supply the outbound timestamps on Entity State and Entity State Update PDUs on a per Entity basis. Simulation applications wishing to use this strategy should call the Entity::sample_time method as part of updating the Entity state supplying the precise time of the information in the Entity. A time::reference::TimePoint is required. Generally, just pass the elapsed duration since the standard clock epoch into the time::reference::TimePoint constructor to create one of these. Reference clock time points have nanosecond precision, but DIS timestamps only have 1.676 microsecond precision. The more precise this value matches the entity state information the better. When a sample_time value is supplied, Disorder will use it in the timestamp field of the PDU header instead of the time the PDU is sent.

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

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

8.3.3. Entity Appearance Attributes

Each DIS entity has a 32-bit field that contains a set of appearance attributes that depends on the type of the entity. See UIDs 31-43 in the SISO-REF-010 for more details. Working with this 32-bit field directly is cumbersome and error prone, so Disorder provides a more convenient mechanism for examining and manipulating these appearance attributes in addition to allowing simulation applications to interact with the bits directly if so desired.

Generally, external entities should be treated as const Entity objects from the perspective of simulation applications because they are read-only. Internal entities are non-const as the simulation application manages the state of those entities and will call mutator methods on the Entity objects to update that state.

The convenient appearance mechanism is available via the appearance() method of Entity. There are two different overloads of this method, a const version and a non-const version. The const version provides read-only access to the appearance attributes whereas the non-const version provides read/write access. The appearance() method returns an Appearance object. That object provides access to entity kind, and domain in the case of platforms, specific appearance interfaces. See the appearance.hpp header in the source tree for more details.

Writing Appearance Attributes

Calls to update appearance attributes can be chained together to update multiple bits. For example, to turn the headlights and power plant on and set the camouflage type to forest on a land platform Entity:

entity_.appearance().land_platform()
    .head_lights_on(true)
    .power_plant_on(true)
    .camouflage_type(siso::AppearanceCamouflageType::FOREST_CAMOUFLAGE);
Any appearance attributes not explicitly set by a simulation application will have a default value of 0.
Disorder manages the deactivated bit on internal entities automatically so simulation applications need not worry about updating it. Disorder will automatically publish an Entity State PDU with the deactivated bit set when internal entities are deleted in accordance with the 1278.1 standard.
Reading Appearance Attributes

For const Entity objects, reading a specific appearance attribute is via a bit field member variable, not a function call. For example, to see if the landing gear is extended on an air platform:

if (entity_.appearance().air_platform().landing_gear_extended)
{
    // do something useful here...
}

The interface is slightly different for non-const Entity objects. Generally, non-const objects are only used for internal entities which would normally operate on appearance attributes in write only mode. Sometimes one may want to read an appearance attribute from a non-const Entity, so Disorder can do that. Here’s the above example for a non-const object. Notice the only difference is the attribute is accessed via a member function instead of a member variable:

if (entity_.appearance().air_platform().landing_gear_extended())
{
    // do something useful here...
}

8.3.4. Entity Events

Disorder provides two different mechanisms for entity event notification, an entity specific mechanism and a general mechanism.

8.3.5. Entity Specific Events

The entity specific event mechanism is useful for efficiently handling changes to a specific entity. Entity specific events are available for both internal and external entities.

Only one change handler can be registered with an Entity. Even though a change handler can be configured to react to multiple types of changes, the change handler is only invoked once per Entity State/Entity State Update PDU after all the pending changes to the Entity have been applied.

A concrete example will probably help illustrate the practical usefulness of this mechanism. Consider a simulation object that represents a DIS entity that needs to react to appearance changes:

class Sheep
{
public:
    Sheep(disorder::entity::Entity& entity):
        entity_(entity)
    {
        entity_.register_change_handler(
            disorder::entity::APPEARANCE_CHANGED,
            std::bind(&Sheep::update, this));  (1)
    }

    ~Sheep()
    {
        auto scoped_lock = disorder::entity::Manager::instance().entities();
        entity_.deregister_change_handler(); (2)
    }

private:
    disorder::entity::Entity& entity_;

    void update()
    {
        // do stuff with entity_ here (3)
    }
};
1 Register the change handler with the DIS entity to start getting change notification events.
  • Multiple trigger conditions can be specified to the registration call by bitwise 'or'-ing them together.

  • Arbitrary parameters can be bound to the handler using std::bind or boost::bind.

  • This example assumes the creator of the Sheep has exclusive access to the simulation entities.

2 Deregister the change handler from the DIS entity when it is no longer needed. If you don’t want your simulation to crash, don’t destroy the thing handling change events before deregistering the change handler.
3 The DIS Entity can be asked what changed via its changed method and otherwise interrogated. The interaction with the Entity in the event handler is guaranteed to be thread-safe by disorder.
Registering or deregistering a change handler counts as interacting with an Entity and, as such, may not be thread-safe. See Entity Manager for more details on how to not screw this up.

8.3.6. General Entity Events

The general mechanism is perhaps most useful for reacting to entities joining and leaving the simulation, but it can also be used for handling changes to existing entities.

Applications can subscribe for events from internal and external entities within Disorder. External entity events are probably more useful than internal because the simulation is less likely to anticipate changes to the entities it does not manage.

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.
The above example is silly. It would be better to bind the two different events to two different functions and avoid the switch statement. This is just a dumb example to illustrate library capabilities. Use your best judgment when writing your own simulation applications to avoid writing code that sucks.
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 causing event handlers associated with it to 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.

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

Information about a single entity can be obtained in a thread-safe manner using the disorder::entity::Manager::query method. Here’s an example which gets the extrapolated location and orientation of a known entity with id entity_id at a desired simulation time time assuming both entity_id and time are defined in the current scope:

struct EntityState
{
    bool entity_exists;
    disorder::geospatial::Location location;
    disorder::geospatial::Orientation orientation;

    EntityState(): entity_exists(false) {} (1)

    EntityState(const disorder::geospatial::DeadReckonedState& state):
        entity_exists(true),
        location(state.location),
        orientation(state.orientation)
    {
    }
};

const auto state =
    disorder::entity::Manager::instance().query(
        entity_id,
        [time](const entity::Entity& entity) (2)
        {
            return EntityState(entity.extrapolate_to(time));
        });

if (state.entity_exists)
{
    // do something productive with state.location and state.orientation
}
1 A default constructed version of the query result type is returned if the specified entity does not exist.
2 This lambda is passed the entity when found. The lambda returns the desired information back through the entity manager’s query function to the caller.

8.3.8. Entity Ownership Transfer

Disorder can transfer entity ownership between simulation applications in accordance with section 5.9.4 and Annex H of the 1278.1-2012 DIS standard. Entity ownership transfer is an asynchronous process with several coordinated steps between the divesting and acquiring simulation applications. A simulation application initiates an entity transfer by calling disorder::entity::Manager::instance().transfer_ownership(…​). This method is used to push an entity to or pull an entity from another simulation application. It does the right thing depending on the current ownership status of the entity being transferred. When entity ownership is transferred, existing disorder::entity::Entity instances remain valid, the ownership attribute just changes accordingly. Ownership status changes are considered modifications like other changes to an entity and event handlers can be attached to ownership changes as desired.

Disorder will ignore incoming transfer ownership requests by default because they allow other simulation applications to steal internal entities and thrust other entities a simulation application may not want to deal with upon it. To process incoming transfer ownership requests, call disorder::Exercise::instance().configuration().process_transfer_ownership_requests(true) prior to initializing the disorder exercise.

Ownership Transfer Limitations

Section 5.9.4 of the 1278.1-2012 standard specifies a lot of optional transfer ownership behavior. The following things are not currently handled by the library (patches welcome):

  • use of records sets within Transfer Ownership PDU

  • communication of additional information using Set Record-R and Record-R PDUs

  • Data Query PDU and Data PDU mechanism for determining entity ownership

  • ownership conflict rules

  • most of the transfer ownership related records in table 20 are not supported (only the Total Records Sets, Launched Munition, and Ownership Status records are currently supported)

  • environmental process ownership transfers

8.4. Laser Designator Management

Laser designator stuff is contained within the disorder::designator namespace and works much the same way as entity management in Disorder. Laser designator management is disabled by default. This feature is enabled by calling disorder::Exercise::instance().configuration().manage_designators(true) prior to disorder::Exercise::instance().initialize.

The internal, external, event, and threading concepts described above for entities apply to laser designators too.

8.4.1. Unique Internal Designator Identifiers

The designator::Manager doesn’t manage or create unique internal designator identifiers automatically. A unique designator identifier has to be supplied to the designator::Manager::new_internal_designator function. A desigantor::Id consists of an entity identifier and a designator system name.

The entity is the unique identifier of the designating entity and the system uniquely identifies the type of designating system within the entity.

8.4.2. Designator Dead Reckoning

Designator PDUs seem to contain state that implies designators can be dead reckoned using an algorithm and a linear acceleration. At present, Disorder is too dumb to use this information to actually dead reckon designators as that modicum of information does not seem sufficient to adequately perform reasonable dead reckoning. Disorder will only dead reckon designators that are on or offset from an entity using the dead reckoning information in the entity and the entity referenced spot information in the Designator PDU.

If you know how the designator spots are supposed to be dead reckoned using the information in the Designator PDU, please submit a patch or e-mail information to disorder@squallline.com so the library can be improved.

8.5. Transmitter Management

Transmitter stuff is contained within the disorder::transmitter namespace and works much the same way as entity management in Disorder. Transmitter management is disabled by default. This feature is enabled by calling disorder::Exercise::instance().configuration().manage_transmitters(true) prior to disorder::Exercise::instance().initialize.

The internal, external, event, and threading concepts described above for entities apply to transmitters too.

8.5.1. Unique Internal Transmitter Identifiers

The transmitter::Manager doesn’t manage or create unique internal transmitter identifiers automatically. A unique transmitter identifier has to be supplied to the transmitter::Manager::new_internal_transmitter function by the simulation application. A transmitter::Id consists of a reference_id and a number.

The reference_id is generally the unique identifier of the entity the transmitter is attached to. For intercoms and other special cases, the 1278.1-2012 says an "Unattached Identifier Record" shall be used instead of an entity identifier. An "Unattached Identifier Record" cannot be practically distinguished from a proper entity identifier, so Disorder recommends using a legitimate entity identifier even for an intercom.

The number field uniquely identifies the transmitter within an entity. This is just a 1 based index. If an entity has multiple transmitters, the first one should be assigned a number 1, the second gets 2, and so on.

8.6. Identification Friend or Foe (IFF) System Management

IFF stuff is contained within the disorder::iff namespace and works much the same way as entity management in Disorder. IFF system management is disabled by default. This feature is enabled by calling disorder::Exercise::instance().configuration().manage_iff_systems(true) prior to disorder::Exercise::instance().initialize.

The internal, external, event, and threading concepts described above for entities apply to IFF systems too.

8.6.1. IFF Systems

Disorder models transponders and interrogators with disorder::iff::Transponder and disorder::iff::Interrogator objects respectively. Similar to entities, these come in internal and external flavors depending on whether or not the local simulation application owns them.

New internal transponders and interrogators and created via the disorder::iff::Manager by calling the new_internal_transponder or new_internal_interrogator functions. Disorder does not manage or create unique IFF system identifiers. When creating a new internal IFF system, a simulation application must supply a unique identifier for the new system. Unique IFF system identifiers consist of an entity_id, type, name, and a designator. Please reference the Disorder API Manual or disorder::iff::Id class documentation for guidance on how to specify a unique IFF system identifier.

IFF PDU Layers

The IEEE 1278.1-2012 IFF PDU layers are rolled into the state of the Transponder and Interrogator models in Disorder. The PDU layers are interacted with indirectly by reading and writing the applicable state information. For example, an IFF system’s Mode 5 information is communicated via Layer 3 of the IFF PDU. For an internal transponder, this information can be manipulated via the Transponder::mode_5 method. When the transponder has Mode 5 equipment, the Mode 5 state should be provided to the Transponder::mode_5 method. Subsequently, Layer 3 will be automatically added to published IFF PDUs. The same works in reverse for external IFF systems. The parameterless mode_5 method to query information returns a data structure that can be empty to indicate that Layer 3 was not present in the IFF PDU.

8.6.2. Simulation Modes

Disorder supports both regeneration and interactive IFF simulation modes. By default, all internal IFF systems are capable of supporting interactive mode. Simulation applications can elect to disable interactive mode support on a per IFF system basis via the disorder::iff::System::interactive_mode_enabled method. When interactive mode is disabled on a transponder, any received interactive interrogations for that transponder are completely ignored.

Regeneration Mode

In Disorder, all IFF systems always support regeneration mode. Under the rules of regeneration mode, Disorder publishes periodic IFF PDUs for each internal IFF system managed by a simulation application. The library will also publish immediate IFF PDUs in response to IFF system state changes made by simulation applications. The 1278.1-2012 standard allows for a configured latency period on changed information, but Disorder sends the updated IFF PDU on the next Exercise::update.

Interactive Mode

Interactive IFF simulation mode allows for modeling of IFF system interrogations with greater simulation fidelity to foster more immersive simulation experiences for pilots.

An interactive interrogation is initiated by calling the interrogate method on a disorder::iff::Interrogator. The caller has to supply the identifier of the IFF system to interrogate, the IFF modes to interrogate, a response closure, and a timeout.

When a simulation application receives an interactive interrogation on one of its internal IFF systems that has interactive mode enabled, the Disorder library will automatically issue a response. The content of that response is controlled by the state of the IFF system representing the transponder. A simulation application can register to receive notification of an interactive interrogation via the Transponder::register_interrogated_handler method.

The following things are relevant:

  • If the IFF system mode is OFF, incoming interrogations are ignored and no reply IFF PDU will be sent.

  • If the IFF system mode is STANDBY, incoming interrogations are received and simulation applications will receive notification when registered for the interrogated notification, but no reply IFF PDU will be sent.

  • If the IFF system mode is NORMAL or EMERGENCY, incoming interrogations are received and a reply IFF PDU will be sent.

  • When an interactive reply IFF PDU is sent, the content is pulled from the IFF system state.

    • For example, if the Mode 1 equipment of the interrogated transponder is not powered on (the on bit inside the pdu::record::iff::Mode1Code record is false) then the interrogation reply will indicate that it does not contain a Mode 1 response by setting the mode_1 field of the InterrogatedModes record inside Layer 5 of the reply IFF PDU to false.

8.7. Synthetic Environment Object Management

Disorder is capable of sending and receiving areal, linear, and point synthetic environment objects. Synthetic environment object stuff is contained within the disorder::object namespace and works much the same way as entity management in Disorder. Object management is disabled by default. This feature is enabled by calling disorder::Exercise::instance().configuration().manage_objects(true) prior to disorder::Exercise::instance().initialize.

The internal, external, event, and threading concepts described above for entities apply to synthetic environment objects too.

8.7.1. Unique Synthetic Environment Object Identifiers

Simulation applications have a unique set of identifiers that entities and synthetic environment objects share. An entity and an object cannot have the same unique identifier. When the library selects identifiers for new internal entities and objects, it prevents multiple things from having the same identifier. The library will also try to prevent simulation applications that choose their own identifiers for objects and entities from overlapping entity and object unique identifiers.

8.7.2. Objects Removed From DIS 8

The draft 1278.1 standard version 8 removes synthetic environment objects. They have been combined with entities, so the object management facility of Disorder should be considered deprecated.

8.8. Time

Disorder uses two clocks, a reference clock and a simulation clock where the simulation time is based on the reference time.

8.8.1. Simulation Clock

disorder::time::SimulationClock is a wall clock in the simulated environment. Simulation time adheres to the following rules:

  • Simulation time need not correspond to real-world time.

  • Simulation time need not advance at the same rate as real-world time.

  • Subsequent simulation time values need not be contiguous or linear.

  • Simulation time values need not correspond to single reference time.

  • Simulation time values cannot generally be converted into equivalent reference time.

  • The only possible conversion between simulation time and reference time is from the current instant in reference time to the current instant of simulation time.

The simulation time is available via disorder::time::SimulationClock::instance().now(). The simulation time quantity is a std::chrono::time_point with nanosecond precision which can be interacted with just like standard std::chrono clock times.

8.8.2. Reference Clock

disorder::time::ReferenceClock provides the time for PDU timestamps and is the basis for simulation time. By default, the reference clock is equivalent to a real-world wall clock.

The reference clock is probably not very useful to simulation applications. If you really want to know what time the reference clock thinks it is at any given instant, call disorder::time::ReferenceClock::instance().now().

Chronometers

The reference clock is backed by a chronometer driven by the system clock on the host computer called disorder::time::RealWorldChronometer. Simulation applications wishing to have finer grained control over the reference clock can inject a different disorder::time::Chronometer implementation.

Disorder provides a manual chronometer alternative that turns over control of the advancement of time to the simulation application. To use the manual chronometer instead of the default system clock based chronometer, do this stuff prior to calling disorder::Exercise::initialize:

// create the manual chronometer instance and store it off for later use
// NOTE: This code sets the manual chronometer to the current system time,
// but a simulation application can choose to initialize and advance
// the reference clock a different way in accordance with whatever exercise
// time rules are in play.
auto chronometer =
    std::make_shared<disorder::time::ManualChronometer>(
        std::chrono::system_clock::now());

// tell the reference clock to use the manual chronometer
disorder::time::ReferenceClock::instance().chronometer(chronometer);

// initialize disorder (must be done after overriding the chronometer)
if (disorder::Exercise::instance().initialize(simulation_address))
{
    // ...

    for (;;) // assume this is the main application loop
    {
        // do whatever the simulation application does

        // ...

        // sleep until the next frame starts and advance time accordingly
        std::this_thread::sleep_until(next_frame_abs_time);
        chronometer.add(frame_time);
    }
}

8.9. Working with DI Guy

Disorder has basic support for handling nonstandard DI Guy PDUs and standard variable records that are part of Attribute PDUs. It tracks DI Guy character type, appearance (body, head, and hand item), action, weight, scale, name, and uniform patch information in Entity. Applications can register for change notification on DI Guy extended entity state changes the same way as other entity modifications. See Entity Events for more details.

DI Guy support is disabled by default. DI Guy support is enabled by making a call to disorder::Exercise::instance().configuration().di_guy_processing_enabled(true); prior to initializing disorder::Exercise.

The DI Guy PDU uses a unique identifier of 220 by default. If your exercise agreement uses a different PDU identifier for it, call disorder::Exercise::instance().configuration().di_guy_pdu_id(<put your desired PDU ID here>); to set it to your desired PDU ID prior to initializing the disorder::Exercise. Processing of the custom DI Guy PDU can be disabled by supplying either 0 or siso::DISPDUType::OTHER to disorder::Exercise::instance().configuration().di_guy_pdu_id. When DI Guy processing is enabled and the custom PDU is disabled, Attribute PDUs containing custom DI Guy standard variable records will still be processed.

The current DI Guy extended entity state can be retrieved from a disorder::entity::Entity via its di_guy_state() method.

DI Guy uses nonstandard siso::VariableRecordType values from 23000000 to at least 23000006 for the nonstandard standard variable records it puts in Attribute PDUs, so these values should not be otherwise used by simulation applications that interact with DI Guy. These values cannot currently be adjusted in Disorder as they do not appear to be configurable in DI Guy.

The DI Guy support in Disorder is receive only. The library does not publish any DI Guy state information for internally managed entities because there has not been a need to do so yet, but the library can be made to publish DI Guy state if the need arises. Patches to support this feature are welcome.

8.10. Geospatial Ramblings

There are 3 coordinate systems in play in disorder: geocentric, geodetic, and flattened.

The geocentric coordinate system is the DIS standard right handed geocentric coordinate system based on a WGS-84 earth approximating ellipsoid. The unit of measure for all axes is meters.

The geodetic coordinate system is the standard latitude/longitude model with an elevation to specify a 3D position. The latitude and longitude angles are in radians and the elevation is in meters from the Earth approximating ellipsoid.

The flattened coordinate system is an optional arbitrary map projection.

8.10.1. Location

The disorder::geospatial::Location class models an absolute point in space somewhere on or above the surface of the Earth. A location exists in all three coordinate systems. One can use the Location in any of the coordinate systems and it will lazily and non-redundantly convert between them. For example, it’s perfectly fine to assign the value in one coordinate system and then ask for the value in a different coordinate system.

To get the dead reckoned position at the current simulation time from an Entity:

// get the current location in the standard DIS geocentric reference frame
entity_.extrapolated_location().geocentric()

// get the current location in latitude/longitude/elevation
entity_.extrapolated_location().geodetic()

Here are a few examples showing how to set the location of an Entity:

// latitude and longitude are known in degrees and elevation in meters
entity_.last_known_location(
    disorder::geospatial::GeodeticLocation::from_degrees(
        -83.27992,   // this value is longitude (horizontal or X direction first)
        23.3194850,  // this value is latitude (vertical or Y direction second)
        18.53));

// geocentric location in meters
entity_.last_known_location(
    disorder::geospatial::GeocentricLocation(-2311559.7312, -3655449.7450, 4681969.4654));

8.10.2. Orientation

The disorder::geospatial::Orientation class models the attitude of an object. An orientation exists in all three coordinate systems. One can use the Orientation in any of the coordinate systems and it will lazily and non-redundantly convert between them. For example, it’s perfectly fine to assign the value in one coordinate system and then ask for the value in a different coordinate system.

All angles in disorder are measured in radians. Convenience functions to convert between degrees and radians are provided in disorder/linear_algebra/angle_conversion.hpp.

Orientations coexist in four different notations: Tait-Bryan ZYX intrinsic convention Euler angles, yaw/pitch/roll, quaternion, and matrix.

Heading, Pitch, and Roll

Disorder’s orientation interface may seem too damn complicated when all you want to do is get or set the orientation of an entity using the familiar heading, pitch, and roll angles. Okay, take some deep breaths. The geodetic orientation is where you’ll find these values. They are based upon a local tangent plane at the entity location.

To get the dead reckoned familiar heading, pitch, and roll in radians at the current simulation time from an Entity:

entity_.extrapolated_orientation().geodetic().yaw_pitch_roll()

To set the current heading, pitch, and roll of an Entity assuming the last known location has already been updated:

// assuming heading_rad, pitch_rad, and roll_rad are in radians
entity_.last_known_orientation(
    disorder::geospatial::GeodeticOrientation(
        disorder::YawPitchRoll(heading_rad, pitch_rad, roll_rad),
        entity_.last_known_location()));

// assuming heading_deg, pitch_deg, and roll_deg are in degrees
entity_.last_known_orientation(
    disorder::geospatial::GeodeticOrientation(
        disorder::YawPitchRoll::from_degrees(heading_deg, pitch_deg, roll_deg),
        entity_.last_known_location()));

8.10.3. Let’s Do Some Maths

Disorder is built on top of the powerful Eigen linear algebra library, so there are many convenient ways to compute all sorts of geospatial and orientation values of interest. Getting the right answer will often require some math and 3D reasoning skills. The library provides you with the tools, but leaves a lot up to the simulation application author. This affords great flexibility at the cost of some complexity.

Let’s look at a few examples. These are in no way exhaustive. They are just intended to give you an idea of what power is at your fingertips.

Compute the Distance, in Meters, Between Two Entities
(  entity2_.extrapolated_location().geocentric()
 - entity1_.extrapolated_location().geocentric()).norm()
The result of the above subtraction is a disorder::Vector3 which also has a squaredNorm() function that can be used to avoid the expensive square root operation when comparing distances.
Compute the Geodetic Location 1 Kilometer Directly in Front of an Entity
disorder::geospatial::GeocentricLocation(
      entity_.extrapolated_location().geocentric()
    + disorder::geospatial::BodyVector(
          entity_.extrapolated_orientation(),
          disorder::Vector3(1000, 0, 0)).world().geocentric()).geodetic()
Compute the Sensor Heading and Pitch to Stare at Something

Consider a camera attached to a drone aircraft that we want to compute the world referenced familiar heading and pitch to stare at a rather interesting looking mountain goat that is up to shenanigans.

// camera offset in meters in the DIS entity coordinate system from
// the center of the drone entity's bounding volume (drone entity location)
static const disorder::Vector3 camera_offset(1.2, -3.4, 0.23);

// determine the world referenced camera location
const disorder::geospatial::Location camera_location =
    disorder::geospatial::GeocentricLocation(
          drone_entity_.extrapolated_location().geocentric()
        + disorder::geospatial::BodyVector(
              drone_entity_.extrapolated_orientation(),
              camera_offset).world().geocentric());

// calculate a vector in the direction the camera needs to point to look at the goat
const disorder::XYZVector direction_topo =
    disorder::geospatial::GeocentricVector(
          goat_entity_.extrapolated_location().geocentric()
        - camera_location.geocentric()).geodetic(camera_location);

// calculate world referenced heading and pitch using trigonometry where
// positive pitch is upward and positive heading is to the right
const double heading_radians = ::atan2(direction_topo.y, direction_topo.x);

const double pitch_radians =
    ::atan2(
        -direction_topo.z,
        ::sqrt(
              (direction_topo.x * direction_topo.x)
            + (direction_topo.y * direction_topo.y)));

8.10.4. The Flattened Coordinate System

If your simulation application has a moving map or otherwise renders a 2D projection, the flattened coordinate system might be able to help. It can be configured as a local tangent space, Lambert conformal conic, transverse mercator, or universal transverse mercator projection. It must be configured prior to initializing the disorder exercise and cannot be changed thereafter.

Here’s an example of how to configure it as a local tangent space with origin at the intersection of the Equator and the Prime Meridian:

disorder::geospatial::FlattenedConfiguration flat_config;

flat_config.earth_ellipsoid = disorder::geospatial::EarthEllipsoid::WGS84; (1)
flat_config.projection = disorder::geospatial::MapProjection::LOCAL_TANGENT_SPACE; (2)

flat_config.lts.origin_longitude_radians = 0; (3)
flat_config.lts.origin_latitude_radians = 0; (3)
flat_config.lts.azimuth_radians = 0; (3)
flat_config.lts.false_origin_x = 0; (3)
flat_config.lts.false_origin_y = 0; (3)
flat_config.lts.height_offset = 0; (3)

disorder::geospatial::ReferenceFrame::instance().configure_flattened(flat_config); (4)

disorder::Exercise::instance().initialize(disorder::SimulationAddress(123, 456)); (5)
1 set the desired Earth approximating ellipsoid
2 set the desired projection type
3 configure the projection specific settings relevant for the desired projection type (note that all fields must be assigned because they default to random nonsense)
4 tell the geospatial reference frame about the desired configuration before initializing the disorder exercise
5 initialize the disorder exercise after setting up the flattened coordinate system

Once the flattened configuration is in place, the flattened reference frame can be accessed via the standard Location and Orientation objects.

8.10.5. Reference Frame Conversion Libraries

Disorder comes with two different geospatial conversion backends. By default, the library prefers GeographicLib, but it also contains built-in support for SEDRIS SRM. Both backends provide the full feature set for geospatial conversions. Which one to choose comes down to simulation application author preference. It’s also possible to extend disorder to use a different conversion backend. See Geospatial Convertors for more details on that.

If you want to use the SEDRIS SRM, there’s a little more work to do. Because the SEDRIS SRM creators don’t give away their source code without forcing people to have an account in their system and agree to their license, Disorder is not distributed with the SEDRIS SRM and cannot automatically download it for you. The following additional steps are required to use the SEDRIS SRM:

  • Download the SEDRIS SRM source code .tgz file. NOTE: Always pick the Unix version even on Windows. They are the same except for the compression format.

  • Put the srm_c_cpp_sdk_4.4.tgz file in a subdirectory off the root of the disorder source tree called subprojects/packagecache

  • Use -Denabled_geospatial_libraries=sedris_srm option when configuring disorder

    • -Denabled_geospatial_libraries=sedris_srm,geographic_lib can be used to include support for both libraries allowing run-time selection of the one that gets used

      • If both libraries are enabled and you want to use SEDIRS_SRM by default instead of GeographicLib, also apply the -Dpreferred_geospatial_library=sedris_srm option

When both libraries are configured, run-time selection during initialization is possible. Instantiate the desired convertor and give it to disorder::geospatial::ReferenceFrame::instance().convertor(…​) prior to initializing the disorder exercise.

9. Performance Tuning

9.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::FileScribe("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.

9.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 disorder’s UDP transports can be adjusted via the receive_buffer_size method on the disorder::network::udp::Options object used to configure the UDP transport.

9.3. Multi-threading

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

10.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-2024 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)

10.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-2024 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 <disorder/dso_api.hpp>

#include <disorder/network/processing_model.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 DISORDER_DSO_PUBLIC Transport
{
public:
    /// Destructor.
    virtual ~Transport();

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

    /// Terminates the communication channels this transport uses.
    virtual void terminate();

    /**
    * 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.

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

10.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!

10.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-2024 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

#include <disorder/dso_api.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 DISORDER_DSO_PUBLIC 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.

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

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

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

10.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 declare traits and register 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>

constexpr siso::DISPDUType AWESOME_PDU_ID(static_cast<siso::DISPDUType>(237)); (1)

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

    uint32_t awesomeness; (3)

    AwesomePDU():
        disorder::pdu::PDU(
            AWESOME_PDU_ID,
            siso::DISProtocolFamily::OTHER): (4)
        awesomeness(0)
    {
    }

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

...
DISORDER_DECLARE_PDU_TRAITS(AWESOME_PDU_ID, AwesomePDU) (6)
DISORDER_REGISTER_PDU(AWESOME_PDU_ID) (7)
1 Declare a constant expression for the unique PDU identifier of the nonstandard PDU. This is mostly for convenience. 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.
2 Ground rule #1. Check! Look at the damn disorder::pdu::PDU header in the source tree to get a clue.
3 Declare some content within the ridiculous custom PDU. Notice that the PDU header is missing here because it is declared in the base class.
4 Pass some important things to the base class constructor. All PDUs require a type and a protocol family. OTHER might be a good choice for the protocol family of custom/experimental PDUs.
5 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.
6 Declaring PDU traits allows the library to resolve PDU information, like the type of the C++ class that models a PDU, via the unique PDU identifier. This declaration should generally live in a header file after the declaration of the PDU class that models it. Traits must be declared before the PDU is registered.
7 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 nonstandard PDUs that are only intended to be sent. This macro is best placed in the global scope in some implementation file.
If you’re wondering why the hell items 6 and 7 above are not combined into one damn step, stop being so judgemental and try to be more humble. The best way to do something isn’t always intuitively obvious. The reason this takes two separate steps is that traits are generally necessary in header files but the factory registration belongs in an implementation file. For the purpose of this simple example, the steps could be combined but it is almost always better to have the two steps separated in practice.
If you expect something else to receive and comprehend your new PDU, that something else must also know about it.

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

10.5.1. Sending and Receiving PDUs with Custom Fixed and Variable Records

Sending PDUs with custom variable records is not likely to induce headaches or carpal tunnel syndrome.

Are you sick of examples yet? No? Good. Here’s an example.

constexpr siso::VariableRecordType INTENSITY_DATUM_ID =
    static_cast<siso::VariableRecordType>(666001); (1)

struct PleaseShineYourLightOnMeDatum: public disorder::pdu::record::VariableDatum (2)
{
    static constexpr siso::VariableRecordType ID =
        static_cast<siso::VariableRecordType>(666002); (3)

    uint8_t red; (4)
    uint8_t green; (4)
    uint8_t blue; (4)
    std::string justification; (4)

    PleaseShineYourLightOnMeDatum() = default; (5)

    PleaseShineYourLightOnMeDatum( (6)
        uint8_t r,
        uint8_t g,
        uint8_t b,
        const std::string& justification):
            VariableDatum(ID, (3 + justification.size() + 1) * 8),
            red(r), green(g), blue(b), justification(justification)
    {}

    void accept(disorder::pdu::field::Visitor& visitor) override (7)
    {
        VariableDatum::accept(visitor);

        visitor.visit(red);
        visitor.visit(green);
        visitor.visit(blue);
        visitor.visit(justification);
        visitor.pad_to_boundary<8>(3 + justification.size() + 1); (8)
    }
};

DISORDER_REGISTER_FIXED_DATUM(INTENSITY_DATUM_ID, std::int32_t) (9)

DISORDER_REGISTER_CUSTOM_VARIABLE_DATUM(
    PleaseShineYourLightOnMeDatum::ID,
    PleaseShineYourLightOnMeDatum) (10)

// ...

disorder::pdu::SetData set_data;

// set originating_entity_id, receiving_entity_id, request_id, etc

set_data.datum.fixed_datum_records.push_back(
    disorder::pdu::record::make_fixed_datum(INTENSITY_DATUM_ID, std::int32_t(1234))); (11)

set_data.datum.variable_datum_records.push_back( (12)
    std::make_shared<PleaseShineYourLightOnMeDatum>(
        255,
        255,
        255,
        "Because it's dark."));

// ...

set_data.send();
1 Declare a constant expression for the unique datum identifier for a custom fixed datum. This isn’t strictly required, but it might be good practice.
2 Create a structure to house the custom variable record information. It must derive from disorder::pdu::record::VariableDatum or one of its subclasses.
3 It might be convenient to tuck the unique identifier in here.
4 Declare the content of the datum. In this example, the content is the color components of the light to be shined and a justification string.
5 The default constructor is used when a PDU with this type of record is received. This is not necessary to only send these things.
6 This type of convenience initializer simplifies creation of these records for sending. This is not necessary to only receive these things.
7 Override the accept method and visit all the data members of the variable record. It is imperative that the base class version of this method execute first via VariableDatum::accept(visitor);.
8 Variable records must be 64-bit aligned to comply with the DIS standard. This line adds appropriate padding when necessary.
9 Register the custom fixed record. This is not necessary to only send these things.
10 Register the custom variable record. This is not necessary to only send these things.
11 Add a custom fixed record to the SetData PDU with a unique identifier of 666001 and an int32_t value of 1234. The data type of the value must be exactly what should be on the wire.
12 Add a custom variable record to the SetData PDU with the specified values. Note that the convenience initializer defined in #6 set the unique identifier and length fields of the base class appropriately via the base class constructor.
The fixed and variable record registrations should be placed in an implementation file at global scope or within some arbitrary namespace.

10.6. Attaching Custom Data to Entity State PDUs

The IEEE 1278.1-2012 standard offers the Attribute PDU for extending the information available in other PDUs including Entity State and Entity State Update. So, it may be advisable to consider that approach before resorting to the mechanism described in this section although the standard seems to endorse this extension mechanism too.

Disorder allows custom records to be attached to Entity State and Entity State Update PDUs as variable parameters. This is where articulated and attached part records as well as a few other less common but standard records (entity association, entity type, and separation) are kept. Records placed in this area must be 128 bits (16 bytes) with 120 bits (15 bytes) of usable payload as the required record type field takes up the first byte.

Like all entity data, care must be taken to not read or write custom variable parameters (including adding or removing them from an entity) while disorder::Exercise::update is running because disorder will read parameters associated with internal entities and write parameters associated with external entities during that time. If your simulation application is multi-threaded and might read or write these records while disorder is using them, you must acquire the entity manager’s lock via disorder::entity::Manager::instance().entities() while doing so in order to maintain thread-safety.

There are two extension points for deriving custom variable parameter records, disorder::pdu::record::VariableParameter and disorder::pdu::record::VariableParameterWithChangeIndicator. disorder::pdu::record::VariableParameterWithChangeIndicator extends disorder::pdu::record::VariableParameter adding a change indicator field making it easier for receivers to determine if the record content changed since the last time it was received. When using either base class, one must override the accept method and register the custom variable with the factory if the simulation application needs to receive PDUs with that type of variable parameter record. Using disorder::pdu::record::VariableParameter comes with the additional requirement that operator== must be overridden.

Let’s look at some examples.

struct ThermalState: public disorder::pdu::record::VariableParameterWithChangeIndicator (1)
{
    static constexpr siso::VariableParameterRecordType ID =
        static_cast<siso::VariableParameterRecordType>(124); (2)

    std::array<uint8_t, 2> engines; (3)
    std::array<uint8_t, 8> wheels;  (3)
    std::array<uint8_t, 4> guns;    (3)

    ThermalState(): disorder::pdu::record::VariableParameterWithChangeIndicator(ID) {} (4)

    void accept(disorder::pdu::field::Visitor& visitor) override (5)
    {
        disorder::pdu::record::VariableParameterWithChangeIndicator::accept(visitor);

        visitor.visit(engines);
        visitor.visit(wheels);
        visitor.visit(guns);
    }
};

DISORDER_REGISTER_VARIABLE_PARAMETER(ThermalState::ID, ThermalState) (7)

struct Odor: public disorder::pdu::record::VariableParameter (1)
{
    static constexpr siso::VariableParameterRecordType ID =
        static_cast<siso::VariableParameterRecordType>(111); (2)

    enum class Body: uint16_t
    {
        SHOWER_FRESH,
        SWEAT_AND_STINK,
        CIGARETTE_STENCH,
        TOO_MUCH_COLOGNE,
        FLATULENCE,
    };

    enum class Mouth: uint32_t
    {
        MINTY_FRESH,
        HALITOSIS,
        ONIONS,
    };

    enum class Feet: uint64_t
    {
        NOT_SNIFFED,
        OKAY_I_GUESS,
        CHEESE,
        SOUR_MILK,
    };

    disorder::pdu::record::Padding<uint8_t> padding; (3)
    Body body;   (3)
    Mouth mouth; (3)
    Feet feet;   (3)

    Odor(): disorder::pdu::record::VariableParameter(ID) {} (4)

    void accept(disorder::pdu::field::Visitor& visitor) override (5)
    {
        disorder::pdu::record::VariableParameter::accept(visitor);

        visitor.visit(padding);
        visitor.visit(body);
        visitor.visit(mouth);
        visitor.visit(feet);
    }

    bool operator==(const disorder::pdu::record::VariableParameter& parameter) const override (6)
    {
        if (disorder::pdu::record::VariableParameter::operator==(parameter))
        {
            try
            {
                const auto& odor = dynamic_cast<const Odor&>(parameter);

                return (body == odor.body) && (mouth == odor.mouth) && (feet == odor.feet);
            }
            catch (const std::bad_cast&) {}
        }

        return false;
    }
};

DISORDER_REGISTER_VARIABLE_PARAMETER(Odor::ID, Odor) (7)
1 Derive the custom variable parameter from the desired base class. disorder::pdu::record::VariableParameterWithChangeIndicator which comes with record_type and change_indicator fields whereas disorder::pdu::record::VariableParameter only comes with the record_type field.
2 Declare a constant expression for the unique identifier of the custom variable parameter. This isn’t strictly required, but it might be good practice. Choose a value less than or equal to 255 that isn’t one of the standard values and add this customization to the exercise agreement to avoid conflicts.
3 Declare the custom record data. This stuff must take up 120 bits (15 bytes) when deriving from disorder::pdu::record::VariableParameter because it uses 1 byte for the record_type field. This stuff must take up 112 bits (14 bytes) when deriving from disorder::pdu::record::VariableParameterWithChangeIndicator because it uses 16 bits (2 bytes) for the record_type and change_indicator fields. Padding should be used to fill in unused bytes to make up the required size.
4 A constructor is not strictly required, but it’s probably a good idea. A default constructed version of these objects will not have the correct value for the record_type field. Things that derive from disorder::pdu::record::VariableParameterWithChangeIndicator will have the change_indicator field set to 0 by default. Add a convenience initializer to allow all the fields to be set via constructor parameters if so desired.
5 Override and implement the accept method. It must call the base class implementation first and then visit the custom data members. This method provides the mechanism disorder uses to transform this data to and from the on-the-wire format.
6 When deriving from disorder::pdu::record::VariableParameter, one must override operator== to provide a means for the library to compare two records. This is used to determine when the content of the custom record changes in order to publish the relevant PDU with the updated content and it is also used on the receiving side to determine when the content changed for the purpose of generating change events. It’s probably prudent to call the base class implementation first which verifies the record_type fields match.
7 Register the custom variable parameter record with the factory. This is only required for simulation applications that intend to receive these custom records.
Disorder determines whether or not changes have occurred to records derived from disorder::pdu::record::VariableParameterWithChangeIndicator strictly based upon the change_indicator field. Don’t forget to increment the change_indicator field when making changes to record values or your changes will not result in applicable PDUs being published in a timely manner.

Now that some custom variable parameter types are defined, putting them to use isn’t too difficult. The general strategy for adding a custom variable parameter to an entity is to create a std::shared_ptr to an instance of it, assign it applicable values, tell the entity about it, and save it for later modification. Here’s an example.

class OdiferousHuman
{
public:
    OdiferousHuman():
        entity_(*disorder::entity::Manager::instance().new_internal_entity()),
        odor_(std::make_shared<Odor>())
    {
        odor_->body = Odor::Body::SHOWER_FRESH;
        odor_->mouth = Odor::Mouth::MINTY_FRESH;
        odor_->feet = Odor::Feet::NOT_SNIFFED;

        entity_.variable_parameters().add(odor_);
    }

    void blight_with_foot_fungus()
    {
        odor_->feet = Odor::Feet::CHEESE;
    }

private:
    disorder::entity::Entity& entity_;
    std::shared_ptr<Odor> odor_;
};
In the above example, the custom variable record is always associated with the entity. This need not be the case. It can be arbitrarily removed via entity_.variable_parameters().remove(…​); if desired.
Disorder will detect custom variable parameter changes and publish the appropriate entity state or entity state update PDU when disorder::Exercise::update is called.

Examining custom variable parameter records on external entities is also accomplished through the object returned by the Entity::variable_parameters() method. One might register a handler for the VARIABLE_PARAMETERS_CHANGED event (note that this event indicates at least one of the variable parameter records changed but it does not indicate which one(s) changed) and then do something like this if there’s an expectation that there will only ever be one of a particular type of custom variable parameter record associated with an entity:

    auto odor = entity_.variable_parameters().first_of<Odor>(Odor::ID);

    if (odor)
    {
        // do something with odor
    }

All the variable parameters can be iterated over manually if desired:

    for (auto& parameter: entity_.variable_parameters().parameters())
    {
        if (parameter->record_type == Odor::ID)
        {
            auto odor = std::dynamic_pointer_cast<const Odor>(parameter);

            if (odor)
            {
                // do something with odor
            }
        }
    }
A copy of the std::shared_ptr can be stored for later use and to determine whether a variable parameter record has changed since it was last examined. The library makes a new instance each time a relevant PDU is received.
If a received PDU contains a variable parameter with a record_type that is unknown to the variable parameter factory, disorder will log a message documenting the first occurrence of each unknown record type otherwise unknown variable parameter records are cheerfully ignored. All recognized records will still be available.

10.7. Geospatial Convertors

To make disorder use your favorite geospatial conversion backend instead of one of the built-in mechanisms, just derive a concrete implementation of the disorder::geospatial::Convertor interface. Then create an instance of that class and give it to disorder::geospatial::ReferenceFrame::instance().convertor(…​) prior to initializing the disorder exercise.

If your extension is not based upon a proprietary implementation, please consider contributing it back to disorder as other people may find it useful.

11. Tools

Tools are located in the disorder source tree under the, go figure, tools directory. Tools are not built by default. If you want tools, pass -Dbuild_tools=true to meson when configuring disorder for compilation.

11.1. Display

The display tool is a command line utility for playing back DIS packet captures created via a libpcap application such as wireshark or tcpdump.

11.1.1. Why?

Why the hell did you waste your damn time making such a stupid thing? I already use <insert your favorite packet capture playback utility here> for that!

The main reason is that rewriting DIS timestamps is necessary to replay a packet capture with absolute DIS timestamps.

11.1.2. Usage

Run the tool with no arguments and it will tell you how to use it. Here is a sample of that output:

usage: display <pcap_file> [options]

options:
        -exercise_id <exercise_id>: overwrite the DIS exercise id to <exercise_id>
        -local_host <local_host>  : bind to this local NIC (IP address or host name)
        -local_port <local_port>  : bind to this local port
        -remote_host <remote_host>: send packets to this host name or IP address
        -remote_port <remote_port>: send packets to this port
        -absolute_timestamps      : use absolute DIS timestamps (defaults to relative)

This thing plays back DIS packet captures rewriting timestamps and destination endpoint as specified.

The only required argument is a capture file to play back. It will use the destination endpoint from the captured packets and relative DIS timestamp mode by default. If that’s not what you want, pass it more options.

11.1.3. DIS Timestamps

Display rewrites DIS timestamps when the captured PDU’s timestamp mode is absolute or display is made to produce absolute timestamps. DIS timestamps on replayed PDUs are unmolested when the captured PDU’s timestamp mode is relative and display is producing relative timestamps.

12. Platform Specific Notes

12.1. Windows

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