Multiplayer scripting in Nasal

From FlightGear wiki
Jump to navigation Jump to search
WIP.png Work in progress
This article or section will be worked on in the upcoming hours or days.
See history for the latest developments.

At the moment (10/2011) there is unfortunately no documentation available, you should refer to the mp_broadcast.nas module or other Nasal modules making use of it, such as the wildfire module or the bombable addon.

Also, the developers of both Nasal modules are usually available at the forum, too. The main developer of the mp_broadcast.nas and wildfire modules is "AndersG", while the bombable addon was developed by "flug".

Also see Howto: Transmit properties over MP

Status

According to AndersG, the dual control system could certainly be more user friendly. He has some ideas for creating a wrapper where you just register the properties you want to have shared and it would create a configuration of the current building blocks internally. However, spare time and inspiration for this part is in short supply while he's also hoping for HLA not being pie in the sky..

Currently the easiest component for listing up properties you want sent/recieved is the TDMEncode/TDMDecode components. The properties will propagate somewhat slowly since they share the same MP string property. ZLT-NT uses 4 TDMEncoder:s to propagate state to the copilot.

Be a bit careful with MP string properties - you can (or could?) easily overflow the maximum packet size...

There is a sanity check of some kind - or at least it propably doesn't crash FG - we'd see way more crashes if it did. But it means that some data that ought to have been sent is not - and the big string value remains in its property until it is replaced.

Foo

Depending on what exactly it is that you are trying to do, you are probably WAY better off looking at AndersG's mp_broadcast.nas script in $FG_ROOT, it's basically designed to "abuse" FlightGear's MP properties to propagate, i.e. encode and transmit/decode custom stuff:

See the wildfire script for usage examples.

All active MP-enabled properties are sent in every packet.

On the other hand, only the MP-enabled properties that exist in the property tree when the MP subsystem is initialized will be active, so if a /instrumentation/radar/transmit-mode property would be MP-enabled (i.e. in the list in multiplaymgr.cxx) only aircraft that define it in their -set file would transmit that property. You might find the "message passing/group communication" abstraction I made for the distributed wild fire simulation useful (it is in mp_broadcast.nas).

How it works

basically it will "overload" a generic string property to become a data container for arbitrary data, the data needs to be encoded/decoded using the helpers provided by AndersG. The string property will then be sent to subscribed clients - each client will then parse/decode the string property to get the data out again.

So these multiplayer "channels" simply use "generic properties" (=string properties), these generic properties have been supported by FG for years, they are used by MOST MP aircraft already, Now, AndersG has come up with an abstraction layer to conveniently package Nasal variables in these generic properties, so that you can encode/decode values in custom message types, all of this ON TOP of the current multiplayer protocol.

you would be using a single string property to package other information in it and transmit it, some "addons" already use this, such as the wildfire system or flug's bombable addon - the mp_broadcast.nas script is flexible enough to be useful in other contexts, such as this one

I'd suggest to start two fgfs instances and connect them (either via MP or directly) and then use the Nasal console to play around with the broadcast channel system.


It should definitely allow you to set up your own "shared control" system. And there should be some usage examples in $FG_ROOT, I already mentioned the wildfire script.

I don't know if there's a tutorial or any other docs around (check AndersG website), but otherwise we can also start writing a tutorial - i.e. step by step, like we used to :D

Effiency

I would look into differentiating between properties that you only need to send once they change, and those that need to be sent continously - to reduce the MP traffic overhead as much as possible.

Obviously, you'll then need a way to request initial state first.

If anyone wants to have a go at it, here it is: first construct a map of properties to be synced from the host. Send the map in an initial handshake when the copilot joins. Store the map as a vector and hash it (useful for verifying integrity too. When a property changes or is added in the tree, send the changed index and the value, together with the hash. Place an event observer on each frame iteration. By sending only the array index and the value, you minimize the packet size, since it's unlikely to have the whole tree changed during a session. There's some more handling involved, but you get the idea. I would look at sending initial values to generate states rather than full states. (It could be the random seed and start time but also some more data). Deciding that player X manages the master instance of the Y part of the shared state will be a problem when X decides to go offline (or his/her FG crashes). Also just deciding who takes care of what part of the state in the first place is also none too easy.

To some degree this actually is how AndersG's code is working behind the scenes - however, no matter if it's Nasal or C++ - the real issue is not the implementation language, but rather the underlying transport mechanism, i.e. the MP protocol - and here, it doesn't matter if the packets are assembled by Nasal or C++, because we really only have a fixed/static map of properties currently, which is why people (I think it was Melchior) came up with "generic properties" by piggybacking things using pre-defined generic properties, using generic STRING properties for custom purposes. AndersG extended the concept by building an event management channel on top of the whole thing using Nasal abstraction wrappers - so, currently, I'd even say that it is probably "easier" to do this in Nasal vs. C++, simply because most of the difficult work has already been done.

Wildfire only distributes events and the state of the cellular automaton is computed from the history of events. Sharing is currently imperfect for those joining an ongoing session - but in principle the "common" history could be passed around and those joining late could fast forward from the first event to now. For Wildfire the shared state is potentially huge while events (ignition and fire fighting actions) are fairly uncommon.

Another system with shared state is the shared scenery state (primarily doors) managed by scenery.nas. Here the state (of each door, at least) is small so it is broadcast periodically (by everyone - there could be need for adaptive tuning of the period here) and events are broadcast when they occur. The problem of converging to a common shared state is handled by attaching Lamport clocks to the states and events, causing newly arrived players to accept the common state rather than persisting in pushing their own initial state.

Message Passing

If you rather use message passing, i.e. you want to send and receive your own data messages, you should look at Nasal/mp_broadcast.nas which provides an API for that: see mp_broadcast.nas in $FG_ROOT/Nasal, which implements MP channels on top of multiplayer generic properties, encoding messages using the bits.nas module mentioned by Philosopher.

If it is on/off choices you could also use the SwitchEncoder/Decoder in Aircraft/Generic/DualControl/ to send 32 booleans in one int.

To set it up outside the DualControl setting requires a bit of wrapper code, though.

Note: mp_broadcast.nas does not connect to the internet. It uses a MP enabled property in the property tree which the regular MP system transmits to other users.

Nasal in FG cannot create sockets. $FG_ROOT/Nasal/mp_broadcast.nas may cover your communication needs.

For example, if an attacker broadcasts a message containing callsign of target and hit details when a local hit is detected the target will receive this and can take appropriate action. See Nasal/wildfire.nas for an example of how to use mp_broadcast.}}

You might find mp_broadcast.nas and its use in AndersG's Submarine Scout blimp interesting for the MP communication. It doesn't solve the lag problem (since that is more or less fundamentally unsolvable in our setting, though we could do a bit better than we do today).

mp_broadcast.nas is useful for sending infrequent (but potentially large) events/messages in a way that is fairly bandwidth friendly for the current MP protocol. In the Submarine Scout I currently use it to communicate the position of the ground crew of each airship and any bomb impacts to all other airships and observers.

The file mp-network.nas in Aircraft/ZLT-NT/Systems is one example of how to use it.

TDM Example

To send a bunch of properties from the pilot to the copilot with TDM components you need to set up the pilot_connect_copilot() and copilot_connect_pilot() functions for your aircraft (typically in <your-aircraft>-dual-control.nas) as in the example below. ZLT-NT is a more full featured example.

What these two functions do is to set up the components that (on the pilot side here) monitor, encode and send the registered properties and (on the copilot side) receive, decode and write the updates into the property tree there. For this last step you specify a function func (v) { ... } that receives the new value and is responsible for writing it to the right property (which may or may not be the same as it originated from - animations normally look under the MP/AI entry available as "pilot" here).

The Dual Control system takes care of monitoring the registered properties (every frame) and encoding, decoding sending and receiving messages (which you don't have to care about) at a suitable rate.

var DCT = dual_control_tools; # A short-cut to dual_control_tools.
var pilot_TDM1_mpp       = "sim/multiplay/generic/string[0]"; # Must be created in the -set file too.

######################################################################
# Used by dual_control to set up the mappings for the pilot.
var pilot_connect_copilot = func (copilot) {
    return [
         ######################################################################
         # Process properties to send.
         ######################################################################
         ##################################################
         # Set up TDM transmission of slow state properties.
         DCT.TDMEncoder.new
         ([
           props.globals.getNode(...),
           props.globals.getNode(...),
           props.globals.getNode(...),
           ...
           props.globals.getNode(...)
          ],
          props.globals.getNode(pilot_TDM1_mpp),
         ),
         DCT.TDMEncoder.new(...),
         ...
         DCT.TDMEncoder.new(...)
    ];
}
######################################################################
# Used by dual_control to set up the mappings for the pilot.
var copilot_connect_pilot = func (pilot) {
    return [
         ######################################################################
         # Process received properties.
         ######################################################################
         ##################################################
         # Set up TDM reception of slow state properties.
         DCT.TDMDecoder.new
         (pilot.getNode(pilot_TDM1_mpp),
          [
           func (v) {
               pilot.getNode(...).setValue(v); # Set the property for the MP/AI model.
           },
         DCT.TDMDecoder.new(...),
         ...
         DCT.TDMDecoder.new(...)
    ];
}