32 Commits

Author SHA1 Message Date
3a5a7a17a3 idk 2026-04-13 08:04:43 -03:00
0486f2a285 Added initial COT Sensor payloads to support UAV Tools integration for Arma Drones 2026-04-13 08:03:31 -03:00
753dcab26e Added hellanmaaw winter support 2026-03-31 07:23:48 -03:00
2f53488ba8 Added video url prop to 3den editor/zeus, allowing to parse __video prop to cots 2026-03-31 07:21:29 -03:00
323339e679 Removed video addon, too simple for a specific addon 2026-03-31 07:20:19 -03:00
3f14a75e81 Added video url parser to CoT types 2026-03-31 07:19:39 -03:00
469a54c141 Added Hellanmma map support 2026-03-31 07:18:23 -03:00
2ee9030c00 Updated media folder 2026-03-26 14:45:08 -03:00
5b29a40990 Improved mTLS description on readme 2026-03-26 03:47:54 -03:00
708fe5e670 Fixed CoT queue during armatak connection to the TAK Server, running soft as butter 2026-03-26 03:45:05 -03:00
e32aadda4e Splitted Connection Module 2026-03-26 01:05:54 -03:00
c35b7f0268 Updated project readme file 2026-03-24 16:56:26 -03:00
876cf900c3 Changed dialogs and module UI to get mTLS needed params 2026-03-24 16:56:19 -03:00
778ac0ac54 Added the mTLS connection calls to zeus and 3den modules 2026-03-24 16:55:53 -03:00
b816144fb0 Added transport layer and configured extension commands to call mTLS socket connection 2026-03-24 16:55:36 -03:00
61ba9f6d63 Added connector and enrollment for mTLS client certificate auto enrollment on game sessions, will MOCK a official tak client behavior when authenticating 2026-03-24 16:55:05 -03:00
f88c02a7aa formatted some rust files for linting porpuses 2026-03-24 16:44:22 -03:00
5ffc08e6f1 Readded reqwest dependency to cargo toml, will be used for TAK Server API interaction on authencated tak server connections 2026-03-24 16:41:38 -03:00
9392380c78 Added hemtt private key to git ignore 2026-03-24 16:40:58 -03:00
a18343b81d Commented video module 2026-03-24 14:03:28 -03:00
Valmo Trindade
13cd08c655 Added mandol map support 2026-01-03 02:55:39 -03:00
Valmo Trindade
8fe14dc18d Added Clafghan Map Support 2026-01-02 03:28:10 -03:00
Valmo Trindade
1bec26df8a Added UMP Colombia map support 2025-12-21 02:21:02 -03:00
Valmo Trindade
c5d5da636f added malvinas pradero ganso function call 2025-12-13 15:00:33 -03:00
Valmo Trindade
c2e137e67c Added lawn map 2025-12-13 14:59:05 -03:00
Valmo Trindade
de5ac9dbb5 Added Malvinas Maps 2025-12-12 19:12:04 -03:00
Valmo Trindade
ef3be1e768 fixed router entity remover function 2025-12-10 23:07:53 -03:00
Valmo Trindade
9bda92d389 Removed once cell dependency 2025-11-30 10:43:43 -03:00
Valmo Trindade
9763cb6697 reoganized command groups on extension call 2025-11-30 10:43:18 -03:00
Valmo Trindade
5ac49e12f8 customized sensor markers to dont cheat enemy info 2025-11-26 20:21:09 -03:00
Valmo Trindade
2108d20b01 changed default affiliation to be unkown 2025-11-26 20:20:51 -03:00
Valmo Trindade
524e7a0b3e Added vehicle sensor handler to get sensor input and throw it on ATAK 2025-11-22 12:15:15 -03:00
152 changed files with 13341 additions and 918 deletions

3
.gitignore vendored
View File

@@ -3,6 +3,7 @@
hemtt
hemtt.exe
*.biprivatekey
.hemttprivatekey
source/
.vscode
releases/
@@ -87,4 +88,4 @@ target/
.cxx
local.properties
*.apk
*.apk

1074
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,10 @@ chrono = "0.4.39"
lazy_static = "1.5.0"
log = "0.4.22"
log4rs = "1.3.0"
once_cell = "1.19.0"
reqwest = { version = "0.12.15", default-features = false, features = ["blocking", "json", "rustls-tls"] }
rcgen = { version = "0.13.2", default-features = false, features = ["crypto", "pem", "aws_lc_rs"] }
rustls = "0.23.23"
rustls-pemfile = "2.2.0"
serde = { version = "1.0.210", features = ["derive"] }
[dependencies.uuid]

View File

@@ -4,6 +4,10 @@
ARMATAK is a server side Arma 3 addons for streaming unit positions to TAK Clients in sessions on real locations maps. It can be runned both as a clientside mod or a serverside mod, when runned serverside, it will create a TCP Socket connection between Arma 3 and the TAK Server, sending the game session information into it. When used clientside, Arma 3 will host a websocket server that you can connect to your phone and mock the phone's location to the player's in game location.
The server-side CoT router supports two transports:
- Plain TCP, for legacy TAK ingress.
- Mutual TLS, using the TAK Server authentication API, so the Arma session can publish as an authenticated TAK device on port `8089`.
## Get in Touch
[Join the Discord Server for ARMATAK!](https://discord.gg/svK64PCycU)

View File

@@ -1,22 +1,22 @@
#include "..\script_component.hpp"
/*
* Author: Valmo Trindade
* This function is used to convert the position of a unit to the world world location.
*
* Argument:
* 0: in game latitude <NUMBER> is the latitude of the unit.
* 1: in game longitude <NUMBER> is the longitude of the unit.
* 2: in game altitude <NUMBER> is the altitude of the unit.
* 3: in game bearing <NUMBER> is the bearing of the unit.
*
* Return Value:
* ARRAY -> [latitude, longitude, altitude, bearing]
*
* Example:
* [player] call armatak_client_fnc_convertClientLocation;
*
* Public: Yes
* Author: Valmo Trindade
* This function is used to convert the position of a unit to the world world location.
*
* Argument:
* 0: in game latitude <NUMBER> is the latitude of the unit.
* 1: in game longitude <NUMBER> is the longitude of the unit.
* 2: in game altitude <NUMBER> is the altitude of the unit.
* 3: in game bearing <NUMBER> is the bearing of the unit.
*
* Return Value:
* ARRAY -> [latitude, longitude, altitude, bearing]
*
* Example:
* [player] call armatak_client_fnc_convertClientLocation;
*
* Public: Yes
*/
params["_latitude", "_longitude", "_altitude"];
@@ -38,6 +38,9 @@ switch (toLower worldName) do {
case "vr": {
_realLocation = _position call armatak_fnc_convert_to_vr;
};
case "lawn": {
_realLocation = _position call armatak_fnc_convert_to_lawn;
};
case "cucui": {
_realLocation = _position call armatak_fnc_convert_to_cucui;
};
@@ -74,12 +77,42 @@ switch (toLower worldName) do {
case "kunduz_valley": {
_realLocation = _position call armatak_fnc_convert_to_kunduz_valley;
};
case "malvinasfalkands": {
_realLocation = _position call armatak_fnc_convert_to_malvinas_malvinasfalkands;
};
case "pebble_island_airfield": {
_realLocation = _position call armatak_fnc_convert_to_malvinas_pebble_island_airfield;
};
case "p_argentino_stanley": {
_realLocation = _position call armatak_fnc_convert_to_malvinas_p_argentino_stanley;
};
case "top_malo_house": {
_realLocation = _position call armatak_fnc_convert_to_malvinas_top_malo_house;
};
case "pradera_ganso": {
_realLocation = _position call armatak_fnc_convert_to_malvinas_pradera_ganso;
};
case "tanoa": {
_realLocation = _position call armatak_fnc_convert_to_tanoa;
};
case "zagor_zagorsk_reserved_forest": {
_realLocation = _position call armatak_fnc_convert_to_zagor_zagorsk_reserved_forest;
};
case "umb_colombia": {
_realLocation = _position call armatak_fnc_convert_to_colombia;
};
case "clafghan": {
_realLocation = _position call armatak_fnc_convert_to_clafghan;
};
case "rut_mandol": {
_realLocation = _position call armatak_fnc_convert_to_rut_mandol;
};
case "hellanmaa": {
_realLocation = _position call armatak_fnc_convert_to_hellanmaa;
};
case "hellanmaaw": {
_realLocation = _position call armatak_fnc_convert_to_hellanmaa;
};
default {
_warning = format ["<t color='#FF8021'>ARMATAK</t><br/> %1", "Unsupported Map"];
[[_warning, 1.5]] call CBA_fnc_notify;

View File

@@ -78,6 +78,16 @@ class Cfg3den {
condition = "objectVehicle";
typeName = "STRING";
};
class armatak_attribute_video_url {
displayName = "Video URL (RTSP)";
tooltip = "RTSP stream URL for UAS Tool integration. When set, the drone will appear in the ATAK UAS Tool with FOV cone and video feed. Format: rtsp://address:port/path (e.g. rtsp://192.168.1.10:8554/live/drone1). Leave empty to disable UAS Tool integration for this entity.";
property = "armatak_attribute_video_url";
control = "Edit";
expression = "_this setVariable ['armatak_attribute_video_url',_value]";
defaultValue = "''";
condition = "objectVehicle";
typeName = "STRING";
};
};
};
};

View File

@@ -10,12 +10,21 @@ class CfgFunctions {
class send_group_cots {
file = "\armatak\armatak\addons\main\functions\api\fn_send_group_cots.sqf";
};
class send_enemy_cot {
file = "\armatak\armatak\addons\main\functions\api\fn_send_enemy_cot.sqf";
};
class send_eud_cot {
file = "\armatak\armatak\addons\main\functions\api\fn_send_eud_cot.sqf";
};
class send_marker_cot {
file = "\armatak\armatak\addons\main\functions\api\fn_send_marker_cot.sqf";
};
class send_uas_video_cot {
file = "\armatak\armatak\addons\main\functions\api\fn_send_uas_video_cot.sqf";
};
class send_uas_sensor_cot {
file = "\armatak\armatak\addons\main\functions\api\fn_send_uas_sensor_cot.sqf";
};
class stop_tcp_socket {
file = "\armatak\armatak\addons\main\functions\api\fn_stop_tcp_socket.sqf";
};
@@ -31,6 +40,9 @@ class CfgFunctions {
class extract_role {
file = "\armatak\armatak\addons\main\functions\extract_data\fn_extract_role.sqf";
};
class extract_sensor_data {
file = "\armatak\armatak\addons\main\functions\extract_data\fn_extract_sensor_data.sqf";
};
class extract_unit_callsign {
file = "\armatak\armatak\addons\main\functions\extract_data\fn_extract_unit_callsign.sqf";
};
@@ -62,12 +74,30 @@ class CfgFunctions {
class convert_to_kunduz_valley {
file = "\armatak\armatak\addons\main\functions\map\fn_convert_to_kunduz_valley.sqf";
};
class convert_to_lawn {
file = "\armatak\armatak\addons\main\functions\map\fn_convert_to_lawn.sqf";
};
class convert_to_livonia {
file = "\armatak\armatak\addons\main\functions\map\fn_convert_to_livonia.sqf";
};
class convert_to_malden {
file = "\armatak\armatak\addons\main\functions\map\fn_convert_to_malden.sqf";
};
class convert_to_malvinas_malvinasfalkands {
file = "\armatak\armatak\addons\main\functions\map\fn_convert_to_malvinas_malvinasfalkands.sqf";
};
class convert_to_malvinas_p_argentino_stanley {
file = "\armatak\armatak\addons\main\functions\map\fn_convert_to_malvinas_p_argentino_stanley.sqf";
};
class convert_to_malvinas_pebble_island_airfield {
file = "\armatak\armatak\addons\main\functions\map\fn_convert_to_malvinas_pebble_island_airfield.sqf";
};
class convert_to_malvinas_pradera_ganso {
file = "\armatak\armatak\addons\main\functions\map\fn_convert_to_malvinas_pradera_ganso.sqf";
};
class convert_to_malvinas_top_malo_house {
file = "\armatak\armatak\addons\main\functions\map\fn_convert_to_malvinas_top_malo_house.sqf";
};
class convert_to_southen_sahrani {
file = "\armatak\armatak\addons\main\functions\map\fn_convert_to_southen_sahrani.sqf";
};
@@ -89,6 +119,15 @@ class CfgFunctions {
class convert_to_zagor_zagorsk_reserved_forest {
file = "\armatak\armatak\addons\main\functions\map\fn_convert_to_zagor_zagorsk_reserved_forest.sqf";
};
class convert_to_colombia {
file = "\armatak\armatak\addons\main\functions\map\fn_convert_to_colombia.sqf";
};
class convert_to_clafghan {
file = "\armatak\armatak\addons\main\functions\map\fn_convert_to_clafghan.sqf";
};
class convert_to_rut_mandol {
file = "\armatak\armatak\addons\main\functions\map\fn_convert_to_rut_mandol.sqf";
};
};
};
};

View File

@@ -35,7 +35,12 @@ addMissionEventHandler ["ExtensionCallback", {
[_function, "success", _name] call FUNC(notify);
};
case "TCP SOCKET ERROR": {
[_function, "error", _name] call FUNC(notify);
_message = _function;
if (_data isNotEqualTo "") then {
_message = format ["%1: %2", _function, _data];
};
[_message, "error", _name] call FUNC(notify);
};
case "VIDEO": {
[_function, "success", _name] call FUNC(notify);

View File

@@ -10,5 +10,5 @@ if (!isNull _digitalPointer) then {
_dpCot = [_link_uid, _contact_callsign, _digitalPointerPosition select 1, _digitalPointerPosition select 2, _digitalPointerPosition select 3];
"armatak" callExtension ["tcp_socket:send_digital_pointer_cot", [_dpCot]];
"armatak" callExtension ["tcp_socket:cot:digital_pointer", [_dpCot]];
};

View File

@@ -32,3 +32,6 @@ if (!isNil "_pre_defined_role") then {
};
_cot = [_drone, _atak_role, _atak_callsign] call armatak_fnc_send_marker_cot;
[_drone] call armatak_fnc_send_uas_video_cot;
[_drone] call armatak_fnc_send_uas_sensor_cot;

View File

@@ -9,7 +9,7 @@ _unit_position = _unit call armatak_client_fnc_extractClientPosition;
_uuid = _unit call armatak_fnc_extract_uuid;
_type = _unit call armatak_fnc_extract_role;
_callsign = _unit call armatak_fnc_extract_marker_callsign;
_marker_cot = [_uuid, _type, _unit_position select 1, _unit_position select 2, _unit_position select 3, _callsign, _unit_position select 5, _unit_position select 6];
"armatak" callExtension ["tcp_socket:send_marker_cot", [_marker_cot]];
"armatak" callExtension ["tcp_socket:cot:marker", [_marker_cot]];

View File

@@ -9,4 +9,4 @@ _position = _unit call armatak_client_fnc_extractClientPosition;
_uuid = _unit call armatak_fnc_extract_uuid;
_eud_cot = [_uuid, _position select 1, _position select 2, _position select 3, _callsign, _group_name, _group_role, _position select 5, _position select 6];
"armatak" callExtension ["tcp_socket:send_eud_cot", [_eud_cot]];
"armatak" callExtension ["tcp_socket:cot:eud", [_eud_cot]];

View File

@@ -4,10 +4,11 @@
params ["_unit", "_type", "_callsign"];
_unit_position = _unit call armatak_client_fnc_extractClientPosition;
_uuid = _unit call armatak_fnc_extract_uuid;
_marker_cot = [_uuid, _type, _unit_position select 1, _unit_position select 2, _unit_position select 3, _callsign, _unit_position select 5, _unit_position select 6];
"armatak" callExtension ["tcp_socket:send_marker_cot", [_marker_cot]];
_unit_position = _unit call armatak_client_fnc_extractClientPosition;
_video_url = [_unit] call armatak_fnc_extract_marker_video_url;
_uuid = _unit call armatak_fnc_extract_uuid;
_marker_cot = [_uuid, _type, _unit_position select 1, _unit_position select 2, _unit_position select 3, _callsign, _unit_position select 5, _unit_position select 6, _video_url];
"armatak" callExtension ["tcp_socket:cot:marker", [_marker_cot]];

View File

@@ -0,0 +1,55 @@
// function name: armatak_fnc_send_uas_sensor_cot
// function author: Valmo / ArmaTAK contributors
// function description:
// Sends a b-m-p-s-p-loc CoT event every router tick (1 s) for a drone.
// This is the "sensor position" event consumed by the ATAK UAS Tool to:
// - Draw the FOV cone on the moving map.
// - Compute four-corners for AR marker overlay on the video feed.
// - Show the SPoI (Sensor Point of Interest) crosshair.
//
// The event references the drone's b-i-v video endpoint via the drone UUID,
// so armatak_fnc_send_uas_video_cot must also be called for the same drone.
//
// Exits silently when "armatak_attribute_video_url" is not set, which keeps
// the behavior identical to the old fn_send_drone_cot for drones without a
// configured video stream.
//
// Arguments:
// 0: _drone <OBJECT> The drone object.
//
// Return value: none
params ["_drone"];
private _video_url = _drone getVariable ["armatak_attribute_video_url", ""];
if (_video_url == "") exitWith {};
private _uuid = _drone call armatak_fnc_extract_uuid;
private _sensor_uid = _uuid + "-sensor";
private _callsign = [_drone] call armatak_fnc_extract_marker_callsign;
private _pos = (getPos _drone) call armatak_client_fnc_convertClientLocation;
private _lat = _pos select 0;
private _lon = _pos select 1;
private _hae = _pos select 2;
private _azimuth = parseNumber ((getDir _drone) toFixed 0);
private _allTurrets = [_drone, false] call BIS_fnc_allTurrets;
if (count _allTurrets > 0) then {
private _firstTurretPath = _allTurrets select 0;
private _turretWeapons = _drone weaponsTurret _firstTurretPath;
if (_turretWeapons isNotEqualTo []) then {
private _tDir = _drone weaponDirection (_turretWeapons select 0);
if (!((_tDir select 0) == 0 && (_tDir select 1) == 0)) then {
_azimuth = round (((_tDir select 0) atan2 (_tDir select 1) + 360) mod 360);
};
};
};
private _fov = _drone getVariable ["armatak_uas_fov", 60];
private _range = round (((getPosATL _drone) select 2) max 1);
private _payload = [_sensor_uid, _uuid, _callsign, _lat, _lon, _hae, _azimuth, _fov, _range];
"armatak" callExtension ["tcp_socket:cot:uas_sensor", [_payload]];

View File

@@ -0,0 +1,30 @@
// function name: armatak_fnc_send_uas_video_cot
// function author: Valmo / ArmaTAK contributors
// function description:
// Sends a b-i-v CoT event that declares the RTSP video endpoint for a drone.
// The ATAK UAS Tool picks this up and shows the drone in its UAS list with
// the associated video feed available for playback.
//
// The drone entity MUST have the variable "armatak_attribute_video_url" set
// to a valid RTSP URL, e.g.:
// _drone setVariable ["armatak_attribute_video_url", "rtsp://192.168.1.10:8554/live/drone1"];
// or via the 3DEN attribute "Video URL (RTSP)" in the ARMA Team Awareness Kit
// attribute category.
//
// If the variable is absent or empty the function exits silently.
//
// Arguments:
// 0: _drone <OBJECT> The drone object.
//
// Return value: none
params ["_drone"];
private _video_url = _drone getVariable ["armatak_attribute_video_url", ""];
if (_video_url == "") exitWith {};
private _uuid = _drone call armatak_fnc_extract_uuid;
private _callsign = [_drone] call armatak_fnc_extract_marker_callsign;
private _payload = [_uuid, _callsign, _video_url];
"armatak" callExtension ["tcp_socket:cot:uas_video", [_payload]];

View File

@@ -0,0 +1,13 @@
// function name: armatak_fnc_extract_marker_video_url
// function author: Codex
// function description: Gets the marker video URL configured in 3DEN for a vehicle
params ["_unit"];
private _videoUrl = _unit getVariable ["armatak_attribute_marker_video_url", ""];
if (isNil "_videoUrl") exitWith {
""
};
_videoUrl

View File

@@ -7,8 +7,15 @@ params["_unit"];
private _affiliation = "f";
private _type = "G";
private _role = "a-f-G-U-C-I";
switch (str side _unit) do {
private _side = side _unit;
if (isNil {
_unit getVariable "armatak_current_side"
}) then {
_side = _unit getVariable "armatak_current_side";
};
switch (str _side) do {
case "WEST": {
_affiliation = "f";
};
@@ -22,7 +29,7 @@ switch (str side _unit) do {
_affiliation = "u";
};
default {
_affiliation = "f";
_affiliation = "u";
};
};
@@ -110,7 +117,6 @@ if ((typeOf (vehicle _unit) != typeOf _unit) or ((_unit_type select 0) == "Vehic
_role = "a-" + _affiliation + "-" + _type;
armatak_attribute_marker_type = _unit getVariable "armatak_attribute_marker_type";
if (!isNil "armatak_attribute_marker_type" or armatak_attribute_marker_type != '') then {

View File

@@ -0,0 +1,27 @@
params["_unit"];
_target = getSensorTargets (_unit);
{
_unit = _x select 0;
_position = _x select 1;
_status = _x select 2;
if (isNil {
_unit getVariable "armatak_current_side"
}) then {
_unit setVariable ["armatak_current_side", side _unit];
};
if (_status != "destroyed" && !(_unit in armatak_server_syncedUnits)) then {
_unit_position = _unit call armatak_client_fnc_extractClientPosition;
_uuid = _unit call armatak_fnc_extract_uuid;
_type = _unit call armatak_fnc_extract_role;
_callsign = getText (configOf _unit >> "displayName");
_marker_cot = [_uuid, _type, _unit_position select 1, _unit_position select 2, _unit_position select 3, _callsign, _unit_position select 5, _unit_position select 6];
"armatak" callExtension ["tcp_socket:cot:marker", [_marker_cot]];
};
} forEach _target;

View File

@@ -16,6 +16,10 @@ if (roleDescription _unit != "") then {
};
};
if (side _unit == east) then {
_callsign = getText (configOf _unit >> "displayName");
};
armatak_attribute_unit_callsign = _unit getVariable "armatak_attribute_unit_callsign";
if (!isNil "armatak_attribute_unit_callsign" or armatak_attribute_unit_callsign != '') then {

View File

@@ -0,0 +1,23 @@
params["_latitude", "_longitude", "_altitude"];
_playerPosition = [_latitude, _longitude, _altitude];
_playerLatitude = _playerPosition select 0;
_playerLongitude = _playerPosition select 1;
_playerMaxLatitude = 20480;
_playerMaxLongitude = 20480;
_MapMaxLongitude = 33.728772;
_MapMinLongitude = 33.542815;
_MapMaxLatitude = 63.169746;
_MapMinLatitude = 62.938820;
_LongitudeDifference = _MapMaxLongitude - _MapMinLongitude;
_LatitudeDifference = _MapMaxLatitude - _MapMinLatitude;
_RealLongitude = (_playerLongitude / _playerMaxLongitude) * _LongitudeDifference + _MapMinLongitude;
_RealLatitude = (_playerLatitude / _playerMaxLatitude) * _LatitudeDifference + _MapMinLatitude;
[_RealLongitude, _RealLatitude, _playerPosition select 2]

View File

@@ -0,0 +1,23 @@
params["_latitude", "_longitude", "_altitude"];
_playerPosition = [_latitude, _longitude, _altitude];
_playerLatitude = _playerPosition select 0;
_playerLongitude = _playerPosition select 1;
_playerMaxLatitude = 20480;
_playerMaxLongitude = 20480;
_MapMaxLatitude = -67.765153;
_MapMinLatitude = -68.223664;
_MapMaxLongitude = 10.593815;
_MapMinLongitude = 10.137466;
_LongitudeDifference = _MapMaxLongitude - _MapMinLongitude;
_LatitudeDifference = _MapMaxLatitude - _MapMinLatitude;
_RealLongitude = (_playerLongitude / _playerMaxLongitude) * _LongitudeDifference + _MapMinLongitude;
_RealLatitude = (_playerLatitude / _playerMaxLatitude) * _LatitudeDifference + _MapMinLatitude;
[_RealLongitude, _RealLatitude, _playerPosition select 2]

View File

@@ -0,0 +1,30 @@
params ["_longitudeInGame", "_latitudeInGame", "_altitude"];
private _mapWidth = 8192;
private _mapHeight = 8192;
// SW corner (used as origin)
private _SW_lat = 63.005389;
private _SW_lon = 22.638957;
// SE corner
private _SE_lat = 63.010092;
private _SE_lon = 22.800107;
// NW corner
private _NW_lat = 63.078713;
private _NW_lon = 22.628542;
private _edgeSE_lat = _SE_lat - _SW_lat;
private _edgeSE_lon = _SE_lon - _SW_lon;
private _edgeNW_lat = _NW_lat - _SW_lat;
private _edgeNW_lon = _NW_lon - _SW_lon;
private _fx = _longitudeInGame / _mapWidth;
private _fy = _latitudeInGame / _mapHeight;
private _realLat = _SW_lat + (_fx * _edgeSE_lat) + (_fy * _edgeNW_lat);
private _realLon = _SW_lon + (_fx * _edgeSE_lon) + (_fy * _edgeNW_lon);
[_realLat, _realLon, _altitude]

View File

@@ -0,0 +1,23 @@
params["_latitude", "_longitude", "_altitude"];
_playerPosition = [_latitude, _longitude, _altitude];
_playerLatitude = _playerPosition select 0;
_playerLongitude = _playerPosition select 1;
_playerMaxLongitude = 4992;
_playerMaxLatitude = 4992;
_MapMaxLongitude = -99.722665;
_MapMinLongitude = -99.775505;
_MapMaxLatitude = 32.159272;
_MapMinLatitude = 32.114011;
_LongitudeDifference = _MapMaxLongitude - _MapMinLongitude;
_LatitudeDifference = _MapMaxLatitude - _MapMinLatitude;
_RealLongitude = (_playerLongitude / _playerMaxLongitude) * _LongitudeDifference + _MapMinLongitude;
_RealLatitude = (_playerLatitude / _playerMaxLatitude) * _LatitudeDifference + _MapMinLatitude;
[_RealLongitude, _RealLatitude, _playerPosition select 2]

View File

@@ -0,0 +1,30 @@
params ["_longitudeInGame", "_latitudeInGame", "_altitude"];
private _mapWidth = 11264;
private _mapHeight = 11264;
// SW corner (used as origin)
private _SW_lat = -51.736078;
private _SW_lon = -57.915032;
// SE corner
private _SE_lat = -51.736078;
private _SE_lon = -58.077879;
// NW corner
private _NW_lat = -51.634750;
private _NW_lon = -58.077879;
private _edgeSE_lat = _SE_lat - _SW_lat;
private _edgeSE_lon = _SE_lon - _SW_lon;
private _edgeNW_lat = _NW_lat - _SW_lat;
private _edgeNW_lon = _NW_lon - _SW_lon;
private _fx = _longitudeInGame / _mapWidth;
private _fy = _latitudeInGame / _mapHeight;
private _realLat = _SW_lat + (_fx * _edgeSE_lat) + (_fy * _edgeNW_lat);
private _realLon = _SW_lon + (_fx * _edgeSE_lon) + (_fy * _edgeNW_lon);
[_realLat, _realLon, _altitude]

View File

@@ -0,0 +1,30 @@
params ["_longitudeInGame", "_latitudeInGame", "_altitude"];
private _mapWidth = 16384;
private _mapHeight = 16384;
// SW corner (used as origin)
private _SW_lat = -51.806546;
private _SW_lon = -57.939747;
// SE corner
private _SE_lat = -51.806546;
private _SE_lon = -57.701978;
// NW corner
private _NW_lat = -51.658913;
private _NW_lon = -57.939747;
private _edgeSE_lat = _SE_lat - _SW_lat;
private _edgeSE_lon = _SE_lon - _SW_lon;
private _edgeNW_lat = _NW_lat - _SW_lat;
private _edgeNW_lon = _NW_lon - _SW_lon;
private _fx = _longitudeInGame / _mapWidth;
private _fy = _latitudeInGame / _mapHeight;
private _realLat = _SW_lat + (_fx * _edgeSE_lat) + (_fy * _edgeNW_lat);
private _realLon = _SW_lon + (_fx * _edgeSE_lon) + (_fy * _edgeNW_lon);
[_realLat, _realLon, _altitude]

View File

@@ -0,0 +1,30 @@
params ["_longitudeInGame", "_latitudeInGame", "_altitude"];
private _mapWidth = 10240;
private _mapHeight = 10240;
// SW corner (used as origin)
private _SW_lat = -51.863358;
private _SW_lon = -59.054585;
// SE corner
private _SE_lat = -51.863358;
private _SE_lon = -58.906155;
// NW corner
private _NW_lat = -51.771493;
private _NW_lon = -59.054585;
private _edgeSE_lat = _SE_lat - _SW_lat;
private _edgeSE_lon = _SE_lon - _SW_lon;
private _edgeNW_lat = _NW_lat - _SW_lat;
private _edgeNW_lon = _NW_lon - _SW_lon;
private _fx = _longitudeInGame / _mapWidth;
private _fy = _latitudeInGame / _mapHeight;
private _realLat = _SW_lat + (_fx * _edgeSE_lat) + (_fy * _edgeNW_lat);
private _realLon = _SW_lon + (_fx * _edgeSE_lon) + (_fy * _edgeNW_lon);
[_realLat, _realLon, _altitude]

View File

@@ -0,0 +1,23 @@
params["_latitude", "_longitude", "_altitude"];
_playerPosition = [_latitude, _longitude, _altitude];
_playerLatitude = _playerPosition select 0;
_playerLongitude = _playerPosition select 1;
_playerMaxLongitude = 5120;
_playerMaxLatitude = 5120;
_MapMaxLatitude = -51.619725;
_MapMinLatitude = -51.664223;
_MapMaxLongitude = -58.394630;
_MapMinLongitude = -58.469580;
_LongitudeDifference = _MapMaxLongitude - _MapMinLongitude;
_LatitudeDifference = _MapMaxLatitude - _MapMinLatitude;
_RealLongitude = (_playerLongitude / _playerMaxLongitude) * _LongitudeDifference + _MapMinLongitude;
_RealLatitude = (_playerLatitude / _playerMaxLatitude) * _LatitudeDifference + _MapMinLatitude;
[_RealLongitude, _RealLatitude, _playerPosition select 2]

View File

@@ -0,0 +1,23 @@
params["_latitude", "_longitude", "_altitude"];
_playerPosition = [_latitude, _longitude, _altitude];
_playerLatitude = _playerPosition select 0;
_playerLongitude = _playerPosition select 1;
_playerMaxLatitude = 32768;
_playerMaxLongitude = 32768;
_MapMaxLongitude = 35.285485;
_MapMinLongitude = 34.927617;
_MapMaxLatitude = 70.445404;
_MapMinLatitude = 70.016783;
_LongitudeDifference = _MapMaxLongitude - _MapMinLongitude;
_LatitudeDifference = _MapMaxLatitude - _MapMinLatitude;
_RealLongitude = (_playerLongitude / _playerMaxLongitude) * _LongitudeDifference + _MapMinLongitude;
_RealLatitude = (_playerLatitude / _playerMaxLatitude) * _LatitudeDifference + _MapMinLatitude;
[_RealLongitude, _RealLatitude, _playerPosition select 2]

View File

@@ -1,42 +1,28 @@
class CfgVehicles {
class Logic;
class Module_F : Logic
{
class AttributesBase
{
class Default;
class Logic;
class Module_F : Logic {
class AttributesBase {
class Edit;
class Combo;
class Checkbox;
class CheckboxNumber;
class ModuleDescription;
class Units;
};
class ModuleDescription
{
class AnyBrain;
};
class ModuleDescription;
};
class GVAR(moduleBase): Module_F {
author = PROJECT_AUTHOR;
category = QEGVAR(main,moduleCategory);
function = QUOTE({});
functionPriority = 1;
isGlobal = 1;
isTriggerActivated = 0;
scope = 1;
scopeCurator = 2;
};
author = PROJECT_AUTHOR;
category = QEGVAR(main,moduleCategory);
function = QUOTE({});
functionPriority = 1;
isGlobal = 1;
isTriggerActivated = 0;
scope = 1;
scopeCurator = 2;
};
class GVAR(coreModule): GVAR(moduleBase) {
scope = 2;
class GVAR(connectionModuleBase): GVAR(moduleBase) {
scopeCurator = 0;
displayname = "CoT Router";
icon = "\a3\Modules_F_Curator\Data\iconRadio_ca.paa";
category = QEGVAR(main,moduleCategory);
function = QFUNC(3denCoreModuleConfig);
functionPriority = 1;
isGlobal = 0;
isTriggerActivated = 1;
@@ -47,19 +33,25 @@ class CfgVehicles {
canSetArea = 0;
canSetAreaShape = 0;
canSetAreaHeight = 0;
};
class GVAR(tcpModule): GVAR(connectionModuleBase) {
scope = 2;
displayName = "CoT Router (TCP)";
function = QFUNC(3denTcpModuleConfig);
class Attributes: AttributesBase {
class GVAR(moduleInstanceAddress): Edit {
property = QGVAR(moduleInstanceAddress);
displayname = "TAK Server Address";
tooltip = "TAK Server Instance Address";
displayName = "TAK Server Address";
tooltip = "Hostname or IP address for the TAK or IronTAK server.";
typeName = "STRING";
defaultValue = "localhost";
defaultValue = "'localhost'";
};
class GVAR(moduleInstancePort): Edit {
property = QGVAR(moduleInstancePort);
displayname = "TAK Server TCP Port";
tooltip = "TAK Server instance Port for TCP connection";
displayName = "TAK Server TCP Port";
tooltip = "Port for the unauthenticated TCP socket.";
typeName = "NUMBER";
defaultValue = "8088";
};
@@ -67,24 +59,75 @@ class CfgVehicles {
};
class ModuleDescription: ModuleDescription {
description = "Generate the initial ARMATAK configuration, syncronizing all players to the TAK server instance";
description = "Connect ArmaTAK to a TAK server over plain TCP.";
sync[] = {"LocationArea_F"};
};
};
class GVAR(coreModuleCurator): GVAR(coreModule) {
class GVAR(enrollModule): GVAR(connectionModuleBase) {
scope = 2;
displayName = "CoT Router (Authenticated)";
function = QFUNC(3denEnrollModuleConfig);
class Attributes: AttributesBase {
class GVAR(moduleInstanceAddress): Edit {
property = QGVAR(moduleInstanceAddress);
displayname = "TAK Server Address";
tooltip = "Hostname or IP address used for enrollment and the final TLS connection.";
typeName = "STRING";
defaultValue = "'localhost'";
};
class GVAR(moduleEnrollmentPort): Edit {
property = QGVAR(moduleEnrollmentPort);
displayName = "Enrollment HTTPS Port";
tooltip = "Port used for GET /Marti/api/tls/config and POST /Marti/api/tls/signClient/v2.";
typeName = "NUMBER";
defaultValue = "8446";
};
class GVAR(moduleEnrollmentUsername): Edit {
property = QGVAR(moduleEnrollmentUsername);
displayName = "Enrollment Username";
tooltip = "Username used in Basic Auth for client certificate enrollment.";
typeName = "STRING";
defaultValue = "''";
};
class GVAR(moduleEnrollmentPassword): Edit {
property = QGVAR(moduleEnrollmentPassword);
displayName = "Enrollment Password";
tooltip = "Password used in Basic Auth for client certificate enrollment.";
typeName = "STRING";
defaultValue = "''";
};
class ModuleDescription: ModuleDescription {};
};
class ModuleDescription: ModuleDescription {
description = "Enroll a client certificate and connect ArmaTAK over mTLS.";
sync[] = {"LocationArea_F"};
};
};
class GVAR(tcpModuleCurator): GVAR(tcpModule) {
scope = 1;
scopeCurator = 2;
function = "";
displayName = "CoT Router (Zeus)";
curatorInfoType = "armatak_zeus_core_module_dialog";
displayName = "CoT Router (TCP, Zeus)";
curatorInfoType = "armatak_zeus_tcp_module_dialog";
};
class GVAR(enrollModuleCurator): GVAR(enrollModule) {
scope = 1;
scopeCurator = 2;
function = "";
displayName = "CoT Router (Authenticated, Zeus)";
curatorInfoType = "armatak_zeus_enroll_module_dialog";
};
class GVAR(markEntity): GVAR(moduleBase) {
curatorCanAttach = 1;
category = QEGVAR(main,moduleCategory);
curatorCanAttach = 1;
category = QEGVAR(main,moduleCategory);
displayname = "Mark Entity";
function = QFUNC(routerEntityAdd);
icon = "\a3\Modules_F_Curator\Data\iconRadio_ca.paa";
};
function = QFUNC(routerEntityAdd);
icon = "\a3\Modules_F_Curator\Data\iconRadio_ca.paa";
};
};

View File

@@ -1,4 +1,7 @@
PREP(3denCoreModuleConfig);
PREP(3denEnrollModuleConfig);
PREP(3denTcpModuleConfig);
PREP(routerEntityAdd);
PREP(routerEntityRemove);
PREP(ZeusCoreModuleConfig);
PREP(startCotRouter);
PREP(ZeusEnrollModuleConfig);
PREP(ZeusTcpModuleConfig);

View File

@@ -4,8 +4,10 @@ class CfgPatches {
class ADDON {
name = COMPONENT_NAME;
units[] = {
QGVAR(coreModule),
QGVAR(coreModuleCurator),
QGVAR(tcpModule),
QGVAR(tcpModuleCurator),
QGVAR(enrollModule),
QGVAR(enrollModuleCurator),
QGVAR(markEntity)
};
weapons[] = {};

View File

@@ -3,69 +3,172 @@ class RscBackground;
class RscButton;
class RscEdit;
class armatak_zeus_core_module_dialog {
class armatak_zeus_tcp_module_dialog {
idd = 999991;
movingEnable = 0;
class ControlsBackground {
class armatak_gui_module_zeus_core_dialog_main_frame: RscBackground {
class main_frame: RscBackground {
idc = 1800;
x = "0.386562 * safezoneW + safezoneX";
y = "0.401 * safezoneH + safezoneY";
y = "0.29 * safezoneH + safezoneY";
w = "0.216563 * safezoneW";
h = "0.242 * safezoneH";
colorBackground[]={0,0,0,0.45};
h = "0.32 * safezoneH";
colorBackground[] = {0,0,0,0.45};
};
};
class Controls {
class armatak_gui_module_zeus_core_dialog_address_edit: RscEdit {
idc = 14000;
text = "localhost";
x = "0.391719 * safezoneW + safezoneX";
y = "0.445 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.044 * safezoneH";
colorBackground[]={0,0,0,0.5};
};
class armatak_gui_module_zeus_core_dialog_address_port_edit: RscEdit {
idc = 14001;
text = "8088";
x = "0.391719 * safezoneW + safezoneX";
y = "0.522 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.044 * safezoneH";
colorBackground[]={0,0,0,0.5};
};
class armatak_gui_module_zeus_core_dialog_address_text: RscText {
class address_text: RscText {
idc = 1000;
text = "TAK Server Address";
x = "0.391719 * safezoneW + safezoneX";
y = "0.412 * safezoneH + safezoneY";
y = "0.332 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.033 * safezoneH";
};
class armatak_gui_module_zeus_core_dialog_address_port_text: RscText {
class address_edit: RscEdit {
idc = 14000;
text = "localhost";
x = "0.391719 * safezoneW + safezoneX";
y = "0.365 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.044 * safezoneH";
colorBackground[] = {0,0,0,0.5};
};
class port_text: RscText {
idc = 1001;
text = "TAK Server Port";
x = "0.391719 * safezoneW + safezoneX";
y = "0.489 * safezoneH + safezoneY";
y = "0.425 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.033 * safezoneH";
};
class armatak_gui_module_zeus_core_dialog_address_button_cancel: RscButton {
class port_edit: RscEdit {
idc = 14001;
text = "8088";
x = "0.391719 * safezoneW + safezoneX";
y = "0.458 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.044 * safezoneH";
colorBackground[] = {0,0,0,0.5};
};
class button_cancel: RscButton {
idc = 1601;
text = "Cancel";
action = "closeDialog 2;";
x = "0.551563 * safezoneW + safezoneX";
y = "0.577 * safezoneH + safezoneY";
y = "0.535 * safezoneH + safezoneY";
w = "0.0464063 * safezoneW";
h = "0.055 * safezoneH";
};
class armatak_gui_module_zeus_core_dialog_address_button_ok: RscButton {
class button_ok: RscButton {
idc = 1600;
text = "Ok";
action = QUOTE(call FUNC(zeusCoreModuleConfig));
action = QUOTE(call FUNC(ZeusTcpModuleConfig));
x = "0.5 * safezoneW + safezoneX";
y = "0.577 * safezoneH + safezoneY";
y = "0.535 * safezoneH + safezoneY";
w = "0.0464063 * safezoneW";
h = "0.055 * safezoneH";
};
};
};
class armatak_zeus_enroll_module_dialog {
idd = 999992;
movingEnable = 0;
class ControlsBackground {
class main_frame: RscBackground {
idc = 1810;
x = "0.386562 * safezoneW + safezoneX";
y = "0.2 * safezoneH + safezoneY";
w = "0.216563 * safezoneW";
h = "0.52 * safezoneH";
colorBackground[] = {0,0,0,0.45};
};
};
class Controls {
class address_text: RscText {
idc = 1010;
text = "TAK Server Address";
x = "0.391719 * safezoneW + safezoneX";
y = "0.242 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.033 * safezoneH";
};
class address_edit: RscEdit {
idc = 14100;
text = "localhost";
x = "0.391719 * safezoneW + safezoneX";
y = "0.275 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.044 * safezoneH";
colorBackground[] = {0,0,0,0.5};
};
class enroll_port_text: RscText {
idc = 1011;
text = "Enrollment HTTPS Port";
x = "0.391719 * safezoneW + safezoneX";
y = "0.335 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.033 * safezoneH";
};
class enroll_port_edit: RscEdit {
idc = 14101;
text = "8446";
x = "0.391719 * safezoneW + safezoneX";
y = "0.368 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.044 * safezoneH";
colorBackground[] = {0,0,0,0.5};
};
class username_text: RscText {
idc = 1012;
text = "Enrollment Username";
x = "0.391719 * safezoneW + safezoneX";
y = "0.428 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.033 * safezoneH";
};
class username_edit: RscEdit {
idc = 14102;
text = "";
x = "0.391719 * safezoneW + safezoneX";
y = "0.461 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.044 * safezoneH";
colorBackground[] = {0,0,0,0.5};
};
class password_text: RscText {
idc = 1013;
text = "Enrollment Password";
x = "0.391719 * safezoneW + safezoneX";
y = "0.521 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.033 * safezoneH";
};
class password_edit: RscEdit {
idc = 14103;
text = "";
x = "0.391719 * safezoneW + safezoneX";
y = "0.554 * safezoneH + safezoneY";
w = "0.20625 * safezoneW";
h = "0.044 * safezoneH";
colorBackground[] = {0,0,0,0.5};
};
class button_cancel: RscButton {
idc = 1611;
text = "Cancel";
action = "closeDialog 2;";
x = "0.551563 * safezoneW + safezoneX";
y = "0.645 * safezoneH + safezoneY";
w = "0.0464063 * safezoneW";
h = "0.055 * safezoneH";
};
class button_ok: RscButton {
idc = 1610;
text = "Ok";
action = QUOTE(call FUNC(ZeusEnrollModuleConfig));
x = "0.5 * safezoneW + safezoneX";
y = "0.645 * safezoneH + safezoneY";
w = "0.0464063 * safezoneW";
h = "0.055 * safezoneH";
};

View File

@@ -1,60 +0,0 @@
#include "..\script_component.hpp"
params [
["_logic", objNull, [objNull]],
["_units", [], [[]]],
["_activated", true, [true]]
];
if (isServer) exitWith {
["Connecting to TCP Socket", "success", "TCP Socket"] call EFUNC(main,notify);
_tak_server_instance_address = _logic getVariable QGVAR(moduleInstanceAddress);
_tak_server_instance_port = _logic getVariable QGVAR(moduleInstancePort);
_tak_server_fulladdress = _tak_server_instance_address + ":" + (str _tak_server_instance_port);
missionNamespace setVariable ["armatak_server_instance", _tak_server_fulladdress];
missionNamespace setVariable ["armatak_tcp_socket_is_running", true];
"armatak" callExtension ["tcp_socket:start", [_tak_server_fulladdress]];
_syncUnits = synchronizedObjects _logic;
missionNamespace setVariable ["armatak_server_syncedUnits", _syncUnits];
GVAR(syncedUnits) = missionNamespace getVariable "armatak_server_syncedUnits";
[{
GVAR(syncedUnits) = missionNamespace getVariable "armatak_server_syncedUnits";
{
_objectType = _x call BIS_fnc_objectType;
switch (true) do {
case ((_objectType select 0) == "Soldier"): {
_callsign = [_x] call armatak_fnc_extract_unit_callsign;
_group_name = [group _x] call armatak_fnc_extract_group_color;
_group_role = [_x] call armatak_fnc_extract_group_role;
[_x, _callsign, _group_name, _group_role] call armatak_fnc_send_eud_cot;
[_x] call armatak_fnc_send_digital_pointer_cot;
};
case ((_objectType select 0) == "Vehicle"): {
_atak_type = [_x] call armatak_fnc_extract_role;
_callsign = [_x] call armatak_fnc_extract_marker_callsign;
[_x, _atak_type, _callsign] call armatak_fnc_send_marker_cot;
};
case ((_objectType select 0) == "VehicleAutonomous"): {
_atak_type = [_x] call armatak_fnc_extract_role;
_callsign = [_x] call armatak_fnc_extract_marker_callsign;
[_x, _atak_type, _callsign] call armatak_fnc_send_drone_cot;
[_x] call armatak_fnc_send_digital_pointer_cot;
};
};
} forEach GVAR(syncedUnits);
}, 1, []] call CBA_fnc_addPerFrameHandler;
};
true;

View File

@@ -0,0 +1,37 @@
#include "..\script_component.hpp"
params [
["_logic", objNull, [objNull]],
["_units", [], [[]]],
["_activated", true, [true]]
];
if (isServer) exitWith {
if (missionNamespace getVariable ["armatak_tcp_socket_is_running", false]) exitWith {
["Socket was called twice", "error", "TCP Socket"] call EFUNC(main,notify);
};
["Connecting to authenticated TAK socket", "success", "TCP Socket"] call EFUNC(main,notify);
_tak_server_instance_address = _logic getVariable [QGVAR(moduleInstanceAddress), "localhost"];
_tak_server_enrollment_port = _logic getVariable [QGVAR(moduleEnrollmentPort), 8446];
_tak_server_enrollment_username = _logic getVariable [QGVAR(moduleEnrollmentUsername), ""];
_tak_server_enrollment_password = _logic getVariable [QGVAR(moduleEnrollmentPassword), ""];
"armatak" callExtension [
"tcp_socket:start_enroll_mtls",
[
_tak_server_instance_address,
_tak_server_instance_address,
str _tak_server_enrollment_port,
_tak_server_enrollment_username,
_tak_server_enrollment_password,
""
]
];
missionNamespace setVariable ["armatak_server_syncedUnits", synchronizedObjects _logic];
_tak_server_instance_address call FUNC(startCotRouter);
};
true

View File

@@ -0,0 +1,26 @@
#include "..\script_component.hpp"
params [
["_logic", objNull, [objNull]],
["_units", [], [[]]],
["_activated", true, [true]]
];
if (isServer) exitWith {
if (missionNamespace getVariable ["armatak_tcp_socket_is_running", false]) exitWith {
["Socket was called twice", "error", "TCP Socket"] call EFUNC(main,notify);
};
["Connecting to TCP Socket", "success", "TCP Socket"] call EFUNC(main,notify);
_tak_server_instance_address = _logic getVariable [QGVAR(moduleInstanceAddress), "localhost"];
_tak_server_instance_port = _logic getVariable [QGVAR(moduleInstancePort), 8088];
_tak_server_fulladdress = _tak_server_instance_address + ":" + (str _tak_server_instance_port);
"armatak" callExtension ["tcp_socket:start", [_tak_server_fulladdress]];
missionNamespace setVariable ["armatak_server_syncedUnits", synchronizedObjects _logic];
_tak_server_fulladdress call FUNC(startCotRouter);
};
true

View File

@@ -1,63 +0,0 @@
#include "..\script_component.hpp"
params ["_logic"];
_socket_is_running = missionNamespace getVariable ["armatak_tcp_socket_is_running", false];
if (_socket_is_running) exitWith {
["Socket was called twice", "error", "TCP Socket"] call EFUNC(main,notify);
closeDialog 1;
};
disableSerialization;
["Connecting to TCP Socket", "success", "TCP Socket"] call EFUNC(main,notify);
_tak_server_instance_address = ctrlText 14000;
_tak_server_instance_port = ctrlText 14001;
_tak_server_fulladdress = ((_tak_server_instance_address) + ":" + (_tak_server_instance_port));
missionNamespace setVariable ["armatak_server_instance", _tak_server_fulladdress];
missionNamespace setVariable ["armatak_tcp_socket_is_running", true];
"armatak" callExtension ["tcp_socket:start", [_tak_server_fulladdress]];
_syncUnits = [];
missionNamespace setVariable ["armatak_server_syncedUnits", _syncUnits];
GVAR(syncedUnits) = missionNamespace getVariable "armatak_server_syncedUnits";
[{
GVAR(syncedUnits) = missionNamespace getVariable "armatak_server_syncedUnits";
{
_objectType = _x call BIS_fnc_objectType;
switch (true) do {
case ((_objectType select 0) == "Soldier"): {
_callsign = [_x] call armatak_fnc_extract_unit_callsign;
_group_name = [group _x] call armatak_fnc_extract_group_color;
_group_role = [_x] call armatak_fnc_extract_group_role;
[_x, _callsign, _group_name, _group_role] call armatak_fnc_send_eud_cot;
[_x] call armatak_fnc_send_digital_pointer_cot;
};
case ((_objectType select 0) == "Vehicle"): {
_atak_type = [_x] call armatak_fnc_extract_role;
_callsign = [_x] call armatak_fnc_extract_marker_callsign;
[_x, _atak_type, _callsign] call armatak_fnc_send_marker_cot;
};
case ((_objectType select 0) == "VehicleAutonomous"): {
_atak_type = [_x] call armatak_fnc_extract_role;
_callsign = [_x] call armatak_fnc_extract_marker_callsign;
[_x, _atak_type, _callsign] call armatak_fnc_send_drone_cot;
[_x] call armatak_fnc_send_digital_pointer_cot;
};
};
} forEach GVAR(syncedUnits);
}, 1, []] call CBA_fnc_addPerFrameHandler;
deleteVehicle _logic;
closeDialog 1;

View File

@@ -0,0 +1,33 @@
#include "..\script_component.hpp"
params ["_logic"];
if (missionNamespace getVariable ["armatak_tcp_socket_is_running", false]) exitWith {
["Socket was called twice", "error", "TCP Socket"] call EFUNC(main,notify);
closeDialog 1;
};
disableSerialization;
["Connecting to authenticated TAK socket", "success", "TCP Socket"] call EFUNC(main,notify);
_tak_server_instance_address = ctrlText 14100;
_tak_server_enrollment_port = ctrlText 14101;
_tak_server_enrollment_username = ctrlText 14102;
_tak_server_enrollment_password = ctrlText 14103;
"armatak" callExtension [
"tcp_socket:start_enroll_mtls",
[
_tak_server_instance_address,
_tak_server_instance_address,
_tak_server_enrollment_port,
_tak_server_enrollment_username,
_tak_server_enrollment_password,
""
]
];
_tak_server_instance_address call FUNC(startCotRouter);
deleteVehicle _logic;
closeDialog 1;

View File

@@ -0,0 +1,22 @@
#include "..\script_component.hpp"
params ["_logic"];
if (missionNamespace getVariable ["armatak_tcp_socket_is_running", false]) exitWith {
["Socket was called twice", "error", "TCP Socket"] call EFUNC(main,notify);
closeDialog 1;
};
disableSerialization;
["Connecting to TCP Socket", "success", "TCP Socket"] call EFUNC(main,notify);
_tak_server_instance_address = ctrlText 14000;
_tak_server_instance_port = ctrlText 14001;
_tak_server_fulladdress = _tak_server_instance_address + ":" + _tak_server_instance_port;
"armatak" callExtension ["tcp_socket:start", [_tak_server_fulladdress]];
_tak_server_fulladdress call FUNC(startCotRouter);
deleteVehicle _logic;
closeDialog 1;

View File

@@ -34,7 +34,7 @@ switch (false) do {
};
} forEach GVAR(syncedUnits);
missionNmaespace setVariable ["armatak_server_syncedUnits", GVAR(syncedUnits)];
missionNamespace setVariable ["armatak_server_syncedUnits", GVAR(syncedUnits)];
SETVAR(_unit,GVAR(isRouting),false);
deleteVehicle _logic;

View File

@@ -0,0 +1,47 @@
#include "..\script_component.hpp"
params [["_server_instance", "", [""]]];
missionNamespace setVariable ["armatak_server_instance", _server_instance];
missionNamespace setVariable ["armatak_tcp_socket_is_running", true];
if (isNil { missionNamespace getVariable "armatak_server_syncedUnits" }) then {
missionNamespace setVariable ["armatak_server_syncedUnits", []];
};
GVAR(syncedUnits) = missionNamespace getVariable "armatak_server_syncedUnits";
[{
GVAR(syncedUnits) = missionNamespace getVariable "armatak_server_syncedUnits";
{
_objectType = _x call BIS_fnc_objectType;
switch (true) do {
case ((_objectType select 0) == "Soldier"): {
_callsign = [_x] call armatak_fnc_extract_unit_callsign;
_group_name = [group _x] call armatak_fnc_extract_group_color;
_group_role = [_x] call armatak_fnc_extract_group_role;
[_x, _callsign, _group_name, _group_role] call armatak_fnc_send_eud_cot;
[_x] call armatak_fnc_send_digital_pointer_cot;
};
case ((_objectType select 0) == "Vehicle"): {
_atak_type = [_x] call armatak_fnc_extract_role;
_callsign = [_x] call armatak_fnc_extract_marker_callsign;
[_x, _atak_type, _callsign] call armatak_fnc_send_marker_cot;
_x call armatak_fnc_extract_sensor_data;
};
case ((_objectType select 0) == "VehicleAutonomous"): {
_atak_type = [_x] call armatak_fnc_extract_role;
_callsign = [_x] call armatak_fnc_extract_marker_callsign;
[_x, _atak_type, _callsign] call armatak_fnc_send_drone_cot;
[_x] call armatak_fnc_send_digital_pointer_cot;
_x call armatak_fnc_extract_sensor_data;
};
};
} forEach GVAR(syncedUnits);
}, 1, []] call CBA_fnc_addPerFrameHandler;
true

View File

@@ -1 +0,0 @@
armatak\armatak\addons\video

View File

@@ -1,11 +0,0 @@
class Extended_PreStart_EventHandlers {
class ADDON {
init = QUOTE(call COMPILE_SCRIPT(XEH_preStart));
};
};
class Extended_PreInit_EventHandlers {
class ADDON {
init = QUOTE(call COMPILE_SCRIPT(XEH_preInit));
};
};

View File

@@ -1,73 +0,0 @@
class CfgVehicles {
class Logic;
class Module_F : Logic
{
class AttributesBase
{
class Default;
class Edit;
class Combo;
class Checkbox;
class CheckboxNumber;
class ModuleDescription;
class Units;
};
class ModuleDescription
{
class AnyBrain;
};
};
class EGVAR(server,moduleBase);
class GVAR(videoModule): EGVAR(server,moduleBase) {
scope = 2;
scopeCurator = 0;
displayname = "Video Streaming Handler";
icon = "\a3\Modules_F_Curator\Data\iconRadio_ca.paa";
category = QEGVAR(main,moduleCategory);
function = QFUNC(videoParser);
functionPriority = 1;
isGlobal = 0;
isTriggerActivated = 1;
isDisposable = 1;
is3den = 0;
curatorCanAttach = 0;
curatorInfoType = "RscDisplayAttributeModuleNuke";
canSetArea = 0;
canSetAreaShape = 0;
canSetAreaHeight = 0;
/*
class Attributes: AttributesBase {
class GVAR(instanceAddress): Edit {
property = QGVAR(instanceAddress);
displayname = "MediaMTX Provider Address";
tooltip = "MediaMTX Provider Instance Address";
typeName = "STRING";
defaultValue = "localhost";
};
class GVAR(instancePort): Edit {
property = QGVAR(instancePort);
displayname = QUOTE(MediaMTX Provider Port);
tooltip = QUOTE(MediaMTX Provider Port for handling video streams);
typeName = "STRING";
defaultValue = "8554";
};
class GVAR(instanceAuthUser): Edit {
property = QGVAR(instanceAuthUser);
displayname = QUOTE(MediaMTX Provider Username);
tooltip = QUOTE(MediaMTX Provider Instance Username);
typeName = "STRING";
defaultValue = "administrator";
};
class GVAR(instanceAuthPassword): Edit {
property = QGVAR(instanceAuthPassword);
displayname = QUOTE(MediaMTX Provider Password);
tooltip = QUOTE(MediaMTX Provider Instance Password);
typeName = "STRING";
defaultValue = "password";
};
};
*/
};
};

View File

@@ -1 +0,0 @@
PREP(videoParser);

View File

@@ -1,9 +0,0 @@
#include "script_component.hpp"
ADDON = false;
PREP_RECOMPILE_START;
#include "XEH_PREP.hpp"
PREP_RECOMPILE_END;
ADDON = true;

View File

@@ -1,3 +0,0 @@
#include "script_component.hpp"
#include "XEH_PREP.hpp"

View File

@@ -1,23 +0,0 @@
#include "script_component.hpp"
class CfgPatches {
class ADDON {
name = COMPONENT_NAME;
units[] = {
QGVAR(videoModule)
};
weapons[] = {};
requiredAddons[] = {
"cba_main",
"ace_main",
"armatak_main",
"armatak_server"
};
requiredVersion = REQUIRED_VERSION;
author = PROJECT_AUTHOR;
url = "https://github.com/valmojr/armatak";
};
};
#include "CfgEventHandlers.hpp"
//#include "CfgVehicles.hpp"

View File

@@ -1,83 +0,0 @@
#include "..\script_component.hpp"
params [
["_logic", objNull, [objNull]],
["_units", [], [[]]],
["_activated", true, [true]]
];
if (isServer) exitWith {
private _instance_address = GETVAR(_logic,GVAR(instanceAddress),false);
private _instance_port = GETVAR(_logic,GVAR(instancePort),false);
private _instance_auth_user = GETVAR(_logic,GVAR(instanceAuthUser),false);
private _instance_auth_pass = GETVAR(_logic,GVAR(instanceAuthPassword),false);
SETMVAR(GVAR(instanceAddress),_instance_address);
SETMVAR(GVAR(instancePort),_instance_port);
SETMVAR(GVAR(instanceAuthUser),_instance_auth_user);
SETMVAR(GVAR(instanceAuthPassword),_instance_auth_pass);
_startAction = [
QGVAR(startStream),
"Start Video Feed",
"",
{
_uuid = (_this select 0) call armatak_fnc_extract_uuid;
_uuid_short = _uuid select [0, 8];
_role = roleDescription (_this select 0);
_name = name (_this select 0);
_role = [_role] call BIS_fnc_filterString;
_name = [_name] call BIS_fnc_filterString;
_stream_path = _name + "_" + _role + "_" + _uuid_short;
armatak_mediamtx_video_stream_instance_address = GETMVAR(instance_address,false);
armatak_mediamtx_video_stream_instance_port = missionNamespace getVariable "instance_port";
armatak_mediamtx_video_stream_instance_auth_user = missionNamespace getVariable "instance_auth_user";
armatak_mediamtx_video_stream_instance_auth_pass = missionNamespace getVariable "instance_auth_pass";
"armatak" callExtension ["video_stream:start", [armatak_mediamtx_video_stream_instance_address, armatak_mediamtx_video_stream_instance_port, _stream_path, armatak_mediamtx_video_stream_instance_auth_user, armatak_mediamtx_video_stream_instance_auth_pass]];
(_this select 0) setVariable ["armatak_video_feed_is_streaming", true];
},
{
(_this select 0) getVariable "armatak_video_feed_is_streaming" == false
}
] call ace_interact_menu_fnc_createAction;
[
"Man",
1,
["ACE_SelfActions"],
_startAction,
true
] call ace_interact_menu_fnc_addActionToClass;
_stopAction = [
"ArmatakStopStream",
"Stop Video Feed",
"",
{
"armatak" callExtension ["video_stream:stop", []];
SETVAR(_this select 0,GVAR(isStreaming),false);
},
{
GETVAR((this select 0),GVAR(isStreaming),false)
}
] call ace_interact_menu_fnc_createAction;
[
"Man",
1,
["ACE_SelfActions"],
_stopAction,
true
] call ace_interact_menu_fnc_addActionToClass;
if (isMultiplayer) then {
{
SETVAR(_x,GVAR(isStreaming),false);
} forEach playableUnits;
} else {
SETVAR(player,GVAR(isStreaming),false);
};
};
true;

View File

@@ -1,17 +0,0 @@
#define COMPONENT video
#define COMPONENT_BEAUTIFIED Video Streaming
#include "\armatak\armatak\addons\main\script_mod.hpp"
// #define DEBUG_MODE_FULL
// #define DISABLE_COMPILE_CACHE
// #define ENABLE_PERFORMANCE_COUNTERS
#ifdef DEBUG_ENABLED_MAIN
#define DEBUG_MODE_FULL
#endif
#ifdef DEBUG_SETTINGS_MAIN
#define DEBUG_SETTINGS DEBUG_SETTINGS_MAIN
#endif
#include "\z\ace\addons\main\script_macros.hpp"

BIN
media/delta_larp.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

BIN
media/picture.png LFS Normal file

Binary file not shown.

View File

@@ -1,5 +1,5 @@
use uuid::Uuid;
use chrono::{Duration, SecondsFormat, Utc};
use uuid::Uuid;
pub struct CursorOverTime {
pub uuid: Option<String>,
@@ -16,9 +16,18 @@ pub struct CursorOverTime {
pub track_speed: Option<f32>,
pub link_uid: Option<String>,
pub remarker: Option<String>,
pub video_url: Option<String>,
}
impl CursorOverTime {
fn escape_xml_attribute(value: &str) -> String {
value
.replace('&', "&amp;")
.replace('"', "&quot;")
.replace('<', "&lt;")
.replace('>', "&gt;")
}
pub fn convert_to_xml(&self) -> String {
let uuid = match &self.uuid {
Some(uuid) => uuid,
@@ -107,6 +116,18 @@ impl CursorOverTime {
xml.push_str(format!("<remarks>ARMATAK | {}</remarks>", remark).as_str());
}
if let Some(video_url) = &self.video_url {
if !video_url.trim().is_empty() {
xml.push_str(
format!(
"<__video url=\"{}\" />",
Self::escape_xml_attribute(video_url.trim())
)
.as_str(),
);
}
}
xml.push_str("</detail></event>");
return xml;

View File

@@ -1,45 +1,47 @@
use arma_rs::{FromArma, FromArmaError};
use super::cot::CursorOverTime;
use arma_rs::{FromArma, FromArmaError};
pub struct DigitalPointerPayload {
pub link_uid: String,
pub contact_callsign: String,
pub point_lat: f64,
pub point_lon: f64,
pub point_hae: f32,
pub link_uid: String,
pub contact_callsign: String,
pub point_lat: f64,
pub point_lon: f64,
pub point_hae: f32,
}
impl FromArma for DigitalPointerPayload {
fn from_arma(data: String) -> Result<DigitalPointerPayload, FromArmaError> {
let (link_uid, contact_callsign, point_lat, point_lon, point_hae) =
<(String, String, f64, f64, f32)>::from_arma(data)?;
Ok(Self {
link_uid,
contact_callsign,
point_lat,
point_lon,
point_hae,
})
}
fn from_arma(data: String) -> Result<DigitalPointerPayload, FromArmaError> {
let (link_uid, contact_callsign, point_lat, point_lon, point_hae) =
<(String, String, f64, f64, f32)>::from_arma(data)?;
Ok(Self {
link_uid,
contact_callsign,
point_lat,
point_lon,
point_hae,
})
}
}
impl DigitalPointerPayload {
pub fn to_cot(&self) -> CursorOverTime {
CursorOverTime {
uuid: Some(format!("{}{}", self.link_uid.clone(), ".SPI1")),
r#type: Some("b-m-p-s-p-i".to_string()),
point_lat: self.point_lat,
point_lon: self.point_lon,
point_hae: self.point_hae,
point_ce: None,
point_le: None,
contact_callsign: self.contact_callsign.clone(),
group_name: None,
group_role: None,
track_course: None,
track_speed: None,
link_uid: Some(self.link_uid.clone()),
remarker: None,
}
}
pub fn to_cot(&self) -> CursorOverTime {
CursorOverTime {
uuid: Some(format!("{}{}", self.link_uid.clone(), ".SPI1")),
r#type: Some("b-m-p-s-p-i".to_string()),
point_lat: self.point_lat,
point_lon: self.point_lon,
point_hae: self.point_hae,
point_ce: None,
point_le: None,
contact_callsign: self.contact_callsign.clone(),
group_name: None,
group_role: None,
track_course: None,
track_speed: None,
link_uid: Some(self.link_uid.clone()),
remarker: None,
video_url: None,
}
}
}

126
src/cot/draws/circle.rs Normal file
View File

@@ -0,0 +1,126 @@
use arma_rs::{FromArma, FromArmaError};
pub struct CircleCoTPayload {
pub uuid: String,
pub center_lat: f64,
pub center_lon: f64,
pub center_hae: f32,
pub major: f64,
pub minor: f64,
pub angle: f32,
pub callsign: String,
pub creator_uid: String,
pub creator_callsign: String,
}
impl FromArma for CircleCoTPayload {
fn from_arma(data: String) -> Result<Self, FromArmaError> {
let (
uuid,
center_lat,
center_lon,
center_hae,
major,
minor,
angle,
callsign,
creator_uid,
creator_callsign,
) = <(String, f64, f64, f32, f64, f64, f32, String, String, String)>::from_arma(data)?;
Ok(Self {
uuid,
center_lat,
center_lon,
center_hae,
major,
minor,
angle,
callsign,
creator_uid,
creator_callsign,
})
}
}
pub struct ShapeCircleCoT {
pub uid: String,
pub lat: f64,
pub lon: f64,
pub hae: f32,
pub major: f64,
pub minor: f64,
pub angle: f32,
pub callsign: String,
pub creator_uid: String,
pub creator_callsign: String,
}
impl CircleCoTPayload {
pub fn to_cot(&self) -> ShapeCircleCoT {
ShapeCircleCoT {
uid: self.uuid.clone(),
lat: self.center_lat,
lon: self.center_lon,
hae: self.center_hae,
major: self.major,
minor: self.minor,
angle: self.angle,
callsign: self.callsign.clone(),
creator_uid: self.creator_uid.clone(),
creator_callsign: self.creator_callsign.clone(),
}
}
}
impl ShapeCircleCoT {
pub fn to_xml(&self, now: &str, stale: &str) -> String {
format!(
r#"<event version="2.0" uid="{uid}" type="u-d-c-c"
time="{t}" start="{t}" stale="{stale}"
how="h-e" access="Undefined">
<point lat="{lat}" lon="{lon}" hae="{hae}" ce="10.9" le="9999999.0" />
<detail>
<shape>
<ellipse major="{major}" minor="{minor}" angle="{angle}" />
<link uid="{uid}.Style" type="b-x-KmlStyle" relation="p-c">
<Style>
<LineStyle>
<color>ffffffff</color>
<width>3.0</width>
</LineStyle>
<PolyStyle>
<color>96ffffff</color>
</PolyStyle>
</Style>
</link>
<link uid="{creator_uid}" type="self" relation="p-p-CenterAnchor" />
</shape>
<__shapeExtras cpvis="true" editable="true" />
<remarks />
<contact callsign="{callsign}" />
<creator uid="{creator_uid}" callsign="{creator_callsign}" time="{t}" type="a-f-G-U-C" />
<archive />
<labels_on value="true" />
<strokeColor value="-1" />
<strokeWeight value="3.0" />
<strokeStyle value="solid" />
<fillColor value="-1761607681" />
<precisionlocation altsrc="GPS" geopointsrc="GPS" />
</detail>
</event>"#,
uid = self.uid,
t = now,
stale = stale,
lat = self.lat,
lon = self.lon,
hae = self.hae,
major = self.major,
minor = self.minor,
angle = self.angle,
callsign = self.callsign,
creator_uid = self.creator_uid,
creator_callsign = self.creator_callsign
)
}
}

1
src/cot/draws/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod circle;

View File

@@ -1,62 +1,64 @@
use arma_rs::{FromArma, FromArmaError};
use super::cot::CursorOverTime;
use arma_rs::{FromArma, FromArmaError};
pub struct EudCoTPayload {
pub uuid: String,
pub point_lat: f64,
pub point_lon: f64,
pub point_hae: f32,
pub contact_callsign: String,
pub group_name: String,
pub group_role: String,
pub track_course: i32,
pub track_speed: f32,
pub uuid: String,
pub point_lat: f64,
pub point_lon: f64,
pub point_hae: f32,
pub contact_callsign: String,
pub group_name: String,
pub group_role: String,
pub track_course: i32,
pub track_speed: f32,
}
impl FromArma for EudCoTPayload {
fn from_arma(data: String) -> Result<EudCoTPayload, FromArmaError> {
let (
uuid,
point_lat,
point_lon,
point_hae,
contact_callsign,
group_name,
group_role,
track_course,
track_speed,
) = <(String, f64, f64, f32, String, String, String, i32, f32)>::from_arma(data)?;
Ok(Self {
uuid,
point_lat,
point_lon,
point_hae,
contact_callsign,
group_name,
group_role,
track_course,
track_speed,
})
}
fn from_arma(data: String) -> Result<EudCoTPayload, FromArmaError> {
let (
uuid,
point_lat,
point_lon,
point_hae,
contact_callsign,
group_name,
group_role,
track_course,
track_speed,
) = <(String, f64, f64, f32, String, String, String, i32, f32)>::from_arma(data)?;
Ok(Self {
uuid,
point_lat,
point_lon,
point_hae,
contact_callsign,
group_name,
group_role,
track_course,
track_speed,
})
}
}
impl EudCoTPayload {
pub fn to_cot(&self) -> CursorOverTime {
CursorOverTime {
uuid: Some(self.uuid.clone()),
r#type: None,
point_lat: self.point_lat,
point_lon: self.point_lon,
point_hae: self.point_hae,
point_ce: None,
point_le: None,
contact_callsign: self.contact_callsign.clone(),
group_name: Some(self.group_name.clone()),
group_role: Some(self.group_role.clone()),
track_course: Some(self.track_course),
track_speed: Some(self.track_speed),
link_uid: None,
remarker: None,
}
}
pub fn to_cot(&self) -> CursorOverTime {
CursorOverTime {
uuid: Some(self.uuid.clone()),
r#type: None,
point_lat: self.point_lat,
point_lon: self.point_lon,
point_hae: self.point_hae,
point_ce: None,
point_le: None,
contact_callsign: self.contact_callsign.clone(),
group_name: Some(self.group_name.clone()),
group_role: Some(self.group_role.clone()),
track_course: Some(self.track_course),
track_speed: Some(self.track_speed),
link_uid: None,
remarker: None,
video_url: None,
}
}
}

View File

@@ -1,5 +1,5 @@
use arma_rs::{FromArma, FromArmaError};
use super::cot::CursorOverTime;
use arma_rs::{FromArma, FromArmaError};
pub struct ExternalPositionPayload {
pub uuid: String,
@@ -13,47 +13,49 @@ pub struct ExternalPositionPayload {
}
impl FromArma for ExternalPositionPayload {
fn from_arma(data: String) -> Result<ExternalPositionPayload, FromArmaError> {
let (
uuid,
point_lat,
point_lon,
point_hae,
contact_callsign,
track_course,
track_speed,
remarker,
) = <(String, f64, f64, f32, String, i32, f32, String)>::from_arma(data)?;
Ok(Self {
uuid,
point_lat,
point_lon,
point_hae,
contact_callsign,
track_course,
track_speed,
remarker,
})
}
fn from_arma(data: String) -> Result<ExternalPositionPayload, FromArmaError> {
let (
uuid,
point_lat,
point_lon,
point_hae,
contact_callsign,
track_course,
track_speed,
remarker,
) = <(String, f64, f64, f32, String, i32, f32, String)>::from_arma(data)?;
Ok(Self {
uuid,
point_lat,
point_lon,
point_hae,
contact_callsign,
track_course,
track_speed,
remarker,
})
}
}
impl ExternalPositionPayload {
pub fn to_cot(&self) -> CursorOverTime {
CursorOverTime {
uuid: Some(self.uuid.clone()),
r#type: None,
point_lat: self.point_lat,
point_lon: self.point_lon,
point_hae: self.point_hae,
point_ce: None,
point_le: None,
contact_callsign: self.contact_callsign.clone(),
group_name: None,
group_role: None,
track_course: Some(self.track_course),
track_speed: Some(self.track_speed),
link_uid: None,
remarker: Some(self.remarker.clone()),
}
}
pub fn to_cot(&self) -> CursorOverTime {
CursorOverTime {
uuid: Some(self.uuid.clone()),
r#type: None,
point_lat: self.point_lat,
point_lon: self.point_lon,
point_hae: self.point_hae,
point_ce: None,
point_le: None,
contact_callsign: self.contact_callsign.clone(),
group_name: None,
group_role: None,
track_course: Some(self.track_course),
track_speed: Some(self.track_speed),
link_uid: None,
remarker: Some(self.remarker.clone()),
video_url: None,
}
}
}

147
src/cot/message.rs Normal file
View File

@@ -0,0 +1,147 @@
use arma_rs::{FromArma, FromArmaError};
use chrono::{Duration, SecondsFormat, Utc};
use uuid::Uuid;
pub struct MessagePayload {
pub sender_callsign: String,
pub chatroom: String,
pub message_text: String,
pub point_lat: f64,
pub point_lon: f64,
pub point_hae: f32,
pub sender_uid: String,
}
impl FromArma for MessagePayload {
fn from_arma(data: String) -> Result<Self, FromArmaError> {
let (sender_callsign, chatroom, message_text, point_lat, point_lon, point_hae, sender_uid) =
<(String, String, String, f64, f64, f32, String)>::from_arma(data)?;
Ok(Self {
sender_callsign,
chatroom,
message_text,
point_lat,
point_lon,
point_hae,
sender_uid,
})
}
}
pub struct MessageCot {
pub sender_callsign: String,
pub chatroom: String,
pub message_text: String,
pub point_lat: f64,
pub point_lon: f64,
pub point_hae: f32,
pub sender_uid: String,
}
impl MessageCot {
pub fn from_payload(p: MessagePayload) -> Self {
Self {
sender_callsign: p.sender_callsign,
chatroom: p.chatroom,
message_text: p.message_text,
point_lat: p.point_lat,
point_lon: p.point_lon,
point_hae: p.point_hae,
sender_uid: p.sender_uid,
}
}
pub fn to_xml(&self) -> String {
let created_time = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
let stale_time =
(Utc::now() + Duration::days(1)).to_rfc3339_opts(SecondsFormat::Millis, true);
// MESSAGE ID (random UUID)
let message_uuid = Uuid::new_v4().to_string();
// FULL EVENT UID
// format: GeoChat.{sender}.{chatroom}.{uuid}
let event_uid = format!(
"GeoChat.{}.{}.{}",
self.sender_uid,
self.chatroom.replace(" ", "_"),
message_uuid,
);
let mut xml = String::new();
xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
xml.push_str(
format!(
"<event version=\"2.0\" uid=\"{}\" type=\"b-t-f\" time=\"{}\" start=\"{}\" stale=\"{}\" how=\"h-g-i-g-o\" access=\"Undefined\">",
event_uid, created_time, created_time, stale_time
)
.as_str(),
);
xml.push_str(
format!(
"<point lat=\"{}\" lon=\"{}\" hae=\"{}\" ce=\"10.3\" le=\"9999999.0\"/>",
self.point_lat, self.point_lon, self.point_hae
)
.as_str(),
);
xml.push_str("<detail>");
// ========== CHAT OBJECT ==========
xml.push_str(
format!(
"<__chat parent=\"RootContactGroup\" groupOwner=\"false\" \
messageId=\"{}\" chatroom=\"{}\" id=\"{}\" senderCallsign=\"{}\">",
message_uuid, self.chatroom, self.chatroom, self.sender_callsign,
)
.as_str(),
);
xml.push_str(
format!(
"<chatgrp uid0=\"{}\" uid1=\"{}\" id=\"{}\" />",
self.sender_uid, self.chatroom, self.chatroom
)
.as_str(),
);
xml.push_str("</__chat>");
// ========== LINK ELEMENT ==========
xml.push_str(
format!(
"<link uid=\"{}\" type=\"a-f-G-U-C\" relation=\"p-p\" />",
self.sender_uid
)
.as_str(),
);
// ========== SERVER DEST ==========
// This is optional — you may remove or customize it
xml.push_str(
format!(
"<__serverdestination destinations=\"0.0.0.0:0:tcp:{}\" />",
self.sender_uid
)
.as_str(),
);
// ========== MESSAGE REMARKS ==========
xml.push_str(
format!(
"<remarks source=\"ARMATAK.{}\" to=\"{}\" time=\"{}\">{}</remarks>",
self.sender_uid, self.chatroom, created_time, self.message_text
)
.as_str(),
);
xml.push_str("</detail></event>");
xml
}
}

View File

@@ -1,5 +1,8 @@
pub mod cot;
pub mod digital_pointer;
pub mod draws;
pub mod eud;
pub mod gps;
pub mod nato;
pub mod message;
pub mod nato;
pub mod uas;

View File

@@ -11,10 +11,40 @@ pub struct MarkerCoTPayload {
pub contact_callsign: String,
pub track_course: i32,
pub track_speed: f32,
pub video_url: Option<String>,
}
impl FromArma for MarkerCoTPayload {
fn from_arma(data: String) -> Result<MarkerCoTPayload, FromArmaError> {
if let Ok((
uuid,
r#type,
point_lat,
point_lon,
point_hae,
contact_callsign,
track_course,
track_speed,
video_url,
)) = <(String, String, f64, f64, f32, String, i32, f32, String)>::from_arma(data.clone())
{
return Ok(Self {
uuid,
r#type,
point_lat,
point_lon,
point_hae,
contact_callsign,
track_course,
track_speed,
video_url: if video_url.trim().is_empty() {
None
} else {
Some(video_url)
},
});
}
let (
uuid,
r#type,
@@ -34,6 +64,7 @@ impl FromArma for MarkerCoTPayload {
contact_callsign,
track_course,
track_speed,
video_url: None,
})
}
}
@@ -55,6 +86,7 @@ impl MarkerCoTPayload {
track_speed: Some(self.track_speed),
link_uid: None,
remarker: None,
video_url: self.video_url.clone(),
}
}
}

243
src/cot/uas.rs Normal file
View File

@@ -0,0 +1,243 @@
// src/cot/uas.rs
//
// CoT types required for ATAK UAS Tool integration.
//
// Two event types are needed so that the UAS Tool plugin recognises a drone:
//
// b-i-v — Video endpoint declaration. Tells the UAS Tool where
// to pull the RTSP stream for this drone.
//
// b-m-p-s-p-loc — Sensor position event. Carries the camera azimuth,
// field-of-view, and slant-range that the UAS Tool uses
// to draw the FOV cone on the map and to project AR
// markers onto the video feed.
//
// The two events are linked: the b-m-p-s-p-loc detail contains
// <__video uid="<drone-uuid>"/>
// which references the uid of the b-i-v event, so the UAS Tool knows which
// video stream belongs to this sensor.
use arma_rs::{FromArma, FromArmaError};
use chrono::{Duration, SecondsFormat, Utc};
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/// Parse an RTSP URL of the form rtsp://address:port/path
/// into its three components.
fn parse_rtsp_url(url: &str) -> Option<(String, String, String)> {
let without_proto = url.strip_prefix("rtsp://")?;
let slash_pos = without_proto.find('/')?;
let host_port = &without_proto[..slash_pos];
let path = &without_proto[slash_pos..]; // includes the leading '/'
let colon_pos = host_port.rfind(':')?;
let address = host_port[..colon_pos].to_string();
let port = host_port[colon_pos + 1..].to_string();
Some((address, port, path.to_string()))
}
// ---------------------------------------------------------------------------
// b-i-v Video endpoint declaration
// ---------------------------------------------------------------------------
pub struct UasVideoCoTPayload {
/// The drone's persistent ATAK UUID (same uid used for PPLI / marker CoT).
pub uid: String,
/// Human-readable label shown in the UAS Tool video list.
pub callsign: String,
/// Full RTSP URL, e.g. "rtsp://192.168.1.10:8554/live/drone1".
pub video_url: String,
}
impl FromArma for UasVideoCoTPayload {
fn from_arma(data: String) -> Result<UasVideoCoTPayload, FromArmaError> {
let (uid, callsign, video_url) =
<(String, String, String)>::from_arma(data)?;
Ok(Self {
uid,
callsign,
video_url,
})
}
}
impl UasVideoCoTPayload {
/// Build the complete XML string for the b-i-v CoT event.
/// Returns an empty string if the RTSP URL cannot be parsed.
pub fn to_xml(&self) -> String {
let (address, port, path) = match parse_rtsp_url(&self.video_url) {
Some(parts) => parts,
None => {
log::warn!(
"UasVideoCoTPayload: could not parse RTSP URL: {}",
self.video_url
);
return String::new();
}
};
let now =
Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
// Long stale time: the video endpoint is considered valid for 1 hour.
// The CoT is re-sent every router tick so it stays fresh even if the
// TAK server restarts.
let stale = (Utc::now() + Duration::seconds(3600))
.to_rfc3339_opts(SecondsFormat::Millis, true);
let mut xml = String::new();
xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
xml.push_str(&format!(
"<event type=\"b-i-v\" version=\"2.0\" how=\"m-g\" \
uid=\"{uid}\" time=\"{now}\" start=\"{now}\" stale=\"{stale}\">",
uid = self.uid,
now = now,
stale = stale
));
// b-i-v events carry no real geographic position.
xml.push_str(
"<point lat=\"0\" lon=\"0\" hae=\"9999999.0\" \
ce=\"9999999.0\" le=\"9999999.0\"/>",
);
xml.push_str("<detail>");
xml.push_str("<__video>");
xml.push_str(&format!(
"<ConnectionEntry \
protocol=\"rtsp\" \
path=\"{path}\" \
address=\"{address}\" \
port=\"{port}\" \
uid=\"{uid}\" \
alias=\"{callsign}\" \
roverPort=\"-1\" \
rtspReliable=\"0\" \
ignoreEmbeddedKLV=\"False\" \
networkTimeout=\"0\" \
bufferTime=\"-1\"/>",
path = path,
address = address,
port = port,
uid = self.uid,
callsign = self.callsign,
));
xml.push_str("</__video>");
xml.push_str(&format!(
"<contact callsign=\"{}\"/>",
self.callsign
));
xml.push_str("</detail>");
xml.push_str("</event>");
xml
}
}
// ---------------------------------------------------------------------------
// b-m-p-s-p-loc Sensor position (FOV cone + video link)
// ---------------------------------------------------------------------------
pub struct UasSensorCoTPayload {
/// UID for this sensor event — conventionally "<drone-uuid>-sensor".
pub uid: String,
/// The drone's ATAK UUID; must match the uid used in the b-i-v event so
/// the UAS Tool can link sensor data to the correct video stream.
pub video_uid: String,
/// Callsign shown in the UAS Tool sensor list.
pub callsign: String,
/// Drone latitude in decimal degrees (WGS-84).
pub point_lat: f64,
/// Drone longitude in decimal degrees (WGS-84).
pub point_lon: f64,
/// Drone height above ellipsoid in metres (WGS-84).
pub point_hae: f32,
/// Camera azimuth in degrees, clockwise from true North (0359).
pub azimuth: i32,
/// Camera horizontal field of view in degrees.
pub fov: i32,
/// Estimated slant range from drone to ground point in metres.
/// A good approximation is the drone's AGL altitude.
pub range: i32,
}
impl FromArma for UasSensorCoTPayload {
fn from_arma(data: String) -> Result<UasSensorCoTPayload, FromArmaError> {
let (
uid,
video_uid,
callsign,
point_lat,
point_lon,
point_hae,
azimuth,
fov,
range,
) = <(String, String, String, f64, f64, f32, i32, i32, i32)>::from_arma(
data,
)?;
Ok(Self {
uid,
video_uid,
callsign,
point_lat,
point_lon,
point_hae,
azimuth,
fov,
range,
})
}
}
impl UasSensorCoTPayload {
/// Build the complete XML string for the b-m-p-s-p-loc CoT event.
pub fn to_xml(&self) -> String {
let now =
Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
// 60-second stale: must be refreshed every router tick (1 s) to keep
// the FOV cone visible on the map.
let stale = (Utc::now() + Duration::seconds(60))
.to_rfc3339_opts(SecondsFormat::Millis, true);
let mut xml = String::new();
xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
xml.push_str(&format!(
"<event type=\"b-m-p-s-p-loc\" version=\"2.0\" how=\"h-g-i-g-o\" \
uid=\"{uid}\" time=\"{now}\" start=\"{now}\" stale=\"{stale}\">",
uid = self.uid,
now = now,
stale = stale,
));
xml.push_str(&format!(
"<point lat=\"{lat}\" lon=\"{lon}\" hae=\"{hae}\" \
ce=\"9999999.0\" le=\"9999999.0\"/>",
lat = self.point_lat,
lon = self.point_lon,
hae = self.point_hae,
));
xml.push_str("<detail>");
// fovAlpha controls the transparency of the FOV cone fill (01).
// 0.537 ≈ 137/255, the value used by the real UAS Tool.
xml.push_str(&format!(
"<sensor \
fov=\"{fov}\" \
fovRed=\"1\" \
fovGreen=\"1\" \
fovBlue=\"1\" \
fovAlpha=\"0.5372549\" \
displayMagneticReference=\"0\" \
range=\"{range}\" \
azimuth=\"{az}\"/>",
fov = self.fov,
range = self.range,
az = self.azimuth,
));
// Link this sensor event to the b-i-v video endpoint.
xml.push_str(&format!("<__video uid=\"{}\"/>", self.video_uid));
xml.push_str(&format!(
"<contact callsign=\"{}\"/>",
self.callsign
));
xml.push_str("</detail>");
xml.push_str("</event>");
xml
}
}

View File

@@ -1,8 +1,9 @@
use arma_rs::{arma, Extension, Group};
use rustls::crypto::aws_lc_rs;
mod structs;
mod tcp;
mod tests;
mod udp_socket;
mod tcp_socket;
mod video_stream;
mod cot;
@@ -31,32 +32,55 @@ pub fn init() -> Extension {
log4rs::init_config(config).unwrap();
let _ = aws_lc_rs::default_provider().install_default();
log::info!("Initialized rustls aws-lc crypto provider.");
Extension::build()
.command("local_ip", utils::address::get_local_address)
.command("uuid", utils::uuid::get_uuid)
.command("log", utils::log::log_info)
.group("udp_socket",
.group(
"udp_socket",
Group::new()
.command("start", udp_socket::start)
.command("send_payload", udp_socket::send_payload)
.command("send_gps_cot", udp_socket::send_gps_cot)
.command("stop", udp_socket::stop)
.command("stop", udp_socket::stop),
)
.group(
"tcp_socket",
Group::new()
.command("start", tcp_socket::start)
.command("send_payload", tcp_socket::send_payload)
.command("send_eud_cot", tcp_socket::send_eud_cot)
.command("send_marker_cot", tcp_socket::send_marker_cot)
.command("send_digital_pointer_cot", tcp_socket::send_digital_pointer_cot)
.command("stop", tcp_socket::stop)
.command("start", tcp::start)
.command("start_mtls", tcp::start_mtls)
.command("start_enroll_mtls", tcp::start_enroll_mtls)
.command("stop", tcp::stop)
.command("send_payload", tcp::send_payload)
.group(
"cot",
Group::new()
.command("eud", tcp::cot::send_eud_cot)
.command("marker", tcp::cot::send_marker_cot)
.command("digital_pointer", tcp::cot::send_digital_pointer_cot)
.command("chat", tcp::cot::send_message_cot)
// UAS Tool integration
.command("uas_video", tcp::cot::send_uas_video_cot)
.command("uas_sensor", tcp::cot::send_uas_sensor_cot),
)
.group(
"draw",
Group::new()
.command("circle", tcp::draw::send_circle_cot)
.command("ellipse", tcp::draw::send_ellipse_cot)
.command("rectangle", tcp::draw::send_rectangle_cot)
.command("free", tcp::draw::send_freedraw_cot)
.command("vector", tcp::draw::send_vectordraw_cot),
),
)
.group(
"video_stream",
Group::new()
.command("start", video_stream::start_stream)
.command("stop", video_stream::stop_stream)
.command("stop", video_stream::stop_stream),
)
.finish()
}

View File

@@ -10,9 +10,6 @@ pub struct LogPayload {
impl FromArma for LogPayload {
fn from_arma(data: String) -> Result<LogPayload, FromArmaError> {
let (status, message) = <(String, String)>::from_arma(data)?;
Ok(Self {
status,
message
})
Ok(Self { status, message })
}
}

280
src/tcp/client.rs Normal file
View File

@@ -0,0 +1,280 @@
use arma_rs::Context;
use log::{info, warn};
use std::collections::VecDeque;
use std::panic::{self, AssertUnwindSafe};
use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender, TryRecvError};
use std::thread;
use std::time::Duration;
use super::config::ConnectionConfig;
use super::transport::{connect_stream, TransportStream};
use super::TCP_CLIENT;
const CONNECT_POLL_INTERVAL: Duration = Duration::from_millis(200);
const MAX_PENDING_MESSAGES: usize = 128;
pub enum TcpCommand {
SendMessage(String, Context),
Stop,
}
pub struct TcpClient {
pub(crate) tx: Sender<TcpCommand>,
}
enum ConnectionState {
Connecting,
Connected,
Failed(String),
}
enum ConnectEvent {
Connected(TransportStream),
Failed(String),
}
fn describe_panic_payload(payload: Box<dyn std::any::Any + Send>) -> String {
if let Some(message) = payload.downcast_ref::<&str>() {
(*message).to_string()
} else if let Some(message) = payload.downcast_ref::<String>() {
message.clone()
} else {
"unknown panic payload".to_string()
}
}
fn log_message_preview(message: &str) -> String {
message.chars().take(96).collect::<String>()
}
fn send_over_stream(
stream: &mut TransportStream,
context: &Context,
message: String,
) -> Result<(), String> {
let message_len = message.len();
info!("Sending TCP payload ({} bytes)", message_len);
stream
.write_message(message.as_bytes())
.map_err(|e| {
let message = e.to_string();
let _ = context.callback_data(
"TCP SOCKET ERROR",
"TAK Socket disconnected",
message.clone(),
);
message
})
}
fn flush_pending_messages(
connection: &mut Option<TransportStream>,
pending_messages: &mut VecDeque<(String, Context)>,
state: &mut ConnectionState,
) {
if pending_messages.is_empty() {
return;
}
let Some(stream) = connection.as_mut() else {
return;
};
info!(
"Flushing {} queued TCP payload(s) after connection became active",
pending_messages.len()
);
while let Some((message, context)) = pending_messages.pop_front() {
if let Err(error) = send_over_stream(stream, &context, message) {
info!("Failed to send queued message: {}", error);
*state = ConnectionState::Failed(error);
*connection = None;
return;
}
}
}
fn poll_connect_event(
connect_rx: &Receiver<ConnectEvent>,
connection: &mut Option<TransportStream>,
state: &mut ConnectionState,
pending_messages: &mut VecDeque<(String, Context)>,
ctx: &Context,
connection_message: &str,
target: &str,
) {
loop {
match connect_rx.try_recv() {
Ok(ConnectEvent::Connected(stream)) => {
info!("TCP connection established successfully: {}", target);
let _ = ctx.callback_data("TCP SOCKET", connection_message, target.to_string());
*connection = Some(stream);
*state = ConnectionState::Connected;
flush_pending_messages(connection, pending_messages, state);
}
Ok(ConnectEvent::Failed(error)) => {
info!("Failed to connect to TCP server: {}", error);
let _ = ctx.callback_data(
"TCP SOCKET ERROR",
"TAK Socket connection failed",
error.clone(),
);
*state = ConnectionState::Failed(error);
}
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => break,
}
}
}
impl TcpClient {
pub fn start(&self, config: ConnectionConfig, rx: Receiver<TcpCommand>, ctx: Context) {
if let Some(ref client) = *TCP_CLIENT.lock().unwrap() {
info!("Existing TCP client detected; stopping previous instance before restart.");
client.stop();
}
thread::spawn(move || {
let mut running = true;
let connection_message = config.connected_message();
let config_description = config.describe();
let target = config.target();
let mut state = ConnectionState::Connecting;
let mut connection: Option<TransportStream> = None;
let mut pending_messages: VecDeque<(String, Context)> = VecDeque::new();
let (connect_tx, connect_rx) = mpsc::channel();
info!("TCP worker thread started with config: {}", config_description);
let tcp_thread = thread::spawn(move || {
let connect_result = panic::catch_unwind(AssertUnwindSafe(|| connect_stream(&config)));
match connect_result {
Ok(Ok(stream)) => {
let _ = connect_tx.send(ConnectEvent::Connected(stream));
}
Ok(Err(error)) => {
let _ = connect_tx.send(ConnectEvent::Failed(error));
}
Err(payload) => {
let message = format!(
"TCP connection worker panicked: {}",
describe_panic_payload(payload)
);
let _ = connect_tx.send(ConnectEvent::Failed(message));
}
}
});
while running {
poll_connect_event(
&connect_rx,
&mut connection,
&mut state,
&mut pending_messages,
&ctx,
connection_message,
&target,
);
match rx.recv_timeout(CONNECT_POLL_INTERVAL) {
Ok(TcpCommand::SendMessage(message, context)) => {
let message_len = message.len();
match &mut state {
ConnectionState::Connected => {
if let Some(stream) = connection.as_mut() {
if let Err(error) = send_over_stream(stream, &context, message) {
info!("Failed to send message: {}", error);
state = ConnectionState::Failed(error);
connection = None;
}
} else {
warn!(
"Connection state said connected, but no socket was present; queuing payload."
);
pending_messages.push_back((message, context));
}
}
ConnectionState::Connecting => {
if pending_messages.len() >= MAX_PENDING_MESSAGES {
let preview = log_message_preview(&message);
warn!(
"Dropping TCP payload because connection is still pending and queue is full ({} bytes, preview={:?})",
message_len, preview
);
let _ = context.callback_data(
"TCP SOCKET ERROR",
"TAK Socket is still connecting",
format!(
"queue full while connecting; dropped payload ({} bytes, preview={:?})",
message_len, preview
),
);
} else {
info!(
"Queueing TCP payload while connection is pending ({} bytes, queued={})",
message_len,
pending_messages.len() + 1
);
pending_messages.push_back((message, context));
}
}
ConnectionState::Failed(error) => {
let preview = log_message_preview(&message);
warn!(
"Dropping TCP payload because connection is in failed state ({} bytes, preview={:?}, error={})",
message_len, preview, error
);
let _ = context.callback_data(
"TCP SOCKET ERROR",
"TAK Socket is not connected",
error.clone(),
);
}
}
}
Ok(TcpCommand::Stop) => {
running = false;
info!("Stopping TCP client.");
}
Err(RecvTimeoutError::Timeout) => {}
Err(RecvTimeoutError::Disconnected) => {
warn!("TCP command channel disconnected.");
running = false;
}
}
}
info!("Waiting for TCP connection thread to finish.");
match tcp_thread.join() {
Ok(()) => info!("TCP connection thread joined successfully."),
Err(payload) => warn!(
"TCP connection thread join reported a panic: {}",
describe_panic_payload(payload)
),
}
info!("TCP worker thread finished.");
});
}
pub fn send_payload(&self, context: Context, payload: String) {
let tx = self.tx.clone();
thread::spawn(move || {
info!("Dispatching queued TCP payload command.");
if let Err(error) = tx.send(TcpCommand::SendMessage(payload, context)) {
warn!("Failed to dispatch TCP payload command: {}", error);
}
});
}
pub fn stop(&self) {
let tx = self.tx.clone();
thread::spawn(move || {
info!("Dispatching TCP stop command.");
if let Err(error) = tx.send(TcpCommand::Stop) {
warn!("Failed to dispatch TCP stop command: {}", error);
}
});
}
}

64
src/tcp/config.rs Normal file
View File

@@ -0,0 +1,64 @@
pub enum ConnectionConfig {
Plain {
address: String,
},
Mtls {
address: String,
server_name: String,
ca_cert_path: String,
client_cert_path: String,
client_key_path: String,
},
EnrollMtls {
host: String,
server_name: String,
enroll_port: String,
username: String,
password: String,
client_uid: String,
},
}
impl ConnectionConfig {
pub fn connected_message(&self) -> &'static str {
match self {
Self::Plain { .. } => "Connected to TCP Server",
Self::Mtls { .. } => "Connected to TAK Server via mTLS",
Self::EnrollMtls { .. } => "Connected to TAK Server via enrolled mTLS certificate",
}
}
pub fn target(&self) -> String {
match self {
Self::Plain { address } | Self::Mtls { address, .. } => address.clone(),
Self::EnrollMtls { host, .. } => host.clone(),
}
}
pub fn describe(&self) -> String {
match self {
Self::Plain { address } => format!("plain tcp -> {}", address),
Self::Mtls {
address,
server_name,
ca_cert_path,
client_cert_path,
client_key_path,
} => format!(
"manual mtls -> {} (server_name={}, ca={}, cert={}, key={})",
address, server_name, ca_cert_path, client_cert_path, client_key_path
),
Self::EnrollMtls {
host,
server_name,
enroll_port,
username,
client_uid,
..
} => format!(
"enroll mtls -> host={} enroll_port={} server_name={} username={} client_uid={}",
host, enroll_port, server_name, username, client_uid
),
}
}
}

70
src/tcp/cot.rs Normal file
View File

@@ -0,0 +1,70 @@
use arma_rs::Context;
use crate::{cot, tcp::send_payload};
pub fn send_eud_cot(ctx: Context, cursor_over_time: cot::eud::EudCoTPayload) -> &'static str {
let payload = cursor_over_time.to_cot().convert_to_xml();
send_payload(ctx, payload);
"Sending End User Device Cursor Over Time to TCP server"
}
pub fn send_marker_cot(
ctx: Context,
cursor_over_time: cot::nato::MarkerCoTPayload,
) -> &'static str {
let payload = cursor_over_time.to_cot().convert_to_xml();
send_payload(ctx, payload);
"Sending Marker Cursor Over Time to TCP server"
}
pub fn send_digital_pointer_cot(
ctx: Context,
cursor_over_time: cot::digital_pointer::DigitalPointerPayload,
) -> &'static str {
let payload = cursor_over_time.to_cot().convert_to_xml();
send_payload(ctx, payload);
"Sending Digital Pointer Cursor Over Time to TCP server"
}
pub fn send_message_cot(
ctx: Context,
message_payload: cot::message::MessagePayload,
) -> &'static str {
let message_cot = cot::message::MessageCot::from_payload(message_payload);
let payload = message_cot.to_xml();
send_payload(ctx, payload);
"Sending Message CoT to TCP server"
}
/// Send a b-i-v CoT that declares the RTSP video endpoint for a drone.
/// Called by SQF via: "armatak" callExtension ["tcp_socket:cot:uas_video", [payload]]
///
/// Returns early without sending if the RTSP URL in the payload cannot be parsed.
pub fn send_uas_video_cot(
ctx: Context,
payload: cot::uas::UasVideoCoTPayload,
) -> &'static str {
let xml = payload.to_xml();
if !xml.is_empty() {
send_payload(ctx, xml);
}
"Sending UAS Video (b-i-v) CoT to TCP server"
}
/// Send a b-m-p-s-p-loc CoT carrying the drone camera's azimuth, FOV, and
/// slant-range so the UAS Tool can draw the FOV cone on the map.
/// Called by SQF via: "armatak" callExtension ["tcp_socket:cot:uas_sensor", [payload]]
pub fn send_uas_sensor_cot(
ctx: Context,
payload: cot::uas::UasSensorCoTPayload,
) -> &'static str {
let xml = payload.to_xml();
send_payload(ctx, xml);
"Sending UAS Sensor (b-m-p-s-p-loc) CoT to TCP server"
}

37
src/tcp/draw.rs Normal file
View File

@@ -0,0 +1,37 @@
use arma_rs::Context;
use crate::{cot, tcp::send_payload};
pub fn send_circle_cot(
ctx: Context,
circle_payload: cot::draws::circle::CircleCoTPayload,
) -> &'static str {
let shape_circle_cot = circle_payload.to_cot();
let now = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
let stale = (chrono::Utc::now() + chrono::Duration::days(1))
.to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
let payload = shape_circle_cot.to_xml(&now, &stale);
send_payload(ctx, payload);
"Sending Circle CoT to TCP server"
}
pub fn send_ellipse_cot(ctx: Context) -> &'static str {
let _ = ctx;
"Not implemented: send_ellipse_cot"
}
pub fn send_rectangle_cot(ctx: Context) -> &'static str {
let _ = ctx;
"Not implemented: send_ellipse_cot"
}
pub fn send_freedraw_cot(ctx: Context) -> &'static str {
let _ = ctx;
"Not implemented: send_ellipse_cot"
}
pub fn send_vectordraw_cot(ctx: Context) -> &'static str {
let _ = ctx;
"Not implemented: send_ellipse_cot"
}

107
src/tcp/mod.rs Normal file
View File

@@ -0,0 +1,107 @@
use arma_rs::Context;
use lazy_static::lazy_static;
use log::info;
use std::sync::mpsc::{self, Receiver, Sender};
use std::sync::{Arc, Mutex};
mod client;
mod config;
mod tls;
mod transport;
pub mod cot;
pub mod draw;
use client::{TcpClient, TcpCommand};
use config::ConnectionConfig;
lazy_static! {
static ref TCP_CLIENT: Arc<Mutex<Option<TcpClient>>> = Arc::new(Mutex::new(None));
}
fn start_with_config(ctx: Context, config: ConnectionConfig) {
info!("Starting TCP client with config: {}", config.describe());
let (tx, rx): (Sender<TcpCommand>, Receiver<TcpCommand>) = mpsc::channel();
let client = TcpClient { tx };
client.start(config, rx, ctx);
let mut client_guard = TCP_CLIENT.lock().unwrap();
*client_guard = Some(client);
}
pub fn start(ctx: Context, address: String) -> &'static str {
start_with_config(ctx, ConnectionConfig::Plain { address });
"Starting TCP Client"
}
pub fn start_mtls(
ctx: Context,
address: String,
server_name: String,
ca_cert_path: String,
client_cert_path: String,
client_key_path: String,
) -> &'static str {
start_with_config(
ctx,
ConnectionConfig::Mtls {
address,
server_name,
ca_cert_path,
client_cert_path,
client_key_path,
},
);
"Starting mTLS TCP Client"
}
pub fn start_enroll_mtls(
ctx: Context,
host: String,
server_name: String,
enroll_port: String,
username: String,
password: String,
client_uid: String,
) -> &'static str {
start_with_config(
ctx,
ConnectionConfig::EnrollMtls {
host,
server_name,
enroll_port,
username,
password,
client_uid,
},
);
"Starting enrolled mTLS TCP Client"
}
pub fn send_payload(ctx: Context, payload: String) -> &'static str {
if let Some(ref client) = *TCP_CLIENT.lock().unwrap() {
info!("Queueing TCP payload ({} bytes)", payload.len());
client.send_payload(ctx, payload);
} else {
let _ = ctx.callback_null("TCP SOCKET ERROR", "TCP Client is not running");
info!("TCP client is not running.");
}
"Sending payload to TCP server"
}
pub fn stop(ctx: Context) -> &'static str {
if let Some(ref client) = *TCP_CLIENT.lock().unwrap() {
info!("Stopping TCP client via extension command.");
client.stop();
let _ = ctx.callback_null("TCP SOCKET", "TCP client stopped");
} else {
let _ = ctx.callback_null("TCP SOCKET ERROR", "TCP client is not running");
}
"Stopping TCP Client"
}

221
src/tcp/tls/connector.rs Normal file
View File

@@ -0,0 +1,221 @@
use log::info;
use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
use rustls::{ClientConfig, ClientConnection, RootCertStore, StreamOwned};
use rustls_pemfile::{certs, private_key};
use std::fs::File;
use std::io::BufReader;
use std::io::Cursor;
use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
use std::time::Duration;
use std::sync::Arc;
use crate::tcp::transport::TransportStream;
const TCP_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
const SOCKET_IO_TIMEOUT: Duration = Duration::from_secs(10);
fn load_certificates(path: &str) -> Result<Vec<CertificateDer<'static>>, String> {
let file = File::open(path).map_err(|e| format!("failed to open cert file {}: {}", path, e))?;
let mut reader = BufReader::new(file);
certs(&mut reader)
.collect::<Result<Vec<_>, _>>()
.map_err(|e| format!("failed to read certs from {}: {}", path, e))
}
fn load_private_key(path: &str) -> Result<PrivateKeyDer<'static>, String> {
let file = File::open(path).map_err(|e| format!("failed to open key file {}: {}", path, e))?;
let mut reader = BufReader::new(file);
private_key(&mut reader)
.map_err(|e| format!("failed to read private key from {}: {}", path, e))?
.ok_or_else(|| format!("no supported private key found in {}", path))
}
fn load_certificates_from_pem(pem: &str) -> Result<Vec<CertificateDer<'static>>, String> {
let mut reader = Cursor::new(pem.as_bytes());
certs(&mut reader)
.collect::<Result<Vec<_>, _>>()
.map_err(|e| format!("failed to read certs from PEM payload: {}", e))
}
fn load_private_key_from_pem(pem: &str) -> Result<PrivateKeyDer<'static>, String> {
let mut reader = Cursor::new(pem.as_bytes());
private_key(&mut reader)
.map_err(|e| format!("failed to read private key from PEM payload: {}", e))?
.ok_or_else(|| "no supported private key found in PEM payload".to_string())
}
fn infer_server_name(address: &str) -> &str {
address
.trim()
.trim_start_matches('[')
.split(']')
.next()
.unwrap_or(address)
.split(':')
.next()
.unwrap_or(address)
}
fn resolve_address(address: &str) -> Result<SocketAddr, String> {
address
.to_socket_addrs()
.map_err(|e| format!("failed to resolve {}: {}", address, e))?
.next()
.ok_or_else(|| format!("failed to resolve {}: no socket addresses returned", address))
}
fn connect_tcp(address: &str) -> Result<TcpStream, String> {
let socket_addr = resolve_address(address)?;
info!(
"Opening TCP connection to {} (resolved={}) with timeout {:?}",
address, socket_addr, TCP_CONNECT_TIMEOUT
);
let tcp_stream = TcpStream::connect_timeout(&socket_addr, TCP_CONNECT_TIMEOUT)
.map_err(|e| format!("failed to connect to {}: {}", address, e))?;
tcp_stream
.set_read_timeout(Some(SOCKET_IO_TIMEOUT))
.map_err(|e| format!("failed to set read timeout on {}: {}", address, e))?;
tcp_stream
.set_write_timeout(Some(SOCKET_IO_TIMEOUT))
.map_err(|e| format!("failed to set write timeout on {}: {}", address, e))?;
Ok(tcp_stream)
}
pub fn connect_mtls(
address: &str,
server_name: &str,
ca_cert_path: &str,
client_cert_path: &str,
client_key_path: &str,
) -> Result<TransportStream, String> {
info!(
"Connecting mTLS from file paths to {} using server_name={}",
address, server_name
);
let mut root_store = RootCertStore::empty();
let ca_certificates = load_certificates(ca_cert_path)?;
info!(
"Loaded {} CA certificate(s) from {}",
ca_certificates.len(),
ca_cert_path
);
for certificate in ca_certificates {
root_store
.add(certificate)
.map_err(|e| format!("failed to add CA certificate from {}: {}", ca_cert_path, e))?;
}
let client_certificates = load_certificates(client_cert_path)?;
info!(
"Loaded {} client certificate(s) from {}",
client_certificates.len(),
client_cert_path
);
let client_key = load_private_key(client_key_path)?;
info!("Loaded client private key from {}", client_key_path);
let tls_config = ClientConfig::builder()
.with_root_certificates(root_store)
.with_client_auth_cert(client_certificates, client_key)
.map_err(|e| format!("failed to configure mTLS client: {}", e))?;
info!("Constructed rustls client config for {}", address);
let tcp_stream = connect_tcp(address)?;
let resolved_server_name = if server_name.trim().is_empty() {
infer_server_name(address).to_string()
} else {
server_name.trim().to_string()
};
let server_name = ServerName::try_from(resolved_server_name.clone())
.map_err(|_| format!("invalid TLS server name: {}", resolved_server_name))?;
let mut tls_stream = StreamOwned::new(
ClientConnection::new(Arc::new(tls_config), server_name)
.map_err(|e| format!("failed to create TLS client: {}", e))?,
tcp_stream,
);
info!("Starting mTLS handshake for {}", address);
while tls_stream.conn.is_handshaking() {
tls_stream
.conn
.complete_io(&mut tls_stream.sock)
.map_err(|e| format!("TLS handshake failed: {}", e))?;
}
info!("mTLS handshake completed successfully for {}", address);
Ok(TransportStream::Mtls(tls_stream))
}
pub fn connect_mtls_from_pem(
address: &str,
server_name: &str,
ca_cert_pem: &str,
client_cert_pem: &str,
client_key_pem: &str,
) -> Result<TransportStream, String> {
info!(
"Connecting mTLS from in-memory PEM payloads to {} using server_name={}",
address, server_name
);
let mut root_store = RootCertStore::empty();
let ca_certificates = load_certificates_from_pem(ca_cert_pem)?;
info!(
"Loaded {} CA certificate(s) from enrollment payload",
ca_certificates.len()
);
for certificate in ca_certificates {
root_store
.add(certificate)
.map_err(|e| format!("failed to add CA certificate from PEM payload: {}", e))?;
}
let client_certificates = load_certificates_from_pem(client_cert_pem)?;
info!(
"Loaded {} client certificate(s) from enrollment payload",
client_certificates.len()
);
let client_key = load_private_key_from_pem(client_key_pem)?;
info!("Loaded client private key from enrollment payload");
let tls_config = ClientConfig::builder()
.with_root_certificates(root_store)
.with_client_auth_cert(client_certificates, client_key)
.map_err(|e| format!("failed to configure mTLS client: {}", e))?;
info!("Constructed rustls client config for {}", address);
let tcp_stream = connect_tcp(address)?;
let resolved_server_name = if server_name.trim().is_empty() {
infer_server_name(address).to_string()
} else {
server_name.trim().to_string()
};
let server_name = ServerName::try_from(resolved_server_name.clone())
.map_err(|_| format!("invalid TLS server name: {}", resolved_server_name))?;
let mut tls_stream = StreamOwned::new(
ClientConnection::new(Arc::new(tls_config), server_name)
.map_err(|e| format!("failed to create TLS client: {}", e))?,
tcp_stream,
);
info!("Starting mTLS handshake for {}", address);
while tls_stream.conn.is_handshaking() {
tls_stream
.conn
.complete_io(&mut tls_stream.sock)
.map_err(|e| format!("TLS handshake failed: {}", e))?;
}
info!("mTLS handshake completed successfully for {}", address);
Ok(TransportStream::Mtls(tls_stream))
}

228
src/tcp/tls/enrollment.rs Normal file
View File

@@ -0,0 +1,228 @@
use rcgen::{CertificateParams, DistinguishedName, DnType, KeyPair, PKCS_RSA_SHA256};
use log::info;
use reqwest::blocking::Client;
use serde::Deserialize;
use uuid::Uuid;
use super::connector::connect_mtls_from_pem;
use crate::tcp::transport::TransportStream;
#[derive(Deserialize)]
struct EnrollmentResponse {
#[serde(rename = "signedCert")]
signed_cert: String,
ca0: String,
}
struct EnrollmentConfig {
server_port: String,
enroll_path: String,
}
fn extract_tag_value(xml: &str, tag_name: &str) -> Option<String> {
let open_tag = format!("<{}>", tag_name);
let close_tag = format!("</{}>", tag_name);
let start = xml.find(&open_tag)? + open_tag.len();
let end = xml[start..].find(&close_tag)? + start;
Some(xml[start..end].trim().to_string())
}
fn wrap_pem_body(base64_body: &str, begin: &str, end: &str) -> String {
let mut wrapped = String::new();
let normalized = base64_body.trim().replace(['\r', '\n'], "");
wrapped.push_str(begin);
wrapped.push('\n');
for chunk in normalized.as_bytes().chunks(64) {
wrapped.push_str(std::str::from_utf8(chunk).unwrap_or_default());
wrapped.push('\n');
}
wrapped.push_str(end);
wrapped.push('\n');
wrapped
}
fn enrollment_http_client() -> Result<Client, String> {
Client::builder()
.danger_accept_invalid_certs(true)
.build()
.map_err(|e| format!("failed to build enrollment HTTP client: {}", e))
}
fn response_error_details(response: reqwest::blocking::Response) -> String {
let status = response.status();
match response.text() {
Ok(body) => {
let trimmed = body.trim();
if trimmed.is_empty() {
status.to_string()
} else {
format!("{}: {}", status, trimmed)
}
}
Err(_) => status.to_string(),
}
}
fn fetch_enrollment_config(host: &str, enroll_port: &str) -> Result<EnrollmentConfig, String> {
let url = format!(
"https://{}:{}/Marti/api/tls/config",
host.trim(),
enroll_port.trim()
);
info!("Fetching TAK enrollment config from {}", url);
let response = enrollment_http_client()?
.get(&url)
.send()
.map_err(|e| format!("failed to fetch {}: {}", url, e))?;
if !response.status().is_success() {
return Err(format!(
"failed to fetch {}: {}",
url,
response_error_details(response)
));
}
let response_text = response
.text()
.map_err(|e| format!("failed to read config response from {}: {}", url, e))?;
let server_port = extract_tag_value(&response_text, "serverPort")
.ok_or_else(|| "missing serverPort in /Marti/api/tls/config response".to_string())?;
let enroll_path = extract_tag_value(&response_text, "enrollPath")
.ok_or_else(|| "missing enrollPath in /Marti/api/tls/config response".to_string())?;
info!(
"Enrollment config received: server_port={} enroll_path={}",
server_port, enroll_path
);
Ok(EnrollmentConfig {
server_port,
enroll_path,
})
}
fn enroll_client_certificate(
host: &str,
enroll_port: &str,
enroll_path: &str,
username: &str,
password: &str,
client_uid: &str,
) -> Result<(String, String, String), String> {
info!(
"Generating RSA client keypair and CSR for enrolled TAK client {}",
client_uid
);
let key_pair = KeyPair::generate_for(&PKCS_RSA_SHA256)
.map_err(|e| format!("failed to generate client keypair: {}", e))?;
let mut distinguished_name = DistinguishedName::new();
distinguished_name.push(DnType::CommonName, client_uid);
distinguished_name.push(DnType::OrganizationName, "ArmaTAK");
distinguished_name.push(DnType::OrganizationalUnitName, "ArmaTAK Session");
let mut params = CertificateParams::new(vec![])
.map_err(|e| format!("failed to create CSR params: {}", e))?;
params.distinguished_name = distinguished_name;
let csr = params
.serialize_request(&key_pair)
.map_err(|e| format!("failed to generate CSR: {}", e))?;
let csr_der = csr.der().as_ref().to_vec();
let url = format!(
"https://{}:{}{}?clientUid={}",
host.trim(),
enroll_port.trim(),
enroll_path.trim(),
client_uid.trim()
);
info!(
"Submitting client certificate enrollment request for {} to {}",
client_uid, url
);
let response = enrollment_http_client()?
.post(&url)
.basic_auth(username.trim(), Some(password.to_string()))
.header("Accept", "application/json")
.header("Content-Type", "application/pkcs10")
.body(csr_der)
.send()
.map_err(|e| format!("failed to enroll client certificate at {}: {}", url, e))?;
if !response.status().is_success() {
return Err(format!(
"failed to enroll client certificate at {}: {}",
url,
response_error_details(response)
));
}
let enrollment: EnrollmentResponse = response
.json()
.map_err(|e| format!("failed to parse enrollment response: {}", e))?;
info!(
"Enrollment response parsed successfully for {} (signed_cert_len={}, ca_len={})",
client_uid,
enrollment.signed_cert.len(),
enrollment.ca0.len()
);
let cert_pem = wrap_pem_body(
&enrollment.signed_cert,
"-----BEGIN CERTIFICATE-----",
"-----END CERTIFICATE-----",
);
let key_pem = key_pair.serialize_pem();
Ok((enrollment.ca0, cert_pem, key_pem))
}
pub fn enroll_and_connect(
host: &str,
server_name: &str,
enroll_port: &str,
username: &str,
password: &str,
client_uid: &str,
) -> Result<TransportStream, String> {
let normalized_client_uid = if client_uid.trim().is_empty() {
format!("armatak-{}", Uuid::new_v4())
} else {
client_uid.trim().to_string()
};
info!(
"Starting enroll_and_connect for host={} enroll_port={} server_name={} client_uid={}",
host,
enroll_port,
server_name,
normalized_client_uid
);
let enrollment_config = fetch_enrollment_config(host, enroll_port)?;
let (ca_cert_pem, client_cert_pem, client_key_pem) = enroll_client_certificate(
host,
enroll_port,
&enrollment_config.enroll_path,
username,
password,
&normalized_client_uid,
)?;
connect_mtls_from_pem(
&format!("{}:{}", host.trim(), enrollment_config.server_port.trim()),
if server_name.trim().is_empty() {
host.trim()
} else {
server_name.trim()
},
&ca_cert_pem,
&client_cert_pem,
&client_key_pem,
)
}

5
src/tcp/tls/mod.rs Normal file
View File

@@ -0,0 +1,5 @@
mod connector;
mod enrollment;
pub use connector::connect_mtls;
pub use enrollment::enroll_and_connect;

90
src/tcp/transport.rs Normal file
View File

@@ -0,0 +1,90 @@
use log::info;
use rustls::{ClientConnection, StreamOwned};
use std::io::Write;
use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
use std::time::Duration;
use super::config::ConnectionConfig;
use super::tls::{connect_mtls, enroll_and_connect};
const TCP_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
const SOCKET_IO_TIMEOUT: Duration = Duration::from_secs(10);
pub enum TransportStream {
Plain(TcpStream),
Mtls(StreamOwned<ClientConnection, TcpStream>),
}
impl TransportStream {
pub fn write_message(&mut self, message: &[u8]) -> Result<(), std::io::Error> {
match self {
Self::Plain(stream) => {
stream.write_all(message)?;
stream.flush()
}
Self::Mtls(stream) => {
stream.write_all(message)?;
stream.flush()
}
}
}
}
fn connect_plain(address: &str) -> Result<TransportStream, String> {
let socket_addr: SocketAddr = address
.to_socket_addrs()
.map_err(|e| format!("failed to resolve {}: {}", address, e))?
.next()
.ok_or_else(|| format!("failed to resolve {}: no socket addresses returned", address))?;
info!(
"Opening plain TCP connection to {} (resolved={}) with timeout {:?}",
address, socket_addr, TCP_CONNECT_TIMEOUT
);
let stream = TcpStream::connect_timeout(&socket_addr, TCP_CONNECT_TIMEOUT)
.map_err(|e| format!("failed to connect to {}: {}", address, e))?;
stream
.set_read_timeout(Some(SOCKET_IO_TIMEOUT))
.map_err(|e| format!("failed to set read timeout on {}: {}", address, e))?;
stream
.set_write_timeout(Some(SOCKET_IO_TIMEOUT))
.map_err(|e| format!("failed to set write timeout on {}: {}", address, e))?;
Ok(TransportStream::Plain(stream))
}
pub fn connect_stream(config: &ConnectionConfig) -> Result<TransportStream, String> {
info!("connect_stream invoked for {}", config.describe());
match config {
ConnectionConfig::Plain { address } => connect_plain(address),
ConnectionConfig::Mtls {
address,
server_name,
ca_cert_path,
client_cert_path,
client_key_path,
} => connect_mtls(
address,
server_name,
ca_cert_path,
client_cert_path,
client_key_path,
),
ConnectionConfig::EnrollMtls {
host,
server_name,
enroll_port,
username,
password,
client_uid,
} => enroll_and_connect(
host,
server_name,
enroll_port,
username,
password,
client_uid,
),
}
}

View File

@@ -1,156 +0,0 @@
use arma_rs::Context;
use lazy_static::lazy_static;
use log::info;
use std::io::Write;
use std::net::TcpStream;
use std::sync::mpsc::{self, Receiver, Sender};
use std::sync::{Arc, Mutex};
use std::thread;
use crate::cot;
pub enum TcpCommand {
SendMessage(String, Context),
Stop,
}
pub struct TcpClient {
pub(crate) tx: Sender<TcpCommand>,
}
impl TcpClient {
pub fn start(&self, address: String, rx: Receiver<TcpCommand>, ctx: Context) {
if let Some(ref client) = *TCP_CLIENT.lock().unwrap() {
client.stop();
}
let connection = Arc::new(Mutex::new(None));
let connection_clone = Arc::clone(&connection);
thread::spawn(move || {
let mut running = true;
let tcp_thread = thread::spawn(move || match TcpStream::connect(&address) {
Ok(stream) => {
let _ = ctx.callback_data("TCP SOCKET", "Connected to TCP Server", address);
*connection_clone.lock().unwrap() = Some(stream);
}
Err(e) => {
let _ = ctx.callback_data(
"TCP SOCKET ERROR",
"TAK Socket connection failed",
e.to_string(),
);
info!("Failed to connect to TCP server: {}", e);
}
});
while running {
match rx.recv() {
Ok(TcpCommand::SendMessage(message, context)) => {
if let Some(mut stream) = connection.lock().unwrap().as_ref() {
if let Err(e) = stream.write_all(message.as_bytes()) {
info!("Failed to send message: {}", e);
let _ = context.callback_data(
"TCP SOCKET ERROR",
"TAK Socket disconnected",
e.to_string(),
);
running = false;
}
} else {
let _ = context.callback_null(
"TCP SOCKET ERROR",
"TAK Socket is not active",
);
}
}
Ok(TcpCommand::Stop) => {
running = false;
info!("Stopping TCP client.");
}
Err(error) => {
info!("Error receiving command: {}", error.to_string());
}
}
}
tcp_thread.join().unwrap();
});
}
pub fn send_payload(&self, context: Context, payload: String) {
let tx = self.tx.clone();
thread::spawn(move || {
tx.send(TcpCommand::SendMessage(payload, context)).unwrap();
});
}
pub fn stop(&self) {
let tx = self.tx.clone();
thread::spawn(move || {
tx.send(TcpCommand::Stop).unwrap();
});
}
}
lazy_static! {
static ref TCP_CLIENT: Arc<Mutex<Option<TcpClient>>> = Arc::new(Mutex::new(None));
}
pub fn start(ctx: Context, address: String) -> &'static str {
let (tx, rx): (Sender<TcpCommand>, Receiver<TcpCommand>) = mpsc::channel();
let client = TcpClient { tx };
client.start(address, rx, ctx);
let mut client_guard = TCP_CLIENT.lock().unwrap();
*client_guard = Some(client);
"Starting TCP Client"
}
pub fn send_payload(ctx: Context, payload: String) -> &'static str {
if let Some(ref client) = *TCP_CLIENT.lock().unwrap() {
client.send_payload(ctx, payload);
} else {
let _ = ctx.callback_null("TCP SOCKET ERROR", "TCP Client is not running");
info!("TCP client is not running.");
}
"Sending payload to TCP server"
}
pub fn send_eud_cot(ctx: Context, cursor_over_time: cot::eud::EudCoTPayload) -> &'static str {
let payload = cursor_over_time.to_cot().convert_to_xml();
send_payload(ctx, payload);
"Sending End User Device Cursor Over Time to TCP server"
}
pub fn send_marker_cot(ctx: Context, cursor_over_time: cot::nato::MarkerCoTPayload) -> &'static str {
let payload = cursor_over_time.to_cot().convert_to_xml();
send_payload(ctx, payload);
"Sending Marker Cursor Over Time to TCP server"
}
pub fn send_digital_pointer_cot(ctx: Context, cursor_over_time: cot::digital_pointer::DigitalPointerPayload) -> &'static str {
let payload = cursor_over_time.to_cot().convert_to_xml();
send_payload(ctx, payload);
"Sending Digital Pointer Cursor Over Time to TCP server"
}
pub fn stop(ctx: Context) -> &'static str {
if let Some(ref client) = *TCP_CLIENT.lock().unwrap() {
client.stop();
let _ = ctx.callback_null("TCP SOCKET", "TCP client stopped");
} else {
let _ = ctx.callback_null("TCP SOCKET ERROR", "TCP client is not running");
}
"Stopping TCP Client"
}

View File

@@ -1,25 +1,25 @@
#[cfg(test)]
mod tests {
use crate::init;
use uuid::Uuid;
use std::vec;
use crate::init;
use std::vec;
use uuid::Uuid;
#[test]
fn uuid_output_is_uuid4_identifier() {
let extension = init().testing();
let (output, _) = extension.call("uuid", None);
#[test]
fn uuid_output_is_uuid4_identifier() {
let extension = init().testing();
let (output, _) = extension.call("uuid", None);
let validation = Uuid::parse_str(&output);
let validation = Uuid::parse_str(&output);
assert!(validation.is_ok())
}
assert!(validation.is_ok())
}
#[test]
fn uuid_output_throws_if_passed_args() {
let extension = init().testing();
let args: Vec<String> = vec![1.to_string(),2.to_string()];
let (output, _) = extension.call("uuid", Some(args));
#[test]
fn uuid_output_throws_if_passed_args() {
let extension = init().testing();
let args: Vec<String> = vec![1.to_string(), 2.to_string()];
let (output, _) = extension.call("uuid", Some(args));
assert_eq!(output,"")
}
assert_eq!(output, "")
}
}

View File

@@ -9,116 +9,119 @@ use std::thread;
use crate::cot;
pub enum UdpCommand {
SendMessage(String, Context),
Stop,
SendMessage(String, Context),
Stop,
}
pub struct UdpClient {
pub(crate) tx: Sender<UdpCommand>,
pub(crate) tx: Sender<UdpCommand>,
}
impl UdpClient {
pub fn start(&self, address: String, rx: Receiver<UdpCommand>, ctx: Context) {
if let Some(ref client) = *UDP_CLIENT.lock().unwrap() {
client.stop();
pub fn start(&self, address: String, rx: Receiver<UdpCommand>, ctx: Context) {
if let Some(ref client) = *UDP_CLIENT.lock().unwrap() {
client.stop();
}
thread::spawn(move || {
let socket = match UdpSocket::bind("0.0.0.0:0") {
Ok(s) => s,
Err(e) => {
let _ = ctx.callback_data(
"UDP SOCKET ERROR",
"Failed to bind UDP socket",
e.to_string(),
);
info!("Failed to bind UDP socket: {}", e);
return;
}
};
let _ = ctx.callback_data("UDP SOCKET", "EUD Connected", address.clone());
let mut running = true;
while running {
match rx.recv() {
Ok(UdpCommand::SendMessage(message, context)) => {
if let Err(e) = socket.send_to(message.as_bytes(), &address) {
info!("Failed to send UDP message: {}", e);
let _ = context.callback_data(
"UDP SOCKET ERROR",
"Failed to send UDP message",
e.to_string(),
);
}
}
Ok(UdpCommand::Stop) => {
running = false;
info!("Stopping UDP client.");
}
Err(error) => {
info!("Error receiving command: {}", error.to_string());
}
}
}
});
}
thread::spawn(move || {
let socket = match UdpSocket::bind("0.0.0.0:0") {
Ok(s) => s,
Err(e) => {
let _ = ctx.callback_data(
"UDP SOCKET ERROR",
"Failed to bind UDP socket",
e.to_string(),
);
info!("Failed to bind UDP socket: {}", e);
return;
}
};
pub fn send_payload(&self, context: Context, payload: String) {
let tx = self.tx.clone();
thread::spawn(move || {
tx.send(UdpCommand::SendMessage(payload, context)).unwrap();
});
}
let _ = ctx.callback_data("UDP SOCKET", "EUD Connected", address.clone());
let mut running = true;
while running {
match rx.recv() {
Ok(UdpCommand::SendMessage(message, context)) => {
if let Err(e) = socket.send_to(message.as_bytes(), &address) {
info!("Failed to send UDP message: {}", e);
let _ = context.callback_data(
"UDP SOCKET ERROR",
"Failed to send UDP message",
e.to_string(),
);
}
}
Ok(UdpCommand::Stop) => {
running = false;
info!("Stopping UDP client.");
}
Err(error) => {
info!("Error receiving command: {}", error.to_string());
}
}
}
});
}
pub fn send_payload(&self, context: Context, payload: String) {
let tx = self.tx.clone();
thread::spawn(move || {
tx.send(UdpCommand::SendMessage(payload, context)).unwrap();
});
}
pub fn stop(&self) {
let tx = self.tx.clone();
thread::spawn(move || {
tx.send(UdpCommand::Stop).unwrap();
});
}
pub fn stop(&self) {
let tx = self.tx.clone();
thread::spawn(move || {
tx.send(UdpCommand::Stop).unwrap();
});
}
}
lazy_static! {
static ref UDP_CLIENT: Arc<Mutex<Option<UdpClient>>> = Arc::new(Mutex::new(None));
static ref UDP_CLIENT: Arc<Mutex<Option<UdpClient>>> = Arc::new(Mutex::new(None));
}
pub fn start(ctx: Context, address: String) -> &'static str {
let (tx, rx): (Sender<UdpCommand>, Receiver<UdpCommand>) = mpsc::channel();
let (tx, rx): (Sender<UdpCommand>, Receiver<UdpCommand>) = mpsc::channel();
let client = UdpClient { tx };
client.start(address, rx, ctx);
let client = UdpClient { tx };
client.start(address, rx, ctx);
let mut client_guard = UDP_CLIENT.lock().unwrap();
*client_guard = Some(client);
let mut client_guard = UDP_CLIENT.lock().unwrap();
*client_guard = Some(client);
"Starting UDP Client"
"Starting UDP Client"
}
pub fn send_payload(ctx: Context, payload: String) -> &'static str {
if let Some(ref client) = *UDP_CLIENT.lock().unwrap() {
client.send_payload(ctx, payload);
} else {
let _ = ctx.callback_null("UDP SOCKET ERROR", "UDP Socket is not running");
}
if let Some(ref client) = *UDP_CLIENT.lock().unwrap() {
client.send_payload(ctx, payload);
} else {
let _ = ctx.callback_null("UDP SOCKET ERROR", "UDP Socket is not running");
}
"Sending payload to UDP server"
"Sending payload to UDP server"
}
pub fn send_gps_cot(ctx: Context, cursor_over_time: cot::gps::ExternalPositionPayload) -> &'static str {
let payload = cursor_over_time.to_cot().convert_to_xml();
send_payload(ctx, payload);
pub fn send_gps_cot(
ctx: Context,
cursor_over_time: cot::gps::ExternalPositionPayload,
) -> &'static str {
let payload = cursor_over_time.to_cot().convert_to_xml();
send_payload(ctx, payload);
"Sending GPS Cursor Over Time to UDP server"
"Sending GPS Cursor Over Time to UDP server"
}
pub fn stop(ctx: Context) -> &'static str {
if let Some(ref client) = *UDP_CLIENT.lock().unwrap() {
client.stop();
let _ = ctx.callback_null("UDP SOCKET", "EUD Disconnected");
} else {
let _ = ctx.callback_null("UDP SOCKET ERROR", "UDP Socket is not running");
}
if let Some(ref client) = *UDP_CLIENT.lock().unwrap() {
client.stop();
let _ = ctx.callback_null("UDP SOCKET", "EUD Disconnected");
} else {
let _ = ctx.callback_null("UDP SOCKET ERROR", "UDP Socket is not running");
}
"Stopping UDP Client"
"Stopping UDP Client"
}

View File

@@ -1,23 +1,23 @@
use std::net::{IpAddr, UdpSocket};
pub fn get_local_address() -> String {
fn get_local_ip() -> Result<IpAddr, String> {
let socket = UdpSocket::bind("0.0.0.0:0").map_err(|e| e.to_string())?;
socket.connect("8.8.8.8:80").map_err(|e| e.to_string())?;
socket
.local_addr()
.map(|addr| addr.ip())
.map_err(|e| e.to_string())
}
fn get_local_ip() -> Result<IpAddr, String> {
let socket = UdpSocket::bind("0.0.0.0:0").map_err(|e| e.to_string())?;
socket.connect("8.8.8.8:80").map_err(|e| e.to_string())?;
socket
.local_addr()
.map(|addr| addr.ip())
.map_err(|e| e.to_string())
}
let parsed_data = get_local_ip();
let parsed_data = get_local_ip();
match parsed_data {
Ok(ip) => {
return format!("ws://{}:4152", ip.to_string());
}
Err(_) => {
return "not provided".to_string();
}
}
match parsed_data {
Ok(ip) => {
return format!("ws://{}:4152", ip.to_string());
}
Err(_) => {
return "not provided".to_string();
}
}
}

View File

@@ -2,11 +2,11 @@ use crate::structs::LogPayload;
use log::{error, info, warn};
pub fn log_info(data: LogPayload) -> String {
match data.status.as_str() {
"info" => info!("{}", data.message),
"warn" => warn!("{}", data.message),
"error" => error!("{}", data.message),
_ => error!("{}", "Wrong log call"),
}
"logged".to_string()
}
match data.status.as_str() {
"info" => info!("{}", data.message),
"warn" => warn!("{}", data.message),
"error" => error!("{}", data.message),
_ => error!("{}", "Wrong log call"),
}
"logged".to_string()
}

View File

@@ -1,3 +1,3 @@
pub mod uuid;
pub mod address;
pub mod log;
pub mod log;
pub mod uuid;

View File

@@ -1,7 +1,7 @@
pub fn get_uuid() -> String {
use uuid::Uuid;
use uuid::Uuid;
let id = Uuid::new_v4().to_string();
let id = Uuid::new_v4().to_string();
return id;
return id;
}

View File

@@ -16,7 +16,13 @@ lazy_static! {
#[cfg(target_os = "windows")]
const CREATE_NO_WINDOW: u32 = 0x08000000;
fn build_rtsp_url(address: &str, port: &str, stream_path: &str, username: &str, password: &str) -> String {
fn build_rtsp_url(
address: &str,
port: &str,
stream_path: &str,
username: &str,
password: &str,
) -> String {
if username.is_empty() || password.is_empty() {
format!("rtsp://{}:{}/{}", address, port, stream_path)
} else {
@@ -28,20 +34,22 @@ fn build_rtsp_url(address: &str, port: &str, stream_path: &str, username: &str,
}
#[cfg(any(target_os = "windows", target_os = "linux"))]
fn spawn_ffmpeg(
rtsp_url: String,
stop_rx: Receiver<()>,
status_tx: Sender<Result<(), String>>,
) {
fn spawn_ffmpeg(rtsp_url: String, stop_rx: Receiver<()>, status_tx: Sender<Result<(), String>>) {
thread::spawn(move || {
let mut cmd = Command::new("ffmpeg");
cmd.args(&[
"-f","x11grab",
"-framerate","30",
"-video_size","1920x1080",
"-i" ,":0",
"-f","rtsp",
"-rtsp_transport","tcp",
"-f",
"x11grab",
"-framerate",
"30",
"-video_size",
"1920x1080",
"-i",
":0",
"-f",
"rtsp",
"-rtsp_transport",
"tcp",
&rtsp_url,
]);

1
vendor/arma-rs-proc/.cargo-ok vendored Normal file
View File

@@ -0,0 +1 @@
{"v":1}

View File

@@ -0,0 +1,6 @@
{
"git": {
"sha1": "adfc323899e58f20c05ebb37595d5ca4fd09367f"
},
"path_in_vcs": "arma-rs-proc"
}

42
vendor/arma-rs-proc/Cargo.toml vendored Normal file
View File

@@ -0,0 +1,42 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
name = "arma-rs-proc"
version = "1.11.1"
authors = ["Brett Mayson"]
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "proc macros for arma-rs"
readme = false
keywords = ["arma"]
license = "MIT"
repository = "https://github.com/brettmayson/arma-rs"
[lib]
name = "arma_rs_proc"
path = "src/lib.rs"
proc-macro = true
[dependencies.proc-macro2]
version = "1.0.92"
[dependencies.quote]
version = "1.0.37"
[dependencies.syn]
version = "2.0.90"
features = ["full"]

17
vendor/arma-rs-proc/Cargo.toml.orig generated vendored Normal file
View File

@@ -0,0 +1,17 @@
[package]
name = "arma-rs-proc"
description = "proc macros for arma-rs"
version = "1.11.1"
edition = "2021"
authors = ["Brett Mayson"]
repository = "https://github.com/brettmayson/arma-rs"
license = "MIT"
keywords = ["arma"]
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0.92"
quote = "1.0.37"
syn = { version = "2.0.90", features = ["full"] }

View File

@@ -0,0 +1,145 @@
use syn::{Error, Result};
use crate::derive::CombinedErrors;
pub struct ContainerAttributes {
pub transparent: Attribute<bool>,
pub default: Attribute<bool>,
}
impl Default for ContainerAttributes {
fn default() -> Self {
Self {
transparent: Attribute::new(false),
default: Attribute::new(false),
}
}
}
impl ParseAttr for ContainerAttributes {
fn parse_attr(&mut self, meta: syn::meta::ParseNestedMeta) -> Result<()> {
if meta.path.is_ident("transparent") {
return self.transparent.set(&meta, true);
}
if meta.path.is_ident("default") {
return self.default.set(&meta, true);
}
Err(meta.error(format!(
"unknown arma container attribute `{}`",
path_to_string(&meta.path)
)))
}
}
pub struct FieldAttributes {
pub default: Attribute<bool>,
pub from_str: Attribute<bool>,
pub to_string: Attribute<bool>,
}
impl Default for FieldAttributes {
fn default() -> Self {
Self {
default: Attribute::new(false),
from_str: Attribute::new(false),
to_string: Attribute::new(false),
}
}
}
impl ParseAttr for FieldAttributes {
fn parse_attr(&mut self, meta: syn::meta::ParseNestedMeta) -> Result<()> {
if meta.path.is_ident("default") {
return self.default.set(&meta, true);
}
if meta.path.is_ident("from_str") {
return self.from_str.set(&meta, true);
}
if meta.path.is_ident("to_string") {
return self.to_string.set(&meta, true);
}
Err(meta.error(format!(
"unknown arma field attribute `{}`",
path_to_string(&meta.path)
)))
}
}
pub trait ParseAttr {
fn parse_attr(&mut self, meta: syn::meta::ParseNestedMeta) -> Result<()>;
}
pub fn parse_attributes<T>(errors: &mut CombinedErrors, attrs: &[syn::Attribute]) -> T
where
T: ParseAttr + Default + Sized,
{
attrs.iter().fold(T::default(), |mut attributes, attr| {
if !attr.path().is_ident("arma") {
return attributes;
}
let result = attr.parse_nested_meta(|meta| {
if let Err(err) = attributes.parse_attr(meta) {
errors.add(err);
}
Ok(())
});
if let Err(err) = result {
errors.add(err);
}
attributes
})
}
pub struct Attribute<T> {
value: T,
path: Option<syn::Path>,
}
impl<T> Attribute<T> {
fn new(default: T) -> Self {
Self {
value: default,
path: None,
}
}
fn set(&mut self, meta: &syn::meta::ParseNestedMeta, value: T) -> Result<()> {
if self.is_set() {
return Err(meta.error(format!(
"duplicate arma attribute `{}`",
path_to_string(&meta.path)
)));
}
self.value = value;
self.path = Some(meta.path.clone());
Ok(())
}
pub fn is_set(&self) -> bool {
self.path.is_some()
}
pub fn value(&self) -> &T {
&self.value
}
#[must_use]
pub fn error(&self, message: &str) -> Error {
Error::new_spanned(self.path.as_ref().unwrap(), message)
}
}
fn path_to_string(path: &syn::Path) -> String {
path.segments
.iter()
.map(|s| s.ident.to_string())
.collect::<Vec<_>>()
.join("::")
}

124
vendor/arma-rs-proc/src/derive/data.rs vendored Normal file
View File

@@ -0,0 +1,124 @@
use proc_macro2::{Span, TokenStream};
use quote::ToTokens;
use syn::{Error, Result};
use crate::derive::{
attributes::{parse_attributes, ContainerAttributes, FieldAttributes},
r#struct, CombinedErrors,
};
pub struct ContainerData {
pub attributes: ContainerAttributes,
pub ident: syn::Ident,
pub generics: syn::Generics,
pub data: Data,
}
pub enum Data {
Struct(StructData),
}
pub enum StructData {
Map(Vec<FieldNamed>),
Tuple(Vec<FieldUnnamed>),
NewType(FieldUnnamed),
}
impl ContainerData {
pub fn from_input(errors: &mut CombinedErrors, input: syn::DeriveInput) -> Result<Self> {
let data = match input.data {
syn::Data::Struct(data) => Data::Struct(StructData::new(errors, data)?),
syn::Data::Enum(_) => Err(Error::new(Span::call_site(), "enums aren't supported"))?,
syn::Data::Union(_) => Err(Error::new(Span::call_site(), "unions aren't supported"))?,
};
let attributes = parse_attributes::<ContainerAttributes>(errors, &input.attrs);
Ok(Self {
attributes,
ident: input.ident,
generics: input.generics,
data,
})
}
pub fn validate_attributes(&self, errors: &mut CombinedErrors) {
match self.data {
Data::Struct(ref data) => {
r#struct::validate_attributes(errors, &self.attributes, data);
}
}
}
pub fn impl_into_arma(&self) -> TokenStream {
match self.data {
Data::Struct(ref data) => r#struct::impl_into_arma(&self.attributes, data),
}
}
pub fn impl_from_arma(&self) -> TokenStream {
match self.data {
Data::Struct(ref data) => r#struct::impl_from_arma(&self.attributes, data),
}
}
}
pub struct FieldNamed {
pub attributes: FieldAttributes,
pub ident: syn::Ident,
pub name: String,
pub _ty: syn::Type,
}
pub struct FieldUnnamed {
pub attributes: FieldAttributes,
pub index: syn::Index,
pub ty: syn::Type,
}
impl FieldNamed {
pub fn new(errors: &mut CombinedErrors, field: syn::Field) -> Self {
let ident = field.ident.unwrap();
let name = ident.to_string();
Self {
attributes: parse_attributes::<FieldAttributes>(errors, &field.attrs),
ident,
name,
_ty: field.ty,
}
}
}
impl FieldUnnamed {
pub fn new(errors: &mut CombinedErrors, field: syn::Field, index: usize) -> Self {
Self {
attributes: parse_attributes::<FieldAttributes>(errors, &field.attrs),
index: syn::Index::from(index),
ty: field.ty,
}
}
}
pub trait Field {
fn attributes(&self) -> &FieldAttributes;
fn token(&self) -> TokenStream;
}
impl Field for FieldNamed {
fn attributes(&self) -> &FieldAttributes {
&self.attributes
}
fn token(&self) -> TokenStream {
self.ident.to_token_stream()
}
}
impl Field for FieldUnnamed {
fn attributes(&self) -> &FieldAttributes {
&self.attributes
}
fn token(&self) -> TokenStream {
self.index.to_token_stream()
}
}

72
vendor/arma-rs-proc/src/derive/mod.rs vendored Normal file
View File

@@ -0,0 +1,72 @@
mod attributes;
mod data;
mod r#struct;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{DeriveInput, Result};
use data::ContainerData;
pub fn generate_from_arma(input: DeriveInput) -> Result<TokenStream> {
let container = parse_container_data(input)?;
let body = container.impl_from_arma();
let ident = container.ident;
let (impl_generics, ty_generics, where_clause) = container.generics.split_for_impl();
Ok(quote! {
#[automatically_derived]
impl #impl_generics arma_rs::FromArma for #ident #ty_generics #where_clause {
fn from_arma(func_input: String) -> Result<Self, arma_rs::FromArmaError> {
#body
}
}
})
}
pub fn generate_into_arma(input: DeriveInput) -> Result<TokenStream> {
let container = parse_container_data(input)?;
let body = container.impl_into_arma();
let ident = container.ident;
let (impl_generics, ty_generics, where_clause) = container.generics.split_for_impl();
Ok(quote! {
#[automatically_derived]
impl #impl_generics arma_rs::IntoArma for #ident #ty_generics #where_clause {
fn to_arma(&self) -> arma_rs::Value {
#body
}
}
})
}
fn parse_container_data(input: DeriveInput) -> Result<ContainerData> {
let mut errors = CombinedErrors::new();
let container = ContainerData::from_input(&mut errors, input)?;
container.validate_attributes(&mut errors);
errors.into_result().and(Ok(container))
}
pub struct CombinedErrors {
root: Option<syn::Error>,
}
impl CombinedErrors {
fn new() -> Self {
Self { root: None }
}
pub fn add(&mut self, error: syn::Error) {
match &mut self.root {
Some(root) => root.combine(error),
None => self.root = Some(error),
}
}
pub fn into_result(self) -> Result<()> {
match self.root {
Some(error) => Err(error),
None => Ok(()),
}
}
}

View File

@@ -0,0 +1,162 @@
use proc_macro2::TokenStream;
use quote::quote;
use crate::derive::{
attributes::ContainerAttributes,
data::{Field, FieldNamed, FieldUnnamed, StructData},
};
pub fn impl_from_arma(attributes: &ContainerAttributes, data: &StructData) -> TokenStream {
// For simplicity sake we assume that theres no conflicts and everything has already been validated
match &data {
StructData::Map(fields) => map_struct(attributes, fields),
StructData::Tuple(fields) => tuple_struct(attributes, fields),
StructData::NewType(field) => newtype_struct(attributes, field),
}
}
fn map_struct(attributes: &ContainerAttributes, fields: &[FieldNamed]) -> TokenStream {
if *attributes.transparent.value() {
return newtype_struct(attributes, fields.first().unwrap());
}
let mut setup = TokenStream::new();
setup.extend(quote! {
let mut input_as_values = std::collections::HashMap::<String, arma_rs::Value>::default();
let input_pairs: Vec<(String, arma_rs::Value)> = FromArma::from_arma(func_input)?;
for (k, v) in input_pairs {
if input_as_values.insert(k.clone(), v).is_some() {
return Err(arma_rs::FromArmaError::DuplicateField(k));
}
}
});
if *attributes.default.value() {
setup.extend(quote! {
let container_default: Self = std::default::Default::default();
});
};
let field_bodies = fields.iter().map(|field| {
let (ident, name) = (&field.ident, &field.name);
let some_match = if *field.attributes.from_str.value() {
quote!(input_value
.to_string()
.parse()
.map_err(arma_rs::FromArmaError::custom)?)
} else {
quote!(arma_rs::FromArma::from_arma(input_value.to_string())?)
};
let none_match = if *field.attributes.default.value() {
quote!(std::default::Default::default())
} else if *attributes.default.value() {
quote!(container_default.#ident)
} else {
quote!(return Err(arma_rs::FromArmaError::MissingField(#name.to_string())))
};
quote! {
#ident: match input_as_values.remove(#name) {
Some(input_value) => #some_match,
None => #none_match,
}
}
});
let check_unknown = quote! {
if let Some(unknown) = input_as_values.keys().next() {
return Err(arma_rs::FromArmaError::UnknownField(unknown.clone()));
}
};
quote! {
#setup
let result = Self {
#(#field_bodies),*
};
#check_unknown
Ok(result)
}
}
fn tuple_struct(attributes: &ContainerAttributes, fields: &[FieldUnnamed]) -> TokenStream {
let mut setup = TokenStream::new();
setup.extend(quote! {
let input_as_values: Vec<arma_rs::Value> = arma_rs::FromArma::from_arma(func_input)?;
let mut input_as_values = input_as_values.into_iter();
});
if *attributes.default.value() {
setup.extend(quote! {
let container_default: Self = std::default::Default::default();
});
};
let expected_len = fields.len();
let field_bodies = fields.iter().map(|field| {
let index = &field.index;
let some_match = if *field.attributes.from_str.value() {
quote!(input_value
.to_string()
.parse()
.map_err(arma_rs::FromArmaError::custom)?)
} else {
quote!(arma_rs::FromArma::from_arma(input_value.to_string())?)
};
let none_match = if *field.attributes.default.value() {
quote!(std::default::Default::default())
} else if *attributes.default.value() {
quote!(container_default.#index)
} else {
quote!(return Err(arma_rs::FromArmaError::InvalidLength {
expected: #expected_len,
actual: #index,
}))
};
quote! {
match input_as_values.next() {
Some(input_value) => #some_match,
None => #none_match,
}
}
});
let check_unknown = quote! {
let remaining = input_as_values.len();
if remaining > 0 {
return Err(arma_rs::FromArmaError::InvalidLength {
expected: #expected_len,
actual: #expected_len + remaining,
});
}
};
quote! {
#setup
let result = Self (
#(#field_bodies),*
);
#check_unknown
Ok(result)
}
}
fn newtype_struct(_attributes: &ContainerAttributes, field: &impl Field) -> TokenStream {
let token = field.token();
let field_body = if *field.attributes().from_str.value() {
quote!(func_input.parse().map_err(arma_rs::FromArmaError::custom)?)
} else {
quote!(arma_rs::FromArma::from_arma(func_input)?)
};
quote! {
Ok(Self {
#token: #field_body
})
}
}

View File

@@ -0,0 +1,72 @@
use proc_macro2::TokenStream;
use quote::quote;
use crate::derive::{
attributes::ContainerAttributes,
data::{Field, FieldNamed, FieldUnnamed, StructData},
};
pub fn impl_into_arma(attributes: &ContainerAttributes, data: &StructData) -> TokenStream {
// For simplicity sake we assume that theres no conflicts and everything has already been validated
match &data {
StructData::Map(fields) => map_struct(attributes, fields),
StructData::Tuple(fields) => tuple_struct(attributes, fields),
StructData::NewType(field) => newtype_struct(attributes, field),
}
}
fn map_struct(attributes: &ContainerAttributes, fields: &[FieldNamed]) -> TokenStream {
if *attributes.transparent.value() {
return newtype_struct(attributes, fields.first().unwrap());
}
let field_bodies = fields.iter().map(|field| {
let (ident, name) = (&field.ident, &field.name);
let (key, value) = if *field.attributes.to_string.value() {
(quote!(#name.to_string()), quote!(self.#ident.to_string()))
} else {
(quote!(#name.to_string()), quote!(self.#ident))
};
quote!((#key, arma_rs::IntoArma::to_arma(&#value)))
});
quote! {
std::collections::HashMap::<String, arma_rs::Value>::from([
#(#field_bodies),*
]).to_arma()
}
}
fn tuple_struct(_attributes: &ContainerAttributes, fields: &[FieldUnnamed]) -> TokenStream {
let field_bodies = fields.iter().map(|field| {
let index = &field.index;
if *field.attributes.to_string.value() {
quote!(self.#index.to_string())
} else {
quote!(self.#index)
}
});
quote! {
Vec::<arma_rs::Value>::from([
#(arma_rs::IntoArma::to_arma(&#field_bodies)),*
]).to_arma()
}
}
fn newtype_struct(_attributes: &ContainerAttributes, field: &impl Field) -> TokenStream {
let token = field.token();
let field_body = if *field.attributes().to_string.value() {
quote!(self.#token.to_string())
} else {
quote!(self.#token)
};
quote! {
arma_rs::IntoArma::to_arma(&#field_body)
}
}

View File

@@ -0,0 +1,62 @@
mod from;
mod into;
mod validate;
use proc_macro2::Span;
use syn::{Error, Result};
pub use from::impl_from_arma;
pub use into::impl_into_arma;
pub use validate::validate_attributes;
use crate::derive::{
data::{FieldNamed, FieldUnnamed, StructData},
CombinedErrors,
};
impl StructData {
pub fn new(errors: &mut CombinedErrors, data: syn::DataStruct) -> Result<Self> {
match data.fields {
syn::Fields::Unit => Err(Error::new(
Span::call_site(),
"unit-like structs aren't supported",
)),
syn::Fields::Named(fields) => {
if fields.named.is_empty() {
return Err(Error::new(
Span::call_site(),
"unit-like structs aren't supported",
));
}
let fields = fields
.named
.into_iter()
.map(|f| FieldNamed::new(errors, f))
.collect::<_>();
Ok(Self::Map(fields))
}
syn::Fields::Unnamed(fields) => {
if fields.unnamed.is_empty() {
return Err(Error::new(
Span::call_site(),
"unit-like structs aren't supported",
));
}
if fields.unnamed.len() == 1 {
let field = FieldUnnamed::new(errors, fields.unnamed[0].clone(), 0);
Ok(Self::NewType(field))
} else {
let fields = fields
.unnamed
.into_iter()
.enumerate()
.map(|(i, f)| FieldUnnamed::new(errors, f, i))
.collect::<_>();
Ok(Self::Tuple(fields))
}
}
}
}
}

View File

@@ -0,0 +1,89 @@
use syn::Error;
use crate::derive::{
attributes::{Attribute, ContainerAttributes, FieldAttributes},
data::StructData,
CombinedErrors,
};
pub fn validate_attributes(
errors: &mut CombinedErrors,
attributes: &ContainerAttributes,
data: &StructData,
) {
if *attributes.transparent.value() {
match data {
StructData::Map(fields) if fields.len() > 1 => {
errors.add(
attributes
.transparent
.error("#[arma(transparent)] structs must have exactly one field"),
);
}
StructData::Tuple(_) => {
errors.add(
attributes
.transparent
.error("#[arma(transparent)] cannot be used on tuple like structs"),
);
}
_ => {}
}
}
if let Some(attr) = get_default_attr(attributes, data) {
match data {
StructData::Map(_) if *attributes.transparent.value() => {
errors.add(
attr.error("#[arma(default)] and #[arma(transparent)] cannot be used together"),
);
}
StructData::NewType(_) => {
errors.add(attr.error("#[arma(default)] cannot be used on new type structs"));
}
_ => {}
}
}
if let StructData::Tuple(fields) = data {
let mut index_first_default = None;
for (index, field) in fields.iter().enumerate() {
match index_first_default {
None => {
if field.attributes.default.is_set() {
index_first_default = Some(index);
}
}
Some(index) => {
if !field.attributes.default.is_set() {
errors.add(Error::new_spanned(&field.ty,
format!("field must have #[arma(default)] because previous field {} has #[arma(default)]", index)
));
}
}
}
}
}
}
fn get_default_attr<'a>(
attributes: &'a ContainerAttributes,
data: &'a StructData,
) -> Option<&'a Attribute<bool>> {
if *attributes.default.value() {
return Some(&attributes.default);
}
field_attributes(data)
.iter()
.find(|attr| *attr.default.value())
.map(|f| &f.default)
}
fn field_attributes(data: &StructData) -> Vec<&FieldAttributes> {
match data {
StructData::Map(fields) => fields.iter().map(|f| &f.attributes).collect(),
StructData::Tuple(fields) => fields.iter().map(|f| &f.attributes).collect(),
StructData::NewType(field) => vec![&field.attributes],
}
}

159
vendor/arma-rs-proc/src/lib.rs vendored Normal file
View File

@@ -0,0 +1,159 @@
mod derive;
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::quote;
use syn::{DeriveInput, Error, ItemFn};
#[proc_macro_attribute]
/// Used to generate the necessary boilerplate for an Arma extension.
/// It should be applied to a function that takes no arguments and returns an extension.
pub fn arma(_attr: TokenStream, item: TokenStream) -> TokenStream {
let ast = syn::parse_macro_input!(item as ItemFn);
let init = ast.sig.ident.clone();
let extern_type = if cfg!(windows) { "stdcall" } else { "C" };
let ext_init = quote! {
if RV_EXTENSION.is_none() {
RV_EXTENSION = Some(#init());
}
};
#[cfg(all(target_os = "windows", target_arch = "x86"))]
let prefix = "safe32_";
#[cfg(not(all(target_os = "windows", target_arch = "x86")))]
let prefix = "";
macro_rules! fn_ident {
( $name:literal ) => {
Ident::new(&format!("{prefix}{}", $name), Span::call_site())
};
}
let versionfn = fn_ident!("RVExtensionVersion");
let noargfn = fn_ident!("RVExtension");
let argfn = fn_ident!("RVExtensionArgs");
let callbackfn = fn_ident!("RVExtensionRegisterCallback");
let contextfn = fn_ident!("RVExtensionContext");
TokenStream::from(quote! {
use arma_rs::libc as arma_rs_libc;
static mut RV_EXTENSION: Option<Extension> = None;
#[cfg(all(target_os="windows", target_arch="x86"))]
arma_rs::link_args::windows! {
unsafe {
raw("/EXPORT:_RVExtensionVersion@8=_safe32_RVExtensionVersion@8");
raw("/EXPORT:_RVExtension@12=_safe32_RVExtension@12");
raw("/EXPORT:_RVExtensionArgs@20=_safe32_RVExtensionArgs@20");
raw("/EXPORT:_RVExtensionRegisterCallback@4=_safe32_RVExtensionRegisterCallback@4");
raw("/EXPORT:_RVExtensionContext@8=_safe32_RVExtensionContext@8");
}
}
/// Returns extension version, called by Arma on extension load.
/// This function is generated by the [`arma_rs::arma`] proc macro.
#[no_mangle]
#[doc(hidden)]
pub unsafe extern #extern_type fn #versionfn(output: *mut arma_rs_libc::c_char, size: arma_rs_libc::size_t) -> arma_rs_libc::c_int {
#ext_init
if let Some(ext) = &RV_EXTENSION {
arma_rs::write_cstr(ext.version().to_string(), output, size);
}
0
}
/// Run extension function, called by Arma on `callExtension` without arguments.
/// This function is generated by the [`arma_rs::arma`] proc macro.
#[no_mangle]
#[doc(hidden)]
pub unsafe extern #extern_type fn #noargfn(output: *mut arma_rs_libc::c_char, size: arma_rs_libc::size_t, function: *mut arma_rs_libc::c_char) {
#ext_init
if let Some(ext) = &RV_EXTENSION {
if ext.allow_no_args() {
ext.handle_call(function, output, size, None, None, true);
}
}
}
/// Run extension function with arguments, called by Arma on `callExtension` with arguments.
/// This function is generated by the [`arma_rs::arma`] proc macro.
#[no_mangle]
#[doc(hidden)]
pub unsafe extern #extern_type fn #argfn(output: *mut arma_rs_libc::c_char, size: arma_rs_libc::size_t, function: *mut arma_rs_libc::c_char, args: *mut *mut arma_rs_libc::c_char, arg_count: arma_rs_libc::c_int) -> arma_rs_libc::c_int {
#ext_init
if let Some(ext) = &RV_EXTENSION {
ext.handle_call(function, output, size, Some(args), Some(arg_count), true)
} else {
0
}
}
/// Set extension callback, called by Arma on extension load.
/// This function is generated by the [`arma_rs::arma`] proc macro.
#[no_mangle]
#[doc(hidden)]
pub unsafe extern #extern_type fn #callbackfn(callback: arma_rs::Callback) {
#ext_init
if let Some(ext) = &mut RV_EXTENSION {
ext.register_callback(callback);
ext.run_callbacks();
}
}
/// Provide extension call context, called by Arma on `callExtension`.
/// This function is generated by the [`arma_rs::arma`] proc macro.
#[no_mangle]
#[doc(hidden)]
pub unsafe extern #extern_type fn #contextfn(args: *mut *mut arma_rs_libc::c_char, arg_count: arma_rs_libc::c_int) {
#ext_init
if let Some(ext) = &mut RV_EXTENSION {
ext.handle_call_context(args, arg_count);
}
}
#ast
})
}
/// Derive implementation of `FromArma`, only supports structs.
/// - Map structs are converted from an hashmap.
/// - Tuple structs are converted from an array.
/// - Newtype structs directly use's the value's `FromArma` implementation.
/// - Unit-like structs are not supported.
///
/// ### Container Attributes
/// - `#[arma(transparent)]`: treat single field map structs as if its a newtype structs.
/// - `#[arma(default)]`: any missing field will be filled by the structs `Default` implementation.
///
/// ### Field Attributes
/// - `#[arma(from_str)]`: use the types `std::str::FromStr` instead of `FromArma`.
/// - `#[arma(default)]`: if missing use its `Default` implementation (takes precedence over container).
#[proc_macro_derive(FromArma, attributes(arma))]
pub fn derive_from_arma(item: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(item as DeriveInput);
derive::generate_from_arma(input)
.unwrap_or_else(Error::into_compile_error)
.into()
}
/// Derive implementation of `IntoArma`, only supports structs.
/// - Map structs are converted to an hashmap.
/// - Tuple structs are converted to an array.
/// - Newtype structs directly use's the value's `IntoArma` implementation.
/// - Unit-like structs are not supported.
///
/// ### Container Attributes
/// - `#[arma(transparent)]`: treat single field map structs as if its a newtype structs.
///
/// ### Field Attributes
/// - `#[arma(to_string)]`: use the types `std::string::ToString` instead of `IntoArma`.
#[proc_macro_derive(IntoArma, attributes(arma))]
pub fn derive_into_arma(item: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(item as DeriveInput);
derive::generate_into_arma(input)
.unwrap_or_else(Error::into_compile_error)
.into()
}

1
vendor/arma-rs/.cargo-ok vendored Normal file
View File

@@ -0,0 +1 @@
{"v":1}

6
vendor/arma-rs/.cargo_vcs_info.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"git": {
"sha1": "6cac89d7a9d02027bb85a6fa583b3ef0e8bf0f5a"
},
"path_in_vcs": "arma-rs"
}

951
vendor/arma-rs/Cargo.lock generated vendored Normal file
View File

@@ -0,0 +1,951 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "arma-rs"
version = "1.11.14"
dependencies = [
"arma-rs-proc",
"chrono",
"crossbeam-channel",
"libc",
"link_args",
"log",
"seq-macro",
"serde",
"serde_json",
"state",
"trybuild",
"uuid",
"winapi",
"windows 0.61.1",
]
[[package]]
name = "arma-rs-proc"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf67c0d0c7a59275e5ac4f3fce0cbdbcf3ba12e47bc30be6a3327d6a1bc151f8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bumpalo"
version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "cc"
version = "1.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "crossbeam-channel"
version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "generator"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
dependencies = [
"cc",
"libc",
"log",
"rustversion",
"windows 0.48.0",
]
[[package]]
name = "glob"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "hashbrown"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
[[package]]
name = "iana-time-zone"
version = "0.1.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "indexmap"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "js-sys"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
[[package]]
name = "link_args"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c7721e472624c9aaad27a5eb6b7c9c6045c7a396f2efb6dabaec1b640d5e89b"
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "loom"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5"
dependencies = [
"cfg-if",
"generator",
"scoped-tls",
"serde",
"serde_json",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "matchers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata 0.1.10",
]
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "proc-macro2"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"regex-syntax 0.6.29",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.8.5",
]
[[package]]
name = "regex-syntax"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rustversion"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "seq-macro"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
dependencies = [
"serde",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "smallvec"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
[[package]]
name = "state"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8"
dependencies = [
"loom",
]
[[package]]
name = "syn"
version = "2.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "target-triple"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ac9aa371f599d22256307c24a9d748c041e548cbf599f35d890f9d365361790"
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "thread_local"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
dependencies = [
"cfg-if",
"once_cell",
]
[[package]]
name = "toml"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "tracing"
version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]
name = "trybuild"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ae08be68c056db96f0e6c6dd820727cca756ced9e1f4cc7fdd20e2a55e23898"
dependencies = [
"glob",
"serde",
"serde_derive",
"serde_json",
"target-triple",
"termcolor",
"toml",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "uuid"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows"
version = "0.61.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-link",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
dependencies = [
"windows-core",
]
[[package]]
name = "windows-core"
version = "0.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-future"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32"
dependencies = [
"windows-core",
"windows-link",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-numerics"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [
"windows-core",
"windows-link",
]
[[package]]
name = "windows-result"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36"
dependencies = [
"memchr",
]

108
vendor/arma-rs/Cargo.toml vendored Normal file
View File

@@ -0,0 +1,108 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
name = "arma-rs"
version = "1.11.14"
authors = ["Brett Mayson"]
build = "build.rs"
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Arma 3 Extensions in Rust"
readme = "README.md"
keywords = ["arma"]
license = "MIT"
repository = "https://github.com/brettmayson/arma-rs"
[features]
default = ["extension"]
extension = [
"libc",
"crossbeam-channel",
]
[lib]
name = "arma_rs"
path = "src/lib.rs"
[[example]]
name = "hello_world"
path = "examples/hello_world.rs"
[[test]]
name = "derive"
path = "tests/derive.rs"
[[test]]
name = "emulate"
path = "tests/emulate.rs"
[[test]]
name = "main"
path = "tests/main.rs"
[dependencies.arma-rs-proc]
version = "1.11.1"
[dependencies.chrono]
version = "0.4.40"
optional = true
[dependencies.crossbeam-channel]
version = "0.5.14"
optional = true
[dependencies.libc]
version = "0.2.171"
optional = true
[dependencies.log]
version = "0.4.27"
[dependencies.seq-macro]
version = "0.3.6"
[dependencies.serde]
version = "1.0.219"
features = ["derive"]
optional = true
[dependencies.serde_json]
version = "1.0.140"
optional = true
[dependencies.state]
version = "0.6.0"
[dependencies.uuid]
version = "1.16.0"
optional = true
[dev-dependencies.trybuild]
version = "1.0.104"
[target.'cfg(all(target_os="windows", target_arch="x86"))'.dependencies.link_args]
version = "0.6.0"
[target."cfg(windows)".dependencies.winapi]
version = "0.3.9"
features = ["libloaderapi"]
[target."cfg(windows)".dependencies.windows]
version = "0.61.1"
features = [
"Win32_Foundation",
"Win32_System_Console",
]

41
vendor/arma-rs/Cargo.toml.orig generated vendored Normal file
View File

@@ -0,0 +1,41 @@
[package]
name = "arma-rs"
description = "Arma 3 Extensions in Rust"
version = "1.11.14"
edition = "2021"
authors = ["Brett Mayson"]
repository = "https://github.com/brettmayson/arma-rs"
license = "MIT"
keywords = ["arma"]
readme = "../README.md"
[dependencies]
arma-rs-proc = { path = "../arma-rs-proc", version = "1.11.1" }
log = "0.4.27"
state = "0.6.0"
seq-macro = "0.3.6"
chrono = { version = "0.4.40", optional = true }
crossbeam-channel = { version = "0.5.14", optional = true }
libc = { version = "0.2.171", optional = true }
serde = { version = "1.0.219", features = ["derive"], optional = true }
serde_json = { version = "1.0.140", optional = true }
uuid = { version = "1.16.0", optional = true }
[target.'cfg(all(target_os="windows", target_arch="x86"))'.dependencies]
link_args = "0.6.0"
[target.'cfg(windows)'.dependencies.winapi]
version = "0.3.9"
features = ["libloaderapi"]
[target.'cfg(windows)'.dependencies.windows]
version = "0.61.1"
features = ["Win32_Foundation", "Win32_System_Console"]
[dev-dependencies]
trybuild = "1.0.104"
[features]
default = ["extension"]
extension = ["libc", "crossbeam-channel"]

439
vendor/arma-rs/README.md vendored Normal file
View File

@@ -0,0 +1,439 @@
# arma-rs
[Join the arma-rs Discord!](https://discord.gg/qXWUrrwy5d)
[![codecov](https://codecov.io/gh/BrettMayson/arma-rs/branch/main/graph/badge.svg?token=A1H7SEZ434)](https://codecov.io/gh/BrettMayson/arma-rs)
The best way to make Arma 3 Extensions.
## Usage
```toml
[dependencies]
arma-rs = "1.11.10"
[lib]
name = "my_extension"
crate-type = ["cdylib"]
```
### Hello World
```rust
use arma_rs::{arma, Extension};
#[arma]
fn init() -> Extension {
Extension::build()
.command("hello", hello)
.command("welcome", welcome)
.finish()
}
pub fn hello() -> &'static str {
"Hello"
}
pub fn welcome(name: String) -> String {
format!("Welcome {}", name)
}
```
```sqf
"my_extension" callExtension ["hello", []]; // Returns ["Hello", 0, 0]
"my_extension" callExtension ["welcome", ["John"]]; // Returns ["Welcome John", 0, 0]
```
## Command Groups
Commands can be grouped together, making your large projects much easier to manage.
```rust
use arma_rs::{arma, Extension, Group};
#[arma]
fn init() -> Extension {
Extension::build()
.group("hello",
Group::new()
.command("english", hello::english)
.group("english",
Group::new()
.command("casual", hello::english_casual)
)
.command("french", hello::french),
)
.group("welcome",
Group::new()
.command("english", welcome::english)
.command("french", welcome::french),
)
.finish()
}
mod hello {
pub fn english() -> &'static str {
"Hello"
}
pub fn english_casual() -> &'static str {
"Hey"
}
pub fn french() -> &'static str {
"Bonjour"
}
}
mod welcome {
pub fn english(name: String) -> String {
format!("Welcome {}", name)
}
pub fn french(name: String) -> String {
format!("Bienvenue {}", name)
}
}
```
Commands groups are called by using the format `group:command`. You can nest groups as much as you want.
```sqf
"my_extension" callExtension ["hello:english", []]; // Returns ["Hello", 0, 0]
"my_extension" callExtension ["hello:english:casual", []]; // Returns ["Hey", 0, 0]
"my_extension" callExtension ["hello:french", []]; // Returns ["Bonjour", 0, 0]
```
## Callbacks
Extension callbacks can be invoked anywhere in the extension by adding a variable of type `Context` to the start of a handler.
```rust
use arma_rs::Context;
pub fn sleep(ctx: Context, duration: u64, id: String) {
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs(duration));
ctx.callback_data("example_timer", "done", Some(id));
});
}
pub fn group() -> arma_rs::Group {
arma_rs::Group::new().command("sleep", sleep)
}
```
## Call Context
Since Arma v2.11 additional context is provided each time the extension is called. This context can be accessed through the optional `ArmaCallContext` argument.
Since Arma v2.18 the context is only requested from Arma when the functionh has `ArmaCallContext` as an argument.
```rust
use arma_rs::{CallContext, CallContextStackTrace};
pub fn call_context(call_context: CallContext) -> String {
format!(
"{:?},{:?},{:?},{:?},{:?}",
call_context.caller(),
call_context.source(),
call_context.mission(),
call_context.server(),
call_context.remote_exec_owner(),
)
}
pub fn stack_trace(call_context: CallContextStackTrace) -> String {
format!(
"{:?}\n{:?}",
call_context.source(),
call_context.stack_trace()
)
}
pub fn group() -> arma_rs::Group {
arma_rs::Group::new()
.command("call_context", call_context)
.command("stack_trace", stack_trace)
}
```
## Persistent State
Both the extension and command groups allow for type based persistent state values with at most one instance per type. These state values can then be accessed through the optional `Context` argument.
### Global State
Extension state is accessible from any command handler.
```rust
use arma_rs::{arma, Context, ContextState, Extension};
use std::sync::atomic::{AtomicU32, Ordering};
#[arma]
fn init() -> Extension {
Extension::build()
.command("counter_increment", increment)
.state(AtomicU32::new(0))
.finish()
}
pub fn increment(ctx: Context) -> Result<(), ()> {
let Some(counter) = ctx.global().get::<AtomicU32>() else {
return Err(());
};
counter.fetch_add(1, Ordering::SeqCst);
Ok(())
}
```
### Group State
Command group state is only accessible from command handlers within the same group.
```rust
use arma_rs::{Context, ContextState, Extension};
use std::sync::atomic::{AtomicU32, Ordering};
pub fn increment(ctx: Context) -> Result<(), ()> {
let Some(counter) = ctx.group().get::<AtomicU32>() else {
return Err(());
};
counter.fetch_add(1, Ordering::SeqCst);
Ok(())
}
pub fn group() -> arma_rs::Group {
arma_rs::Group::new()
.command("increment", increment)
.state(AtomicU32::new(0))
}
```
## Custom Types
If you're bringing your existing Rust library with your own types, you can easily define how they are converted to and from Arma.
```rust
use arma_rs::{FromArma, IntoArma, Value, FromArmaError};
pub struct MemoryReport {
total: u64,
free: u64,
avail: u64,
}
impl FromArma for MemoryReport {
fn from_arma(s: String) -> Result<Self, FromArmaError> {
let (total, free, avail) = <(u64, u64, u64)>::from_arma(s)?;
Ok(Self { total, free, avail })
}
}
impl IntoArma for MemoryReport {
fn to_arma(&self) -> Value {
Value::Array(
vec![self.total, self.free, self.avail]
.into_iter()
.map(|v| v.to_string().to_arma())
.collect(),
)
}
}
```
### Derive
Alternatively you can derive these traits. Note that the derive and manual implementation examples slightly differ, as when deriving map like structs its represented as an hashmap rather than an array. For more information on data representation and attributes see: [FromArma](https://docs.rs/arma-rs/latest/arma_rs/derive.FromArma.html) and [IntoArma](https://docs.rs/arma-rs/latest/arma_rs/derive.IntoArma.html).
```rust
use arma_rs::{FromArma, IntoArma};
#[derive(FromArma, IntoArma)]
struct MemoryReport {
#[arma(to_string)]
total: u64,
#[arma(to_string)]
free: u64,
#[arma(to_string)]
avail: u64,
}
```
Deriving is currently only supported for structs, this might change in the future.
## Error Codes
By default arma-rs will only allow commands via `RvExtensionArgs`. Using `callExtension` with only a function name will return an empty string.
```sqf
"my_extension" callExtension "hello:english" // returns ""
"my_extension" callExtension ["hello:english", []] // returns ["Hello", 0, 0]
```
This behaviour can be changed by calling `.allow_no_args()` when building the extension. It is recommended not to use this, and to implement error handling instead.
| Code | Description |
|------|---------------------------------------------------|
| 0 | Success |
| 1 | Command not found |
| 2x | Invalid argument count, x is received count |
| 3x | Invalid argument type, x is argument position |
| 4 | Attempted to write a value larger than the buffer |
| 9 | Application error, from using a Result |
### Error Examples
```rust
use arma_rs::Context;
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub fn overflow(ctx: Context) -> String {
"X".repeat(ctx.buffer_len() + 1)
}
pub fn should_error(error: bool) -> Result<String, String> {
if error {
Err(String::from("told to error"))
} else {
Ok(String::from("told to succeed"))
}
}
```
```sqf
"my_extension" callExtension ["add", [1, 2]]; // Returns ["3", 0, 0]
"my_extension" callExtension ["sub", [1, 2]]; // Returns ["", 1, 0]
"my_extension" callExtension ["add", [1, 2, 3]]; // Returns ["", 23, 0], didn't expect 3 elements
"my_extension" callExtension ["add", [1, "two"]]; // Returns ["", 31, 0], unable to parse the second argument
"my_extension" callExtension ["overflow", []]; // Returns ["", 4, 0], the return size was larger than the buffer
"my_extension" callExtension ["should_error", [true]]; // Returns ["told to error", 9, 0]
"my_extension" callExtension ["should_error", [false]]; // Returns ["told to succeed", 0, 0]
```
## Testing
Tests can be created utilizing the `extension.call()` method.
```rust,ignore
mod tests {
#[test]
fn hello() {
let extension = init().testing();
let (output, _) = extension.call("hello:english", None);
assert_eq!(output, "hello");
}
#[test]
fn welcome() {
let extension = init().testing();
let (output, _) =
extension.call("welcome:english", Some(vec!["John".to_string()]));
assert_eq!(output, "Welcome John");
}
#[test]
fn sleep_1sec() {
let extension = Extension::build()
.group("timer", super::group())
.finish()
.testing();
let (_, code) = extension.call(
"timer:sleep",
Some(vec!["1".to_string(), "test".to_string()]),
);
assert_eq!(code, 0);
let result = extension.callback_handler(
|name, func, data| {
assert_eq!(name, "timer:sleep");
assert_eq!(func, "done");
if let Some(Value::String(s)) = data {
Result::Ok(s)
} else {
Result::Err("Data was not a string".to_string())
}
},
Duration::from_secs(2),
);
assert_eq!(Result::Ok("test".to_string()), result);
}
}
```
## Unit Loadout Array
arma-rs includes a [loadout module](https://docs.rs/arma-rs/latest/arma_rs/loadout/index.html) to assist with the handling of [Arma's Unit Loadout Array](https://community.bistudio.com/wiki/Unit_Loadout_Array).
```rust
use arma_rs::{FromArma, loadout::{Loadout, InventoryItem, Weapon, Magazine}};
let l = r#"[[],[],[],["U_Marshal",[]],[],[],"H_Cap_headphones","G_Aviator",[],["ItemMap","ItemGPS","","ItemCompass","ItemWatch",""]]"#;
let mut loadout = Loadout::from_arma(l.to_string()).unwrap();
loadout.set_secondary({
let mut weapon = Weapon::new("launch_B_Titan_short_F".to_string());
weapon.set_primary_magazine(Magazine::new("Titan_AT".to_string(), 1));
weapon
});
loadout.set_primary({
let mut weapon = Weapon::new("arifle_MXC_F".to_string());
weapon.set_optic("optic_Holosight".to_string());
weapon
});
let uniform = loadout.uniform_mut();
uniform.set_class("U_B_CombatUniform_mcam".to_string());
let uniform_items = uniform.items_mut().unwrap();
uniform_items.push(InventoryItem::new_item("FirstAidKit".to_string(), 3));
uniform_items.push(InventoryItem::new_magazine("30Rnd_65x39_caseless_mag".to_string(), 5, 30));
```
## Common Rust Libraries
arma-rs supports some common Rust libraries.
You can enable their support by adding their name to the features of arma-rs.
```toml
arma-rs = { version = "1.8.0", features = ["chrono"] }
```
Please create an issue first if you would like to add support for a new library.
### chrono
[`crates.io`](https://crates.io/crates/chrono)
#### chrono - Convert to Arma
[`NaiveDateTime`](https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDateTime.html) and [`DateTime<TimeZone>`](https://docs.rs/chrono/latest/chrono/struct.DateTime.html) will be converted to [Arma's date array](https://community.bistudio.com/wiki/systemTimeUTC).
The timezone will always be converted to UTC.
#### chrono - Convert From Arma
[Arma's date array](https://community.bistudio.com/wiki/systemTimeUTC) can be converted to [`NaiveDateTime`](https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDateTime.html).
### uuid
[`crates.io`](https://crates.io/crates/uuid)
#### uuid - Convert To Arma
[`Uuid`](https://docs.rs/uuid/latest/uuid/struct.Uuid.html) will be converted to a string.
### serde_json
[`crates.io`](https://crates.io/crates/serde_json)
#### serde_json - Convert To Arma
Any variant of [`serde_json::Value`](https://docs.serde.rs/serde_json/enum.Value.html) will be converted to the appropriate Arma type.
## Building for x86 (32 Bit)
```sh
rustup toolchain install stable-i686-pc-windows-msvc
cargo +stable-i686-pc-windows-msvc build
```
## Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

16
vendor/arma-rs/build.rs vendored Normal file
View File

@@ -0,0 +1,16 @@
use std::path::Path;
fn main() {
let mut root = Path::new("../../README.md");
if !root.exists() {
root = Path::new("../README.md");
}
if !root.exists() {
root = Path::new("README.md");
}
std::fs::copy(
root,
Path::new(&format!("{}/README.md", std::env::var("OUT_DIR").unwrap())),
)
.unwrap();
}

Some files were not shown because too many files have changed in this diff Show More