Shooting SAMs now has option to set datalink channel in the priority dialog window.

This commit is contained in:
Nikolai V. Chr 2022-11-29 09:36:52 +01:00
parent 2220a819da
commit a9c7770cb2
20 changed files with 2475 additions and 233 deletions

View File

@ -378,7 +378,7 @@
<damage type="double">100</damage>
</sam>
<enemies>
<enemies>
<e1 userarchive="y" type="string">target1</e1>
<e2 userarchive="y" type="string">target2</e2>
<e3 userarchive="y" type="string">target3</e3>
@ -395,6 +395,17 @@
<priority type="int">0</priority>
<priority0 type="int">1</priority0>
<instrumentation>
<datalink>
<data type="int">0</data>
<power type="bool">true</power>
<channel type="int">1212</channel>
<power_prop type="string">/instrumentation/datalink/power</power_prop>
<channel_prop type="string">instrumentation/datalink/channel</channel_prop>
<receive_period type="double">1</receive_period>
</datalink>
</instrumentation>
<nasal>
<!--emesary><file>Aircraft/S-300/Nasal/emesary.nas</file></emesary>
@ -418,7 +429,11 @@
<armament>
<file>Aircraft/MIM-104D/Nasal/guided-missiles.nas</file>
</armament>
<datalink>
<file>Aircraft/MIM-104D/Nasal/datalink.nas</file>
</datalink>
<fire_control>
<file>Aircraft/MIM-104D/Nasal/fire-control-custom.nas</file>
<file>Aircraft/MIM-104D/Nasal/fire-control.nas</file>
</fire_control>
</nasal>

681
MIM-104D/Nasal/datalink.nas Normal file
View File

@ -0,0 +1,681 @@
#### Datalink
# Copyright 2020-2021 Colin Geniet.
# Licensed under the GNU General Public License 2.0 or any later version.
#### Usage
### Generalities
#
# The datalink protocol consists of a core protocol which implements a notion
# of datalink channel, and extensions which allow transmitting actual data.
### Core protocol usage:
#
# Define the following properties (must be defined at nasal loading time).
# * Mandatory
# /instrumentation/datalink/power_prop path to property indicating if datalink is on
# /instrumentation/datalink/channel_prop path to property containing datalink channel
# (the channel property can contain anything, and is transmitted/compared as a string).
# * Optional
# /instrumentation/datalink/receive_period = 1 receiving loop update rate
#
# Optional: Re-define the function
# datalink.can_transmit(callsign, mp_prop, mp_index)
#
# This function should return 'true' when the given aircraft is able to transmit over datalink to us.
# For instance, it can be used to check line of sight and maximum range.
# The default implementation always returns true (always able to transmit).
# Arguments are callsign, property node /ai/models/multiplayer[i], index of the former node.
#
#
# API:
# - get_data(callsign)
# Returns all datalink information about 'callsign' as an object, or nil if there is none.
# This object must not be modified.
# It contains the following methods:
# callsign(): The aircraft callsign (same as the argument of get_data()).
# index(): The aircraft index in /ai/models/multiplayer[i].
# on_link(): Returns a bool indicating whether 'callsign' is connected to this aircraft through datalink.
#
# Extensions can define other methods in this object.
#
# - get_connected_callsigns() / get_connected_indices()
# Returns a vector containing all callsigns, resp. indices
# in /ai/models/multiplayer[i], of aircrafts connected on datalink.
# Both vectors use the same order, i.e. get_connected_callsigns()[i]
# and get_connected_indices()[i] correspond to the same aircraft.
# Furthermore this order is stable (the relative order of two aircrafts
# does not change as long as neither disconnects from multiplayer).
#
# - get_all_callsigns()
# Returns a vector containing all callsigns of aircraft with any associated data.
# There is no guarantee on the order of callsigns.
#
# - send_data(data, timeout=nil)
# Send data on the datalink. 'data' is a hash of the form
# {
# <extension_name>: <extension_data>,
# ...
# }
# If 'timeout' is set, clear_data() will be called after this delay.
# Data sent with send_data() is deleted at the next call of send_data(), or by clear_data().
#
# - clear_data()
# Clear data transmitted by this aircraft.
#
# Important note:
# After a send_data(), and until the next send_data() or clear_data(),
# the datalink behaves as if you are continuously sending the same data.
# Thus, it is important to
# 1. either call send_data() regularly
# 2. or set the timeout argument of send_data()
### Extensions
### Aircraft contacts (extension name: "contacts")
#
# This extension allows to simulate an aircraft transmitting information about
# another aircraft (typically one tracked on radar). The position data is not
# actually transmitted (since everyone can access it from simulator internals).
#
## Receiving data
# This extension adds the following methods to the result of get_data("A"):
# tracked(): A bool indicating that some aircraft "B" connected on datalink
# is transmitting information about aircraft "A".
# iff(): One of IFF_UNKNOWN, IFF_HOSTILE, IFF_FRIENDLY, or nil if tracked() is false.
# Indicates the result of IFF interrogation of "A" by "B"
# IFF_UNKNOWN means that e.g. no IFF interrogation was performed.
# tracked_by(): The callsign of the transmitting aircraft ("A"), or nil if tracked() is false.
# tracked_by_index(): The index of the transmitting aircraft, or nil if tracked() is false.
# The index refers to property nodes /ai/models/multiplayer[i].
# is_known(): Equivalent to (on_link() or tracked()).
# Indicates if the position of this aircraft is supposed to be known
# (i.e. whether or not it should be displayed on a HSD or whatever).
# is_friendly(): Equivalent to (on_link() or iff() == IFF_FRIENDLY).
# is_hostile(): Equivalent to (!on_link() and iff() == IFF_HOSTILE).
#
## Sending data
# usage: send_data({ contacts: <contacts>, ...}, ...)
# where <contacts> is a vector of hashes of the form { callsign: <callsign>, [iff: <iff>,] }.
# <callsign> is the multiplayer callsign of the tracked aircraft.
# <iff> (optional) is one of IFF_UNKNOWN, IFF_HOSTILE, IFF_FRIENDLY
### Datalink identifier (extension name: "identifier")
#
# This extension allows each aircraft on datalink to transmit a personal
# identifier, e.g. the number of the aircraft in a flight.
#
## Receiving data
# This extension adds the method identifier() to the result of get_data(),
# which returns the identifier, or nil if there is none).
#
## Sending data
# Set the identifier with send_data({"identifier": <identifier>, ...});
# The identifier must be a string. It must not contain '!'.
### Coordinate transmission (extension name: "point")
#
# This extension allows each aircraft to broadcast a coordinate (geo.Coord object).
#
## Receiving data
# This extension adds the method point() to the result of get_data(),
# which results the transmitted geo.Coord object, or nil if there is none.
#
## Sending data
# Transmit a geo.Coord object <coord> with send_data({"point": <coord>, ...});
#### Protocol:
#
# Data is transmitted on MP generic string[7], with the following format:
# <channel>(!<data>)+
#
# <channel> is a hash of the datalink channel. See hash_channel() and check_channel_hash().
# Each <data> block corresponds to data sent by an extension.
# It starts with a prefix uniquely defining the extension.
# The rest of the block can contain any character (including non-ascii) except '!'.
#
# Remark: '!' as separator is specifically chosen to allow encoding with emesary.Transfer<type>.
#
# The current extension prefixes are the following:
# contacts: C
# identifier: I
# point: P
#### Extensions API
#
# Creating a new extension is done with
# register_extension(name, prefix, object, encode, decode)
# name the extension name, used as key in the 'data' argument of send_data().
# prefix the protocol prefix.
# class contact class parent.
# A class from which all contact objects will inherit.
# It must have an init() method, which is called whenever a contact is created.
#
# encode(data) extension encoding function.
# Must return the encoding of the extension data (i.e. <data> when calling
# send_data({name: <data>})) into a string, which may use any character except '!'.
# The extension prefix must not be part of the encoded string.
#
# decode(aircrafts_data, callsign, index, string) extension decoding function.
# 'aircrafts_data' is a hash from callsigns to contact objects (see below).
# 'callsign' is the callsign of the aircraft which transmitted this data.
# 'index' is the index of the aircraft which transmitted this data.
# 'string' is the data encoded by encode() and transmitted through datalink.
# Each contact in 'data' inherits from the core 'Contact' class, and the extension 'class'.
# decode() is expected to modify 'aircrafts_data', by possibly editing
# existing contacts and adding new ones. It should be careful when
# overwriting existing data in these contacts, including its own: decode()
# will be called several time on the same 'aircrafts_data' (once for each
# transmitting aircraft).
# The modified 'aircrafts_data' hash must be returned.
#
# decode() may use the following helper functions:
# add_if_missing(aircrafts_data, callsign):
# Create a new contact object for 'callsign' and add it to 'aircrafts_data',
# unless an entry for 'callsign' already exists. Returns the modified hash.
#### Version and changelog
# current: v1.1.0, minimum compatible: v1.0.0
#
## v1.1.0:
# Allow external transmission restrictions
# Make transmitting contact IFF optional
# Ensure personal identifier has no '!'
# '\n' is redundant for printf()
# Fix separator character in documentation
# Fix error when sending unknown extension
#
## v1.0.1:
# Add is_known(), is_friendly(), is_hostile() helpers to extension "contacts".
#
## v1.0.0: Initial version
# - Core protocol for datalink channel.
# - Extensions "contacts", "identifier", and "point".
### Parameters
#
# Remark: most parameters need to be the same on all aircrafts.
# Index of multiplayer string used to transmit datalink info.
# Must be the same for all aircrafts.
var mp_string = 7;
var mp_path = "sim/multiplay/generic/string["~mp_string~"]";
var channel_hash_period = 600;
var receive_period = getprop("/instrumentation/datalink/receive_period") or 1;
# Should be overwitten to add transmission restrictions.
var can_transmit = func(contact, mp_prop, mp_index) {
return 1;
}
### Properties
var input = {
power: getprop("/instrumentation/datalink/power_prop"),
channel: getprop("/instrumentation/datalink/channel_prop"),
ident: getprop("/instrumentation/datalink/identifier_prop"),
mp: mp_path,
models: "/ai/models",
callsign: "/sim/multiplay/callsign",
};
foreach (var name; keys(input)) {
if (input[name] != nil) {
input[name] = props.globals.getNode(input[name], 1);
}
}
#### Core protocol implementation
### Channel hash (based on iff.nas)
#
# Channel is hashed with current time (rounded to 10min) and own callsign.
var clean_callsign = func(callsign) {
return damage.processCallsign(callsign);
}
var my_callsign = func {
return clean_callsign(input.callsign.getValue());
}
# Time, rounded to 'channel_hash_period'. This is used to hash channel.
var get_time = func {
return int(math.floor(systime() / channel_hash_period) * channel_hash_period);
}
# Previous / next time (with channel_hash_period interval).
# This is used to give a bit of margin on the time check.
# (will work if system clocks are coordinated within 10min).
var get_prev_time = func { return get_time() - channel_hash_period; }
var get_next_time = func { return get_time() + channel_hash_period; }
var parse_hexadecimal = func(str) {
var res = 0;
for (var i=0; i<size(str); i+=1) {
res *= 10;
var c = str[i];
if (c >= 48 and c < 58) {
# digit
res += c - 48;
} elsif (c >= 65 and c < 71) {
# upper case letter
res += c - 55;
} elsif (c >= 97 and c < 103) {
# lower case letter
res += c - 87;
}
}
return res;
}
var _hash_channel = func(time, callsign, channel) {
# 5 hex digits (2^20) fit in 3 chars for emesary int encoding.
var hash = parse_hexadecimal(left(md5(time ~ callsign ~ channel), 5));
return emesary.TransferInt.encode(hash, 3);
}
# Hash channel (when sending).
var encode_channel = func(channel) {
return _hash_channel(get_time(), my_callsign(), channel);
}
# Check that the hash transmitted by aircraft 'callsign' is correct for 'channel'.
var check_channel = func(hash, callsign, channel) {
return hash == _hash_channel(get_time(), callsign, channel)
or hash == _hash_channel(get_prev_time(), callsign, channel)
or hash == _hash_channel(get_next_time(), callsign, channel);
}
### Contact object
var Contact = {
new: func(callsign) {
var c = {
# contact_parents is the list of all classes from which contacts inherit (for extensions).
parents: contact_parents,
_callsign: callsign,
};
# Initialize all inherited classes.
foreach (var class; contact_parents) {
call(class.init, [], c, nil, nil);
}
return c;
},
init: func { me._on_link = 0; },
callsign: func { return me._callsign; },
index: func { return callsign_to_index[me._callsign]; },
on_link: func { return me._on_link; },
set_on_link: func(b) { me._on_link = b; },
};
### Extensions
var extensions = {};
var extension_prefixes = {};
var max_prefix_length = 0;
var contact_parents = [Contact];
var register_extension = func(name, prefix, class, encode, decode) {
if (contains(extensions, name)) {
printf("Datalink: double registration of extension '%s'. Skipping.", name);
return -1;
}
if (contains(extension_prefixes, prefix)) {
printf("Datalink: double registration of extension prefix '%s'. Skipping.", name);
return -1;
}
extensions[name] = { prefix: prefix, encode: encode, decode: decode, };
extension_prefixes[prefix] = name;
max_prefix_length = math.max(max_prefix_length, size(prefix));
append(contact_parents, class);
return 0;
}
var data_separator = "!";
### Transmission
var clear_data = func {
send_data({});
}
var clear_timer = maketimer(1, clear_data);
clear_timer.singleShot = 1;
# Send data through datalink.
#
# timeout: if set, sent data will be cleared after this time (other aircrafts
# won't receive it anymore). Useful if 'send_data' is not called often.
var send_data = func(data, timeout=nil) {
if (!input.power.getBoolValue()) {
last_data = {};
input.mp.setValue("");
return;
}
# First encode channel
var str = encode_channel(input.channel.getValue());
# Then all extensions
last_data = data;
foreach(var ext; keys(data)) {
# Skip missing extensions with a warning
if (!contains(extensions, ext)) {
printf("Warning: unknown datalink extension %s in send_data().", ext);
continue;
}
str = str ~ data_separator ~ extensions[ext].prefix ~ extensions[ext].encode(data[ext]);
}
input.mp.setValue(str);
if (timeout != nil) {
clear_timer.restart(timeout);
}
}
# Used internally to update the channel/identifier while keeping the same data.
# Does not touch timeout.
var last_data = {};
var resend_data = func {
send_data(last_data);
}
# Very slow timer to ensure the channel hash is updated regularly.
# Only relevant if you never call send_data();
var hash_update_timer = maketimer(channel_hash_period/2, resend_data);
hash_update_timer.start();
### Receiving
# callsign to data hash
var aircrafts_data = {};
# List of callsigns / indices connected on datalink (index is for /ai/models/multiplayer[i]).
var connected_callsigns = [];
var connected_indices = [];
# Maintain callsign to multiplayer index hash
# (doesn't cost much since we already iterate over MP models).
var callsign_to_index = {};
var get_data = func(callsign) {
return aircrafts_data[callsign];
}
var get_connected_callsigns = func {
return connected_callsigns;
}
var get_connected_indices = func {
return connected_indices;
}
var get_all_callsigns = func {
return keys(aircrafts_data);
}
# Helper for modifying aircrafts_data.
var add_if_missing = func(aircrafts_data, callsign) {
if (!contains(aircrafts_data, callsign)) {
aircrafts_data[callsign] = Contact.new(callsign);
}
return aircrafts_data;
}
var receive_loop = func {
var my_channel = input.channel.getValue();
aircrafts_data = {};
connected_callsigns = [];
connected_indices = [];
var mp_models = input.models.getChildren("multiplayer");
foreach(var mp; mp_models) {
var idx = mp.getIndex();
if (!mp.getValue("valid")) continue;
var callsign = mp.getValue("callsign");
if (callsign == nil) continue;
callsign_to_index[callsign] = idx;
var data = mp.getValue(mp_path);
if (data == nil) continue;
# Split channel part and data part
var tokens = split(data_separator, data);
# Check channel
if (!check_channel(tokens[0], callsign, my_channel)) continue;
# We check this _after_ the channel. Checking the channel is quite cheap,
# and we don't know how slow this function is, it might have a get_cart_ground_intersection()
if (!can_transmit(callsign, mp, idx)) continue;
# Add to list of connected aircrafts.
append(connected_callsigns, callsign);
append(connected_indices, idx);
# Add to data
aircrafts_data = add_if_missing(aircrafts_data, callsign);
aircrafts_data[callsign].set_on_link(1);
# Parse extensions data
for (var i=1; i<size(tokens); i+=1) {
var extension = nil;
# Identify extension prefix. This is not very clever code, but
# realistically it doesn't matter since prefixes are very short.
var len = 1;
for (; len <= max_prefix_length; len += 1) {
if (len > size(tokens)) break;
var prefix = left(tokens[i], len);
if (contains(extension_prefixes, prefix)) {
extension = extension_prefixes[prefix];
break;
}
}
# Unknown extension, skip
if (extension == nil) continue;
# Remove prefix
var data = substr(tokens[i], len);
# Decode
aircrafts_data = extensions[extension].decode(aircrafts_data, callsign, data);
}
}
}
var receive_timer = maketimer(receive_period, receive_loop);
# Start / stop listener
setlistener(input.power, func (node) {
if (node.getBoolValue()) {
receive_timer.start();
resend_data(); # Sets channel/identifier
} else {
receive_timer.stop();
aircrafts_data = {};
clear_data();
}
}, 1, 0);
# Listener to resend data so as to update the channel.
setlistener(input.channel, resend_data);
#### Extensions
## Identifier
var ContactIdentifier = {
init: func {
me._identifier = nil;
},
set_identifier: func(ident) {
me._identifier = ident;
},
identifier: func {
return me._identifier;
},
};
var encode_identifier = func(ident) {
# Force string conversion
ident = ""~ident;
if (find("!", ident) >= 0) {
printf("Datalink: Identifier is not allowed to contain '!': %s.", ident);
return "";
} else {
return ident;
}
}
var decode_identifier = func(aircrafts_data, callsign, str) {
aircrafts_data = add_if_missing(aircrafts_data, callsign);
aircrafts_data[callsign].set_identifier(str);
return aircrafts_data;
}
register_extension("identifier", "I", ContactIdentifier, encode_identifier, decode_identifier);
## Contacts
# IFF status transmitted over datalink.
var IFF_UNKNOWN = 0; # Unknown status
var IFF_HOSTILE = 1; # Considered hostile (no response to IFF).
var IFF_FRIENDLY = 2; # Friendly, because positive IFF identification.
# This is also the priority order for IFF reports in case of conflicts:
# e.g. a contact will be reported as friendly if anyone on datalink reports it as friendly.
var ContactTracked = {
init: func {
me._tracked_by = nil;
me._iff = IFF_UNKNOWN;
},
set_tracked_by: func(callsign) {
me._tracked_by = callsign;
},
set_iff: func(iff) {
# Priority order on IFF values (friendly, then hostile, then no data).
me._iff = math.max(me._iff, iff);
},
tracked: func {
return me._tracked_by != nil;
},
tracked_by: func {
return me._tracked_by;
},
tracked_by_index: func {
return (me._tracked_by != nil) ? callsign_to_index[me._tracked_by] : nil;
},
iff: func {
return me._iff;
},
is_known: func {
return me.on_link() or me.tracked();
},
is_friendly: func {
return me.on_link() or me.iff() == IFF_FRIENDLY;
},
is_hostile: func {
return !me.on_link() and me.iff() == IFF_HOSTILE;
},
};
# Contact encoding: callsign + bits
# callsign: the callsign encoded with emesary.TransferString
# bits: bitfield |xxxxxxff| (left is most significant)
# f: IFF, x: unused
# encoded with emesary.TransferByte
# Additional values may be appended for extensions.
var encode_contact = func(contact) {
# Encode bitfield
var bits = contact["iff"] != nil ? contact.iff : IFF_UNKNOWN;
return emesary.TransferString.encode(clean_callsign(contact.callsign))
~ emesary.TransferByte.encode(bits);
}
var decode_contact = func(str) {
var res = {};
var dv = emesary.TransferString.decode(str, 0);
res.callsign = dv.value;
dv = emesary.TransferByte.decode(str, dv.pos);
var bits = dv.value;
res.iff = math.mod(bits, 4);
return res;
}
# Special character, won't be used by emesary encoding.
var contacts_separator = "#";
var encode_contacts = func(contacts) {
var str = "";
foreach (var contact; contacts) {
str = str~encode_contact(contact)~contacts_separator;
}
return str;
}
var decode_contacts = func(aircrafts_data, callsign, str) {
var contacts = split(contacts_separator, str);
foreach (var contact; contacts) {
if (contact == "") continue;
var res = decode_contact(contact);
aircrafts_data = add_if_missing(aircrafts_data, res.callsign);
aircrafts_data[res.callsign].set_iff(res.iff);
aircrafts_data[res.callsign].set_tracked_by(callsign);
}
return aircrafts_data;
}
register_extension("contacts", "C", ContactTracked, encode_contacts, decode_contacts);
## Coordinate
var ContactPoint = {
init: func {
me._point = nil;
},
set_point: func(point) {
me._point = point;
},
point: func {
return me._point;
},
};
var encode_point = func(coord) {
return emesary.TransferCoord.encode(coord);
}
var decode_point = func(aircrafts_data, callsign, str) {
var coord = emesary.TransferCoord.decode(str, 0).value;
aircrafts_data = add_if_missing(aircrafts_data, callsign);
aircrafts_data[callsign].set_point(coord);
return aircrafts_data;
}
register_extension("point", "P", ContactPoint, encode_point, decode_point);

View File

@ -0,0 +1,44 @@
################### SAM INFO
var setupTime = 300;#minimum 'launcher_tilt_time' secs no matter what, due to anim and stuff.
var reload_time = 900;
var launcher_final_tilt_deg = 45;
var launcher_start_tilt_deg = 0;
var launcher_tilt_time = 15;
var sam_align_to_target = 0;
var launcher_align_to_target = 1;
var align_speed_dps = 20;
var radar_elevation_above_terrain_m = 25;
var radar_lowest_pitch = -5;# 0.5 degs = roughly 925 feet at 20 nm, 25 feet at half a nm. # 0.35 = roughly 925 feet at 20 nm, 25 feet at half a nm.
#reaction tme for s-300p is 28 secs acording to http://www.astronautix.com/s/s-300p.html
# sounds a bit high for pmu, as its 4 secs for s-400
################### CIWS INFO
var ciws_installed = 0;
var ciws_domain_nm = 1.50; #range where it can kill
var ciws_chance = 0.20; #chance to get a kill at 0nm distance
var ciws_burst_rounds = 60;#how many rounds in a burst
var ciws_shell = 15;#from lookup table in damage.nas
var ROUNDS_init = 30;
var ROUNDS = ROUNDS_init;#CIWS bursts remaining
################### MISSILE INFO
var NUM_MISSILES = 3; # total carried minus 1
var missile_name = "GEM";
var missile_brevity = "GEM/T";
var missile_max_distance = 43; #max distance of target in nm when it will fire
var missile_min_distance = 0.5; #minimum distance in nm when it will fire
var lockon_time = 12; #time in seconds it takes to lock on and get a firing solution on a target
var fire_minimum_interval = 7;# time since last track was initiated till a new can be initiated
var same_target_max_missiles = 1;# max number of missiles in air against same target
var isInEngagementEnvelope = func (target_radial_airspeed, target_ground_distance, target_relative_altitude) {
return 1;
}
var midflight = nil;

View File

@ -20,49 +20,6 @@ var ACTIVE_MISSILE = 0;
var semi_active_track = nil;# with multiple missiles flying in semi-active-radar mode, this can change rather fast. Which means pilots wont get a steady tone, but disrupted tones. (tradeoff)
var mutexLock = thread.newlock();
################### SAM INFO
var setupTime = 300;#minimum 'launcher_tilt_time' secs no matter what, due to anim and stuff.
var reload_time = 900;
var launcher_final_tilt_deg = 45;
var launcher_start_tilt_deg = 0;
var launcher_tilt_time = 15;
var sam_align_to_target = 0;
var launcher_align_to_target = 1;
var align_speed_dps = 20;
var radar_elevation_above_terrain_m = 25;
var radar_lowest_pitch = -5;# 0.5 degs = roughly 925 feet at 20 nm, 25 feet at half a nm. # 0.35 = roughly 925 feet at 20 nm, 25 feet at half a nm.
#reaction tme for s-300p is 28 secs acording to http://www.astronautix.com/s/s-300p.html
# sounds a bit high for pmu, as its 4 secs for s-400
################### CIWS INFO
var ciws_installed = 0;
var ciws_domain_nm = 1.50; #range where it can kill
var ciws_chance = 0.20; #chance to get a kill at 0nm distance
var ciws_burst_rounds = 60;#how many rounds in a burst
var ciws_shell = 15;#from lookup table in damage.nas
var ROUNDS_init = 30;
var ROUNDS = ROUNDS_init;#CIWS bursts remaining
################### MISSILE INFO
var NUM_MISSILES = 3; # total carried minus 1
var missile_name = "GEM";
var missile_brevity = "GEM/T";
var missile_max_distance = 43; #max distance of target in nm when it will fire
var missile_min_distance = 0.5; #minimum distance in nm when it will fire
var lockon_time = 12; #time in seconds it takes to lock on and get a firing solution on a target
var fire_minimum_interval = 7;# time since last track was initiated till a new can be initiated
var same_target_max_missiles = 1;# max number of missiles in air against same target
var isInEngagementEnvelope = func (target_radial_airspeed, target_ground_distance, target_relative_altitude) {
return 1;
}
var midflight = nil;
##########################################################################
######################## Common SAM code ###########################
@ -70,6 +27,8 @@ var midflight = nil;
################### VARIOUS
setprop("sim/multiplay/generic/float[7]", 0);# tracking radar vert rotation
setprop("sim/multiplay/generic/float[8]", 0);# tracking radar horiz rotation
setprop("sim/multiplay/generic/float[5]", 0);# launcher vert rotation
setprop("sim/multiplay/generic/float[6]", 0);# launcher horiz rotation
var start_time = systime();
@ -130,7 +89,7 @@ var buildTargetList = func {
};
var lookup = func (sign) {
foreach (tgt ; targetsV2.vector) {
if (tgt.callsign == sign) {
return tgt;
@ -168,9 +127,11 @@ var scan = func() {
buildTargetList();
if ( getprop("/carrier/sunk") == 1 or getprop("/carrier/disabled") == 1) {
setprop("sim/multiplay/generic/string[6]", "");
datalink.clear_data();
return;
}
if (systime() - start_time < setupTime ) {
printf("Seconds till activation: %.1f", setupTime - (systime() - start_time));
setprop("sam/timeleft", setupTime - (systime() - start_time));
@ -184,8 +145,11 @@ var scan = func() {
setprop("sim/multiplay/generic/float[5]", launcher_final_tilt_deg);
}
setprop("sim/multiplay/generic/float[6]", 0);
setprop("sim/multiplay/generic/float[7]", 0);
setprop("sim/multiplay/generic/float[8]", 0);
setprop("sim/multiplay/generic/string[6]", "");
setprop("/sim/multiplay/generic/int[2]", 1);
datalink.clear_data();
return;
}
setprop("sim/multiplay/generic/float[5]", launcher_final_tilt_deg);
@ -214,12 +178,12 @@ var scan = func() {
}
prio_vector = sort(prio_vector, func (a,b) {if (a[2]>b[2]){return -1;} elsif (a[2]<b[2]) {return 1;} else {return 0;}});
foreach(var mp; prio_vector) {
#### DO WE HAVE FIRING SOLUTION... ####
# is plane in range, do we still have missiles, and is a missile already inbound, and has it been 4 seconds since the last missile launch?
var trigger = mp[1];
#print("dist to target = " ~ dist_to_target);
var lu = lookup(mp[0].getNode("callsign").getValue());
if ( launch_in_progress == 0 and trigger == true and lu != nil and lu.in_air < same_target_max_missiles and !lu.tracking and ACTIVE_MISSILE <= NUM_MISSILES and ( systime() - missile_delay_time > fire_minimum_interval ) ) { #
#### ... FOR THE MISSILE ####
@ -229,7 +193,7 @@ var scan = func() {
armament.contact = radar_logic.Contact.new(mp[0], AIR);
launch_in_progress = 1;
missile_launch(mp[0], systime(), my_pos);
} elsif ( ciws_installed and trigger == -1 and ROUNDS > 0 and ( systime() - ciws_delay_time > 1.0 ) and lu != nil) {
#### ... FOR THE CIWS ####
var contact = radar_logic.Contact.new(mp[0], AIR);
@ -263,7 +227,7 @@ var scan = func() {
reloading = 1;
reload_starting = systime();
}
}
}
if (lu != nil and lu.tracking) {
radarOn = 1;
}
@ -309,8 +273,10 @@ var clearSingleLock = func () {
thread.lock(mutexLock);
if (semi_active_track == nil) {
setprop("sim/multiplay/generic/string[6]", "");
datalink.clear_data();
} else {
setprop("sim/multiplay/generic/string[6]", left(md5(semi_active_track), 4));
datalink.send_data({"contacts":[{"callsign":semi_active_track,"iff":0}]});
}
thread.unlock(mutexLock);
}
@ -326,7 +292,7 @@ var fire_control = func(mp, my_pos) {
var ufo_pos = geo.Coord.new().set_latlon(mp.getNode("position/latitude-deg").getValue(),mp.getNode("position/longitude-deg").getValue(),(mp.getNode("position/altitude-ft").getValue() * FT2M));
var target_distance = my_pos.direct_distance_to(ufo_pos);
var target_altitude = mp.getNode("position/altitude-ft").getValue();
# is this plane a friend or foe?
var lu = lookup(mp.getNode("callsign").getValue());
if ( lu == nil ) { return [mp,false,0,0]; }
@ -335,11 +301,11 @@ var fire_control = func(mp, my_pos) {
var target_ground_distance = my_pos.distance_to(ufo_pos);
var target_heading = mp.getNode("orientation/true-heading-deg").getValue();
var relative_bearing = math.abs(geo.normdeg180(ufo_pos.course_to(my_pos) - target_heading));
var target_relative_altitude = target_altitude - my_pos.alt()*M2FT;
var target_radial_airspeed = math.abs(math.abs(((relative_bearing / 90 ) - 1) * target_airspeed));#200kt against us = 200kt, 200kt at angle of us = 0kt, 200kt away from us = 200kt
var target_relative_pitch = vector.Math.getPitch(my_pos, ufo_pos);
# can the radar see it?
var data_link_match = false;
@ -349,7 +315,7 @@ var fire_control = func(mp, my_pos) {
# #print('tgt confirmed via datalink');
# }
# }
var visible = radar_logic.isNotBehindTerrain(mp);
@ -358,9 +324,10 @@ var fire_control = func(mp, my_pos) {
var radarSeeTarget = 1;
if ( data_link_match == false ) {
if ( visible[0] == false ) {radarSeeTarget = 0 }
if ( target_relative_pitch < radar_lowest_pitch ) { radarSeeTarget = 0 } #
if ( target_relative_pitch < radar_lowest_pitch ) { radarSeeTarget = 0 } #
if ( target_distance*M2NM > 1.0 and visible[1] and math.abs(target_radial_airspeed) < 20 ) {radarSeeTarget = 0 } # i.e. notching with terrain behind
}
if (radarSeeTarget and rand() > target_distance*M2NM / ciws_domain_nm) {
var score = 0;
var priority = getprop("priority");
@ -386,26 +353,26 @@ var fire_control = func(mp, my_pos) {
if ( data_link_match == false ) {
if (target_airspeed < 60) {return [mp,false,0,0];}
if ( visible[0] == false ) { return [mp,false,0,0]; }
if ( target_relative_pitch < radar_lowest_pitch ) { return [mp,false,0,0]; } #
if ( target_relative_pitch < radar_lowest_pitch ) { return [mp,false,0,0]; } #
if ( visible[1] and math.abs(target_radial_airspeed) < 20 ) { return [mp,false,0,0]; } # i.e. notching, landed aircraft
}
if ( target_distance * M2NM > missile_max_distance ) { return [mp,false,0,0]; }
# is the plane within the engagement envelope?
# the numbers after target_radial_airspeed are ( offset_of_engagement_envelope / speed_to_apply_that_offset )
# larger offset means it wont fire until the plane is closer.
# for visualization: https://www.desmos.com/calculator/gw570fa9km
if (!isInEngagementEnvelope(target_radial_airspeed, target_ground_distance, target_relative_altitude) ) { return [mp,false,0,0]; }
if (!isInEngagementEnvelope(target_radial_airspeed, target_ground_distance, target_relative_altitude) ) { return [mp,false,0,0]; }
var the_dice = rand();
if ( the_dice > 0.20 ) {
var score = 0;
var priority = getprop("priority");
@ -452,7 +419,7 @@ var reload = func(force = 1) {
if (ROUNDS == 0 or force == 1) {
ROUNDS = ROUNDS_init;
print("Reloaded with "~(ROUNDS)~" bursts.");
}
}
setprop("sam/missiles",(NUM_MISSILES+1-ACTIVE_MISSILE));
setprop("sam/bursts", ROUNDS);
reloading = 0;
@ -484,6 +451,7 @@ var missile_launch = func(mp, launchtime, my_pos) {
}
var ufo_pos = geo.Coord.new().set_latlon(mp.getNode("position/latitude-deg").getValue(),mp.getNode("position/longitude-deg").getValue(),(mp.getNode("position/altitude-ft").getValue() * 0.3048));
var target_bearing = my_pos.course_to(ufo_pos);
var target_pitch = vector.Math.getPitch(my_pos, ufo_pos);
var info = mp.getNode("callsign").getValue();
var lu = lookup(mp.getNode("callsign").getValue());
if (getprop("sam/timeleft") != 0) {
@ -501,6 +469,8 @@ var missile_launch = func(mp, launchtime, my_pos) {
setprop("sim/multiplay/generic/float[6]", target_bearing-getprop("orientation/heading-deg"));
theMissile.rail_head_deg = getprop("sim/multiplay/generic/float[6]");
}
setprop("sim/multiplay/generic/float[7]", target_pitch);
setprop("sim/multiplay/generic/float[8]", target_bearing-getprop("orientation/heading-deg"));
if (sam_align_to_target) {
setprop("/orientation/heading-deg",target_bearing);
}
@ -515,7 +485,7 @@ var missile_launch = func(mp, launchtime, my_pos) {
settimer(autoreload, reload_time);
reloading = 1;
reload_starting = systime();
}
}
if (lu != nil) {
append(lu.missiles, theMissile);
lu.in_air += 1;
@ -542,15 +512,19 @@ var missile_launch = func(mp, launchtime, my_pos) {
} else {
info = info~" (tracking)";
if (systime()-missile_release_time > 1.5) {
var tgt_dir = target_bearing-getprop("orientation/heading-deg");
var max_dir = launch_update_time*align_speed_dps;
if (launcher_align_to_target) {
# now smoothly rotate launcher:
var tgt_dir = target_bearing-getprop("orientation/heading-deg");
var cur_dir = getprop("sim/multiplay/generic/float[6]");
var move_dir = geo.normdeg180(tgt_dir-cur_dir);
var max_dir = launch_update_time*align_speed_dps;
move_dir = math.clamp(move_dir, -max_dir, max_dir);
setprop("sim/multiplay/generic/float[6]", cur_dir+move_dir);
}
var cur_dir2 = getprop("sim/multiplay/generic/float[8]");
var move_dir2 = geo.normdeg180(tgt_dir-cur_dir2);
move_dir2 = math.clamp(move_dir2, -max_dir, max_dir);
setprop("sim/multiplay/generic/float[8]", cur_dir2+move_dir2);
if (sam_align_to_target) {
var tgt_dir = target_bearing;
var cur_dir = getprop("orientation/heading-deg");
@ -564,6 +538,7 @@ var missile_launch = func(mp, launchtime, my_pos) {
setprop("sam/info", info);
if (mp.getNode("callsign") != nil and mp.getNode("callsign").getValue() != nil and mp.getNode("callsign").getValue() != "") {
setprop("sim/multiplay/generic/string[6]", left(md5(mp.getNode("callsign").getValue()), 4));
datalink.send_data({"contacts":[{"callsign":mp.getNode("callsign").getValue(),"iff":0}]});
} else {
clearSingleLock();
}
@ -571,6 +546,7 @@ var missile_launch = func(mp, launchtime, my_pos) {
lu.tracking = 1;
}
}
settimer( func { missile_launch(mp, launchtime, my_pos); },launch_update_time);
}

View File

@ -5,8 +5,8 @@
<layout>vbox</layout>
<draggable>true</draggable>
<resizable>false</resizable>
<width>125</width>
<height>200</height>
<width>150</width>
<height>250</height>
<group>
<empty><stretch>1</stretch></empty>
@ -144,5 +144,23 @@
<script>setprop("priority",6);setprop("priority6",1);setprop("priority0",0);setprop("priority1",0);setprop("priority2",0);setprop("priority3",0);setprop("priority4",0);setprop("priority5",0);</script>
</binding>
</radio>
<input>
<row>7</row>
<col>0</col>
<width>75</width>
<height>25</height>
<label>DLNK</label>
<property>instrumentation/datalink/channel</property>
<live>true</live>
<halign>right</halign>
<binding>
<command>dialog-apply</command>
</binding>
<color>
<red>0.75</red>
<green>0.75</green>
<blue>1.0</blue>
</color>
</input>
</group>
</PropertyList>

View File

@ -38,11 +38,6 @@
<splash type="bool">true</splash>
<path>gui/1.png</path>
</preview>
<preview>
<type>exterior</type>
<splash type="bool">true</splash>
<path>gui/2.png</path>
</preview>
</previews>
<current-view>
@ -226,6 +221,8 @@
<MAW-bearing type="double">0</MAW-bearing>
<MAW-active type="bool">false</MAW-active>
<spike type="bool">false</spike>
</armament>
</payload>
@ -275,6 +272,17 @@
<priority type="int">1</priority>
<priority1 type="int">1</priority1>
<instrumentation>
<datalink>
<data type="int">0</data>
<power type="bool">true</power>
<channel type="int">1212</channel>
<power_prop type="string">/instrumentation/datalink/power</power_prop>
<channel_prop type="string">instrumentation/datalink/channel</channel_prop>
<receive_period type="double">1</receive_period>
</datalink>
</instrumentation>
<nasal>
<notifications>
@ -296,7 +304,11 @@
<armament>
<file>Aircraft/S-200/Nasal/guided-missiles.nas</file>
</armament>
<datalink>
<file>Aircraft/S-200/Nasal/datalink.nas</file>
</datalink>
<fire_control>
<file>Aircraft/S-200/Nasal/fire-control-custom.nas</file>
<file>Aircraft/S-200/Nasal/fire-control.nas</file>
</fire_control>
</nasal>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

681
S-75/Nasal/datalink.nas Normal file
View File

@ -0,0 +1,681 @@
#### Datalink
# Copyright 2020-2021 Colin Geniet.
# Licensed under the GNU General Public License 2.0 or any later version.
#### Usage
### Generalities
#
# The datalink protocol consists of a core protocol which implements a notion
# of datalink channel, and extensions which allow transmitting actual data.
### Core protocol usage:
#
# Define the following properties (must be defined at nasal loading time).
# * Mandatory
# /instrumentation/datalink/power_prop path to property indicating if datalink is on
# /instrumentation/datalink/channel_prop path to property containing datalink channel
# (the channel property can contain anything, and is transmitted/compared as a string).
# * Optional
# /instrumentation/datalink/receive_period = 1 receiving loop update rate
#
# Optional: Re-define the function
# datalink.can_transmit(callsign, mp_prop, mp_index)
#
# This function should return 'true' when the given aircraft is able to transmit over datalink to us.
# For instance, it can be used to check line of sight and maximum range.
# The default implementation always returns true (always able to transmit).
# Arguments are callsign, property node /ai/models/multiplayer[i], index of the former node.
#
#
# API:
# - get_data(callsign)
# Returns all datalink information about 'callsign' as an object, or nil if there is none.
# This object must not be modified.
# It contains the following methods:
# callsign(): The aircraft callsign (same as the argument of get_data()).
# index(): The aircraft index in /ai/models/multiplayer[i].
# on_link(): Returns a bool indicating whether 'callsign' is connected to this aircraft through datalink.
#
# Extensions can define other methods in this object.
#
# - get_connected_callsigns() / get_connected_indices()
# Returns a vector containing all callsigns, resp. indices
# in /ai/models/multiplayer[i], of aircrafts connected on datalink.
# Both vectors use the same order, i.e. get_connected_callsigns()[i]
# and get_connected_indices()[i] correspond to the same aircraft.
# Furthermore this order is stable (the relative order of two aircrafts
# does not change as long as neither disconnects from multiplayer).
#
# - get_all_callsigns()
# Returns a vector containing all callsigns of aircraft with any associated data.
# There is no guarantee on the order of callsigns.
#
# - send_data(data, timeout=nil)
# Send data on the datalink. 'data' is a hash of the form
# {
# <extension_name>: <extension_data>,
# ...
# }
# If 'timeout' is set, clear_data() will be called after this delay.
# Data sent with send_data() is deleted at the next call of send_data(), or by clear_data().
#
# - clear_data()
# Clear data transmitted by this aircraft.
#
# Important note:
# After a send_data(), and until the next send_data() or clear_data(),
# the datalink behaves as if you are continuously sending the same data.
# Thus, it is important to
# 1. either call send_data() regularly
# 2. or set the timeout argument of send_data()
### Extensions
### Aircraft contacts (extension name: "contacts")
#
# This extension allows to simulate an aircraft transmitting information about
# another aircraft (typically one tracked on radar). The position data is not
# actually transmitted (since everyone can access it from simulator internals).
#
## Receiving data
# This extension adds the following methods to the result of get_data("A"):
# tracked(): A bool indicating that some aircraft "B" connected on datalink
# is transmitting information about aircraft "A".
# iff(): One of IFF_UNKNOWN, IFF_HOSTILE, IFF_FRIENDLY, or nil if tracked() is false.
# Indicates the result of IFF interrogation of "A" by "B"
# IFF_UNKNOWN means that e.g. no IFF interrogation was performed.
# tracked_by(): The callsign of the transmitting aircraft ("A"), or nil if tracked() is false.
# tracked_by_index(): The index of the transmitting aircraft, or nil if tracked() is false.
# The index refers to property nodes /ai/models/multiplayer[i].
# is_known(): Equivalent to (on_link() or tracked()).
# Indicates if the position of this aircraft is supposed to be known
# (i.e. whether or not it should be displayed on a HSD or whatever).
# is_friendly(): Equivalent to (on_link() or iff() == IFF_FRIENDLY).
# is_hostile(): Equivalent to (!on_link() and iff() == IFF_HOSTILE).
#
## Sending data
# usage: send_data({ contacts: <contacts>, ...}, ...)
# where <contacts> is a vector of hashes of the form { callsign: <callsign>, [iff: <iff>,] }.
# <callsign> is the multiplayer callsign of the tracked aircraft.
# <iff> (optional) is one of IFF_UNKNOWN, IFF_HOSTILE, IFF_FRIENDLY
### Datalink identifier (extension name: "identifier")
#
# This extension allows each aircraft on datalink to transmit a personal
# identifier, e.g. the number of the aircraft in a flight.
#
## Receiving data
# This extension adds the method identifier() to the result of get_data(),
# which returns the identifier, or nil if there is none).
#
## Sending data
# Set the identifier with send_data({"identifier": <identifier>, ...});
# The identifier must be a string. It must not contain '!'.
### Coordinate transmission (extension name: "point")
#
# This extension allows each aircraft to broadcast a coordinate (geo.Coord object).
#
## Receiving data
# This extension adds the method point() to the result of get_data(),
# which results the transmitted geo.Coord object, or nil if there is none.
#
## Sending data
# Transmit a geo.Coord object <coord> with send_data({"point": <coord>, ...});
#### Protocol:
#
# Data is transmitted on MP generic string[7], with the following format:
# <channel>(!<data>)+
#
# <channel> is a hash of the datalink channel. See hash_channel() and check_channel_hash().
# Each <data> block corresponds to data sent by an extension.
# It starts with a prefix uniquely defining the extension.
# The rest of the block can contain any character (including non-ascii) except '!'.
#
# Remark: '!' as separator is specifically chosen to allow encoding with emesary.Transfer<type>.
#
# The current extension prefixes are the following:
# contacts: C
# identifier: I
# point: P
#### Extensions API
#
# Creating a new extension is done with
# register_extension(name, prefix, object, encode, decode)
# name the extension name, used as key in the 'data' argument of send_data().
# prefix the protocol prefix.
# class contact class parent.
# A class from which all contact objects will inherit.
# It must have an init() method, which is called whenever a contact is created.
#
# encode(data) extension encoding function.
# Must return the encoding of the extension data (i.e. <data> when calling
# send_data({name: <data>})) into a string, which may use any character except '!'.
# The extension prefix must not be part of the encoded string.
#
# decode(aircrafts_data, callsign, index, string) extension decoding function.
# 'aircrafts_data' is a hash from callsigns to contact objects (see below).
# 'callsign' is the callsign of the aircraft which transmitted this data.
# 'index' is the index of the aircraft which transmitted this data.
# 'string' is the data encoded by encode() and transmitted through datalink.
# Each contact in 'data' inherits from the core 'Contact' class, and the extension 'class'.
# decode() is expected to modify 'aircrafts_data', by possibly editing
# existing contacts and adding new ones. It should be careful when
# overwriting existing data in these contacts, including its own: decode()
# will be called several time on the same 'aircrafts_data' (once for each
# transmitting aircraft).
# The modified 'aircrafts_data' hash must be returned.
#
# decode() may use the following helper functions:
# add_if_missing(aircrafts_data, callsign):
# Create a new contact object for 'callsign' and add it to 'aircrafts_data',
# unless an entry for 'callsign' already exists. Returns the modified hash.
#### Version and changelog
# current: v1.1.0, minimum compatible: v1.0.0
#
## v1.1.0:
# Allow external transmission restrictions
# Make transmitting contact IFF optional
# Ensure personal identifier has no '!'
# '\n' is redundant for printf()
# Fix separator character in documentation
# Fix error when sending unknown extension
#
## v1.0.1:
# Add is_known(), is_friendly(), is_hostile() helpers to extension "contacts".
#
## v1.0.0: Initial version
# - Core protocol for datalink channel.
# - Extensions "contacts", "identifier", and "point".
### Parameters
#
# Remark: most parameters need to be the same on all aircrafts.
# Index of multiplayer string used to transmit datalink info.
# Must be the same for all aircrafts.
var mp_string = 7;
var mp_path = "sim/multiplay/generic/string["~mp_string~"]";
var channel_hash_period = 600;
var receive_period = getprop("/instrumentation/datalink/receive_period") or 1;
# Should be overwitten to add transmission restrictions.
var can_transmit = func(contact, mp_prop, mp_index) {
return 1;
}
### Properties
var input = {
power: getprop("/instrumentation/datalink/power_prop"),
channel: getprop("/instrumentation/datalink/channel_prop"),
ident: getprop("/instrumentation/datalink/identifier_prop"),
mp: mp_path,
models: "/ai/models",
callsign: "/sim/multiplay/callsign",
};
foreach (var name; keys(input)) {
if (input[name] != nil) {
input[name] = props.globals.getNode(input[name], 1);
}
}
#### Core protocol implementation
### Channel hash (based on iff.nas)
#
# Channel is hashed with current time (rounded to 10min) and own callsign.
var clean_callsign = func(callsign) {
return damage.processCallsign(callsign);
}
var my_callsign = func {
return clean_callsign(input.callsign.getValue());
}
# Time, rounded to 'channel_hash_period'. This is used to hash channel.
var get_time = func {
return int(math.floor(systime() / channel_hash_period) * channel_hash_period);
}
# Previous / next time (with channel_hash_period interval).
# This is used to give a bit of margin on the time check.
# (will work if system clocks are coordinated within 10min).
var get_prev_time = func { return get_time() - channel_hash_period; }
var get_next_time = func { return get_time() + channel_hash_period; }
var parse_hexadecimal = func(str) {
var res = 0;
for (var i=0; i<size(str); i+=1) {
res *= 10;
var c = str[i];
if (c >= 48 and c < 58) {
# digit
res += c - 48;
} elsif (c >= 65 and c < 71) {
# upper case letter
res += c - 55;
} elsif (c >= 97 and c < 103) {
# lower case letter
res += c - 87;
}
}
return res;
}
var _hash_channel = func(time, callsign, channel) {
# 5 hex digits (2^20) fit in 3 chars for emesary int encoding.
var hash = parse_hexadecimal(left(md5(time ~ callsign ~ channel), 5));
return emesary.TransferInt.encode(hash, 3);
}
# Hash channel (when sending).
var encode_channel = func(channel) {
return _hash_channel(get_time(), my_callsign(), channel);
}
# Check that the hash transmitted by aircraft 'callsign' is correct for 'channel'.
var check_channel = func(hash, callsign, channel) {
return hash == _hash_channel(get_time(), callsign, channel)
or hash == _hash_channel(get_prev_time(), callsign, channel)
or hash == _hash_channel(get_next_time(), callsign, channel);
}
### Contact object
var Contact = {
new: func(callsign) {
var c = {
# contact_parents is the list of all classes from which contacts inherit (for extensions).
parents: contact_parents,
_callsign: callsign,
};
# Initialize all inherited classes.
foreach (var class; contact_parents) {
call(class.init, [], c, nil, nil);
}
return c;
},
init: func { me._on_link = 0; },
callsign: func { return me._callsign; },
index: func { return callsign_to_index[me._callsign]; },
on_link: func { return me._on_link; },
set_on_link: func(b) { me._on_link = b; },
};
### Extensions
var extensions = {};
var extension_prefixes = {};
var max_prefix_length = 0;
var contact_parents = [Contact];
var register_extension = func(name, prefix, class, encode, decode) {
if (contains(extensions, name)) {
printf("Datalink: double registration of extension '%s'. Skipping.", name);
return -1;
}
if (contains(extension_prefixes, prefix)) {
printf("Datalink: double registration of extension prefix '%s'. Skipping.", name);
return -1;
}
extensions[name] = { prefix: prefix, encode: encode, decode: decode, };
extension_prefixes[prefix] = name;
max_prefix_length = math.max(max_prefix_length, size(prefix));
append(contact_parents, class);
return 0;
}
var data_separator = "!";
### Transmission
var clear_data = func {
send_data({});
}
var clear_timer = maketimer(1, clear_data);
clear_timer.singleShot = 1;
# Send data through datalink.
#
# timeout: if set, sent data will be cleared after this time (other aircrafts
# won't receive it anymore). Useful if 'send_data' is not called often.
var send_data = func(data, timeout=nil) {
if (!input.power.getBoolValue()) {
last_data = {};
input.mp.setValue("");
return;
}
# First encode channel
var str = encode_channel(input.channel.getValue());
# Then all extensions
last_data = data;
foreach(var ext; keys(data)) {
# Skip missing extensions with a warning
if (!contains(extensions, ext)) {
printf("Warning: unknown datalink extension %s in send_data().", ext);
continue;
}
str = str ~ data_separator ~ extensions[ext].prefix ~ extensions[ext].encode(data[ext]);
}
input.mp.setValue(str);
if (timeout != nil) {
clear_timer.restart(timeout);
}
}
# Used internally to update the channel/identifier while keeping the same data.
# Does not touch timeout.
var last_data = {};
var resend_data = func {
send_data(last_data);
}
# Very slow timer to ensure the channel hash is updated regularly.
# Only relevant if you never call send_data();
var hash_update_timer = maketimer(channel_hash_period/2, resend_data);
hash_update_timer.start();
### Receiving
# callsign to data hash
var aircrafts_data = {};
# List of callsigns / indices connected on datalink (index is for /ai/models/multiplayer[i]).
var connected_callsigns = [];
var connected_indices = [];
# Maintain callsign to multiplayer index hash
# (doesn't cost much since we already iterate over MP models).
var callsign_to_index = {};
var get_data = func(callsign) {
return aircrafts_data[callsign];
}
var get_connected_callsigns = func {
return connected_callsigns;
}
var get_connected_indices = func {
return connected_indices;
}
var get_all_callsigns = func {
return keys(aircrafts_data);
}
# Helper for modifying aircrafts_data.
var add_if_missing = func(aircrafts_data, callsign) {
if (!contains(aircrafts_data, callsign)) {
aircrafts_data[callsign] = Contact.new(callsign);
}
return aircrafts_data;
}
var receive_loop = func {
var my_channel = input.channel.getValue();
aircrafts_data = {};
connected_callsigns = [];
connected_indices = [];
var mp_models = input.models.getChildren("multiplayer");
foreach(var mp; mp_models) {
var idx = mp.getIndex();
if (!mp.getValue("valid")) continue;
var callsign = mp.getValue("callsign");
if (callsign == nil) continue;
callsign_to_index[callsign] = idx;
var data = mp.getValue(mp_path);
if (data == nil) continue;
# Split channel part and data part
var tokens = split(data_separator, data);
# Check channel
if (!check_channel(tokens[0], callsign, my_channel)) continue;
# We check this _after_ the channel. Checking the channel is quite cheap,
# and we don't know how slow this function is, it might have a get_cart_ground_intersection()
if (!can_transmit(callsign, mp, idx)) continue;
# Add to list of connected aircrafts.
append(connected_callsigns, callsign);
append(connected_indices, idx);
# Add to data
aircrafts_data = add_if_missing(aircrafts_data, callsign);
aircrafts_data[callsign].set_on_link(1);
# Parse extensions data
for (var i=1; i<size(tokens); i+=1) {
var extension = nil;
# Identify extension prefix. This is not very clever code, but
# realistically it doesn't matter since prefixes are very short.
var len = 1;
for (; len <= max_prefix_length; len += 1) {
if (len > size(tokens)) break;
var prefix = left(tokens[i], len);
if (contains(extension_prefixes, prefix)) {
extension = extension_prefixes[prefix];
break;
}
}
# Unknown extension, skip
if (extension == nil) continue;
# Remove prefix
var data = substr(tokens[i], len);
# Decode
aircrafts_data = extensions[extension].decode(aircrafts_data, callsign, data);
}
}
}
var receive_timer = maketimer(receive_period, receive_loop);
# Start / stop listener
setlistener(input.power, func (node) {
if (node.getBoolValue()) {
receive_timer.start();
resend_data(); # Sets channel/identifier
} else {
receive_timer.stop();
aircrafts_data = {};
clear_data();
}
}, 1, 0);
# Listener to resend data so as to update the channel.
setlistener(input.channel, resend_data);
#### Extensions
## Identifier
var ContactIdentifier = {
init: func {
me._identifier = nil;
},
set_identifier: func(ident) {
me._identifier = ident;
},
identifier: func {
return me._identifier;
},
};
var encode_identifier = func(ident) {
# Force string conversion
ident = ""~ident;
if (find("!", ident) >= 0) {
printf("Datalink: Identifier is not allowed to contain '!': %s.", ident);
return "";
} else {
return ident;
}
}
var decode_identifier = func(aircrafts_data, callsign, str) {
aircrafts_data = add_if_missing(aircrafts_data, callsign);
aircrafts_data[callsign].set_identifier(str);
return aircrafts_data;
}
register_extension("identifier", "I", ContactIdentifier, encode_identifier, decode_identifier);
## Contacts
# IFF status transmitted over datalink.
var IFF_UNKNOWN = 0; # Unknown status
var IFF_HOSTILE = 1; # Considered hostile (no response to IFF).
var IFF_FRIENDLY = 2; # Friendly, because positive IFF identification.
# This is also the priority order for IFF reports in case of conflicts:
# e.g. a contact will be reported as friendly if anyone on datalink reports it as friendly.
var ContactTracked = {
init: func {
me._tracked_by = nil;
me._iff = IFF_UNKNOWN;
},
set_tracked_by: func(callsign) {
me._tracked_by = callsign;
},
set_iff: func(iff) {
# Priority order on IFF values (friendly, then hostile, then no data).
me._iff = math.max(me._iff, iff);
},
tracked: func {
return me._tracked_by != nil;
},
tracked_by: func {
return me._tracked_by;
},
tracked_by_index: func {
return (me._tracked_by != nil) ? callsign_to_index[me._tracked_by] : nil;
},
iff: func {
return me._iff;
},
is_known: func {
return me.on_link() or me.tracked();
},
is_friendly: func {
return me.on_link() or me.iff() == IFF_FRIENDLY;
},
is_hostile: func {
return !me.on_link() and me.iff() == IFF_HOSTILE;
},
};
# Contact encoding: callsign + bits
# callsign: the callsign encoded with emesary.TransferString
# bits: bitfield |xxxxxxff| (left is most significant)
# f: IFF, x: unused
# encoded with emesary.TransferByte
# Additional values may be appended for extensions.
var encode_contact = func(contact) {
# Encode bitfield
var bits = contact["iff"] != nil ? contact.iff : IFF_UNKNOWN;
return emesary.TransferString.encode(clean_callsign(contact.callsign))
~ emesary.TransferByte.encode(bits);
}
var decode_contact = func(str) {
var res = {};
var dv = emesary.TransferString.decode(str, 0);
res.callsign = dv.value;
dv = emesary.TransferByte.decode(str, dv.pos);
var bits = dv.value;
res.iff = math.mod(bits, 4);
return res;
}
# Special character, won't be used by emesary encoding.
var contacts_separator = "#";
var encode_contacts = func(contacts) {
var str = "";
foreach (var contact; contacts) {
str = str~encode_contact(contact)~contacts_separator;
}
return str;
}
var decode_contacts = func(aircrafts_data, callsign, str) {
var contacts = split(contacts_separator, str);
foreach (var contact; contacts) {
if (contact == "") continue;
var res = decode_contact(contact);
aircrafts_data = add_if_missing(aircrafts_data, res.callsign);
aircrafts_data[res.callsign].set_iff(res.iff);
aircrafts_data[res.callsign].set_tracked_by(callsign);
}
return aircrafts_data;
}
register_extension("contacts", "C", ContactTracked, encode_contacts, decode_contacts);
## Coordinate
var ContactPoint = {
init: func {
me._point = nil;
},
set_point: func(point) {
me._point = point;
},
point: func {
return me._point;
},
};
var encode_point = func(coord) {
return emesary.TransferCoord.encode(coord);
}
var decode_point = func(aircrafts_data, callsign, str) {
var coord = emesary.TransferCoord.decode(str, 0).value;
aircrafts_data = add_if_missing(aircrafts_data, callsign);
aircrafts_data[callsign].set_point(coord);
return aircrafts_data;
}
register_extension("point", "P", ContactPoint, encode_point, decode_point);

View File

@ -0,0 +1,54 @@
################### SAM INFO
var setupTime = 300;#minimum 'launcher_tilt_time' secs no matter what, due to anim and stuff.
var reload_time = 400;
var launcher_final_tilt_deg = 35;
var launcher_start_tilt_deg = 35;
var launcher_tilt_time = 0;
var sam_align_to_target = 0;
var launcher_align_to_target = 1;
var align_speed_dps = 20;
var radar_elevation_above_terrain_m = 25;
var radar_lowest_pitch = 3.5;# 0.5 degs = roughly 925 feet at 20 nm, 25 feet at half a nm. # 0.35 = roughly 925 feet at 20 nm, 25 feet at half a nm.
#reaction tme for s-300p is 28 secs acording to http://www.astronautix.com/s/s-300p.html
# sounds a bit high for pmu, as its 4 secs for s-400
################### CIWS INFO
var ciws_installed = 0;
var ciws_domain_nm = 1.50; #range where it can kill
var ciws_chance = 0.20; #chance to get a kill at 0nm distance
var ciws_burst_rounds = 60;#how many rounds in a burst
var ciws_shell = 15;#from lookup table in damage.nas
var ROUNDS_init = 30;
var ROUNDS = ROUNDS_init;#CIWS bursts remaining
################### MISSILE INFO
var NUM_MISSILES = 5; # total carried minus 1
var missile_name = "Volga-M";
var missile_brevity = "5Ya23";
var missile_max_distance = 36; #max distance of target in nm when it will fire
var missile_min_distance = 3.5; #minimum distance in nm when it will fire
var lockon_time = 12; #time in seconds it takes to lock on and get a firing solution on a target
var fire_minimum_interval = 7;# time since last track was initiated till a new can be initiated
var same_target_max_missiles = 2;# max number of missiles in air against same target
var isInEngagementEnvelope = func (target_radial_airspeed, target_ground_distance, target_relative_altitude) {
return 1;
}
var midflight = func (struct) {
if (struct.guidance == "semi-radar") {
# This makes the SAM system keep lock on target when missile in-flight and no longer tracking the target.
# Usage is to make the RWR lock sound go off in targets cockpit.
thread.lock(mutexLock);
semi_active_track = struct.callsign;
thread.unlock(mutexLock);
}
return {};
};

View File

@ -20,58 +20,6 @@ var ACTIVE_MISSILE = 0;
var semi_active_track = nil;# with multiple missiles flying in semi-active-radar mode, this can change rather fast. Which means pilots wont get a steady tone, but disrupted tones. (tradeoff)
var mutexLock = thread.newlock();
################### SAM INFO
var setupTime = 300;#minimum 'launcher_tilt_time' secs no matter what, due to anim and stuff.
var reload_time = 400;
var launcher_final_tilt_deg = 35;
var launcher_start_tilt_deg = 35;
var launcher_tilt_time = 0;
var sam_align_to_target = 0;
var launcher_align_to_target = 1;
var align_speed_dps = 20;
var radar_elevation_above_terrain_m = 25;
var radar_lowest_pitch = 3.5;# 0.5 degs = roughly 925 feet at 20 nm, 25 feet at half a nm. # 0.35 = roughly 925 feet at 20 nm, 25 feet at half a nm.
#reaction tme for s-300p is 28 secs acording to http://www.astronautix.com/s/s-300p.html
# sounds a bit high for pmu, as its 4 secs for s-400
################### CIWS INFO
var ciws_installed = 0;
var ciws_domain_nm = 1.50; #range where it can kill
var ciws_chance = 0.20; #chance to get a kill at 0nm distance
var ciws_burst_rounds = 60;#how many rounds in a burst
var ciws_shell = 15;#from lookup table in damage.nas
var ROUNDS_init = 30;
var ROUNDS = ROUNDS_init;#CIWS bursts remaining
################### MISSILE INFO
var NUM_MISSILES = 5; # total carried minus 1
var missile_name = "Volga-M";
var missile_brevity = "5Ya23";
var missile_max_distance = 36; #max distance of target in nm when it will fire
var missile_min_distance = 3.5; #minimum distance in nm when it will fire
var lockon_time = 12; #time in seconds it takes to lock on and get a firing solution on a target
var fire_minimum_interval = 7;# time since last track was initiated till a new can be initiated
var same_target_max_missiles = 2;# max number of missiles in air against same target
var isInEngagementEnvelope = func (target_radial_airspeed, target_ground_distance, target_relative_altitude) {
return 1;
}
var midflight = func (struct) {
if (struct.guidance == "semi-radar") {
# This makes the SAM system keep lock on target when missile in-flight and no longer tracking the target.
# Usage is to make the RWR lock sound go off in targets cockpit.
thread.lock(mutexLock);
semi_active_track = struct.callsign;
thread.unlock(mutexLock);
}
return {};
};
##########################################################################
######################## Common SAM code ###########################
@ -79,6 +27,8 @@ var midflight = func (struct) {
################### VARIOUS
setprop("sim/multiplay/generic/float[7]", 0);# tracking radar vert rotation
setprop("sim/multiplay/generic/float[8]", 0);# tracking radar horiz rotation
setprop("sim/multiplay/generic/float[5]", 0);# launcher vert rotation
setprop("sim/multiplay/generic/float[6]", 0);# launcher horiz rotation
var start_time = systime();
@ -139,7 +89,7 @@ var buildTargetList = func {
};
var lookup = func (sign) {
foreach (tgt ; targetsV2.vector) {
if (tgt.callsign == sign) {
return tgt;
@ -177,9 +127,11 @@ var scan = func() {
buildTargetList();
if ( getprop("/carrier/sunk") == 1 or getprop("/carrier/disabled") == 1) {
setprop("sim/multiplay/generic/string[6]", "");
datalink.clear_data();
return;
}
if (systime() - start_time < setupTime ) {
printf("Seconds till activation: %.1f", setupTime - (systime() - start_time));
setprop("sam/timeleft", setupTime - (systime() - start_time));
@ -193,8 +145,11 @@ var scan = func() {
setprop("sim/multiplay/generic/float[5]", launcher_final_tilt_deg);
}
setprop("sim/multiplay/generic/float[6]", 0);
setprop("sim/multiplay/generic/float[7]", 0);
setprop("sim/multiplay/generic/float[8]", 0);
setprop("sim/multiplay/generic/string[6]", "");
setprop("/sim/multiplay/generic/int[2]", 1);
datalink.clear_data();
return;
}
setprop("sim/multiplay/generic/float[5]", launcher_final_tilt_deg);
@ -223,12 +178,12 @@ var scan = func() {
}
prio_vector = sort(prio_vector, func (a,b) {if (a[2]>b[2]){return -1;} elsif (a[2]<b[2]) {return 1;} else {return 0;}});
foreach(var mp; prio_vector) {
#### DO WE HAVE FIRING SOLUTION... ####
# is plane in range, do we still have missiles, and is a missile already inbound, and has it been 4 seconds since the last missile launch?
var trigger = mp[1];
#print("dist to target = " ~ dist_to_target);
var lu = lookup(mp[0].getNode("callsign").getValue());
if ( launch_in_progress == 0 and trigger == true and lu != nil and lu.in_air < same_target_max_missiles and !lu.tracking and ACTIVE_MISSILE <= NUM_MISSILES and ( systime() - missile_delay_time > fire_minimum_interval ) ) { #
#### ... FOR THE MISSILE ####
@ -238,7 +193,7 @@ var scan = func() {
armament.contact = radar_logic.Contact.new(mp[0], AIR);
launch_in_progress = 1;
missile_launch(mp[0], systime(), my_pos);
} elsif ( ciws_installed and trigger == -1 and ROUNDS > 0 and ( systime() - ciws_delay_time > 1.0 ) and lu != nil) {
#### ... FOR THE CIWS ####
var contact = radar_logic.Contact.new(mp[0], AIR);
@ -272,7 +227,7 @@ var scan = func() {
reloading = 1;
reload_starting = systime();
}
}
}
if (lu != nil and lu.tracking) {
radarOn = 1;
}
@ -318,8 +273,10 @@ var clearSingleLock = func () {
thread.lock(mutexLock);
if (semi_active_track == nil) {
setprop("sim/multiplay/generic/string[6]", "");
datalink.clear_data();
} else {
setprop("sim/multiplay/generic/string[6]", left(md5(semi_active_track), 4));
datalink.send_data({"contacts":[{"callsign":semi_active_track,"iff":0}]});
}
thread.unlock(mutexLock);
}
@ -335,7 +292,7 @@ var fire_control = func(mp, my_pos) {
var ufo_pos = geo.Coord.new().set_latlon(mp.getNode("position/latitude-deg").getValue(),mp.getNode("position/longitude-deg").getValue(),(mp.getNode("position/altitude-ft").getValue() * FT2M));
var target_distance = my_pos.direct_distance_to(ufo_pos);
var target_altitude = mp.getNode("position/altitude-ft").getValue();
# is this plane a friend or foe?
var lu = lookup(mp.getNode("callsign").getValue());
if ( lu == nil ) { return [mp,false,0,0]; }
@ -344,11 +301,11 @@ var fire_control = func(mp, my_pos) {
var target_ground_distance = my_pos.distance_to(ufo_pos);
var target_heading = mp.getNode("orientation/true-heading-deg").getValue();
var relative_bearing = math.abs(geo.normdeg180(ufo_pos.course_to(my_pos) - target_heading));
var target_relative_altitude = target_altitude - my_pos.alt()*M2FT;
var target_radial_airspeed = math.abs(math.abs(((relative_bearing / 90 ) - 1) * target_airspeed));#200kt against us = 200kt, 200kt at angle of us = 0kt, 200kt away from us = 200kt
var target_relative_pitch = vector.Math.getPitch(my_pos, ufo_pos);
# can the radar see it?
var data_link_match = false;
@ -358,7 +315,7 @@ var fire_control = func(mp, my_pos) {
# #print('tgt confirmed via datalink');
# }
# }
var visible = radar_logic.isNotBehindTerrain(mp);
@ -367,9 +324,10 @@ var fire_control = func(mp, my_pos) {
var radarSeeTarget = 1;
if ( data_link_match == false ) {
if ( visible[0] == false ) {radarSeeTarget = 0 }
if ( target_relative_pitch < radar_lowest_pitch ) { radarSeeTarget = 0 } #
if ( target_relative_pitch < radar_lowest_pitch ) { radarSeeTarget = 0 } #
if ( target_distance*M2NM > 1.0 and visible[1] and math.abs(target_radial_airspeed) < 20 ) {radarSeeTarget = 0 } # i.e. notching with terrain behind
}
if (radarSeeTarget and rand() > target_distance*M2NM / ciws_domain_nm) {
var score = 0;
var priority = getprop("priority");
@ -395,26 +353,26 @@ var fire_control = func(mp, my_pos) {
if ( data_link_match == false ) {
if (target_airspeed < 60) {return [mp,false,0,0];}
if ( visible[0] == false ) { return [mp,false,0,0]; }
if ( target_relative_pitch < radar_lowest_pitch ) { return [mp,false,0,0]; } #
if ( target_relative_pitch < radar_lowest_pitch ) { return [mp,false,0,0]; } #
if ( visible[1] and math.abs(target_radial_airspeed) < 20 ) { return [mp,false,0,0]; } # i.e. notching, landed aircraft
}
if ( target_distance * M2NM > missile_max_distance ) { return [mp,false,0,0]; }
# is the plane within the engagement envelope?
# the numbers after target_radial_airspeed are ( offset_of_engagement_envelope / speed_to_apply_that_offset )
# larger offset means it wont fire until the plane is closer.
# for visualization: https://www.desmos.com/calculator/gw570fa9km
if (!isInEngagementEnvelope(target_radial_airspeed, target_ground_distance, target_relative_altitude) ) { return [mp,false,0,0]; }
if (!isInEngagementEnvelope(target_radial_airspeed, target_ground_distance, target_relative_altitude) ) { return [mp,false,0,0]; }
var the_dice = rand();
if ( the_dice > 0.20 ) {
var score = 0;
var priority = getprop("priority");
@ -461,7 +419,7 @@ var reload = func(force = 1) {
if (ROUNDS == 0 or force == 1) {
ROUNDS = ROUNDS_init;
print("Reloaded with "~(ROUNDS)~" bursts.");
}
}
setprop("sam/missiles",(NUM_MISSILES+1-ACTIVE_MISSILE));
setprop("sam/bursts", ROUNDS);
reloading = 0;
@ -493,6 +451,7 @@ var missile_launch = func(mp, launchtime, my_pos) {
}
var ufo_pos = geo.Coord.new().set_latlon(mp.getNode("position/latitude-deg").getValue(),mp.getNode("position/longitude-deg").getValue(),(mp.getNode("position/altitude-ft").getValue() * 0.3048));
var target_bearing = my_pos.course_to(ufo_pos);
var target_pitch = vector.Math.getPitch(my_pos, ufo_pos);
var info = mp.getNode("callsign").getValue();
var lu = lookup(mp.getNode("callsign").getValue());
if (getprop("sam/timeleft") != 0) {
@ -510,6 +469,8 @@ var missile_launch = func(mp, launchtime, my_pos) {
setprop("sim/multiplay/generic/float[6]", target_bearing-getprop("orientation/heading-deg"));
theMissile.rail_head_deg = getprop("sim/multiplay/generic/float[6]");
}
setprop("sim/multiplay/generic/float[7]", target_pitch);
setprop("sim/multiplay/generic/float[8]", target_bearing-getprop("orientation/heading-deg"));
if (sam_align_to_target) {
setprop("/orientation/heading-deg",target_bearing);
}
@ -524,7 +485,7 @@ var missile_launch = func(mp, launchtime, my_pos) {
settimer(autoreload, reload_time);
reloading = 1;
reload_starting = systime();
}
}
if (lu != nil) {
append(lu.missiles, theMissile);
lu.in_air += 1;
@ -551,15 +512,19 @@ var missile_launch = func(mp, launchtime, my_pos) {
} else {
info = info~" (tracking)";
if (systime()-missile_release_time > 1.5) {
var tgt_dir = target_bearing-getprop("orientation/heading-deg");
var max_dir = launch_update_time*align_speed_dps;
if (launcher_align_to_target) {
# now smoothly rotate launcher:
var tgt_dir = target_bearing-getprop("orientation/heading-deg");
var cur_dir = getprop("sim/multiplay/generic/float[6]");
var move_dir = geo.normdeg180(tgt_dir-cur_dir);
var max_dir = launch_update_time*align_speed_dps;
move_dir = math.clamp(move_dir, -max_dir, max_dir);
setprop("sim/multiplay/generic/float[6]", cur_dir+move_dir);
}
var cur_dir2 = getprop("sim/multiplay/generic/float[8]");
var move_dir2 = geo.normdeg180(tgt_dir-cur_dir2);
move_dir2 = math.clamp(move_dir2, -max_dir, max_dir);
setprop("sim/multiplay/generic/float[8]", cur_dir2+move_dir2);
if (sam_align_to_target) {
var tgt_dir = target_bearing;
var cur_dir = getprop("orientation/heading-deg");
@ -573,6 +538,7 @@ var missile_launch = func(mp, launchtime, my_pos) {
setprop("sam/info", info);
if (mp.getNode("callsign") != nil and mp.getNode("callsign").getValue() != nil and mp.getNode("callsign").getValue() != "") {
setprop("sim/multiplay/generic/string[6]", left(md5(mp.getNode("callsign").getValue()), 4));
datalink.send_data({"contacts":[{"callsign":mp.getNode("callsign").getValue(),"iff":0}]});
} else {
clearSingleLock();
}
@ -580,6 +546,7 @@ var missile_launch = func(mp, launchtime, my_pos) {
lu.tracking = 1;
}
}
settimer( func { missile_launch(mp, launchtime, my_pos); },launch_update_time);
}

View File

@ -368,6 +368,17 @@
<priority type="int">1</priority>
<priority1 type="int">1</priority1>
<instrumentation>
<datalink>
<data type="int">0</data>
<power type="bool">true</power>
<channel type="int">1212</channel>
<power_prop type="string">/instrumentation/datalink/power</power_prop>
<channel_prop type="string">instrumentation/datalink/channel</channel_prop>
<receive_period type="double">1</receive_period>
</datalink>
</instrumentation>
<nasal>
<notifications>
@ -389,7 +400,11 @@
<armament>
<file>Aircraft/S-75/Nasal/guided-missiles.nas</file>
</armament>
<datalink>
<file>Aircraft/S-75/Nasal/datalink.nas</file>
</datalink>
<fire_control>
<file>Aircraft/S-75/Nasal/fire-control-custom.nas</file>
<file>Aircraft/S-75/Nasal/fire-control.nas</file>
</fire_control>
</nasal>

View File

@ -5,8 +5,8 @@
<layout>vbox</layout>
<draggable>true</draggable>
<resizable>false</resizable>
<width>125</width>
<height>200</height>
<width>150</width>
<height>250</height>
<group>
<empty><stretch>1</stretch></empty>
@ -144,5 +144,23 @@
<script>setprop("priority",6);setprop("priority6",1);setprop("priority0",0);setprop("priority1",0);setprop("priority2",0);setprop("priority3",0);setprop("priority4",0);setprop("priority5",0);</script>
</binding>
</radio>
<input>
<row>7</row>
<col>0</col>
<width>75</width>
<height>25</height>
<label>DLNK</label>
<property>instrumentation/datalink/channel</property>
<live>true</live>
<halign>right</halign>
<binding>
<command>dialog-apply</command>
</binding>
<color>
<red>0.75</red>
<green>0.75</green>
<blue>1.0</blue>
</color>
</input>
</group>
</PropertyList>

681
SA-6/Nasal/datalink.nas Normal file
View File

@ -0,0 +1,681 @@
#### Datalink
# Copyright 2020-2021 Colin Geniet.
# Licensed under the GNU General Public License 2.0 or any later version.
#### Usage
### Generalities
#
# The datalink protocol consists of a core protocol which implements a notion
# of datalink channel, and extensions which allow transmitting actual data.
### Core protocol usage:
#
# Define the following properties (must be defined at nasal loading time).
# * Mandatory
# /instrumentation/datalink/power_prop path to property indicating if datalink is on
# /instrumentation/datalink/channel_prop path to property containing datalink channel
# (the channel property can contain anything, and is transmitted/compared as a string).
# * Optional
# /instrumentation/datalink/receive_period = 1 receiving loop update rate
#
# Optional: Re-define the function
# datalink.can_transmit(callsign, mp_prop, mp_index)
#
# This function should return 'true' when the given aircraft is able to transmit over datalink to us.
# For instance, it can be used to check line of sight and maximum range.
# The default implementation always returns true (always able to transmit).
# Arguments are callsign, property node /ai/models/multiplayer[i], index of the former node.
#
#
# API:
# - get_data(callsign)
# Returns all datalink information about 'callsign' as an object, or nil if there is none.
# This object must not be modified.
# It contains the following methods:
# callsign(): The aircraft callsign (same as the argument of get_data()).
# index(): The aircraft index in /ai/models/multiplayer[i].
# on_link(): Returns a bool indicating whether 'callsign' is connected to this aircraft through datalink.
#
# Extensions can define other methods in this object.
#
# - get_connected_callsigns() / get_connected_indices()
# Returns a vector containing all callsigns, resp. indices
# in /ai/models/multiplayer[i], of aircrafts connected on datalink.
# Both vectors use the same order, i.e. get_connected_callsigns()[i]
# and get_connected_indices()[i] correspond to the same aircraft.
# Furthermore this order is stable (the relative order of two aircrafts
# does not change as long as neither disconnects from multiplayer).
#
# - get_all_callsigns()
# Returns a vector containing all callsigns of aircraft with any associated data.
# There is no guarantee on the order of callsigns.
#
# - send_data(data, timeout=nil)
# Send data on the datalink. 'data' is a hash of the form
# {
# <extension_name>: <extension_data>,
# ...
# }
# If 'timeout' is set, clear_data() will be called after this delay.
# Data sent with send_data() is deleted at the next call of send_data(), or by clear_data().
#
# - clear_data()
# Clear data transmitted by this aircraft.
#
# Important note:
# After a send_data(), and until the next send_data() or clear_data(),
# the datalink behaves as if you are continuously sending the same data.
# Thus, it is important to
# 1. either call send_data() regularly
# 2. or set the timeout argument of send_data()
### Extensions
### Aircraft contacts (extension name: "contacts")
#
# This extension allows to simulate an aircraft transmitting information about
# another aircraft (typically one tracked on radar). The position data is not
# actually transmitted (since everyone can access it from simulator internals).
#
## Receiving data
# This extension adds the following methods to the result of get_data("A"):
# tracked(): A bool indicating that some aircraft "B" connected on datalink
# is transmitting information about aircraft "A".
# iff(): One of IFF_UNKNOWN, IFF_HOSTILE, IFF_FRIENDLY, or nil if tracked() is false.
# Indicates the result of IFF interrogation of "A" by "B"
# IFF_UNKNOWN means that e.g. no IFF interrogation was performed.
# tracked_by(): The callsign of the transmitting aircraft ("A"), or nil if tracked() is false.
# tracked_by_index(): The index of the transmitting aircraft, or nil if tracked() is false.
# The index refers to property nodes /ai/models/multiplayer[i].
# is_known(): Equivalent to (on_link() or tracked()).
# Indicates if the position of this aircraft is supposed to be known
# (i.e. whether or not it should be displayed on a HSD or whatever).
# is_friendly(): Equivalent to (on_link() or iff() == IFF_FRIENDLY).
# is_hostile(): Equivalent to (!on_link() and iff() == IFF_HOSTILE).
#
## Sending data
# usage: send_data({ contacts: <contacts>, ...}, ...)
# where <contacts> is a vector of hashes of the form { callsign: <callsign>, [iff: <iff>,] }.
# <callsign> is the multiplayer callsign of the tracked aircraft.
# <iff> (optional) is one of IFF_UNKNOWN, IFF_HOSTILE, IFF_FRIENDLY
### Datalink identifier (extension name: "identifier")
#
# This extension allows each aircraft on datalink to transmit a personal
# identifier, e.g. the number of the aircraft in a flight.
#
## Receiving data
# This extension adds the method identifier() to the result of get_data(),
# which returns the identifier, or nil if there is none).
#
## Sending data
# Set the identifier with send_data({"identifier": <identifier>, ...});
# The identifier must be a string. It must not contain '!'.
### Coordinate transmission (extension name: "point")
#
# This extension allows each aircraft to broadcast a coordinate (geo.Coord object).
#
## Receiving data
# This extension adds the method point() to the result of get_data(),
# which results the transmitted geo.Coord object, or nil if there is none.
#
## Sending data
# Transmit a geo.Coord object <coord> with send_data({"point": <coord>, ...});
#### Protocol:
#
# Data is transmitted on MP generic string[7], with the following format:
# <channel>(!<data>)+
#
# <channel> is a hash of the datalink channel. See hash_channel() and check_channel_hash().
# Each <data> block corresponds to data sent by an extension.
# It starts with a prefix uniquely defining the extension.
# The rest of the block can contain any character (including non-ascii) except '!'.
#
# Remark: '!' as separator is specifically chosen to allow encoding with emesary.Transfer<type>.
#
# The current extension prefixes are the following:
# contacts: C
# identifier: I
# point: P
#### Extensions API
#
# Creating a new extension is done with
# register_extension(name, prefix, object, encode, decode)
# name the extension name, used as key in the 'data' argument of send_data().
# prefix the protocol prefix.
# class contact class parent.
# A class from which all contact objects will inherit.
# It must have an init() method, which is called whenever a contact is created.
#
# encode(data) extension encoding function.
# Must return the encoding of the extension data (i.e. <data> when calling
# send_data({name: <data>})) into a string, which may use any character except '!'.
# The extension prefix must not be part of the encoded string.
#
# decode(aircrafts_data, callsign, index, string) extension decoding function.
# 'aircrafts_data' is a hash from callsigns to contact objects (see below).
# 'callsign' is the callsign of the aircraft which transmitted this data.
# 'index' is the index of the aircraft which transmitted this data.
# 'string' is the data encoded by encode() and transmitted through datalink.
# Each contact in 'data' inherits from the core 'Contact' class, and the extension 'class'.
# decode() is expected to modify 'aircrafts_data', by possibly editing
# existing contacts and adding new ones. It should be careful when
# overwriting existing data in these contacts, including its own: decode()
# will be called several time on the same 'aircrafts_data' (once for each
# transmitting aircraft).
# The modified 'aircrafts_data' hash must be returned.
#
# decode() may use the following helper functions:
# add_if_missing(aircrafts_data, callsign):
# Create a new contact object for 'callsign' and add it to 'aircrafts_data',
# unless an entry for 'callsign' already exists. Returns the modified hash.
#### Version and changelog
# current: v1.1.0, minimum compatible: v1.0.0
#
## v1.1.0:
# Allow external transmission restrictions
# Make transmitting contact IFF optional
# Ensure personal identifier has no '!'
# '\n' is redundant for printf()
# Fix separator character in documentation
# Fix error when sending unknown extension
#
## v1.0.1:
# Add is_known(), is_friendly(), is_hostile() helpers to extension "contacts".
#
## v1.0.0: Initial version
# - Core protocol for datalink channel.
# - Extensions "contacts", "identifier", and "point".
### Parameters
#
# Remark: most parameters need to be the same on all aircrafts.
# Index of multiplayer string used to transmit datalink info.
# Must be the same for all aircrafts.
var mp_string = 7;
var mp_path = "sim/multiplay/generic/string["~mp_string~"]";
var channel_hash_period = 600;
var receive_period = getprop("/instrumentation/datalink/receive_period") or 1;
# Should be overwitten to add transmission restrictions.
var can_transmit = func(contact, mp_prop, mp_index) {
return 1;
}
### Properties
var input = {
power: getprop("/instrumentation/datalink/power_prop"),
channel: getprop("/instrumentation/datalink/channel_prop"),
ident: getprop("/instrumentation/datalink/identifier_prop"),
mp: mp_path,
models: "/ai/models",
callsign: "/sim/multiplay/callsign",
};
foreach (var name; keys(input)) {
if (input[name] != nil) {
input[name] = props.globals.getNode(input[name], 1);
}
}
#### Core protocol implementation
### Channel hash (based on iff.nas)
#
# Channel is hashed with current time (rounded to 10min) and own callsign.
var clean_callsign = func(callsign) {
return damage.processCallsign(callsign);
}
var my_callsign = func {
return clean_callsign(input.callsign.getValue());
}
# Time, rounded to 'channel_hash_period'. This is used to hash channel.
var get_time = func {
return int(math.floor(systime() / channel_hash_period) * channel_hash_period);
}
# Previous / next time (with channel_hash_period interval).
# This is used to give a bit of margin on the time check.
# (will work if system clocks are coordinated within 10min).
var get_prev_time = func { return get_time() - channel_hash_period; }
var get_next_time = func { return get_time() + channel_hash_period; }
var parse_hexadecimal = func(str) {
var res = 0;
for (var i=0; i<size(str); i+=1) {
res *= 10;
var c = str[i];
if (c >= 48 and c < 58) {
# digit
res += c - 48;
} elsif (c >= 65 and c < 71) {
# upper case letter
res += c - 55;
} elsif (c >= 97 and c < 103) {
# lower case letter
res += c - 87;
}
}
return res;
}
var _hash_channel = func(time, callsign, channel) {
# 5 hex digits (2^20) fit in 3 chars for emesary int encoding.
var hash = parse_hexadecimal(left(md5(time ~ callsign ~ channel), 5));
return emesary.TransferInt.encode(hash, 3);
}
# Hash channel (when sending).
var encode_channel = func(channel) {
return _hash_channel(get_time(), my_callsign(), channel);
}
# Check that the hash transmitted by aircraft 'callsign' is correct for 'channel'.
var check_channel = func(hash, callsign, channel) {
return hash == _hash_channel(get_time(), callsign, channel)
or hash == _hash_channel(get_prev_time(), callsign, channel)
or hash == _hash_channel(get_next_time(), callsign, channel);
}
### Contact object
var Contact = {
new: func(callsign) {
var c = {
# contact_parents is the list of all classes from which contacts inherit (for extensions).
parents: contact_parents,
_callsign: callsign,
};
# Initialize all inherited classes.
foreach (var class; contact_parents) {
call(class.init, [], c, nil, nil);
}
return c;
},
init: func { me._on_link = 0; },
callsign: func { return me._callsign; },
index: func { return callsign_to_index[me._callsign]; },
on_link: func { return me._on_link; },
set_on_link: func(b) { me._on_link = b; },
};
### Extensions
var extensions = {};
var extension_prefixes = {};
var max_prefix_length = 0;
var contact_parents = [Contact];
var register_extension = func(name, prefix, class, encode, decode) {
if (contains(extensions, name)) {
printf("Datalink: double registration of extension '%s'. Skipping.", name);
return -1;
}
if (contains(extension_prefixes, prefix)) {
printf("Datalink: double registration of extension prefix '%s'. Skipping.", name);
return -1;
}
extensions[name] = { prefix: prefix, encode: encode, decode: decode, };
extension_prefixes[prefix] = name;
max_prefix_length = math.max(max_prefix_length, size(prefix));
append(contact_parents, class);
return 0;
}
var data_separator = "!";
### Transmission
var clear_data = func {
send_data({});
}
var clear_timer = maketimer(1, clear_data);
clear_timer.singleShot = 1;
# Send data through datalink.
#
# timeout: if set, sent data will be cleared after this time (other aircrafts
# won't receive it anymore). Useful if 'send_data' is not called often.
var send_data = func(data, timeout=nil) {
if (!input.power.getBoolValue()) {
last_data = {};
input.mp.setValue("");
return;
}
# First encode channel
var str = encode_channel(input.channel.getValue());
# Then all extensions
last_data = data;
foreach(var ext; keys(data)) {
# Skip missing extensions with a warning
if (!contains(extensions, ext)) {
printf("Warning: unknown datalink extension %s in send_data().", ext);
continue;
}
str = str ~ data_separator ~ extensions[ext].prefix ~ extensions[ext].encode(data[ext]);
}
input.mp.setValue(str);
if (timeout != nil) {
clear_timer.restart(timeout);
}
}
# Used internally to update the channel/identifier while keeping the same data.
# Does not touch timeout.
var last_data = {};
var resend_data = func {
send_data(last_data);
}
# Very slow timer to ensure the channel hash is updated regularly.
# Only relevant if you never call send_data();
var hash_update_timer = maketimer(channel_hash_period/2, resend_data);
hash_update_timer.start();
### Receiving
# callsign to data hash
var aircrafts_data = {};
# List of callsigns / indices connected on datalink (index is for /ai/models/multiplayer[i]).
var connected_callsigns = [];
var connected_indices = [];
# Maintain callsign to multiplayer index hash
# (doesn't cost much since we already iterate over MP models).
var callsign_to_index = {};
var get_data = func(callsign) {
return aircrafts_data[callsign];
}
var get_connected_callsigns = func {
return connected_callsigns;
}
var get_connected_indices = func {
return connected_indices;
}
var get_all_callsigns = func {
return keys(aircrafts_data);
}
# Helper for modifying aircrafts_data.
var add_if_missing = func(aircrafts_data, callsign) {
if (!contains(aircrafts_data, callsign)) {
aircrafts_data[callsign] = Contact.new(callsign);
}
return aircrafts_data;
}
var receive_loop = func {
var my_channel = input.channel.getValue();
aircrafts_data = {};
connected_callsigns = [];
connected_indices = [];
var mp_models = input.models.getChildren("multiplayer");
foreach(var mp; mp_models) {
var idx = mp.getIndex();
if (!mp.getValue("valid")) continue;
var callsign = mp.getValue("callsign");
if (callsign == nil) continue;
callsign_to_index[callsign] = idx;
var data = mp.getValue(mp_path);
if (data == nil) continue;
# Split channel part and data part
var tokens = split(data_separator, data);
# Check channel
if (!check_channel(tokens[0], callsign, my_channel)) continue;
# We check this _after_ the channel. Checking the channel is quite cheap,
# and we don't know how slow this function is, it might have a get_cart_ground_intersection()
if (!can_transmit(callsign, mp, idx)) continue;
# Add to list of connected aircrafts.
append(connected_callsigns, callsign);
append(connected_indices, idx);
# Add to data
aircrafts_data = add_if_missing(aircrafts_data, callsign);
aircrafts_data[callsign].set_on_link(1);
# Parse extensions data
for (var i=1; i<size(tokens); i+=1) {
var extension = nil;
# Identify extension prefix. This is not very clever code, but
# realistically it doesn't matter since prefixes are very short.
var len = 1;
for (; len <= max_prefix_length; len += 1) {
if (len > size(tokens)) break;
var prefix = left(tokens[i], len);
if (contains(extension_prefixes, prefix)) {
extension = extension_prefixes[prefix];
break;
}
}
# Unknown extension, skip
if (extension == nil) continue;
# Remove prefix
var data = substr(tokens[i], len);
# Decode
aircrafts_data = extensions[extension].decode(aircrafts_data, callsign, data);
}
}
}
var receive_timer = maketimer(receive_period, receive_loop);
# Start / stop listener
setlistener(input.power, func (node) {
if (node.getBoolValue()) {
receive_timer.start();
resend_data(); # Sets channel/identifier
} else {
receive_timer.stop();
aircrafts_data = {};
clear_data();
}
}, 1, 0);
# Listener to resend data so as to update the channel.
setlistener(input.channel, resend_data);
#### Extensions
## Identifier
var ContactIdentifier = {
init: func {
me._identifier = nil;
},
set_identifier: func(ident) {
me._identifier = ident;
},
identifier: func {
return me._identifier;
},
};
var encode_identifier = func(ident) {
# Force string conversion
ident = ""~ident;
if (find("!", ident) >= 0) {
printf("Datalink: Identifier is not allowed to contain '!': %s.", ident);
return "";
} else {
return ident;
}
}
var decode_identifier = func(aircrafts_data, callsign, str) {
aircrafts_data = add_if_missing(aircrafts_data, callsign);
aircrafts_data[callsign].set_identifier(str);
return aircrafts_data;
}
register_extension("identifier", "I", ContactIdentifier, encode_identifier, decode_identifier);
## Contacts
# IFF status transmitted over datalink.
var IFF_UNKNOWN = 0; # Unknown status
var IFF_HOSTILE = 1; # Considered hostile (no response to IFF).
var IFF_FRIENDLY = 2; # Friendly, because positive IFF identification.
# This is also the priority order for IFF reports in case of conflicts:
# e.g. a contact will be reported as friendly if anyone on datalink reports it as friendly.
var ContactTracked = {
init: func {
me._tracked_by = nil;
me._iff = IFF_UNKNOWN;
},
set_tracked_by: func(callsign) {
me._tracked_by = callsign;
},
set_iff: func(iff) {
# Priority order on IFF values (friendly, then hostile, then no data).
me._iff = math.max(me._iff, iff);
},
tracked: func {
return me._tracked_by != nil;
},
tracked_by: func {
return me._tracked_by;
},
tracked_by_index: func {
return (me._tracked_by != nil) ? callsign_to_index[me._tracked_by] : nil;
},
iff: func {
return me._iff;
},
is_known: func {
return me.on_link() or me.tracked();
},
is_friendly: func {
return me.on_link() or me.iff() == IFF_FRIENDLY;
},
is_hostile: func {
return !me.on_link() and me.iff() == IFF_HOSTILE;
},
};
# Contact encoding: callsign + bits
# callsign: the callsign encoded with emesary.TransferString
# bits: bitfield |xxxxxxff| (left is most significant)
# f: IFF, x: unused
# encoded with emesary.TransferByte
# Additional values may be appended for extensions.
var encode_contact = func(contact) {
# Encode bitfield
var bits = contact["iff"] != nil ? contact.iff : IFF_UNKNOWN;
return emesary.TransferString.encode(clean_callsign(contact.callsign))
~ emesary.TransferByte.encode(bits);
}
var decode_contact = func(str) {
var res = {};
var dv = emesary.TransferString.decode(str, 0);
res.callsign = dv.value;
dv = emesary.TransferByte.decode(str, dv.pos);
var bits = dv.value;
res.iff = math.mod(bits, 4);
return res;
}
# Special character, won't be used by emesary encoding.
var contacts_separator = "#";
var encode_contacts = func(contacts) {
var str = "";
foreach (var contact; contacts) {
str = str~encode_contact(contact)~contacts_separator;
}
return str;
}
var decode_contacts = func(aircrafts_data, callsign, str) {
var contacts = split(contacts_separator, str);
foreach (var contact; contacts) {
if (contact == "") continue;
var res = decode_contact(contact);
aircrafts_data = add_if_missing(aircrafts_data, res.callsign);
aircrafts_data[res.callsign].set_iff(res.iff);
aircrafts_data[res.callsign].set_tracked_by(callsign);
}
return aircrafts_data;
}
register_extension("contacts", "C", ContactTracked, encode_contacts, decode_contacts);
## Coordinate
var ContactPoint = {
init: func {
me._point = nil;
},
set_point: func(point) {
me._point = point;
},
point: func {
return me._point;
},
};
var encode_point = func(coord) {
return emesary.TransferCoord.encode(coord);
}
var decode_point = func(aircrafts_data, callsign, str) {
var coord = emesary.TransferCoord.decode(str, 0).value;
aircrafts_data = add_if_missing(aircrafts_data, callsign);
aircrafts_data[callsign].set_point(coord);
return aircrafts_data;
}
register_extension("point", "P", ContactPoint, encode_point, decode_point);

View File

@ -0,0 +1,55 @@
################### SAM INFO ###################
var setupTime = 300;#minimum 'launcher_tilt_time' secs no matter what, due to anim and stuff.
var reload_time = 600;
var launcher_final_tilt_deg = 33;
var launcher_start_tilt_deg = 0;
var launcher_tilt_time = 15;
var sam_align_to_target = 0;
var launcher_align_to_target = 1;
var align_speed_dps = 20;
var radar_elevation_above_terrain_m = 10;#estimate
var radar_lowest_pitch = 0.1;# 0.5 degs = roughly 925 feet at 20 nm, 25 feet at half a nm. # 0.35 = roughly 925 feet at 20 nm, 25 feet at half a nm.
#reaction tme for s-300p is 28 secs acording to http://www.astronautix.com/s/s-300p.html
# sounds a bit high for pmu, as its 4 secs for s-400
################### CIWS INFO ###################
var ciws_installed = 0;
var ciws_domain_nm = 1.50; #range where it can kill
var ciws_chance = 0.20; #chance to get a kill at 0nm distance
var ciws_burst_rounds = 60;#how many rounds in a burst
var ciws_shell = 15;#from lookup table in damage.nas
var ROUNDS_init = 30;
var ROUNDS = ROUNDS_init;#CIWS bursts remaining
################### MISSILE INFO - http://www.astronautix.com/k/kub.html ###################
var NUM_MISSILES = 2; # total carried minus 1
var missile_name = "m3m9";
var missile_brevity = "3M9";
var missile_max_distance = 18; #max distance of target in nm when it will fire
var missile_min_distance = 0.5; #minimum distance in nm when it will fire
var lockon_time = 24; #time in seconds it takes to lock on and get a firing solution on a target
var fire_minimum_interval = 7;# time since last track was initiated till a new can be initiated
var same_target_max_missiles = 2;# max number of missiles in air against same target
var isInEngagementEnvelope = func (target_radial_airspeed, target_ground_distance, target_relative_altitude) {
return 1;
}
var midflight = func (struct) {
if (struct.guidance == "semi-radar") {
# This makes the SAM system keep lock on target when missile in-flight and no longer tracking the target.
# Usage is to make the RWR lock sound go off in targets cockpit.
thread.lock(mutexLock);
semi_active_track = struct.callsign;
thread.unlock(mutexLock);
}
return {};
};

View File

@ -1,4 +1,4 @@
################### GLOBALS ###################
################### GLOBALS
var false = 0;
var true = 1;
@ -20,58 +20,6 @@ var ACTIVE_MISSILE = 0;
var semi_active_track = nil;# with multiple missiles flying in semi-active-radar mode, this can change rather fast. Which means pilots wont get a steady tone, but disrupted tones. (tradeoff)
var mutexLock = thread.newlock();
################### SAM INFO ###################
var setupTime = 300;#minimum 'launcher_tilt_time' secs no matter what, due to anim and stuff.
var reload_time = 600;
var launcher_final_tilt_deg = 33;
var launcher_start_tilt_deg = 0;
var launcher_tilt_time = 15;
var sam_align_to_target = 0;
var launcher_align_to_target = 1;
var align_speed_dps = 20;
var radar_elevation_above_terrain_m = 10;#estimate
var radar_lowest_pitch = 0.1;# 0.5 degs = roughly 925 feet at 20 nm, 25 feet at half a nm. # 0.35 = roughly 925 feet at 20 nm, 25 feet at half a nm.
#reaction tme for s-300p is 28 secs acording to http://www.astronautix.com/s/s-300p.html
# sounds a bit high for pmu, as its 4 secs for s-400
################### CIWS INFO ###################
var ciws_installed = 0;
var ciws_domain_nm = 1.50; #range where it can kill
var ciws_chance = 0.20; #chance to get a kill at 0nm distance
var ciws_burst_rounds = 60;#how many rounds in a burst
var ciws_shell = 15;#from lookup table in damage.nas
var ROUNDS_init = 30;
var ROUNDS = ROUNDS_init;#CIWS bursts remaining
################### MISSILE INFO - http://www.astronautix.com/k/kub.html ###################
var NUM_MISSILES = 2; # total carried minus 1
var missile_name = "m3m9";
var missile_brevity = "3M9";
var missile_max_distance = 18; #max distance of target in nm when it will fire
var missile_min_distance = 0.5; #minimum distance in nm when it will fire
var lockon_time = 24; #time in seconds it takes to lock on and get a firing solution on a target
var fire_minimum_interval = 7;# time since last track was initiated till a new can be initiated
var same_target_max_missiles = 2;# max number of missiles in air against same target
var isInEngagementEnvelope = func (target_radial_airspeed, target_ground_distance, target_relative_altitude) {
return 1;
}
var midflight = func (struct) {
if (struct.guidance == "semi-radar") {
# This makes the SAM system keep lock on target when missile in-flight and no longer tracking the target.
# Usage is to make the RWR lock sound go off in targets cockpit.
thread.lock(mutexLock);
semi_active_track = struct.callsign;
thread.unlock(mutexLock);
}
return {};
};
##########################################################################
######################## Common SAM code ###########################
@ -79,6 +27,8 @@ var midflight = func (struct) {
################### VARIOUS
setprop("sim/multiplay/generic/float[7]", 0);# tracking radar vert rotation
setprop("sim/multiplay/generic/float[8]", 0);# tracking radar horiz rotation
setprop("sim/multiplay/generic/float[5]", 0);# launcher vert rotation
setprop("sim/multiplay/generic/float[6]", 0);# launcher horiz rotation
var start_time = systime();
@ -139,7 +89,7 @@ var buildTargetList = func {
};
var lookup = func (sign) {
foreach (tgt ; targetsV2.vector) {
if (tgt.callsign == sign) {
return tgt;
@ -177,9 +127,11 @@ var scan = func() {
buildTargetList();
if ( getprop("/carrier/sunk") == 1 or getprop("/carrier/disabled") == 1) {
setprop("sim/multiplay/generic/string[6]", "");
datalink.clear_data();
return;
}
if (systime() - start_time < setupTime ) {
printf("Seconds till activation: %.1f", setupTime - (systime() - start_time));
setprop("sam/timeleft", setupTime - (systime() - start_time));
@ -193,8 +145,11 @@ var scan = func() {
setprop("sim/multiplay/generic/float[5]", launcher_final_tilt_deg);
}
setprop("sim/multiplay/generic/float[6]", 0);
setprop("sim/multiplay/generic/float[7]", 0);
setprop("sim/multiplay/generic/float[8]", 0);
setprop("sim/multiplay/generic/string[6]", "");
setprop("/sim/multiplay/generic/int[2]", 1);
datalink.clear_data();
return;
}
setprop("sim/multiplay/generic/float[5]", launcher_final_tilt_deg);
@ -223,12 +178,12 @@ var scan = func() {
}
prio_vector = sort(prio_vector, func (a,b) {if (a[2]>b[2]){return -1;} elsif (a[2]<b[2]) {return 1;} else {return 0;}});
foreach(var mp; prio_vector) {
#### DO WE HAVE FIRING SOLUTION... ####
# is plane in range, do we still have missiles, and is a missile already inbound, and has it been 4 seconds since the last missile launch?
var trigger = mp[1];
#print("dist to target = " ~ dist_to_target);
var lu = lookup(mp[0].getNode("callsign").getValue());
if ( launch_in_progress == 0 and trigger == true and lu != nil and lu.in_air < same_target_max_missiles and !lu.tracking and ACTIVE_MISSILE <= NUM_MISSILES and ( systime() - missile_delay_time > fire_minimum_interval ) ) { #
#### ... FOR THE MISSILE ####
@ -238,7 +193,7 @@ var scan = func() {
armament.contact = radar_logic.Contact.new(mp[0], AIR);
launch_in_progress = 1;
missile_launch(mp[0], systime(), my_pos);
} elsif ( ciws_installed and trigger == -1 and ROUNDS > 0 and ( systime() - ciws_delay_time > 1.0 ) and lu != nil) {
#### ... FOR THE CIWS ####
var contact = radar_logic.Contact.new(mp[0], AIR);
@ -272,7 +227,7 @@ var scan = func() {
reloading = 1;
reload_starting = systime();
}
}
}
if (lu != nil and lu.tracking) {
radarOn = 1;
}
@ -318,8 +273,10 @@ var clearSingleLock = func () {
thread.lock(mutexLock);
if (semi_active_track == nil) {
setprop("sim/multiplay/generic/string[6]", "");
datalink.clear_data();
} else {
setprop("sim/multiplay/generic/string[6]", left(md5(semi_active_track), 4));
datalink.send_data({"contacts":[{"callsign":semi_active_track,"iff":0}]});
}
thread.unlock(mutexLock);
}
@ -335,7 +292,7 @@ var fire_control = func(mp, my_pos) {
var ufo_pos = geo.Coord.new().set_latlon(mp.getNode("position/latitude-deg").getValue(),mp.getNode("position/longitude-deg").getValue(),(mp.getNode("position/altitude-ft").getValue() * FT2M));
var target_distance = my_pos.direct_distance_to(ufo_pos);
var target_altitude = mp.getNode("position/altitude-ft").getValue();
# is this plane a friend or foe?
var lu = lookup(mp.getNode("callsign").getValue());
if ( lu == nil ) { return [mp,false,0,0]; }
@ -344,11 +301,11 @@ var fire_control = func(mp, my_pos) {
var target_ground_distance = my_pos.distance_to(ufo_pos);
var target_heading = mp.getNode("orientation/true-heading-deg").getValue();
var relative_bearing = math.abs(geo.normdeg180(ufo_pos.course_to(my_pos) - target_heading));
var target_relative_altitude = target_altitude - my_pos.alt()*M2FT;
var target_radial_airspeed = math.abs(math.abs(((relative_bearing / 90 ) - 1) * target_airspeed));#200kt against us = 200kt, 200kt at angle of us = 0kt, 200kt away from us = 200kt
var target_relative_pitch = vector.Math.getPitch(my_pos, ufo_pos);
# can the radar see it?
var data_link_match = false;
@ -358,7 +315,7 @@ var fire_control = func(mp, my_pos) {
# #print('tgt confirmed via datalink');
# }
# }
var visible = radar_logic.isNotBehindTerrain(mp);
@ -367,9 +324,10 @@ var fire_control = func(mp, my_pos) {
var radarSeeTarget = 1;
if ( data_link_match == false ) {
if ( visible[0] == false ) {radarSeeTarget = 0 }
if ( target_relative_pitch < radar_lowest_pitch ) { radarSeeTarget = 0 } #
if ( target_relative_pitch < radar_lowest_pitch ) { radarSeeTarget = 0 } #
if ( target_distance*M2NM > 1.0 and visible[1] and math.abs(target_radial_airspeed) < 20 ) {radarSeeTarget = 0 } # i.e. notching with terrain behind
}
if (radarSeeTarget and rand() > target_distance*M2NM / ciws_domain_nm) {
var score = 0;
var priority = getprop("priority");
@ -395,26 +353,26 @@ var fire_control = func(mp, my_pos) {
if ( data_link_match == false ) {
if (target_airspeed < 60) {return [mp,false,0,0];}
if ( visible[0] == false ) { return [mp,false,0,0]; }
if ( target_relative_pitch < radar_lowest_pitch ) { return [mp,false,0,0]; } #
if ( target_relative_pitch < radar_lowest_pitch ) { return [mp,false,0,0]; } #
if ( visible[1] and math.abs(target_radial_airspeed) < 20 ) { return [mp,false,0,0]; } # i.e. notching, landed aircraft
}
if ( target_distance * M2NM > missile_max_distance ) { return [mp,false,0,0]; }
# is the plane within the engagement envelope?
# the numbers after target_radial_airspeed are ( offset_of_engagement_envelope / speed_to_apply_that_offset )
# larger offset means it wont fire until the plane is closer.
# for visualization: https://www.desmos.com/calculator/gw570fa9km
if (!isInEngagementEnvelope(target_radial_airspeed, target_ground_distance, target_relative_altitude) ) { return [mp,false,0,0]; }
if (!isInEngagementEnvelope(target_radial_airspeed, target_ground_distance, target_relative_altitude) ) { return [mp,false,0,0]; }
var the_dice = rand();
if ( the_dice > 0.20 ) {
var score = 0;
var priority = getprop("priority");
@ -461,7 +419,7 @@ var reload = func(force = 1) {
if (ROUNDS == 0 or force == 1) {
ROUNDS = ROUNDS_init;
print("Reloaded with "~(ROUNDS)~" bursts.");
}
}
setprop("sam/missiles",(NUM_MISSILES+1-ACTIVE_MISSILE));
setprop("sam/bursts", ROUNDS);
reloading = 0;
@ -493,6 +451,7 @@ var missile_launch = func(mp, launchtime, my_pos) {
}
var ufo_pos = geo.Coord.new().set_latlon(mp.getNode("position/latitude-deg").getValue(),mp.getNode("position/longitude-deg").getValue(),(mp.getNode("position/altitude-ft").getValue() * 0.3048));
var target_bearing = my_pos.course_to(ufo_pos);
var target_pitch = vector.Math.getPitch(my_pos, ufo_pos);
var info = mp.getNode("callsign").getValue();
var lu = lookup(mp.getNode("callsign").getValue());
if (getprop("sam/timeleft") != 0) {
@ -510,6 +469,8 @@ var missile_launch = func(mp, launchtime, my_pos) {
setprop("sim/multiplay/generic/float[6]", target_bearing-getprop("orientation/heading-deg"));
theMissile.rail_head_deg = getprop("sim/multiplay/generic/float[6]");
}
setprop("sim/multiplay/generic/float[7]", target_pitch);
setprop("sim/multiplay/generic/float[8]", target_bearing-getprop("orientation/heading-deg"));
if (sam_align_to_target) {
setprop("/orientation/heading-deg",target_bearing);
}
@ -524,7 +485,7 @@ var missile_launch = func(mp, launchtime, my_pos) {
settimer(autoreload, reload_time);
reloading = 1;
reload_starting = systime();
}
}
if (lu != nil) {
append(lu.missiles, theMissile);
lu.in_air += 1;
@ -551,15 +512,19 @@ var missile_launch = func(mp, launchtime, my_pos) {
} else {
info = info~" (tracking)";
if (systime()-missile_release_time > 1.5) {
var tgt_dir = target_bearing-getprop("orientation/heading-deg");
var max_dir = launch_update_time*align_speed_dps;
if (launcher_align_to_target) {
# now smoothly rotate launcher:
var tgt_dir = target_bearing-getprop("orientation/heading-deg");
var cur_dir = getprop("sim/multiplay/generic/float[6]");
var move_dir = geo.normdeg180(tgt_dir-cur_dir);
var max_dir = launch_update_time*align_speed_dps;
move_dir = math.clamp(move_dir, -max_dir, max_dir);
setprop("sim/multiplay/generic/float[6]", cur_dir+move_dir);
}
var cur_dir2 = getprop("sim/multiplay/generic/float[8]");
var move_dir2 = geo.normdeg180(tgt_dir-cur_dir2);
move_dir2 = math.clamp(move_dir2, -max_dir, max_dir);
setprop("sim/multiplay/generic/float[8]", cur_dir2+move_dir2);
if (sam_align_to_target) {
var tgt_dir = target_bearing;
var cur_dir = getprop("orientation/heading-deg");
@ -573,6 +538,7 @@ var missile_launch = func(mp, launchtime, my_pos) {
setprop("sam/info", info);
if (mp.getNode("callsign") != nil and mp.getNode("callsign").getValue() != nil and mp.getNode("callsign").getValue() != "") {
setprop("sim/multiplay/generic/string[6]", left(md5(mp.getNode("callsign").getValue()), 4));
datalink.send_data({"contacts":[{"callsign":mp.getNode("callsign").getValue(),"iff":0}]});
} else {
clearSingleLock();
}
@ -580,6 +546,7 @@ var missile_launch = func(mp, launchtime, my_pos) {
lu.tracking = 1;
}
}
settimer( func { missile_launch(mp, launchtime, my_pos); },launch_update_time);
}

View File

@ -356,7 +356,7 @@
<damage type="double">100</damage>
</sam>
<enemies>
<enemies>
<e1 userarchive="y" type="string">target1</e1>
<e2 userarchive="y" type="string">target2</e2>
<e3 userarchive="y" type="string">target3</e3>
@ -373,6 +373,17 @@
<priority type="int">0</priority>
<priority0 type="int">1</priority0>
<instrumentation>
<datalink>
<data type="int">0</data>
<power type="bool">true</power>
<channel type="int">1212</channel>
<power_prop type="string">/instrumentation/datalink/power</power_prop>
<channel_prop type="string">instrumentation/datalink/channel</channel_prop>
<receive_period type="double">1</receive_period>
</datalink>
</instrumentation>
<nasal>
<notifications>
@ -394,7 +405,11 @@
<armament>
<file>Aircraft/SA-6/Nasal/guided-missiles.nas</file>
</armament>
<datalink>
<file>Aircraft/SA-6/Nasal/datalink.nas</file>
</datalink>
<fire_control>
<file>Aircraft/SA-6/Nasal/fire-control-custom.nas</file>
<file>Aircraft/SA-6/Nasal/fire-control.nas</file>
</fire_control>
</nasal>

BIN
SA-6/Sounds/launch.wav Normal file

Binary file not shown.

25
SA-6/Sounds/sound.xml Normal file
View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<PropertyList>
<fx>
<launch>
<name>missile</name>
<path>Aircraft/SA-6/Sounds/launch.wav</path>
<mode>once</mode>
<condition>
<property>payload/armament/m3m9/sound-fire-on-off</property>
</condition>
<reference-dist>50</reference-dist>
<max-dist>15000</max-dist>
<position>
<x> 0.01 </x>
<y> 0.01 </y>
<z> 0.01 </z>
</position>
<volume>
<factor>1</factor>
<max>1</max>
</volume>
</launch>
</fx>
</PropertyList>

View File

@ -5,8 +5,8 @@
<layout>vbox</layout>
<draggable>true</draggable>
<resizable>false</resizable>
<width>125</width>
<height>200</height>
<width>150</width>
<height>250</height>
<group>
<empty><stretch>1</stretch></empty>
@ -144,5 +144,23 @@
<script>setprop("priority",6);setprop("priority6",1);setprop("priority0",0);setprop("priority1",0);setprop("priority2",0);setprop("priority3",0);setprop("priority4",0);setprop("priority5",0);</script>
</binding>
</radio>
<input>
<row>7</row>
<col>0</col>
<width>75</width>
<height>25</height>
<label>DLNK</label>
<property>instrumentation/datalink/channel</property>
<live>true</live>
<halign>right</halign>
<binding>
<command>dialog-apply</command>
</binding>
<color>
<red>0.75</red>
<green>0.75</green>
<blue>1.0</blue>
</color>
</input>
</group>
</PropertyList>