mirror of
https://github.com/valmojr/armatak.git
synced 2026-06-13 16:03:31 +00:00
Compare commits
2 Commits
v1.5.0
...
fuck_arma_
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a5a7a17a3 | |||
| 0486f2a285 |
@@ -38,31 +38,28 @@ preset = "Hemtt"
|
||||
[hemtt.launch.default]
|
||||
workshop = [
|
||||
"450814997", # CBA_A3
|
||||
"463939057", # ace
|
||||
"463939057", # ACE
|
||||
"751965892", # ACRE2
|
||||
"2522638637", # ACE3 Arsenal Extended - Core
|
||||
"2522638637", # ACE Extended Arsenal
|
||||
"333310405", # Enhanced Movement
|
||||
"2034363662", # Enhanced Movement Rework
|
||||
"2941986336", # Hatchet Interaction Framework - Stable Version
|
||||
"1745501605", # Hatchet H-60 pack - Stable Version
|
||||
"843577117", # RHSUSAF
|
||||
"3147473073", # TOTT Core
|
||||
"843425103", # RHSAFRF
|
||||
"843632231", # RHSSAF
|
||||
"843593391", # RHSGREF
|
||||
"1673456286", # 3CB Factions
|
||||
"623475643", # 3den Enhanced
|
||||
"520618345", # Jbad
|
||||
"1779063631", # Zeus Enhanced
|
||||
"2397360831", # USAF Mod - Main
|
||||
"2397376046", # USAF Mod - Utility
|
||||
"3147476552", # TOTT Optics
|
||||
"583496184", # CUP Terrains - Core
|
||||
"2560276469", # Restrict Markers
|
||||
"1858075458", # LAMBS_Danger.fsm
|
||||
"3407948300", # JSRS SOUNDMOD 2025
|
||||
"2834576684", # ITN - Illuminate The Night
|
||||
"3375788189", # Immersion Cigs - Rewrite
|
||||
"2095827925", # Brighter Flares
|
||||
"2257686620", # Blastcore Murr Edition
|
||||
"2260572637", # BettIR NVG
|
||||
"3493557838", # Broad Spectrum Warfare
|
||||
"1291778160", # Hellanmaa
|
||||
"3533734689", # FPV
|
||||
"583496184", # CUP Terrains - Core
|
||||
"3078351739", # Kunduz River
|
||||
"1858075458", # LAMBS_Danger.fsm
|
||||
"1808238502", # LAMBS_Suppression
|
||||
"3425368881", # M4A1_URGI
|
||||
"2268351256", # Tier One Weapons
|
||||
"2560276469", # Restrict Markers
|
||||
"3493557838" # Ballad of the Green Berets
|
||||
]
|
||||
|
||||
parameters = [
|
||||
|
||||
@@ -10,79 +10,44 @@ class armatak_udp_socket_start_dialog {
|
||||
class armatak_gui_module_udp_socket_dialog_main_frame: RscBackground {
|
||||
idc = 16960;
|
||||
x = "0.386562 * safezoneW + safezoneX";
|
||||
y = "0.357 * safezoneH + safezoneY";
|
||||
y = "0.401 * safezoneH + safezoneY";
|
||||
w = "0.216563 * safezoneW";
|
||||
h = "0.418 * safezoneH";
|
||||
h = "0.242 * safezoneH";
|
||||
colorBackground[]={0,0,0,0.45};
|
||||
};
|
||||
};
|
||||
class Controls {
|
||||
class armatak_gui_module_udp_socket_dialog_address_edit: RscEdit {
|
||||
idc = 16961;
|
||||
text = "192.168.15.121";
|
||||
x = "0.391719 * safezoneW + safezoneX";
|
||||
y = "0.401 * safezoneH + safezoneY";
|
||||
w = "0.20625 * safezoneW";
|
||||
h = "0.044 * safezoneH";
|
||||
colorBackground[]={0,0,0,0.5};
|
||||
};
|
||||
class armatak_gui_module_udp_socket_dialog_gnss_port_edit: RscEdit {
|
||||
idc = 16962;
|
||||
text = "4349";
|
||||
x = "0.391719 * safezoneW + safezoneX";
|
||||
y = "0.478 * safezoneH + safezoneY";
|
||||
w = "0.20625 * safezoneW";
|
||||
h = "0.044 * safezoneH";
|
||||
colorBackground[]={0,0,0,0.5};
|
||||
};
|
||||
class armatak_gui_module_udp_socket_dialog_mavlink_port_edit: RscEdit {
|
||||
idc = 16967;
|
||||
text = "14550";
|
||||
x = "0.391719 * safezoneW + safezoneX";
|
||||
y = "0.555 * safezoneH + safezoneY";
|
||||
w = "0.20625 * safezoneW";
|
||||
h = "0.044 * safezoneH";
|
||||
colorBackground[]={0,0,0,0.5};
|
||||
};
|
||||
class armatak_gui_module_udp_socket_dialog_video_feed_url_edit: RscEdit {
|
||||
idc = 16969;
|
||||
text = "";
|
||||
x = "0.391719 * safezoneW + safezoneX";
|
||||
y = "0.632 * safezoneH + safezoneY";
|
||||
w = "0.20625 * safezoneW";
|
||||
h = "0.044 * safezoneH";
|
||||
colorBackground[]={0,0,0,0.5};
|
||||
tooltip = "Optional shared feed URL. If empty, the UAV 3DEN URL is used first, then a local RTP fallback.";
|
||||
};
|
||||
class armatak_gui_module_udp_socket_dialog_address_text: RscText {
|
||||
idc = 16963;
|
||||
text = "EUD's Address";
|
||||
x = "0.391719 * safezoneW + safezoneX";
|
||||
y = "0.368 * safezoneH + safezoneY";
|
||||
w = "0.20625 * safezoneW";
|
||||
h = "0.033 * safezoneH";
|
||||
};
|
||||
class armatak_gui_module_udp_socket_dialog_gnss_port_text: RscText {
|
||||
idc = 16964;
|
||||
text = "Network GNSS Port";
|
||||
text = "168.15.0.3";
|
||||
x = "0.391719 * safezoneW + safezoneX";
|
||||
y = "0.445 * safezoneH + safezoneY";
|
||||
w = "0.20625 * safezoneW";
|
||||
h = "0.033 * safezoneH";
|
||||
h = "0.044 * safezoneH";
|
||||
colorBackground[]={0,0,0,0.5};
|
||||
};
|
||||
class armatak_gui_module_udp_socket_dialog_mavlink_port_text: RscText {
|
||||
idc = 16968;
|
||||
text = "Mavlink Port";
|
||||
class armatak_gui_module_udp_socket_dialog_address_port_edit: RscEdit {
|
||||
idc = 16962;
|
||||
text = "4349";
|
||||
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_udp_socket_dialog_address_text: RscText {
|
||||
idc = 16963;
|
||||
text = "Phone's Socket Local Address";
|
||||
x = "0.391719 * safezoneW + safezoneX";
|
||||
y = "0.412 * safezoneH + safezoneY";
|
||||
w = "0.20625 * safezoneW";
|
||||
h = "0.033 * safezoneH";
|
||||
};
|
||||
class armatak_gui_module_udp_socket_dialog_video_feed_url_text: RscText {
|
||||
idc = 16970;
|
||||
text = "Video Feed URL (Optional)";
|
||||
class armatak_gui_module_udp_socket_dialog_address_port_text: RscText {
|
||||
idc = 16964;
|
||||
text = "Phone's Socket Local Port";
|
||||
x = "0.391719 * safezoneW + safezoneX";
|
||||
y = "0.599 * safezoneH + safezoneY";
|
||||
y = "0.489 * safezoneH + safezoneY";
|
||||
w = "0.20625 * safezoneW";
|
||||
h = "0.033 * safezoneH";
|
||||
};
|
||||
@@ -91,7 +56,7 @@ class armatak_udp_socket_start_dialog {
|
||||
text = "Cancel";
|
||||
action = "closeDialog 2;";
|
||||
x = "0.551563 * safezoneW + safezoneX";
|
||||
y = "0.709 * safezoneH + safezoneY";
|
||||
y = "0.577 * safezoneH + safezoneY";
|
||||
w = "0.0464063 * safezoneW";
|
||||
h = "0.055 * safezoneH";
|
||||
};
|
||||
@@ -100,7 +65,7 @@ class armatak_udp_socket_start_dialog {
|
||||
text = "Ok";
|
||||
action = QUOTE(call FUNC(startUDPSocket));
|
||||
x = "0.5 * safezoneW + safezoneX";
|
||||
y = "0.709 * safezoneH + safezoneY";
|
||||
y = "0.577 * safezoneH + safezoneY";
|
||||
w = "0.0464063 * safezoneW";
|
||||
h = "0.055 * safezoneH";
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
params ["_logic"];
|
||||
|
||||
private _socket_is_running = player getVariable [QGVAR(eudConnected), false];
|
||||
_socket_is_running = player getVariable [QGVAR(eudConnected), false];
|
||||
|
||||
if (_socket_is_running) exitWith {
|
||||
["Socket is already running", "error", "UDP Socket"] call EFUNC(main,notify);
|
||||
@@ -11,34 +11,20 @@ if (_socket_is_running) exitWith {
|
||||
|
||||
disableSerialization;
|
||||
|
||||
private _eud_address = ctrlText 16961;
|
||||
private _gnss_port = ctrlText 16962;
|
||||
private _mavlink_port = ctrlText 16967;
|
||||
private _video_feed_url = ctrlText 16969;
|
||||
_udp_socket_instance_address = ctrlText 16961;
|
||||
_udp_socket_instance_port = ctrlText 16962;
|
||||
|
||||
private _udp_socket_fulladdress = _eud_address + ":" + _gnss_port;
|
||||
private _mavlink_address = _eud_address + ":" + _mavlink_port;
|
||||
_udp_socket_fulladdress = ((_udp_socket_instance_address) + ":" + (_udp_socket_instance_port));
|
||||
|
||||
player setVariable [QGVAR(udp_socket_address), _udp_socket_fulladdress];
|
||||
player setVariable [QGVAR(mavlink_address), _mavlink_address];
|
||||
player setVariable [QGVAR(video_feed_url), trim _video_feed_url];
|
||||
player setVariable [QGVAR(eudConnected), true];
|
||||
|
||||
private _advertised_video_uri = [objNull] call EFUNC(uav,resolveVideoUri);
|
||||
|
||||
"armatak" callExtension ["udp_socket:start", [_udp_socket_fulladdress]];
|
||||
"armatak" callExtension ["uas:start_endpoint", [parseNumber _mavlink_port]];
|
||||
|
||||
private _mdnsInstanceName = format ["ArmaTAK-%1", name player];
|
||||
"armatak" callExtension ["mdns:start_uas_advertisement", [_mdnsInstanceName, parseNumber _mavlink_port, _advertised_video_uri]];
|
||||
"armatak" callExtension ["log", [["info", format ["Client UDP socket started for %1, MAVLink target set to %2 and advertised video URI set to %3", _udp_socket_fulladdress, _mavlink_address, _advertised_video_uri]]]];
|
||||
|
||||
call EFUNC(uav,startMavlinkBroadcast);
|
||||
|
||||
[{
|
||||
if !(player getVariable [QGVAR(eudConnected), false]) exitWith {};
|
||||
|
||||
"armatak" callExtension ["udp_socket:send_gps_cot", [player call FUNC(extractClientPosition)]];
|
||||
if (player getVariable [QGVAR(eudConnected), false]) then {
|
||||
"armatak" callExtension ["udp_socket:send_gps_cot", [player call FUNC(extractClientPosition)]];
|
||||
};
|
||||
}, 0.5, []] call CBA_fnc_addPerFrameHandler;
|
||||
|
||||
deleteVehicle _logic;
|
||||
|
||||
@@ -78,12 +78,12 @@ class Cfg3den {
|
||||
condition = "objectVehicle";
|
||||
typeName = "STRING";
|
||||
};
|
||||
class armatak_attribute_marker_video_url {
|
||||
displayName = "Video Feed URL";
|
||||
tooltip = "Shared UAV video URL. This per-vehicle value overrides the optional session video URL from Connect to EUD.";
|
||||
property = "armatak_attribute_marker_video_url";
|
||||
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_marker_video_url',_value]";
|
||||
expression = "_this setVariable ['armatak_attribute_video_url',_value]";
|
||||
defaultValue = "''";
|
||||
condition = "objectVehicle";
|
||||
typeName = "STRING";
|
||||
|
||||
@@ -19,18 +19,12 @@ class CfgFunctions {
|
||||
class send_marker_cot {
|
||||
file = "\armatak\armatak\addons\main\functions\api\fn_send_marker_cot.sqf";
|
||||
};
|
||||
class send_uas_platform_cot {
|
||||
file = "\armatak\armatak\addons\main\functions\api\fn_send_uas_platform_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 set_uas_camera_override {
|
||||
file = "\armatak\armatak\addons\main\functions\api\fn_set_uas_camera_override.sqf";
|
||||
};
|
||||
class stop_tcp_socket {
|
||||
file = "\armatak\armatak\addons\main\functions\api\fn_stop_tcp_socket.sqf";
|
||||
};
|
||||
@@ -43,18 +37,12 @@ class CfgFunctions {
|
||||
class extract_marker_callsign {
|
||||
file = "\armatak\armatak\addons\main\functions\extract_data\fn_extract_marker_callsign.sqf";
|
||||
};
|
||||
class extract_marker_video_url {
|
||||
file = "\armatak\armatak\addons\main\functions\extract_data\fn_extract_marker_video_url.sqf";
|
||||
};
|
||||
class extract_role {
|
||||
file = "\armatak\armatak\addons\main\functions\extract_data\fn_extract_role.sqf";
|
||||
};
|
||||
class extract_sensor_data {
|
||||
file = "\armatak\armatak\addons\main\functions\extract_data\fn_extract_sensor_data.sqf";
|
||||
};
|
||||
class extract_uas_camera_data {
|
||||
file = "\armatak\armatak\addons\main\functions\extract_data\fn_extract_uas_camera_data.sqf";
|
||||
};
|
||||
class extract_unit_callsign {
|
||||
file = "\armatak\armatak\addons\main\functions\extract_data\fn_extract_unit_callsign.sqf";
|
||||
};
|
||||
@@ -140,10 +128,6 @@ class CfgFunctions {
|
||||
class convert_to_rut_mandol {
|
||||
file = "\armatak\armatak\addons\main\functions\map\fn_convert_to_rut_mandol.sqf";
|
||||
};
|
||||
class convert_to_hellanmaa {
|
||||
file = "\armatak\armatak\addons\main\functions\map\fn_convert_to_hellanmaa.sqf";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -13,9 +13,6 @@ addMissionEventHandler ["ExtensionCallback", {
|
||||
};
|
||||
case "EUD Disconnected": {
|
||||
SETVAR(player,EGVAR(client,eudConnected),false);
|
||||
call EFUNC(uav,stopMavlinkBroadcast);
|
||||
"armatak" callExtension ["uas:stop_endpoint", []];
|
||||
"armatak" callExtension ["mdns:stop", []];
|
||||
};
|
||||
default {};
|
||||
};
|
||||
@@ -28,52 +25,10 @@ addMissionEventHandler ["ExtensionCallback", {
|
||||
|
||||
if (_function == "UDP Socket is not running") then {
|
||||
SETVAR(player,EGVAR(client,eudConnected),false);
|
||||
call EFUNC(uav,stopMavlinkBroadcast);
|
||||
"armatak" callExtension ["uas:stop_endpoint", []];
|
||||
"armatak" callExtension ["mdns:stop", []];
|
||||
};
|
||||
|
||||
if (_function == "failed to bind UDP socket") then {
|
||||
SETVAR(player,EGVAR(client,eudConnected),false);
|
||||
call EFUNC(uav,stopMavlinkBroadcast);
|
||||
"armatak" callExtension ["uas:stop_endpoint", []];
|
||||
"armatak" callExtension ["mdns:stop", []];
|
||||
};
|
||||
};
|
||||
case "MAVLINK UDP ERROR": {
|
||||
_message = _function;
|
||||
if (_data isNotEqualTo "") then {
|
||||
_message = format ["%1: %2", _function, _data];
|
||||
};
|
||||
|
||||
[_message, "warning", _name] call FUNC(notify);
|
||||
};
|
||||
case "MAVLINK UDP": {
|
||||
private _history = missionNamespace getVariable ["armatak_uav_mavlink_callback_history", []];
|
||||
_history pushBack [diag_tickTime, _function, _data];
|
||||
if ((count _history) > 50) then {
|
||||
_history deleteRange [0, (count _history) - 50];
|
||||
};
|
||||
missionNamespace setVariable ["armatak_uav_mavlink_callback_history", _history];
|
||||
missionNamespace setVariable ["armatak_uav_last_mavlink_callback", [diag_tickTime, _function, _data]];
|
||||
|
||||
switch (_function) do {
|
||||
case "COMMAND_LONG";
|
||||
case "COMMAND_INT";
|
||||
case "COMMAND_ACK";
|
||||
case "MISSION_COUNT";
|
||||
case "MISSION_ITEM";
|
||||
case "MISSION_ITEM_INT";
|
||||
case "MISSION_CLEAR_ALL";
|
||||
case "MISSION_SET_CURRENT";
|
||||
case "SET_HOME_POSITION";
|
||||
case "SET_MODE";
|
||||
case "SET_POSITION_TARGET_GLOBAL_INT";
|
||||
case "MANUAL_CONTROL": {
|
||||
"armatak" callExtension ["log", [["info", format ["MAVLINK UDP CALLBACK %1 %2", _function, _data]]]];
|
||||
[_function, _data] call EFUNC(uav,handleMavlinkCallback);
|
||||
};
|
||||
default {};
|
||||
};
|
||||
};
|
||||
case "TCP SOCKET": {
|
||||
@@ -90,12 +45,6 @@ addMissionEventHandler ["ExtensionCallback", {
|
||||
case "VIDEO": {
|
||||
[_function, "success", _name] call FUNC(notify);
|
||||
};
|
||||
case "MDNS": {
|
||||
[_function, "success", _name] call FUNC(notify);
|
||||
};
|
||||
case "MDNS ERROR": {
|
||||
[_function, "warning", _name] call FUNC(notify);
|
||||
};
|
||||
case "VIDEO ERROR": {
|
||||
[_function, "error", _name] call FUNC(notify);
|
||||
|
||||
|
||||
@@ -4,4 +4,34 @@
|
||||
|
||||
params["_drone"];
|
||||
|
||||
[_drone] call armatak_fnc_send_uas_platform_cot;
|
||||
private _atak_role = "a-f-A";
|
||||
private _atak_callsign = [_drone] call armatak_fnc_extract_marker_callsign;
|
||||
|
||||
switch (side _drone) do {
|
||||
case "WEST": {
|
||||
_atak_role = "a-f-A-M-F-Q"
|
||||
};
|
||||
case "EAST": {
|
||||
_atak_role = "a-h-A-M-F-Q"
|
||||
};
|
||||
case "INDEPENDENT": {
|
||||
_atak_role = "a-n-A-M-F-Q"
|
||||
};
|
||||
case "CIVILIAN": {
|
||||
_atak_role = "a-f-A-C"
|
||||
};
|
||||
default {
|
||||
_atak_role = "a-f-A-M-F-Q"
|
||||
};
|
||||
};
|
||||
|
||||
_pre_defined_role = _drone getVariable "_atak_group_role";
|
||||
|
||||
if (!isNil "_pre_defined_role") then {
|
||||
_callsign = _pre_defined_role;
|
||||
};
|
||||
|
||||
_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;
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
params ["_drone"];
|
||||
|
||||
private _uuid = _drone call armatak_fnc_extract_uuid;
|
||||
private _uavControl = UAVControl _drone;
|
||||
private _controller = _uavControl param [0, objNull];
|
||||
private _controller_uid = if (!isNull _controller) then { [_controller] call armatak_fnc_extract_uuid } else { _drone getVariable ["armatak_uas_controller_uid", _uuid] };
|
||||
private _callsign = [_drone] call armatak_fnc_extract_marker_callsign;
|
||||
private _video_url = [_drone] call armatak_fnc_extract_marker_video_url;
|
||||
|
||||
private _atak_role = "a-f-A-M-H-Q";
|
||||
switch (side _drone) do {
|
||||
case west: {
|
||||
_atak_role = "a-f-A-M-H-Q";
|
||||
};
|
||||
case east: {
|
||||
_atak_role = "a-h-A-M-H-Q";
|
||||
};
|
||||
case independent: {
|
||||
_atak_role = "a-n-A-M-H-Q";
|
||||
};
|
||||
case civilian: {
|
||||
_atak_role = "a-f-A-C";
|
||||
};
|
||||
default {
|
||||
_atak_role = "a-f-A-M-H-Q";
|
||||
};
|
||||
};
|
||||
|
||||
private _position = _drone call armatak_client_fnc_extractClientPosition;
|
||||
private _lat = _position select 1;
|
||||
private _lon = _position select 2;
|
||||
private _hae = _position select 3;
|
||||
private _course = _position select 5;
|
||||
private _speed = _position select 6;
|
||||
|
||||
private _cameraData = [_drone] call armatak_fnc_extract_uas_camera_data;
|
||||
private _azimuth = _cameraData select 0;
|
||||
private _elevation = _cameraData select 1;
|
||||
private _fov = _cameraData select 2;
|
||||
private _range = _cameraData select 3;
|
||||
private _vfov = _drone getVariable ["armatak_uas_vfov", _fov];
|
||||
|
||||
private _yaw = round (getDir _drone);
|
||||
private _pitch = (vectorDir _drone) select 2;
|
||||
private _roll = (vectorUp _drone) select 0;
|
||||
private _isFlying = parseNumber (isEngineOn _drone);
|
||||
private _hal = ((getPosATL _drone) select 2) max 0;
|
||||
private _vehicleType = if (_video_url == "") then {
|
||||
typeOf _drone
|
||||
} else {
|
||||
format ["%1|armatak_video_url=%2", typeOf _drone, _video_url]
|
||||
};
|
||||
|
||||
private _payload = [
|
||||
_uuid,
|
||||
_atak_role,
|
||||
_callsign,
|
||||
_lat,
|
||||
_lon,
|
||||
_hae,
|
||||
_course,
|
||||
_speed,
|
||||
_azimuth,
|
||||
_elevation,
|
||||
_fov,
|
||||
_vfov,
|
||||
_range,
|
||||
_yaw,
|
||||
_pitch,
|
||||
_roll,
|
||||
_hal,
|
||||
_vehicleType,
|
||||
_isFlying,
|
||||
_controller_uid
|
||||
];
|
||||
|
||||
"armatak" callExtension ["tcp_socket:cot:uas_platform", [_payload]];
|
||||
@@ -1,22 +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] call armatak_fnc_extract_marker_video_url;
|
||||
private _video_url = _drone getVariable ["armatak_attribute_video_url", ""];
|
||||
if (_video_url == "") exitWith {};
|
||||
|
||||
private _uuid = _drone call armatak_fnc_extract_uuid;
|
||||
private _video_uid = _uuid + "-video";
|
||||
private _uuid = _drone call armatak_fnc_extract_uuid;
|
||||
private _sensor_uid = _uuid + "-sensor";
|
||||
private _callsign = [_drone] call armatak_fnc_extract_marker_callsign;
|
||||
private _callsign = [_drone] call armatak_fnc_extract_marker_callsign;
|
||||
|
||||
private _position = _drone call armatak_client_fnc_extractClientPosition;
|
||||
private _lat = _position select 1;
|
||||
private _lon = _position select 2;
|
||||
private _hae = _position select 3;
|
||||
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 _cameraData = [_drone] call armatak_fnc_extract_uas_camera_data;
|
||||
private _azimuth = _cameraData select 0;
|
||||
private _fov = _cameraData select 2;
|
||||
private _range = _cameraData select 3;
|
||||
private _azimuth = parseNumber ((getDir _drone) toFixed 0);
|
||||
|
||||
private _payload = [_sensor_uid, _video_uid, _callsign, _lat, _lon, _hae, _azimuth, _fov, _range];
|
||||
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]];
|
||||
|
||||
@@ -1,20 +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] call armatak_fnc_extract_marker_video_url;
|
||||
private _video_url = _drone getVariable ["armatak_attribute_video_url", ""];
|
||||
if (_video_url == "") exitWith {};
|
||||
|
||||
private _uuid = _drone call armatak_fnc_extract_uuid;
|
||||
private _video_uid = _uuid + "-video";
|
||||
private _uuid = _drone call armatak_fnc_extract_uuid;
|
||||
private _callsign = [_drone] call armatak_fnc_extract_marker_callsign;
|
||||
|
||||
private _signature = format ["%1|%2|%3", _video_uid, _callsign, _video_url];
|
||||
private _nextRefreshAt = _drone getVariable ["armatak_next_uas_video_refresh_at", 0];
|
||||
private _lastSignature = _drone getVariable ["armatak_last_uas_video_signature", ""];
|
||||
|
||||
if (_signature == _lastSignature && {diag_tickTime < _nextRefreshAt}) exitWith {};
|
||||
|
||||
_drone setVariable ["armatak_last_uas_video_signature", _signature, false];
|
||||
_drone setVariable ["armatak_next_uas_video_refresh_at", diag_tickTime + 300, false];
|
||||
|
||||
private _payload = [_video_uid, _callsign, _video_url];
|
||||
private _payload = [_uuid, _callsign, _video_url];
|
||||
"armatak" callExtension ["tcp_socket:cot:uas_video", [_payload]];
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
params ["_drone", ["_cameraData", []]];
|
||||
|
||||
if (isNull _drone) exitWith {};
|
||||
|
||||
if ((_cameraData isEqualType []) && {(count _cameraData) >= 6}) then {
|
||||
_drone setVariable ["armatak_uas_camera_data_override", _cameraData + [serverTime], false];
|
||||
} else {
|
||||
_drone setVariable ["armatak_uas_camera_data_override", nil, false];
|
||||
};
|
||||
@@ -5,45 +5,29 @@
|
||||
params["_unit"];
|
||||
|
||||
private _callsign = "";
|
||||
private _displayName = localize (getText (configOf _unit >> "displayName"));
|
||||
private _markerCallsignOverride = _unit getVariable ["armatak_attribute_marker_callsign", ""];
|
||||
|
||||
if (_markerCallsignOverride isNotEqualTo "") exitWith {
|
||||
_markerCallsignOverride
|
||||
};
|
||||
|
||||
if (_displayName isEqualTo "") then {
|
||||
_displayName = typeOf _unit;
|
||||
};
|
||||
|
||||
private _vehicleName = vehicleVarName _unit;
|
||||
|
||||
if ((([_unit] call BIS_fnc_objectType) select 0) == "Vehicle") then {
|
||||
_callsign = [_displayName, _vehicleName] select (_vehicleName isNotEqualTo "");
|
||||
_callsign = getText (configOf _unit >> "displayName");
|
||||
|
||||
if (!isNull driver _unit) then {
|
||||
_callsign = _displayName + " | " + ([name (driver _unit)] call armatak_fnc_shorten_name);
|
||||
_callsign = getText (configOf _unit >> "displayName") + " | " + ([name (driver _unit)] call armatak_fnc_shorten_name);
|
||||
};
|
||||
};
|
||||
|
||||
if (unitIsUAV _unit) then {
|
||||
_callsign = [_displayName, _vehicleName] select (_vehicleName isNotEqualTo "");
|
||||
|
||||
private _uavControl = UAVControl _unit;
|
||||
private _controller = _uavControl param [0, objNull];
|
||||
if (!isNull _controller) then {
|
||||
_callsign = _callsign + " | " + ([name _controller] call armatak_fnc_shorten_name);
|
||||
};
|
||||
_callsign = getText (configOf _unit >> "displayName");
|
||||
|
||||
if (isUAVConnected _unit) then {
|
||||
_callsign = _callsign + " [ON]";
|
||||
_callsign = (_callsign) + "[ON]";
|
||||
} else {
|
||||
_callsign = _callsign + " [OFF]";
|
||||
_callsign = (_callsign) + "[OFF]";
|
||||
}
|
||||
};
|
||||
|
||||
if (_callsign isEqualTo "") then {
|
||||
_callsign = _displayName;
|
||||
armatak_attribute_marker_callsign = _unit getVariable "armatak_attribute_marker_callsign";
|
||||
|
||||
if (!isNil "armatak_attribute_marker_callsign" or armatak_attribute_marker_callsign != '') then {
|
||||
_callsign = armatak_attribute_marker_callsign;
|
||||
};
|
||||
|
||||
_callsign
|
||||
|
||||
@@ -7,7 +7,13 @@ params["_unit"];
|
||||
private _affiliation = "f";
|
||||
private _type = "G";
|
||||
private _role = "a-f-G-U-C-I";
|
||||
private _side = _unit getVariable ["armatak_current_side", side _unit];
|
||||
private _side = side _unit;
|
||||
|
||||
if (isNil {
|
||||
_unit getVariable "armatak_current_side"
|
||||
}) then {
|
||||
_side = _unit getVariable "armatak_current_side";
|
||||
};
|
||||
|
||||
switch (str _side) do {
|
||||
case "WEST": {
|
||||
|
||||
@@ -3,28 +3,25 @@ params["_unit"];
|
||||
_target = getSensorTargets (_unit);
|
||||
|
||||
{
|
||||
private _targetUnit = _x select 0;
|
||||
_unit = _x select 0;
|
||||
_position = _x select 1;
|
||||
_status = _x select 2;
|
||||
private _targetType = toLower (typeOf _targetUnit);
|
||||
|
||||
if ((_targetType find "lasertarget") < 0) then {
|
||||
if (isNil {
|
||||
_targetUnit getVariable "armatak_current_side"
|
||||
}) then {
|
||||
_targetUnit setVariable ["armatak_current_side", side _targetUnit];
|
||||
};
|
||||
if (isNil {
|
||||
_unit getVariable "armatak_current_side"
|
||||
}) then {
|
||||
_unit setVariable ["armatak_current_side", side _unit];
|
||||
};
|
||||
|
||||
if (_status != "destroyed" && !(_targetUnit in armatak_server_syncedUnits)) then {
|
||||
_unit_position = _targetUnit call armatak_client_fnc_extractClientPosition;
|
||||
if (_status != "destroyed" && !(_unit in armatak_server_syncedUnits)) then {
|
||||
_unit_position = _unit call armatak_client_fnc_extractClientPosition;
|
||||
|
||||
_uuid = _targetUnit call armatak_fnc_extract_uuid;
|
||||
_type = _targetUnit call armatak_fnc_extract_role;
|
||||
_callsign = getText (configOf _targetUnit >> "displayName");
|
||||
_uuid = _unit call armatak_fnc_extract_uuid;
|
||||
_type = _unit call armatak_fnc_extract_role;
|
||||
_callsign = getText (configOf _unit >> "displayName");
|
||||
|
||||
_marker_cot = [_uuid, _type, _unit_position select 1, _unit_position select 2, _unit_position select 3, _callsign, _unit_position select 5, _unit_position select 6];
|
||||
_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]];
|
||||
};
|
||||
"armatak" callExtension ["tcp_socket:cot:marker", [_marker_cot]];
|
||||
};
|
||||
} forEach _target;
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
params ["_drone", ["_cameraMode", "turret"]];
|
||||
|
||||
private _override = _drone getVariable ["armatak_uas_camera_data_override", []];
|
||||
private _isLocalController = hasInterface && {!isNull player} && {(getConnectedUAV player) isEqualTo _drone};
|
||||
|
||||
if (!_isLocalController && {_override isEqualType []} && {(count _override) >= 7}) then {
|
||||
private _updatedAt = _override param [6, -1000];
|
||||
if ((time - _updatedAt) <= 5) exitWith {
|
||||
private _overrideSpiAsl = _override param [4, []];
|
||||
private _overrideSpiGeo = _override param [5, []];
|
||||
_drone setVariable ["armatak_uas_spi_asl", _overrideSpiAsl, false];
|
||||
_drone setVariable ["armatak_uas_spi_geo", _overrideSpiGeo, false];
|
||||
_override select [0, 6]
|
||||
};
|
||||
};
|
||||
|
||||
private _defaultFov = _drone getVariable ["armatak_uas_fov", 60];
|
||||
private _maxRange = _drone getVariable ["armatak_uas_max_range", 15000];
|
||||
private _originASL = getPosASL _drone;
|
||||
private _originAGL = ASLToAGL _originASL;
|
||||
private _cameraDir = [];
|
||||
private _spiASL = [];
|
||||
private _slantRange = 0;
|
||||
|
||||
if (_cameraMode isNotEqualTo "fpv") then {
|
||||
private _laserTarget = laserTarget _drone;
|
||||
if (!isNull _laserTarget) then {
|
||||
private _laserTargetWorld = getPosWorld _laserTarget;
|
||||
private _laserTargetAslZ = (getPosASL _laserTarget) select 2;
|
||||
_spiASL = [_laserTargetWorld select 0, _laserTargetWorld select 1, _laserTargetAslZ];
|
||||
_cameraDir = _spiASL vectorDiff _originASL;
|
||||
_slantRange = _originASL vectorDistance _spiASL;
|
||||
};
|
||||
};
|
||||
|
||||
if (_cameraDir isEqualTo [] && {_cameraMode isNotEqualTo "fpv"}) then {
|
||||
private _uavControl = UAVControl _drone;
|
||||
private _controlledTurretPath = _uavControl param [1, []];
|
||||
private _candidateTurrets = [];
|
||||
|
||||
if ((_controlledTurretPath isEqualType []) && {_controlledTurretPath isNotEqualTo []}) then {
|
||||
_candidateTurrets pushBack _controlledTurretPath;
|
||||
};
|
||||
|
||||
{
|
||||
if !(_x in _candidateTurrets) then {
|
||||
_candidateTurrets pushBack _x;
|
||||
};
|
||||
} forEach (allTurrets _drone);
|
||||
|
||||
{
|
||||
private _turretWeapons = _drone weaponsTurret _x;
|
||||
if (_turretWeapons isNotEqualTo []) exitWith {
|
||||
private _weapon = _turretWeapons select 0;
|
||||
private _weaponDirection = _drone weaponDirection _weapon;
|
||||
if (_weaponDirection isNotEqualTo [0, 0, 0]) then {
|
||||
_cameraDir = _weaponDirection;
|
||||
};
|
||||
};
|
||||
} forEach _candidateTurrets;
|
||||
};
|
||||
|
||||
if (_cameraDir isEqualTo []) then {
|
||||
_cameraDir = vectorDirVisual _drone;
|
||||
};
|
||||
|
||||
private _dirMagnitude = vectorMagnitude _cameraDir;
|
||||
if (_dirMagnitude <= 0) then {
|
||||
private _fallbackAzimuth = getDir _drone;
|
||||
_cameraDir = [sin _fallbackAzimuth, cos _fallbackAzimuth, -1];
|
||||
_dirMagnitude = vectorMagnitude _cameraDir;
|
||||
};
|
||||
|
||||
_cameraDir = _cameraDir vectorMultiply (1 / _dirMagnitude);
|
||||
|
||||
private _dirX = _cameraDir select 0;
|
||||
private _dirY = _cameraDir select 1;
|
||||
private _dirZ = _cameraDir select 2;
|
||||
private _horizontalMagnitude = sqrt ((_dirX * _dirX) + (_dirY * _dirY));
|
||||
|
||||
private _azimuth = (((_dirX atan2 _dirY) + 360) mod 360);
|
||||
private _elevation = (_dirZ atan2 (_horizontalMagnitude max 0.001));
|
||||
|
||||
if (_spiASL isEqualTo []) then {
|
||||
private _altitudeAGL = (_originAGL select 2) max 0.1;
|
||||
private _probeASL = _originASL vectorAdd (_cameraDir vectorMultiply _maxRange);
|
||||
|
||||
if (_dirZ < -0.01 && {terrainIntersectASL [_originASL, _probeASL]}) then {
|
||||
private _near = _originASL;
|
||||
private _far = _probeASL;
|
||||
|
||||
for "_i" from 0 to 24 do {
|
||||
private _mid = [
|
||||
((_near select 0) + (_far select 0)) / 2,
|
||||
((_near select 1) + (_far select 1)) / 2,
|
||||
((_near select 2) + (_far select 2)) / 2
|
||||
];
|
||||
|
||||
if (terrainIntersectASL [_originASL, _mid]) then {
|
||||
_far = _mid;
|
||||
} else {
|
||||
_near = _mid;
|
||||
};
|
||||
};
|
||||
|
||||
_spiASL = _far;
|
||||
_slantRange = _originASL vectorDistance _spiASL;
|
||||
} else {
|
||||
private _verticalComponent = abs _dirZ;
|
||||
|
||||
if (_verticalComponent > 0.01) then {
|
||||
_slantRange = (_altitudeAGL / _verticalComponent) min _maxRange;
|
||||
} else {
|
||||
_slantRange = _maxRange;
|
||||
};
|
||||
|
||||
_slantRange = _slantRange max 1;
|
||||
_spiASL = _originASL vectorAdd (_cameraDir vectorMultiply _slantRange);
|
||||
_spiASL set [2, getTerrainHeightASL [_spiASL select 0, _spiASL select 1]];
|
||||
};
|
||||
};
|
||||
|
||||
if (_slantRange <= 0) then {
|
||||
_slantRange = (_originASL vectorDistance _spiASL) max 1;
|
||||
};
|
||||
|
||||
private _spiAgl = ASLToAGL _spiASL;
|
||||
private _spiWorld = [_spiAgl select 0, _spiAgl select 1, (_spiAgl select 2) max 0];
|
||||
private _spiGeo = _spiWorld call armatak_client_fnc_convertClientLocation;
|
||||
|
||||
_drone setVariable ["armatak_uas_spi_asl", _spiASL, false];
|
||||
_drone setVariable ["armatak_uas_spi_geo", _spiGeo, false];
|
||||
|
||||
[
|
||||
round _azimuth,
|
||||
round _elevation,
|
||||
round _defaultFov,
|
||||
round (_slantRange max 1),
|
||||
_spiASL,
|
||||
_spiGeo
|
||||
]
|
||||
|
||||
|
||||
@@ -20,10 +20,10 @@ if (side _unit == east) then {
|
||||
_callsign = getText (configOf _unit >> "displayName");
|
||||
};
|
||||
|
||||
private _unitCallsignOverride = _unit getVariable ["armatak_attribute_unit_callsign", ""];
|
||||
armatak_attribute_unit_callsign = _unit getVariable "armatak_attribute_unit_callsign";
|
||||
|
||||
if (_unitCallsignOverride isNotEqualTo "") then {
|
||||
_callsign = _unitCallsignOverride;
|
||||
if (!isNil "armatak_attribute_unit_callsign" or armatak_attribute_unit_callsign != '') then {
|
||||
_callsign = armatak_attribute_unit_callsign;
|
||||
};
|
||||
|
||||
_callsign
|
||||
|
||||
@@ -25,16 +25,6 @@ GVAR(syncedUnits) = missionNamespace getVariable "armatak_server_syncedUnits";
|
||||
[_x, _callsign, _group_name, _group_role] call armatak_fnc_send_eud_cot;
|
||||
[_x] call armatak_fnc_send_digital_pointer_cot;
|
||||
};
|
||||
case (unitIsUAV _x): {
|
||||
if !(_x getVariable ["armatak_uav_mavlink_broadcasting", false]) then {
|
||||
_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;
|
||||
};
|
||||
};
|
||||
case ((_objectType select 0) == "Vehicle"): {
|
||||
_atak_type = [_x] call armatak_fnc_extract_role;
|
||||
_callsign = [_x] call armatak_fnc_extract_marker_callsign;
|
||||
@@ -43,14 +33,12 @@ GVAR(syncedUnits) = missionNamespace getVariable "armatak_server_syncedUnits";
|
||||
_x call armatak_fnc_extract_sensor_data;
|
||||
};
|
||||
case ((_objectType select 0) == "VehicleAutonomous"): {
|
||||
if !(_x getVariable ["armatak_uav_mavlink_broadcasting", false]) then {
|
||||
_atak_type = [_x] call armatak_fnc_extract_role;
|
||||
_callsign = [_x] call armatak_fnc_extract_marker_callsign;
|
||||
_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;
|
||||
};
|
||||
[_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 +0,0 @@
|
||||
armatak\armatak\addons\uav
|
||||
@@ -1,17 +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));
|
||||
};
|
||||
};
|
||||
|
||||
class Extended_PostInit_EventHandlers {
|
||||
class ADDON {
|
||||
init = QUOTE(call COMPILE_SCRIPT(XEH_postInit));
|
||||
};
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
PREP(startMavlinkBroadcast);
|
||||
PREP(stopMavlinkBroadcast);
|
||||
PREP(updateMavlinkBroadcast);
|
||||
PREP(resolveVideoUri);
|
||||
PREP(handleMavlinkCallback);
|
||||
PREP(parseMavlinkCallbackData);
|
||||
@@ -1,5 +0,0 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
if (!hasInterface) exitWith {};
|
||||
|
||||
SETVAR(player,GVAR(mavlinkPFH),-1);
|
||||
@@ -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,20 +0,0 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
class CfgPatches {
|
||||
class ADDON {
|
||||
name = COMPONENT_NAME;
|
||||
units[] = {};
|
||||
weapons[] = {};
|
||||
requiredAddons[] = {
|
||||
"cba_main",
|
||||
"ace_main",
|
||||
"armatak_main",
|
||||
"armatak_client"
|
||||
};
|
||||
requiredVersion = REQUIRED_VERSION;
|
||||
author = PROJECT_AUTHOR;
|
||||
url = "https://github.com/valmojr/armatak";
|
||||
};
|
||||
};
|
||||
|
||||
#include "CfgEventHandlers.hpp"
|
||||
@@ -1,382 +0,0 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
params ["_function", ["_data", "", [""]]];
|
||||
|
||||
if (!hasInterface) exitWith {};
|
||||
|
||||
private _payload = [_data] call FUNC(parseMavlinkCallbackData);
|
||||
private _uav = getConnectedUAV player;
|
||||
if (isNull _uav) then {
|
||||
_uav = player getVariable [QGVAR(broadcastingUav), objNull];
|
||||
};
|
||||
|
||||
if (isNull _uav) exitWith {
|
||||
"armatak" callExtension ["log", [["warn", format ["Ignoring MAVLINK UDP callback %1 because no UAV is connected: %2", _function, _data]]]];
|
||||
};
|
||||
|
||||
private _number = {
|
||||
params ["_key", ["_default", 0]];
|
||||
private _raw = _payload getOrDefault [_key, str _default];
|
||||
private _value = parseNumber _raw;
|
||||
if (!finite _value) exitWith {_default};
|
||||
_value
|
||||
};
|
||||
|
||||
private _uavGroup = {
|
||||
params ["_vehicle"];
|
||||
private _crew = crew _vehicle;
|
||||
if (_crew isEqualTo []) exitWith {grpNull};
|
||||
group (_crew select 0)
|
||||
};
|
||||
|
||||
private _clearWaypoints = {
|
||||
params ["_group"];
|
||||
if (isNull _group) exitWith {};
|
||||
for "_i" from ((count waypoints _group) - 1) to 0 step -1 do {
|
||||
deleteWaypoint [_group, _i];
|
||||
};
|
||||
};
|
||||
|
||||
private _clearUavRoute = {
|
||||
private _group = [_uav] call _uavGroup;
|
||||
if (isNull _group) exitWith {false};
|
||||
[_group] call _clearWaypoints;
|
||||
_uav setVariable ["armatak_uas_mission_items", [], true];
|
||||
true
|
||||
};
|
||||
|
||||
private _geoToAtl = {
|
||||
params ["_vehicle", "_lat", "_lon", ["_alt", -1]];
|
||||
|
||||
private _current = [_vehicle] call EFUNC(client,extractClientPosition);
|
||||
private _currentLat = _current select 1;
|
||||
private _currentLon = _current select 2;
|
||||
private _currentAtl = getPosATL _vehicle;
|
||||
|
||||
private _northM = (_lat - _currentLat) * 111320;
|
||||
private _eastM = (_lon - _currentLon) * (111320 * (cos _currentLat));
|
||||
|
||||
[
|
||||
(_currentAtl select 0) + _eastM,
|
||||
(_currentAtl select 1) + _northM,
|
||||
if (_alt >= 0) then {_alt} else {_currentAtl select 2}
|
||||
]
|
||||
};
|
||||
|
||||
private _commandMove = {
|
||||
params ["_vehicle", "_positionAtl", ["_type", "MOVE"], ["_radius", 80], ["_completion", 50]];
|
||||
|
||||
private _group = [_vehicle] call _uavGroup;
|
||||
if (isNull _group) exitWith {false};
|
||||
|
||||
[_group] call _clearWaypoints;
|
||||
|
||||
_vehicle engineOn true;
|
||||
_vehicle setVariable ["armatak_uas_armed", true, true];
|
||||
_vehicle setFuel ((fuel _vehicle) max 0.1);
|
||||
_vehicle flyInHeight ((_positionAtl select 2) max 10);
|
||||
_vehicle doMove _positionAtl;
|
||||
|
||||
private _wp = _group addWaypoint [_positionAtl, 0];
|
||||
_wp setWaypointType _type;
|
||||
_wp setWaypointBehaviour "CARELESS";
|
||||
_wp setWaypointCombatMode "BLUE";
|
||||
_wp setWaypointSpeed "NORMAL";
|
||||
_wp setWaypointCompletionRadius _completion;
|
||||
|
||||
if (_type == "LOITER") then {
|
||||
_wp setWaypointLoiterRadius (_radius max 25);
|
||||
_wp setWaypointLoiterType "CIRCLE_L";
|
||||
};
|
||||
|
||||
true
|
||||
};
|
||||
|
||||
private _appendMissionWaypoint = {
|
||||
params ["_vehicle", "_positionAtl", "_command", "_seq", ["_radius", 80]];
|
||||
|
||||
private _group = [_vehicle] call _uavGroup;
|
||||
if (isNull _group) exitWith {false};
|
||||
|
||||
private _type = switch (_command) do {
|
||||
case 17;
|
||||
case 18;
|
||||
case 19;
|
||||
case 31: {"LOITER"};
|
||||
case 21: {"MOVE"};
|
||||
default {"MOVE"};
|
||||
};
|
||||
|
||||
private _wp = _group addWaypoint [_positionAtl, 0];
|
||||
_wp setWaypointType _type;
|
||||
_wp setWaypointBehaviour "CARELESS";
|
||||
_wp setWaypointCombatMode "BLUE";
|
||||
_wp setWaypointSpeed "NORMAL";
|
||||
_wp setWaypointCompletionRadius 35;
|
||||
|
||||
if (_type == "LOITER") then {
|
||||
_wp setWaypointLoiterRadius (_radius max 25);
|
||||
_wp setWaypointLoiterType "CIRCLE_L";
|
||||
};
|
||||
if (_command == 21) then {
|
||||
_wp setWaypointStatements ["true", "(vehicle this) land 'LAND'"];
|
||||
};
|
||||
|
||||
private _items = _vehicle getVariable ["armatak_uas_mission_items", []];
|
||||
_items pushBack [_seq, _command, _positionAtl];
|
||||
_vehicle setVariable ["armatak_uas_mission_items", _items, true];
|
||||
|
||||
true
|
||||
};
|
||||
|
||||
private _commandName = _payload getOrDefault ["command_name", "UNKNOWN"];
|
||||
private _command = [_payload getOrDefault ["command", "-1"]] call BIS_fnc_parseNumber;
|
||||
private _callsign = [_uav] call armatak_fnc_extract_marker_callsign;
|
||||
|
||||
private _applySpeed = {
|
||||
params ["_speed"];
|
||||
if (_speed <= 0) exitWith {false};
|
||||
_uav limitSpeed _speed;
|
||||
systemChat format ["ATAK SPEED %1m/s %2", round _speed, _callsign];
|
||||
true
|
||||
};
|
||||
|
||||
private _applyMode = {
|
||||
params ["_mode"];
|
||||
|
||||
switch (_mode) do {
|
||||
case 4: {
|
||||
_uav engineOn true;
|
||||
_uav setVariable ["armatak_uas_armed", true, true];
|
||||
_uav setFuel ((fuel _uav) max 0.1);
|
||||
systemChat format ["ATAK GUIDED %1", _callsign];
|
||||
};
|
||||
case 5: {
|
||||
private _pos = getPosATL _uav;
|
||||
[_uav, _pos, "LOITER", 80, 25] call _commandMove;
|
||||
systemChat format ["ATAK LOITER %1", _callsign];
|
||||
};
|
||||
case 6;
|
||||
case 21;
|
||||
case 27: {
|
||||
private _home = _uav getVariable ["armatak_uas_home_atl", getPosATL _uav];
|
||||
_home set [2, ((_home select 2) max 60)];
|
||||
[_uav, _home, "MOVE", 80, 60] call _commandMove;
|
||||
systemChat format ["ATAK RTL %1", _callsign];
|
||||
};
|
||||
case 9: {
|
||||
private _pos = getPosATL _uav;
|
||||
_pos set [2, 0];
|
||||
[_uav, _pos, "MOVE", 30, 20] call _commandMove;
|
||||
_uav flyInHeight 0;
|
||||
systemChat format ["ATAK LAND %1", _callsign];
|
||||
};
|
||||
default {
|
||||
"armatak" callExtension ["log", [["info", format ["Unhandled MAVLINK mode %1 for UAV %2", _mode, _uav]]]];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
private _setHomeFromGeo = {
|
||||
params ["_lat", "_lon", "_alt"];
|
||||
if (_lat == 0 && {_lon == 0}) exitWith {false};
|
||||
private _homeAtl = [_uav, _lat, _lon, _alt] call _geoToAtl;
|
||||
_uav setVariable ["armatak_uas_home_atl", _homeAtl, true];
|
||||
_uav setVariable ["armatak_uas_home_geo", [_lat, _lon, _alt], true];
|
||||
systemChat format ["ATAK HOME %1", _callsign];
|
||||
true
|
||||
};
|
||||
|
||||
switch (_function) do {
|
||||
case "COMMAND_LONG": {
|
||||
switch (_command) do {
|
||||
case 176: {
|
||||
private _mode = ["param2", -1] call _number;
|
||||
if (_mode < 0) then {
|
||||
_mode = ["param1", -1] call _number;
|
||||
};
|
||||
[_mode] call _applyMode;
|
||||
};
|
||||
case 178: {
|
||||
private _speed = ["param2", -1] call _number;
|
||||
if (_speed <= 0) then {
|
||||
_speed = ["param1", -1] call _number;
|
||||
};
|
||||
[_speed] call _applySpeed;
|
||||
};
|
||||
case 179: {
|
||||
private _useCurrent = (["param1", 0] call _number) >= 1;
|
||||
if (_useCurrent) then {
|
||||
private _pos = [_uav] call EFUNC(client,extractClientPosition);
|
||||
private _relAlt = ((getPosATL _uav) select 2) max 0;
|
||||
private _homeAtl = getPosATL _uav;
|
||||
_uav setVariable ["armatak_uas_home_atl", _homeAtl, true];
|
||||
_uav setVariable ["armatak_uas_home_geo", [_pos select 1, _pos select 2, (_pos select 3) - _relAlt], true];
|
||||
systemChat format ["ATAK HOME %1", _callsign];
|
||||
} else {
|
||||
[["param5", 0] call _number, ["param6", 0] call _number, ["param7", 0] call _number] call _setHomeFromGeo;
|
||||
};
|
||||
};
|
||||
case 400: {
|
||||
private _doArm = (["param1", 0] call _number) >= 1;
|
||||
_uav engineOn _doArm;
|
||||
_uav setVariable ["armatak_uas_armed", _doArm, true];
|
||||
if (_doArm) then {
|
||||
_uav setFuel ((fuel _uav) max 0.1);
|
||||
};
|
||||
systemChat format ["ATAK %1 %2", ["DISARM", "ARM"] select _doArm, _callsign];
|
||||
"armatak" callExtension ["log", [["info", format ["Applied MAVLINK ARM=%1 to UAV %2", _doArm, _uav]]]];
|
||||
};
|
||||
case 22: {
|
||||
private _alt = (["param7", 75] call _number) max 10;
|
||||
private _pos = getPosATL _uav;
|
||||
_pos set [2, _alt];
|
||||
[_uav, _pos, "MOVE", 80, 25] call _commandMove;
|
||||
_uav setVelocityModelSpace [0, 15, 8];
|
||||
systemChat format ["ATAK TAKEOFF %1m %2", round _alt, _callsign];
|
||||
};
|
||||
case 21: {
|
||||
[9] call _applyMode;
|
||||
};
|
||||
case 20: {
|
||||
[6] call _applyMode;
|
||||
};
|
||||
case 16: {
|
||||
private _lat = ["param5", 0] call _number;
|
||||
private _lon = ["param6", 0] call _number;
|
||||
private _alt = ["param7", -1] call _number;
|
||||
private _pos = [_uav, _lat, _lon, _alt] call _geoToAtl;
|
||||
[_uav, _pos, "MOVE", 80, 50] call _commandMove;
|
||||
systemChat format ["ATAK MOVE %1", _callsign];
|
||||
};
|
||||
case 17: {
|
||||
private _lat = ["param5", 0] call _number;
|
||||
private _lon = ["param6", 0] call _number;
|
||||
private _alt = ["param7", -1] call _number;
|
||||
private _radius = abs (["param3", 80] call _number);
|
||||
private _pos = [_uav, _lat, _lon, _alt] call _geoToAtl;
|
||||
[_uav, _pos, "LOITER", _radius, 30] call _commandMove;
|
||||
systemChat format ["ATAK LOITER %1", _callsign];
|
||||
};
|
||||
case 43000: {
|
||||
private _speed = ["param2", -1] call _number;
|
||||
if (_speed <= 0) then {
|
||||
_speed = ["param1", -1] call _number;
|
||||
};
|
||||
[_speed] call _applySpeed;
|
||||
};
|
||||
case 43001: {
|
||||
private _alt = ["param1", -1] call _number;
|
||||
if (_alt < 0) then {
|
||||
_alt = ["param7", -1] call _number;
|
||||
};
|
||||
private _pos = getPosATL _uav;
|
||||
_pos set [2, _alt max 10];
|
||||
[_uav, _pos, "MOVE", 80, 25] call _commandMove;
|
||||
systemChat format ["ATAK ALT %1m %2", round (_pos select 2), _callsign];
|
||||
};
|
||||
case 43002: {
|
||||
private _heading = ["param1", -1] call _number;
|
||||
if (_heading >= 0) then {
|
||||
_uav setDir _heading;
|
||||
systemChat format ["ATAK HDG %1 %2", round _heading, _callsign];
|
||||
};
|
||||
};
|
||||
default {
|
||||
"armatak" callExtension ["log", [["info", format ["Unhandled MAVLINK COMMAND_LONG %1 (%2): %3", _command, _commandName, _data]]]];
|
||||
};
|
||||
};
|
||||
};
|
||||
case "COMMAND_INT": {
|
||||
private _lat = (["x", 0] call _number) / 1e7;
|
||||
private _lon = (["y", 0] call _number) / 1e7;
|
||||
private _alt = ["z", -1] call _number;
|
||||
|
||||
switch (_command) do {
|
||||
case 16: {
|
||||
private _pos = [_uav, _lat, _lon, _alt] call _geoToAtl;
|
||||
[_uav, _pos, "MOVE", 80, 50] call _commandMove;
|
||||
systemChat format ["ATAK MOVE %1", _callsign];
|
||||
};
|
||||
case 17;
|
||||
case 192: {
|
||||
private _radius = abs (["param3", 80] call _number);
|
||||
private _direction = ["CIRCLE_L", "CIRCLE_R"] select ((["param4", 0] call _number) < 0);
|
||||
private _pos = [_uav, _lat, _lon, _alt] call _geoToAtl;
|
||||
private _type = ["MOVE", "LOITER"] select (_radius > 1);
|
||||
[_uav, _pos, _type, _radius, 30] call _commandMove;
|
||||
if (_type == "LOITER") then {
|
||||
private _group = [_uav] call _uavGroup;
|
||||
private _waypoints = waypoints _group;
|
||||
if (_waypoints isNotEqualTo []) then {
|
||||
(_waypoints select -1) setWaypointLoiterType _direction;
|
||||
};
|
||||
};
|
||||
systemChat format ["ATAK %1 %2", _type, _callsign];
|
||||
};
|
||||
default {
|
||||
"armatak" callExtension ["log", [["info", format ["Unhandled MAVLINK COMMAND_INT %1 (%2): %3", _command, _commandName, _data]]]];
|
||||
};
|
||||
};
|
||||
};
|
||||
case "MISSION_COUNT": {
|
||||
private _count = ["count", 0] call _number;
|
||||
[] call _clearUavRoute;
|
||||
systemChat format ["ATAK ROUTE %1 pts %2", round _count, _callsign];
|
||||
"armatak" callExtension ["log", [["info", format ["Receiving MAVLINK mission count=%1 for UAV %2", _count, _uav]]]];
|
||||
};
|
||||
case "MISSION_CLEAR_ALL": {
|
||||
[] call _clearUavRoute;
|
||||
systemChat format ["ATAK ROUTE CLEAR %1", _callsign];
|
||||
};
|
||||
case "MISSION_SET_CURRENT": {
|
||||
private _seq = ["seq", 0] call _number;
|
||||
"armatak" callExtension ["log", [["info", format ["MAVLINK mission set current seq=%1 for UAV %2", _seq, _uav]]]];
|
||||
};
|
||||
case "MISSION_ITEM";
|
||||
case "MISSION_ITEM_INT": {
|
||||
private _seq = ["seq", 0] call _number;
|
||||
private _missionCommand = ["command", -1] call _number;
|
||||
private _lat = ["lat", 0] call _number;
|
||||
private _lon = ["lon", 0] call _number;
|
||||
private _alt = ["alt", -1] call _number;
|
||||
|
||||
if (_lat == 0 && {_lon == 0}) exitWith {
|
||||
"armatak" callExtension ["log", [["warn", format ["Ignoring MAVLINK mission item at zero coordinate: %1", _data]]]];
|
||||
};
|
||||
|
||||
private _pos = [_uav, _lat, _lon, _alt] call _geoToAtl;
|
||||
private _radius = abs (["param3", 80] call _number);
|
||||
[_uav, _pos, _missionCommand, _seq, _radius] call _appendMissionWaypoint;
|
||||
_uav engineOn true;
|
||||
_uav setVariable ["armatak_uas_armed", true, true];
|
||||
_uav setFuel ((fuel _uav) max 0.1);
|
||||
_uav flyInHeight ((_pos select 2) max 10);
|
||||
systemChat format ["ATAK ROUTE WP %1 %2", round _seq, _callsign];
|
||||
"armatak" callExtension ["log", [["info", format ["Added MAVLINK mission item seq=%1 command=%2 posATL=%3 for UAV %4", _seq, _missionCommand, _pos, _uav]]]];
|
||||
};
|
||||
case "SET_HOME_POSITION": {
|
||||
[["lat", 0] call _number, ["lon", 0] call _number, ["alt", 0] call _number] call _setHomeFromGeo;
|
||||
};
|
||||
case "SET_POSITION_TARGET_GLOBAL_INT": {
|
||||
private _lat = ["lat", 0] call _number;
|
||||
private _lon = ["lon", 0] call _number;
|
||||
private _alt = ["alt", -1] call _number;
|
||||
private _pos = [_uav, _lat, _lon, _alt] call _geoToAtl;
|
||||
[_uav, _pos, "MOVE", 80, 40] call _commandMove;
|
||||
systemChat format ["ATAK GUIDED MOVE %1", _callsign];
|
||||
};
|
||||
case "SET_MODE": {
|
||||
private _mode = ["custom_mode", -1] call _number;
|
||||
[_mode] call _applyMode;
|
||||
};
|
||||
case "COMMAND_ACK": {
|
||||
"armatak" callExtension ["log", [["info", format ["Received MAVLINK COMMAND_ACK %1 (%2): %3", _command, _commandName, _data]]]];
|
||||
};
|
||||
case "MANUAL_CONTROL": {
|
||||
"armatak" callExtension ["log", [["info", format ["Received MAVLINK MANUAL_CONTROL: %1", _data]]]];
|
||||
};
|
||||
default {
|
||||
"armatak" callExtension ["log", [["info", format ["Unhandled MAVLINK UDP callback %1: %2", _function, _data]]]];
|
||||
};
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
params [["_raw", "", [""]]];
|
||||
|
||||
private _pairs = createHashMap;
|
||||
|
||||
{
|
||||
private _entry = _x;
|
||||
private _separatorIndex = _entry find "=";
|
||||
if (_separatorIndex > 0) then {
|
||||
private _key = _entry select [0, _separatorIndex];
|
||||
private _value = _entry select [_separatorIndex + 1];
|
||||
_pairs set [_key, _value];
|
||||
};
|
||||
} forEach (_raw splitString ";");
|
||||
|
||||
_pairs
|
||||
@@ -1,45 +0,0 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
params [["_uav", objNull, [objNull]]];
|
||||
|
||||
private _defaultVideoUri = "rtsp://undefined:554/fpv";
|
||||
private _activelyControlledUav = if (!isNull player) then {getConnectedUAV player} else {objNull};
|
||||
|
||||
private _normalize = {
|
||||
params ["_rawUrl"];
|
||||
|
||||
private _url = trim _rawUrl;
|
||||
if (_url isEqualTo "") exitWith {""};
|
||||
|
||||
if (_url find "://" >= 0) exitWith {_url};
|
||||
|
||||
if (_url find "/" >= 0) exitWith {
|
||||
format ["rtsp://%1", _url]
|
||||
};
|
||||
|
||||
format ["rtp://%1", _url]
|
||||
};
|
||||
|
||||
if (!isNull _uav) then {
|
||||
private _objectVideoUrl = [_uav] call armatak_fnc_extract_marker_video_url;
|
||||
private _normalizedObjectVideoUrl = [_objectVideoUrl] call _normalize;
|
||||
if (_normalizedObjectVideoUrl isNotEqualTo "") exitWith {
|
||||
_normalizedObjectVideoUrl
|
||||
};
|
||||
|
||||
private _activeSessionVideoUrl = player getVariable [QEGVAR(client,video_feed_url), ""];
|
||||
private _normalizedActiveSessionVideoUrl = [_activeSessionVideoUrl] call _normalize;
|
||||
if (_normalizedActiveSessionVideoUrl isNotEqualTo "") exitWith {
|
||||
_normalizedActiveSessionVideoUrl
|
||||
};
|
||||
|
||||
_defaultVideoUri
|
||||
};
|
||||
|
||||
private _sessionVideoUrl = player getVariable [QEGVAR(client,video_feed_url), ""];
|
||||
private _normalizedSessionVideoUrl = [_sessionVideoUrl] call _normalize;
|
||||
if (_normalizedSessionVideoUrl isNotEqualTo "") exitWith {
|
||||
_normalizedSessionVideoUrl
|
||||
};
|
||||
|
||||
_defaultVideoUri
|
||||
@@ -1,16 +0,0 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
if (!hasInterface) exitWith {};
|
||||
|
||||
private _existingPfh = player getVariable [QGVAR(mavlinkPFH), -1];
|
||||
if (_existingPfh >= 0) then {
|
||||
[_existingPfh] call CBA_fnc_removePerFrameHandler;
|
||||
};
|
||||
|
||||
player setVariable [QGVAR(broadcastingUav), objNull];
|
||||
|
||||
private _pfh = [{
|
||||
call FUNC(updateMavlinkBroadcast);
|
||||
}, 0.5, []] call CBA_fnc_addPerFrameHandler;
|
||||
|
||||
player setVariable [QGVAR(mavlinkPFH), _pfh];
|
||||
@@ -1,17 +0,0 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
if (!hasInterface) exitWith {};
|
||||
|
||||
private _existingPfh = player getVariable [QGVAR(mavlinkPFH), -1];
|
||||
if (_existingPfh >= 0) then {
|
||||
[_existingPfh] call CBA_fnc_removePerFrameHandler;
|
||||
player setVariable [QGVAR(mavlinkPFH), -1];
|
||||
};
|
||||
|
||||
private _broadcastingUav = player getVariable [QGVAR(broadcastingUav), objNull];
|
||||
if (!isNull _broadcastingUav) then {
|
||||
_broadcastingUav setVariable ["armatak_uav_mavlink_broadcasting", false, true];
|
||||
systemChat "UAV broadcasting stopped";
|
||||
};
|
||||
|
||||
player setVariable [QGVAR(broadcastingUav), objNull];
|
||||
@@ -1,122 +0,0 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
private _broadcastingUav = player getVariable [QGVAR(broadcastingUav), objNull];
|
||||
|
||||
if !(player getVariable [QEGVAR(client,eudConnected), false]) exitWith {
|
||||
if (!isNull _broadcastingUav) then {
|
||||
_broadcastingUav setVariable ["armatak_uav_mavlink_broadcasting", false, true];
|
||||
player setVariable [QGVAR(broadcastingUav), objNull];
|
||||
};
|
||||
};
|
||||
|
||||
private _uav = getConnectedUAV player;
|
||||
if (isNull _uav) then {
|
||||
_uav = _broadcastingUav;
|
||||
};
|
||||
|
||||
if (isNull _uav || {!alive _uav}) exitWith {
|
||||
if (!isNull _broadcastingUav) then {
|
||||
_broadcastingUav setVariable ["armatak_uav_mavlink_broadcasting", false, true];
|
||||
player setVariable [QGVAR(broadcastingUav), objNull];
|
||||
systemChat "UAV broadcasting stopped";
|
||||
"armatak" callExtension ["log", [["info", "UAV broadcasting stopped because the UAV is no longer available"]]];
|
||||
};
|
||||
};
|
||||
|
||||
if (_broadcastingUav isNotEqualTo _uav) then {
|
||||
if (!isNull _broadcastingUav) then {
|
||||
_broadcastingUav setVariable ["armatak_uav_mavlink_broadcasting", false, true];
|
||||
};
|
||||
player setVariable [QGVAR(broadcastingUav), _uav];
|
||||
_uav setVariable ["armatak_uav_mavlink_broadcasting", true, true];
|
||||
private _callsign = [_uav] call armatak_fnc_extract_marker_callsign;
|
||||
systemChat format ["Broadcasting UAV %1", _callsign];
|
||||
"armatak" callExtension ["log", [["info", format ["Broadcasting UAV %1 via MAVLink mock to %2", _callsign, player getVariable [QEGVAR(client,mavlink_address), ""]]]]];
|
||||
};
|
||||
|
||||
_uav setVariable ["armatak_uav_mavlink_broadcasting", true, true];
|
||||
|
||||
private _mavlinkAddress = player getVariable [QEGVAR(client,mavlink_address), ""];
|
||||
if (_mavlinkAddress isEqualTo "") exitWith {};
|
||||
|
||||
private _pos = [_uav] call EFUNC(client,extractClientPosition);
|
||||
private _relAlt = ((getPosATL _uav) select 2) max 0;
|
||||
if (isNil {_uav getVariable "armatak_uas_home_atl"}) then {
|
||||
_uav setVariable ["armatak_uas_home_atl", getPosATL _uav, true];
|
||||
_uav setVariable ["armatak_uas_home_geo", [_pos select 1, _pos select 2, (_pos select 3) - _relAlt], true];
|
||||
};
|
||||
private _uuid = [_uav] call armatak_fnc_extract_uuid;
|
||||
private _callsign = [_uav] call armatak_fnc_extract_marker_callsign;
|
||||
private _videoUri = [_uav] call FUNC(resolveVideoUri);
|
||||
private _dir = vectorDir _uav;
|
||||
private _up = vectorUp _uav;
|
||||
private _yaw = getDir _uav;
|
||||
private _pitch = asin (((_dir select 2) max -1) min 1);
|
||||
private _roll = asin (((_up select 0) max -1) min 1);
|
||||
private _uavType = if (_uav isKindOf "Plane") then {1} else {[2, 3] select (_uav isKindOf "Helicopter")};
|
||||
private _armed = _uav getVariable ["armatak_uas_armed", isEngineOn _uav];
|
||||
if !(isEngineOn _uav) then {
|
||||
_armed = false;
|
||||
_uav setVariable ["armatak_uas_armed", false, true];
|
||||
};
|
||||
private _groundSpeed = abs (_pos select 6);
|
||||
private _landed = (_relAlt <= 1.5) && {_groundSpeed <= 0.5};
|
||||
private _batteryRemaining = round ((((fuel _uav) max 0) min 1) * 100);
|
||||
|
||||
private _gimbalRoll = 0;
|
||||
private _gimbalPitch = _pitch;
|
||||
private _gimbalYaw = _yaw;
|
||||
private _hfov = _uav getVariable ["armatak_uas_fov", 60];
|
||||
private _vfov = _uav getVariable ["armatak_uas_vfov", (_hfov * 0.5625)];
|
||||
private _imageLat = _pos select 1;
|
||||
private _imageLon = _pos select 2;
|
||||
private _imageAlt = _pos select 3;
|
||||
private _cameraData = [_uav, "turret"] call armatak_fnc_extract_uas_camera_data;
|
||||
private _uavControl = UAVControl _uav;
|
||||
private _controlledTurretPath = _uavControl param [1, []];
|
||||
private _hasTurretCamera = ((_controlledTurretPath isEqualType []) && {_controlledTurretPath isNotEqualTo []}) || {(allTurrets _uav) isNotEqualTo []};
|
||||
|
||||
if (_cameraData isEqualType [] && {(count _cameraData) >= 6}) then {
|
||||
_gimbalYaw = _cameraData param [0, _yaw];
|
||||
_gimbalPitch = _cameraData param [1, _pitch];
|
||||
_hfov = _cameraData param [2, _hfov];
|
||||
_vfov = _uav getVariable ["armatak_uas_vfov", (_hfov * 0.5625)];
|
||||
|
||||
private _spiGeo = _cameraData param [5, []];
|
||||
if (_spiGeo isEqualType [] && {(count _spiGeo) >= 3}) then {
|
||||
_imageLat = _spiGeo select 0;
|
||||
_imageLon = _spiGeo select 1;
|
||||
_imageAlt = _spiGeo select 2;
|
||||
};
|
||||
};
|
||||
|
||||
private _systemPayload = [
|
||||
_mavlinkAddress,
|
||||
_uuid,
|
||||
_callsign,
|
||||
_uavType,
|
||||
_pos select 1,
|
||||
_pos select 2,
|
||||
_pos select 3,
|
||||
_relAlt,
|
||||
_pos select 5,
|
||||
_pos select 6,
|
||||
_roll,
|
||||
_pitch,
|
||||
_yaw,
|
||||
parseNumber _armed,
|
||||
parseNumber _landed,
|
||||
_gimbalRoll,
|
||||
_gimbalPitch,
|
||||
_gimbalYaw,
|
||||
_videoUri,
|
||||
_hfov,
|
||||
_vfov,
|
||||
_imageLat,
|
||||
_imageLon,
|
||||
_imageAlt,
|
||||
parseNumber _hasTurretCamera,
|
||||
_batteryRemaining
|
||||
];
|
||||
|
||||
"armatak" callExtension ["uas:send_uas_system", [_systemPayload]];
|
||||
@@ -1,17 +0,0 @@
|
||||
#define COMPONENT uav
|
||||
#define COMPONENT_BEAUTIFIED UAV
|
||||
#include "\armatak\armatak\addons\main\script_mod.hpp"
|
||||
|
||||
// #define DEBUG_MODE_FULL
|
||||
// #define DISABLE_COMPILE_CACHE
|
||||
// #define ENABLE_PERFORMANCE_COUNTERS
|
||||
|
||||
#ifdef DEBUG_ENABLED_UAV
|
||||
#define DEBUG_MODE_FULL
|
||||
#endif
|
||||
|
||||
#ifdef DEBUG_SETTINGS_UAV
|
||||
#define DEBUG_SETTINGS DEBUG_SETTINGS_UAV
|
||||
#endif
|
||||
|
||||
#include "\z\ace\addons\main\script_macros.hpp"
|
||||
@@ -1,4 +1,3 @@
|
||||
use super::video::video_detail_xml;
|
||||
use chrono::{Duration, SecondsFormat, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -21,6 +20,14 @@ pub struct CursorOverTime {
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -111,7 +118,13 @@ impl CursorOverTime {
|
||||
|
||||
if let Some(video_url) = &self.video_url {
|
||||
if !video_url.trim().is_empty() {
|
||||
xml.push_str(&video_detail_xml(video_url, uuid, &self.contact_callsign));
|
||||
xml.push_str(
|
||||
format!(
|
||||
"<__video url=\"{}\" />",
|
||||
Self::escape_xml_attribute(video_url.trim())
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,4 +6,3 @@ pub mod gps;
|
||||
pub mod message;
|
||||
pub mod nato;
|
||||
pub mod uas;
|
||||
pub mod video;
|
||||
|
||||
333
src/cot/uas.rs
333
src/cot/uas.rs
@@ -1,206 +1,59 @@
|
||||
use super::video::video_detail_xml;
|
||||
// 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};
|
||||
|
||||
fn escape_xml(value: &str) -> String {
|
||||
value
|
||||
.replace('&', "&")
|
||||
.replace('"', """)
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('\'', "'")
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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..];
|
||||
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()))
|
||||
}
|
||||
|
||||
pub struct UasPlatformCoTPayload {
|
||||
pub uid: String,
|
||||
pub cot_type: String,
|
||||
pub callsign: String,
|
||||
pub point_lat: f64,
|
||||
pub point_lon: f64,
|
||||
pub point_hae: f32,
|
||||
pub track_course: i32,
|
||||
pub track_speed: f32,
|
||||
pub sensor_azimuth: i32,
|
||||
pub sensor_elevation: i32,
|
||||
pub sensor_fov: i32,
|
||||
pub sensor_vfov: i32,
|
||||
pub sensor_range: i32,
|
||||
pub attitude_yaw: i32,
|
||||
pub attitude_pitch: f32,
|
||||
pub attitude_roll: f32,
|
||||
pub hal: f32,
|
||||
pub vehicle_type_tag: String,
|
||||
pub is_flying: i32,
|
||||
pub link_uid: String,
|
||||
}
|
||||
|
||||
impl FromArma for UasPlatformCoTPayload {
|
||||
fn from_arma(data: String) -> Result<UasPlatformCoTPayload, FromArmaError> {
|
||||
let (
|
||||
uid,
|
||||
cot_type,
|
||||
callsign,
|
||||
point_lat,
|
||||
point_lon,
|
||||
point_hae,
|
||||
track_course,
|
||||
track_speed,
|
||||
sensor_azimuth,
|
||||
sensor_elevation,
|
||||
sensor_fov,
|
||||
sensor_vfov,
|
||||
sensor_range,
|
||||
attitude_yaw,
|
||||
attitude_pitch,
|
||||
attitude_roll,
|
||||
hal,
|
||||
vehicle_type_tag,
|
||||
is_flying,
|
||||
link_uid,
|
||||
) = <(
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
f64,
|
||||
f64,
|
||||
f32,
|
||||
i32,
|
||||
f32,
|
||||
i32,
|
||||
i32,
|
||||
i32,
|
||||
i32,
|
||||
i32,
|
||||
i32,
|
||||
f32,
|
||||
f32,
|
||||
f32,
|
||||
String,
|
||||
i32,
|
||||
String,
|
||||
)>::from_arma(data)?;
|
||||
|
||||
Ok(Self {
|
||||
uid,
|
||||
cot_type,
|
||||
callsign,
|
||||
point_lat,
|
||||
point_lon,
|
||||
point_hae,
|
||||
track_course,
|
||||
track_speed,
|
||||
sensor_azimuth,
|
||||
sensor_elevation,
|
||||
sensor_fov,
|
||||
sensor_vfov,
|
||||
sensor_range,
|
||||
attitude_yaw,
|
||||
attitude_pitch,
|
||||
attitude_roll,
|
||||
hal,
|
||||
vehicle_type_tag,
|
||||
is_flying,
|
||||
link_uid,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl UasPlatformCoTPayload {
|
||||
pub fn to_xml(&self) -> String {
|
||||
let uid = escape_xml(&self.uid);
|
||||
let cot_type = escape_xml(&self.cot_type);
|
||||
let callsign = escape_xml(&self.callsign);
|
||||
let link_uid = escape_xml(&self.link_uid);
|
||||
let (vehicle_type_tag, video_url) =
|
||||
match self.vehicle_type_tag.split_once("|armatak_video_url=") {
|
||||
Some((vehicle_type_tag, video_url)) => (
|
||||
escape_xml(vehicle_type_tag),
|
||||
Some(escape_xml(video_url.trim())).filter(|value| !value.is_empty()),
|
||||
),
|
||||
None => (escape_xml(&self.vehicle_type_tag), None),
|
||||
};
|
||||
let now = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
|
||||
let stale =
|
||||
(Utc::now() + Duration::milliseconds(3500)).to_rfc3339_opts(SecondsFormat::Millis, true);
|
||||
|
||||
let mut xml = String::new();
|
||||
xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>");
|
||||
xml.push_str(&format!(
|
||||
"<event version=\"2.0\" uid=\"{uid}\" type=\"{cot_type}\" time=\"{now}\" start=\"{now}\" stale=\"{stale}\" how=\"m-g\" access=\"Undefined\">",
|
||||
cot_type = cot_type,
|
||||
uid = 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>");
|
||||
xml.push_str("<_uastool extendedCot=\"true\" activeRoute=\"false\"/>");
|
||||
xml.push_str(&format!(
|
||||
"<track course=\"{}\" slope=\"0.0\" speed=\"{}\"/>",
|
||||
self.track_course,
|
||||
self.track_speed,
|
||||
));
|
||||
xml.push_str(&format!(
|
||||
"<sensor elevation=\"{}\" vfov=\"{}\" north=\"0.0\" roll=\"0.0\" range=\"{}\" azimuth=\"{}\" fov=\"{}\" type=\"r-e\" version=\"0.6\"/>",
|
||||
self.sensor_elevation,
|
||||
self.sensor_vfov,
|
||||
self.sensor_range,
|
||||
self.sensor_azimuth,
|
||||
self.sensor_fov,
|
||||
));
|
||||
xml.push_str(&format!(
|
||||
"<spatial><attitude roll=\"{}\" pitch=\"{}\" yaw=\"{}\"/><spin roll=\"0.0\" pitch=\"0.0\" yaw=\"0.0\"/></spatial>",
|
||||
self.attitude_roll,
|
||||
self.attitude_pitch,
|
||||
self.attitude_yaw,
|
||||
));
|
||||
xml.push_str(&format!(
|
||||
"<vehicle goHomeBatteryPercent=\"-2147483648\" hal=\"{}\" flightTimeRemaining=\"-2147483648\" typeTag=\"{}\" batteryRemainingCapacity=\"-2147483648\" isFlying=\"{}\" flightTime=\"-2147483648\" type=\"Generic\" batteryMaxCapacity=\"-2147483648\"/>",
|
||||
self.hal,
|
||||
vehicle_type_tag,
|
||||
if self.is_flying != 0 { "true" } else { "false" },
|
||||
));
|
||||
xml.push_str("<_radio rssi=\"-2147483648\" gps=\"false\"/>");
|
||||
xml.push_str(&format!("<contact callsign=\"{}\"/>", callsign));
|
||||
xml.push_str("<waypointCollection></waypointCollection>");
|
||||
xml.push_str(&format!("<_route sender=\"{}\"/>", link_uid));
|
||||
xml.push_str("<commandedData climbRate=\"0.0\"/>");
|
||||
if let Some(video_url) = video_url {
|
||||
xml.push_str(&video_detail_xml(&video_url, &self.uid, &self.callsign));
|
||||
} else {
|
||||
xml.push_str("<__video></__video>");
|
||||
}
|
||||
xml.push_str(&format!("<link uid=\"{}\" type=\"a-f-G-U-C\" relation=\"p-p\" />", link_uid));
|
||||
xml.push_str("</detail></event>");
|
||||
xml
|
||||
}
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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)?;
|
||||
let (uid, callsign, video_url) =
|
||||
<(String, String, String)>::from_arma(data)?;
|
||||
Ok(Self {
|
||||
uid,
|
||||
callsign,
|
||||
@@ -210,6 +63,8 @@ impl FromArma for UasVideoCoTPayload {
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -221,60 +76,103 @@ impl UasVideoCoTPayload {
|
||||
return String::new();
|
||||
}
|
||||
};
|
||||
let callsign = escape_xml(&self.callsign);
|
||||
let uid = escape_xml(&self.uid);
|
||||
let address = escape_xml(&address);
|
||||
let path = escape_xml(&path);
|
||||
|
||||
let now = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
|
||||
let stale =
|
||||
(Utc::now() + Duration::seconds(3600)).to_rfc3339_opts(SecondsFormat::Millis, true);
|
||||
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 = uid,
|
||||
"<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\"/>",
|
||||
"<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\"/>",
|
||||
"<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 = uid,
|
||||
callsign = callsign,
|
||||
uid = self.uid,
|
||||
callsign = self.callsign,
|
||||
));
|
||||
xml.push_str("</__video>");
|
||||
xml.push_str(&format!("<contact callsign=\"{}\"/>", callsign));
|
||||
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)?;
|
||||
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,
|
||||
@@ -290,41 +188,56 @@ impl FromArma for UasSensorCoTPayload {
|
||||
}
|
||||
|
||||
impl UasSensorCoTPayload {
|
||||
/// Build the complete XML string for the b-m-p-s-p-loc CoT event.
|
||||
pub fn to_xml(&self) -> String {
|
||||
let uid = escape_xml(&self.uid);
|
||||
let video_uid = escape_xml(&self.video_uid);
|
||||
let callsign = escape_xml(&self.callsign);
|
||||
let now = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
|
||||
let stale =
|
||||
(Utc::now() + Duration::seconds(60)).to_rfc3339_opts(SecondsFormat::Millis, true);
|
||||
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 = uid,
|
||||
"<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\"/>",
|
||||
"<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}\"/>",
|
||||
"<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,
|
||||
));
|
||||
xml.push_str(&format!("<__video uid=\"{}\"/>", video_uid));
|
||||
xml.push_str(&format!("<contact callsign=\"{}\"/>", callsign));
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
fn escape_xml_attribute(value: &str) -> String {
|
||||
value
|
||||
.replace('&', "&")
|
||||
.replace('"', """)
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('\'', "'")
|
||||
}
|
||||
|
||||
fn parse_video_url(url: &str) -> Option<(String, String, String, String)> {
|
||||
let (protocol, rest) = url.trim().split_once("://")?;
|
||||
let (authority, path) = match rest.split_once('/') {
|
||||
Some((authority, path)) => (authority, format!("/{}", path)),
|
||||
None => (rest, String::new()),
|
||||
};
|
||||
let host_port = authority.rsplit_once('@').map_or(authority, |(_, host_port)| host_port);
|
||||
let (address, port) = host_port.rsplit_once(':')?;
|
||||
|
||||
if protocol.is_empty() || address.is_empty() || port.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((
|
||||
protocol.to_ascii_lowercase(),
|
||||
address.to_string(),
|
||||
port.to_string(),
|
||||
path,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn video_detail_xml(video_url: &str, uid: &str, callsign: &str) -> String {
|
||||
let trimmed_url = video_url.trim();
|
||||
if trimmed_url.is_empty() {
|
||||
return "<__video></__video>".to_string();
|
||||
}
|
||||
|
||||
let Some((protocol, address, port, path)) = parse_video_url(trimmed_url) else {
|
||||
return format!(
|
||||
"<__video url=\"{}\"/>",
|
||||
escape_xml_attribute(trimmed_url)
|
||||
);
|
||||
};
|
||||
|
||||
format!(
|
||||
"<__video><ConnectionEntry protocol=\"{}\" path=\"{}\" address=\"{}\" port=\"{}\" uid=\"{}\" alias=\"{}\" roverPort=\"-1\" rtspReliable=\"0\" ignoreEmbeddedKLV=\"False\" networkTimeout=\"0\" bufferTime=\"-1\"/></__video>",
|
||||
escape_xml_attribute(&protocol),
|
||||
escape_xml_attribute(&path),
|
||||
escape_xml_attribute(&address),
|
||||
escape_xml_attribute(&port),
|
||||
escape_xml_attribute(uid),
|
||||
escape_xml_attribute(callsign),
|
||||
)
|
||||
}
|
||||
18
src/lib.rs
18
src/lib.rs
@@ -1,7 +1,5 @@
|
||||
use arma_rs::{arma, Extension, Group};
|
||||
use rustls::crypto::aws_lc_rs;
|
||||
mod uas;
|
||||
mod mdns;
|
||||
mod structs;
|
||||
mod tcp;
|
||||
mod tests;
|
||||
@@ -41,20 +39,6 @@ pub fn init() -> Extension {
|
||||
.command("local_ip", utils::address::get_local_address)
|
||||
.command("uuid", utils::uuid::get_uuid)
|
||||
.command("log", utils::log::log_info)
|
||||
.group(
|
||||
"uas",
|
||||
Group::new()
|
||||
.command("start_endpoint", uas::start_endpoint)
|
||||
.command("stop_endpoint", uas::stop_endpoint)
|
||||
.command("send_uas_telemetry", uas::send_uas_telemetry)
|
||||
.command("send_uas_system", uas::send_uas_system),
|
||||
)
|
||||
.group(
|
||||
"mdns",
|
||||
Group::new()
|
||||
.command("start_uas_advertisement", mdns::start_uas_advertisement)
|
||||
.command("stop", mdns::stop),
|
||||
)
|
||||
.group(
|
||||
"udp_socket",
|
||||
Group::new()
|
||||
@@ -78,7 +62,7 @@ pub fn init() -> Extension {
|
||||
.command("marker", tcp::cot::send_marker_cot)
|
||||
.command("digital_pointer", tcp::cot::send_digital_pointer_cot)
|
||||
.command("chat", tcp::cot::send_message_cot)
|
||||
.command("uas_platform", tcp::cot::send_uas_platform_cot)
|
||||
// UAS Tool integration
|
||||
.command("uas_video", tcp::cot::send_uas_video_cot)
|
||||
.command("uas_sensor", tcp::cot::send_uas_sensor_cot),
|
||||
)
|
||||
|
||||
207
src/mdns.rs
207
src/mdns.rs
@@ -1,207 +0,0 @@
|
||||
use arma_rs::Context;
|
||||
use lazy_static::lazy_static;
|
||||
use log::info;
|
||||
use std::net::{Ipv4Addr, SocketAddrV4, UdpSocket};
|
||||
use std::sync::mpsc::{self, Receiver, Sender};
|
||||
use std::sync::Mutex;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
lazy_static! {
|
||||
static ref MDNS_CTRL: Mutex<Option<Sender<()>>> = Mutex::new(None);
|
||||
}
|
||||
|
||||
fn detect_local_ipv4() -> Result<Ipv4Addr, 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())?;
|
||||
match socket.local_addr().map_err(|e| e.to_string())? {
|
||||
std::net::SocketAddr::V4(addr) => Ok(*addr.ip()),
|
||||
std::net::SocketAddr::V6(_) => Err("Local address is not IPv4".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn sanitize_label(value: &str, fallback: &str) -> String {
|
||||
let mut sanitized = value
|
||||
.chars()
|
||||
.map(|c| if c.is_ascii_alphanumeric() || c == '-' { c } else { '-' })
|
||||
.collect::<String>()
|
||||
.trim_matches('-')
|
||||
.to_string();
|
||||
|
||||
if sanitized.is_empty() {
|
||||
sanitized = fallback.to_string();
|
||||
}
|
||||
|
||||
if sanitized.len() > 63 {
|
||||
sanitized.truncate(63);
|
||||
}
|
||||
|
||||
sanitized
|
||||
}
|
||||
|
||||
fn encode_name(name: &str) -> Vec<u8> {
|
||||
let mut encoded = Vec::new();
|
||||
for label in name.split('.') {
|
||||
let bytes = label.as_bytes();
|
||||
encoded.push(bytes.len() as u8);
|
||||
encoded.extend_from_slice(bytes);
|
||||
}
|
||||
encoded.push(0);
|
||||
encoded
|
||||
}
|
||||
|
||||
fn push_u16(buf: &mut Vec<u8>, value: u16) {
|
||||
buf.extend_from_slice(&value.to_be_bytes());
|
||||
}
|
||||
|
||||
fn push_u32(buf: &mut Vec<u8>, value: u32) {
|
||||
buf.extend_from_slice(&value.to_be_bytes());
|
||||
}
|
||||
|
||||
fn push_record(buf: &mut Vec<u8>, name: &str, rr_type: u16, rr_class: u16, ttl: u32, rdata: &[u8]) {
|
||||
buf.extend_from_slice(&encode_name(name));
|
||||
push_u16(buf, rr_type);
|
||||
push_u16(buf, rr_class);
|
||||
push_u32(buf, ttl);
|
||||
push_u16(buf, rdata.len() as u16);
|
||||
buf.extend_from_slice(rdata);
|
||||
}
|
||||
|
||||
fn build_mdns_packet(instance_name: &str, host_name: &str, ip: Ipv4Addr, port: u16, video_uri: &str) -> Vec<u8> {
|
||||
let service_type = "_mavlink._udp.local";
|
||||
let instance_fqdn = format!("{}.{}", instance_name, service_type);
|
||||
let host_fqdn = format!("{}.local", host_name);
|
||||
|
||||
let mut packet = Vec::new();
|
||||
push_u16(&mut packet, 0);
|
||||
push_u16(&mut packet, 0x8400);
|
||||
push_u16(&mut packet, 0);
|
||||
push_u16(&mut packet, 4);
|
||||
push_u16(&mut packet, 0);
|
||||
push_u16(&mut packet, 0);
|
||||
|
||||
let ptr_rdata = encode_name(&instance_fqdn);
|
||||
push_record(&mut packet, service_type, 12, 0x0001, 120, &ptr_rdata);
|
||||
|
||||
let mut srv_rdata = Vec::new();
|
||||
push_u16(&mut srv_rdata, 0);
|
||||
push_u16(&mut srv_rdata, 0);
|
||||
push_u16(&mut srv_rdata, port);
|
||||
srv_rdata.extend_from_slice(&encode_name(&host_fqdn));
|
||||
push_record(&mut packet, &instance_fqdn, 33, 0x8001, 120, &srv_rdata);
|
||||
|
||||
let txt_value = format!("uri={}", video_uri);
|
||||
let txt_bytes = txt_value.as_bytes();
|
||||
let mut txt_rdata = Vec::new();
|
||||
txt_rdata.push(txt_bytes.len() as u8);
|
||||
txt_rdata.extend_from_slice(txt_bytes);
|
||||
push_record(&mut packet, &instance_fqdn, 16, 0x8001, 120, &txt_rdata);
|
||||
|
||||
let a_rdata = ip.octets();
|
||||
push_record(&mut packet, &host_fqdn, 1, 0x8001, 120, &a_rdata);
|
||||
|
||||
packet
|
||||
}
|
||||
|
||||
fn stop_existing() {
|
||||
if let Ok(mut lock) = MDNS_CTRL.lock() {
|
||||
if let Some(tx) = lock.take() {
|
||||
let _ = tx.send(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_uas_advertisement(
|
||||
ctx: Context,
|
||||
instance_name: String,
|
||||
mavlink_port: i32,
|
||||
video_uri: String,
|
||||
) -> &'static str {
|
||||
stop_existing();
|
||||
|
||||
let local_ip = match detect_local_ipv4() {
|
||||
Ok(ip) => ip,
|
||||
Err(error) => {
|
||||
let _ = ctx.callback_data("MDNS ERROR", "Failed to determine local IPv4", error.clone());
|
||||
return "mdns local IPv4 error";
|
||||
}
|
||||
};
|
||||
|
||||
let port = mavlink_port.clamp(1, 65535) as u16;
|
||||
let safe_instance = sanitize_label(&instance_name, "ArmaTAK-UAS");
|
||||
let host_label = sanitize_label(
|
||||
&format!("armatak-{}", safe_instance.to_lowercase()),
|
||||
"armatak-uas-host",
|
||||
);
|
||||
let packet = build_mdns_packet(&safe_instance, &host_label, local_ip, port, &video_uri);
|
||||
let callback_video_uri = video_uri.clone();
|
||||
let multicast_addr = SocketAddrV4::new(Ipv4Addr::new(224, 0, 0, 251), 5353);
|
||||
|
||||
let (stop_tx, stop_rx): (Sender<()>, Receiver<()>) = mpsc::channel();
|
||||
if let Ok(mut lock) = MDNS_CTRL.lock() {
|
||||
*lock = Some(stop_tx);
|
||||
}
|
||||
|
||||
thread::spawn(move || {
|
||||
let socket = match UdpSocket::bind("0.0.0.0:0") {
|
||||
Ok(socket) => socket,
|
||||
Err(error) => {
|
||||
info!("mDNS failed to bind UDP socket: {}", error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let _ = socket.set_multicast_ttl_v4(255);
|
||||
let _ = socket.set_multicast_loop_v4(true);
|
||||
|
||||
info!(
|
||||
"Starting mDNS UAS advertisement instance={} host={} ip={} port={} video_uri={}",
|
||||
safe_instance, host_label, local_ip, port, video_uri
|
||||
);
|
||||
|
||||
loop {
|
||||
match socket.send_to(&packet, multicast_addr) {
|
||||
Ok(size) => info!("Sent mDNS UAS advertisement ({} bytes) to {}", size, multicast_addr),
|
||||
Err(error) => info!("Failed sending mDNS UAS advertisement: {}", error),
|
||||
}
|
||||
|
||||
match stop_rx.recv_timeout(Duration::from_secs(5)) {
|
||||
Ok(_) => break,
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => {}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
|
||||
info!("Stopped mDNS UAS advertisement for instance={}", safe_instance);
|
||||
});
|
||||
|
||||
let _ = ctx.callback_data(
|
||||
"MDNS",
|
||||
"UAS advertisement started",
|
||||
format!("{}:{} | {}", local_ip, port, callback_video_uri),
|
||||
);
|
||||
|
||||
"starting mdns uas advertisement"
|
||||
}
|
||||
|
||||
pub fn stop(ctx: Context) -> &'static str {
|
||||
let had_running = match MDNS_CTRL.lock() {
|
||||
Ok(mut lock) => {
|
||||
if let Some(tx) = lock.take() {
|
||||
let _ = tx.send(());
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Err(_) => false,
|
||||
};
|
||||
|
||||
if had_running {
|
||||
let _ = ctx.callback_null("MDNS", "UAS advertisement stopped");
|
||||
"stopping mdns advertisement"
|
||||
} else {
|
||||
let _ = ctx.callback_null("MDNS ERROR", "No mDNS advertisement is running");
|
||||
"no mdns advertisement running"
|
||||
}
|
||||
}
|
||||
@@ -40,16 +40,10 @@ pub fn send_message_cot(
|
||||
"Sending Message CoT to TCP server"
|
||||
}
|
||||
|
||||
pub fn send_uas_platform_cot(
|
||||
ctx: Context,
|
||||
payload: cot::uas::UasPlatformCoTPayload,
|
||||
) -> &'static str {
|
||||
let xml = payload.to_xml();
|
||||
send_payload(ctx, xml);
|
||||
|
||||
"Sending UAS Platform main 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,
|
||||
@@ -62,6 +56,9 @@ pub fn send_uas_video_cot(
|
||||
"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,
|
||||
|
||||
1021
src/uas/callbacks.rs
1021
src/uas/callbacks.rs
File diff suppressed because it is too large
Load Diff
@@ -1,43 +0,0 @@
|
||||
pub const AUTOPILOT_COMPONENT_ID: u8 = 1;
|
||||
pub const CAMERA_COMPONENT_ID: u8 = 100;
|
||||
pub const TURRET_CAMERA_COMPONENT_ID: u8 = 101;
|
||||
pub const GIMBAL_COMPONENT_ID: u8 = 154;
|
||||
|
||||
pub const MAV_TYPE_FIXED_WING: u8 = 1;
|
||||
pub const MAV_TYPE_QUADROTOR: u8 = 2;
|
||||
pub const MAV_TYPE_HELICOPTER: u8 = 4;
|
||||
pub const MAV_TYPE_GIMBAL: u8 = 26;
|
||||
pub const MAV_TYPE_CAMERA: u8 = 30;
|
||||
|
||||
pub const MAV_AUTOPILOT_ARDUPILOTMEGA: u8 = 3;
|
||||
pub const MAV_AUTOPILOT_INVALID: u8 = 8;
|
||||
|
||||
pub const MAV_STATE_STANDBY: u8 = 3;
|
||||
pub const MAV_STATE_ACTIVE: u8 = 4;
|
||||
|
||||
pub const MAV_MODE_FLAG_CUSTOM_MODE_ENABLED: u8 = 1;
|
||||
pub const MAV_MODE_FLAG_SAFETY_ARMED: u8 = 128;
|
||||
pub const MAV_LANDED_STATE_UNDEFINED: u8 = 0;
|
||||
pub const MAV_LANDED_STATE_ON_GROUND: u8 = 1;
|
||||
pub const MAV_LANDED_STATE_IN_AIR: u8 = 2;
|
||||
|
||||
pub const MAV_PROTOCOL_CAPABILITY_MISSION_INT: u64 = 4;
|
||||
pub const MAV_PROTOCOL_CAPABILITY_COMMAND_INT: u64 = 8;
|
||||
pub const MAV_PROTOCOL_CAPABILITY_FTP: u64 = 32;
|
||||
pub const MAV_PROTOCOL_CAPABILITY_SET_POSITION_TARGET_GLOBAL_INT: u64 = 256;
|
||||
pub const MAV_PROTOCOL_CAPABILITY_MAVLINK2: u64 = 8192;
|
||||
pub const MAV_PROTOCOL_CAPABILITY_COMPONENT_IMPLEMENTS_GIMBAL_MANAGER: u64 = 262_144;
|
||||
|
||||
pub const CAMERA_CAP_FLAGS_CAPTURE_VIDEO: u32 = 1;
|
||||
pub const CAMERA_CAP_FLAGS_CAPTURE_IMAGE: u32 = 2;
|
||||
pub const CAMERA_CAP_FLAGS_HAS_MODES: u32 = 4;
|
||||
pub const CAMERA_CAP_FLAGS_HAS_BASIC_ZOOM: u32 = 64;
|
||||
pub const CAMERA_CAP_FLAGS_HAS_VIDEO_STREAM: u32 = 256;
|
||||
pub const VIDEO_STREAM_STATUS_FLAGS_RUNNING: u16 = 1;
|
||||
pub const VIDEO_STREAM_TYPE_RTSP: u8 = 0;
|
||||
pub const VIDEO_STREAM_TYPE_RTPUDP: u8 = 1;
|
||||
pub const VIDEO_STREAM_TYPE_TCP_MPEG: u8 = 2;
|
||||
pub const VIDEO_STREAM_TYPE_MPEG_TS: u8 = 3;
|
||||
pub const VIDEO_STREAM_ENCODING_H264: u8 = 1;
|
||||
|
||||
pub const GIMBAL_MANAGER_CAP_FLAGS_BASIC_PITCH_YAW: u32 = 32 | 128 | 256 | 1024 | 4096 | 131_072;
|
||||
102
src/uas/crc.rs
102
src/uas/crc.rs
@@ -1,102 +0,0 @@
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
|
||||
static MAVLINK_SEQUENCE: AtomicU8 = AtomicU8::new(0);
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct FieldSpec {
|
||||
pub ty: &'static str,
|
||||
pub name: &'static str,
|
||||
pub array_len: usize,
|
||||
}
|
||||
|
||||
fn crc_accumulate(byte: u8, crc: &mut u16) {
|
||||
let mut tmp = byte ^ (*crc as u8);
|
||||
tmp ^= tmp << 4;
|
||||
*crc = (*crc >> 8) ^ ((tmp as u16) << 8) ^ ((tmp as u16) << 3) ^ ((tmp as u16) >> 4);
|
||||
}
|
||||
|
||||
fn mavlink_crc(bytes: &[u8], crc_extra: u8) -> u16 {
|
||||
let mut crc = 0xFFFFu16;
|
||||
for byte in bytes {
|
||||
crc_accumulate(*byte, &mut crc);
|
||||
}
|
||||
crc_accumulate(crc_extra, &mut crc);
|
||||
crc
|
||||
}
|
||||
|
||||
pub(crate) fn calculate_crc_extra(message_name: &str, base_fields: &[FieldSpec]) -> u8 {
|
||||
let mut crc = 0xFFFFu16;
|
||||
|
||||
for byte in message_name.as_bytes() {
|
||||
crc_accumulate(*byte, &mut crc);
|
||||
}
|
||||
crc_accumulate(b' ', &mut crc);
|
||||
|
||||
for field in base_fields {
|
||||
for byte in field.ty.as_bytes() {
|
||||
crc_accumulate(*byte, &mut crc);
|
||||
}
|
||||
crc_accumulate(b' ', &mut crc);
|
||||
|
||||
for byte in field.name.as_bytes() {
|
||||
crc_accumulate(*byte, &mut crc);
|
||||
}
|
||||
crc_accumulate(b' ', &mut crc);
|
||||
|
||||
if field.array_len > 0 {
|
||||
crc_accumulate(field.array_len as u8, &mut crc);
|
||||
}
|
||||
}
|
||||
|
||||
((crc & 0xFF) ^ (crc >> 8)) as u8
|
||||
}
|
||||
|
||||
pub(crate) fn build_v1_packet(
|
||||
system_id: u8,
|
||||
component_id: u8,
|
||||
msg_id: u8,
|
||||
payload: &[u8],
|
||||
crc_extra: u8,
|
||||
) -> Vec<u8> {
|
||||
let seq = MAVLINK_SEQUENCE.fetch_add(1, Ordering::Relaxed);
|
||||
let mut packet = Vec::with_capacity(payload.len() + 8);
|
||||
packet.push(0xFE);
|
||||
packet.push(payload.len() as u8);
|
||||
packet.push(seq);
|
||||
packet.push(system_id);
|
||||
packet.push(component_id);
|
||||
packet.push(msg_id);
|
||||
packet.extend_from_slice(payload);
|
||||
|
||||
let crc = mavlink_crc(&packet[1..], crc_extra);
|
||||
packet.push((crc & 0xFF) as u8);
|
||||
packet.push((crc >> 8) as u8);
|
||||
packet
|
||||
}
|
||||
|
||||
pub(crate) fn build_v2_packet(
|
||||
system_id: u8,
|
||||
component_id: u8,
|
||||
msg_id: u32,
|
||||
payload: &[u8],
|
||||
crc_extra: u8,
|
||||
) -> Vec<u8> {
|
||||
let seq = MAVLINK_SEQUENCE.fetch_add(1, Ordering::Relaxed);
|
||||
let mut packet = Vec::with_capacity(payload.len() + 12);
|
||||
packet.push(0xFD);
|
||||
packet.push(payload.len() as u8);
|
||||
packet.push(0);
|
||||
packet.push(0);
|
||||
packet.push(seq);
|
||||
packet.push(system_id);
|
||||
packet.push(component_id);
|
||||
packet.push((msg_id & 0xFF) as u8);
|
||||
packet.push(((msg_id >> 8) & 0xFF) as u8);
|
||||
packet.push(((msg_id >> 16) & 0xFF) as u8);
|
||||
packet.extend_from_slice(payload);
|
||||
|
||||
let crc = mavlink_crc(&packet[1..], crc_extra);
|
||||
packet.push((crc & 0xFF) as u8);
|
||||
packet.push((crc >> 8) as u8);
|
||||
packet
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
use arma_rs::Context;
|
||||
use log::info;
|
||||
use std::net::UdpSocket;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread::{self, JoinHandle};
|
||||
use std::time::Duration;
|
||||
|
||||
use super::callbacks::{mavlink_callback_event, mavlink_packet_summary, mavlink_response_packets};
|
||||
|
||||
pub(crate) struct MavlinkEndpoint {
|
||||
pub socket: UdpSocket,
|
||||
pub running: Arc<AtomicBool>,
|
||||
pub listener: Option<JoinHandle<()>>,
|
||||
pub bind_port: u16,
|
||||
}
|
||||
|
||||
static MAVLINK_ENDPOINT: Mutex<Option<MavlinkEndpoint>> = Mutex::new(None);
|
||||
|
||||
pub(crate) fn socket_for_send() -> Option<UdpSocket> {
|
||||
MAVLINK_ENDPOINT.lock().ok().and_then(|endpoint| {
|
||||
endpoint
|
||||
.as_ref()
|
||||
.and_then(|entry| entry.socket.try_clone().ok())
|
||||
})
|
||||
}
|
||||
|
||||
fn stop_endpoint_internal() {
|
||||
let endpoint = MAVLINK_ENDPOINT.lock().unwrap().take();
|
||||
|
||||
if let Some(mut endpoint) = endpoint {
|
||||
endpoint.running.store(false, Ordering::Relaxed);
|
||||
info!(
|
||||
"Stopping MAVLink UDP endpoint on 0.0.0.0:{}",
|
||||
endpoint.bind_port
|
||||
);
|
||||
|
||||
if let Some(listener) = endpoint.listener.take() {
|
||||
let _ = listener.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_endpoint(ctx: Context, bind_port: i32) -> &'static str {
|
||||
let bind_port = bind_port.clamp(1, 65535) as u16;
|
||||
|
||||
stop_endpoint_internal();
|
||||
|
||||
let socket = match UdpSocket::bind(format!("0.0.0.0:{bind_port}")) {
|
||||
Ok(socket) => socket,
|
||||
Err(error) => {
|
||||
let _ = ctx.callback_data(
|
||||
"MAVLINK UDP ERROR",
|
||||
"failed to bind MAVLink UDP endpoint",
|
||||
error.to_string(),
|
||||
);
|
||||
info!(
|
||||
"Failed to bind MAVLink UDP endpoint on 0.0.0.0:{}: {}",
|
||||
bind_port, error
|
||||
);
|
||||
return "Failed to bind MAVLink UDP endpoint";
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(error) = socket.set_read_timeout(Some(Duration::from_millis(500))) {
|
||||
info!(
|
||||
"Failed to set MAVLink UDP endpoint read timeout on 0.0.0.0:{}: {}",
|
||||
bind_port, error
|
||||
);
|
||||
}
|
||||
|
||||
let listener_socket = match socket.try_clone() {
|
||||
Ok(listener_socket) => listener_socket,
|
||||
Err(error) => {
|
||||
let _ = ctx.callback_data(
|
||||
"MAVLINK UDP ERROR",
|
||||
"failed to clone MAVLink UDP endpoint socket",
|
||||
error.to_string(),
|
||||
);
|
||||
info!(
|
||||
"Failed to clone MAVLink UDP endpoint socket on 0.0.0.0:{}: {}",
|
||||
bind_port, error
|
||||
);
|
||||
return "Failed to clone MAVLink UDP endpoint socket";
|
||||
}
|
||||
};
|
||||
|
||||
let running = Arc::new(AtomicBool::new(true));
|
||||
let listener_running = Arc::clone(&running);
|
||||
|
||||
let listener_ctx = ctx;
|
||||
let listener = thread::spawn(move || {
|
||||
let mut buffer = [0u8; 2048];
|
||||
info!("MAVLink UDP endpoint listening on 0.0.0.0:{}", bind_port);
|
||||
|
||||
while listener_running.load(Ordering::Relaxed) {
|
||||
match listener_socket.recv_from(&mut buffer) {
|
||||
Ok((received, source)) => {
|
||||
let source_string = source.to_string();
|
||||
info!(
|
||||
"MAVLink UDP endpoint received {} bytes from {}: {}",
|
||||
received,
|
||||
source,
|
||||
mavlink_packet_summary(&buffer[..received])
|
||||
);
|
||||
if let Some(event) = mavlink_callback_event(&buffer[..received], &source_string)
|
||||
{
|
||||
let _ =
|
||||
listener_ctx.callback_data("MAVLINK UDP", event.function, event.data);
|
||||
}
|
||||
for packet in mavlink_response_packets(&buffer[..received]) {
|
||||
if let Err(error) = listener_socket.send_to(&packet, source) {
|
||||
info!(
|
||||
"MAVLink UDP endpoint failed sending response to {}: {}",
|
||||
source, error
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error)
|
||||
if matches!(
|
||||
error.kind(),
|
||||
std::io::ErrorKind::WouldBlock
|
||||
| std::io::ErrorKind::TimedOut
|
||||
| std::io::ErrorKind::ConnectionReset
|
||||
) => {}
|
||||
Err(error) => {
|
||||
if listener_running.load(Ordering::Relaxed) {
|
||||
info!(
|
||||
"MAVLink UDP endpoint listener error on 0.0.0.0:{}: {}",
|
||||
bind_port, error
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
"MAVLink UDP endpoint listener stopped on 0.0.0.0:{}",
|
||||
bind_port
|
||||
);
|
||||
});
|
||||
|
||||
*MAVLINK_ENDPOINT.lock().unwrap() = Some(MavlinkEndpoint {
|
||||
socket,
|
||||
running,
|
||||
listener: Some(listener),
|
||||
bind_port,
|
||||
});
|
||||
|
||||
info!("Started MAVLink UDP endpoint on 0.0.0.0:{}", bind_port);
|
||||
"Started MAVLink UDP endpoint"
|
||||
}
|
||||
|
||||
pub fn stop_endpoint(_ctx: Context) -> &'static str {
|
||||
stop_endpoint_internal();
|
||||
"Stopped MAVLink UDP endpoint"
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
use super::constants::{MAV_TYPE_FIXED_WING, MAV_TYPE_HELICOPTER, MAV_TYPE_QUADROTOR};
|
||||
|
||||
pub(crate) fn map_vehicle_type(vehicle_type: u8) -> u8 {
|
||||
match vehicle_type {
|
||||
1 => MAV_TYPE_FIXED_WING,
|
||||
3 => MAV_TYPE_HELICOPTER,
|
||||
2 => MAV_TYPE_QUADROTOR,
|
||||
value => value,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_heading_deg(value: f32) -> f32 {
|
||||
((value % 360.0) + 360.0) % 360.0
|
||||
}
|
||||
|
||||
pub(crate) fn stable_system_id(entity_uuid: &str) -> u8 {
|
||||
let mut hash: u32 = 0x811C9DC5;
|
||||
for byte in entity_uuid.as_bytes() {
|
||||
hash ^= *byte as u32;
|
||||
hash = hash.wrapping_mul(0x01000193);
|
||||
}
|
||||
|
||||
((hash % 250) as u8) + 1
|
||||
}
|
||||
|
||||
pub(crate) fn stable_mavlink_identity(callsign: &str, entity_uuid: &str) -> String {
|
||||
let mut identity = callsign.trim().to_string();
|
||||
|
||||
for suffix in [" [ON]", " [OFF]"] {
|
||||
if identity.ends_with(suffix) {
|
||||
identity.truncate(identity.len() - suffix.len());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if identity.is_empty() {
|
||||
entity_uuid.trim().to_string()
|
||||
} else {
|
||||
identity
|
||||
}
|
||||
}
|
||||
|
||||
fn uuid16(entity_uuid: &str) -> [u8; 16] {
|
||||
let hex = entity_uuid.replace('-', "");
|
||||
let mut bytes = [0u8; 16];
|
||||
|
||||
for index in 0..16 {
|
||||
let start = index * 2;
|
||||
if start + 2 <= hex.len() {
|
||||
if let Ok(value) = u8::from_str_radix(&hex[start..start + 2], 16) {
|
||||
bytes[index] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bytes
|
||||
}
|
||||
|
||||
pub(crate) fn uid64_from_uuid(entity_uuid: &str) -> u64 {
|
||||
let uuid = uuid16(entity_uuid);
|
||||
let mut bytes = [0u8; 8];
|
||||
bytes.copy_from_slice(&uuid[..8]);
|
||||
u64::from_le_bytes(bytes)
|
||||
}
|
||||
|
||||
pub(crate) fn uid2_from_uuid(entity_uuid: &str) -> [u8; 18] {
|
||||
let uuid = uuid16(entity_uuid);
|
||||
let mut uid2 = [0u8; 18];
|
||||
uid2[..16].copy_from_slice(&uuid);
|
||||
|
||||
let checksum = entity_uuid
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.fold(0u16, |acc, value| acc.wrapping_add(*value as u16));
|
||||
uid2[16] = (checksum & 0xFF) as u8;
|
||||
uid2[17] = (checksum >> 8) as u8;
|
||||
uid2
|
||||
}
|
||||
|
||||
pub(crate) fn fixed_string<const N: usize>(value: &str) -> [u8; N] {
|
||||
let mut bytes = [0u8; N];
|
||||
let raw = value.as_bytes();
|
||||
let len = raw.len().min(N.saturating_sub(1));
|
||||
bytes[..len].copy_from_slice(&raw[..len]);
|
||||
bytes
|
||||
}
|
||||
|
||||
pub(crate) fn should_send_video_stream_information(video_uri: &str) -> bool {
|
||||
let trimmed = video_uri.trim().to_ascii_lowercase();
|
||||
trimmed.starts_with("rtsp://")
|
||||
|| trimmed.starts_with("rtp://")
|
||||
|| trimmed.starts_with("udp://")
|
||||
|| trimmed.starts_with("mpegts://")
|
||||
|| trimmed.starts_with("tcp://")
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
mod callbacks;
|
||||
mod constants;
|
||||
mod crc;
|
||||
mod endpoint;
|
||||
mod identity;
|
||||
mod packets;
|
||||
mod payload;
|
||||
mod send;
|
||||
mod state;
|
||||
|
||||
pub use endpoint::{start_endpoint, stop_endpoint};
|
||||
#[allow(unused_imports)]
|
||||
pub use payload::{UasSystemPayload, UasTelemetryPayload};
|
||||
pub use send::{send_uas_system, send_uas_telemetry};
|
||||
@@ -1,644 +0,0 @@
|
||||
use chrono::Utc;
|
||||
|
||||
use super::constants::{
|
||||
AUTOPILOT_COMPONENT_ID, CAMERA_CAP_FLAGS_CAPTURE_IMAGE, CAMERA_CAP_FLAGS_CAPTURE_VIDEO,
|
||||
CAMERA_CAP_FLAGS_HAS_BASIC_ZOOM, CAMERA_CAP_FLAGS_HAS_MODES, CAMERA_CAP_FLAGS_HAS_VIDEO_STREAM,
|
||||
GIMBAL_COMPONENT_ID, GIMBAL_MANAGER_CAP_FLAGS_BASIC_PITCH_YAW, MAV_AUTOPILOT_ARDUPILOTMEGA,
|
||||
MAV_AUTOPILOT_INVALID, MAV_LANDED_STATE_IN_AIR, MAV_LANDED_STATE_ON_GROUND,
|
||||
MAV_LANDED_STATE_UNDEFINED, MAV_MODE_FLAG_CUSTOM_MODE_ENABLED, MAV_MODE_FLAG_SAFETY_ARMED,
|
||||
MAV_PROTOCOL_CAPABILITY_COMMAND_INT,
|
||||
MAV_PROTOCOL_CAPABILITY_COMPONENT_IMPLEMENTS_GIMBAL_MANAGER, MAV_PROTOCOL_CAPABILITY_FTP,
|
||||
MAV_PROTOCOL_CAPABILITY_MAVLINK2, MAV_PROTOCOL_CAPABILITY_MISSION_INT,
|
||||
MAV_PROTOCOL_CAPABILITY_SET_POSITION_TARGET_GLOBAL_INT, MAV_STATE_ACTIVE, MAV_STATE_STANDBY,
|
||||
VIDEO_STREAM_ENCODING_H264, VIDEO_STREAM_STATUS_FLAGS_RUNNING, VIDEO_STREAM_TYPE_MPEG_TS,
|
||||
VIDEO_STREAM_TYPE_RTPUDP, VIDEO_STREAM_TYPE_RTSP, VIDEO_STREAM_TYPE_TCP_MPEG,
|
||||
};
|
||||
use super::crc::{build_v1_packet, build_v2_packet, calculate_crc_extra, FieldSpec};
|
||||
use super::identity::{fixed_string, normalize_heading_deg, uid2_from_uuid, uid64_from_uuid};
|
||||
use super::payload::UasTelemetryPayload;
|
||||
|
||||
pub(crate) fn heartbeat_packet(payload: &UasTelemetryPayload) -> Vec<u8> {
|
||||
let mut msg = Vec::with_capacity(9);
|
||||
let custom_mode = if payload.flying { 5u32 } else { 0u32 };
|
||||
msg.extend_from_slice(&custom_mode.to_le_bytes());
|
||||
msg.push(payload.vehicle_type);
|
||||
msg.push(MAV_AUTOPILOT_ARDUPILOTMEGA);
|
||||
msg.push(if payload.flying {
|
||||
MAV_MODE_FLAG_CUSTOM_MODE_ENABLED | MAV_MODE_FLAG_SAFETY_ARMED
|
||||
} else {
|
||||
MAV_MODE_FLAG_CUSTOM_MODE_ENABLED
|
||||
});
|
||||
msg.push(if payload.flying {
|
||||
MAV_STATE_ACTIVE
|
||||
} else {
|
||||
MAV_STATE_STANDBY
|
||||
});
|
||||
msg.push(3);
|
||||
build_v1_packet(payload.system_id, payload.component_id, 0, &msg, 50)
|
||||
}
|
||||
|
||||
pub(crate) fn command_ack_packet(
|
||||
system_id: u8,
|
||||
component_id: u8,
|
||||
command: u16,
|
||||
result: u8,
|
||||
) -> Vec<u8> {
|
||||
let mut msg = Vec::with_capacity(3);
|
||||
msg.extend_from_slice(&command.to_le_bytes());
|
||||
msg.push(result);
|
||||
build_v2_packet(system_id, component_id, 77, &msg, 143)
|
||||
}
|
||||
|
||||
pub(crate) fn mission_request_int_packet(
|
||||
system_id: u8,
|
||||
component_id: u8,
|
||||
target_system: u8,
|
||||
target_component: u8,
|
||||
seq: u16,
|
||||
mission_type: u8,
|
||||
) -> Vec<u8> {
|
||||
let mut msg = Vec::with_capacity(5);
|
||||
msg.extend_from_slice(&seq.to_le_bytes());
|
||||
msg.push(target_system);
|
||||
msg.push(target_component);
|
||||
msg.push(mission_type);
|
||||
build_v2_packet(system_id, component_id, 51, &msg, 196)
|
||||
}
|
||||
|
||||
pub(crate) fn mission_ack_packet(
|
||||
system_id: u8,
|
||||
component_id: u8,
|
||||
target_system: u8,
|
||||
target_component: u8,
|
||||
mission_type: u8,
|
||||
) -> Vec<u8> {
|
||||
let mut msg = Vec::with_capacity(4);
|
||||
msg.push(target_system);
|
||||
msg.push(target_component);
|
||||
msg.push(0);
|
||||
msg.push(mission_type);
|
||||
build_v2_packet(system_id, component_id, 47, &msg, 153)
|
||||
}
|
||||
|
||||
pub(crate) fn gps_raw_int_packet(payload: &UasTelemetryPayload) -> Vec<u8> {
|
||||
let time_usec = (Utc::now().timestamp_millis().max(0) as u64) * 1_000;
|
||||
let fix_type = if payload.flying { 3u8 } else { 2u8 };
|
||||
|
||||
let mut msg = Vec::with_capacity(30);
|
||||
msg.extend_from_slice(&time_usec.to_le_bytes());
|
||||
msg.extend_from_slice(&((payload.lat_deg * 1e7).round() as i32).to_le_bytes());
|
||||
msg.extend_from_slice(&((payload.lon_deg * 1e7).round() as i32).to_le_bytes());
|
||||
msg.extend_from_slice(&((payload.alt_msl_m * 1000.0).round() as i32).to_le_bytes());
|
||||
msg.extend_from_slice(&u16::MAX.to_le_bytes());
|
||||
msg.extend_from_slice(&u16::MAX.to_le_bytes());
|
||||
msg.extend_from_slice(&u16::MAX.to_le_bytes());
|
||||
msg.extend_from_slice(&u16::MAX.to_le_bytes());
|
||||
msg.push(fix_type);
|
||||
msg.push(10);
|
||||
build_v1_packet(payload.system_id, payload.component_id, 24, &msg, 24)
|
||||
}
|
||||
|
||||
pub(crate) fn global_position_int_packet(payload: &UasTelemetryPayload) -> Vec<u8> {
|
||||
let time_boot_ms = Utc::now().timestamp_millis().max(0) as u32;
|
||||
let vx =
|
||||
(payload.groundspeed_mps * payload.heading_deg.to_radians().sin() * 100.0).round() as i16;
|
||||
let vy =
|
||||
(payload.groundspeed_mps * payload.heading_deg.to_radians().cos() * 100.0).round() as i16;
|
||||
let heading = (normalize_heading_deg(payload.heading_deg) * 100.0).round() as u16;
|
||||
|
||||
let mut msg = Vec::with_capacity(28);
|
||||
msg.extend_from_slice(&time_boot_ms.to_le_bytes());
|
||||
msg.extend_from_slice(&((payload.lat_deg * 1e7).round() as i32).to_le_bytes());
|
||||
msg.extend_from_slice(&((payload.lon_deg * 1e7).round() as i32).to_le_bytes());
|
||||
msg.extend_from_slice(&((payload.alt_msl_m * 1000.0).round() as i32).to_le_bytes());
|
||||
msg.extend_from_slice(&((payload.rel_alt_m * 1000.0).round() as i32).to_le_bytes());
|
||||
msg.extend_from_slice(&vx.to_le_bytes());
|
||||
msg.extend_from_slice(&vy.to_le_bytes());
|
||||
msg.extend_from_slice(&0i16.to_le_bytes());
|
||||
msg.extend_from_slice(&heading.to_le_bytes());
|
||||
build_v1_packet(payload.system_id, payload.component_id, 33, &msg, 104)
|
||||
}
|
||||
|
||||
pub(crate) fn attitude_packet(payload: &UasTelemetryPayload) -> Vec<u8> {
|
||||
let now_ms = Utc::now().timestamp_millis().max(0) as u32;
|
||||
let roll = payload.roll_deg.to_radians();
|
||||
let pitch = payload.pitch_deg.to_radians();
|
||||
let yaw = payload.yaw_deg.to_radians();
|
||||
|
||||
let mut msg = Vec::with_capacity(28);
|
||||
msg.extend_from_slice(&now_ms.to_le_bytes());
|
||||
msg.extend_from_slice(&roll.to_le_bytes());
|
||||
msg.extend_from_slice(&pitch.to_le_bytes());
|
||||
msg.extend_from_slice(&yaw.to_le_bytes());
|
||||
msg.extend_from_slice(&0f32.to_le_bytes());
|
||||
msg.extend_from_slice(&0f32.to_le_bytes());
|
||||
msg.extend_from_slice(&0f32.to_le_bytes());
|
||||
build_v1_packet(payload.system_id, payload.component_id, 30, &msg, 39)
|
||||
}
|
||||
|
||||
pub(crate) fn vfr_hud_packet(payload: &UasTelemetryPayload) -> Vec<u8> {
|
||||
let heading = normalize_heading_deg(payload.heading_deg).round() as i16;
|
||||
let throttle = if payload.flying { 50u16 } else { 0u16 };
|
||||
|
||||
let mut msg = Vec::with_capacity(20);
|
||||
msg.extend_from_slice(&payload.groundspeed_mps.to_le_bytes());
|
||||
msg.extend_from_slice(&payload.groundspeed_mps.to_le_bytes());
|
||||
msg.extend_from_slice(&payload.alt_msl_m.to_le_bytes());
|
||||
msg.extend_from_slice(&0f32.to_le_bytes());
|
||||
msg.extend_from_slice(&heading.to_le_bytes());
|
||||
msg.extend_from_slice(&throttle.to_le_bytes());
|
||||
build_v1_packet(payload.system_id, payload.component_id, 74, &msg, 20)
|
||||
}
|
||||
|
||||
pub(crate) fn system_status_packet(system_id: u8, battery_remaining_pct: i8) -> Vec<u8> {
|
||||
let fields = [
|
||||
FieldSpec {
|
||||
ty: "uint32_t",
|
||||
name: "onboard_control_sensors_present",
|
||||
array_len: 0,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "uint32_t",
|
||||
name: "onboard_control_sensors_enabled",
|
||||
array_len: 0,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "uint32_t",
|
||||
name: "onboard_control_sensors_health",
|
||||
array_len: 0,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "uint16_t",
|
||||
name: "load",
|
||||
array_len: 0,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "uint16_t",
|
||||
name: "voltage_battery",
|
||||
array_len: 0,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "int16_t",
|
||||
name: "current_battery",
|
||||
array_len: 0,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "uint16_t",
|
||||
name: "drop_rate_comm",
|
||||
array_len: 0,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "uint16_t",
|
||||
name: "errors_comm",
|
||||
array_len: 0,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "uint16_t",
|
||||
name: "errors_count1",
|
||||
array_len: 0,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "uint16_t",
|
||||
name: "errors_count2",
|
||||
array_len: 0,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "uint16_t",
|
||||
name: "errors_count3",
|
||||
array_len: 0,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "uint16_t",
|
||||
name: "errors_count4",
|
||||
array_len: 0,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "int8_t",
|
||||
name: "battery_remaining",
|
||||
array_len: 0,
|
||||
},
|
||||
];
|
||||
let crc_extra = calculate_crc_extra("SYS_STATUS", &fields);
|
||||
|
||||
let sensors = 0x2000u32 | 0x4000u32 | 0x8000u32 | 0x20u32;
|
||||
let mut msg = Vec::with_capacity(31);
|
||||
msg.extend_from_slice(&sensors.to_le_bytes());
|
||||
msg.extend_from_slice(&sensors.to_le_bytes());
|
||||
msg.extend_from_slice(&sensors.to_le_bytes());
|
||||
msg.extend_from_slice(&500u16.to_le_bytes());
|
||||
msg.extend_from_slice(&12000u16.to_le_bytes());
|
||||
msg.extend_from_slice(&(-1i16).to_le_bytes());
|
||||
msg.extend_from_slice(&0u16.to_le_bytes());
|
||||
msg.extend_from_slice(&0u16.to_le_bytes());
|
||||
msg.extend_from_slice(&0u16.to_le_bytes());
|
||||
msg.extend_from_slice(&0u16.to_le_bytes());
|
||||
msg.extend_from_slice(&0u16.to_le_bytes());
|
||||
msg.extend_from_slice(&0u16.to_le_bytes());
|
||||
msg.push(battery_remaining_pct.clamp(0, 100) as u8);
|
||||
|
||||
build_v2_packet(system_id, AUTOPILOT_COMPONENT_ID, 1, &msg, crc_extra)
|
||||
}
|
||||
|
||||
pub(crate) fn extended_sys_state_packet(system_id: u8, landed: bool) -> Vec<u8> {
|
||||
let fields = [
|
||||
FieldSpec {
|
||||
ty: "uint8_t",
|
||||
name: "vtol_state",
|
||||
array_len: 0,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "uint8_t",
|
||||
name: "landed_state",
|
||||
array_len: 0,
|
||||
},
|
||||
];
|
||||
let crc_extra = calculate_crc_extra("EXTENDED_SYS_STATE", &fields);
|
||||
|
||||
let mut msg = Vec::with_capacity(2);
|
||||
msg.push(MAV_LANDED_STATE_UNDEFINED);
|
||||
msg.push(if landed {
|
||||
MAV_LANDED_STATE_ON_GROUND
|
||||
} else {
|
||||
MAV_LANDED_STATE_IN_AIR
|
||||
});
|
||||
|
||||
build_v2_packet(system_id, AUTOPILOT_COMPONENT_ID, 245, &msg, crc_extra)
|
||||
}
|
||||
|
||||
pub(crate) fn autopilot_version_packet(system_id: u8, entity_uuid: &str) -> Vec<u8> {
|
||||
let fields = [
|
||||
FieldSpec {
|
||||
ty: "uint64_t",
|
||||
name: "capabilities",
|
||||
array_len: 0,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "uint32_t",
|
||||
name: "flight_sw_version",
|
||||
array_len: 0,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "uint32_t",
|
||||
name: "middleware_sw_version",
|
||||
array_len: 0,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "uint32_t",
|
||||
name: "os_sw_version",
|
||||
array_len: 0,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "uint32_t",
|
||||
name: "board_version",
|
||||
array_len: 0,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "uint8_t",
|
||||
name: "flight_custom_version",
|
||||
array_len: 8,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "uint8_t",
|
||||
name: "middleware_custom_version",
|
||||
array_len: 8,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "uint8_t",
|
||||
name: "os_custom_version",
|
||||
array_len: 8,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "uint16_t",
|
||||
name: "vendor_id",
|
||||
array_len: 0,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "uint16_t",
|
||||
name: "product_id",
|
||||
array_len: 0,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "uint64_t",
|
||||
name: "uid",
|
||||
array_len: 0,
|
||||
},
|
||||
];
|
||||
let crc_extra = calculate_crc_extra("AUTOPILOT_VERSION", &fields);
|
||||
|
||||
let capabilities = MAV_PROTOCOL_CAPABILITY_MISSION_INT
|
||||
| MAV_PROTOCOL_CAPABILITY_COMMAND_INT
|
||||
| MAV_PROTOCOL_CAPABILITY_FTP
|
||||
| MAV_PROTOCOL_CAPABILITY_SET_POSITION_TARGET_GLOBAL_INT
|
||||
| MAV_PROTOCOL_CAPABILITY_MAVLINK2
|
||||
| MAV_PROTOCOL_CAPABILITY_COMPONENT_IMPLEMENTS_GIMBAL_MANAGER;
|
||||
let uid = uid64_from_uuid(entity_uuid);
|
||||
let uid2 = uid2_from_uuid(entity_uuid);
|
||||
|
||||
let mut msg = Vec::with_capacity(78);
|
||||
msg.extend_from_slice(&capabilities.to_le_bytes());
|
||||
msg.extend_from_slice(&0x010100FFu32.to_le_bytes());
|
||||
msg.extend_from_slice(&0u32.to_le_bytes());
|
||||
msg.extend_from_slice(&0u32.to_le_bytes());
|
||||
msg.extend_from_slice(&0u32.to_le_bytes());
|
||||
msg.extend_from_slice(&[0u8; 8]);
|
||||
msg.extend_from_slice(&[0u8; 8]);
|
||||
msg.extend_from_slice(&[0u8; 8]);
|
||||
msg.extend_from_slice(&0x5441u16.to_le_bytes());
|
||||
msg.extend_from_slice(&0x5541u16.to_le_bytes());
|
||||
msg.extend_from_slice(&uid.to_le_bytes());
|
||||
msg.extend_from_slice(&uid2);
|
||||
|
||||
build_v2_packet(system_id, AUTOPILOT_COMPONENT_ID, 148, &msg, crc_extra)
|
||||
}
|
||||
|
||||
pub(crate) fn home_position_packet(
|
||||
system_id: u8,
|
||||
lat_deg: f64,
|
||||
lon_deg: f64,
|
||||
alt_msl_m: f32,
|
||||
heading_deg: f32,
|
||||
) -> Vec<u8> {
|
||||
let yaw = normalize_heading_deg(heading_deg).to_radians();
|
||||
let q = [(yaw * 0.5).cos(), 0.0f32, 0.0f32, (yaw * 0.5).sin()];
|
||||
|
||||
let mut msg = Vec::with_capacity(52);
|
||||
msg.extend_from_slice(&((lat_deg * 1e7).round() as i32).to_le_bytes());
|
||||
msg.extend_from_slice(&((lon_deg * 1e7).round() as i32).to_le_bytes());
|
||||
msg.extend_from_slice(&((alt_msl_m * 1000.0).round() as i32).to_le_bytes());
|
||||
msg.extend_from_slice(&0f32.to_le_bytes());
|
||||
msg.extend_from_slice(&0f32.to_le_bytes());
|
||||
msg.extend_from_slice(&0f32.to_le_bytes());
|
||||
for value in q {
|
||||
msg.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
msg.extend_from_slice(&0f32.to_le_bytes());
|
||||
msg.extend_from_slice(&0f32.to_le_bytes());
|
||||
msg.extend_from_slice(&0f32.to_le_bytes());
|
||||
|
||||
build_v2_packet(system_id, AUTOPILOT_COMPONENT_ID, 242, &msg, 85)
|
||||
}
|
||||
|
||||
pub(crate) fn component_heartbeat_packet(
|
||||
system_id: u8,
|
||||
component_id: u8,
|
||||
component_type: u8,
|
||||
) -> Vec<u8> {
|
||||
let mut msg = Vec::with_capacity(9);
|
||||
msg.extend_from_slice(&0u32.to_le_bytes());
|
||||
msg.push(component_type);
|
||||
msg.push(MAV_AUTOPILOT_INVALID);
|
||||
msg.push(0);
|
||||
msg.push(MAV_STATE_ACTIVE);
|
||||
msg.push(3);
|
||||
build_v2_packet(system_id, component_id, 0, &msg, 50)
|
||||
}
|
||||
|
||||
pub(crate) fn camera_information_packet_for_component(
|
||||
system_id: u8,
|
||||
component_id: u8,
|
||||
callsign: &str,
|
||||
gimbal_device_id: u8,
|
||||
) -> Vec<u8> {
|
||||
let vendor = fixed_string::<32>("ArmaTAK");
|
||||
let model = fixed_string::<32>(callsign);
|
||||
let cam_definition_uri = fixed_string::<140>("");
|
||||
let flags = CAMERA_CAP_FLAGS_CAPTURE_VIDEO
|
||||
| CAMERA_CAP_FLAGS_CAPTURE_IMAGE
|
||||
| CAMERA_CAP_FLAGS_HAS_MODES
|
||||
| CAMERA_CAP_FLAGS_HAS_BASIC_ZOOM
|
||||
| CAMERA_CAP_FLAGS_HAS_VIDEO_STREAM;
|
||||
|
||||
let mut msg = Vec::with_capacity(235);
|
||||
msg.extend_from_slice(&(Utc::now().timestamp_millis().max(0) as u32).to_le_bytes());
|
||||
msg.extend_from_slice(&0x010100FFu32.to_le_bytes());
|
||||
msg.extend_from_slice(&2.8f32.to_le_bytes());
|
||||
msg.extend_from_slice(&6.4f32.to_le_bytes());
|
||||
msg.extend_from_slice(&4.8f32.to_le_bytes());
|
||||
msg.extend_from_slice(&flags.to_le_bytes());
|
||||
msg.extend_from_slice(&1280u16.to_le_bytes());
|
||||
msg.extend_from_slice(&720u16.to_le_bytes());
|
||||
msg.extend_from_slice(&0u16.to_le_bytes());
|
||||
msg.extend_from_slice(&vendor);
|
||||
msg.extend_from_slice(&model);
|
||||
msg.push(0);
|
||||
msg.extend_from_slice(&cam_definition_uri);
|
||||
msg.push(gimbal_device_id);
|
||||
msg.push(0);
|
||||
|
||||
build_v2_packet(system_id, component_id, 259, &msg, 92)
|
||||
}
|
||||
|
||||
pub(crate) fn video_stream_information_packet_for_component(
|
||||
system_id: u8,
|
||||
component_id: u8,
|
||||
callsign: &str,
|
||||
video_uri: &str,
|
||||
hfov_deg: f32,
|
||||
stream_id: u8,
|
||||
stream_count: u8,
|
||||
thermal: bool,
|
||||
) -> Vec<u8> {
|
||||
let stream_type = if video_uri.starts_with("rtsp://") {
|
||||
VIDEO_STREAM_TYPE_RTSP
|
||||
} else if video_uri.starts_with("rtp://") {
|
||||
VIDEO_STREAM_TYPE_RTPUDP
|
||||
} else if video_uri.starts_with("tcp://") {
|
||||
VIDEO_STREAM_TYPE_TCP_MPEG
|
||||
} else if video_uri.starts_with("mpegts://") || video_uri.starts_with("udp://") {
|
||||
VIDEO_STREAM_TYPE_MPEG_TS
|
||||
} else {
|
||||
VIDEO_STREAM_TYPE_RTSP
|
||||
};
|
||||
|
||||
let name = fixed_string::<32>(callsign);
|
||||
let uri = fixed_string::<160>(video_uri);
|
||||
let flags = VIDEO_STREAM_STATUS_FLAGS_RUNNING | if thermal { 2 } else { 0 };
|
||||
|
||||
let mut msg = Vec::with_capacity(208);
|
||||
msg.extend_from_slice(&30f32.to_le_bytes());
|
||||
msg.extend_from_slice(&4_000_000u32.to_le_bytes());
|
||||
msg.extend_from_slice(&flags.to_le_bytes());
|
||||
msg.extend_from_slice(&1280u16.to_le_bytes());
|
||||
msg.extend_from_slice(&720u16.to_le_bytes());
|
||||
msg.extend_from_slice(&0u16.to_le_bytes());
|
||||
msg.extend_from_slice(&(hfov_deg.clamp(1.0, 360.0).round() as u16).to_le_bytes());
|
||||
msg.push(stream_id);
|
||||
msg.push(stream_count);
|
||||
msg.push(stream_type);
|
||||
msg.extend_from_slice(&name);
|
||||
msg.extend_from_slice(&uri);
|
||||
msg.push(VIDEO_STREAM_ENCODING_H264);
|
||||
msg.push(0);
|
||||
|
||||
build_v2_packet(system_id, component_id, 269, &msg, 109)
|
||||
}
|
||||
|
||||
pub(crate) fn video_stream_status_packet_for_component(
|
||||
system_id: u8,
|
||||
component_id: u8,
|
||||
hfov_deg: f32,
|
||||
stream_id: u8,
|
||||
thermal: bool,
|
||||
) -> Vec<u8> {
|
||||
let flags = VIDEO_STREAM_STATUS_FLAGS_RUNNING | if thermal { 2 } else { 0 };
|
||||
let mut msg = Vec::with_capacity(19);
|
||||
msg.extend_from_slice(&30f32.to_le_bytes());
|
||||
msg.extend_from_slice(&4_000_000u32.to_le_bytes());
|
||||
msg.extend_from_slice(&flags.to_le_bytes());
|
||||
msg.extend_from_slice(&1280u16.to_le_bytes());
|
||||
msg.extend_from_slice(&720u16.to_le_bytes());
|
||||
msg.extend_from_slice(&0u16.to_le_bytes());
|
||||
msg.extend_from_slice(&(hfov_deg.clamp(1.0, 360.0).round() as u16).to_le_bytes());
|
||||
msg.push(stream_id);
|
||||
msg.push(0);
|
||||
|
||||
build_v2_packet(system_id, component_id, 270, &msg, 59)
|
||||
}
|
||||
|
||||
pub(crate) fn mount_orientation_packet_for_component(
|
||||
system_id: u8,
|
||||
component_id: u8,
|
||||
pitch_deg: f32,
|
||||
yaw_deg: f32,
|
||||
) -> Vec<u8> {
|
||||
let mut msg = Vec::with_capacity(16);
|
||||
msg.extend_from_slice(&(Utc::now().timestamp_millis().max(0) as u32).to_le_bytes());
|
||||
msg.extend_from_slice(&pitch_deg.to_le_bytes());
|
||||
msg.extend_from_slice(&0f32.to_le_bytes());
|
||||
msg.extend_from_slice(&normalize_heading_deg(yaw_deg).to_le_bytes());
|
||||
|
||||
build_v2_packet(system_id, component_id, 265, &msg, 26)
|
||||
}
|
||||
|
||||
pub(crate) fn camera_fov_status_packet_for_component(
|
||||
system_id: u8,
|
||||
component_id: u8,
|
||||
lat_camera_deg: f64,
|
||||
lon_camera_deg: f64,
|
||||
alt_camera_msl_m: f32,
|
||||
lat_image_deg: f64,
|
||||
lon_image_deg: f64,
|
||||
alt_image_msl_m: f32,
|
||||
roll_deg: f32,
|
||||
pitch_deg: f32,
|
||||
yaw_deg: f32,
|
||||
hfov_deg: f32,
|
||||
vfov_deg: f32,
|
||||
) -> Vec<u8> {
|
||||
let q = attitude_quaternion(roll_deg, pitch_deg, yaw_deg);
|
||||
let mut msg = Vec::with_capacity(53);
|
||||
msg.extend_from_slice(&(Utc::now().timestamp_millis().max(0) as u32).to_le_bytes());
|
||||
msg.extend_from_slice(&((lat_camera_deg * 1e7).round() as i32).to_le_bytes());
|
||||
msg.extend_from_slice(&((lon_camera_deg * 1e7).round() as i32).to_le_bytes());
|
||||
msg.extend_from_slice(&((alt_camera_msl_m * 1000.0).round() as i32).to_le_bytes());
|
||||
msg.extend_from_slice(&((lat_image_deg * 1e7).round() as i32).to_le_bytes());
|
||||
msg.extend_from_slice(&((lon_image_deg * 1e7).round() as i32).to_le_bytes());
|
||||
msg.extend_from_slice(&((alt_image_msl_m * 1000.0).round() as i32).to_le_bytes());
|
||||
for value in q {
|
||||
msg.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
msg.extend_from_slice(&hfov_deg.to_le_bytes());
|
||||
msg.extend_from_slice(&vfov_deg.to_le_bytes());
|
||||
msg.push(0);
|
||||
|
||||
build_v2_packet(system_id, component_id, 271, &msg, 22)
|
||||
}
|
||||
|
||||
pub(crate) fn mount_status_packet(
|
||||
system_id: u8,
|
||||
pitch_deg: f32,
|
||||
roll_deg: f32,
|
||||
relative_yaw_deg: f32,
|
||||
) -> Vec<u8> {
|
||||
let mut msg = Vec::with_capacity(15);
|
||||
msg.extend_from_slice(&((pitch_deg * 100.0).round() as i32).to_le_bytes());
|
||||
msg.extend_from_slice(&((roll_deg * 100.0).round() as i32).to_le_bytes());
|
||||
msg.extend_from_slice(&((relative_yaw_deg * 100.0).round() as i32).to_le_bytes());
|
||||
msg.push(system_id);
|
||||
msg.push(GIMBAL_COMPONENT_ID);
|
||||
msg.push(2);
|
||||
|
||||
build_v2_packet(system_id, AUTOPILOT_COMPONENT_ID, 158, &msg, 134)
|
||||
}
|
||||
|
||||
pub(crate) fn gimbal_manager_information_packet(system_id: u8) -> Vec<u8> {
|
||||
let fields = [
|
||||
FieldSpec {
|
||||
ty: "uint32_t",
|
||||
name: "time_boot_ms",
|
||||
array_len: 0,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "uint32_t",
|
||||
name: "cap_flags",
|
||||
array_len: 0,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "float",
|
||||
name: "roll_min",
|
||||
array_len: 0,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "float",
|
||||
name: "roll_max",
|
||||
array_len: 0,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "float",
|
||||
name: "pitch_min",
|
||||
array_len: 0,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "float",
|
||||
name: "pitch_max",
|
||||
array_len: 0,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "float",
|
||||
name: "yaw_min",
|
||||
array_len: 0,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "float",
|
||||
name: "yaw_max",
|
||||
array_len: 0,
|
||||
},
|
||||
FieldSpec {
|
||||
ty: "uint8_t",
|
||||
name: "gimbal_device_id",
|
||||
array_len: 0,
|
||||
},
|
||||
];
|
||||
let crc_extra = calculate_crc_extra("GIMBAL_MANAGER_INFORMATION", &fields);
|
||||
|
||||
let mut msg = Vec::with_capacity(33);
|
||||
msg.extend_from_slice(&(Utc::now().timestamp_millis().max(0) as u32).to_le_bytes());
|
||||
msg.extend_from_slice(&GIMBAL_MANAGER_CAP_FLAGS_BASIC_PITCH_YAW.to_le_bytes());
|
||||
msg.extend_from_slice(&0f32.to_le_bytes());
|
||||
msg.extend_from_slice(&0f32.to_le_bytes());
|
||||
msg.extend_from_slice(&(-90f32).to_radians().to_le_bytes());
|
||||
msg.extend_from_slice(&30f32.to_radians().to_le_bytes());
|
||||
msg.extend_from_slice(&(-180f32).to_radians().to_le_bytes());
|
||||
msg.extend_from_slice(&180f32.to_radians().to_le_bytes());
|
||||
msg.push(GIMBAL_COMPONENT_ID);
|
||||
|
||||
build_v2_packet(system_id, GIMBAL_COMPONENT_ID, 280, &msg, crc_extra)
|
||||
}
|
||||
|
||||
fn attitude_quaternion(roll_deg: f32, pitch_deg: f32, yaw_deg: f32) -> [f32; 4] {
|
||||
let (roll, pitch, yaw) = (
|
||||
roll_deg.to_radians(),
|
||||
pitch_deg.to_radians(),
|
||||
normalize_heading_deg(yaw_deg).to_radians(),
|
||||
);
|
||||
let (sr, cr) = (roll * 0.5).sin_cos();
|
||||
let (sp, cp) = (pitch * 0.5).sin_cos();
|
||||
let (sy, cy) = (yaw * 0.5).sin_cos();
|
||||
|
||||
[
|
||||
cr * cp * cy + sr * sp * sy,
|
||||
sr * cp * cy - cr * sp * sy,
|
||||
cr * sp * cy + sr * cp * sy,
|
||||
cr * cp * sy - sr * sp * cy,
|
||||
]
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
use arma_rs::{FromArma, FromArmaError};
|
||||
|
||||
pub struct UasTelemetryPayload {
|
||||
pub address: String,
|
||||
pub system_id: u8,
|
||||
pub component_id: u8,
|
||||
pub vehicle_type: u8,
|
||||
pub lat_deg: f64,
|
||||
pub lon_deg: f64,
|
||||
pub alt_msl_m: f32,
|
||||
pub rel_alt_m: f32,
|
||||
pub heading_deg: f32,
|
||||
pub groundspeed_mps: f32,
|
||||
pub roll_deg: f32,
|
||||
pub pitch_deg: f32,
|
||||
pub yaw_deg: f32,
|
||||
pub flying: bool,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct UasSystemPayload {
|
||||
pub address: String,
|
||||
pub entity_uuid: String,
|
||||
pub callsign: String,
|
||||
pub vehicle_type: u8,
|
||||
pub lat_deg: f64,
|
||||
pub lon_deg: f64,
|
||||
pub alt_msl_m: f32,
|
||||
pub rel_alt_m: f32,
|
||||
pub heading_deg: f32,
|
||||
pub groundspeed_mps: f32,
|
||||
pub roll_deg: f32,
|
||||
pub pitch_deg: f32,
|
||||
pub yaw_deg: f32,
|
||||
pub flying: bool,
|
||||
pub landed: bool,
|
||||
pub gimbal_roll_deg: f32,
|
||||
pub gimbal_pitch_deg: f32,
|
||||
pub gimbal_yaw_deg: f32,
|
||||
pub video_uri: String,
|
||||
pub hfov_deg: f32,
|
||||
pub vfov_deg: f32,
|
||||
pub image_lat_deg: f64,
|
||||
pub image_lon_deg: f64,
|
||||
pub image_alt_msl_m: f32,
|
||||
pub has_turret_camera: bool,
|
||||
pub battery_remaining_pct: i8,
|
||||
}
|
||||
|
||||
impl FromArma for UasTelemetryPayload {
|
||||
fn from_arma(data: String) -> Result<Self, FromArmaError> {
|
||||
let (
|
||||
address,
|
||||
system_id,
|
||||
component_id,
|
||||
vehicle_type,
|
||||
lat_deg,
|
||||
lon_deg,
|
||||
alt_msl_m,
|
||||
rel_alt_m,
|
||||
heading_deg,
|
||||
groundspeed_mps,
|
||||
roll_deg,
|
||||
pitch_deg,
|
||||
yaw_deg,
|
||||
flying,
|
||||
) = <(
|
||||
String,
|
||||
i32,
|
||||
i32,
|
||||
i32,
|
||||
f64,
|
||||
f64,
|
||||
f32,
|
||||
f32,
|
||||
f32,
|
||||
f32,
|
||||
f32,
|
||||
f32,
|
||||
f32,
|
||||
i32,
|
||||
)>::from_arma(data)?;
|
||||
|
||||
Ok(Self {
|
||||
address,
|
||||
system_id: system_id.clamp(1, 255) as u8,
|
||||
component_id: component_id.clamp(1, 255) as u8,
|
||||
vehicle_type: vehicle_type.clamp(0, 255) as u8,
|
||||
lat_deg,
|
||||
lon_deg,
|
||||
alt_msl_m,
|
||||
rel_alt_m,
|
||||
heading_deg,
|
||||
groundspeed_mps,
|
||||
roll_deg,
|
||||
pitch_deg,
|
||||
yaw_deg,
|
||||
flying: flying != 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FromArma for UasSystemPayload {
|
||||
fn from_arma(data: String) -> Result<Self, FromArmaError> {
|
||||
let (
|
||||
address,
|
||||
entity_uuid,
|
||||
callsign,
|
||||
vehicle_type,
|
||||
lat_deg,
|
||||
lon_deg,
|
||||
alt_msl_m,
|
||||
rel_alt_m,
|
||||
heading_deg,
|
||||
groundspeed_mps,
|
||||
roll_deg,
|
||||
pitch_deg,
|
||||
yaw_deg,
|
||||
flying,
|
||||
landed,
|
||||
gimbal_roll_deg,
|
||||
gimbal_pitch_deg,
|
||||
gimbal_yaw_deg,
|
||||
video_uri,
|
||||
hfov_deg,
|
||||
vfov_deg,
|
||||
image_lat_deg,
|
||||
image_lon_deg,
|
||||
image_alt_msl_m,
|
||||
has_turret_camera,
|
||||
battery_remaining_pct,
|
||||
) = <(
|
||||
String,
|
||||
String,
|
||||
String,
|
||||
i32,
|
||||
f64,
|
||||
f64,
|
||||
f32,
|
||||
f32,
|
||||
f32,
|
||||
f32,
|
||||
f32,
|
||||
f32,
|
||||
f32,
|
||||
i32,
|
||||
i32,
|
||||
f32,
|
||||
f32,
|
||||
f32,
|
||||
String,
|
||||
f32,
|
||||
f32,
|
||||
f64,
|
||||
f64,
|
||||
f32,
|
||||
i32,
|
||||
i32,
|
||||
)>::from_arma(data)?;
|
||||
|
||||
Ok(Self {
|
||||
address,
|
||||
entity_uuid,
|
||||
callsign,
|
||||
vehicle_type: vehicle_type.clamp(0, 255) as u8,
|
||||
lat_deg,
|
||||
lon_deg,
|
||||
alt_msl_m,
|
||||
rel_alt_m,
|
||||
heading_deg,
|
||||
groundspeed_mps,
|
||||
roll_deg,
|
||||
pitch_deg,
|
||||
yaw_deg,
|
||||
flying: flying != 0,
|
||||
landed: landed != 0,
|
||||
gimbal_roll_deg,
|
||||
gimbal_pitch_deg,
|
||||
gimbal_yaw_deg,
|
||||
video_uri,
|
||||
hfov_deg,
|
||||
vfov_deg,
|
||||
image_lat_deg,
|
||||
image_lon_deg,
|
||||
image_alt_msl_m,
|
||||
has_turret_camera: has_turret_camera != 0,
|
||||
battery_remaining_pct: battery_remaining_pct.clamp(0, 100) as i8,
|
||||
})
|
||||
}
|
||||
}
|
||||
403
src/uas/send.rs
403
src/uas/send.rs
@@ -1,403 +0,0 @@
|
||||
use arma_rs::Context;
|
||||
use log::info;
|
||||
use std::net::UdpSocket;
|
||||
|
||||
use super::constants::{
|
||||
AUTOPILOT_COMPONENT_ID, CAMERA_COMPONENT_ID, GIMBAL_COMPONENT_ID, MAV_TYPE_CAMERA,
|
||||
MAV_TYPE_GIMBAL, TURRET_CAMERA_COMPONENT_ID,
|
||||
};
|
||||
use super::endpoint::socket_for_send;
|
||||
use super::identity::{
|
||||
map_vehicle_type, should_send_video_stream_information, stable_mavlink_identity,
|
||||
stable_system_id,
|
||||
};
|
||||
use super::packets::{
|
||||
attitude_packet, autopilot_version_packet, camera_fov_status_packet_for_component,
|
||||
camera_information_packet_for_component, component_heartbeat_packet, extended_sys_state_packet,
|
||||
gimbal_manager_information_packet, global_position_int_packet, gps_raw_int_packet,
|
||||
heartbeat_packet, home_position_packet, mount_orientation_packet_for_component,
|
||||
mount_status_packet, system_status_packet, vfr_hud_packet,
|
||||
video_stream_information_packet_for_component, video_stream_status_packet_for_component,
|
||||
};
|
||||
use super::payload::{UasSystemPayload, UasTelemetryPayload};
|
||||
use super::state::{latest_system, record_system};
|
||||
|
||||
fn sending_socket(ctx: &Context, error_prefix: &str) -> Result<UdpSocket, &'static str> {
|
||||
if let Some(socket) = socket_for_send() {
|
||||
return Ok(socket);
|
||||
}
|
||||
|
||||
match UdpSocket::bind("0.0.0.0:0") {
|
||||
Ok(socket) => Ok(socket),
|
||||
Err(error) => {
|
||||
let _ = ctx.callback_data(
|
||||
"MAVLINK MOCK ERROR",
|
||||
"Failed to bind UDP socket",
|
||||
error.to_string(),
|
||||
);
|
||||
info!("{} failed to bind UDP socket: {}", error_prefix, error);
|
||||
Err("Failed to bind MAVLink mock socket")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_uas_telemetry(ctx: Context, payload: UasTelemetryPayload) -> &'static str {
|
||||
info!(
|
||||
"MAVLink mock send requested to {} sysid={} compid={} lat={} lon={} alt_msl={} rel_alt={} heading={} speed={} flying={}",
|
||||
payload.address,
|
||||
payload.system_id,
|
||||
payload.component_id,
|
||||
payload.lat_deg,
|
||||
payload.lon_deg,
|
||||
payload.alt_msl_m,
|
||||
payload.rel_alt_m,
|
||||
payload.heading_deg,
|
||||
payload.groundspeed_mps,
|
||||
payload.flying
|
||||
);
|
||||
|
||||
let socket = match sending_socket(&ctx, "MAVLink mock") {
|
||||
Ok(socket) => socket,
|
||||
Err(message) => return message,
|
||||
};
|
||||
|
||||
let packets = [
|
||||
heartbeat_packet(&payload),
|
||||
gps_raw_int_packet(&payload),
|
||||
global_position_int_packet(&payload),
|
||||
attitude_packet(&payload),
|
||||
vfr_hud_packet(&payload),
|
||||
];
|
||||
|
||||
for (index, packet) in packets.iter().enumerate() {
|
||||
if let Err(error) = socket.send_to(packet, &payload.address) {
|
||||
let _ = ctx.callback_data(
|
||||
"MAVLINK MOCK ERROR",
|
||||
"Failed to send MAVLink packet",
|
||||
error.to_string(),
|
||||
);
|
||||
info!(
|
||||
"MAVLink mock failed sending packet {} to {}: {}",
|
||||
index, payload.address, error
|
||||
);
|
||||
return "Failed to send MAVLink mock telemetry";
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
"MAVLink mock sent {} packets to {}",
|
||||
packets.len(),
|
||||
payload.address
|
||||
);
|
||||
"Sent MAVLink mock telemetry"
|
||||
}
|
||||
|
||||
pub fn send_uas_system(ctx: Context, payload: UasSystemPayload) -> &'static str {
|
||||
let mavlink_identity = stable_mavlink_identity(&payload.callsign, &payload.entity_uuid);
|
||||
let system_id = stable_system_id(&mavlink_identity);
|
||||
let vehicle_type = map_vehicle_type(payload.vehicle_type);
|
||||
record_system(system_id, &mavlink_identity, &payload);
|
||||
let active_camera_component = latest_system(system_id)
|
||||
.map(|system| system.active_camera_component)
|
||||
.unwrap_or(CAMERA_COMPONENT_ID);
|
||||
let (home_lat_deg, home_lon_deg, home_alt_msl_m) = latest_system(system_id)
|
||||
.map(|system| {
|
||||
(
|
||||
system.home_lat_deg,
|
||||
system.home_lon_deg,
|
||||
system.home_alt_msl_m,
|
||||
)
|
||||
})
|
||||
.unwrap_or((
|
||||
payload.lat_deg,
|
||||
payload.lon_deg,
|
||||
payload.alt_msl_m - payload.rel_alt_m,
|
||||
));
|
||||
|
||||
info!(
|
||||
"MAVLink system send requested to {} entity_uuid={} mavlink_identity={} sysid={} callsign={} lat={} lon={} alt_msl={} rel_alt={} heading={} gimbal_pitch={} gimbal_yaw={} video_uri={}",
|
||||
payload.address,
|
||||
payload.entity_uuid,
|
||||
mavlink_identity,
|
||||
system_id,
|
||||
payload.callsign,
|
||||
payload.lat_deg,
|
||||
payload.lon_deg,
|
||||
payload.alt_msl_m,
|
||||
payload.rel_alt_m,
|
||||
payload.heading_deg,
|
||||
payload.gimbal_pitch_deg,
|
||||
payload.gimbal_yaw_deg,
|
||||
payload.video_uri
|
||||
);
|
||||
|
||||
let socket = match sending_socket(&ctx, "MAVLink system") {
|
||||
Ok(socket) => socket,
|
||||
Err(message) => return message,
|
||||
};
|
||||
let (fpv_image_lat, fpv_image_lon, fpv_image_alt) = fpv_image_point(
|
||||
payload.lat_deg,
|
||||
payload.lon_deg,
|
||||
payload.alt_msl_m,
|
||||
payload.rel_alt_m,
|
||||
payload.pitch_deg,
|
||||
payload.yaw_deg,
|
||||
);
|
||||
let active_is_turret =
|
||||
payload.has_turret_camera && active_camera_component == TURRET_CAMERA_COMPONENT_ID;
|
||||
info!(
|
||||
"MAVLink active camera sysid={} active_component={} has_turret={} active_is_turret={}",
|
||||
system_id, active_camera_component, payload.has_turret_camera, active_is_turret
|
||||
);
|
||||
let (
|
||||
primary_pitch,
|
||||
primary_roll,
|
||||
primary_yaw,
|
||||
primary_image_lat,
|
||||
primary_image_lon,
|
||||
primary_image_alt,
|
||||
) = if active_is_turret {
|
||||
(
|
||||
payload.gimbal_pitch_deg,
|
||||
payload.gimbal_roll_deg,
|
||||
payload.gimbal_yaw_deg,
|
||||
payload.image_lat_deg,
|
||||
payload.image_lon_deg,
|
||||
payload.image_alt_msl_m,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
payload.pitch_deg,
|
||||
payload.roll_deg,
|
||||
payload.yaw_deg,
|
||||
fpv_image_lat,
|
||||
fpv_image_lon,
|
||||
fpv_image_alt,
|
||||
)
|
||||
};
|
||||
|
||||
let autopilot_payload = UasTelemetryPayload {
|
||||
address: payload.address.clone(),
|
||||
system_id,
|
||||
component_id: AUTOPILOT_COMPONENT_ID,
|
||||
vehicle_type,
|
||||
lat_deg: payload.lat_deg,
|
||||
lon_deg: payload.lon_deg,
|
||||
alt_msl_m: payload.alt_msl_m,
|
||||
rel_alt_m: payload.rel_alt_m,
|
||||
heading_deg: payload.heading_deg,
|
||||
groundspeed_mps: payload.groundspeed_mps,
|
||||
roll_deg: payload.roll_deg,
|
||||
pitch_deg: payload.pitch_deg,
|
||||
yaw_deg: payload.yaw_deg,
|
||||
flying: payload.flying,
|
||||
};
|
||||
|
||||
let mut packets = vec![
|
||||
heartbeat_packet(&autopilot_payload),
|
||||
gps_raw_int_packet(&autopilot_payload),
|
||||
global_position_int_packet(&autopilot_payload),
|
||||
attitude_packet(&autopilot_payload),
|
||||
vfr_hud_packet(&autopilot_payload),
|
||||
system_status_packet(system_id, payload.battery_remaining_pct),
|
||||
extended_sys_state_packet(system_id, payload.landed),
|
||||
autopilot_version_packet(system_id, &mavlink_identity),
|
||||
home_position_packet(
|
||||
system_id,
|
||||
home_lat_deg,
|
||||
home_lon_deg,
|
||||
home_alt_msl_m,
|
||||
payload.heading_deg,
|
||||
),
|
||||
component_heartbeat_packet(system_id, CAMERA_COMPONENT_ID, MAV_TYPE_CAMERA),
|
||||
component_heartbeat_packet(system_id, GIMBAL_COMPONENT_ID, MAV_TYPE_GIMBAL),
|
||||
camera_information_packet_for_component(
|
||||
system_id,
|
||||
CAMERA_COMPONENT_ID,
|
||||
&format!("{} FPV", payload.callsign),
|
||||
0,
|
||||
),
|
||||
mount_orientation_packet_for_component(
|
||||
system_id,
|
||||
CAMERA_COMPONENT_ID,
|
||||
primary_pitch,
|
||||
primary_yaw,
|
||||
),
|
||||
camera_fov_status_packet_for_component(
|
||||
system_id,
|
||||
CAMERA_COMPONENT_ID,
|
||||
payload.lat_deg,
|
||||
payload.lon_deg,
|
||||
payload.alt_msl_m,
|
||||
primary_image_lat,
|
||||
primary_image_lon,
|
||||
primary_image_alt,
|
||||
primary_roll,
|
||||
primary_pitch,
|
||||
primary_yaw,
|
||||
payload.hfov_deg,
|
||||
payload.vfov_deg,
|
||||
),
|
||||
gimbal_manager_information_packet(system_id),
|
||||
];
|
||||
|
||||
if payload.has_turret_camera {
|
||||
packets.push(component_heartbeat_packet(
|
||||
system_id,
|
||||
TURRET_CAMERA_COMPONENT_ID,
|
||||
MAV_TYPE_CAMERA,
|
||||
));
|
||||
packets.push(camera_information_packet_for_component(
|
||||
system_id,
|
||||
TURRET_CAMERA_COMPONENT_ID,
|
||||
&format!("{} Turret", payload.callsign),
|
||||
GIMBAL_COMPONENT_ID,
|
||||
));
|
||||
packets.push(mount_orientation_packet_for_component(
|
||||
system_id,
|
||||
TURRET_CAMERA_COMPONENT_ID,
|
||||
payload.gimbal_pitch_deg,
|
||||
payload.gimbal_yaw_deg,
|
||||
));
|
||||
packets.push(camera_fov_status_packet_for_component(
|
||||
system_id,
|
||||
TURRET_CAMERA_COMPONENT_ID,
|
||||
payload.lat_deg,
|
||||
payload.lon_deg,
|
||||
payload.alt_msl_m,
|
||||
payload.image_lat_deg,
|
||||
payload.image_lon_deg,
|
||||
payload.image_alt_msl_m,
|
||||
payload.gimbal_roll_deg,
|
||||
payload.gimbal_pitch_deg,
|
||||
payload.gimbal_yaw_deg,
|
||||
payload.hfov_deg,
|
||||
payload.vfov_deg,
|
||||
));
|
||||
}
|
||||
|
||||
let (active_pitch, active_roll, active_relative_yaw) = if active_is_turret {
|
||||
(
|
||||
payload.gimbal_pitch_deg,
|
||||
payload.gimbal_roll_deg,
|
||||
normalize_signed_deg(payload.gimbal_yaw_deg - payload.yaw_deg),
|
||||
)
|
||||
} else {
|
||||
(payload.pitch_deg, payload.roll_deg, 0.0)
|
||||
};
|
||||
packets.push(mount_status_packet(
|
||||
system_id,
|
||||
active_pitch,
|
||||
active_roll,
|
||||
active_relative_yaw,
|
||||
));
|
||||
|
||||
if should_send_video_stream_information(&payload.video_uri) {
|
||||
info!(
|
||||
"Sending VIDEO_STREAM_INFORMATION for sysid={} uri={}",
|
||||
system_id, payload.video_uri
|
||||
);
|
||||
packets.push(video_stream_information_packet_for_component(
|
||||
system_id,
|
||||
CAMERA_COMPONENT_ID,
|
||||
&format!("{} FPV", payload.callsign),
|
||||
&payload.video_uri,
|
||||
payload.hfov_deg,
|
||||
1,
|
||||
1,
|
||||
false,
|
||||
));
|
||||
packets.push(video_stream_status_packet_for_component(
|
||||
system_id,
|
||||
CAMERA_COMPONENT_ID,
|
||||
payload.hfov_deg,
|
||||
1,
|
||||
false,
|
||||
));
|
||||
|
||||
if payload.has_turret_camera {
|
||||
packets.push(video_stream_information_packet_for_component(
|
||||
system_id,
|
||||
TURRET_CAMERA_COMPONENT_ID,
|
||||
&format!("{} Turret", payload.callsign),
|
||||
&payload.video_uri,
|
||||
payload.hfov_deg,
|
||||
1,
|
||||
1,
|
||||
false,
|
||||
));
|
||||
packets.push(video_stream_status_packet_for_component(
|
||||
system_id,
|
||||
TURRET_CAMERA_COMPONENT_ID,
|
||||
payload.hfov_deg,
|
||||
1,
|
||||
false,
|
||||
));
|
||||
}
|
||||
} else if !payload.video_uri.trim().is_empty() {
|
||||
info!(
|
||||
"Skipping VIDEO_STREAM_INFORMATION for sysid={} because URI is not a supported stream URI: {}",
|
||||
system_id, payload.video_uri
|
||||
);
|
||||
}
|
||||
|
||||
for (index, packet) in packets.iter().enumerate() {
|
||||
if let Err(error) = socket.send_to(packet, &payload.address) {
|
||||
let _ = ctx.callback_data(
|
||||
"MAVLINK MOCK ERROR",
|
||||
"Failed to send MAVLink packet",
|
||||
error.to_string(),
|
||||
);
|
||||
info!(
|
||||
"MAVLink system failed sending packet {} to {}: {}",
|
||||
index, payload.address, error
|
||||
);
|
||||
return "Failed to send MAVLink system telemetry";
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
"MAVLink system sent {} packets to {} for sysid={} (camera comp={}, gimbal comp={})",
|
||||
packets.len(),
|
||||
payload.address,
|
||||
system_id,
|
||||
CAMERA_COMPONENT_ID,
|
||||
GIMBAL_COMPONENT_ID
|
||||
);
|
||||
"Sent MAVLink system telemetry"
|
||||
}
|
||||
|
||||
fn normalize_signed_deg(value: f32) -> f32 {
|
||||
let normalized = ((value % 360.0) + 360.0) % 360.0;
|
||||
if normalized > 180.0 {
|
||||
normalized - 360.0
|
||||
} else {
|
||||
normalized
|
||||
}
|
||||
}
|
||||
|
||||
fn fpv_image_point(
|
||||
lat_deg: f64,
|
||||
lon_deg: f64,
|
||||
alt_msl_m: f32,
|
||||
rel_alt_m: f32,
|
||||
pitch_deg: f32,
|
||||
yaw_deg: f32,
|
||||
) -> (f64, f64, f32) {
|
||||
let pitch_rad = pitch_deg.to_radians();
|
||||
let vertical = (-pitch_rad.sin()).max(0.01);
|
||||
let slant_m = (rel_alt_m.max(1.0) / vertical).clamp(1.0, 15_000.0);
|
||||
let ground_m = slant_m * pitch_rad.cos().abs();
|
||||
let yaw_rad = yaw_deg.to_radians();
|
||||
let north_m = ground_m * yaw_rad.cos();
|
||||
let east_m = ground_m * yaw_rad.sin();
|
||||
let lat_rad = lat_deg.to_radians();
|
||||
let meters_per_degree_lat = 111_320.0;
|
||||
let meters_per_degree_lon = (111_320.0 * lat_rad.cos().abs()).max(1.0);
|
||||
|
||||
(
|
||||
lat_deg + north_m as f64 / meters_per_degree_lat,
|
||||
lon_deg + east_m as f64 / meters_per_degree_lon,
|
||||
alt_msl_m - rel_alt_m,
|
||||
)
|
||||
}
|
||||
112
src/uas/state.rs
112
src/uas/state.rs
@@ -1,112 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use super::payload::UasSystemPayload;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct LatestUasSystem {
|
||||
pub mavlink_identity: String,
|
||||
pub callsign: String,
|
||||
pub lat_deg: f64,
|
||||
pub lon_deg: f64,
|
||||
pub alt_msl_m: f32,
|
||||
pub rel_alt_m: f32,
|
||||
pub heading_deg: f32,
|
||||
pub fpv_pitch_deg: f32,
|
||||
pub fpv_yaw_deg: f32,
|
||||
pub gimbal_pitch_deg: f32,
|
||||
pub gimbal_yaw_deg: f32,
|
||||
pub video_uri: String,
|
||||
pub hfov_deg: f32,
|
||||
pub vfov_deg: f32,
|
||||
pub image_lat_deg: f64,
|
||||
pub image_lon_deg: f64,
|
||||
pub image_alt_msl_m: f32,
|
||||
pub has_turret_camera: bool,
|
||||
pub active_camera_component: u8,
|
||||
pub home_lat_deg: f64,
|
||||
pub home_lon_deg: f64,
|
||||
pub home_alt_msl_m: f32,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref LATEST_UAS_SYSTEMS: Mutex<HashMap<u8, LatestUasSystem>> = Mutex::new(HashMap::new());
|
||||
}
|
||||
|
||||
pub(crate) fn record_system(system_id: u8, mavlink_identity: &str, payload: &UasSystemPayload) {
|
||||
if let Ok(mut systems) = LATEST_UAS_SYSTEMS.lock() {
|
||||
let active_camera_component = systems
|
||||
.get(&system_id)
|
||||
.map(|system| system.active_camera_component)
|
||||
.unwrap_or(super::constants::CAMERA_COMPONENT_ID);
|
||||
let home = systems
|
||||
.get(&system_id)
|
||||
.map(|system| {
|
||||
(
|
||||
system.home_lat_deg,
|
||||
system.home_lon_deg,
|
||||
system.home_alt_msl_m,
|
||||
)
|
||||
})
|
||||
.unwrap_or((
|
||||
payload.lat_deg,
|
||||
payload.lon_deg,
|
||||
payload.alt_msl_m - payload.rel_alt_m,
|
||||
));
|
||||
|
||||
systems.insert(
|
||||
system_id,
|
||||
LatestUasSystem {
|
||||
mavlink_identity: mavlink_identity.to_string(),
|
||||
callsign: payload.callsign.clone(),
|
||||
lat_deg: payload.lat_deg,
|
||||
lon_deg: payload.lon_deg,
|
||||
alt_msl_m: payload.alt_msl_m,
|
||||
rel_alt_m: payload.rel_alt_m,
|
||||
heading_deg: payload.heading_deg,
|
||||
fpv_pitch_deg: payload.pitch_deg,
|
||||
fpv_yaw_deg: payload.yaw_deg,
|
||||
gimbal_pitch_deg: payload.gimbal_pitch_deg,
|
||||
gimbal_yaw_deg: payload.gimbal_yaw_deg,
|
||||
video_uri: payload.video_uri.clone(),
|
||||
hfov_deg: payload.hfov_deg,
|
||||
vfov_deg: payload.vfov_deg,
|
||||
image_lat_deg: payload.image_lat_deg,
|
||||
image_lon_deg: payload.image_lon_deg,
|
||||
image_alt_msl_m: payload.image_alt_msl_m,
|
||||
has_turret_camera: payload.has_turret_camera,
|
||||
active_camera_component,
|
||||
home_lat_deg: home.0,
|
||||
home_lon_deg: home.1,
|
||||
home_alt_msl_m: home.2,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_home(system_id: u8, lat_deg: f64, lon_deg: f64, alt_msl_m: f32) {
|
||||
if let Ok(mut systems) = LATEST_UAS_SYSTEMS.lock() {
|
||||
if let Some(system) = systems.get_mut(&system_id) {
|
||||
system.home_lat_deg = lat_deg;
|
||||
system.home_lon_deg = lon_deg;
|
||||
system.home_alt_msl_m = alt_msl_m;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn latest_system(system_id: u8) -> Option<LatestUasSystem> {
|
||||
LATEST_UAS_SYSTEMS
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|systems| systems.get(&system_id).cloned())
|
||||
}
|
||||
|
||||
pub(crate) fn set_active_camera(system_id: u8, component_id: u8) {
|
||||
if let Ok(mut systems) = LATEST_UAS_SYSTEMS.lock() {
|
||||
if let Some(system) = systems.get_mut(&system_id) {
|
||||
system.active_camera_component = component_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,14 +15,15 @@ pub enum UdpCommand {
|
||||
|
||||
pub struct UdpClient {
|
||||
pub(crate) tx: Sender<UdpCommand>,
|
||||
pub(crate) address: String,
|
||||
}
|
||||
|
||||
impl UdpClient {
|
||||
pub fn start(&self, address: String, rx: Receiver<UdpCommand>, ctx: Context) {
|
||||
thread::spawn(move || {
|
||||
info!("Starting UDP client thread for destination {}", address);
|
||||
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) => {
|
||||
@@ -31,28 +32,19 @@ impl UdpClient {
|
||||
"Failed to bind UDP socket",
|
||||
e.to_string(),
|
||||
);
|
||||
info!("Failed to bind UDP socket for {}: {}", address, e);
|
||||
info!("Failed to bind UDP socket: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Ok(local_addr) = socket.local_addr() {
|
||||
info!(
|
||||
"UDP client bound local socket {} for destination {}",
|
||||
local_addr, address
|
||||
);
|
||||
}
|
||||
|
||||
let _ = ctx.callback_data("UDP SOCKET", "EUD Connected", address.clone());
|
||||
info!("UDP client reported EUD Connected for {}", address);
|
||||
|
||||
let mut running = true;
|
||||
while running {
|
||||
match rx.recv() {
|
||||
Ok(UdpCommand::SendMessage(message, context)) => {
|
||||
info!("UDP client sending {} bytes to {}", message.len(), address);
|
||||
if let Err(e) = socket.send_to(message.as_bytes(), &address) {
|
||||
info!("Failed to send UDP message to {}: {}", address, e);
|
||||
info!("Failed to send UDP message: {}", e);
|
||||
let _ = context.callback_data(
|
||||
"UDP SOCKET ERROR",
|
||||
"Failed to send UDP message",
|
||||
@@ -62,15 +54,13 @@ impl UdpClient {
|
||||
}
|
||||
Ok(UdpCommand::Stop) => {
|
||||
running = false;
|
||||
info!("Stopping UDP client for {}", address);
|
||||
info!("Stopping UDP client.");
|
||||
}
|
||||
Err(error) => {
|
||||
info!("Error receiving UDP command for {}: {}", address, error);
|
||||
info!("Error receiving command: {}", error.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("UDP client thread exited for {}", address);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -83,9 +73,7 @@ impl UdpClient {
|
||||
|
||||
pub fn stop(&self) {
|
||||
let tx = self.tx.clone();
|
||||
let address = self.address.clone();
|
||||
thread::spawn(move || {
|
||||
info!("Queueing stop for UDP client {}", address);
|
||||
tx.send(UdpCommand::Stop).unwrap();
|
||||
});
|
||||
}
|
||||
@@ -96,32 +84,14 @@ lazy_static! {
|
||||
}
|
||||
|
||||
pub fn start(ctx: Context, address: String) -> &'static str {
|
||||
info!("UDP socket start requested for {}", address);
|
||||
|
||||
let (tx, rx): (Sender<UdpCommand>, Receiver<UdpCommand>) = mpsc::channel();
|
||||
|
||||
let client = UdpClient {
|
||||
tx,
|
||||
address: address.clone(),
|
||||
};
|
||||
|
||||
{
|
||||
let mut client_guard = UDP_CLIENT.lock().unwrap();
|
||||
if let Some(ref existing_client) = *client_guard {
|
||||
info!(
|
||||
"Stopping previous UDP client {} before starting {}",
|
||||
existing_client.address, address
|
||||
);
|
||||
existing_client.stop();
|
||||
}
|
||||
*client_guard = Some(UdpClient {
|
||||
tx: client.tx.clone(),
|
||||
address: client.address.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
let client = UdpClient { tx };
|
||||
client.start(address, rx, ctx);
|
||||
|
||||
let mut client_guard = UDP_CLIENT.lock().unwrap();
|
||||
*client_guard = Some(client);
|
||||
|
||||
"Starting UDP Client"
|
||||
}
|
||||
|
||||
@@ -130,7 +100,6 @@ pub fn send_payload(ctx: Context, payload: String) -> &'static str {
|
||||
client.send_payload(ctx, payload);
|
||||
} else {
|
||||
let _ = ctx.callback_null("UDP SOCKET ERROR", "UDP Socket is not running");
|
||||
info!("UDP send requested while socket was not running");
|
||||
}
|
||||
|
||||
"Sending payload to UDP server"
|
||||
@@ -148,12 +117,10 @@ pub fn send_gps_cot(
|
||||
|
||||
pub fn stop(ctx: Context) -> &'static str {
|
||||
if let Some(ref client) = *UDP_CLIENT.lock().unwrap() {
|
||||
info!("UDP socket stop requested for {}", client.address);
|
||||
client.stop();
|
||||
let _ = ctx.callback_null("UDP SOCKET", "EUD Disconnected");
|
||||
} else {
|
||||
let _ = ctx.callback_null("UDP SOCKET ERROR", "UDP Socket is not running");
|
||||
info!("UDP stop requested while socket was not running");
|
||||
}
|
||||
|
||||
"Stopping UDP Client"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use arma_rs::Context;
|
||||
use lazy_static::lazy_static;
|
||||
use log::info;
|
||||
use std::process::Command;
|
||||
use std::sync::mpsc::{self, Receiver, Sender};
|
||||
use std::sync::Mutex;
|
||||
@@ -17,34 +16,53 @@ lazy_static! {
|
||||
#[cfg(target_os = "windows")]
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
fn stop_existing_stream() {
|
||||
if let Ok(mut lock) = STREAM_CTRL.lock() {
|
||||
if let Some(tx) = lock.take() {
|
||||
let _ = tx.send(());
|
||||
}
|
||||
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 {
|
||||
format!(
|
||||
"rtsp://{}:{}@{}:{}/{}",
|
||||
username, password, address, port, stream_path
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_ffmpeg_with_args(
|
||||
mut cmd: Command,
|
||||
stop_rx: Receiver<()>,
|
||||
status_tx: Sender<Result<(), String>>,
|
||||
description: String,
|
||||
) {
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
fn spawn_ffmpeg(rtsp_url: String, stop_rx: Receiver<()>, status_tx: Sender<Result<(), String>>) {
|
||||
thread::spawn(move || {
|
||||
info!("Starting FFmpeg video stream: {}", description);
|
||||
let mut cmd = Command::new("ffmpeg");
|
||||
cmd.args(&[
|
||||
"-f",
|
||||
"x11grab",
|
||||
"-framerate",
|
||||
"30",
|
||||
"-video_size",
|
||||
"1920x1080",
|
||||
"-i",
|
||||
":0",
|
||||
"-f",
|
||||
"rtsp",
|
||||
"-rtsp_transport",
|
||||
"tcp",
|
||||
&rtsp_url,
|
||||
]);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let child_result = cmd.creation_flags(CREATE_NO_WINDOW).spawn();
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[cfg(target_os = "linux")]
|
||||
let child_result = cmd.spawn();
|
||||
|
||||
match child_result {
|
||||
Ok(mut child) => {
|
||||
let _ = status_tx.send(Ok(()));
|
||||
let _ = stop_rx.recv();
|
||||
info!("Stopping FFmpeg video stream: {}", description);
|
||||
let _ = child.kill();
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -62,169 +80,47 @@ pub fn start_stream(
|
||||
username: String,
|
||||
password: String,
|
||||
) -> &'static str {
|
||||
stop_existing_stream();
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
{
|
||||
let (stop_tx, stop_rx) = mpsc::channel();
|
||||
let (status_tx, status_rx) = mpsc::channel();
|
||||
|
||||
let (stop_tx, stop_rx) = mpsc::channel();
|
||||
let (status_tx, status_rx) = mpsc::channel();
|
||||
let rtsp_url = build_rtsp_url(&address, &port, &stream_path, &username, &password);
|
||||
|
||||
let rtsp_url = if username.is_empty() || password.is_empty() {
|
||||
format!("rtsp://{}:{}/{}", address, port, stream_path)
|
||||
} else {
|
||||
format!("rtsp://{}:{}@{}:{}/{}", username, password, address, port, stream_path)
|
||||
};
|
||||
spawn_ffmpeg(rtsp_url, stop_rx, status_tx);
|
||||
|
||||
let mut cmd = Command::new("ffmpeg");
|
||||
#[cfg(target_os = "windows")]
|
||||
cmd.args([
|
||||
"-f",
|
||||
"gdigrab",
|
||||
"-framerate",
|
||||
"15",
|
||||
"-i",
|
||||
"desktop",
|
||||
"-an",
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-preset",
|
||||
"ultrafast",
|
||||
"-tune",
|
||||
"zerolatency",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
"-f",
|
||||
"rtsp",
|
||||
"-rtsp_transport",
|
||||
"tcp",
|
||||
&rtsp_url,
|
||||
]);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
cmd.args([
|
||||
"-f",
|
||||
"x11grab",
|
||||
"-framerate",
|
||||
"15",
|
||||
"-video_size",
|
||||
"1280x720",
|
||||
"-i",
|
||||
":0.0",
|
||||
"-an",
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-preset",
|
||||
"ultrafast",
|
||||
"-tune",
|
||||
"zerolatency",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
"-f",
|
||||
"rtsp",
|
||||
"-rtsp_transport",
|
||||
"tcp",
|
||||
&rtsp_url,
|
||||
]);
|
||||
|
||||
spawn_ffmpeg_with_args(cmd, stop_rx, status_tx, format!("RTSP {}", rtsp_url));
|
||||
|
||||
if let Ok(mut lock) = STREAM_CTRL.lock() {
|
||||
*lock = Some(stop_tx);
|
||||
}
|
||||
|
||||
match status_rx.recv_timeout(Duration::from_secs(2)) {
|
||||
Ok(Ok(())) => {
|
||||
let _ = ctx.callback_null("VIDEO", "FFmpeg RTSP stream started successfully");
|
||||
"starting video stream"
|
||||
match STREAM_CTRL.lock() {
|
||||
Ok(mut lock) => *lock = Some(stop_tx),
|
||||
Err(e) => {
|
||||
let _ = ctx.callback_data(
|
||||
"VIDEO ERROR",
|
||||
"Failed to acquire lock for stream control",
|
||||
e.to_string(),
|
||||
);
|
||||
return "stream control lock error";
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
let _ = ctx.callback_data("VIDEO ERROR", "FFmpeg failed to start", e);
|
||||
"ffmpeg failed to start"
|
||||
}
|
||||
Err(_) => {
|
||||
let _ = ctx.callback_null("VIDEO ERROR", "FFmpeg did not respond in time");
|
||||
"ffmpeg did not respond"
|
||||
|
||||
match status_rx.recv_timeout(Duration::from_secs(2)) {
|
||||
Ok(Ok(())) => {
|
||||
let _ = ctx.callback_null("VIDEO", "FFmpeg started successfully");
|
||||
"starting video stream"
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
let _ = ctx.callback_data("VIDEO ERROR", "FFmpeg failed to start", e);
|
||||
"ffmpeg failed to start"
|
||||
}
|
||||
Err(_) => {
|
||||
let _ = ctx.callback_null("VIDEO ERROR", "FFmpeg did not respond in time");
|
||||
"ffmpeg did not respond"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_rtp_stream(ctx: Context, address: String, port: String) -> &'static str {
|
||||
stop_existing_stream();
|
||||
|
||||
let (stop_tx, stop_rx) = mpsc::channel();
|
||||
let (status_tx, status_rx) = mpsc::channel();
|
||||
let rtp_url = format!("rtp://{}:{}", address, port);
|
||||
|
||||
let mut cmd = Command::new("ffmpeg");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
cmd.args([
|
||||
"-f",
|
||||
"gdigrab",
|
||||
"-framerate",
|
||||
"15",
|
||||
"-i",
|
||||
"desktop",
|
||||
"-an",
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-preset",
|
||||
"ultrafast",
|
||||
"-tune",
|
||||
"zerolatency",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
"-g",
|
||||
"30",
|
||||
"-f",
|
||||
"rtp",
|
||||
&rtp_url,
|
||||
]);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
cmd.args([
|
||||
"-f",
|
||||
"x11grab",
|
||||
"-framerate",
|
||||
"15",
|
||||
"-video_size",
|
||||
"1280x720",
|
||||
"-i",
|
||||
":0.0",
|
||||
"-an",
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-preset",
|
||||
"ultrafast",
|
||||
"-tune",
|
||||
"zerolatency",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
"-g",
|
||||
"30",
|
||||
"-f",
|
||||
"rtp",
|
||||
&rtp_url,
|
||||
]);
|
||||
|
||||
spawn_ffmpeg_with_args(cmd, stop_rx, status_tx, format!("RTP {}", rtp_url));
|
||||
|
||||
if let Ok(mut lock) = STREAM_CTRL.lock() {
|
||||
*lock = Some(stop_tx);
|
||||
}
|
||||
|
||||
match status_rx.recv_timeout(Duration::from_secs(2)) {
|
||||
Ok(Ok(())) => {
|
||||
info!("Started RTP video stream toward {}", rtp_url);
|
||||
let _ = ctx.callback_null("VIDEO", "FFmpeg RTP stream started successfully");
|
||||
"starting RTP video stream"
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
let _ = ctx.callback_data("VIDEO ERROR", "FFmpeg failed to start RTP stream", e);
|
||||
"ffmpeg failed to start RTP stream"
|
||||
}
|
||||
Err(_) => {
|
||||
let _ = ctx.callback_null("VIDEO ERROR", "FFmpeg RTP stream did not respond in time");
|
||||
"ffmpeg RTP stream did not respond"
|
||||
}
|
||||
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
|
||||
{
|
||||
ctx.callback_null("VIDEO ERROR", "Screen capture is only supported on Windows");
|
||||
"unsupported platform"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
171
vendor/arma-rs/src/context/mod.rs
vendored
Normal file
171
vendor/arma-rs/src/context/mod.rs
vendored
Normal file
@@ -0,0 +1,171 @@
|
||||
//! Contextual execution information.
|
||||
|
||||
use crossbeam_channel::Sender;
|
||||
|
||||
use crate::{CallbackMessage, IntoArma, Value};
|
||||
|
||||
mod global;
|
||||
mod group;
|
||||
mod state;
|
||||
|
||||
pub use self::state::ContextState;
|
||||
pub use global::GlobalContext;
|
||||
pub use group::GroupContext;
|
||||
|
||||
/// Contains information about the current execution context
|
||||
pub struct Context {
|
||||
callback_tx: Sender<CallbackMessage>,
|
||||
global: GlobalContext,
|
||||
group: GroupContext,
|
||||
buffer_size: usize,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub(crate) fn new(
|
||||
callback_tx: Sender<CallbackMessage>,
|
||||
global: GlobalContext,
|
||||
group: GroupContext,
|
||||
) -> Self {
|
||||
Self {
|
||||
callback_tx,
|
||||
global,
|
||||
group,
|
||||
buffer_size: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_group(mut self, ctx: GroupContext) -> Self {
|
||||
self.group = ctx;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) const fn with_buffer_size(mut self, buffer_size: usize) -> Self {
|
||||
self.buffer_size = buffer_size;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Global context
|
||||
pub const fn global(&self) -> &GlobalContext {
|
||||
&self.global
|
||||
}
|
||||
|
||||
/// Group context, is equal to `GlobalContext` if the call is from the global scope.
|
||||
pub const fn group(&self) -> &GroupContext {
|
||||
&self.group
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Returns the length in bytes of the output buffer.
|
||||
/// This is the maximum size of the data that can be returned by the extension.
|
||||
pub const fn buffer_len(&self) -> usize {
|
||||
if self.buffer_size == 0 {
|
||||
0
|
||||
} else {
|
||||
self.buffer_size - 1
|
||||
}
|
||||
}
|
||||
|
||||
fn callback(&self, name: &str, func: &str, data: Option<Value>) -> Result<(), CallbackError> {
|
||||
self.callback_tx
|
||||
.send(CallbackMessage::Call(
|
||||
name.to_string(),
|
||||
func.to_string(),
|
||||
data,
|
||||
))
|
||||
.map_err(|_| CallbackError::ChannelClosed)
|
||||
}
|
||||
|
||||
/// Sends a callback with data into Arma
|
||||
/// <https://community.bistudio.com/wiki/Arma_3:_Mission_Event_Handlers#ExtensionCallback>
|
||||
pub fn callback_data<V>(&self, name: &str, func: &str, data: V) -> Result<(), CallbackError>
|
||||
where
|
||||
V: IntoArma,
|
||||
{
|
||||
self.callback(name, func, Some(data.to_arma()))
|
||||
}
|
||||
|
||||
/// Sends a callback without data into Arma
|
||||
/// <https://community.bistudio.com/wiki/Arma_3:_Mission_Event_Handlers#ExtensionCallback>
|
||||
pub fn callback_null(&self, name: &str, func: &str) -> Result<(), CallbackError> {
|
||||
self.callback(name, func, None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Error that can occur when sending a callback
|
||||
#[derive(Debug)]
|
||||
pub enum CallbackError {
|
||||
/// The callback channel has been closed
|
||||
ChannelClosed,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CallbackError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::ChannelClosed => write!(f, "Callback channel closed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoArma for CallbackError {
|
||||
fn to_arma(&self) -> Value {
|
||||
Value::String(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::State;
|
||||
use crossbeam_channel::{bounded, Sender};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn context(tx: Sender<CallbackMessage>) -> Context {
|
||||
Context::new(
|
||||
tx,
|
||||
GlobalContext::new(String::new(), Arc::new(State::default())),
|
||||
GroupContext::new(Arc::new(State::default())),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_buffer_len_zero() {
|
||||
let (tx, _) = bounded(0);
|
||||
assert_eq!(context(tx).buffer_len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_buffer_len() {
|
||||
let (tx, _) = bounded(0);
|
||||
assert_eq!(context(tx).with_buffer_size(100).buffer_len(), 99);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_callback_block() {
|
||||
let (tx, rx) = bounded(0);
|
||||
let callback_tx = tx.clone();
|
||||
std::thread::spawn(|| {
|
||||
context(callback_tx).callback_null("", "").unwrap();
|
||||
});
|
||||
let callback_tx = tx;
|
||||
std::thread::spawn(|| {
|
||||
context(callback_tx).callback_data("", "", "").unwrap();
|
||||
});
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
assert_eq!(rx.iter().count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_callback_closed() {
|
||||
let (tx, _) = bounded(0);
|
||||
assert!(matches!(
|
||||
context(tx.clone()).callback_null("", ""),
|
||||
Err(CallbackError::ChannelClosed)
|
||||
));
|
||||
assert!(matches!(
|
||||
context(tx).callback_data("", "", ""),
|
||||
Err(CallbackError::ChannelClosed)
|
||||
));
|
||||
}
|
||||
}
|
||||
12
vendor/arma-rs/src/context/state.rs
vendored
Normal file
12
vendor/arma-rs/src/context/state.rs
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
/// A trait for accessing state values
|
||||
pub trait ContextState {
|
||||
/// Get a reference to a state value
|
||||
fn get<T>(&self) -> Option<&T>
|
||||
where
|
||||
T: Send + Sync + 'static;
|
||||
|
||||
/// Set a state value
|
||||
fn set<T>(&self, value: T) -> bool
|
||||
where
|
||||
T: Send + Sync + 'static;
|
||||
}
|
||||
98
vendor/arma-rs/src/ext_result.rs
vendored
Normal file
98
vendor/arma-rs/src/ext_result.rs
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
use crate::value::{IntoArma, Value};
|
||||
|
||||
/// Convert a type to a successful or failed extension result
|
||||
pub trait IntoExtResult {
|
||||
/// Convert a type to a successful or failed extension result
|
||||
fn to_ext_result(&self) -> Result<Value, Value>;
|
||||
}
|
||||
|
||||
impl IntoExtResult for Value {
|
||||
fn to_ext_result(&self) -> Result<Value, Value> {
|
||||
Ok(self.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoExtResult for T
|
||||
where
|
||||
T: IntoArma,
|
||||
{
|
||||
fn to_ext_result(&self) -> Result<Value, Value> {
|
||||
self.to_arma().to_ext_result()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoExtResult for Result<Value, Value> {
|
||||
fn to_ext_result(&self) -> Result<Value, Value> {
|
||||
self.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> IntoExtResult for Result<T, E>
|
||||
where
|
||||
T: IntoArma,
|
||||
E: IntoArma,
|
||||
{
|
||||
fn to_ext_result(&self) -> Result<Value, Value> {
|
||||
match self {
|
||||
Ok(v) => Ok(v.to_arma()),
|
||||
Err(e) => Err(e.to_arma()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn value() {
|
||||
assert_eq!(
|
||||
Ok(Value::Boolean(true)),
|
||||
Value::Boolean(true).to_ext_result()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn option_none() {
|
||||
assert_eq!(Ok(Value::Null), None::<&str>.to_ext_result());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn option_some() {
|
||||
assert_eq!(
|
||||
Ok(Value::String("Hello".into())),
|
||||
Some("Hello".to_string()).to_ext_result()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn result_ok() {
|
||||
assert_eq!(
|
||||
Ok(Value::Number(42.0)),
|
||||
Ok(Value::Number(42.0)).to_ext_result()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn result_err() {
|
||||
assert_eq!(
|
||||
Err(Value::String("Hello".into())),
|
||||
Err(Value::String("Hello".into())).to_ext_result()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn result_unit_ok() {
|
||||
assert_eq!(Ok(Value::Null), Ok::<(), String>(()).to_ext_result());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn result_unit_err() {
|
||||
assert_eq!(Err(Value::Null), Err::<String, ()>(()).to_ext_result());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn result_unit_both() {
|
||||
assert_eq!(Ok(Value::Null), Ok::<(), ()>(()).to_ext_result());
|
||||
}
|
||||
}
|
||||
73
vendor/arma-rs/src/flags.rs
vendored
Normal file
73
vendor/arma-rs/src/flags.rs
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
//! Feature flags for RV Extensions
|
||||
//!
|
||||
//! <https://community.bistudio.com/wiki/Extensions#Feature_Flags>
|
||||
|
||||
/// RVExtensionContext takes const void** as argument, instead of the default const char**, and arguments will be passed in their custom types
|
||||
pub const RV_CONTEXT_ARGUMENTS_VOID_PTR: u64 = 1 << 0;
|
||||
/// RVExtensionContext will retrieve a full Stacktrace
|
||||
pub const RV_CONTEXT_STACK_TRACE: u64 = 1 << 1;
|
||||
/// RVExtensionContext will not be called automatically. It must be manually requested via RVExtensionRequestContext (This improves performance when context is not needed).
|
||||
pub const RV_CONTEXT_NO_DEFAULT_CALL: u64 = 1 << 2;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
/// Feature flags for RV Extensions
|
||||
pub struct FeatureFlags {
|
||||
context_stack_trace: bool,
|
||||
}
|
||||
|
||||
impl FeatureFlags {
|
||||
/// Set the context_stack_trace flag
|
||||
pub fn set_context_stack_trace(&mut self, value: bool) {
|
||||
self.context_stack_trace = value;
|
||||
}
|
||||
|
||||
pub fn with_context_stack_trace(mut self, value: bool) -> Self {
|
||||
self.set_context_stack_trace(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the context_stack_trace flag
|
||||
pub fn context_stack_trace(&self) -> bool {
|
||||
self.context_stack_trace
|
||||
}
|
||||
|
||||
/// Create a new FeatureFlags from the given bits
|
||||
pub fn from_bits(bits: u64) -> Self {
|
||||
let mut flags = Self::default();
|
||||
flags.set_context_stack_trace(bits & RV_CONTEXT_STACK_TRACE != 0);
|
||||
flags
|
||||
}
|
||||
|
||||
/// Get the bits of the FeatureFlags
|
||||
pub fn as_bits(&self) -> u64 {
|
||||
let mut bits = RV_CONTEXT_NO_DEFAULT_CALL | RV_CONTEXT_ARGUMENTS_VOID_PTR;
|
||||
if self.context_stack_trace() {
|
||||
bits |= RV_CONTEXT_STACK_TRACE;
|
||||
}
|
||||
bits
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
let flags = FeatureFlags::default();
|
||||
assert_eq!(
|
||||
flags.as_bits(),
|
||||
RV_CONTEXT_NO_DEFAULT_CALL | RV_CONTEXT_ARGUMENTS_VOID_PTR
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn just_stack_trace() {
|
||||
let mut flags = FeatureFlags::default();
|
||||
flags.set_context_stack_trace(true);
|
||||
assert_eq!(
|
||||
flags.as_bits(),
|
||||
RV_CONTEXT_NO_DEFAULT_CALL | RV_CONTEXT_STACK_TRACE | RV_CONTEXT_ARGUMENTS_VOID_PTR
|
||||
);
|
||||
}
|
||||
}
|
||||
123
vendor/arma-rs/src/group.rs
vendored
Normal file
123
vendor/arma-rs/src/group.rs
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
command::{fn_handler, Factory, Handler},
|
||||
context::{Context, GroupContext},
|
||||
State,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
/// A group of commands.
|
||||
/// Called from Arma using `[group]:[command]`.
|
||||
pub struct Group {
|
||||
commands: HashMap<String, Box<Handler>>,
|
||||
children: HashMap<String, Self>,
|
||||
state: State,
|
||||
}
|
||||
|
||||
impl Group {
|
||||
#[must_use]
|
||||
/// Creates a new group
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
commands: HashMap::new(),
|
||||
children: HashMap::new(),
|
||||
state: State::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
/// Add a new state value to the group if it has not be added already
|
||||
pub fn state<T>(self, state: T) -> Self
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
self.state.set(state);
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
/// Freeze the group's state, preventing the state from changing, allowing for faster reads
|
||||
pub fn freeze_state(mut self) -> Self {
|
||||
self.state.freeze();
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
/// Add a command to the group
|
||||
pub fn command<S, F, I, R>(mut self, name: S, handler: F) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
F: Factory<I, R> + 'static,
|
||||
{
|
||||
self.commands
|
||||
.insert(name.into(), Box::new(fn_handler(handler)));
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
/// Add a group to the group
|
||||
pub fn group<S>(mut self, name: S, child: Self) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
self.children.insert(name.into(), child);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct InternalGroup {
|
||||
commands: HashMap<String, Box<Handler>>,
|
||||
children: HashMap<String, Self>,
|
||||
pub(crate) state: Arc<State>,
|
||||
}
|
||||
|
||||
impl InternalGroup {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn handle(
|
||||
&self,
|
||||
context: Context,
|
||||
acm: &crate::ArmaContextManager,
|
||||
function: &str,
|
||||
output: *mut libc::c_char,
|
||||
size: libc::size_t,
|
||||
args: Option<*mut *mut i8>,
|
||||
count: Option<libc::c_int>,
|
||||
) -> libc::c_int {
|
||||
if let Some((group, function)) = function.split_once(':') {
|
||||
self.children.get(group).map_or(1, |group| {
|
||||
group.handle(context, acm, function, output, size, args, count)
|
||||
})
|
||||
} else if let Some(handler) = self.commands.get(function) {
|
||||
(handler.handler)(
|
||||
context.with_group(GroupContext::new(self.state.clone())),
|
||||
acm,
|
||||
output,
|
||||
size,
|
||||
args,
|
||||
count,
|
||||
)
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Group> for InternalGroup {
|
||||
fn from(group: Group) -> Self {
|
||||
let children = group
|
||||
.children
|
||||
.into_iter()
|
||||
.map(|(name, group)| (name, Self::from(group)))
|
||||
.collect();
|
||||
Self {
|
||||
commands: group.commands,
|
||||
children,
|
||||
state: Arc::new(group.state),
|
||||
}
|
||||
}
|
||||
}
|
||||
520
vendor/arma-rs/src/lib.rs
vendored
Normal file
520
vendor/arma-rs/src/lib.rs
vendored
Normal file
@@ -0,0 +1,520 @@
|
||||
#![warn(missing_docs, nonstandard_style)]
|
||||
#![doc = include_str!(concat!(env!("OUT_DIR"), "/README.md"))]
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
pub use arma_rs_proc::{arma, FromArma, IntoArma};
|
||||
|
||||
#[cfg(feature = "extension")]
|
||||
use crossbeam_channel::{unbounded, Receiver, Sender};
|
||||
#[cfg(feature = "extension")]
|
||||
pub use libc;
|
||||
|
||||
#[cfg(all(target_os = "windows", target_arch = "x86"))]
|
||||
pub use link_args;
|
||||
|
||||
#[cfg(feature = "extension")]
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
mod flags;
|
||||
|
||||
mod value;
|
||||
pub use value::{loadout, DirectReturn, FromArma, FromArmaError, IntoArma, Value};
|
||||
|
||||
#[cfg(feature = "extension")]
|
||||
mod call_context;
|
||||
#[cfg(feature = "extension")]
|
||||
use call_context::{ArmaCallContext, ArmaContextManager};
|
||||
#[cfg(feature = "extension")]
|
||||
pub use call_context::{CallContext, CallContextStackTrace, Caller, Mission, Server, Source};
|
||||
#[cfg(feature = "extension")]
|
||||
mod ext_result;
|
||||
#[cfg(feature = "extension")]
|
||||
pub use ext_result::IntoExtResult;
|
||||
#[cfg(feature = "extension")]
|
||||
mod command;
|
||||
#[cfg(feature = "extension")]
|
||||
pub use command::*;
|
||||
#[cfg(feature = "extension")]
|
||||
pub mod context;
|
||||
#[cfg(feature = "extension")]
|
||||
pub use context::*;
|
||||
#[cfg(feature = "extension")]
|
||||
mod group;
|
||||
#[cfg(feature = "extension")]
|
||||
pub use group::Group;
|
||||
#[cfg(feature = "extension")]
|
||||
pub mod testing;
|
||||
#[cfg(feature = "extension")]
|
||||
pub use testing::Result;
|
||||
|
||||
#[cfg(all(windows, feature = "extension"))]
|
||||
#[doc(hidden)]
|
||||
/// Used by generated code to call back into Arma
|
||||
pub type Callback = extern "stdcall" fn(
|
||||
*const libc::c_char,
|
||||
*const libc::c_char,
|
||||
*const libc::c_char,
|
||||
) -> libc::c_int;
|
||||
#[cfg(all(not(windows), feature = "extension"))]
|
||||
#[doc(hidden)]
|
||||
/// Used by generated code to call back into Arma
|
||||
pub type Callback =
|
||||
extern "C" fn(*const libc::c_char, *const libc::c_char, *const libc::c_char) -> libc::c_int;
|
||||
/// Requests a call context from Arma
|
||||
pub type ContextRequest = unsafe extern "C" fn();
|
||||
|
||||
#[cfg(feature = "extension")]
|
||||
enum CallbackMessage {
|
||||
Call(String, String, Option<Value>),
|
||||
Terminate,
|
||||
}
|
||||
|
||||
#[cfg(feature = "extension")]
|
||||
/// State TypeMap that can hold at most one value per type key.
|
||||
pub type State = state::TypeMap![Send + Sync];
|
||||
|
||||
#[cfg(windows)]
|
||||
/// Allows a console to be allocated for the extension.
|
||||
static CONSOLE_ALLOCATED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
#[no_mangle]
|
||||
#[allow(non_upper_case_globals, reason = "This is a C API")]
|
||||
/// Feature flags read on each callExtension call.
|
||||
pub static mut RVExtensionFeatureFlags: u64 = flags::RV_CONTEXT_NO_DEFAULT_CALL;
|
||||
|
||||
/// Contains all the information about your extension
|
||||
/// This is used by the generated code to interface with Arma
|
||||
#[cfg(feature = "extension")]
|
||||
pub struct Extension {
|
||||
version: String,
|
||||
group: group::InternalGroup,
|
||||
allow_no_args: bool,
|
||||
callback: Option<Callback>,
|
||||
callback_channel: (Sender<CallbackMessage>, Receiver<CallbackMessage>),
|
||||
callback_thread: Option<std::thread::JoinHandle<()>>,
|
||||
context_manager: Rc<ArmaContextManager>,
|
||||
pre218_clear_context_override: bool,
|
||||
}
|
||||
|
||||
#[cfg(feature = "extension")]
|
||||
impl Extension {
|
||||
#[must_use]
|
||||
/// Creates a new extension.
|
||||
pub fn build() -> ExtensionBuilder {
|
||||
ExtensionBuilder {
|
||||
version: String::from("0.0.0"),
|
||||
group: Group::new(),
|
||||
allow_no_args: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "extension")]
|
||||
impl Extension {
|
||||
#[must_use]
|
||||
/// Returns the version of the extension.
|
||||
pub fn version(&self) -> &str {
|
||||
&self.version
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Returns if the extension can be called without any arguments.
|
||||
/// Example:
|
||||
/// ```sqf
|
||||
/// "my_ext" callExtension "my_func"
|
||||
/// ```
|
||||
pub const fn allow_no_args(&self) -> bool {
|
||||
self.allow_no_args
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Called by generated code, do not call directly.
|
||||
pub fn register_callback(&mut self, callback: Callback) {
|
||||
self.callback = Some(callback);
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Called by generated code, do not call directly.
|
||||
/// # Safety
|
||||
/// This function is unsafe because it interacts with the C API.
|
||||
pub unsafe fn handle_call_context(&mut self, args: *mut *mut i8, count: libc::c_int) {
|
||||
self.context_manager
|
||||
.replace(Some(ArmaCallContext::from_arma(args, count)));
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Get a context for interacting with Arma
|
||||
pub fn context(&self) -> Context {
|
||||
Context::new(
|
||||
self.callback_channel.0.clone(),
|
||||
GlobalContext::new(self.version.clone(), self.group.state.clone()),
|
||||
GroupContext::new(self.group.state.clone()),
|
||||
)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Called by generated code, do not call directly.
|
||||
/// # Safety
|
||||
/// This function is unsafe because it interacts with the C API.
|
||||
pub unsafe fn handle_call(
|
||||
&self,
|
||||
function: *mut libc::c_char,
|
||||
output: *mut libc::c_char,
|
||||
size: libc::size_t,
|
||||
args: Option<*mut *mut i8>,
|
||||
count: Option<libc::c_int>,
|
||||
clear_call_context: bool,
|
||||
) -> libc::c_int {
|
||||
if clear_call_context && !self.pre218_clear_context_override {
|
||||
self.context_manager.replace(None);
|
||||
}
|
||||
let function = if let Ok(cstring) = std::ffi::CStr::from_ptr(function).to_str() {
|
||||
cstring.to_string()
|
||||
} else {
|
||||
return 1;
|
||||
};
|
||||
match function.as_str() {
|
||||
#[cfg(windows)]
|
||||
"::console" => {
|
||||
if !CONSOLE_ALLOCATED.swap(true, std::sync::atomic::Ordering::SeqCst) {
|
||||
let _ = windows::Win32::System::Console::AllocConsole();
|
||||
}
|
||||
0
|
||||
}
|
||||
_ => self.group.handle(
|
||||
self.context().with_buffer_size(size),
|
||||
self.context_manager.as_ref(),
|
||||
&function,
|
||||
output,
|
||||
size,
|
||||
args,
|
||||
count,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Create a version of the extension that can be used in tests.
|
||||
pub fn testing(self) -> testing::Extension {
|
||||
testing::Extension::new(self)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Called by generated code, do not call directly.
|
||||
pub fn run_callbacks(&mut self) {
|
||||
let callback = self.callback;
|
||||
let (_, rx) = self.callback_channel.clone();
|
||||
self.callback_thread = Some(std::thread::spawn(move || {
|
||||
while let Ok(CallbackMessage::Call(name, func, data)) = rx.recv() {
|
||||
if let Some(c) = callback {
|
||||
let name = if let Ok(cstring) = std::ffi::CString::new(name) {
|
||||
cstring
|
||||
} else {
|
||||
error!("callback name was not valid");
|
||||
continue;
|
||||
};
|
||||
let func = if let Ok(cstring) = std::ffi::CString::new(func) {
|
||||
cstring
|
||||
} else {
|
||||
error!("callback func was not valid");
|
||||
continue;
|
||||
};
|
||||
let data = if let Ok(cstring) = std::ffi::CString::new(match data {
|
||||
Some(value) => match value {
|
||||
Value::String(s) => s,
|
||||
v => v.to_string(),
|
||||
},
|
||||
None => String::new(),
|
||||
}) {
|
||||
cstring
|
||||
} else {
|
||||
error!("callback data was not valid");
|
||||
continue;
|
||||
};
|
||||
|
||||
let (name, func, data) = (name.into_raw(), func.into_raw(), data.into_raw());
|
||||
loop {
|
||||
if c(name, func, data) >= 0 {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(1));
|
||||
}
|
||||
unsafe {
|
||||
drop(std::ffi::CString::from_raw(name));
|
||||
drop(std::ffi::CString::from_raw(func));
|
||||
drop(std::ffi::CString::from_raw(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "extension")]
|
||||
impl Drop for Extension {
|
||||
// Never called when loaded by arma, instead this is purely required for rust testing.
|
||||
fn drop(&mut self) {
|
||||
if let Some(thread) = self.callback_thread.take() {
|
||||
let (tx, _) = &self.callback_channel;
|
||||
tx.send(CallbackMessage::Terminate).unwrap();
|
||||
thread.join().unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to build an extension.
|
||||
#[cfg(feature = "extension")]
|
||||
pub struct ExtensionBuilder {
|
||||
version: String,
|
||||
group: Group,
|
||||
allow_no_args: bool,
|
||||
}
|
||||
|
||||
#[cfg(feature = "extension")]
|
||||
impl ExtensionBuilder {
|
||||
#[inline]
|
||||
#[must_use]
|
||||
/// Sets the version of the extension.
|
||||
pub fn version(mut self, version: String) -> Self {
|
||||
self.version = version;
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
/// Add a group to the extension.
|
||||
pub fn group<S>(mut self, name: S, group: Group) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
{
|
||||
self.group = self.group.group(name.into(), group);
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
/// Add a new state value to the extension if it has not be added already
|
||||
pub fn state<T>(mut self, state: T) -> Self
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
self.group = self.group.state(state);
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
/// Freeze the extension's state, preventing the state from changing, allowing for faster reads
|
||||
pub fn freeze_state(mut self) -> Self {
|
||||
self.group = self.group.freeze_state();
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
/// Allows the extension to be called without any arguments.
|
||||
/// Example:
|
||||
/// ```sqf
|
||||
/// "my_ext" callExtension "my_func"
|
||||
/// ```
|
||||
pub const fn allow_no_args(mut self) -> Self {
|
||||
self.allow_no_args = true;
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
/// Add a command to the extension.
|
||||
pub fn command<S, F, I, R>(mut self, name: S, handler: F) -> Self
|
||||
where
|
||||
S: Into<String>,
|
||||
F: Factory<I, R> + 'static,
|
||||
{
|
||||
self.group = self.group.command(name, handler);
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
/// Builds the extension.
|
||||
pub fn finish(self) -> Extension {
|
||||
#[expect(unused_mut, reason = "Only used on Windows release")]
|
||||
let mut pre218 = false;
|
||||
#[allow(unused_variables)]
|
||||
let function_name =
|
||||
std::ffi::CString::new("RVExtensionRequestContext").expect("CString::new failed");
|
||||
#[cfg(all(windows, not(debug_assertions)))]
|
||||
let request_context: ContextRequest = {
|
||||
let handle = unsafe { winapi::um::libloaderapi::GetModuleHandleW(std::ptr::null()) };
|
||||
if handle.is_null() {
|
||||
panic!("GetModuleHandleW failed");
|
||||
}
|
||||
let func_address =
|
||||
unsafe { winapi::um::libloaderapi::GetProcAddress(handle, function_name.as_ptr()) };
|
||||
if func_address.is_null() {
|
||||
pre218 = true;
|
||||
empty_request_context
|
||||
} else {
|
||||
unsafe { std::mem::transmute(func_address) }
|
||||
}
|
||||
};
|
||||
#[cfg(all(not(windows), not(debug_assertions)))]
|
||||
let request_context: ContextRequest = {
|
||||
let handle = unsafe { libc::dlopen(std::ptr::null(), libc::RTLD_LAZY) };
|
||||
if handle.is_null() {
|
||||
panic!("Failed to open handle to current process");
|
||||
}
|
||||
let func_address = unsafe { libc::dlsym(handle, function_name.as_ptr()) };
|
||||
if func_address.is_null() {
|
||||
pre218 = true;
|
||||
empty_request_context
|
||||
} else {
|
||||
let func = unsafe { std::mem::transmute(func_address) };
|
||||
unsafe { libc::dlclose(handle) };
|
||||
func
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let request_context = empty_request_context;
|
||||
|
||||
Extension {
|
||||
version: self.version,
|
||||
group: self.group.into(),
|
||||
allow_no_args: self.allow_no_args,
|
||||
callback: None,
|
||||
callback_channel: unbounded(),
|
||||
callback_thread: None,
|
||||
context_manager: Rc::new(ArmaContextManager::new(request_context)),
|
||||
pre218_clear_context_override: pre218,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn empty_request_context() {}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Called by generated code, do not call directly.
|
||||
///
|
||||
/// # Safety
|
||||
/// This function is unsafe because it interacts with the C API.
|
||||
///
|
||||
/// # Note
|
||||
/// This function assumes `buf_size` includes space for a single terminating zero byte at the end.
|
||||
#[cfg(feature = "extension")]
|
||||
pub unsafe fn write_cstr(
|
||||
string: String,
|
||||
ptr: *mut libc::c_char,
|
||||
buf_size: libc::size_t,
|
||||
) -> Option<libc::size_t> {
|
||||
if string.is_empty() {
|
||||
return Some(0);
|
||||
}
|
||||
|
||||
let cstr = std::ffi::CString::new(string).ok()?;
|
||||
let len_to_copy = cstr.as_bytes().len();
|
||||
if len_to_copy >= buf_size {
|
||||
return None;
|
||||
}
|
||||
|
||||
ptr.copy_from(cstr.as_ptr(), len_to_copy);
|
||||
ptr.add(len_to_copy).write(0x00);
|
||||
Some(len_to_copy)
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "extension"))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn write_size_zero() {
|
||||
const BUF_SIZE: libc::size_t = 0;
|
||||
let mut buf = [0; BUF_SIZE];
|
||||
let result = unsafe { write_cstr("a".to_string(), buf.as_mut_ptr(), BUF_SIZE) };
|
||||
|
||||
assert_eq!(result, None);
|
||||
assert_eq!(buf, [0; BUF_SIZE]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_size_zero_empty() {
|
||||
const BUF_SIZE: libc::size_t = 0;
|
||||
let mut buf = [0; BUF_SIZE];
|
||||
let result = unsafe { write_cstr("".to_string(), buf.as_mut_ptr(), BUF_SIZE) };
|
||||
|
||||
assert_eq!(result, Some(0));
|
||||
assert_eq!(buf, [0; BUF_SIZE]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_size_one() {
|
||||
const BUF_SIZE: libc::size_t = 1;
|
||||
let mut buf = [0; BUF_SIZE];
|
||||
let result = unsafe { write_cstr("a".to_string(), buf.as_mut_ptr(), BUF_SIZE) };
|
||||
|
||||
assert_eq!(result, None);
|
||||
assert_eq!(buf, [0; BUF_SIZE]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_size_one_empty() {
|
||||
const BUF_SIZE: libc::size_t = 1;
|
||||
let mut buf = [0; BUF_SIZE];
|
||||
let result = unsafe { write_cstr("".to_string(), buf.as_mut_ptr(), BUF_SIZE) };
|
||||
|
||||
assert_eq!(result, Some(0));
|
||||
assert_eq!(buf, [0; BUF_SIZE]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_empty() {
|
||||
const BUF_SIZE: libc::size_t = 7;
|
||||
let mut buf = [0; BUF_SIZE];
|
||||
let result = unsafe { write_cstr("".to_string(), buf.as_mut_ptr(), BUF_SIZE) };
|
||||
|
||||
assert_eq!(result, Some(0));
|
||||
assert_eq!(buf, [0; BUF_SIZE]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_half() {
|
||||
const BUF_SIZE: libc::size_t = 7;
|
||||
let mut buf = [0; BUF_SIZE];
|
||||
let result = unsafe { write_cstr("foo".to_string(), buf.as_mut_ptr(), BUF_SIZE) };
|
||||
|
||||
assert_eq!(result, Some(3));
|
||||
assert_eq!(buf, (b"foo\0\0\0\0").map(|c| c as i8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_full() {
|
||||
const BUF_SIZE: libc::size_t = 7;
|
||||
let mut buf = [0; BUF_SIZE];
|
||||
let result = unsafe { write_cstr("foobar".to_string(), buf.as_mut_ptr(), BUF_SIZE) };
|
||||
|
||||
assert_eq!(result, Some(6));
|
||||
assert_eq!(buf, (b"foobar\0").map(|c| c as i8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_overflow() {
|
||||
const BUF_SIZE: libc::size_t = 7;
|
||||
let mut buf = [0; BUF_SIZE];
|
||||
let result = unsafe { write_cstr("foo bar".to_string(), buf.as_mut_ptr(), BUF_SIZE) };
|
||||
|
||||
assert_eq!(result, None);
|
||||
assert_eq!(buf, [0; BUF_SIZE]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_overwrite() {
|
||||
const BUF_SIZE: libc::size_t = 7;
|
||||
let mut buf = (b"zzzzzz\0").map(|c| c as i8);
|
||||
let result = unsafe { write_cstr("a".to_string(), buf.as_mut_ptr(), BUF_SIZE) };
|
||||
|
||||
assert_eq!(result, Some(1));
|
||||
assert_eq!(buf, (b"a\0zzzz\0").map(|c| c as i8));
|
||||
}
|
||||
}
|
||||
167
vendor/arma-rs/src/testing.rs
vendored
Normal file
167
vendor/arma-rs/src/testing.rs
vendored
Normal file
@@ -0,0 +1,167 @@
|
||||
//! For testing your extension.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{CallbackMessage, Context, State, Value};
|
||||
|
||||
use crate::{ArmaCallContext, Caller, Mission, Server, Source};
|
||||
|
||||
/// Wrapper around [`crate::Extension`] used for testing.
|
||||
pub struct Extension(crate::Extension);
|
||||
|
||||
const BUFFER_SIZE: libc::size_t = 10240; // The sized used by Arma 3 as of 2021-12-30
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
/// Result of an event handler
|
||||
pub enum Result<T, E> {
|
||||
/// an event has been handled and the handler is done, the value of T is the return value of the event handler
|
||||
Ok(T),
|
||||
/// the handler has encountered an error, the value of T is the return value of the event handler
|
||||
Err(E),
|
||||
/// an event is handled but the handler is not done and should receive another event
|
||||
Continue,
|
||||
/// the handler reached the specified timeout
|
||||
Timeout,
|
||||
}
|
||||
|
||||
impl<T, E> Result<T, E> {
|
||||
/// Returns true if the result is an ok result
|
||||
pub fn is_ok(&self) -> bool {
|
||||
matches!(self, Self::Ok(_))
|
||||
}
|
||||
|
||||
/// Returns true if the result is an error
|
||||
pub fn is_err(&self) -> bool {
|
||||
matches!(self, Self::Err(_))
|
||||
}
|
||||
|
||||
/// Returns true if the result is a continue result
|
||||
pub fn is_continue(&self) -> bool {
|
||||
matches!(self, Self::Continue)
|
||||
}
|
||||
|
||||
/// Returns true if the result is a timeout result
|
||||
pub fn is_timeout(&self) -> bool {
|
||||
matches!(self, Self::Timeout)
|
||||
}
|
||||
}
|
||||
|
||||
impl Extension {
|
||||
/// Create a new testing Extension
|
||||
pub fn new(ext: crate::Extension) -> Self {
|
||||
Self(ext)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Returns a context for simulating interactions with Arma
|
||||
pub fn context(&self) -> Context {
|
||||
self.0.context().with_buffer_size(BUFFER_SIZE)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Get a reference to the extensions state container
|
||||
pub fn state(&self) -> &State {
|
||||
&self.0.group.state
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
/// Call a function with Arma call context.
|
||||
///
|
||||
/// # Safety
|
||||
/// This function is unsafe because it interacts with the C API.
|
||||
pub fn call_with_context(
|
||||
&self,
|
||||
function: &str,
|
||||
args: Option<Vec<String>>,
|
||||
caller: Caller,
|
||||
source: Source,
|
||||
mission: Mission,
|
||||
server: Server,
|
||||
remote_exec_owner: i16,
|
||||
) -> (String, libc::c_int) {
|
||||
self.0.context_manager.replace(Some(ArmaCallContext::new(
|
||||
caller,
|
||||
source,
|
||||
mission,
|
||||
server,
|
||||
remote_exec_owner,
|
||||
)));
|
||||
unsafe { self.handle_call(function, args) }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Call a function without Arma call context.
|
||||
///
|
||||
/// # Safety
|
||||
/// This function is unsafe because it interacts with the C API.
|
||||
///
|
||||
/// # Note
|
||||
/// If the `call-context` feature is enabled, this function passes default values for each field.
|
||||
pub fn call(&self, function: &str, args: Option<Vec<String>>) -> (String, libc::c_int) {
|
||||
self.0.context_manager.replace(None);
|
||||
unsafe { self.handle_call(function, args) }
|
||||
}
|
||||
|
||||
unsafe fn handle_call(
|
||||
&self,
|
||||
function: &str,
|
||||
args: Option<Vec<String>>,
|
||||
) -> (String, libc::c_int) {
|
||||
let mut output = [0; BUFFER_SIZE];
|
||||
let len = args.as_ref().map(|a| a.len().try_into().unwrap());
|
||||
let mut args_pointer = args.map(|v| {
|
||||
v.into_iter()
|
||||
.map(|s| std::ffi::CString::new(s).unwrap().into_raw())
|
||||
.collect::<Vec<*mut i8>>()
|
||||
});
|
||||
let res = self.0.group.handle(
|
||||
self.context(),
|
||||
&self.0.context_manager,
|
||||
function,
|
||||
output.as_mut_ptr(),
|
||||
BUFFER_SIZE,
|
||||
args_pointer.as_mut().map(Vec::as_mut_ptr),
|
||||
len,
|
||||
);
|
||||
if let Some(args) = args_pointer {
|
||||
for arg in args {
|
||||
let _ = std::ffi::CString::from_raw(arg);
|
||||
}
|
||||
}
|
||||
(
|
||||
std::ffi::CStr::from_ptr(output.as_ptr())
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
res,
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a callback handler
|
||||
///
|
||||
/// Returns a Result from the handler if the callback was handled,
|
||||
/// or `Result::Timeout` if either no event was received, or the handler
|
||||
/// returned `Result::Continue` until the timeout was reached.
|
||||
///
|
||||
/// The handler must return a Result indicating the callback was handled to exit
|
||||
/// `Result::Continue` will continue to provide events to the handler until another variant is returned
|
||||
pub fn callback_handler<F, T, E>(&self, handler: F, timeout: Duration) -> Result<T, E>
|
||||
where
|
||||
F: Fn(&str, &str, Option<Value>) -> Result<T, E>,
|
||||
{
|
||||
let (_, rx) = &self.0.callback_channel;
|
||||
let deadline = std::time::Instant::now() + timeout;
|
||||
loop {
|
||||
match rx.recv_deadline(deadline) {
|
||||
Ok(CallbackMessage::Call(name, func, data)) => match handler(&name, &func, data) {
|
||||
Result::Ok(value) => return Result::Ok(value),
|
||||
Result::Err(error) => return Result::Err(error),
|
||||
Result::Timeout => return Result::Timeout,
|
||||
Result::Continue => {}
|
||||
},
|
||||
_ => return Result::Timeout,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
41
vendor/arma-rs/src/value/features/chrono.rs
vendored
Normal file
41
vendor/arma-rs/src/value/features/chrono.rs
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
use chrono::{DateTime, Datelike, NaiveDate, NaiveDateTime, TimeZone, Timelike};
|
||||
|
||||
use crate::{FromArma, FromArmaError, IntoArma, Value};
|
||||
|
||||
impl IntoArma for NaiveDateTime {
|
||||
fn to_arma(&self) -> Value {
|
||||
vec![
|
||||
self.year() as u32,
|
||||
self.month(),
|
||||
self.day(),
|
||||
self.hour(),
|
||||
self.minute(),
|
||||
self.second(),
|
||||
self.nanosecond() / 1_000_000,
|
||||
]
|
||||
.to_arma()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: TimeZone> IntoArma for DateTime<T> {
|
||||
fn to_arma(&self) -> Value {
|
||||
self.naive_utc().to_arma()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromArma for NaiveDateTime {
|
||||
fn from_arma(s: String) -> Result<Self, FromArmaError> {
|
||||
let arma_date: [i64; 7] = FromArma::from_arma(s)?;
|
||||
Ok(NaiveDate::from_ymd(
|
||||
arma_date[0].try_into().unwrap(),
|
||||
arma_date[1].try_into().unwrap(),
|
||||
arma_date[2].try_into().unwrap(),
|
||||
)
|
||||
.and_hms_milli(
|
||||
arma_date[3].try_into().unwrap(),
|
||||
arma_date[4].try_into().unwrap(),
|
||||
arma_date[5].try_into().unwrap(),
|
||||
arma_date[6].try_into().unwrap(),
|
||||
))
|
||||
}
|
||||
}
|
||||
8
vendor/arma-rs/src/value/features/mod.rs
vendored
Normal file
8
vendor/arma-rs/src/value/features/mod.rs
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
#[cfg(feature = "uuid")]
|
||||
mod uuid;
|
||||
|
||||
#[cfg(feature = "chrono")]
|
||||
mod chrono;
|
||||
|
||||
#[cfg(feature = "serde_json")]
|
||||
mod serde_json;
|
||||
51
vendor/arma-rs/src/value/features/serde_json.rs
vendored
Normal file
51
vendor/arma-rs/src/value/features/serde_json.rs
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
use crate::{FromArma, FromArmaError, IntoArma, Value};
|
||||
|
||||
impl IntoArma for serde_json::Value {
|
||||
fn to_arma(&self) -> Value {
|
||||
match self {
|
||||
serde_json::Value::Null => Value::Null,
|
||||
serde_json::Value::Bool(b) => Value::Boolean(*b),
|
||||
serde_json::Value::Number(n) => Value::Number(if n.is_f64() {
|
||||
n.as_f64().unwrap()
|
||||
} else if n.is_i64() {
|
||||
n.as_i64().unwrap() as f64
|
||||
} else if n.is_u64() {
|
||||
n.as_u64().unwrap() as f64
|
||||
} else {
|
||||
unreachable!()
|
||||
}),
|
||||
serde_json::Value::String(s) => Value::String(s.to_owned()),
|
||||
serde_json::Value::Array(v) => {
|
||||
Value::Array(v.iter().map(|v| v.to_arma()).collect::<Vec<Value>>())
|
||||
}
|
||||
serde_json::Value::Object(o) => o
|
||||
.iter()
|
||||
.map(|(k, v)| vec![Value::String(k.to_owned()), v.to_arma()])
|
||||
.collect::<Vec<Vec<Value>>>()
|
||||
.to_arma(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromArma for serde_json::Value {
|
||||
fn from_arma(s: String) -> Result<Self, FromArmaError> {
|
||||
let value = Value::from_arma(s)?;
|
||||
Ok(value.to_json())
|
||||
}
|
||||
}
|
||||
|
||||
impl Value {
|
||||
/// Convert a Value to a serde_json::Value
|
||||
pub fn to_json(&self) -> serde_json::Value {
|
||||
match self {
|
||||
Value::Null => serde_json::Value::Null,
|
||||
Value::Boolean(b) => serde_json::Value::Bool(*b),
|
||||
Value::Number(n) => {
|
||||
serde_json::Value::Number(serde_json::Number::from_f64(*n).unwrap())
|
||||
}
|
||||
Value::String(s) => serde_json::Value::String(s.to_owned()),
|
||||
Value::Array(v) => serde_json::Value::Array(v.iter().map(|v| v.to_json()).collect()),
|
||||
Value::Unknown(s) => serde_json::Value::String(s.to_owned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
17
vendor/arma-rs/src/value/features/uuid.rs
vendored
Normal file
17
vendor/arma-rs/src/value/features/uuid.rs
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
use crate::{FromArma, FromArmaError, IntoArma, Value};
|
||||
|
||||
impl IntoArma for uuid::Uuid {
|
||||
fn to_arma(&self) -> Value {
|
||||
self.to_string().to_arma()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromArma for uuid::Uuid {
|
||||
fn from_arma(s: String) -> Result<Self, FromArmaError> {
|
||||
let s = s
|
||||
.strip_prefix('"')
|
||||
.and_then(|s| s.strip_suffix('"'))
|
||||
.unwrap_or(&s);
|
||||
uuid::Uuid::parse_str(s).map_err(FromArmaError::custom)
|
||||
}
|
||||
}
|
||||
558
vendor/arma-rs/src/value/from_arma.rs
vendored
Normal file
558
vendor/arma-rs/src/value/from_arma.rs
vendored
Normal file
@@ -0,0 +1,558 @@
|
||||
use crate::Value;
|
||||
|
||||
fn split_array(s: &str) -> Vec<String> {
|
||||
let mut nest = 0;
|
||||
let mut parts = Vec::new();
|
||||
let mut part = String::new();
|
||||
for c in s.chars() {
|
||||
if c == '[' {
|
||||
part.push(c);
|
||||
nest += 1;
|
||||
} else if c == ']' {
|
||||
nest -= 1;
|
||||
part.push(c);
|
||||
} else if c == ',' && nest == 0 {
|
||||
parts.push(part.trim().to_string());
|
||||
part = String::new();
|
||||
} else {
|
||||
part.push(c);
|
||||
}
|
||||
}
|
||||
let part = part.trim().to_string();
|
||||
if !part.is_empty() {
|
||||
parts.push(part);
|
||||
}
|
||||
parts
|
||||
}
|
||||
|
||||
/// Error type for [`FromArma`]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum FromArmaError {
|
||||
/// Invalid [`crate::Value`]
|
||||
InvalidValue(String),
|
||||
/// Invalid primitive value
|
||||
InvalidPrimitive(String),
|
||||
/// Collection size mismatch
|
||||
InvalidLength {
|
||||
/// Expected size
|
||||
expected: usize,
|
||||
/// Actual size
|
||||
actual: usize,
|
||||
},
|
||||
|
||||
/// Missing opening(true) or closing(false) bracket
|
||||
MissingBracket(bool),
|
||||
/// Missing field
|
||||
MissingField(String),
|
||||
/// Unknown field
|
||||
UnknownField(String),
|
||||
/// Duplicate field
|
||||
DuplicateField(String),
|
||||
|
||||
/// Custom error message
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FromArmaError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::InvalidValue(s) => write!(f, "invalid value: {s}"),
|
||||
Self::InvalidPrimitive(s) => write!(f, "error parsing primitive: {s}"),
|
||||
Self::InvalidLength { expected, actual } => {
|
||||
write!(f, "expected {expected} elements, got {actual}")
|
||||
}
|
||||
Self::MissingBracket(start) => match *start {
|
||||
true => write!(f, "missing '[' at start of array"),
|
||||
false => write!(f, "missing ']' at end of array"),
|
||||
},
|
||||
Self::MissingField(s) => write!(f, "missing field: {s}"),
|
||||
Self::UnknownField(s) => write!(f, "unknown field: {s}"),
|
||||
Self::DuplicateField(s) => write!(f, "duplicate field: {s}"),
|
||||
Self::Custom(s) => f.write_str(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromArmaError {
|
||||
/// Creates a new [`FromArmaError::Custom`]
|
||||
pub fn custom(msg: impl std::fmt::Display) -> Self {
|
||||
Self::Custom(msg.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for converting a value from Arma to a Rust value.
|
||||
pub trait FromArma: Sized {
|
||||
/// Converts a value from Arma to a Rust value.
|
||||
/// # Errors
|
||||
/// Will return an error if the value cannot be converted.
|
||||
fn from_arma(s: String) -> Result<Self, FromArmaError>;
|
||||
}
|
||||
|
||||
#[cfg(not(any(test, doc, debug_assertions)))]
|
||||
impl FromArma for String {
|
||||
fn from_arma(s: String) -> Result<Self, FromArmaError> {
|
||||
let Some(s) = s.strip_prefix('"').and_then(|s| s.strip_suffix('"')) else {
|
||||
return Err(FromArmaError::InvalidPrimitive(String::from(
|
||||
"missing '\"' at start or end of string",
|
||||
)));
|
||||
};
|
||||
Ok(s.replace("\"\"", "\""))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, doc, debug_assertions))]
|
||||
impl FromArma for String {
|
||||
fn from_arma(s: String) -> Result<Self, FromArmaError> {
|
||||
let s = s
|
||||
.strip_prefix('"')
|
||||
.and_then(|s| s.strip_suffix('"'))
|
||||
.unwrap_or(&s);
|
||||
Ok(s.replace("\"\"", "\""))
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_from_arma {
|
||||
($($t:ty),*) => {
|
||||
$(
|
||||
impl FromArma for $t {
|
||||
fn from_arma(s: String) -> Result<Self, FromArmaError> {
|
||||
let s = s.strip_suffix('"').and_then(|s| s.strip_prefix('"')).unwrap_or(&s);
|
||||
s.parse::<Self>().map_err(|e| FromArmaError::InvalidPrimitive(e.to_string()))
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
impl_from_arma!(f32, f64, bool, char);
|
||||
|
||||
macro_rules! impl_from_arma_number {
|
||||
($($t:ty),*) => {
|
||||
$(
|
||||
impl FromArma for $t {
|
||||
fn from_arma(s: String) -> Result<Self, FromArmaError> {
|
||||
let s = s.strip_suffix('"').and_then(|s| s.strip_prefix('"')).unwrap_or(&s);
|
||||
if s.contains("e") {
|
||||
// parse exponential notation
|
||||
let mut parts = s.split('e');
|
||||
let base = match parts.next().unwrap() {
|
||||
s if !s.is_empty() => s.parse::<f64>().map_err(|e| FromArmaError::InvalidPrimitive(e.to_string()))?,
|
||||
_ => return Err(FromArmaError::InvalidPrimitive("invalid number literal".to_string())),
|
||||
};
|
||||
let exp = match parts.next().unwrap() {
|
||||
s if !s.is_empty() => s.parse::<i32>().map_err(|e| FromArmaError::InvalidPrimitive(e.to_string()))?,
|
||||
_ => return Err(FromArmaError::InvalidPrimitive("invalid number literal".to_string())),
|
||||
};
|
||||
return Ok((base * 10.0_f64.powi(exp)) as $t);
|
||||
}
|
||||
s.parse::<Self>().map_err(|e| FromArmaError::InvalidPrimitive(e.to_string()))
|
||||
}
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
impl_from_arma_number!(i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize);
|
||||
|
||||
macro_rules! impl_from_arma_tuple {
|
||||
{ $c: expr, $($t:ident)* } => {
|
||||
impl<$($t),*> FromArma for ($($t),*)
|
||||
where
|
||||
$($t: FromArma),*
|
||||
{
|
||||
fn from_arma(s: String) -> Result<Self, FromArmaError> {
|
||||
let v: [Value; $c] = FromArma::from_arma(s)?;
|
||||
let mut iter = v.iter();
|
||||
Ok((
|
||||
$($t::from_arma(iter.next().unwrap().to_string())?),*
|
||||
))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_from_arma_tuple! { 2, A B }
|
||||
impl_from_arma_tuple! { 3, A B C }
|
||||
impl_from_arma_tuple! { 4, A B C D }
|
||||
impl_from_arma_tuple! { 5, A B C D E }
|
||||
impl_from_arma_tuple! { 6, A B C D E F }
|
||||
impl_from_arma_tuple! { 7, A B C D E F G }
|
||||
impl_from_arma_tuple! { 8, A B C D E F G H }
|
||||
impl_from_arma_tuple! { 9, A B C D E F G H I }
|
||||
impl_from_arma_tuple! { 10, A B C D E F G H I J }
|
||||
impl_from_arma_tuple! { 11, A B C D E F G H I J K }
|
||||
impl_from_arma_tuple! { 12, A B C D E F G H I J K L }
|
||||
impl_from_arma_tuple! { 13, A B C D E F G H I J K L M }
|
||||
impl_from_arma_tuple! { 14, A B C D E F G H I J K L M N }
|
||||
impl_from_arma_tuple! { 15, A B C D E F G H I J K L M N O }
|
||||
impl_from_arma_tuple! { 16, A B C D E F G H I J K L M N O P }
|
||||
impl_from_arma_tuple! { 17, A B C D E F G H I J K L M N O P Q }
|
||||
impl_from_arma_tuple! { 18, A B C D E F G H I J K L M N O P Q R }
|
||||
impl_from_arma_tuple! { 19, A B C D E F G H I J K L M N O P Q R S }
|
||||
impl_from_arma_tuple! { 20, A B C D E F G H I J K L M N O P Q R S T }
|
||||
impl_from_arma_tuple! { 21, A B C D E F G H I J K L M N O P Q R S T U }
|
||||
impl_from_arma_tuple! { 22, A B C D E F G H I J K L M N O P Q R S T U V }
|
||||
impl_from_arma_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 }
|
||||
impl_from_arma_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 }
|
||||
impl_from_arma_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 }
|
||||
impl_from_arma_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 }
|
||||
|
||||
impl<T> FromArma for Vec<T>
|
||||
where
|
||||
T: FromArma,
|
||||
{
|
||||
fn from_arma(s: String) -> Result<Self, FromArmaError> {
|
||||
let source = s
|
||||
.strip_prefix('[')
|
||||
.ok_or(FromArmaError::MissingBracket(true))?
|
||||
.strip_suffix(']')
|
||||
.ok_or(FromArmaError::MissingBracket(false))?;
|
||||
let parts = split_array(source);
|
||||
parts.iter().try_fold(Self::new(), |mut acc, p| {
|
||||
acc.push(T::from_arma(p.to_string())?);
|
||||
Ok(acc)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, const N: usize> FromArma for [T; N]
|
||||
where
|
||||
T: FromArma,
|
||||
{
|
||||
fn from_arma(s: String) -> Result<Self, FromArmaError> {
|
||||
let v: Vec<T> = FromArma::from_arma(s)?;
|
||||
let len = v.len();
|
||||
v.try_into().map_err(|_| FromArmaError::InvalidLength {
|
||||
expected: N,
|
||||
actual: len,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V, S> FromArma for std::collections::HashMap<K, V, S>
|
||||
where
|
||||
K: FromArma + Eq + std::hash::Hash,
|
||||
V: FromArma,
|
||||
S: std::hash::BuildHasher + Default,
|
||||
{
|
||||
fn from_arma(s: String) -> Result<Self, FromArmaError> {
|
||||
let data: Vec<(K, V)> = FromArma::from_arma(s)?;
|
||||
let mut ret = Self::default();
|
||||
for (k, v) in data {
|
||||
ret.insert(k, v);
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::Value;
|
||||
|
||||
#[test]
|
||||
fn parse_tuple_varying_types() {
|
||||
assert_eq!(
|
||||
(String::from("hello"), 123),
|
||||
<(String, i32)>::from_arma(r#"["hello", 123]"#.to_string()).unwrap()
|
||||
);
|
||||
assert!(<(String, String)>::from_arma(r#"["hello", 123, "world"]"#.to_string()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_tuple_size_errors() {
|
||||
assert_eq!(
|
||||
<(String, i32)>::from_arma(r#"[]"#.to_string()),
|
||||
Err(FromArmaError::InvalidLength {
|
||||
expected: 2,
|
||||
actual: 0
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
<(String, i32)>::from_arma(r#"["hello"]"#.to_string()),
|
||||
Err(FromArmaError::InvalidLength {
|
||||
expected: 2,
|
||||
actual: 1
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
<(String, i32)>::from_arma(r#"["hello", 123, 456]"#.to_string()),
|
||||
Err(FromArmaError::InvalidLength {
|
||||
expected: 2,
|
||||
actual: 3
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_tuple_bracket_errors() {
|
||||
assert_eq!(
|
||||
<(String, i32)>::from_arma(r#"["hello", 123"#.to_string()),
|
||||
Err(FromArmaError::MissingBracket(false))
|
||||
);
|
||||
assert_eq!(
|
||||
<(String, i32)>::from_arma(r#""hello", 123"#.to_string()),
|
||||
Err(FromArmaError::MissingBracket(true))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tuple_2() {
|
||||
assert_eq!(
|
||||
(0, 1),
|
||||
<(u8, u8)>::from_arma(r#"[0, 1]"#.to_string()).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tuple_3() {
|
||||
assert_eq!(
|
||||
(0, 1, 2),
|
||||
<(u8, u8, u8)>::from_arma(r#"[0, 1, 2]"#.to_string()).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tuple_4() {
|
||||
assert_eq!(
|
||||
(0, 1, 2, 3),
|
||||
<(u8, u8, u8, u8)>::from_arma(r#"[0, 1, 2, 3]"#.to_string()).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tuple_5() {
|
||||
assert_eq!(
|
||||
(0, 1, 2, 3, 4),
|
||||
<(u8, u8, u8, u8, u8)>::from_arma(r#"[0, 1, 2, 3, 4]"#.to_string()).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tuple_6() {
|
||||
assert_eq!(
|
||||
(0, 1, 2, 3, 4, 5),
|
||||
<(u8, u8, u8, u8, u8, u8)>::from_arma(r#"[0, 1, 2, 3, 4, 5]"#.to_string()).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tuple_7() {
|
||||
assert_eq!(
|
||||
(0, 1, 2, 3, 4, 5, 6),
|
||||
<(u8, u8, u8, u8, u8, u8, u8)>::from_arma(r#"[0, 1, 2, 3, 4, 5, 6]"#.to_string())
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tuple_8() {
|
||||
assert_eq!(
|
||||
(0, 1, 2, 3, 4, 5, 6, 7),
|
||||
<(u8, u8, u8, u8, u8, u8, u8, u8)>::from_arma(
|
||||
r#"[0, 1, 2, 3, 4, 5, 6, 7]"#.to_string()
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tuple_9() {
|
||||
assert_eq!(
|
||||
(0, 1, 2, 3, 4, 5, 6, 7, 8),
|
||||
<(u8, u8, u8, u8, u8, u8, u8, u8, u8)>::from_arma(
|
||||
r#"[0, 1, 2, 3, 4, 5, 6, 7, 8]"#.to_string()
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tuple_10() {
|
||||
assert_eq!(
|
||||
(0, 1, 2, 3, 4, 5, 6, 7, 8, 9),
|
||||
<(u8, u8, u8, u8, u8, u8, u8, u8, u8, u8)>::from_arma(
|
||||
r#"[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]"#.to_string()
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_string() {
|
||||
assert_eq!(
|
||||
String::from("hello"),
|
||||
<String>::from_arma(r#""hello""#.to_string()).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
String::from("\"hello\""),
|
||||
<String>::from_arma(r#"""hello"""#.to_string()).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
String::from(r#"hello "john"."#),
|
||||
<String>::from_arma(r#""hello ""john"".""#.to_string()).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_vec() {
|
||||
assert_eq!(
|
||||
vec![String::from("hello"), String::from("bye"),],
|
||||
<Vec<String>>::from_arma(r#"["hello","bye"]"#.to_string()).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
vec![String::from("hello"), String::from("world")],
|
||||
<Vec<String>>::from_arma(r#"[hello, "world"]"#.to_string()).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_vec_bracket_errors() {
|
||||
assert_eq!(
|
||||
<Vec<String>>::from_arma(r#""hello","bye"]"#.to_string()),
|
||||
Err(FromArmaError::MissingBracket(true))
|
||||
);
|
||||
assert_eq!(
|
||||
<Vec<String>>::from_arma(r#"["hello","bye""#.to_string()),
|
||||
Err(FromArmaError::MissingBracket(false))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_vec_tuple() {
|
||||
assert_eq!(
|
||||
(vec![(String::from("hello"), 123), (String::from("bye"), 321),]),
|
||||
<Vec<(String, i32)>>::from_arma(r#"[["hello", 123],["bye", 321]]"#.to_string())
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_slice() {
|
||||
assert_eq!(
|
||||
vec![String::from("hello"), String::from("bye"),],
|
||||
<[String; 2]>::from_arma(r#"["hello","bye"]"#.to_string()).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_slice_size_errors() {
|
||||
assert_eq!(
|
||||
<[String; 2]>::from_arma(r#"[]"#.to_string()),
|
||||
Err(FromArmaError::InvalidLength {
|
||||
expected: 2,
|
||||
actual: 0
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
<[String; 2]>::from_arma(r#"["hello"]"#.to_string()),
|
||||
Err(FromArmaError::InvalidLength {
|
||||
expected: 2,
|
||||
actual: 1
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
<[String; 2]>::from_arma(r#"["hello","bye","world"]"#.to_string()),
|
||||
Err(FromArmaError::InvalidLength {
|
||||
expected: 2,
|
||||
actual: 3
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_hashmap() {
|
||||
assert_eq!(
|
||||
std::collections::HashMap::from([
|
||||
(String::from("hello"), 123),
|
||||
(String::from("bye"), 321),
|
||||
]),
|
||||
<std::collections::HashMap<String, i32>>::from_arma(
|
||||
r#"[["hello", 123],["bye",321]]"#.to_string()
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
std::collections::HashMap::from([
|
||||
(String::from("hello"), 123),
|
||||
(String::from("bye"), 321),
|
||||
(String::from("hello"), 321),
|
||||
]),
|
||||
<std::collections::HashMap<String, i32>>::from_arma(
|
||||
r#"[["hello", 123],["bye",321],["hello", 321]]"#.to_string()
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_exponential() {
|
||||
assert_eq!(1.0e-10, <f64>::from_arma(r#"1.0e-10"#.to_string()).unwrap());
|
||||
assert_eq!(
|
||||
1_227_700,
|
||||
<u32>::from_arma(r#"1.2277e+006"#.to_string()).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_exponential_errors() {
|
||||
assert_eq!(
|
||||
<f64>::from_arma(r#"e-10"#.to_string()),
|
||||
Err(FromArmaError::InvalidPrimitive(
|
||||
"invalid float literal".to_string()
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
<f64>::from_arma(r#"1.0e"#.to_string()),
|
||||
Err(FromArmaError::InvalidPrimitive(
|
||||
"invalid float literal".to_string()
|
||||
))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
<u32>::from_arma(r#"e-10"#.to_string()),
|
||||
Err(FromArmaError::InvalidPrimitive(
|
||||
"invalid number literal".to_string()
|
||||
))
|
||||
);
|
||||
assert_eq!(
|
||||
<u32>::from_arma(r#"1.0e"#.to_string()),
|
||||
Err(FromArmaError::InvalidPrimitive(
|
||||
"invalid number literal".to_string()
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_value_tuple() {
|
||||
assert_eq!(
|
||||
(
|
||||
Value::String(String::from("hello")),
|
||||
Value::String(String::from("world"))
|
||||
),
|
||||
<(Value, Value)>::from_arma(r#"["hello", "world"]"#.to_string()).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_value_vec() {
|
||||
assert_eq!(
|
||||
vec![
|
||||
Value::String(String::from("hello")),
|
||||
Value::String(String::from("world"))
|
||||
],
|
||||
<Vec<Value>>::from_arma(r#"["hello", "world"]"#.to_string()).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_float() {
|
||||
assert_eq!(1.0, <f64>::from_arma(r#"1.0"#.to_string()).unwrap());
|
||||
assert_eq!(1.0, <f64>::from_arma(r#"1"#.to_string()).unwrap());
|
||||
assert_eq!(1.0, <f64>::from_arma(r#"1.0e+0"#.to_string()).unwrap());
|
||||
assert_eq!(-1.0, <f64>::from_arma(r#"-1.0"#.to_string()).unwrap());
|
||||
assert_eq!(1.0, <f64>::from_arma(r#""1.0""#.to_string()).unwrap());
|
||||
assert_eq!(1.0, <f64>::from_arma(r#""1""#.to_string()).unwrap());
|
||||
assert_eq!(1.0, <f64>::from_arma(r#""1.0e+0""#.to_string()).unwrap());
|
||||
assert_eq!(-1.0, <f64>::from_arma(r#""-1.0""#.to_string()).unwrap());
|
||||
}
|
||||
}
|
||||
506
vendor/arma-rs/src/value/into_arma.rs
vendored
Normal file
506
vendor/arma-rs/src/value/into_arma.rs
vendored
Normal file
@@ -0,0 +1,506 @@
|
||||
use super::Value;
|
||||
|
||||
/// Convert a type to a value that can be sent into Arma
|
||||
pub trait IntoArma {
|
||||
/// Convert a type to a value that can be sent into Arma
|
||||
fn to_arma(&self) -> Value;
|
||||
}
|
||||
|
||||
pub struct DirectReturn(Value);
|
||||
impl Value {
|
||||
/// A workaround to return a value directly to Arma
|
||||
pub fn direct(value: Value) -> DirectReturn {
|
||||
DirectReturn(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoArma for DirectReturn {
|
||||
fn to_arma(&self) -> Value {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoArma for () {
|
||||
fn to_arma(&self) -> Value {
|
||||
Value::Null
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_into_arma_tuple {
|
||||
{ $c: expr, $($t:ident)* } => {
|
||||
seq_macro::seq!(N in 0..$c {
|
||||
impl<$($t),*> IntoArma for ($($t),*)
|
||||
where
|
||||
$($t: IntoArma),*
|
||||
{
|
||||
fn to_arma(&self) -> Value {
|
||||
Value::Array(vec![
|
||||
#(
|
||||
self.N.to_arma(),
|
||||
)*
|
||||
])
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
impl_into_arma_tuple! { 2, A B }
|
||||
impl_into_arma_tuple! { 3, A B C }
|
||||
impl_into_arma_tuple! { 4, A B C D }
|
||||
impl_into_arma_tuple! { 5, A B C D E }
|
||||
impl_into_arma_tuple! { 6, A B C D E F }
|
||||
impl_into_arma_tuple! { 7, A B C D E F G }
|
||||
impl_into_arma_tuple! { 8, A B C D E F G H }
|
||||
impl_into_arma_tuple! { 9, A B C D E F G H I }
|
||||
impl_into_arma_tuple! { 10, A B C D E F G H I J }
|
||||
impl_into_arma_tuple! { 11, A B C D E F G H I J K }
|
||||
impl_into_arma_tuple! { 12, A B C D E F G H I J K L }
|
||||
impl_into_arma_tuple! { 13, A B C D E F G H I J K L M }
|
||||
impl_into_arma_tuple! { 14, A B C D E F G H I J K L M N }
|
||||
impl_into_arma_tuple! { 15, A B C D E F G H I J K L M N O }
|
||||
impl_into_arma_tuple! { 16, A B C D E F G H I J K L M N O P }
|
||||
impl_into_arma_tuple! { 17, A B C D E F G H I J K L M N O P Q }
|
||||
impl_into_arma_tuple! { 18, A B C D E F G H I J K L M N O P Q R }
|
||||
impl_into_arma_tuple! { 19, A B C D E F G H I J K L M N O P Q R S }
|
||||
impl_into_arma_tuple! { 20, A B C D E F G H I J K L M N O P Q R S T }
|
||||
impl_into_arma_tuple! { 21, A B C D E F G H I J K L M N O P Q R S T U }
|
||||
impl_into_arma_tuple! { 22, A B C D E F G H I J K L M N O P Q R S T U V }
|
||||
impl_into_arma_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 }
|
||||
impl_into_arma_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 }
|
||||
impl_into_arma_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 }
|
||||
impl_into_arma_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 }
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn test_tuples() {
|
||||
let a = (1, "two");
|
||||
assert_eq!(
|
||||
Value::Array(vec![Value::Number(1.0), Value::String("two".to_string())]),
|
||||
a.to_arma()
|
||||
);
|
||||
}
|
||||
|
||||
impl IntoArma for Vec<Value> {
|
||||
fn to_arma(&self) -> Value {
|
||||
Value::Array(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoArma for Vec<T>
|
||||
where
|
||||
T: IntoArma,
|
||||
{
|
||||
fn to_arma(&self) -> Value {
|
||||
Value::Array(self.iter().map(IntoArma::to_arma).collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn test_vec() {
|
||||
assert_eq!(String::from("[1,2,3]"), vec![1, 2, 3].to_arma().to_string());
|
||||
assert_eq!(
|
||||
String::from(r#"["hello","world"]"#),
|
||||
vec!["hello", "world"].to_arma().to_string()
|
||||
);
|
||||
}
|
||||
|
||||
impl<T> IntoArma for &[T]
|
||||
where
|
||||
T: IntoArma,
|
||||
{
|
||||
fn to_arma(&self) -> Value {
|
||||
Value::Array(self.iter().map(IntoArma::to_arma).collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn test_slice() {
|
||||
assert_eq!(
|
||||
String::from("[1,2,3]"),
|
||||
vec![1, 2, 3].as_slice().to_arma().to_string()
|
||||
)
|
||||
}
|
||||
|
||||
impl IntoArma for String {
|
||||
fn to_arma(&self) -> Value {
|
||||
Value::String(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn test_string() {
|
||||
assert_eq!(
|
||||
String::from("\"hello\""),
|
||||
String::from("hello").to_arma().to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
String::from(r#""hello ""john"".""#),
|
||||
String::from(r#"hello "john"."#).to_arma().to_string()
|
||||
);
|
||||
}
|
||||
|
||||
impl IntoArma for &'static str {
|
||||
fn to_arma(&self) -> Value {
|
||||
Value::String((*self).to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn test_static_str() {
|
||||
assert_eq!(String::from("\"hello\""), "hello".to_arma().to_string())
|
||||
}
|
||||
|
||||
impl IntoArma for bool {
|
||||
fn to_arma(&self) -> Value {
|
||||
Value::Boolean(*self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn test_bool() {
|
||||
assert_eq!(String::from("true"), true.to_arma().to_string())
|
||||
}
|
||||
|
||||
impl IntoArma for i8 {
|
||||
fn to_arma(&self) -> Value {
|
||||
Value::Number(f64::from(self.to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn test_i8() {
|
||||
assert_eq!(String::from("1"), 1i8.to_arma().to_string())
|
||||
}
|
||||
|
||||
impl IntoArma for i16 {
|
||||
fn to_arma(&self) -> Value {
|
||||
Value::Number(f64::from(self.to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn test_i16() {
|
||||
assert_eq!(String::from("1"), 1i16.to_arma().to_string())
|
||||
}
|
||||
|
||||
impl IntoArma for i32 {
|
||||
fn to_arma(&self) -> Value {
|
||||
Value::Number(f64::from(*self))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn test_i32() {
|
||||
assert_eq!(String::from("1"), 1i32.to_arma().to_string())
|
||||
}
|
||||
|
||||
impl IntoArma for f32 {
|
||||
fn to_arma(&self) -> Value {
|
||||
Value::Number(f64::from(*self))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn test_f32() {
|
||||
assert_eq!(String::from("1"), 1f32.to_arma().to_string());
|
||||
assert_eq!(String::from("1.5"), 1.5f32.to_arma().to_string());
|
||||
}
|
||||
|
||||
impl IntoArma for f64 {
|
||||
fn to_arma(&self) -> Value {
|
||||
Value::Number(*self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn test_f64() {
|
||||
assert_eq!(String::from("1"), 1f64.to_arma().to_string());
|
||||
assert_eq!(String::from("1.5"), 1.5f64.to_arma().to_string());
|
||||
}
|
||||
|
||||
impl IntoArma for u8 {
|
||||
fn to_arma(&self) -> Value {
|
||||
Value::Number(f64::from(self.to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn test_u8() {
|
||||
assert_eq!(String::from("1"), 1u8.to_arma().to_string())
|
||||
}
|
||||
|
||||
impl IntoArma for u16 {
|
||||
fn to_arma(&self) -> Value {
|
||||
Value::Number(f64::from(self.to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn test_u16() {
|
||||
assert_eq!(String::from("1"), 1u16.to_arma().to_string())
|
||||
}
|
||||
|
||||
impl IntoArma for u32 {
|
||||
fn to_arma(&self) -> Value {
|
||||
Value::Number(f64::from(*self))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn test_u32() {
|
||||
assert_eq!(String::from("1"), 1u32.to_arma().to_string())
|
||||
}
|
||||
|
||||
impl<T: IntoArma> IntoArma for Option<T> {
|
||||
fn to_arma(&self) -> Value {
|
||||
match self {
|
||||
Some(v) => v.to_arma(),
|
||||
None => Value::Null,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn test_option() {
|
||||
assert_eq!(String::from("null"), None::<i32>.to_arma().to_string());
|
||||
assert_eq!(String::from("1"), Some(1).to_arma().to_string());
|
||||
}
|
||||
|
||||
impl<K, V, S> IntoArma for std::collections::HashMap<K, V, S>
|
||||
where
|
||||
K: IntoArma,
|
||||
V: IntoArma,
|
||||
S: std::hash::BuildHasher,
|
||||
{
|
||||
fn to_arma(&self) -> Value {
|
||||
self.iter()
|
||||
.map(|(k, v)| vec![k.to_arma(), v.to_arma()])
|
||||
.collect::<Vec<Vec<Value>>>()
|
||||
.to_arma()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, S> IntoArma for std::collections::HashMap<K, Value, S>
|
||||
where
|
||||
K: IntoArma,
|
||||
S: std::hash::BuildHasher,
|
||||
{
|
||||
fn to_arma(&self) -> Value {
|
||||
self.iter()
|
||||
.map(|(k, v)| vec![k.to_arma(), v.clone()])
|
||||
.collect::<Vec<Vec<Value>>>()
|
||||
.to_arma()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[test]
|
||||
fn test_hashmap() {
|
||||
use std::collections::HashMap;
|
||||
{
|
||||
let mut map = HashMap::new();
|
||||
map.insert("key".to_string(), "value".to_string());
|
||||
let map = map.to_arma();
|
||||
assert_eq!(map.to_string(), r#"[["key","value"]]"#.to_string());
|
||||
}
|
||||
{
|
||||
let mut map = HashMap::new();
|
||||
map.insert("key1".to_string(), "value1".to_string());
|
||||
map.insert("key2".to_string(), "value2".to_string());
|
||||
let map = map.to_arma().to_string();
|
||||
assert!(
|
||||
map == r#"[["key1","value1"],["key2","value2"]]"#
|
||||
|| map == r#"[["key2","value2"],["key1","value1"]]"#
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Value {
|
||||
#[must_use]
|
||||
/// Returns an Option representing if the value is null
|
||||
pub const fn as_null(&self) -> Option<()> {
|
||||
match self {
|
||||
Self::Null => Some(()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Checks if the value is a null variant
|
||||
pub const fn is_null(&self) -> bool {
|
||||
self.as_null().is_some()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Returns an Option representing if the value is a number
|
||||
pub const fn as_f64(&self) -> Option<f64> {
|
||||
match *self {
|
||||
Self::Number(n) => Some(n),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Checks if the value is a number
|
||||
pub const fn is_number(&self) -> bool {
|
||||
self.as_f64().is_some()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Returns an Option representing if the value is an array
|
||||
pub const fn as_vec(&self) -> Option<&Vec<Self>> {
|
||||
match *self {
|
||||
Self::Array(ref vec) => Some(vec),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Checks if the value is an array
|
||||
pub const fn is_array(&self) -> bool {
|
||||
self.as_vec().is_some()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Returns an Option representing if the value is a boolean
|
||||
pub const fn as_bool(&self) -> Option<bool> {
|
||||
match *self {
|
||||
Self::Boolean(bool) => Some(bool),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Checks if the value is a boolean
|
||||
pub const fn is_boolean(&self) -> bool {
|
||||
self.as_bool().is_some()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Returns an Option representing if the value is a string
|
||||
pub fn as_str(&self) -> Option<&str> {
|
||||
match *self {
|
||||
Self::String(ref string) => Some(string),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Checks if the value is a string
|
||||
pub fn is_string(&self) -> bool {
|
||||
self.as_str().is_some()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// Checks if the value is empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
Self::Null => true,
|
||||
Self::Number(n) => *n == 0.0,
|
||||
Self::Array(a) => a.is_empty(),
|
||||
Self::Boolean(b) => !*b,
|
||||
Self::String(s) => s.is_empty(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn is_null() {
|
||||
assert!(Value::Null.is_null());
|
||||
assert!(!Value::Boolean(false).is_null());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_number() {
|
||||
assert!(Value::Number(54.0).is_number());
|
||||
assert!(!Value::Boolean(false).is_number());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_array() {
|
||||
assert!(Value::Array(Vec::new()).is_array());
|
||||
assert!(!Value::Boolean(false).is_array());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_boolean() {
|
||||
assert!(Value::Boolean(false).is_boolean());
|
||||
assert!(!Value::Number(54.0).is_boolean());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_string() {
|
||||
assert!(Value::String(String::new()).is_string());
|
||||
assert!(!Value::Boolean(false).is_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn as_nil() {
|
||||
assert!(Value::Null.as_null().is_some())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn as_f32() {
|
||||
let v = Value::Number(54.0).as_f64().unwrap();
|
||||
assert!((54.0 - v) == 0.0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn as_vec() {
|
||||
let array = Value::Array(vec![Value::String("hello".into())]);
|
||||
assert_eq!(array.to_string(), r#"["hello"]"#.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn as_bool() {
|
||||
assert!(Value::Boolean(true).as_bool().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn as_str() {
|
||||
let v = Value::String("hello world".into());
|
||||
let s = v.as_str().unwrap();
|
||||
assert_eq!(s, "hello world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_empty() {
|
||||
assert!(Value::String("".into()).is_empty());
|
||||
assert!(Value::Array(vec![]).is_empty());
|
||||
assert!(Value::Boolean(false).is_empty());
|
||||
assert!(Value::String(String::new()).is_empty());
|
||||
assert!(Value::Number(0.0).is_empty());
|
||||
assert!(Value::Null.is_empty());
|
||||
|
||||
assert!(!Value::String("test".into()).is_empty());
|
||||
assert!(!Value::Array(vec![Value::Boolean(false)]).is_empty());
|
||||
assert!(!Value::Boolean(true).is_empty());
|
||||
assert!(!Value::Number(55.0).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_array() {
|
||||
let array = Value::Array(vec![]);
|
||||
assert_eq!(array.to_string(), r#"[]"#.to_string());
|
||||
}
|
||||
}
|
||||
200
vendor/arma-rs/src/value/loadout/assigned.rs
vendored
Normal file
200
vendor/arma-rs/src/value/loadout/assigned.rs
vendored
Normal file
@@ -0,0 +1,200 @@
|
||||
use crate::{FromArma, FromArmaError, IntoArma, Value};
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
/// Assigned items in the loadout
|
||||
pub struct AssignedItems(String, String, String, String, String, String);
|
||||
impl AssignedItems {
|
||||
/// The class name of the assigned map
|
||||
#[must_use]
|
||||
pub fn map(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Set the class name of the assigned map
|
||||
pub fn set_map(&mut self, map: String) {
|
||||
self.0 = map;
|
||||
}
|
||||
|
||||
/// The class name of the assigned terminal
|
||||
#[must_use]
|
||||
pub fn terminal(&self) -> &str {
|
||||
&self.1
|
||||
}
|
||||
|
||||
/// Set the class name of the assigned terminal
|
||||
pub fn set_terminal(&mut self, terminal: String) {
|
||||
self.1 = terminal;
|
||||
}
|
||||
|
||||
/// The class name of the assigned radio
|
||||
#[must_use]
|
||||
pub fn radio(&self) -> &str {
|
||||
&self.2
|
||||
}
|
||||
|
||||
/// Set the class name of the assigned radio
|
||||
pub fn set_radio(&mut self, radio: String) {
|
||||
self.2 = radio;
|
||||
}
|
||||
|
||||
/// The class name of the assigned compass
|
||||
#[must_use]
|
||||
pub fn compass(&self) -> &str {
|
||||
&self.3
|
||||
}
|
||||
|
||||
/// Set the class name of the assigned compass
|
||||
pub fn set_compass(&mut self, compass: String) {
|
||||
self.3 = compass;
|
||||
}
|
||||
|
||||
/// The class name of the assigned watch
|
||||
#[must_use]
|
||||
pub fn watch(&self) -> &str {
|
||||
&self.4
|
||||
}
|
||||
|
||||
/// Set the class name of the assigned watch
|
||||
pub fn set_watch(&mut self, watch: String) {
|
||||
self.4 = watch;
|
||||
}
|
||||
|
||||
/// The class name of the assigned NVG
|
||||
#[must_use]
|
||||
pub fn nvg(&self) -> &str {
|
||||
&self.5
|
||||
}
|
||||
|
||||
/// Set the class name of the assigned NVG
|
||||
pub fn set_nvg(&mut self, nvg: String) {
|
||||
self.5 = nvg;
|
||||
}
|
||||
|
||||
/// Get all items
|
||||
#[must_use]
|
||||
pub fn classes(&self) -> [&str; 6] {
|
||||
[
|
||||
self.map(),
|
||||
self.terminal(),
|
||||
self.radio(),
|
||||
self.compass(),
|
||||
self.watch(),
|
||||
self.nvg(),
|
||||
]
|
||||
}
|
||||
|
||||
#[deprecated(note = "Use `classes` instead")]
|
||||
#[must_use]
|
||||
/// Get all items
|
||||
pub fn items(&self) -> [&str; 6] {
|
||||
self.classes()
|
||||
}
|
||||
}
|
||||
impl FromArma for AssignedItems {
|
||||
fn from_arma(s: String) -> Result<Self, FromArmaError> {
|
||||
<(String, String, String, String, String, String)>::from_arma(s).map(
|
||||
|(map, gps, radio, compass, watch, nvg)| Self(map, gps, radio, compass, watch, nvg),
|
||||
)
|
||||
}
|
||||
}
|
||||
impl IntoArma for AssignedItems {
|
||||
fn to_arma(&self) -> Value {
|
||||
Value::Array(vec![
|
||||
Value::String(self.map().to_owned()),
|
||||
Value::String(self.terminal().to_owned()),
|
||||
Value::String(self.radio().to_owned()),
|
||||
Value::String(self.compass().to_owned()),
|
||||
Value::String(self.watch().to_owned()),
|
||||
Value::String(self.nvg().to_owned()),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::AssignedItems;
|
||||
use crate::{FromArma, IntoArma, Value};
|
||||
|
||||
#[test]
|
||||
fn map() {
|
||||
let mut assigned = AssignedItems::default();
|
||||
assert_eq!(assigned.map(), "");
|
||||
assigned.set_map("map".to_owned());
|
||||
assert_eq!(assigned.map(), "map");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal() {
|
||||
let mut assigned = AssignedItems::default();
|
||||
assert_eq!(assigned.terminal(), "");
|
||||
assigned.set_terminal("terminal".to_owned());
|
||||
assert_eq!(assigned.terminal(), "terminal");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn radio() {
|
||||
let mut assigned = AssignedItems::default();
|
||||
assert_eq!(assigned.radio(), "");
|
||||
assigned.set_radio("radio".to_owned());
|
||||
assert_eq!(assigned.radio(), "radio");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compass() {
|
||||
let mut assigned = AssignedItems::default();
|
||||
assert_eq!(assigned.compass(), "");
|
||||
assigned.set_compass("compass".to_owned());
|
||||
assert_eq!(assigned.compass(), "compass");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn watch() {
|
||||
let mut assigned = AssignedItems::default();
|
||||
assert_eq!(assigned.watch(), "");
|
||||
assigned.set_watch("watch".to_owned());
|
||||
assert_eq!(assigned.watch(), "watch");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nvg() {
|
||||
let mut assigned = AssignedItems::default();
|
||||
assert_eq!(assigned.nvg(), "");
|
||||
assigned.set_nvg("nvg".to_owned());
|
||||
assert_eq!(assigned.nvg(), "nvg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_arma() {
|
||||
let s = "[\"map\",\"terminal\",\"radio\",\"compass\",\"watch\",\"nvg\"]";
|
||||
let assigned = AssignedItems::from_arma(s.to_owned()).unwrap();
|
||||
assert_eq!(assigned.map(), "map");
|
||||
assert_eq!(assigned.terminal(), "terminal");
|
||||
assert_eq!(assigned.radio(), "radio");
|
||||
assert_eq!(assigned.compass(), "compass");
|
||||
assert_eq!(assigned.watch(), "watch");
|
||||
assert_eq!(assigned.nvg(), "nvg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_arma() {
|
||||
let mut assigned = AssignedItems::default();
|
||||
assigned.set_map("map".to_owned());
|
||||
assigned.set_terminal("terminal".to_owned());
|
||||
assigned.set_radio("radio".to_owned());
|
||||
assigned.set_compass("compass".to_owned());
|
||||
assigned.set_watch("watch".to_owned());
|
||||
assigned.set_nvg("nvg".to_owned());
|
||||
let s = assigned.to_arma();
|
||||
assert_eq!(
|
||||
s,
|
||||
Value::Array(vec![
|
||||
Value::String("map".to_owned()),
|
||||
Value::String("terminal".to_owned()),
|
||||
Value::String("radio".to_owned()),
|
||||
Value::String("compass".to_owned()),
|
||||
Value::String("watch".to_owned()),
|
||||
Value::String("nvg".to_owned()),
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
183
vendor/arma-rs/src/value/loadout/container.rs
vendored
Normal file
183
vendor/arma-rs/src/value/loadout/container.rs
vendored
Normal file
@@ -0,0 +1,183 @@
|
||||
use crate::{FromArma, FromArmaError, IntoArma, Value};
|
||||
|
||||
use super::InventoryItem;
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
/// A uniform, vest, or backpack
|
||||
pub struct Container(Option<(String, Vec<InventoryItem>)>);
|
||||
impl Container {
|
||||
/// Create a new container
|
||||
#[must_use]
|
||||
pub const fn new(class: String) -> Self {
|
||||
Self(Some((class, vec![])))
|
||||
}
|
||||
|
||||
/// The container exists
|
||||
#[must_use]
|
||||
pub const fn exists(&self) -> bool {
|
||||
self.0.is_some()
|
||||
}
|
||||
|
||||
/// The class name of the container
|
||||
#[must_use]
|
||||
pub fn class(&self) -> Option<&str> {
|
||||
self.0.as_ref().map(|(class, _)| class.as_str())
|
||||
}
|
||||
|
||||
/// Set the class name of the container
|
||||
pub fn set_class(&mut self, class: String) {
|
||||
if let Some(container) = self.0.as_mut() {
|
||||
container.0 = class;
|
||||
} else {
|
||||
self.0 = Some((class, vec![]));
|
||||
}
|
||||
}
|
||||
|
||||
/// The items in the container
|
||||
#[must_use]
|
||||
pub fn items(&self) -> Option<&Vec<InventoryItem>> {
|
||||
self.0.as_ref().map(|(_, items)| items)
|
||||
}
|
||||
|
||||
/// The items in the container
|
||||
pub fn items_mut(&mut self) -> Option<&mut Vec<InventoryItem>> {
|
||||
self.0.as_mut().map(|(_, items)| items)
|
||||
}
|
||||
|
||||
/// Get all classes and their quantities, including the container itself
|
||||
#[must_use]
|
||||
pub fn classes(&self) -> Vec<(String, u32)> {
|
||||
let mut classes = vec![];
|
||||
if let Some((class, items)) = &self.0 {
|
||||
classes.push((class.clone(), 1));
|
||||
for item in items {
|
||||
classes.push((item.class().to_string(), item.count()));
|
||||
}
|
||||
}
|
||||
classes
|
||||
}
|
||||
}
|
||||
impl FromArma for Container {
|
||||
fn from_arma(s: String) -> Result<Self, FromArmaError> {
|
||||
if s == "[]" {
|
||||
return Ok(Self(None));
|
||||
}
|
||||
<(String, Vec<InventoryItem>)>::from_arma(s).map(|(name, items)| Self(Some((name, items))))
|
||||
}
|
||||
}
|
||||
impl IntoArma for Container {
|
||||
fn to_arma(&self) -> Value {
|
||||
self.0.as_ref().map_or_else(
|
||||
|| Value::Array(vec![]),
|
||||
|container| {
|
||||
Value::Array(vec![
|
||||
Value::String(container.0.clone()),
|
||||
Value::Array(container.1.iter().map(IntoArma::to_arma).collect()),
|
||||
])
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{loadout::InventoryItem, FromArma, IntoArma, Value};
|
||||
|
||||
use super::Container;
|
||||
|
||||
#[test]
|
||||
fn exists() {
|
||||
let container = Container::default();
|
||||
assert!(!container.exists());
|
||||
let container = Container::new("container".to_string());
|
||||
assert!(container.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class() {
|
||||
let container = Container::default();
|
||||
assert!(container.class().is_none());
|
||||
let mut container = Container::new("container".to_string());
|
||||
assert_eq!(container.class().unwrap(), "container");
|
||||
container.set_class("container2".to_string());
|
||||
assert_eq!(container.class().unwrap(), "container2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn items() {
|
||||
let container = Container::default();
|
||||
assert!(container.items().is_none());
|
||||
let mut container = Container::new("container".to_string());
|
||||
assert!(container.items().is_some());
|
||||
let items = vec![
|
||||
InventoryItem::new_item("item1".to_string(), 1),
|
||||
InventoryItem::new_item("item2".to_string(), 2),
|
||||
];
|
||||
container.0.as_mut().unwrap().1 = items.clone();
|
||||
assert_eq!(container.items().unwrap(), &items);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_arma() {
|
||||
let container = Container::from_arma("[]".to_string()).unwrap();
|
||||
assert!(!container.exists());
|
||||
let container =
|
||||
Container::from_arma("[\"container\",[[\"item1\",1],[\"item2\",2]]]".to_string())
|
||||
.unwrap();
|
||||
assert!(container.exists());
|
||||
assert_eq!(container.class().unwrap(), "container");
|
||||
assert_eq!(
|
||||
container.items().unwrap(),
|
||||
&vec![
|
||||
InventoryItem::new_item("item1".to_string(), 1),
|
||||
InventoryItem::new_item("item2".to_string(), 2),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_arma() {
|
||||
let container = Container::default();
|
||||
assert_eq!(container.to_arma(), Value::Array(vec![]));
|
||||
let mut container = Container::new("container".to_string());
|
||||
assert_eq!(
|
||||
container.to_arma(),
|
||||
Value::Array(vec![
|
||||
Value::String("container".to_string()),
|
||||
Value::Array(vec![]),
|
||||
])
|
||||
);
|
||||
let items = vec![
|
||||
InventoryItem::new_item("item1".to_string(), 1),
|
||||
InventoryItem::new_item("item2".to_string(), 2),
|
||||
];
|
||||
container.0.as_mut().unwrap().1 = items;
|
||||
assert_eq!(
|
||||
container.to_arma(),
|
||||
Value::Array(vec![
|
||||
Value::String("container".to_string()),
|
||||
Value::Array(vec![
|
||||
Value::Array(vec![Value::String("item1".to_string()), Value::Number(1.0),]),
|
||||
Value::Array(vec![Value::String("item2".to_string()), Value::Number(2.0),]),
|
||||
]),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classes() {
|
||||
let container = Container::from_arma("[]".to_string()).unwrap();
|
||||
assert!(!container.exists());
|
||||
let container =
|
||||
Container::from_arma("[\"container\",[[\"item1\",1],[\"item2\",2]]]".to_string())
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
container.classes(),
|
||||
vec![
|
||||
("container".to_string(), 1),
|
||||
("item1".to_string(), 1),
|
||||
("item2".to_string(), 2)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
79
vendor/arma-rs/src/value/loadout/extended.rs
vendored
Normal file
79
vendor/arma-rs/src/value/loadout/extended.rs
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{FromArma, FromArmaError, Value};
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
/// CBA Extended Loadout
|
||||
///
|
||||
/// <https://github.com/CBATeam/CBA_A3/pull/1503>
|
||||
pub struct CBAExtended(Option<HashMap<String, Value>>);
|
||||
|
||||
impl CBAExtended {
|
||||
/// Create a new CBA Extended Loadout Array
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// The map has no data or does not exist
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.0.is_none() || self.0.as_ref().unwrap().is_empty()
|
||||
}
|
||||
|
||||
/// Get a value from the map
|
||||
pub fn get(&self, key: &str) -> Option<&Value> {
|
||||
self.0.as_ref().and_then(|map| map.get(key))
|
||||
}
|
||||
|
||||
/// Get a mutable value from the map
|
||||
pub fn get_mut(&mut self, key: &str) -> Option<&mut Value> {
|
||||
self.0.as_mut().and_then(|map| map.get_mut(key))
|
||||
}
|
||||
|
||||
/// Insert a value into the map
|
||||
pub fn insert(&mut self, key: String, value: Value) -> Option<Value> {
|
||||
self.0.get_or_insert_with(HashMap::new).insert(key, value)
|
||||
}
|
||||
|
||||
/// Remove a value from the map
|
||||
pub fn remove(&mut self, key: &str) -> Option<Value> {
|
||||
self.0.as_mut().and_then(|map| map.remove(key))
|
||||
}
|
||||
|
||||
/// Iterate over the keys in the map
|
||||
pub fn values(&self) -> impl Iterator<Item = &Value> {
|
||||
self.0.iter().flat_map(|map| map.values())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromArma for CBAExtended {
|
||||
fn from_arma(s: String) -> Result<Self, FromArmaError> {
|
||||
let value = <Vec<(String, Value)>>::from_arma(s);
|
||||
match value {
|
||||
Ok(value) => Ok(Self(Some(value.into_iter().collect()))),
|
||||
Err(_) => Ok(Self(None)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_from_arma() {
|
||||
let value = CBAExtended::from_arma("[]".to_string());
|
||||
assert!(value.is_ok());
|
||||
assert_eq!(value.unwrap(), CBAExtended(Some(HashMap::new())));
|
||||
|
||||
let value = CBAExtended::from_arma("[[\"cba_xeh_enabled\",true]]".to_string());
|
||||
assert!(value.is_ok());
|
||||
assert_eq!(
|
||||
value.unwrap(),
|
||||
CBAExtended(Some(
|
||||
vec![("cba_xeh_enabled".to_string(), Value::Boolean(true))]
|
||||
.into_iter()
|
||||
.collect()
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
187
vendor/arma-rs/src/value/loadout/inventory_item.rs
vendored
Normal file
187
vendor/arma-rs/src/value/loadout/inventory_item.rs
vendored
Normal file
@@ -0,0 +1,187 @@
|
||||
use crate::{FromArma, FromArmaError, IntoArma, Value};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// An item stored in a uniform, vest, or backpack
|
||||
pub enum InventoryItem {
|
||||
/// An item that is not a magazine
|
||||
Item(String, u32),
|
||||
/// A magazine
|
||||
Magazine(String, u32, u32),
|
||||
}
|
||||
impl InventoryItem {
|
||||
/// Create a new item
|
||||
#[must_use]
|
||||
pub const fn new_item(class: String, count: u32) -> Self {
|
||||
Self::Item(class, count)
|
||||
}
|
||||
|
||||
/// Create a new magazine
|
||||
#[must_use]
|
||||
pub const fn new_magazine(class: String, count: u32, ammo: u32) -> Self {
|
||||
Self::Magazine(class, count, ammo)
|
||||
}
|
||||
|
||||
/// The item is a magazine
|
||||
#[must_use]
|
||||
pub const fn is_magazine(&self) -> bool {
|
||||
matches!(self, Self::Magazine(_, _, _))
|
||||
}
|
||||
|
||||
/// The class name of the item
|
||||
#[must_use]
|
||||
pub fn class(&self) -> &str {
|
||||
match self {
|
||||
Self::Item(c, _) | Self::Magazine(c, _, _) => c.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the class name of the item
|
||||
pub fn set_class(&mut self, class: String) {
|
||||
match self {
|
||||
Self::Item(c, _) | Self::Magazine(c, _, _) => *c = class,
|
||||
}
|
||||
}
|
||||
|
||||
/// The amount of the item
|
||||
#[must_use]
|
||||
pub fn count(&self) -> u32 {
|
||||
match self {
|
||||
Self::Item(_, c) | Self::Magazine(_, c, _) => c.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the amount of the item
|
||||
pub fn set_count(&mut self, count: u32) {
|
||||
match self {
|
||||
Self::Item(_, c) | Self::Magazine(_, c, _) => *c = count,
|
||||
}
|
||||
}
|
||||
|
||||
/// The amount of ammo in the magazine
|
||||
#[must_use]
|
||||
pub fn ammo(&self) -> Option<u32> {
|
||||
match self {
|
||||
Self::Item(..) => None,
|
||||
Self::Magazine(_, _, a) => Some(a.to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the amount of ammo in the magazine
|
||||
/// Returns true if the ammo was set, false if the item is not a magazine
|
||||
pub fn set_ammo(&mut self, ammo: u32) -> bool {
|
||||
match self {
|
||||
Self::Magazine(_, _, a) => {
|
||||
*a = ammo;
|
||||
true
|
||||
}
|
||||
Self::Item(..) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl FromArma for InventoryItem {
|
||||
fn from_arma(s: String) -> Result<Self, FromArmaError> {
|
||||
let commas = s.matches(',').count();
|
||||
match commas {
|
||||
1 => <(String, u32)>::from_arma(s).map(|(name, count)| Self::Item(name, count)),
|
||||
2 => <(String, u32, u32)>::from_arma(s)
|
||||
.map(|(name, count, ammo)| Self::Magazine(name, count, ammo)),
|
||||
_ => Err(FromArmaError::custom(format!(
|
||||
"Invalid inventory item: {s}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl IntoArma for InventoryItem {
|
||||
fn to_arma(&self) -> Value {
|
||||
match self {
|
||||
Self::Item(name, count) => Value::Array(vec![
|
||||
Value::String(name.clone()),
|
||||
Value::Number(f64::from(*count)),
|
||||
]),
|
||||
Self::Magazine(name, count, ammo) => Value::Array(vec![
|
||||
Value::String(name.clone()),
|
||||
Value::Number(f64::from(*count)),
|
||||
Value::Number(f64::from(*ammo)),
|
||||
]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{FromArma, IntoArma, Value};
|
||||
|
||||
use super::InventoryItem;
|
||||
|
||||
#[test]
|
||||
fn is() {
|
||||
let item = InventoryItem::new_item("test".to_owned(), 1);
|
||||
assert_eq!(item.class(), "test");
|
||||
assert_eq!(item.count(), 1);
|
||||
assert_eq!(item.ammo(), None);
|
||||
assert!(!item.is_magazine());
|
||||
let item = InventoryItem::new_magazine("test".to_owned(), 1, 1);
|
||||
assert_eq!(item.class(), "test");
|
||||
assert_eq!(item.count(), 1);
|
||||
assert_eq!(item.ammo(), Some(1));
|
||||
assert!(item.is_magazine());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class() {
|
||||
let mut item = InventoryItem::new_item("test".to_owned(), 1);
|
||||
assert_eq!(item.class(), "test");
|
||||
item.set_class("test2".to_owned());
|
||||
assert_eq!(item.class(), "test2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn count() {
|
||||
let mut item = InventoryItem::new_item("test".to_owned(), 1);
|
||||
assert_eq!(item.count(), 1);
|
||||
item.set_count(2);
|
||||
assert_eq!(item.count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ammo() {
|
||||
let item = InventoryItem::new_magazine("test".to_owned(), 1, 1);
|
||||
assert_eq!(item.ammo(), Some(1));
|
||||
assert!(item.is_magazine());
|
||||
let item = InventoryItem::new_item("test".to_owned(), 1);
|
||||
assert_eq!(item.ammo(), None);
|
||||
assert!(!item.is_magazine());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_arma() {
|
||||
let item = InventoryItem::from_arma("[\"test\",1]".to_owned()).unwrap();
|
||||
assert_eq!(item.class(), "test");
|
||||
assert_eq!(item.count(), 1);
|
||||
assert_eq!(item.ammo(), None);
|
||||
assert!(!item.is_magazine());
|
||||
let item = InventoryItem::from_arma("[\"test\",1,1]".to_owned()).unwrap();
|
||||
assert_eq!(item.class(), "test");
|
||||
assert_eq!(item.count(), 1);
|
||||
assert_eq!(item.ammo(), Some(1));
|
||||
assert!(item.is_magazine());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_arma() {
|
||||
let item = InventoryItem::new_item("test".to_owned(), 1);
|
||||
assert_eq!(
|
||||
item.to_arma(),
|
||||
Value::Array(vec![Value::String("test".to_owned()), Value::Number(1.0),])
|
||||
);
|
||||
let item = InventoryItem::new_magazine("test".to_owned(), 1, 1);
|
||||
assert_eq!(
|
||||
item.to_arma(),
|
||||
Value::Array(vec![
|
||||
Value::String("test".to_owned()),
|
||||
Value::Number(1.0),
|
||||
Value::Number(1.0),
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
135
vendor/arma-rs/src/value/loadout/magazine.rs
vendored
Normal file
135
vendor/arma-rs/src/value/loadout/magazine.rs
vendored
Normal file
@@ -0,0 +1,135 @@
|
||||
use crate::{FromArma, FromArmaError, IntoArma, Value};
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
/// A magazine loaded into a weapon
|
||||
pub struct Magazine(Option<(String, u32)>);
|
||||
impl Magazine {
|
||||
/// Create a new magazine
|
||||
#[must_use]
|
||||
pub const fn new(class: String, count: u32) -> Self {
|
||||
Self(Some((class, count)))
|
||||
}
|
||||
|
||||
/// The magazine exists
|
||||
#[must_use]
|
||||
pub const fn exists(&self) -> bool {
|
||||
self.0.is_some()
|
||||
}
|
||||
|
||||
/// Arma class name of the magazine
|
||||
#[must_use]
|
||||
pub fn class(&self) -> Option<&str> {
|
||||
self.0.as_ref().map(|(c, _)| c.as_str())
|
||||
}
|
||||
|
||||
/// Set the class name of the magazine
|
||||
pub fn set_class(&mut self, class: &str) {
|
||||
if let Some(magazine) = self.0.as_mut() {
|
||||
magazine.0 = class.to_string();
|
||||
} else {
|
||||
self.0 = Some((class.to_string(), 0));
|
||||
}
|
||||
}
|
||||
|
||||
/// The remaining ammo in the magazine
|
||||
#[must_use]
|
||||
pub fn ammo(&self) -> Option<u32> {
|
||||
self.0.as_ref().map(|(_, a)| a.to_owned())
|
||||
}
|
||||
|
||||
/// Set the remaining ammo in the magazine
|
||||
/// Returns true if the ammo was set, false if the magazine was not initialized
|
||||
pub fn set_ammo(&mut self, ammo: u32) -> bool {
|
||||
if let Some(magazine) = self.0.as_mut() {
|
||||
magazine.1 = ammo;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
impl FromArma for Magazine {
|
||||
fn from_arma(s: String) -> Result<Self, FromArmaError> {
|
||||
if s == "[]" {
|
||||
return Ok(Self(None));
|
||||
}
|
||||
<(String, u32)>::from_arma(s).map(|(name, count)| Self(Some((name, count))))
|
||||
}
|
||||
}
|
||||
impl IntoArma for Magazine {
|
||||
fn to_arma(&self) -> Value {
|
||||
self.0.as_ref().map_or_else(
|
||||
|| Value::Array(vec![]),
|
||||
|magazine| {
|
||||
Value::Array(vec![
|
||||
Value::String(magazine.0.clone()),
|
||||
Value::Number(f64::from(magazine.1)),
|
||||
])
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Magazine;
|
||||
use crate::{FromArma, IntoArma, Value};
|
||||
|
||||
#[test]
|
||||
fn test_magazine() {
|
||||
let mut magazine = Magazine::new("ACE_M84".to_string(), 10);
|
||||
assert!(magazine.exists());
|
||||
assert_eq!(magazine.class(), Some("ACE_M84"));
|
||||
assert_eq!(magazine.ammo(), Some(10));
|
||||
magazine.set_class("ACE_M84_HEDP");
|
||||
assert_eq!(magazine.class(), Some("ACE_M84_HEDP"));
|
||||
magazine.set_ammo(20);
|
||||
assert_eq!(magazine.ammo(), Some(20));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exists() {
|
||||
let magazine = Magazine::default();
|
||||
assert!(!magazine.exists());
|
||||
let magazine = Magazine::new("ACE_M84".to_string(), 10);
|
||||
assert!(magazine.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class() {
|
||||
let mut magazine = Magazine::new("ACE_M84".to_string(), 10);
|
||||
assert_eq!(magazine.class(), Some("ACE_M84"));
|
||||
magazine.set_class("ACE_M84_HEDP");
|
||||
assert_eq!(magazine.class(), Some("ACE_M84_HEDP"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ammo() {
|
||||
let mut magazine = Magazine::new("ACE_M84".to_string(), 10);
|
||||
assert_eq!(magazine.ammo(), Some(10));
|
||||
magazine.set_ammo(20);
|
||||
assert_eq!(magazine.ammo(), Some(20));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_arma() {
|
||||
let magazine = Magazine::from_arma("[]".to_string()).unwrap();
|
||||
assert!(!magazine.exists());
|
||||
let magazine = Magazine::from_arma("[\"ACE_M84\", 10]".to_string()).unwrap();
|
||||
assert!(magazine.exists());
|
||||
assert_eq!(magazine.class(), Some("ACE_M84"));
|
||||
assert_eq!(magazine.ammo(), Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_arma() {
|
||||
let magazine = Magazine::new("ACE_M84".to_string(), 10);
|
||||
assert_eq!(
|
||||
magazine.to_arma(),
|
||||
Value::Array(vec![
|
||||
Value::String("ACE_M84".to_string()),
|
||||
Value::Number(10.0),
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
455
vendor/arma-rs/src/value/loadout/mod.rs
vendored
Normal file
455
vendor/arma-rs/src/value/loadout/mod.rs
vendored
Normal file
@@ -0,0 +1,455 @@
|
||||
//! For working with Arma's unit loadout array
|
||||
|
||||
use crate::{FromArma, FromArmaError, IntoArma, Value};
|
||||
|
||||
mod assigned;
|
||||
mod container;
|
||||
mod extended;
|
||||
mod inventory_item;
|
||||
mod magazine;
|
||||
mod weapon;
|
||||
|
||||
pub use assigned::AssignedItems;
|
||||
pub use container::Container;
|
||||
pub use extended::CBAExtended;
|
||||
pub use inventory_item::InventoryItem;
|
||||
pub use magazine::Magazine;
|
||||
pub use weapon::Weapon;
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
/// Arma Unit Loadout Array
|
||||
pub struct Loadout(
|
||||
Weapon,
|
||||
Weapon,
|
||||
Weapon,
|
||||
Container,
|
||||
Container,
|
||||
Container,
|
||||
String,
|
||||
String,
|
||||
Weapon,
|
||||
AssignedItems,
|
||||
CBAExtended,
|
||||
);
|
||||
|
||||
impl Loadout {
|
||||
/// Get the primary weapon
|
||||
#[must_use]
|
||||
pub const fn primary(&self) -> &Weapon {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Get the primary weapon mutably
|
||||
pub fn primary_mut(&mut self) -> &mut Weapon {
|
||||
&mut self.0
|
||||
}
|
||||
|
||||
/// Set the primary weapon
|
||||
pub fn set_primary(&mut self, primary: Weapon) {
|
||||
self.0 = primary;
|
||||
}
|
||||
|
||||
/// Get the secondary weapon (launcher)
|
||||
#[must_use]
|
||||
pub const fn secondary(&self) -> &Weapon {
|
||||
&self.1
|
||||
}
|
||||
|
||||
/// Get the secondary weapon (launcher) mutably
|
||||
pub fn secondary_mut(&mut self) -> &mut Weapon {
|
||||
&mut self.1
|
||||
}
|
||||
|
||||
/// Set the secondary weapon (launcher)
|
||||
pub fn set_secondary(&mut self, secondary: Weapon) {
|
||||
self.1 = secondary;
|
||||
}
|
||||
|
||||
/// Get the handgun weapon
|
||||
#[must_use]
|
||||
pub const fn handgun(&self) -> &Weapon {
|
||||
&self.2
|
||||
}
|
||||
|
||||
/// Get the handgun weapon mutably
|
||||
pub fn handgun_mut(&mut self) -> &mut Weapon {
|
||||
&mut self.2
|
||||
}
|
||||
|
||||
/// Set the handgun weapon
|
||||
pub fn set_handgun(&mut self, handgun: Weapon) {
|
||||
self.2 = handgun;
|
||||
}
|
||||
|
||||
/// Get the uniform
|
||||
#[must_use]
|
||||
pub const fn uniform(&self) -> &Container {
|
||||
&self.3
|
||||
}
|
||||
|
||||
/// Get the uniform mutably
|
||||
pub fn uniform_mut(&mut self) -> &mut Container {
|
||||
&mut self.3
|
||||
}
|
||||
|
||||
/// Set the uniform
|
||||
pub fn set_uniform(&mut self, uniform: Container) {
|
||||
self.3 = uniform;
|
||||
}
|
||||
|
||||
/// Get the vest
|
||||
#[must_use]
|
||||
pub const fn vest(&self) -> &Container {
|
||||
&self.4
|
||||
}
|
||||
|
||||
/// Get the vest mutably
|
||||
pub fn vest_mut(&mut self) -> &mut Container {
|
||||
&mut self.4
|
||||
}
|
||||
|
||||
/// Set the vest
|
||||
pub fn set_vest(&mut self, vest: Container) {
|
||||
self.4 = vest;
|
||||
}
|
||||
|
||||
/// Get the backpack
|
||||
#[must_use]
|
||||
pub const fn backpack(&self) -> &Container {
|
||||
&self.5
|
||||
}
|
||||
|
||||
/// Get the backpack mutably
|
||||
pub fn backpack_mut(&mut self) -> &mut Container {
|
||||
&mut self.5
|
||||
}
|
||||
|
||||
/// Set the backpack
|
||||
pub fn set_backpack(&mut self, backpack: Container) {
|
||||
self.5 = backpack;
|
||||
}
|
||||
|
||||
/// The class name of the current headgear
|
||||
#[must_use]
|
||||
pub fn headgear(&self) -> &str {
|
||||
&self.6
|
||||
}
|
||||
|
||||
/// Set the class name of the current headgear
|
||||
pub fn set_headgear(&mut self, headgear: String) {
|
||||
self.6 = headgear;
|
||||
}
|
||||
|
||||
/// The class name of the current goggles / facewear
|
||||
#[must_use]
|
||||
pub fn goggles(&self) -> &str {
|
||||
&self.7
|
||||
}
|
||||
|
||||
/// Set the class name of the current goggles / facewear
|
||||
pub fn set_goggles(&mut self, goggles: String) {
|
||||
self.7 = goggles;
|
||||
}
|
||||
|
||||
/// Get the binocular
|
||||
#[must_use]
|
||||
pub const fn binoculars(&self) -> &Weapon {
|
||||
&self.8
|
||||
}
|
||||
|
||||
/// Get the binocular mutably
|
||||
pub fn binoculars_mut(&mut self) -> &mut Weapon {
|
||||
&mut self.8
|
||||
}
|
||||
|
||||
/// Set the binocular
|
||||
pub fn set_binoculars(&mut self, binoculars: Weapon) {
|
||||
self.8 = binoculars;
|
||||
}
|
||||
|
||||
/// Get the assigned items
|
||||
#[must_use]
|
||||
pub const fn assigned_items(&self) -> &AssignedItems {
|
||||
&self.9
|
||||
}
|
||||
|
||||
/// Get the assigned items mutably
|
||||
pub fn assigned_items_mut(&mut self) -> &mut AssignedItems {
|
||||
&mut self.9
|
||||
}
|
||||
|
||||
/// Set the assigned items
|
||||
pub fn set_assigned_items(&mut self, assigned_items: AssignedItems) {
|
||||
self.9 = assigned_items;
|
||||
}
|
||||
|
||||
/// Get the CBA Extended Loadout Array
|
||||
pub fn cba_extended(&self) -> &CBAExtended {
|
||||
&self.10
|
||||
}
|
||||
|
||||
/// Get a map of all items in the loadout and their quantities
|
||||
pub fn classes(&self) -> std::collections::HashMap<String, u32> {
|
||||
let mut items = std::collections::HashMap::new();
|
||||
self.0.classes().iter().for_each(|c| {
|
||||
*items.entry(c.to_string()).or_insert(0) += 1;
|
||||
});
|
||||
self.1.classes().iter().for_each(|c| {
|
||||
*items.entry(c.to_string()).or_insert(0) += 1;
|
||||
});
|
||||
self.2.classes().iter().for_each(|c| {
|
||||
*items.entry(c.to_string()).or_insert(0) += 1;
|
||||
});
|
||||
self.3.classes().iter().for_each(|(c, q)| {
|
||||
*items.entry(c.clone()).or_insert(0) += q;
|
||||
});
|
||||
self.4.classes().iter().for_each(|(c, q)| {
|
||||
*items.entry(c.clone()).or_insert(0) += q;
|
||||
});
|
||||
self.5.classes().iter().for_each(|(c, q)| {
|
||||
*items.entry(c.clone()).or_insert(0) += q;
|
||||
});
|
||||
*items.entry(self.6.clone()).or_insert(0) += 1;
|
||||
*items.entry(self.7.clone()).or_insert(0) += 1;
|
||||
self.8.classes().iter().for_each(|c| {
|
||||
*items.entry(c.to_string()).or_insert(0) += 1;
|
||||
});
|
||||
self.9.classes().iter().for_each(|c| {
|
||||
*items.entry(c.to_string()).or_insert(0) += 1;
|
||||
});
|
||||
items.remove("");
|
||||
items
|
||||
}
|
||||
}
|
||||
|
||||
impl FromArma for Loadout {
|
||||
fn from_arma(s: String) -> Result<Self, FromArmaError> {
|
||||
let vanilla = <(
|
||||
Weapon,
|
||||
Weapon,
|
||||
Weapon,
|
||||
Container,
|
||||
Container,
|
||||
Container,
|
||||
String,
|
||||
String,
|
||||
Weapon,
|
||||
AssignedItems,
|
||||
)>::from_arma(s.clone())
|
||||
.map(
|
||||
|(
|
||||
primary,
|
||||
secondary,
|
||||
handgun,
|
||||
uniform,
|
||||
vest,
|
||||
backpack,
|
||||
headgear,
|
||||
goggles,
|
||||
binoculars,
|
||||
linked_items,
|
||||
)| {
|
||||
Self(
|
||||
primary,
|
||||
secondary,
|
||||
handgun,
|
||||
uniform,
|
||||
vest,
|
||||
backpack,
|
||||
headgear,
|
||||
goggles,
|
||||
binoculars,
|
||||
linked_items,
|
||||
CBAExtended::default(),
|
||||
)
|
||||
},
|
||||
);
|
||||
if vanilla.is_err() {
|
||||
return <(
|
||||
(
|
||||
Weapon,
|
||||
Weapon,
|
||||
Weapon,
|
||||
Container,
|
||||
Container,
|
||||
Container,
|
||||
String,
|
||||
String,
|
||||
Weapon,
|
||||
AssignedItems,
|
||||
),
|
||||
CBAExtended,
|
||||
)>::from_arma(s)
|
||||
.map(
|
||||
|(
|
||||
(
|
||||
primary,
|
||||
secondary,
|
||||
handgun,
|
||||
uniform,
|
||||
vest,
|
||||
backpack,
|
||||
headgear,
|
||||
goggles,
|
||||
binoculars,
|
||||
linked_items,
|
||||
),
|
||||
extended,
|
||||
)| {
|
||||
Self(
|
||||
primary,
|
||||
secondary,
|
||||
handgun,
|
||||
uniform,
|
||||
vest,
|
||||
backpack,
|
||||
headgear,
|
||||
goggles,
|
||||
binoculars,
|
||||
linked_items,
|
||||
extended,
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
vanilla
|
||||
}
|
||||
}
|
||||
impl IntoArma for Loadout {
|
||||
fn to_arma(&self) -> Value {
|
||||
Value::Array(vec![
|
||||
self.primary().to_arma(),
|
||||
self.secondary().to_arma(),
|
||||
self.handgun().to_arma(),
|
||||
self.uniform().to_arma(),
|
||||
self.vest().to_arma(),
|
||||
self.backpack().to_arma(),
|
||||
Value::String(self.headgear().to_owned()),
|
||||
Value::String(self.goggles().to_owned()),
|
||||
self.binoculars().to_arma(),
|
||||
self.assigned_items().to_arma(),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn b_soldier_at_f() {
|
||||
let loadout = r#"[["arifle_MXC_Holo_pointer_F", "", "acc_pointer_IR", "optic_Holosight", ["30Rnd_65x39_caseless_mag", 30], [], ""],
|
||||
["launch_B_Titan_short_F", "", "", "", ["Titan_AT", 1], [], ""],
|
||||
["hgun_P07_F", "", "", "", ["16Rnd_9x21_Mag", 16], [], ""],
|
||||
["U_B_CombatUniform_mcam", [["FirstAidKit", 1], ["30Rnd_65x39_caseless_mag", 2, 30], ["Chemlight_green", 1, 1]]],
|
||||
["V_PlateCarrier1_rgr", [["30Rnd_65x39_caseless_mag", 3, 30], ["16Rnd_9x21_Mag", 2, 16], ["SmokeShell", 1 ,1], ["SmokeShellGreen", 1, 1], ["Chemlight_green", 1, 1]]],
|
||||
["B_AssaultPack_mcamo_AT",[["Titan_AT", 2, 1]]],
|
||||
"H_HelmetB_light_desert", "G_Bandanna_tan",[],
|
||||
["ItemMap", "", "ItemRadio", "ItemCompass", "ItemWatch", "NVGoggles"]]"#;
|
||||
Loadout::from_arma(loadout.to_string()).unwrap();
|
||||
let loadout = r#"[["arifle_SPAR_02_blk_F","","","optic_Holosight_blk_F",["30Rnd_556x45_Stanag",30],[],""],[],["hgun_ACPC2_F","","","",["9Rnd_45ACP_Mag",8],[],""],["tacs_Uniform_Polo_TP_LS_TP_TB_NoLogo",[]],["V_PlateCarrier1_rgr_noflag_F",[]],[],"H_Cap_headphones","G_Shades_Black",[],["ItemMap","ItemGPS","ItemRadio","ItemCompass","ItemWatch",""]]"#;
|
||||
let mut loadout = Loadout::from_arma(loadout.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::from_arma(loadout.to_arma().to_string()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marshal() {
|
||||
let loadout = r#"[[],[],[],["U_Marshal",[]],[],[],"H_Cap_headphones","G_Aviator",[],["ItemMap","ItemGPS","","ItemCompass","ItemWatch",""]]"#;
|
||||
let mut loadout = Loadout::from_arma(loadout.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,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extended_empty() {
|
||||
let loadout = r#"[[["arifle_XMS_Shot_lxWS","","tacgt_ANPEQ_15_Low_Light_Black","CUP_optic_Elcan_SpecterDR_black_PIP",["tacgt_30Rnd_556x45_Ball_Tracer_PMAG",30],["6Rnd_12Gauge_Pellets",6],""],["CUP_launch_M136_Loaded","","","",[],[],""],[],["tacs_Uniform_Floral_JP_RS_LP_BB",[["kat_guedel",1],["ACE_EntrenchingTool",1],["ACE_EarPlugs",1],["ACE_CableTie",2],["ACE_quikclot",1],["ACE_packingBandage",2],["ACE_elasticBandage",1],["SmokeShell",2,1],["Chemlight_yellow",1,1],["Chemlight_red",3,1],["ACE_Chemlight_IR",2,1],["ACE_Chemlight_HiBlue",1,1]]],["milgp_v_mmac_marksman_belt_CB",[["ACE_tourniquet",4],["ACE_splint",1],["tacgt_30Rnd_556x45_EPR_PMAG",15,30],["tacgt_30Rnd_556x45_AP_PMAG",2,30],["6rnd_Smoke_Mag_lxWS",1,6]]],["milgp_bp_Pointman_cb",[["ACE_SpraypaintGreen",1],["ACE_splint",1],["synixe_painkillers",2],["ACE_microDAGR",1],["ACE_MapTools",1],["ACE_bodyBag",1],["ACE_packingBandage",10],["ACE_elasticBandage",9],["ACE_quikclot",5],["ACE_EarPlugs",2],["ACRE_PRC152",1],["ACRE_PRC152",1],["6Rnd_12Gauge_Pellets",2,6],["6Rnd_12Gauge_Slug",1,6],["tacgt_30Rnd_556x45_Ball_Tracer_PMAG",1,30]]],"synixe_contractors_Hat_Beret_Black","",["ACE_VectorDay","","","",[],[],""],["ItemMap","ItemGPS","","ItemCompass","ItemWatch",""]],[]]"#;
|
||||
let loadout = Loadout::from_arma(loadout.to_string()).unwrap();
|
||||
assert!(loadout.cba_extended().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extended_crash_1_9_0() {
|
||||
let loadout = r#"[[[],[],[],["synixe_contractors_Uniform_Contractor_Shirt",[]],[],[],"","",[],["","","","","",""]],[]]"#;
|
||||
let loadout = Loadout::from_arma(loadout.to_string()).unwrap();
|
||||
assert!(loadout.cba_extended().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extended_items() {
|
||||
let loadout = r#"[[["CUP_arifle_M4A1_SOMMOD_Grip_tan","","","CUP_optic_Eotech553_Black",["tacgt_30Rnd_556x45_EPR_PMAG_Tan",30],[],""],[],["ACE_VMM3","","","",[],[],""],["casual_plaid_gray_khaki_uniform",[["ACE_packingBandage",10],["ACE_elasticBandage",10],["ACE_CableTie",2],["kat_guedel",1],["ACE_tourniquet",2],["ACE_splint",1],["synixe_painkillers",2]]],["milgp_v_mmac_assaulter_belt_AOR2",[["ACRE_PRC152",1],["SmokeShell",2,1],["HandGrenade",2,1],["tacgt_30Rnd_556x45_EPR_PMAG_Tan",10,30]]],["B_MU_TacticalPack_cbr",[["ACE_bodyBag",1],["ToolKit",1],["ACE_SpraypaintGreen",1],["synixe_axe",1],["ACE_wirecutter",1],["ACE_EntrenchingTool",1],["ACE_rope3",2],["DemoCharge_Remote_Mag",2,1]]],"synixe_contractors_Cap_Headphones_GreenLogo","CUP_G_Tan_Scarf_Shades",["Binocular","","","",[],[],""],["ItemMap","ItemGPS","","ItemCompass","ItemWatch",""]],[["grad_slingHelmet","CUP_H_OpsCore_Grey"]]]"#;
|
||||
let loadout = Loadout::from_arma(loadout.to_string()).unwrap();
|
||||
assert!(!loadout.cba_extended().is_empty());
|
||||
assert_eq!(
|
||||
loadout.primary().class(),
|
||||
Some("CUP_arifle_M4A1_SOMMOD_Grip_tan")
|
||||
);
|
||||
assert_eq!(
|
||||
loadout.cba_extended().get("grad_slingHelmet"),
|
||||
Some(&Value::String("CUP_H_OpsCore_Grey".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classes() {
|
||||
let loadout = r#"[[["CUP_arifle_M4A1_SOMMOD_Grip_tan","","","CUP_optic_Eotech553_Black",["tacgt_30Rnd_556x45_EPR_PMAG_Tan",30],[],""],[],["ACE_VMM3","","","",[],[],""],["casual_plaid_gray_khaki_uniform",[["ACE_packingBandage",10],["ACE_elasticBandage",10],["ACE_CableTie",2],["kat_guedel",1],["ACE_tourniquet",2],["ACE_splint",1],["synixe_painkillers",2]]],["milgp_v_mmac_assaulter_belt_AOR2",[["ACRE_PRC152",1],["SmokeShell",2,1],["HandGrenade",2,1],["tacgt_30Rnd_556x45_EPR_PMAG_Tan",10,30]]],["B_MU_TacticalPack_cbr",[["ACE_bodyBag",1],["ToolKit",1],["ACE_SpraypaintGreen",1],["synixe_axe",1],["ACE_wirecutter",1],["ACE_EntrenchingTool",1],["ACE_rope3",2],["DemoCharge_Remote_Mag",2,1]]],"synixe_contractors_Cap_Headphones_GreenLogo","CUP_G_Tan_Scarf_Shades",["Binocular","","","",[],[],""],["ItemMap","ItemGPS","","ItemCompass","ItemWatch",""]],[["grad_slingHelmet","CUP_H_OpsCore_Grey"]]]"#;
|
||||
let loadout = Loadout::from_arma(loadout.to_string()).unwrap();
|
||||
assert_eq!(loadout.classes(), {
|
||||
let mut classes = std::collections::HashMap::new();
|
||||
classes.insert("CUP_arifle_M4A1_SOMMOD_Grip_tan".to_string(), 1);
|
||||
classes.insert("CUP_optic_Eotech553_Black".to_string(), 1);
|
||||
classes.insert("tacgt_30Rnd_556x45_EPR_PMAG_Tan".to_string(), 11);
|
||||
classes.insert("ACE_VMM3".to_string(), 1);
|
||||
classes.insert("casual_plaid_gray_khaki_uniform".to_string(), 1);
|
||||
classes.insert("ACE_packingBandage".to_string(), 10);
|
||||
classes.insert("ACE_elasticBandage".to_string(), 10);
|
||||
classes.insert("ACE_CableTie".to_string(), 2);
|
||||
classes.insert("kat_guedel".to_string(), 1);
|
||||
classes.insert("ACE_tourniquet".to_string(), 2);
|
||||
classes.insert("ACE_splint".to_string(), 1);
|
||||
classes.insert("synixe_painkillers".to_string(), 2);
|
||||
classes.insert("milgp_v_mmac_assaulter_belt_AOR2".to_string(), 1);
|
||||
classes.insert("ACRE_PRC152".to_string(), 1);
|
||||
classes.insert("SmokeShell".to_string(), 2);
|
||||
classes.insert("HandGrenade".to_string(), 2);
|
||||
classes.insert("B_MU_TacticalPack_cbr".to_string(), 1);
|
||||
classes.insert("ACE_bodyBag".to_string(), 1);
|
||||
classes.insert("ToolKit".to_string(), 1);
|
||||
classes.insert("ACE_SpraypaintGreen".to_string(), 1);
|
||||
classes.insert("synixe_axe".to_string(), 1);
|
||||
classes.insert("ACE_wirecutter".to_string(), 1);
|
||||
classes.insert("ACE_EntrenchingTool".to_string(), 1);
|
||||
classes.insert("ACE_rope3".to_string(), 2);
|
||||
classes.insert("DemoCharge_Remote_Mag".to_string(), 2);
|
||||
classes.insert("synixe_contractors_Cap_Headphones_GreenLogo".to_string(), 1);
|
||||
classes.insert("CUP_G_Tan_Scarf_Shades".to_string(), 1);
|
||||
classes.insert("Binocular".to_string(), 1);
|
||||
classes.insert("ItemMap".to_string(), 1);
|
||||
classes.insert("ItemGPS".to_string(), 1);
|
||||
classes.insert("ItemCompass".to_string(), 1);
|
||||
classes.insert("ItemWatch".to_string(), 1);
|
||||
classes
|
||||
});
|
||||
}
|
||||
}
|
||||
387
vendor/arma-rs/src/value/loadout/weapon.rs
vendored
Normal file
387
vendor/arma-rs/src/value/loadout/weapon.rs
vendored
Normal file
@@ -0,0 +1,387 @@
|
||||
use crate::{FromArma, FromArmaError, IntoArma, Value};
|
||||
|
||||
use super::Magazine;
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
/// A primary, secondary, or handgun weapon
|
||||
pub struct Weapon(Option<(String, String, String, String, Magazine, Magazine, String)>);
|
||||
impl Weapon {
|
||||
/// Create a new weapon
|
||||
#[must_use]
|
||||
pub fn new(class: String) -> Self {
|
||||
Self(Some((
|
||||
class,
|
||||
"".to_string(),
|
||||
"".to_string(),
|
||||
"".to_string(),
|
||||
Magazine::default(),
|
||||
Magazine::default(),
|
||||
"".to_string(),
|
||||
)))
|
||||
}
|
||||
|
||||
/// The weapon slot is occupied
|
||||
#[must_use]
|
||||
pub const fn exists(&self) -> bool {
|
||||
self.0.is_some()
|
||||
}
|
||||
|
||||
/// The class name of the weapon
|
||||
#[must_use]
|
||||
pub fn class(&self) -> Option<&str> {
|
||||
self.0
|
||||
.as_ref()
|
||||
.map(|(class, _, _, _, _, _, _)| class.as_str())
|
||||
}
|
||||
|
||||
/// Set the class name of the weapon
|
||||
pub fn set_class(&mut self, class: String) {
|
||||
if let Some(weapon) = self.0.as_mut() {
|
||||
weapon.0 = class;
|
||||
} else {
|
||||
self.0 = Some((
|
||||
class,
|
||||
"".to_owned(),
|
||||
"".to_owned(),
|
||||
"".to_owned(),
|
||||
Magazine::default(),
|
||||
Magazine::default(),
|
||||
"".to_owned(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// The class name of the attached suppressor
|
||||
#[must_use]
|
||||
pub fn suppressor(&self) -> Option<&str> {
|
||||
self.0
|
||||
.as_ref()
|
||||
.map(|(_, suppressor, _, _, _, _, _)| suppressor.as_str())
|
||||
}
|
||||
|
||||
/// Set the class name of the attached suppressor
|
||||
/// Returns true if the suppressor was set, false if the weapon was not initialized
|
||||
pub fn set_suppressor(&mut self, suppressor: String) -> bool {
|
||||
if let Some(weapon) = self.0.as_mut() {
|
||||
weapon.1 = suppressor;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// The class name of the attached pointer
|
||||
#[must_use]
|
||||
pub fn pointer(&self) -> Option<&str> {
|
||||
self.0
|
||||
.as_ref()
|
||||
.map(|(_, _, pointer, _, _, _, _)| pointer.as_str())
|
||||
}
|
||||
|
||||
/// Set the class name of the attached pointer
|
||||
/// Returns true if the pointer was set, false if the weapon was not initialized
|
||||
pub fn set_pointer(&mut self, pointer: String) -> bool {
|
||||
if let Some(weapon) = self.0.as_mut() {
|
||||
weapon.2 = pointer;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// The class name of the attached optic
|
||||
#[must_use]
|
||||
pub fn optic(&self) -> Option<&str> {
|
||||
self.0
|
||||
.as_ref()
|
||||
.map(|(_, _, _, optic, _, _, _)| optic.as_str())
|
||||
}
|
||||
|
||||
/// Set the class name of the attached optic
|
||||
/// Returns true if the optic was set, false if the weapon was not initialized
|
||||
pub fn set_optic(&mut self, optic: String) -> bool {
|
||||
if let Some(weapon) = self.0.as_mut() {
|
||||
weapon.3 = optic;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the inserted primary magazine
|
||||
#[must_use]
|
||||
pub fn primary_magazine(&self) -> Option<&Magazine> {
|
||||
self.0.as_ref().map(|(_, _, _, _, primary, _, _)| primary)
|
||||
}
|
||||
|
||||
/// Get the inserted primary magazine mutably
|
||||
pub fn primary_magazine_mut(&mut self) -> Option<&mut Magazine> {
|
||||
self.0.as_mut().map(|(_, _, _, _, primary, _, _)| primary)
|
||||
}
|
||||
|
||||
/// Set the inserted primary magazine
|
||||
/// Returns true if the primary magazine was set, false if the weapon was not initialized
|
||||
pub fn set_primary_magazine(&mut self, primary: Magazine) -> bool {
|
||||
if let Some(weapon) = self.0.as_mut() {
|
||||
weapon.4 = primary;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the inserted secondary magazine
|
||||
#[must_use]
|
||||
pub fn secondary_magazine(&self) -> Option<&Magazine> {
|
||||
self.0
|
||||
.as_ref()
|
||||
.map(|(_, _, _, _, _, secondary, _)| secondary)
|
||||
}
|
||||
|
||||
/// Get the inserted secondary magazine mutably
|
||||
pub fn secondary_magazine_mut(&mut self) -> Option<&mut Magazine> {
|
||||
self.0
|
||||
.as_mut()
|
||||
.map(|(_, _, _, _, _, secondary, _)| secondary)
|
||||
}
|
||||
|
||||
/// Set the inserted secondary magazine
|
||||
/// Returns true if the secondary magazine was set, false if the weapon was not initialized
|
||||
pub fn set_secondary_magazine(&mut self, secondary: Magazine) -> bool {
|
||||
if let Some(weapon) = self.0.as_mut() {
|
||||
weapon.5 = secondary;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// The class name of the attached bipod
|
||||
#[must_use]
|
||||
pub fn bipod(&self) -> Option<&str> {
|
||||
self.0
|
||||
.as_ref()
|
||||
.map(|(_, _, _, _, _, _, bipod)| bipod.as_str())
|
||||
}
|
||||
|
||||
/// Set the class name of the attached bipod
|
||||
/// Returns true if the bipod was set, false if the weapon was not initialized
|
||||
pub fn set_bipod(&mut self, bipod: String) -> bool {
|
||||
if let Some(weapon) = self.0.as_mut() {
|
||||
weapon.6 = bipod;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all classes used on the weapon, including the weapon itself
|
||||
pub fn classes(&self) -> Vec<&str> {
|
||||
let mut classes = Vec::new();
|
||||
if let Some(weapon) = self.0.as_ref() {
|
||||
classes.push(weapon.0.as_str());
|
||||
if !weapon.1.is_empty() {
|
||||
classes.push(weapon.1.as_str());
|
||||
}
|
||||
if !weapon.2.is_empty() {
|
||||
classes.push(weapon.2.as_str());
|
||||
}
|
||||
if !weapon.3.is_empty() {
|
||||
classes.push(weapon.3.as_str());
|
||||
}
|
||||
if let Some(class) = weapon.4.class() {
|
||||
classes.push(class);
|
||||
}
|
||||
if let Some(class) = weapon.5.class() {
|
||||
classes.push(class);
|
||||
}
|
||||
if !weapon.6.is_empty() {
|
||||
classes.push(weapon.6.as_str());
|
||||
}
|
||||
}
|
||||
classes
|
||||
}
|
||||
}
|
||||
impl FromArma for Weapon {
|
||||
fn from_arma(s: String) -> Result<Self, FromArmaError> {
|
||||
if s == "[]" {
|
||||
return Ok(Self(None));
|
||||
}
|
||||
<(String, String, String, String, Magazine, Magazine, String)>::from_arma(s).map(
|
||||
|(weapon, suppressor, pointer, optic, primary_mag, secondary_mag, bipod)| {
|
||||
Self(Some((
|
||||
weapon,
|
||||
suppressor,
|
||||
pointer,
|
||||
optic,
|
||||
primary_mag,
|
||||
secondary_mag,
|
||||
bipod,
|
||||
)))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
impl IntoArma for Weapon {
|
||||
fn to_arma(&self) -> Value {
|
||||
self.0.as_ref().map_or_else(
|
||||
|| Value::Array(vec![]),
|
||||
|weaon| {
|
||||
Value::Array(vec![
|
||||
Value::String(weaon.0.clone()),
|
||||
Value::String(weaon.1.clone()),
|
||||
Value::String(weaon.2.clone()),
|
||||
Value::String(weaon.3.clone()),
|
||||
weaon.4.to_arma(),
|
||||
weaon.5.to_arma(),
|
||||
Value::String(weaon.6.clone()),
|
||||
])
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Weapon;
|
||||
use crate::{loadout::Magazine, FromArma, IntoArma, Value};
|
||||
|
||||
#[test]
|
||||
fn exists() {
|
||||
let weapon = Weapon::default();
|
||||
assert!(!weapon.exists());
|
||||
let weapon = Weapon::new("arifle_Mk20_GL_F".to_owned());
|
||||
assert!(weapon.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class() {
|
||||
let weapon = Weapon::default();
|
||||
assert_eq!(weapon.class(), None);
|
||||
let mut weapon = Weapon::new("arifle_Mk20_GL_F".to_owned());
|
||||
assert_eq!(weapon.class(), Some("arifle_Mk20_GL_F"));
|
||||
weapon.set_class("arifle_Mk20_F".to_owned());
|
||||
assert_eq!(weapon.class(), Some("arifle_Mk20_F"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suppressor() {
|
||||
let weapon = Weapon::default();
|
||||
assert_eq!(weapon.suppressor(), None);
|
||||
let mut weapon = Weapon::new("arifle_Mk20_GL_F".to_owned());
|
||||
assert_eq!(weapon.suppressor(), Some(""));
|
||||
weapon.set_suppressor("muzzle_snds_H".to_owned());
|
||||
assert_eq!(weapon.suppressor(), Some("muzzle_snds_H"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pointer() {
|
||||
let weapon = Weapon::default();
|
||||
assert_eq!(weapon.pointer(), None);
|
||||
let mut weapon = Weapon::new("arifle_Mk20_GL_F".to_owned());
|
||||
assert_eq!(weapon.pointer(), Some(""));
|
||||
weapon.set_pointer("acc_pointer_IR".to_owned());
|
||||
assert_eq!(weapon.pointer(), Some("acc_pointer_IR"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optic() {
|
||||
let weapon = Weapon::default();
|
||||
assert_eq!(weapon.optic(), None);
|
||||
let mut weapon = Weapon::new("arifle_Mk20_GL_F".to_owned());
|
||||
assert_eq!(weapon.optic(), Some(""));
|
||||
weapon.set_optic("optic_Hamr".to_owned());
|
||||
assert_eq!(weapon.optic(), Some("optic_Hamr"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn primary_magazine() {
|
||||
let weapon = Weapon::default();
|
||||
assert_eq!(weapon.primary_magazine(), None);
|
||||
let mut weapon = Weapon::new("arifle_Mk20_GL_F".to_owned());
|
||||
assert_eq!(weapon.primary_magazine(), Some(&Magazine::default()));
|
||||
weapon.set_primary_magazine(Magazine::new("30Rnd_556x45_Stanag".to_string(), 30));
|
||||
assert_eq!(
|
||||
weapon.primary_magazine(),
|
||||
Some(&Magazine::new("30Rnd_556x45_Stanag".to_string(), 30))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secondary_magazine() {
|
||||
let weapon = Weapon::default();
|
||||
assert_eq!(weapon.secondary_magazine(), None);
|
||||
let mut weapon = Weapon::new("arifle_Mk20_GL_F".to_owned());
|
||||
assert_eq!(weapon.secondary_magazine(), Some(&Magazine::default()));
|
||||
weapon.set_secondary_magazine(Magazine::new("1Rnd_HE_Grenade_shell".to_string(), 1));
|
||||
assert_eq!(
|
||||
weapon.secondary_magazine(),
|
||||
Some(&Magazine::new("1Rnd_HE_Grenade_shell".to_string(), 1))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bipod() {
|
||||
let weapon = Weapon::default();
|
||||
assert_eq!(weapon.bipod(), None);
|
||||
let mut weapon = Weapon::new("arifle_Mk20_GL_F".to_owned());
|
||||
assert_eq!(weapon.bipod(), Some(""));
|
||||
weapon.set_bipod("bipod_01_F".to_owned());
|
||||
assert_eq!(weapon.bipod(), Some("bipod_01_F"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_arma() {
|
||||
let weapon = Weapon::from_arma("[]".to_owned()).unwrap();
|
||||
assert_eq!(weapon, Weapon::default());
|
||||
let weapon = Weapon::from_arma(
|
||||
"[\"arifle_Mk20_GL_F\",\"\",\"\",\"\",[\"30Rnd_556x45_Stanag\",30],[\"1Rnd_HE_Grenade_shell\",1],\"\"]".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(weapon, {
|
||||
let mut weapon = Weapon::new("arifle_Mk20_GL_F".to_owned());
|
||||
weapon.set_primary_magazine(Magazine::new("30Rnd_556x45_Stanag".to_string(), 30));
|
||||
weapon.set_secondary_magazine(Magazine::new("1Rnd_HE_Grenade_shell".to_string(), 1));
|
||||
weapon
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_arma() {
|
||||
let weapon = Weapon::default();
|
||||
assert_eq!(weapon.to_arma(), Value::Array(vec![]));
|
||||
let weapon = Weapon::new("arifle_Mk20_GL_F".to_owned());
|
||||
assert_eq!(
|
||||
weapon.to_arma(),
|
||||
Value::Array(vec![
|
||||
Value::String("arifle_Mk20_GL_F".to_owned()),
|
||||
Value::String("".to_owned()),
|
||||
Value::String("".to_owned()),
|
||||
Value::String("".to_owned()),
|
||||
Value::Array(vec![]),
|
||||
Value::Array(vec![]),
|
||||
Value::String("".to_owned()),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classes() {
|
||||
let weapon = Weapon::from_arma("[]".to_owned()).unwrap();
|
||||
assert_eq!(weapon, Weapon::default());
|
||||
let weapon = Weapon::from_arma(
|
||||
"[\"arifle_Mk20_GL_F\",\"fake_optic\",\"\",\"\",[\"30Rnd_556x45_Stanag\",30],[\"1Rnd_HE_Grenade_shell\",1],\"fake_bipod\"]".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
weapon.classes(),
|
||||
vec![
|
||||
"arifle_Mk20_GL_F",
|
||||
"fake_optic",
|
||||
"30Rnd_556x45_Stanag",
|
||||
"1Rnd_HE_Grenade_shell",
|
||||
"fake_bipod"
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
145
vendor/arma-rs/src/value/mod.rs
vendored
Normal file
145
vendor/arma-rs/src/value/mod.rs
vendored
Normal file
@@ -0,0 +1,145 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
mod features;
|
||||
mod from_arma;
|
||||
mod into_arma;
|
||||
pub mod loadout;
|
||||
|
||||
pub use from_arma::{FromArma, FromArmaError};
|
||||
pub use into_arma::{DirectReturn, IntoArma};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, PartialOrd)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "serde", serde(untagged))]
|
||||
/// A value that can be converted to and from Arma types.
|
||||
pub enum Value {
|
||||
/// Arma's `nil` value.
|
||||
/// Represented as `null`
|
||||
Null,
|
||||
/// Arma's `number` value.
|
||||
Number(f64),
|
||||
/// Arma's `array` value.
|
||||
/// Represented as `[...]`
|
||||
Array(Vec<Value>),
|
||||
/// Arma's `boolean` value.
|
||||
/// Represented as `true` or `false`
|
||||
Boolean(bool),
|
||||
/// Arma's `string` value.
|
||||
/// Represented as `"..."`
|
||||
///
|
||||
/// Note: Arma escapes quotes with two double quotes.
|
||||
/// This conversation will remove one step of escaping.
|
||||
/// Example: `"My name is ""John""."` will become `My name is "John".`
|
||||
String(String),
|
||||
/// Unknown value. Contains the raw string.
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl Display for Value {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Null => write!(f, "null"),
|
||||
Self::Number(n) => write!(f, "{n}"),
|
||||
Self::Array(a) => write!(
|
||||
f,
|
||||
"[{}]",
|
||||
a.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect::<Vec<String>>()
|
||||
.join(",")
|
||||
),
|
||||
Self::Boolean(b) => write!(f, "{b}"),
|
||||
Self::String(s) => write!(f, "\"{}\"", s.replace('\"', "\"\"")),
|
||||
Self::Unknown(s) => write!(f, "Unknown({})", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromArma for Value {
|
||||
fn from_arma(s: String) -> Result<Self, FromArmaError> {
|
||||
match s.chars().next() {
|
||||
Some('n') => Ok(Self::Null),
|
||||
Some('t') | Some('f') => Ok(Value::Boolean(<bool>::from_arma(s)?)),
|
||||
Some('0'..='9') | Some('-') => Ok(Value::Number(<f64>::from_arma(s)?)),
|
||||
Some('[') => Ok(Value::Array(<Vec<Value>>::from_arma(s)?)),
|
||||
Some('"') => Ok(Value::String(<String>::from_arma(s)?)),
|
||||
_ => Ok(Value::Unknown(s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_value_display() {
|
||||
assert_eq!(Value::Null.to_string(), "null");
|
||||
assert_eq!(Value::Number(1.0).to_string(), "1");
|
||||
assert_eq!(Value::Number(1.5).to_string(), "1.5");
|
||||
assert_eq!(Value::Number(-1.5).to_string(), "-1.5");
|
||||
assert_eq!(Value::Boolean(true).to_string(), "true");
|
||||
assert_eq!(Value::Boolean(false).to_string(), "false");
|
||||
assert_eq!(Value::String("".to_string()).to_string(), "\"\"");
|
||||
assert_eq!(Value::String(" ".to_string()).to_string(), "\" \"");
|
||||
assert_eq!(Value::String("Hello".to_string()).to_string(), "\"Hello\"");
|
||||
assert_eq!(
|
||||
Value::String("Hello \"World\"".to_string()).to_string(),
|
||||
"\"Hello \"\"World\"\"\""
|
||||
);
|
||||
assert_eq!(
|
||||
Value::Array(vec![
|
||||
Value::Number(1.0),
|
||||
Value::Number(2.0),
|
||||
Value::Number(3.0)
|
||||
])
|
||||
.to_string(),
|
||||
"[1,2,3]"
|
||||
);
|
||||
assert_eq!(
|
||||
Value::Array(vec![
|
||||
Value::String("Hello".to_string()),
|
||||
Value::String("World".to_string())
|
||||
])
|
||||
.to_string(),
|
||||
"[\"Hello\",\"World\"]"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value_from_arma() {
|
||||
let value = Value::from_arma("null".to_string()).unwrap();
|
||||
assert_eq!(value, Value::Null);
|
||||
let value = Value::from_arma("true".to_string()).unwrap();
|
||||
assert_eq!(value, Value::Boolean(true));
|
||||
let value = Value::from_arma("false".to_string()).unwrap();
|
||||
assert_eq!(value, Value::Boolean(false));
|
||||
let value = Value::from_arma("1".to_string()).unwrap();
|
||||
assert_eq!(value, Value::Number(1.0));
|
||||
let value = Value::from_arma("1.5".to_string()).unwrap();
|
||||
assert_eq!(value, Value::Number(1.5));
|
||||
let value = Value::from_arma("-1.5".to_string()).unwrap();
|
||||
assert_eq!(value, Value::Number(-1.5));
|
||||
let value = Value::from_arma("[1,2,3]".to_string()).unwrap();
|
||||
assert_eq!(
|
||||
value,
|
||||
Value::Array(vec![
|
||||
Value::Number(1.0),
|
||||
Value::Number(2.0),
|
||||
Value::Number(3.0)
|
||||
])
|
||||
);
|
||||
let value = Value::from_arma("[\"Hello\",\"World\"]".to_string()).unwrap();
|
||||
assert_eq!(
|
||||
value,
|
||||
Value::Array(vec![
|
||||
Value::String("Hello".to_string()),
|
||||
Value::String("World".to_string())
|
||||
])
|
||||
);
|
||||
let value = Value::from_arma("\"Hello\"".to_string()).unwrap();
|
||||
assert_eq!(value, Value::String("Hello".to_string()));
|
||||
let value = Value::from_arma("\"Hello \"\"World\"\"\"".to_string()).unwrap();
|
||||
assert_eq!(value, Value::String("Hello \"World\"".to_string()));
|
||||
}
|
||||
}
|
||||
613
vendor/arma-rs/tests/derive.rs
vendored
Normal file
613
vendor/arma-rs/tests/derive.rs
vendored
Normal file
@@ -0,0 +1,613 @@
|
||||
mod derive {
|
||||
use arma_rs::{FromArma, FromArmaError, IntoArma, Value};
|
||||
|
||||
fn sort_value_array(value: &mut Value) -> &Value {
|
||||
if let Value::Array(values) = value {
|
||||
values.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
}
|
||||
value
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Default)]
|
||||
enum ValueStringImpl {
|
||||
#[default]
|
||||
Even,
|
||||
Odd,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ValueStringImpl {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Even => write!(f, "even"),
|
||||
Self::Odd => write!(f, "odd"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for ValueStringImpl {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.chars().count() % 2 {
|
||||
0 => Ok(Self::Even),
|
||||
_ => Ok(Self::Odd),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(miri))]
|
||||
fn compile() {
|
||||
let tests = trybuild::TestCases::new();
|
||||
tests.compile_fail("tests/derive/*fail*.rs");
|
||||
tests.pass("tests/derive/*pass*.rs");
|
||||
}
|
||||
|
||||
mod map {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn derive() {
|
||||
#[derive(FromArma, IntoArma, Debug, PartialEq)]
|
||||
struct DeriveTest {
|
||||
first: String,
|
||||
second: bool,
|
||||
}
|
||||
|
||||
let serialized = DeriveTest {
|
||||
first: "first".to_string(),
|
||||
second: true,
|
||||
};
|
||||
let deserialized = Value::Array(vec![
|
||||
Value::Array(vec![
|
||||
Value::String("first".to_string()),
|
||||
Value::String("first".to_string()),
|
||||
]),
|
||||
Value::Array(vec![
|
||||
Value::String("second".to_string()),
|
||||
Value::Boolean(true),
|
||||
]),
|
||||
]);
|
||||
|
||||
assert_eq!(sort_value_array(&mut serialized.to_arma()), &deserialized);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(deserialized.to_string()),
|
||||
Ok(serialized)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transparent() {
|
||||
#[derive(FromArma, IntoArma, Debug, PartialEq)]
|
||||
#[arma(transparent)]
|
||||
struct DeriveTest {
|
||||
expected: String,
|
||||
}
|
||||
|
||||
let serialized = DeriveTest {
|
||||
expected: "expected".to_string(),
|
||||
};
|
||||
let deserialized = Value::String("expected".to_string());
|
||||
assert_eq!(serialized.to_arma(), deserialized);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(deserialized.to_string()),
|
||||
Ok(serialized)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str_field() {
|
||||
#[derive(FromArma, Debug, PartialEq)]
|
||||
struct DeriveTest {
|
||||
#[arma(from_str)]
|
||||
expected: ValueStringImpl,
|
||||
}
|
||||
|
||||
let input = Value::Array(vec![Value::Array(vec![
|
||||
Value::String("expected".to_string()),
|
||||
Value::String("odd".to_string()),
|
||||
])]);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(input.to_string()),
|
||||
Ok(DeriveTest {
|
||||
expected: ValueStringImpl::Odd,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_string_field() {
|
||||
#[derive(IntoArma, Debug, PartialEq)]
|
||||
struct DeriveTest {
|
||||
#[arma(to_string)]
|
||||
expected: ValueStringImpl,
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
DeriveTest {
|
||||
expected: ValueStringImpl::Odd,
|
||||
}
|
||||
.to_arma(),
|
||||
Value::Array(vec![Value::Array(vec![
|
||||
Value::String("expected".to_string()),
|
||||
Value::String("odd".to_string()),
|
||||
])])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str_default_field() {
|
||||
#[derive(FromArma, Debug, PartialEq)]
|
||||
struct DeriveTest {
|
||||
#[arma(from_str, default)]
|
||||
expected: ValueStringImpl,
|
||||
}
|
||||
|
||||
let input = Value::Array(vec![]);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(input.to_string()),
|
||||
Ok(DeriveTest {
|
||||
expected: ValueStringImpl::default(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
#[derive(FromArma, Default, Debug, PartialEq)]
|
||||
#[arma(default)]
|
||||
struct DeriveTest {
|
||||
first: String,
|
||||
second: bool,
|
||||
}
|
||||
|
||||
let input = Value::Array(vec![]);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(input.to_string()),
|
||||
Ok(DeriveTest::default())
|
||||
);
|
||||
|
||||
let input = Value::Array(vec![Value::Array(vec![
|
||||
Value::String("first".to_string()),
|
||||
Value::String("first".to_string()),
|
||||
])]);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(input.to_string()),
|
||||
Ok(DeriveTest {
|
||||
first: "first".to_string(),
|
||||
..DeriveTest::default()
|
||||
})
|
||||
);
|
||||
|
||||
let input = Value::Array(vec![Value::Array(vec![
|
||||
Value::String("second".to_string()),
|
||||
Value::Boolean(true),
|
||||
])]);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(input.to_string()),
|
||||
Ok(DeriveTest {
|
||||
second: true,
|
||||
..DeriveTest::default()
|
||||
})
|
||||
);
|
||||
|
||||
let input = Value::Array(vec![
|
||||
Value::Array(vec![
|
||||
Value::String("first".to_string()),
|
||||
Value::String("first".to_string()),
|
||||
]),
|
||||
Value::Array(vec![
|
||||
Value::String("second".to_string()),
|
||||
Value::Boolean(true),
|
||||
]),
|
||||
]);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(input.to_string()),
|
||||
Ok(DeriveTest {
|
||||
first: "first".to_string(),
|
||||
second: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_field() {
|
||||
#[derive(FromArma, Debug, PartialEq)]
|
||||
struct DeriveTest {
|
||||
first: String,
|
||||
#[arma(default)]
|
||||
second: bool,
|
||||
}
|
||||
|
||||
let input = Value::Array(vec![Value::Array(vec![
|
||||
Value::String("first".to_string()),
|
||||
Value::String("first".to_string()),
|
||||
])]);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(input.to_string()),
|
||||
Ok(DeriveTest {
|
||||
first: "first".to_string(),
|
||||
second: false
|
||||
})
|
||||
);
|
||||
|
||||
let input = Value::Array(vec![
|
||||
Value::Array(vec![
|
||||
Value::String("first".to_string()),
|
||||
Value::String("first".to_string()),
|
||||
]),
|
||||
Value::Array(vec![
|
||||
Value::String("second".to_string()),
|
||||
Value::Boolean(true),
|
||||
]),
|
||||
]);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(input.to_string()),
|
||||
Ok(DeriveTest {
|
||||
first: "first".to_string(),
|
||||
second: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_field_precedence() {
|
||||
#[derive(FromArma, Debug, PartialEq)]
|
||||
#[arma(default)]
|
||||
struct DeriveTest {
|
||||
first: String,
|
||||
#[arma(default)]
|
||||
second: bool,
|
||||
}
|
||||
|
||||
impl Default for DeriveTest {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
first: "first".to_string(),
|
||||
second: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let input = Value::Array(vec![]);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(input.to_string()),
|
||||
Ok(DeriveTest {
|
||||
first: "first".to_string(),
|
||||
second: false
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_missing() {
|
||||
#[derive(FromArma, Debug, PartialEq)]
|
||||
struct DeriveTest {
|
||||
_expected: String,
|
||||
}
|
||||
|
||||
let input = Value::Array(vec![]);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(input.to_string()),
|
||||
Err(FromArmaError::MissingField("_expected".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_unknown() {
|
||||
#[derive(FromArma, Debug, PartialEq)]
|
||||
struct DeriveTest {
|
||||
_expected: String,
|
||||
}
|
||||
|
||||
let input = Value::Array(vec![
|
||||
Value::Array(vec![
|
||||
Value::String("_expected".to_string()),
|
||||
Value::String("_expected".to_string()),
|
||||
]),
|
||||
Value::Array(vec![
|
||||
Value::String("unknown".to_string()),
|
||||
Value::String("unknown".to_string()),
|
||||
]),
|
||||
]);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(input.to_string()),
|
||||
Err(FromArmaError::UnknownField("unknown".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_duplicate() {
|
||||
#[derive(FromArma, Debug, PartialEq)]
|
||||
struct DeriveTest {
|
||||
_expected: String,
|
||||
}
|
||||
|
||||
let input = Value::Array(vec![
|
||||
Value::Array(vec![
|
||||
Value::String("_expected".to_string()),
|
||||
Value::String("first".to_string()),
|
||||
]),
|
||||
Value::Array(vec![
|
||||
Value::String("_expected".to_string()),
|
||||
Value::String("second".to_string()),
|
||||
]),
|
||||
]);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(input.to_string()),
|
||||
Err(FromArmaError::DuplicateField("_expected".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_error_unknown() {
|
||||
#[derive(FromArma, Default, Debug, PartialEq)]
|
||||
#[arma(default)]
|
||||
struct DeriveTest {
|
||||
_expected: String,
|
||||
}
|
||||
|
||||
let input = Value::Array(vec![Value::Array(vec![
|
||||
Value::String("unknown".to_string()),
|
||||
Value::String("unknown".to_string()),
|
||||
])]);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(input.to_string()),
|
||||
Err(FromArmaError::UnknownField("unknown".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_field_error_missing() {
|
||||
#[derive(FromArma, Debug, PartialEq)]
|
||||
struct DeriveTest {
|
||||
_expected: String,
|
||||
#[arma(default)]
|
||||
_default: String,
|
||||
}
|
||||
|
||||
let input = Value::Array(vec![]);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(input.to_string()),
|
||||
Err(FromArmaError::MissingField("_expected".to_string()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod tuple {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn derive() {
|
||||
#[derive(FromArma, IntoArma, Debug, PartialEq)]
|
||||
struct DeriveTest(String, bool);
|
||||
|
||||
let serialized = DeriveTest("first".to_string(), true);
|
||||
let deserialized = Value::Array(vec![
|
||||
Value::String("first".to_string()),
|
||||
Value::Boolean(true),
|
||||
]);
|
||||
assert_eq!(serialized.to_arma(), deserialized);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(deserialized.to_string()),
|
||||
Ok(serialized)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_string_field() {
|
||||
#[derive(FromArma, Debug, PartialEq)]
|
||||
struct DeriveTest(String, #[arma(from_str)] ValueStringImpl);
|
||||
|
||||
let input = Value::Array(vec![
|
||||
Value::String("first".to_string()),
|
||||
Value::String("odd".to_string()),
|
||||
]);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(input.to_string()),
|
||||
Ok(DeriveTest("first".to_string(), ValueStringImpl::Odd))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_string_field() {
|
||||
#[derive(IntoArma, Debug, PartialEq)]
|
||||
struct DeriveTest(String, #[arma(to_string)] ValueStringImpl);
|
||||
|
||||
assert_eq!(
|
||||
DeriveTest("first".to_string(), ValueStringImpl::Odd).to_arma(),
|
||||
Value::Array(vec![
|
||||
Value::String("first".to_string()),
|
||||
Value::String("odd".to_string()),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str_default_field() {
|
||||
#[derive(FromArma, Debug, PartialEq)]
|
||||
struct DeriveTest(String, #[arma(from_str, default)] ValueStringImpl);
|
||||
|
||||
let input = Value::Array(vec![Value::String("first".to_string())]);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(input.to_string()),
|
||||
Ok(DeriveTest("first".to_string(), ValueStringImpl::default()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
#[derive(FromArma, Default, Debug, PartialEq)]
|
||||
#[arma(default)]
|
||||
struct DeriveTest(String, bool);
|
||||
|
||||
let input = Value::Array(vec![]);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(input.to_string()),
|
||||
Ok(DeriveTest::default())
|
||||
);
|
||||
|
||||
let input = Value::Array(vec![Value::String("first".to_string())]);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(input.to_string()),
|
||||
Ok(DeriveTest("first".to_string(), false))
|
||||
);
|
||||
|
||||
let input = Value::Array(vec![
|
||||
Value::String("first".to_string()),
|
||||
Value::Boolean(true),
|
||||
]);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(input.to_string()),
|
||||
Ok(DeriveTest("first".to_string(), true))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_field() {
|
||||
#[derive(FromArma, Debug, PartialEq)]
|
||||
struct DeriveTest(String, #[arma(default)] bool);
|
||||
|
||||
let input = Value::Array(vec![Value::String("first".to_string())]);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(input.to_string()),
|
||||
Ok(DeriveTest("first".to_string(), false))
|
||||
);
|
||||
|
||||
let input = Value::Array(vec![
|
||||
Value::String("first".to_string()),
|
||||
Value::Boolean(true),
|
||||
]);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(input.to_string()),
|
||||
Ok(DeriveTest("first".to_string(), true))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_multi_field() {
|
||||
#[derive(FromArma, Debug, PartialEq)]
|
||||
struct DeriveTest(String, #[arma(default)] bool, #[arma(default)] bool);
|
||||
|
||||
let input = Value::Array(vec![Value::String("first".to_string())]);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(input.to_string()),
|
||||
Ok(DeriveTest("first".to_string(), false, false))
|
||||
);
|
||||
|
||||
let input = Value::Array(vec![
|
||||
Value::String("first".to_string()),
|
||||
Value::Boolean(true),
|
||||
]);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(input.to_string()),
|
||||
Ok(DeriveTest("first".to_string(), true, false))
|
||||
);
|
||||
|
||||
let input = Value::Array(vec![
|
||||
Value::String("first".to_string()),
|
||||
Value::Boolean(true),
|
||||
Value::Boolean(true),
|
||||
]);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(input.to_string()),
|
||||
Ok(DeriveTest("first".to_string(), true, true))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_field_precedence() {
|
||||
#[derive(FromArma, Debug, PartialEq)]
|
||||
#[arma(default)]
|
||||
struct DeriveTest(String, #[arma(default)] bool);
|
||||
|
||||
impl Default for DeriveTest {
|
||||
fn default() -> Self {
|
||||
Self("first".to_string(), true)
|
||||
}
|
||||
}
|
||||
|
||||
let input = Value::Array(vec![]);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(input.to_string()),
|
||||
Ok(DeriveTest("first".to_string(), false))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_length() {
|
||||
#[derive(FromArma, Default, Debug, PartialEq)]
|
||||
struct DeriveTest(String, String);
|
||||
|
||||
let input = Value::Array(vec![
|
||||
Value::String("first".to_string()),
|
||||
Value::String("second".to_string()),
|
||||
Value::String("third".to_string()),
|
||||
Value::String("forth".to_string()),
|
||||
]);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(input.to_string()),
|
||||
Err(arma_rs::FromArmaError::InvalidLength {
|
||||
expected: 2,
|
||||
actual: 4,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_field_error() {
|
||||
#[derive(FromArma, Debug, PartialEq)]
|
||||
struct DeriveTest(String, #[arma(default)] String);
|
||||
|
||||
let input = Value::Array(vec![]);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(input.to_string()),
|
||||
Err(arma_rs::FromArmaError::InvalidLength {
|
||||
expected: 2,
|
||||
actual: 0,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
mod newtype {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn derive() {
|
||||
#[derive(FromArma, IntoArma, Debug, PartialEq)]
|
||||
struct DeriveTest(String);
|
||||
|
||||
let serialized = DeriveTest("expected".to_string());
|
||||
let deserialized = Value::String("expected".to_string());
|
||||
assert_eq!(serialized.to_arma(), deserialized);
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(deserialized.to_string()),
|
||||
Ok(serialized)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_str() {
|
||||
#[derive(FromArma, Debug, PartialEq)]
|
||||
struct DeriveTest(#[arma(from_str)] ValueStringImpl);
|
||||
|
||||
let input = Value::String("odd".to_string());
|
||||
assert_eq!(
|
||||
DeriveTest::from_arma(input.to_string()),
|
||||
Ok(DeriveTest(ValueStringImpl::Odd))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_string() {
|
||||
#[derive(IntoArma, Debug, PartialEq)]
|
||||
struct DeriveTest(#[arma(to_string)] ValueStringImpl);
|
||||
|
||||
assert_eq!(
|
||||
DeriveTest(ValueStringImpl::Odd).to_arma(),
|
||||
Value::String("odd".to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
6
vendor/arma-rs/tests/derive/fail_enum.rs
vendored
Normal file
6
vendor/arma-rs/tests/derive/fail_enum.rs
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
use arma_rs::{FromArma, IntoArma};
|
||||
|
||||
#[derive(FromArma, IntoArma)]
|
||||
enum DeriveTest {}
|
||||
|
||||
fn main() {}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user