b61f0c241e
This concerns a single character inside a comment, so won't change the behavior of the Nasal code in any way. Beside using the favored encoding in FlightGear, this little change allows scripts/python/nasal_api_doc.py (from the FlightGear repository) to work again without having to teach it to ignore encoding errors nor to play at guessing the encoding of the files it reads.
460 lines
22 KiB
Text
460 lines
22 KiB
Text
#---------------------------------------------------------------------------
|
|
#
|
|
# Title : EMESARY multiplayer bridge
|
|
#
|
|
# File Type : Implementation File
|
|
#
|
|
# Description : Bridges selected emesary notifications over MP
|
|
# : To send a message use a Transmitter with an object. That's all there is to it.
|
|
#
|
|
# References : http://chateau-logic.com/content/emesary-nasal-implementation-flightgear
|
|
#
|
|
# Author : Richard Harrison (richard@zaretto.com)
|
|
#
|
|
# Creation Date : 04 April 2016
|
|
#
|
|
# Version : 4.8
|
|
#
|
|
# Copyright © 2016 Richard Harrison Released under GPL V2
|
|
#
|
|
#---------------------------------------------------------------------------*/
|
|
|
|
# Example of connecting an incoming and outgoing bridge (should reside inside an aircraft nasal file)
|
|
#
|
|
# var routedNotifications = [notifications.TacticalNotification.new()];
|
|
# var incomingBridge = emesary_mp_bridge.IncomingMPBridge.startMPBridge(routedNotifications);
|
|
# var outgoingBridge = emesary_mp_bridge.OutgoingMPBridge.new("F-15mp",routedNotifications);
|
|
#------------------------------------------------------------------
|
|
#
|
|
# NOTES:
|
|
# * Aircraft do not need to have both an incoming and outgoing bridge, but it is usual.
|
|
#
|
|
# * Only the notifications specified will be routed via the bridge.
|
|
#
|
|
# * Once routed a message will by default not be re-rerouted again by the outgoing bridge.
|
|
#
|
|
# * Transmit frequency and message lifetime may need to be tuned.
|
|
#
|
|
# * IsDistinct messages must be absolute and self contained as a later message will
|
|
# supercede any earlier ones in the outgoing queue (possibly prior to receipt)
|
|
#
|
|
# * Use the message type and ident to identify distinct messages
|
|
#
|
|
# * The outgoing 'port' is a multiplay/emesary/bridge index, however any available string property
|
|
# can be used by specifying it in the construction of the incoming or outgoing bridge.
|
|
# NOTE: This should not often be changed as it different versions of FG or model will
|
|
# have to use the same properties to be able to communicate
|
|
#
|
|
# * multiplay/emesary/bridge-type is used to identify the bridge that is in use. This is to
|
|
# protect against bridges being used for different purposes by different models.
|
|
#
|
|
# * The bridge-type property should contain an ID that identifies the purpose
|
|
# and thereore the message set that the bridge will be using.
|
|
#
|
|
# - ! is used as a seperator between the elements that are used to send the
|
|
# notification (typeid, sequence, notification)
|
|
# - There is an extra ! at the start of the message that is used to indicate protocol version.
|
|
# - ; is used to seperate serialzied elemnts of the notification
|
|
|
|
# General Notes
|
|
#----------------------------------------------------------------------
|
|
# Outgoing messages are sent in a scheduled manner, usually once per
|
|
# second, and each message has a lifetime (to allow for propogation to
|
|
# all clients over UDP). Clients will ignore messages that they have
|
|
# already received (based on the sequence id).
|
|
|
|
# The incoming bridge will usually be created part of the aircraft
|
|
# model file; it is important to understand that each AI/MP model will
|
|
# have an incoming bridge as each element in /ai/models needs its own
|
|
# bridge to keep up with the incoming sequence id. This scheme may not
|
|
# work properly as it relies on the model being loaded which may only
|
|
# happen when visible so it may be necessary to track AI models in a
|
|
# seperate instantiatable incoming bridge manager.
|
|
#
|
|
# The outgoing bridge would usually be created within the aircraft loading Nasal.
|
|
var EmesaryMPBridgeDefaultPropertyIndex=19;
|
|
|
|
var OutgoingMPBridge =
|
|
{
|
|
SeperatorChar : "!",
|
|
MessageEndChar : "~",
|
|
StartMessageIndex : 11,
|
|
DefaultMessageLifetimeSeconds : 10,
|
|
MPStringMaxLen: 128,
|
|
|
|
new: func(_ident, _notifications_to_bridge=nil, _mpidx=19, _root="", _transmitter=nil, _propertybase="emesary/bridge")
|
|
{
|
|
if (_transmitter == nil)
|
|
_transmitter = emesary.GlobalTransmitter;
|
|
|
|
print("OutgoingMPBridge created for "~_ident," mp=",_mpidx);
|
|
var new_class = emesary.Recipient.new("OutgoingMPBridge "~_ident);
|
|
|
|
|
|
if (_notifications_to_bridge == nil)
|
|
new_class.NotificationsToBridge = [];
|
|
else
|
|
new_class.NotificationsToBridge = _notifications_to_bridge;
|
|
|
|
new_class.NotificationsToBridge_Lookup = {};
|
|
|
|
foreach (var n ; new_class.NotificationsToBridge) {
|
|
print(" ",_ident," outwards bridge[",n,"] notifications of type --> ",n.NotificationType, " Id ",n.TypeId);
|
|
n.MessageIndex = OutgoingMPBridge.StartMessageIndex;
|
|
new_class.NotificationsToBridge_Lookup[n.TypeId] = n;
|
|
}
|
|
new_class.MPout = "";
|
|
new_class.MPidx = _mpidx;
|
|
new_class.MessageLifeTime = 10; # seconds
|
|
new_class.OutgoingList = [];
|
|
new_class.Transmitter = _transmitter;
|
|
new_class.TransmitRequired=0;
|
|
new_class.MpVariable = _root~"sim/multiplay/"~_propertybase~"["~new_class.MPidx~"]";
|
|
new_class.TransmitterActive = 0;
|
|
new_class.TransmitFrequencySeconds = 1;
|
|
new_class.trace = 0;
|
|
new_class.MPStringMaxLen = OutgoingMPBridge.MPStringMaxLen;
|
|
|
|
new_class.TransmitTimer =
|
|
maketimer(6, func
|
|
{
|
|
if (new_class.TransmitterActive)
|
|
new_class.Transmit();
|
|
else
|
|
new_class.TransmitEnd();
|
|
|
|
new_class.TransmitTimer.restart(new_class.TransmitFrequencySeconds);
|
|
});
|
|
|
|
new_class.Delete = func
|
|
{
|
|
if (me.Transmitter != nil) {
|
|
me.Transmitter.DeRegister(me);
|
|
me.Transmitter = nil;
|
|
}
|
|
};
|
|
new_class.AddMessage = func(m)
|
|
{
|
|
append(me.NotificationsToBridge, m);
|
|
};
|
|
|
|
#-------------------------------------------
|
|
# Receive override:
|
|
new_class.Receive = func(notification)
|
|
{
|
|
if (notification.FromIncomingBridge)
|
|
return emesary.Transmitter.ReceiptStatus_NotProcessed;
|
|
|
|
#print("Receive ",notification.NotificationType," ",notification.Ident);
|
|
for (var idx = 0; idx < size(me.NotificationsToBridge); idx += 1) {
|
|
if (me.NotificationsToBridge[idx].NotificationType == notification.NotificationType) {
|
|
me.NotificationsToBridge[idx].MessageIndex += 1;
|
|
notification.MessageExpiryTime = systime()+me.MessageLifeTime;
|
|
notification.Expired = 0;
|
|
notification.BridgeMessageId = me.NotificationsToBridge[idx].MessageIndex;
|
|
notification.BridgeMessageNotificationTypeId = me.NotificationsToBridge[idx].TypeId;
|
|
#
|
|
# The message key is a composite of the type and ident to allow for multiple senders
|
|
# of the same message type.
|
|
#print("Received ",notification.BridgeMessageNotificationTypeKey," expire=",notification.MessageExpiryTime);
|
|
me.AddToOutgoing(notification);
|
|
return emesary.Transmitter.ReceiptStatus_Pending;
|
|
}
|
|
}
|
|
return emesary.Transmitter.ReceiptStatus_NotProcessed;
|
|
};
|
|
|
|
new_class.AddToOutgoing = func(notification)
|
|
{
|
|
if (notification.IsDistinct) {
|
|
for (var idx = 0; idx < size(me.OutgoingList); idx += 1) {
|
|
if (me.trace)
|
|
print("Compare [",idx,"] qId=",me.OutgoingList[idx].GetBridgeMessageNotificationTypeKey() ," noti --> ",notification.GetBridgeMessageNotificationTypeKey());
|
|
if (me.OutgoingList[idx].GetBridgeMessageNotificationTypeKey() == notification.GetBridgeMessageNotificationTypeKey()) {
|
|
if (me.trace)
|
|
print(" --> Update ",me.OutgoingList[idx].GetBridgeMessageNotificationTypeKey() ," noti --> ",notification.GetBridgeMessageNotificationTypeKey());
|
|
me.OutgoingList[idx]= notification;
|
|
me.TransmitterActive = size(me.OutgoingList);
|
|
return;
|
|
}
|
|
}
|
|
} else
|
|
if (me.trace)
|
|
print("Not distinct, adding always");
|
|
|
|
if (me.trace)
|
|
print(" --> Added ",notification.GetBridgeMessageNotificationTypeKey());
|
|
append(me.OutgoingList, notification);
|
|
me.TransmitterActive = size(me.OutgoingList);
|
|
};
|
|
new_class.Transmit = func
|
|
{
|
|
var outgoing = "";
|
|
var cur_time=systime();
|
|
var first_time = 1;
|
|
me.OutgoingListNew = [];
|
|
for (var idx = 0; idx < size(me.OutgoingList); idx += 1) {
|
|
var sect = "";
|
|
var notification = me.OutgoingList[idx];
|
|
|
|
if (!notification.Expired and notification.MessageExpiryTime > cur_time) {
|
|
if (notification["sect"] == nil) {
|
|
# This is first time attempting to transmit this notification
|
|
var encval="";
|
|
|
|
var eidx = 0;
|
|
notification.Expired = 0;
|
|
|
|
foreach(var p ; notification.bridgeProperties()) {
|
|
var nv = p.getValue();
|
|
encval = encval ~ nv;
|
|
eidx += 1;
|
|
}
|
|
# !idx!typ!encv~
|
|
sect = sprintf("%s%s%s%s%s%s%s",
|
|
OutgoingMPBridge.SeperatorChar, emesary.BinaryAsciiTransfer.encodeInt(notification.BridgeMessageId,4),
|
|
OutgoingMPBridge.SeperatorChar, emesary.BinaryAsciiTransfer.encodeInt(notification.BridgeMessageNotificationTypeId,1),
|
|
OutgoingMPBridge.SeperatorChar, encval, OutgoingMPBridge.MessageEndChar);
|
|
} else {
|
|
# This notification has already been coded, but was previously not sent due to too little space.
|
|
sect = notification.sect;
|
|
}
|
|
if (size(outgoing) + size(sect) < me.MPStringMaxLen) {
|
|
outgoing = outgoing~sect;
|
|
} else {
|
|
if (first_time) {
|
|
print("Emesary: ERROR [",me.Ident,"] out of space for ",notification.NotificationType, " transmitted count=",idx, " queue size ",size(me.OutgoingList));
|
|
first_time = 0;
|
|
}
|
|
#notification.MessageExpiryTime = systime()+me.MessageLifeTime;
|
|
#break;
|
|
notification.sect = sect;
|
|
append(me.OutgoingListNew, notification);
|
|
}
|
|
} else {
|
|
notification.Expired = 1;
|
|
}
|
|
}
|
|
me.OutgoingList = me.OutgoingListNew;
|
|
me.TransmitterActive = size(me.OutgoingList);
|
|
setprop(me.MpVariable,outgoing);
|
|
};
|
|
new_class.TransmitEnd = func
|
|
{
|
|
if (getprop(me.MpVariable) != "") {
|
|
setprop(me.MpVariable,"");
|
|
}
|
|
};
|
|
new_class.Transmitter.Register(new_class);
|
|
new_class.TransmitTimer.restart(new_class.TransmitFrequencySeconds);
|
|
return new_class;
|
|
},
|
|
|
|
};
|
|
|
|
|
|
#
|
|
#
|
|
# one of these for each model instantiated in the model XML - it will
|
|
# route messages to
|
|
var IncomingMPBridge =
|
|
{
|
|
trace : 0,
|
|
new: func(_ident, _notifications_to_bridge=nil, _mpidx=19, _transmitter=nil, _propertybase="emesary/bridge")
|
|
{
|
|
if (_transmitter == nil)
|
|
_transmitter = emesary.GlobalTransmitter;
|
|
|
|
print("IncomingMPBridge created for "~_ident," mp=",_mpidx, " using Transmitter ",_transmitter.Ident, " with property base sim/multiplayer/"~_propertybase);
|
|
|
|
var new_class = emesary.Transmitter.new("IncominggMPBridge "~_ident);
|
|
|
|
if (_notifications_to_bridge == nil)
|
|
new_class.NotificationsToBridge = [];
|
|
else
|
|
new_class.NotificationsToBridge = _notifications_to_bridge;
|
|
|
|
new_class.NotificationsToBridge_Lookup = {};
|
|
|
|
foreach (var n ; new_class.NotificationsToBridge) {
|
|
print(" Incoming bridge notification type --> ",n.NotificationType);
|
|
var new_n = {parents: [n]};
|
|
new_n.IncomingMessageIndex = OutgoingMPBridge.StartMessageIndex;
|
|
new_class.NotificationsToBridge_Lookup[n.TypeId] = new_n;
|
|
}
|
|
|
|
new_class.MPout = "";
|
|
new_class.MPidx = _mpidx;
|
|
new_class.MPpropertyBase = _propertybase;
|
|
new_class.MessageLifeTime = OutgoingMPBridge.DefaultMessageLifetimeSeconds; # seconds
|
|
new_class.OutgoingList = [];
|
|
new_class.Transmitter = _transmitter;
|
|
new_class.MpVariable = "";
|
|
|
|
new_class.Connect = func(_root)
|
|
{
|
|
me.MpVariable = _root~"sim/multiplay/"~new_class.MPpropertyBase~"["~new_class.MPidx~"]";
|
|
me.CallsignPath = _root~"callsign";
|
|
me.PropertyRoot = _root;
|
|
me.mp_listener = setlistener(me.MpVariable, func(v)
|
|
{
|
|
#print("incoming ",getprop(me.CallsignPath)," -->",me.PropertyRoot," v=",v.getValue());
|
|
me.ProcessIncoming(v.getValue());
|
|
},0,0);
|
|
};
|
|
new_class.setprop = func(property, value){
|
|
if (IncomingMPBridge.trace == 2)
|
|
print("setprop ",new_class.PropertyRoot~property," = ",value);
|
|
setprop(new_class.PropertyRoot~property,value);
|
|
};
|
|
new_class.GetCallsign = func
|
|
{
|
|
return getprop(me.CallsignPath);
|
|
};
|
|
new_class.AddMessage = func(m)
|
|
{
|
|
append(me.NotificationsToBridge, m);
|
|
};
|
|
|
|
new_class.Remove = func
|
|
{
|
|
print("Emesary IncomingMPBridge Remove() ",me.Ident," Property: ",me.MpVariable);
|
|
me.Transmitter.DeRegister(me);
|
|
if (me["mp_listener"] != nil)
|
|
removelistener(me.mp_listener);
|
|
me.mp_listener = nil;
|
|
};
|
|
|
|
#-------------------------------------------
|
|
# Receive override:
|
|
new_class.ProcessIncoming = func(encoded_val)
|
|
{
|
|
if (encoded_val == "")
|
|
return;
|
|
|
|
if(right(encoded_val,1) != OutgoingMPBridge.MessageEndChar)
|
|
printf("Error: emesary.IncomingBridge.ProcessIncoming Missing endChar. From %s. Message=%s",me.GetCallsign(),encoded_val);
|
|
|
|
var encoded_notifications = split(OutgoingMPBridge.MessageEndChar, encoded_val);
|
|
for (var idx = 0; idx < size(encoded_notifications); idx += 1) {
|
|
if (encoded_notifications[idx] == "")
|
|
continue ;
|
|
# get the message parts
|
|
var encoded_notification = split(OutgoingMPBridge.SeperatorChar, encoded_notifications[idx]);
|
|
if (size(encoded_notification) < 4)
|
|
print("Error: emesary.IncomingBridge.ProcessIncoming bad msg ",encoded_notifications[idx]);
|
|
else {
|
|
var msg_idx = emesary.BinaryAsciiTransfer.decodeInt(encoded_notification[1],4,0).value;
|
|
var msg_type_id = emesary.BinaryAsciiTransfer.decodeInt(encoded_notification[2],1,0).value;
|
|
var bridged_notification = new_class.NotificationsToBridge_Lookup[msg_type_id];
|
|
if (bridged_notification == nil) {
|
|
print("Error: emesary.IncomingBridge.ProcessIncoming invalid type_id ",msg_type_id);
|
|
} else {
|
|
bridged_notification.FromIncomingBridge = 1;
|
|
bridged_notification.Callsign = me.GetCallsign();
|
|
if(IncomingMPBridge.trace>1)
|
|
print("ProcessIncoming callsign=",bridged_notification.Callsign," ",me.PropertyRoot, " msg_type=",msg_type_id," idx=",msg_idx, " bridge_idx=",bridged_notification.IncomingMessageIndex);
|
|
if (msg_idx > bridged_notification.IncomingMessageIndex) {
|
|
if(IncomingMPBridge.trace==1)
|
|
print("ProcessIncoming callsign=",bridged_notification.Callsign," ",me.PropertyRoot, " msg_type=",msg_type_id," idx=",msg_idx, " bridge_idx=",bridged_notification.IncomingMessageIndex);
|
|
var msg_body = encoded_notification[3];
|
|
if (IncomingMPBridge.trace > 2)
|
|
print("received idx=",msg_idx," ",msg_type_id,":",bridged_notification.NotificationType);
|
|
|
|
# populate fields
|
|
var bridgedProperties = bridged_notification.bridgeProperties();
|
|
var msglen = size(msg_body);
|
|
if (IncomingMPBridge.trace > 2)
|
|
print("Process ",msg_body," len=",msglen, " BPsize = ",size(bridgedProperties));
|
|
var pos = 0;
|
|
for (var bpi = 0; bpi < size(bridgedProperties); bpi += 1) {
|
|
if (pos < msglen) {
|
|
if (IncomingMPBridge.trace > 2)
|
|
print("dec: pos ",pos);
|
|
var bp = bridgedProperties[bpi];
|
|
dv = bp.setValue(msg_body, me, pos);
|
|
if (IncomingMPBridge.trace > 2)
|
|
print(" --> next pos ", dv.pos);
|
|
|
|
if (dv.pos == pos or dv.pos > msglen)
|
|
break;
|
|
pos = dv.pos;
|
|
} else {
|
|
print("Error: emesary.IncomingBridge.ProcessIncoming: [",bridged_notification.NotificationType,"] supplementary encoded value at position ",bpi);
|
|
break;
|
|
}
|
|
}
|
|
# maybe extend the bridge to allow certain notifications to only be routed to a specific player;
|
|
# i.e.
|
|
# (notification.Callsign == nil or notification.Callsign == getprop("/sim/multiplay/callsign"))
|
|
|
|
if (bridged_notification.Ident == "none")
|
|
bridged_notification.Ident = "mp-bridge";
|
|
|
|
bridged_notification.IncomingMessageIndex = msg_idx;
|
|
me.Transmitter.NotifyAll(bridged_notification);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
foreach (var n; new_class.NotificationsToBridge) {
|
|
print("IncomingBridge: ",n.NotificationType);
|
|
}
|
|
return new_class;
|
|
},
|
|
connectIncomingBridge : func(path, notification_list, mpidx, transmitter, _propertybase){
|
|
var incomingBridge = emesary_mp_bridge.IncomingMPBridge.new(path, notification_list, mpidx, transmitter, _propertybase);
|
|
|
|
incomingBridge.Connect(path~"/");
|
|
if (me.incomingBridgeList[path] == nil) {
|
|
me.incomingBridgeList[path] = [incomingBridge];
|
|
} else {
|
|
append(me.incomingBridgeList[path],incomingBridge);
|
|
}
|
|
return incomingBridge;
|
|
},
|
|
|
|
#
|
|
# Each multiplayer object will have its own incoming bridge. This is necessary to allow message ID
|
|
# tracking (as the bridge knows which messages have been already processed)
|
|
# Whenever a client connects over MP a new bridge is instantiated
|
|
startMPBridge : func(notification_list, mpidx=19, transmitter=nil, _propertybase="emesary/bridge")
|
|
{
|
|
me.incomingBridgeList = {};
|
|
|
|
#
|
|
# Create bridge whenever a client connects
|
|
#
|
|
setlistener("/ai/models/model-added", func(v)
|
|
{
|
|
# Model added will be eg: /ai[0]/models[0]/multiplayer[0]
|
|
var path = v.getValue();
|
|
|
|
# Ensure we only handle multiplayer elements
|
|
if (find("/multiplayer",path) > 0) {
|
|
var callsign = getprop(path~"/callsign");
|
|
print("Creating Emesary MPBridge for ",path);
|
|
if (callsign == "" or callsign == nil)
|
|
callsign = path;
|
|
|
|
me.connectIncomingBridge(path, notification_list, mpidx, transmitter, _propertybase);
|
|
}
|
|
});
|
|
|
|
#
|
|
# when a client disconnects remove the associated bridge.
|
|
#
|
|
setlistener("/ai/models/model-removed", func(v){
|
|
var path = v.getValue();
|
|
var bridges = me.incomingBridgeList[path];
|
|
if (bridges != nil) {
|
|
foreach(bridge;bridges) {
|
|
bridge.Remove();
|
|
# print("Bridge removed for ",v.getValue());
|
|
}
|
|
me.incomingBridgeList[path] = nil;
|
|
}
|
|
});
|
|
},
|
|
};
|