mirror of
https://github.com/valmojr/armatak.git
synced 2026-06-13 15:43:29 +00:00
Compare commits
27 Commits
wings_of_l
...
fuck_arma_
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a5a7a17a3 | |||
| 0486f2a285 | |||
| 753dcab26e | |||
| 2f53488ba8 | |||
| 323339e679 | |||
| 3f14a75e81 | |||
| 469a54c141 | |||
| 2ee9030c00 | |||
| 5b29a40990 | |||
| 708fe5e670 | |||
| e32aadda4e | |||
| c35b7f0268 | |||
| 876cf900c3 | |||
| 778ac0ac54 | |||
| b816144fb0 | |||
| 61ba9f6d63 | |||
| f88c02a7aa | |||
| 5ffc08e6f1 | |||
| 9392380c78 | |||
| a18343b81d | |||
|
|
13cd08c655 | ||
|
|
8fe14dc18d | ||
|
|
1bec26df8a | ||
|
|
c5d5da636f | ||
|
|
c2e137e67c | ||
|
|
de5ac9dbb5 | ||
|
|
ef3be1e768 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,6 +3,7 @@
|
||||
hemtt
|
||||
hemtt.exe
|
||||
*.biprivatekey
|
||||
.hemttprivatekey
|
||||
source/
|
||||
.vscode
|
||||
releases/
|
||||
@@ -87,4 +88,4 @@ target/
|
||||
.cxx
|
||||
local.properties
|
||||
|
||||
*.apk
|
||||
*.apk
|
||||
|
||||
1073
Cargo.lock
generated
1073
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,10 @@ chrono = "0.4.39"
|
||||
lazy_static = "1.5.0"
|
||||
log = "0.4.22"
|
||||
log4rs = "1.3.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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -19,6 +19,12 @@ class CfgFunctions {
|
||||
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";
|
||||
};
|
||||
@@ -68,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";
|
||||
};
|
||||
@@ -95,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";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:cot:marker", [_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]];
|
||||
|
||||
55
addons/main/functions/api/fn_send_uas_sensor_cot.sqf
Normal file
55
addons/main/functions/api/fn_send_uas_sensor_cot.sqf
Normal 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]];
|
||||
30
addons/main/functions/api/fn_send_uas_video_cot.sqf
Normal file
30
addons/main/functions/api/fn_send_uas_video_cot.sqf
Normal 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]];
|
||||
@@ -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
|
||||
23
addons/main/functions/map/fn_convert_to_clafghan.sqf
Normal file
23
addons/main/functions/map/fn_convert_to_clafghan.sqf
Normal 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]
|
||||
23
addons/main/functions/map/fn_convert_to_colombia.sqf
Normal file
23
addons/main/functions/map/fn_convert_to_colombia.sqf
Normal 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]
|
||||
30
addons/main/functions/map/fn_convert_to_hellanmaa.sqf
Normal file
30
addons/main/functions/map/fn_convert_to_hellanmaa.sqf
Normal 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]
|
||||
23
addons/main/functions/map/fn_convert_to_lawn.sqf
Normal file
23
addons/main/functions/map/fn_convert_to_lawn.sqf
Normal 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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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]
|
||||
23
addons/main/functions/map/fn_convert_to_rut_mandol.sqf
Normal file
23
addons/main/functions/map/fn_convert_to_rut_mandol.sqf
Normal 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]
|
||||
@@ -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";
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
PREP(3denCoreModuleConfig);
|
||||
PREP(3denEnrollModuleConfig);
|
||||
PREP(3denTcpModuleConfig);
|
||||
PREP(routerEntityAdd);
|
||||
PREP(routerEntityRemove);
|
||||
PREP(ZeusCoreModuleConfig);
|
||||
PREP(startCotRouter);
|
||||
PREP(ZeusEnrollModuleConfig);
|
||||
PREP(ZeusTcpModuleConfig);
|
||||
|
||||
@@ -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[] = {};
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
|
||||
@@ -1,64 +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;
|
||||
|
||||
_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;
|
||||
37
addons/server/functions/fnc_3denEnrollModuleConfig.sqf
Normal file
37
addons/server/functions/fnc_3denEnrollModuleConfig.sqf
Normal 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
|
||||
26
addons/server/functions/fnc_3denTcpModuleConfig.sqf
Normal file
26
addons/server/functions/fnc_3denTcpModuleConfig.sqf
Normal 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
|
||||
@@ -1,67 +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;
|
||||
|
||||
_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;
|
||||
deleteVehicle _logic;
|
||||
closeDialog 1;
|
||||
33
addons/server/functions/fnc_ZeusEnrollModuleConfig.sqf
Normal file
33
addons/server/functions/fnc_ZeusEnrollModuleConfig.sqf
Normal 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;
|
||||
22
addons/server/functions/fnc_ZeusTcpModuleConfig.sqf
Normal file
22
addons/server/functions/fnc_ZeusTcpModuleConfig.sqf
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
47
addons/server/functions/fnc_startCotRouter.sqf
Normal file
47
addons/server/functions/fnc_startCotRouter.sqf
Normal 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
|
||||
@@ -1 +0,0 @@
|
||||
armatak\armatak\addons\video
|
||||
@@ -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));
|
||||
};
|
||||
};
|
||||
@@ -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";
|
||||
};
|
||||
};
|
||||
*/
|
||||
};
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
PREP(videoParser);
|
||||
@@ -1,9 +0,0 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
ADDON = false;
|
||||
|
||||
PREP_RECOMPILE_START;
|
||||
#include "XEH_PREP.hpp"
|
||||
PREP_RECOMPILE_END;
|
||||
|
||||
ADDON = true;
|
||||
@@ -1,3 +0,0 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
#include "XEH_PREP.hpp"
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
@@ -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
BIN
media/delta_larp.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 143 KiB |
BIN
media/picture.png
LFS
Normal file
BIN
media/picture.png
LFS
Normal file
Binary file not shown.
@@ -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('&', "&")
|
||||
.replace('"', """)
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
pub mod circle;
|
||||
pub mod circle;
|
||||
|
||||
106
src/cot/eud.rs
106
src/cot/eud.rs
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use arma_rs::{FromArma, FromArmaError};
|
||||
use chrono::{Utc, Duration, SecondsFormat};
|
||||
use chrono::{Duration, SecondsFormat, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct MessagePayload {
|
||||
@@ -14,8 +14,7 @@ pub struct MessagePayload {
|
||||
|
||||
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) =
|
||||
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 {
|
||||
@@ -55,8 +54,8 @@ impl MessageCot {
|
||||
|
||||
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);
|
||||
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();
|
||||
@@ -98,10 +97,7 @@ impl MessageCot {
|
||||
format!(
|
||||
"<__chat parent=\"RootContactGroup\" groupOwner=\"false\" \
|
||||
messageId=\"{}\" chatroom=\"{}\" id=\"{}\" senderCallsign=\"{}\">",
|
||||
message_uuid,
|
||||
self.chatroom,
|
||||
self.chatroom,
|
||||
self.sender_callsign,
|
||||
message_uuid, self.chatroom, self.chatroom, self.sender_callsign,
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
@@ -109,9 +105,7 @@ impl MessageCot {
|
||||
xml.push_str(
|
||||
format!(
|
||||
"<chatgrp uid0=\"{}\" uid1=\"{}\" id=\"{}\" />",
|
||||
self.sender_uid,
|
||||
self.chatroom,
|
||||
self.chatroom
|
||||
self.sender_uid, self.chatroom, self.chatroom
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
pub mod draws;
|
||||
pub mod cot;
|
||||
pub mod digital_pointer;
|
||||
pub mod draws;
|
||||
pub mod eud;
|
||||
pub mod gps;
|
||||
pub mod message;
|
||||
pub mod nato;
|
||||
pub mod nato;
|
||||
pub mod uas;
|
||||
|
||||
@@ -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
243
src/cot/uas.rs
Normal 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 (0–359).
|
||||
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 (0–1).
|
||||
// 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
|
||||
}
|
||||
}
|
||||
11
src/lib.rs
11
src/lib.rs
@@ -1,4 +1,5 @@
|
||||
use arma_rs::{arma, Extension, Group};
|
||||
use rustls::crypto::aws_lc_rs;
|
||||
mod structs;
|
||||
mod tcp;
|
||||
mod tests;
|
||||
@@ -31,6 +32,9 @@ 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)
|
||||
@@ -47,6 +51,8 @@ pub fn init() -> Extension {
|
||||
"tcp_socket",
|
||||
Group::new()
|
||||
.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(
|
||||
@@ -55,7 +61,10 @@ pub fn init() -> Extension {
|
||||
.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),
|
||||
.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",
|
||||
|
||||
@@ -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
280
src/tcp/client.rs
Normal 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
64
src/tcp/config.rs
Normal 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
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,24 +9,62 @@ pub fn send_eud_cot(ctx: Context, cursor_over_time: cot::eud::EudCoTPayload) ->
|
||||
"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 {
|
||||
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 {
|
||||
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 {
|
||||
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"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ 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 {
|
||||
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))
|
||||
@@ -14,21 +17,21 @@ pub fn send_circle_cot(ctx: Context, circle_payload: cot::draws::circle::CircleC
|
||||
}
|
||||
|
||||
pub fn send_ellipse_cot(ctx: Context) -> &'static str {
|
||||
let _ = ctx;
|
||||
"Not implemented: send_ellipse_cot"
|
||||
let _ = ctx;
|
||||
"Not implemented: send_ellipse_cot"
|
||||
}
|
||||
|
||||
pub fn send_rectangle_cot(ctx: Context) -> &'static str {
|
||||
let _ = ctx;
|
||||
"Not implemented: send_ellipse_cot"
|
||||
let _ = ctx;
|
||||
"Not implemented: send_ellipse_cot"
|
||||
}
|
||||
|
||||
pub fn send_freedraw_cot(ctx: Context) -> &'static str {
|
||||
let _ = ctx;
|
||||
"Not implemented: send_ellipse_cot"
|
||||
let _ = ctx;
|
||||
"Not implemented: send_ellipse_cot"
|
||||
}
|
||||
|
||||
pub fn send_vectordraw_cot(ctx: Context) -> &'static str {
|
||||
let _ = ctx;
|
||||
"Not implemented: send_ellipse_cot"
|
||||
}
|
||||
let _ = ctx;
|
||||
"Not implemented: send_ellipse_cot"
|
||||
}
|
||||
|
||||
153
src/tcp/mod.rs
153
src/tcp/mod.rs
@@ -1,120 +1,90 @@
|
||||
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;
|
||||
|
||||
mod client;
|
||||
mod config;
|
||||
mod tls;
|
||||
mod transport;
|
||||
|
||||
pub mod cot;
|
||||
pub mod draw;
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
use client::{TcpClient, TcpCommand};
|
||||
use config::ConnectionConfig;
|
||||
|
||||
lazy_static! {
|
||||
static ref TCP_CLIENT: Arc<Mutex<Option<TcpClient>>> = Arc::new(Mutex::new(None));
|
||||
}
|
||||
|
||||
pub fn start(ctx: Context, address: String) -> &'static str {
|
||||
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(address, rx, ctx);
|
||||
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");
|
||||
@@ -126,6 +96,7 @@ pub fn send_payload(ctx: Context, payload: String) -> &'static str {
|
||||
|
||||
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 {
|
||||
|
||||
221
src/tcp/tls/connector.rs
Normal file
221
src/tcp/tls/connector.rs
Normal 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
228
src/tcp/tls/enrollment.rs
Normal 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
5
src/tcp/tls/mod.rs
Normal 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
90
src/tcp/transport.rs
Normal 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,
|
||||
),
|
||||
}
|
||||
}
|
||||
34
src/tests.rs
34
src/tests.rs
@@ -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, "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
pub mod uuid;
|
||||
pub mod address;
|
||||
pub mod log;
|
||||
pub mod log;
|
||||
pub mod uuid;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
1
vendor/arma-rs-proc/.cargo-ok
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"v":1}
|
||||
6
vendor/arma-rs-proc/.cargo_vcs_info.json
vendored
Normal file
6
vendor/arma-rs-proc/.cargo_vcs_info.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"git": {
|
||||
"sha1": "adfc323899e58f20c05ebb37595d5ca4fd09367f"
|
||||
},
|
||||
"path_in_vcs": "arma-rs-proc"
|
||||
}
|
||||
42
vendor/arma-rs-proc/Cargo.toml
vendored
Normal file
42
vendor/arma-rs-proc/Cargo.toml
vendored
Normal 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
17
vendor/arma-rs-proc/Cargo.toml.orig
generated
vendored
Normal 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"] }
|
||||
145
vendor/arma-rs-proc/src/derive/attributes.rs
vendored
Normal file
145
vendor/arma-rs-proc/src/derive/attributes.rs
vendored
Normal 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
124
vendor/arma-rs-proc/src/derive/data.rs
vendored
Normal 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
72
vendor/arma-rs-proc/src/derive/mod.rs
vendored
Normal 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(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
162
vendor/arma-rs-proc/src/derive/struct/from.rs
vendored
Normal file
162
vendor/arma-rs-proc/src/derive/struct/from.rs
vendored
Normal 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
|
||||
})
|
||||
}
|
||||
}
|
||||
72
vendor/arma-rs-proc/src/derive/struct/into.rs
vendored
Normal file
72
vendor/arma-rs-proc/src/derive/struct/into.rs
vendored
Normal 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)
|
||||
}
|
||||
}
|
||||
62
vendor/arma-rs-proc/src/derive/struct/mod.rs
vendored
Normal file
62
vendor/arma-rs-proc/src/derive/struct/mod.rs
vendored
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
89
vendor/arma-rs-proc/src/derive/struct/validate.rs
vendored
Normal file
89
vendor/arma-rs-proc/src/derive/struct/validate.rs
vendored
Normal 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
159
vendor/arma-rs-proc/src/lib.rs
vendored
Normal 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
1
vendor/arma-rs/.cargo-ok
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"v":1}
|
||||
6
vendor/arma-rs/.cargo_vcs_info.json
vendored
Normal file
6
vendor/arma-rs/.cargo_vcs_info.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"git": {
|
||||
"sha1": "6cac89d7a9d02027bb85a6fa583b3ef0e8bf0f5a"
|
||||
},
|
||||
"path_in_vcs": "arma-rs"
|
||||
}
|
||||
951
vendor/arma-rs/Cargo.lock
generated
vendored
Normal file
951
vendor/arma-rs/Cargo.lock
generated
vendored
Normal 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
108
vendor/arma-rs/Cargo.toml
vendored
Normal 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
41
vendor/arma-rs/Cargo.toml.orig
generated
vendored
Normal 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
439
vendor/arma-rs/README.md
vendored
Normal file
@@ -0,0 +1,439 @@
|
||||
# arma-rs
|
||||
|
||||
[Join the arma-rs Discord!](https://discord.gg/qXWUrrwy5d)
|
||||
[](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
16
vendor/arma-rs/build.rs
vendored
Normal 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();
|
||||
}
|
||||
40
vendor/arma-rs/examples/hello_world.rs
vendored
Normal file
40
vendor/arma-rs/examples/hello_world.rs
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
use arma_rs::{arma, Extension};
|
||||
|
||||
#[arma]
|
||||
fn init() -> Extension {
|
||||
Extension::build()
|
||||
.version("1.0.0".to_string())
|
||||
.command("hello", hello)
|
||||
.command("welcome", welcome)
|
||||
.finish()
|
||||
}
|
||||
|
||||
pub fn hello() -> &'static str {
|
||||
"Hello"
|
||||
}
|
||||
|
||||
pub fn welcome(name: String) -> String {
|
||||
format!("Welcome {name}")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::init;
|
||||
|
||||
#[test]
|
||||
fn hello() {
|
||||
let extension = init().testing();
|
||||
let (result, _) = extension.call("hello", None);
|
||||
assert_eq!(result, "Hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn welcome() {
|
||||
let extension = init().testing();
|
||||
let (result, _) = extension.call("welcome", Some(vec!["John".to_string()]));
|
||||
assert_eq!(result, "Welcome John");
|
||||
}
|
||||
}
|
||||
|
||||
// Only required for cargo, don't include in your library
|
||||
fn main() {}
|
||||
364
vendor/arma-rs/src/call_context/call.rs
vendored
Normal file
364
vendor/arma-rs/src/call_context/call.rs
vendored
Normal file
@@ -0,0 +1,364 @@
|
||||
use std::path::Path;
|
||||
|
||||
use super::stack::ArmaContextStackTrace;
|
||||
|
||||
#[repr(C)]
|
||||
struct RawArmaCallContext {
|
||||
pub steam_id: u64,
|
||||
pub source: *const libc::c_char,
|
||||
pub mission: *const libc::c_char,
|
||||
pub server: *const libc::c_char,
|
||||
pub remote_exec_owner: i16,
|
||||
pub call_stack: Option<*const super::stack::RawContextStackTrace>,
|
||||
}
|
||||
|
||||
impl RawArmaCallContext {
|
||||
fn from_arma(args: *mut *mut i8, count: libc::c_int) -> Self {
|
||||
let steam_id = unsafe { *args.offset(0) as u64 };
|
||||
let source = unsafe { *args.offset(1) as *const libc::c_char };
|
||||
let mission = unsafe { *args.offset(2) as *const libc::c_char };
|
||||
let server = unsafe { *args.offset(3) as *const libc::c_char };
|
||||
let remote_exec_owner = unsafe { *args.offset(4) as i16 };
|
||||
|
||||
let call_stack = if count > 5 {
|
||||
let stack = unsafe { *args.offset(5) as *const super::stack::RawContextStackTrace };
|
||||
Some(stack)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Self {
|
||||
steam_id,
|
||||
source,
|
||||
mission,
|
||||
server,
|
||||
remote_exec_owner,
|
||||
call_stack,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait StackRequest {}
|
||||
|
||||
pub struct WithStackTrace;
|
||||
impl StackRequest for WithStackTrace {}
|
||||
|
||||
pub struct WithoutStackTrace;
|
||||
impl StackRequest for WithoutStackTrace {}
|
||||
|
||||
/// Context of the callExtension, provided by Arma.
|
||||
pub type CallContext = ArmaCallContext<WithoutStackTrace>;
|
||||
/// Context of the callExtension, provided by Arma, with a stack trace.
|
||||
pub type CallContextStackTrace = ArmaCallContext<WithStackTrace>;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
/// Context of the Arma call.
|
||||
pub struct ArmaCallContext<T: StackRequest> {
|
||||
pub(super) caller: Caller,
|
||||
pub(super) source: Source,
|
||||
pub(super) mission: Mission,
|
||||
pub(super) server: Server,
|
||||
pub(super) remote_exec_owner: i16,
|
||||
|
||||
_stack_marker: std::marker::PhantomData<T>,
|
||||
stack: Option<ArmaContextStackTrace>,
|
||||
}
|
||||
|
||||
impl<T: StackRequest> ArmaCallContext<T> {
|
||||
pub(crate) const fn new(
|
||||
caller: Caller,
|
||||
source: Source,
|
||||
mission: Mission,
|
||||
server: Server,
|
||||
remote_exec_owner: i16,
|
||||
) -> Self {
|
||||
Self {
|
||||
caller,
|
||||
source,
|
||||
mission,
|
||||
server,
|
||||
remote_exec_owner,
|
||||
|
||||
_stack_marker: std::marker::PhantomData,
|
||||
stack: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new ArmaCallContext from pointers provided by Arma.
|
||||
pub fn from_arma(args: *mut *mut i8, count: libc::c_int) -> Self {
|
||||
let raw = RawArmaCallContext::from_arma(args, count);
|
||||
Self {
|
||||
caller: Caller::Steam(raw.steam_id),
|
||||
source: Source::from(unsafe { std::ffi::CStr::from_ptr(raw.source).to_str().unwrap() }),
|
||||
mission: Mission::from(unsafe {
|
||||
std::ffi::CStr::from_ptr(raw.mission).to_str().unwrap()
|
||||
}),
|
||||
server: Server::from(unsafe { std::ffi::CStr::from_ptr(raw.server).to_str().unwrap() }),
|
||||
remote_exec_owner: raw.remote_exec_owner,
|
||||
|
||||
_stack_marker: std::marker::PhantomData,
|
||||
stack: raw.call_stack.map(ArmaContextStackTrace::from),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Player that called the extension. Can be [`Caller::Unknown`] when the player's steamID64 is unavailable
|
||||
/// # Note
|
||||
/// Unlike <https://community.bistudio.com/wiki/getPlayerUID> [`Caller::Steam`] isn't limited to multiplayer.
|
||||
pub fn caller(&self) -> &Caller {
|
||||
&self.caller
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Source from where the extension was called.
|
||||
pub fn source(&self) -> &Source {
|
||||
&self.source
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Current mission's name.
|
||||
/// # Note
|
||||
/// Can result in [`Mission::None`] in missions made prior to Arma v2.02.
|
||||
pub fn mission(&self) -> &Mission {
|
||||
&self.mission
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Current server's name
|
||||
pub fn server(&self) -> &Server {
|
||||
&self.server
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Remote execution owner.
|
||||
pub fn remote_exec_owner(&self) -> i16 {
|
||||
self.remote_exec_owner
|
||||
}
|
||||
}
|
||||
|
||||
impl ArmaCallContext<WithStackTrace> {
|
||||
#[must_use]
|
||||
/// Call stack of the extension call.
|
||||
pub fn stack_trace(&self) -> &ArmaContextStackTrace {
|
||||
// By the time this gets to consumer code, to_without_stack would've been called if the stack was not requested
|
||||
self.stack.as_ref().expect("Stack is missing")
|
||||
}
|
||||
|
||||
/// Convert the context to one without a stack trace.
|
||||
pub(crate) fn into_without_stack(self) -> ArmaCallContext<WithoutStackTrace> {
|
||||
ArmaCallContext::new(
|
||||
self.caller,
|
||||
self.source,
|
||||
self.mission,
|
||||
self.server,
|
||||
self.remote_exec_owner,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Identification of the player calling your extension.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub enum Caller {
|
||||
/// The player's steamID64.
|
||||
Steam(u64),
|
||||
#[default]
|
||||
/// Unable to determine.
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl Caller {
|
||||
/// Convert the caller to a string.
|
||||
pub fn as_str(&self) -> String {
|
||||
match self {
|
||||
Self::Steam(id) => id.to_string(),
|
||||
Self::Unknown => "0".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert the caller to a u64.
|
||||
pub fn as_u64(&self) -> u64 {
|
||||
match self {
|
||||
Self::Steam(id) => *id,
|
||||
Self::Unknown => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Caller {
|
||||
fn from(s: &str) -> Self {
|
||||
if s.is_empty() || s == "0" {
|
||||
Self::Unknown
|
||||
} else {
|
||||
s.parse::<u64>().map_or(Self::Unknown, Self::Steam)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for Caller {
|
||||
fn from(id: u64) -> Self {
|
||||
if id == 0 {
|
||||
Self::Unknown
|
||||
} else {
|
||||
Self::Steam(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Source of the extension call.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub enum Source {
|
||||
/// Absolute path of the file on the players system.
|
||||
/// For example on windows: `C:\Users\user\Documents\Arma 3\missions\test.VR\fn_armaContext.sqf`.
|
||||
File(String),
|
||||
/// Path inside of a pbo.
|
||||
/// For example: `z\test\addons\main\fn_armaContext.sqf`.
|
||||
Pbo(String),
|
||||
#[default]
|
||||
/// Debug console or an other form of on the fly execution, such as mission triggers.
|
||||
Console,
|
||||
}
|
||||
|
||||
impl Source {
|
||||
/// Convert the source to a string.
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
Self::File(s) | Self::Pbo(s) => s,
|
||||
Self::Console => "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Source {
|
||||
fn from(s: &str) -> Self {
|
||||
if s.is_empty() {
|
||||
Self::Console
|
||||
} else if Path::new(s).is_absolute() {
|
||||
Self::File(s.to_string())
|
||||
} else {
|
||||
Self::Pbo(s.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<*const libc::c_char> for Source {
|
||||
#[allow(clippy::not_unsafe_ptr_arg_deref)]
|
||||
fn from(s: *const libc::c_char) -> Self {
|
||||
Self::from(unsafe { std::ffi::CStr::from_ptr(s).to_str().unwrap() })
|
||||
}
|
||||
}
|
||||
|
||||
/// Current mission.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub enum Mission {
|
||||
/// Mission name.
|
||||
Mission(String),
|
||||
#[default]
|
||||
/// Not in a mission.
|
||||
None,
|
||||
}
|
||||
|
||||
impl Mission {
|
||||
/// Convert the mission to a string.
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
Self::Mission(s) => s,
|
||||
Self::None => "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Mission {
|
||||
fn from(s: &str) -> Self {
|
||||
if s.is_empty() {
|
||||
Self::None
|
||||
} else {
|
||||
Self::Mission(s.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<*const libc::c_char> for Mission {
|
||||
#[allow(clippy::not_unsafe_ptr_arg_deref)]
|
||||
fn from(s: *const libc::c_char) -> Self {
|
||||
Self::from(unsafe { std::ffi::CStr::from_ptr(s).to_str().unwrap() })
|
||||
}
|
||||
}
|
||||
|
||||
/// Current server.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub enum Server {
|
||||
/// Server name
|
||||
Multiplayer(String),
|
||||
#[default]
|
||||
/// Singleplayer or no mission
|
||||
Singleplayer,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
/// Convert the server to a string.
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
Self::Multiplayer(s) => s,
|
||||
Self::Singleplayer => "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Server {
|
||||
fn from(s: &str) -> Self {
|
||||
if s.is_empty() {
|
||||
Self::Singleplayer
|
||||
} else {
|
||||
Self::Multiplayer(s.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<*const libc::c_char> for Server {
|
||||
#[allow(clippy::not_unsafe_ptr_arg_deref)]
|
||||
fn from(s: *const libc::c_char) -> Self {
|
||||
Self::from(unsafe { std::ffi::CStr::from_ptr(s).to_str().unwrap() })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn caller_empty() {
|
||||
assert_eq!(Caller::from(""), Caller::Unknown);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caller_zero() {
|
||||
assert_eq!(Caller::from("0"), Caller::Unknown);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_empty() {
|
||||
assert_eq!(Source::from(""), Source::Console);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_pbo() {
|
||||
let path = "x\\ctx\\addons\\main\\fn_armaContext.sqf";
|
||||
assert_eq!(Source::from(path), Source::Pbo(path.to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_file() {
|
||||
let path = env!("CARGO_MANIFEST_DIR");
|
||||
assert_eq!(Source::from(path), Source::File(path.to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mission_empty() {
|
||||
assert_eq!(Mission::from(""), Mission::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_empty() {
|
||||
assert_eq!(Server::from(""), Server::Singleplayer);
|
||||
}
|
||||
}
|
||||
40
vendor/arma-rs/src/call_context/manager.rs
vendored
Normal file
40
vendor/arma-rs/src/call_context/manager.rs
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use crate::ContextRequest;
|
||||
|
||||
use super::CallContextStackTrace;
|
||||
|
||||
/// Manages requesting and replacing the ArmaCallContext
|
||||
pub struct ArmaContextManager {
|
||||
pub(crate) request: RefCell<ContextRequest>,
|
||||
state: Rc<RefCell<Option<CallContextStackTrace>>>,
|
||||
}
|
||||
|
||||
impl ArmaContextManager {
|
||||
/// Create a new ArmaContextManager
|
||||
pub fn new(request: ContextRequest) -> Self {
|
||||
Self {
|
||||
request: RefCell::new(request),
|
||||
state: Rc::new(RefCell::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Request a new ArmaCallContext from Arma
|
||||
pub fn request(&self) -> CallContextStackTrace {
|
||||
// When the request is called, Arma will send the request to the extension
|
||||
// The extension will set the state to the request it just received
|
||||
unsafe {
|
||||
(self.request.borrow())();
|
||||
}
|
||||
// When the request function returns, the state has been set by Arma
|
||||
// It can now be taken and sent to the Context
|
||||
self.state
|
||||
.replace(None)
|
||||
.expect("Arma should've set the state")
|
||||
}
|
||||
|
||||
/// Replace the current ArmaCallContext with a new one
|
||||
pub fn replace(&self, value: Option<CallContextStackTrace>) {
|
||||
*self.state.borrow_mut() = value;
|
||||
}
|
||||
}
|
||||
6
vendor/arma-rs/src/call_context/mod.rs
vendored
Normal file
6
vendor/arma-rs/src/call_context/mod.rs
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
mod call;
|
||||
mod manager;
|
||||
mod stack;
|
||||
|
||||
pub use call::*;
|
||||
pub use manager::ArmaContextManager;
|
||||
72
vendor/arma-rs/src/call_context/stack.rs
vendored
Normal file
72
vendor/arma-rs/src/call_context/stack.rs
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
use std::ffi::CStr;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct RawStackTraceLine {
|
||||
// Line number in file (before preprocessing if preprocessed with line numbers)
|
||||
pub line_number: u32,
|
||||
// File offset in bytes from the start of the file (after preprocessing)
|
||||
pub file_offset: u32,
|
||||
// Filepath to the source file
|
||||
pub source_file: *const libc::c_char,
|
||||
// scopeName set on that level
|
||||
pub scope_name: *const libc::c_char,
|
||||
// Complete fileContent of the sourceFile (after preprocessing, can be combined with fileOffset to find exact location)
|
||||
pub file_content: *const libc::c_char,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct RawContextStackTrace {
|
||||
pub lines: *mut RawStackTraceLine,
|
||||
pub line_count: u32,
|
||||
}
|
||||
|
||||
impl RawContextStackTrace {
|
||||
pub fn to_lines(&self) -> Option<&[RawStackTraceLine]> {
|
||||
unsafe {
|
||||
self.lines
|
||||
.as_ref()
|
||||
.map(|lines_ptr| std::slice::from_raw_parts(lines_ptr, self.line_count as usize))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ArmaContextStackTrace {
|
||||
pub lines: Vec<ArmaStackTraceLine>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ArmaStackTraceLine {
|
||||
pub line_number: u32,
|
||||
pub file_offset: u32,
|
||||
pub source_file: String,
|
||||
pub scope_name: String,
|
||||
pub file_content: String,
|
||||
}
|
||||
|
||||
impl From<*const RawContextStackTrace> for ArmaContextStackTrace {
|
||||
fn from(raw: *const RawContextStackTrace) -> Self {
|
||||
unsafe {
|
||||
let raw = raw.as_ref().unwrap();
|
||||
let lines = raw
|
||||
.to_lines()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|line| ArmaStackTraceLine {
|
||||
line_number: line.line_number,
|
||||
file_offset: line.file_offset,
|
||||
source_file: CStr::from_ptr(line.source_file)
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
scope_name: CStr::from_ptr(line.scope_name)
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
file_content: CStr::from_ptr(line.file_content)
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
})
|
||||
.collect();
|
||||
Self { lines }
|
||||
}
|
||||
}
|
||||
}
|
||||
334
vendor/arma-rs/src/command.rs
vendored
Normal file
334
vendor/arma-rs/src/command.rs
vendored
Normal file
@@ -0,0 +1,334 @@
|
||||
use crate::call_context::{ArmaContextManager, CallContext, CallContextStackTrace};
|
||||
use crate::ext_result::IntoExtResult;
|
||||
use crate::flags::FeatureFlags;
|
||||
use crate::value::{FromArma, Value};
|
||||
use crate::Context;
|
||||
|
||||
type HandlerFunc = Box<
|
||||
dyn Fn(
|
||||
Context,
|
||||
&ArmaContextManager,
|
||||
*mut libc::c_char,
|
||||
libc::size_t,
|
||||
Option<*mut *mut i8>,
|
||||
Option<libc::c_int>,
|
||||
) -> libc::c_int,
|
||||
>;
|
||||
|
||||
#[doc(hidden)]
|
||||
/// A wrapper for `HandlerFunc`
|
||||
pub struct Handler {
|
||||
/// The function to call
|
||||
pub handler: HandlerFunc,
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Create a new handler from a Factory
|
||||
pub fn fn_handler<C, I, R>(command: C) -> Handler
|
||||
where
|
||||
C: Factory<I, R> + 'static,
|
||||
{
|
||||
Handler {
|
||||
handler: Box::new(
|
||||
move |context: Context,
|
||||
acm: &ArmaContextManager,
|
||||
output: *mut libc::c_char,
|
||||
size: libc::size_t,
|
||||
args: Option<*mut *mut i8>,
|
||||
count: Option<libc::c_int>|
|
||||
-> libc::c_int {
|
||||
unsafe { command.call(context, acm, output, size, args, count) }
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Execute a command
|
||||
pub trait Executor: 'static {
|
||||
/// # Safety
|
||||
/// This function is unsafe because it interacts with the C API.
|
||||
unsafe fn call(
|
||||
&self,
|
||||
context: Context,
|
||||
acm: &ArmaContextManager,
|
||||
output: *mut libc::c_char,
|
||||
size: libc::size_t,
|
||||
args: Option<*mut *mut i8>,
|
||||
count: Option<libc::c_int>,
|
||||
);
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// A factory for creating a command handler.
|
||||
/// Creates a handler from any function that optionally takes a context and up to 12 arguments.
|
||||
/// The arguments must implement `FromArma`
|
||||
/// The return value must implement `IntoExtResult`
|
||||
pub trait Factory<A, R> {
|
||||
/// # Safety
|
||||
/// This function is unsafe because it interacts with the C API.
|
||||
unsafe fn call(
|
||||
&self,
|
||||
context: Context,
|
||||
acm: &ArmaContextManager,
|
||||
output: *mut libc::c_char,
|
||||
size: libc::size_t,
|
||||
args: Option<*mut *mut i8>,
|
||||
count: Option<libc::c_int>,
|
||||
) -> libc::c_int;
|
||||
}
|
||||
|
||||
macro_rules! execute {
|
||||
($s:ident, $c:expr, $count:expr, $output:expr, $size:expr, $args:expr, ($( $vars:ident )*), ($( $param:ident, )*)) => {{
|
||||
let count = $count.unwrap_or_else(|| 0);
|
||||
if count != $c {
|
||||
return format!("2{}", count).parse::<libc::c_int>().unwrap();
|
||||
}
|
||||
if $c == 0 {
|
||||
handle_output_and_return(
|
||||
($s)($( $vars, )* $($param::from_arma("".to_string()).unwrap(),)*),
|
||||
$output,
|
||||
$size
|
||||
)
|
||||
} else {
|
||||
#[allow(unused_variables, unused_mut)]
|
||||
let mut argv: Vec<String> = {
|
||||
let argv: &[*mut libc::c_char; $c] = &*($args.unwrap() as *const [*mut i8; $c]);
|
||||
let mut argv = argv
|
||||
.to_vec()
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
std::ffi::CStr::from_ptr(s).to_string_lossy().to_string()
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
argv.reverse();
|
||||
argv
|
||||
};
|
||||
#[allow(unused_variables, unused_mut)] // Caused by the 0 loop
|
||||
let mut c = 0;
|
||||
#[allow(unused_assignments, clippy::mixed_read_write_in_expression)]
|
||||
handle_output_and_return(
|
||||
{
|
||||
($s)($( $vars, )* $(
|
||||
if let Ok(val) = $param::from_arma(argv.pop().unwrap()) {
|
||||
c += 1;
|
||||
val
|
||||
} else {
|
||||
return format!("3{}", c).parse::<libc::c_int>().unwrap()
|
||||
},
|
||||
)*)
|
||||
},
|
||||
$output,
|
||||
$size
|
||||
)
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! factory_tuple ({ $c: expr, $($param:ident)* } => {
|
||||
impl<$($param,)* ER> Executor for dyn Factory<($($param,)*), ER>
|
||||
where
|
||||
ER: 'static,
|
||||
$($param: FromArma + 'static,)*
|
||||
{
|
||||
unsafe fn call(
|
||||
&self,
|
||||
context: Context,
|
||||
acm: &ArmaContextManager,
|
||||
output: *mut libc::c_char,
|
||||
size: libc::size_t,
|
||||
args: Option<*mut *mut i8>,
|
||||
count: Option<libc::c_int>,
|
||||
) {
|
||||
self.call(context, acm, output, size, args, count);
|
||||
}
|
||||
}
|
||||
|
||||
// No context
|
||||
impl<Func, $($param,)* ER> Factory<($($param,)*), ER> for Func
|
||||
where
|
||||
ER: IntoExtResult + 'static,
|
||||
Func: Fn($($param),*) -> ER,
|
||||
$($param: FromArma,)*
|
||||
{
|
||||
#[allow(non_snake_case)]
|
||||
unsafe fn call(&self, _: Context, _: &ArmaContextManager, output: *mut libc::c_char, size: libc::size_t, args: Option<*mut *mut i8>, count: Option<libc::c_int>) -> libc::c_int {
|
||||
let count = count.unwrap_or_else(|| 0);
|
||||
if count != $c {
|
||||
return format!("2{}", count).parse::<libc::c_int>().unwrap();
|
||||
}
|
||||
if $c == 0 {
|
||||
handle_output_and_return(
|
||||
(self)($($param::from_arma("".to_string()).unwrap(),)*),
|
||||
output,
|
||||
size
|
||||
)
|
||||
} else {
|
||||
#[allow(unused_variables, unused_mut)]
|
||||
let mut argv: Vec<String> = {
|
||||
let argv: &[*mut libc::c_char; $c] = &*(args.unwrap() as *const [*mut i8; $c]);
|
||||
let mut argv = argv
|
||||
.to_vec()
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
std::ffi::CStr::from_ptr(s).to_string_lossy().to_string()
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
argv.reverse();
|
||||
argv
|
||||
};
|
||||
#[allow(unused_variables, unused_mut)] // Caused by the 0 loop
|
||||
let mut c = 0;
|
||||
#[allow(unused_assignments, clippy::mixed_read_write_in_expression)]
|
||||
handle_output_and_return(
|
||||
{
|
||||
(self)($(
|
||||
if let Ok(val) = $param::from_arma(argv.pop().unwrap()) {
|
||||
c += 1;
|
||||
val
|
||||
} else {
|
||||
return format!("3{}", c).parse::<libc::c_int>().unwrap()
|
||||
},
|
||||
)*)
|
||||
},
|
||||
output,
|
||||
size
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Context
|
||||
impl<Func, $($param,)* ER> Factory<(Context, $($param,)*), ER> for Func
|
||||
where
|
||||
ER: IntoExtResult + 'static,
|
||||
Func: Fn(Context, $($param),*) -> ER,
|
||||
$($param: FromArma,)*
|
||||
{
|
||||
#[allow(non_snake_case)]
|
||||
unsafe fn call(&self, context: Context, _: &ArmaContextManager, output: *mut libc::c_char, size: libc::size_t, args: Option<*mut *mut i8>, count: Option<libc::c_int>) -> libc::c_int {
|
||||
execute!(self, $c, count, output, size, args, (context), ($($param,)*))
|
||||
}
|
||||
}
|
||||
|
||||
// Call Context
|
||||
impl<Func, $($param,)* ER> Factory<(CallContext, $($param,)*), ER> for Func
|
||||
where
|
||||
ER: IntoExtResult + 'static,
|
||||
Func: Fn(CallContext, $($param),*) -> ER,
|
||||
$($param: FromArma,)*
|
||||
{
|
||||
#[allow(non_snake_case)]
|
||||
unsafe fn call(&self, _: Context, acm: &ArmaContextManager, output: *mut libc::c_char, size: libc::size_t, args: Option<*mut *mut i8>, count: Option<libc::c_int>) -> libc::c_int {
|
||||
crate::RVExtensionFeatureFlags = FeatureFlags::default().with_context_stack_trace(false).as_bits();
|
||||
let call_context = acm.request().into_without_stack();
|
||||
execute!(self, $c, count, output, size, args, (call_context), ($($param,)*))
|
||||
}
|
||||
}
|
||||
|
||||
// Call Context with Stack Trace
|
||||
impl<Func, $($param,)* ER> Factory<(CallContextStackTrace, $($param,)*), ER> for Func
|
||||
where
|
||||
ER: IntoExtResult + 'static,
|
||||
Func: Fn(CallContextStackTrace, $($param),*) -> ER,
|
||||
$($param: FromArma,)*
|
||||
{
|
||||
#[allow(non_snake_case)]
|
||||
unsafe fn call(&self, _: Context, acm: &ArmaContextManager, output: *mut libc::c_char, size: libc::size_t, args: Option<*mut *mut i8>, count: Option<libc::c_int>) -> libc::c_int {
|
||||
crate::RVExtensionFeatureFlags = FeatureFlags::default().with_context_stack_trace(true).as_bits();
|
||||
let call_context = acm.request();
|
||||
execute!(self, $c, count, output, size, args, (call_context), ($($param,)*))
|
||||
}
|
||||
}
|
||||
|
||||
// Context & Call Context
|
||||
impl<Func, $($param,)* ER> Factory<(Context, CallContext, $($param,)*), ER> for Func
|
||||
where
|
||||
ER: IntoExtResult + 'static,
|
||||
Func: Fn(Context, CallContext, $($param),*) -> ER,
|
||||
$($param: FromArma,)*
|
||||
{
|
||||
#[allow(non_snake_case)]
|
||||
unsafe fn call(&self, context: Context, acm: &ArmaContextManager, output: *mut libc::c_char, size: libc::size_t, args: Option<*mut *mut i8>, count: Option<libc::c_int>) -> libc::c_int {
|
||||
crate::RVExtensionFeatureFlags = FeatureFlags::default().with_context_stack_trace(false).as_bits();
|
||||
let call_context = acm.request().into_without_stack();
|
||||
execute!(self, $c, count, output, size, args, (context call_context), ($($param,)*))
|
||||
}
|
||||
}
|
||||
|
||||
// Context & Call Context with Stack Trace
|
||||
impl<Func, $($param,)* ER> Factory<(Context, CallContextStackTrace, $($param,)*), ER> for Func
|
||||
where
|
||||
ER: IntoExtResult + 'static,
|
||||
Func: Fn(Context, CallContextStackTrace, $($param),*) -> ER,
|
||||
$($param: FromArma,)*
|
||||
{
|
||||
#[allow(non_snake_case)]
|
||||
unsafe fn call(&self, context: Context, acm: &ArmaContextManager, output: *mut libc::c_char, size: libc::size_t, args: Option<*mut *mut i8>, count: Option<libc::c_int>) -> libc::c_int {
|
||||
crate::RVExtensionFeatureFlags = FeatureFlags::default().with_context_stack_trace(true).as_bits();
|
||||
let call_context = acm.request();
|
||||
execute!(self, $c, count, output, size, args, (context call_context), ($($param,)*))
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
unsafe fn handle_output_and_return<R>(
|
||||
ret: R,
|
||||
output: *mut libc::c_char,
|
||||
size: libc::size_t,
|
||||
) -> libc::c_int
|
||||
where
|
||||
R: IntoExtResult + 'static,
|
||||
{
|
||||
let ret = ret.to_ext_result();
|
||||
let ok = ret.is_ok();
|
||||
if crate::write_cstr(
|
||||
{
|
||||
let value = match ret {
|
||||
Ok(x) | Err(x) => x,
|
||||
};
|
||||
match value {
|
||||
Value::String(s) => s,
|
||||
v => v.to_string(),
|
||||
}
|
||||
},
|
||||
output,
|
||||
size,
|
||||
)
|
||||
.is_none()
|
||||
{
|
||||
4
|
||||
} else if ok {
|
||||
0
|
||||
} else {
|
||||
9
|
||||
}
|
||||
}
|
||||
|
||||
factory_tuple! { 0, }
|
||||
factory_tuple! { 1, A }
|
||||
factory_tuple! { 2, A B }
|
||||
factory_tuple! { 3, A B C }
|
||||
factory_tuple! { 4, A B C D }
|
||||
factory_tuple! { 5, A B C D E }
|
||||
factory_tuple! { 6, A B C D E F }
|
||||
factory_tuple! { 7, A B C D E F G }
|
||||
factory_tuple! { 8, A B C D E F G H }
|
||||
factory_tuple! { 9, A B C D E F G H I }
|
||||
factory_tuple! { 10, A B C D E F G H I J }
|
||||
factory_tuple! { 11, A B C D E F G H I J K }
|
||||
factory_tuple! { 12, A B C D E F G H I J K L }
|
||||
factory_tuple! { 13, A B C D E F G H I J K L M }
|
||||
factory_tuple! { 14, A B C D E F G H I J K L M N }
|
||||
factory_tuple! { 15, A B C D E F G H I J K L M N O }
|
||||
factory_tuple! { 16, A B C D E F G H I J K L M N O P }
|
||||
factory_tuple! { 17, A B C D E F G H I J K L M N O P Q }
|
||||
factory_tuple! { 18, A B C D E F G H I J K L M N O P Q R }
|
||||
factory_tuple! { 19, A B C D E F G H I J K L M N O P Q R S }
|
||||
factory_tuple! { 20, A B C D E F G H I J K L M N O P Q R S T }
|
||||
factory_tuple! { 21, A B C D E F G H I J K L M N O P Q R S T U }
|
||||
factory_tuple! { 22, A B C D E F G H I J K L M N O P Q R S T U V }
|
||||
factory_tuple! { 23, A B C D E F G H I J K L M N O P Q R S T U V W }
|
||||
factory_tuple! { 24, A B C D E F G H I J K L M N O P Q R S T U V W X }
|
||||
factory_tuple! { 25, A B C D E F G H I J K L M N O P Q R S T U V W X Y }
|
||||
factory_tuple! { 26, A B C D E F G H I J K L M N O P Q R S T U V W X Y Z }
|
||||
37
vendor/arma-rs/src/context/global.rs
vendored
Normal file
37
vendor/arma-rs/src/context/global.rs
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{ContextState, State};
|
||||
|
||||
/// Contains information about the extension
|
||||
pub struct GlobalContext {
|
||||
version: String,
|
||||
state: Arc<State>,
|
||||
}
|
||||
|
||||
impl GlobalContext {
|
||||
pub(crate) fn new(version: String, state: Arc<State>) -> Self {
|
||||
Self { version, state }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Version of the Arma extension
|
||||
pub fn version(&self) -> &str {
|
||||
&self.version
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextState for GlobalContext {
|
||||
fn get<T>(&self) -> Option<&T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
self.state.try_get()
|
||||
}
|
||||
|
||||
fn set<T>(&self, value: T) -> bool
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
self.state.set(value)
|
||||
}
|
||||
}
|
||||
30
vendor/arma-rs/src/context/group.rs
vendored
Normal file
30
vendor/arma-rs/src/context/group.rs
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{ContextState, State};
|
||||
|
||||
/// Contains information about the current group
|
||||
pub struct GroupContext {
|
||||
state: Arc<State>,
|
||||
}
|
||||
|
||||
impl GroupContext {
|
||||
pub(crate) fn new(state: Arc<State>) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextState for GroupContext {
|
||||
fn get<T>(&self) -> Option<&T>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
self.state.try_get()
|
||||
}
|
||||
|
||||
fn set<T>(&self, value: T) -> bool
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
self.state.set(value)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user