Initial Commit

This commit is contained in:
AviiNL
2023-12-19 19:16:41 +01:00
commit 15f764aae8
47 changed files with 9097 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
[package]
name = "dcs-grpc"
version.workspace = true
edition.workspace = true
repository.workspace = true
license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bevy.workspace = true
tonic = "0.10"
prost = "0.12"
prost-types = "0.12"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[build-dependencies]
tonic-build = "0.10.2"
protoc-bundled = { git = "https://github.com/rkusa/protoc-bundled.git", rev = "3.21.6" }

82
crates/dcs-grpc/build.rs Normal file
View File

@@ -0,0 +1,82 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
std::env::set_var("PROTOC", protoc_bundled::PROTOC);
std::env::set_var("PROTOC_INCLUDE", protoc_bundled::PROTOC_INCLUDE);
println!("cargo:rerun-if-changed=migrations");
println!("cargo:rerun-if-changed=protos/dcs");
build_dcs_grpc_db()?;
// build_commands()?;
Ok(())
}
// fn build_commands() -> Result<(), Box<dyn std::error::Error>> {
// Ok(())
// }
fn build_dcs_grpc_db() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::configure()
.type_attribute(".", "#[derive(::serde::Serialize, ::serde::Deserialize)]")
.type_attribute(".", "#[serde(rename_all = \"camelCase\")]")
.type_attribute(
"dcs.mission.v0.StreamEventsResponse.event",
"#[serde(tag = \"type\")]",
)
.type_attribute(
"dcs.common.v0.Unit",
"#[serde(from = \"UnitIntermediate\")]",
)
.type_attribute(
"dcs.common.v0.Weapon",
"#[serde(from = \"WeaponIntermediate\")]",
)
.type_attribute(
"dcs.unit.v0.GetTransformResponse",
"#[serde(from = \"GetTransformResponseIntermediate\")]",
)
.type_attribute(
"dcs.mission.v0.StreamUnitsResponse.update",
"#[allow(clippy::large_enum_variant)]",
)
.field_attribute(
"dcs.mission.v0.StreamEventsResponse.MarkAddEvent.visibility",
"#[serde(flatten)]",
)
.field_attribute(
"dcs.mission.v0.StreamEventsResponse.MarkChangeEvent.visibility",
"#[serde(flatten)]",
)
.field_attribute(
"dcs.mission.v0.StreamEventsResponse.MarkRemoveEvent.visibility",
"#[serde(flatten)]",
)
.field_attribute(
"dcs.mission.v0.AddMissionCommandRequest.details",
r#"#[serde(with = "crate::dcs::utils::proto_struct")]"#,
)
.field_attribute(
"dcs.mission.v0.StreamEventsResponse.MissionCommandEvent.details",
r#"#[serde(with = "crate::dcs::utils::proto_struct")]"#,
)
.field_attribute(
"dcs.mission.v0.AddCoalitionCommandRequest.details",
r#"#[serde(with = "crate::dcs::utils::proto_struct")]"#,
)
.field_attribute(
"dcs.mission.v0.StreamEventsResponse.CoalitionCommandEvent.details",
r#"#[serde(with = "crate::dcs::utils::proto_struct")]"#,
)
.field_attribute(
"dcs.mission.v0.AddGroupCommandRequest.details",
r#"#[serde(with = "crate::dcs::utils::proto_struct")]"#,
)
.field_attribute(
"dcs.mission.v0.StreamEventsResponse.GroupCommandEvent.details",
r#"#[serde(with = "crate::dcs::utils::proto_struct")]"#,
)
.build_client(true)
.compile(&["protos/dcs/dcs.proto"], &["protos"])?;
Ok(())
}

View File

@@ -0,0 +1,61 @@
syntax = "proto3";
package dcs.atmosphere.v0;
import "dcs/common/v0/common.proto";
option csharp_namespace = "RurouniJones.Dcs.Grpc.V0.Atmosphere";
option go_package = "github.com/DCS-gRPC/go-bindings/dcs/v0/atmosphere";
// https://wiki.hoggitworld.com/view/DCS_singleton_atmosphere
service AtmosphereService {
// https://wiki.hoggitworld.com/view/DCS_func_getWind
rpc GetWind(GetWindRequest) returns (GetWindResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_getWindWithTurbulence
rpc GetWindWithTurbulence(GetWindWithTurbulenceRequest)
returns (GetWindWithTurbulenceResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_getWindWithTurbulence
rpc GetTemperatureAndPressure(GetTemperatureAndPressureRequest)
returns (GetTemperatureAndPressureResponse) {}
}
message GetWindRequest {
// The position on the map we want the wind information for.
// Requires lat/lon/alt fields to be populated, there are
// no default values
dcs.common.v0.InputPosition position = 1;
}
message GetWindResponse {
// The heading the wind is coming from.
float heading = 1;
// The strength of the wind in meters per second
float strength = 2;
}
message GetWindWithTurbulenceRequest {
// The position on the map we want the wind information for.
// Requires lat/lon/alt fields to be populated, there are
// no default values
dcs.common.v0.InputPosition position = 1;
}
message GetWindWithTurbulenceResponse {
// The heading the wind is coming from.
float heading = 1;
// The strength of the wind in meters per second.
float strength = 2;
}
message GetTemperatureAndPressureRequest {
// The position on the map we want the wind information for.
// Requires lat/lon/alt fields to be populated, there are
// no default values
dcs.common.v0.InputPosition position = 1;
}
message GetTemperatureAndPressureResponse {
// The temperature in Kelvin
float temperature = 1;
// The pressure in Pascals
float pressure = 2;
}

View File

@@ -0,0 +1,226 @@
syntax = "proto3";
package dcs.coalition.v0;
import "dcs/common/v0/common.proto";
option csharp_namespace = "RurouniJones.Dcs.Grpc.V0.Coalition";
option go_package = "github.com/DCS-gRPC/go-bindings/dcs/v0/coalition";
// https://wiki.hoggitworld.com/view/DCS_singleton_coalition
service CoalitionService {
// https://wiki.hoggitworld.com/view/DCS_func_addGroup
rpc AddGroup(AddGroupRequest) returns (AddGroupResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_getStaticObjects
rpc GetStaticObjects(GetStaticObjectsRequest)
returns (GetStaticObjectsResponse) {}
// Focussed on statics (linked statics - see `AddLinkedStatic`)
// https://wiki.hoggitworld.com/view/DCS_func_addStaticObject
rpc AddStaticObject(AddStaticObjectRequest)
returns (AddStaticObjectResponse) {}
// Focussed on properties relevant to linked static objects
// https://wiki.hoggitworld.com/view/DCS_func_addStaticObject
rpc AddLinkedStatic(AddLinkedStaticRequest)
returns (AddLinkedStaticResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_getGroups
rpc GetGroups(GetGroupsRequest) returns (GetGroupsResponse) {}
/*
* Get the Bullseye for the coalition
*
* This position is set at mission start and does not change for the duration
* of the mission.
*
* See https://wiki.hoggitworld.com/view/DCS_func_getMainRefPoint for more
* details
*/
rpc GetBullseye(GetBullseyeRequest) returns (GetBullseyeResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_getPlayers
rpc GetPlayerUnits(GetPlayerUnitsRequest) returns (GetPlayerUnitsResponse) {}
}
message AddGroupRequest {
// The coalition is determined by the provided Country
// and the coalition setup of the mission
dcs.common.v0.Country country = 2;
dcs.common.v0.GroupCategory group_category = 3;
oneof template {
GroundGroupTemplate ground_template = 4;
ShipGroupTemplate ship_template = 5;
HelicopterGroupTemplate helicopter_template = 6;
PlaneGroupTemplate plane_template = 7;
}
message GroundGroupTemplate {
optional uint32 group_id = 1;
bool hidden = 2;
bool late_activation = 3;
string name = 4;
dcs.common.v0.InputPosition position = 5;
repeated Point waypoints = 6;
uint32 start_time = 7;
string task = 8;
bool task_selected = 9;
repeated Task tasks = 10;
bool uncontrollable = 11;
repeated GroundUnitTemplate units = 12;
bool visible = 13;
}
message GroundUnitTemplate {
string name = 1;
string type = 2;
dcs.common.v0.InputPosition position = 3;
optional uint32 unit_id = 4;
optional uint32 heading = 5;
Skill skill = 6;
}
message ShipGroupTemplate {
}
message ShipUnitTemplate {
}
message HelicopterGroupTemplate {
}
message HelicopterUnitTemplate {
}
message PlaneGroupTemplate {
}
message PlaneUnitTemplate {
}
message Point {
enum AltitudeType {
ALTITUDE_TYPE_UNSPECIFIED = 0;
ALTITUDE_TYPE_BAROMETRIC = 1;
ALTITUDE_TYPE_RADIO = 2;
}
enum PointType {
// protolint:disable:next ENUM_FIELD_NAMES_ZERO_VALUE_END_WITH
POINT_TYPE_RANDOM = 0;
POINT_TYPE_TAKEOFF = 1;
POINT_TYPE_TAKEOFF_PARKING = 2;
POINT_TYPE_TURNING_POINT = 3;
POINT_TYPE_TAKEOFF_PARKING_HOT = 4;
POINT_TYPE_LAND = 5;
}
dcs.common.v0.InputPosition position = 1;
AltitudeType altitude_type = 2;
PointType type = 3;
string action = 4;
string form = 5;
double speed = 6;
}
enum Skill {
// protolint:disable:next ENUM_FIELD_NAMES_ZERO_VALUE_END_WITH
SKILL_RANDOM = 0;
SKILL_AVERAGE = 1;
SKILL_GOOD = 2;
SKILL_HIGH = 3;
SKILL_EXCELLENT = 4;
SKILL_PLAYER = 5;
}
message Task {
}
}
message AddGroupResponse {
dcs.common.v0.Group group = 1;
}
message GetStaticObjectsRequest {
// the coalition which the statics belong to
dcs.common.v0.Coalition coalition = 1;
}
message GetStaticObjectsResponse {
// the list of statics
repeated dcs.common.v0.Static statics = 1;
}
message AddStaticObjectRequest {
// the name of the static; must be unique or would destroy previous object
string name = 1;
// country the unit belongs to
dcs.common.v0.Country country = 2;
// type of the static object (e.g. "Farm A", "AS32-31A")
string type = 3;
// string name of the livery for the aircraft
string livery = 4;
// boolean for whether or not the object will appear as a wreck
bool dead = 5;
// number value for the "score" of the object when it is killed
optional uint32 rate = 6;
double heading = 7;
dcs.common.v0.InputPosition position = 8;
// cargo mass in kilograms
uint32 cargo_mass = 9;
}
message AddStaticObjectResponse {
string name = 1;
}
message AddLinkedStaticRequest {
// the name of the static; must be unique or would destroy previous object
string name = 1;
// country the unit belongs to
dcs.common.v0.Country country = 2;
// type of the static object (e.g. "Farm A", "AS32-31A")
string type = 3;
// string name of the livery for the aircraft
string livery = 4;
// boolean for whether or not the object will appear as a wreck
bool dead = 5;
// number value for the "score" of the object when it is killed
optional uint32 rate = 6;
// the name of the unit to offset from
string unit = 7;
// the angle to relative to the linked unit, in a clockwise direction.
// negative values are anti-clockwise
double angle = 8;
// x offset from linked unit center (positive is forward; negative is aft)
double x = 9;
// y offset from linked unit center (positive is starboard-side;
// negative is port-side)
double y = 10;
}
message AddLinkedStaticResponse {
string name = 1;
}
message GetGroupsRequest {
dcs.common.v0.Coalition coalition = 1;
dcs.common.v0.GroupCategory category = 2;
}
message GetGroupsResponse {
repeated dcs.common.v0.Group groups = 1;
}
message GetBullseyeRequest {
// A specific coalition must be used for this API call. Do not use
// `COALITION_ALL`
dcs.common.v0.Coalition coalition = 1;
}
message GetBullseyeResponse {
dcs.common.v0.Position position = 1;
}
message GetPlayerUnitsRequest {
dcs.common.v0.Coalition coalition = 1;
}
message GetPlayerUnitsResponse {
repeated dcs.common.v0.Unit units = 1;
}

View File

@@ -0,0 +1,472 @@
syntax = "proto3";
package dcs.common.v0;
option csharp_namespace = "RurouniJones.Dcs.Grpc.V0.Common";
option go_package = "github.com/DCS-gRPC/go-bindings/dcs/v0/common";
/**
* The category the object belongs to
*
* All DCS objects are one of the following categories. Unlike many other
* enums created by DCS, this one is not 0 indexed. Therefore we do not
* need to do any modification of the value by incrementing it by one to
* make it work with gRPC and DCS.
*
* See https://wiki.hoggitworld.com/view/DCS_Class_Object for more information
*/
enum ObjectCategory {
OBJECT_CATEGORY_UNSPECIFIED = 0;
OBJECT_CATEGORY_UNIT = 1;
OBJECT_CATEGORY_WEAPON = 2;
OBJECT_CATEGORY_STATIC = 3;
OBJECT_CATEGORY_SCENERY = 4;
OBJECT_CATEGORY_BASE = 5;
OBJECT_CATEGORY_CARGO = 6;
}
/**
* The category the object belongs to
*
* Some of these are less than obvious. For example an oilrig counts as a
* HELIPAD airfield.
*/
enum AirbaseCategory {
AIRBASE_CATEGORY_UNSPECIFIED = 0;
AIRBASE_CATEGORY_AIRDROME = 1;
AIRBASE_CATEGORY_HELIPAD = 2;
AIRBASE_CATEGORY_SHIP = 3;
}
/**
* Coalitions in DCS
*
* The coalitions supported by DCS. The NEUTRAL coalition is a relatively new
* one and may not be as supported as the belligerant ones.
*/
enum Coalition {
// protolint:disable:next ENUM_FIELD_NAMES_ZERO_VALUE_END_WITH
COALITION_ALL = 0;
COALITION_NEUTRAL = 1;
COALITION_RED = 2;
COALITION_BLUE = 3;
}
/**
* Countries in DCS
*
* Every country belongs to a coalition and this association is set per mission.
* The values of these enums are correct such that they will work with DCS
* however the text names have been Made to follow gRPC conventions to to aid
* in language bindings and acronyms have been replaced with their full english
* names to aid in recognition. In some cases this can be a big change
* (e.g. USSR -> Soviet Union).
*
* We have also added a dummy value for the missing enum value 14 to prevent
* possible issues in the various language bindings
*
* See https://wiki.hoggitworld.com/view/DCS_enum_country for more information
*/
enum Country {
COUNTRY_UNSPECIFIED = 0;
COUNTRY_RUSSIA = 1;
COUNTRY_UKRAINE = 2;
COUNTRY_UNITED_STATES_OF_AMERICA = 3;
COUNTRY_TURKEY = 4;
COUNTRY_UNITED_KINGDOM = 5;
COUNTRY_FRANCE = 6;
COUNTRY_GERMANY = 7;
COUNTRY_AGGRESSORS = 8;
COUNTRY_CANADA = 9;
COUNTRY_SPAIN = 10;
COUNTRY_THE_NETHERLANDS = 11;
COUNTRY_BELGIUM = 12;
COUNTRY_NORWAY = 13;
COUNTRY_DENMARK = 14;
COUNTRY_UNUSED = 15;
COUNTRY_ISRAEL = 16;
COUNTRY_GEORGIA = 17;
COUNTRY_INSURGENTS = 18;
COUNTRY_ABKHAZIA = 19;
COUNTRY_SOUTH_OSETIA = 20;
COUNTRY_ITALY = 21;
COUNTRY_AUSTRALIA = 22;
COUNTRY_SWITZERLAND = 23;
COUNTRY_AUSTRIA = 24;
COUNTRY_BELARUS = 25;
COUNTRY_BULGARIA = 26;
COUNTRY_CZECH_REPUBLIC = 27;
COUNTRY_CHINA = 28;
COUNTRY_CROATIA = 29;
COUNTRY_EGYPT = 30;
COUNTRY_FINLAND = 31;
COUNTRY_GREECE = 32;
COUNTRY_HUNGARY = 33;
COUNTRY_INDIA = 34;
COUNTRY_IRAN = 35;
COUNTRY_IRAQ = 36;
COUNTRY_JAPAN = 37;
COUNTRY_KAZAKHSTAN = 38;
COUNTRY_NORTH_KOREA = 39;
COUNTRY_PAKISTAN = 40;
COUNTRY_POLAND = 41;
COUNTRY_ROMANIA = 42;
COUNTRY_SAUDI_ARABIA = 43;
COUNTRY_SERBIA = 44;
COUNTRY_SLOVAKIA = 45;
COUNTRY_SOUTH_KOREA = 46;
COUNTRY_SWEDEN = 47;
COUNTRY_SYRIA = 48;
COUNTRY_YEMEN = 49;
COUNTRY_VIETNAM = 50;
COUNTRY_VENEZUELA = 51;
COUNTRY_TUNISIA = 52;
COUNTRY_THAILAND = 53;
COUNTRY_SUDAN = 54;
COUNTRY_PHILIPPINES = 55;
COUNTRY_MOROCCO = 56;
COUNTRY_MEXICO = 57;
COUNTRY_MALAYSIA = 58;
COUNTRY_LIBYA = 59;
COUNTRY_JORDAN = 60;
COUNTRY_INDONESIA = 61;
COUNTRY_HONDURAS = 62;
COUNTRY_ETHIOPIA = 63;
COUNTRY_CHILE = 64;
COUNTRY_BRAZIL = 65;
COUNTRY_BAHRAIN = 66;
COUNTRY_THIRDREICH = 67;
COUNTRY_YUGOSLAVIA = 68;
COUNTRY_SOVIET_UNION = 69;
COUNTRY_ITALIAN_SOCIAL_REPUBLIC = 70;
COUNTRY_ALGERIA = 71;
COUNTRY_KUWAIT = 72;
COUNTRY_QATAR = 73;
COUNTRY_OMAN = 74;
COUNTRY_UNITED_ARAB_EMIRATES = 75;
COUNTRY_SOUTH_AFRICA = 76;
COUNTRY_CUBA = 77;
COUNTRY_PORTUGAL = 78;
COUNTRY_GERMAN_DEMOCRATIC_REPUBLIC = 79;
COUNTRY_LEBANON = 80;
COUNTRY_COMBINED_JOINT_TASK_FORCE_BLUE = 81;
COUNTRY_COMBINED_JOINT_TASK_FORCE_RED = 82;
COUNTRY_UNITED_NATIONS_PEACEKEEPERS = 83;
COUNTRY_ARGENTINA = 84;
COUNTRY_CYPRUS = 85;
COUNTRY_SLOVENIA = 86;
}
/**
* Position of an object in DCS
*
* Latitude and Longitude are in Decimal Degrees format (e.g. 41.33 / 37.21).
* Negative values are used for West of the meridian and south of the equator
*
* Altitude is given in meters above Mean Sea Level (MSL) and can be a decimal
* value.
*/
message Position {
// Latitude in Decimal Degrees format
double lat = 1;
// Longitude in Decimal Degrees format
double lon = 2;
// Altitude in Meters above Mean Sea Level (MSL)
double alt = 3;
// Distance between DCS' map origin to object in meters on west-east axis.
double u = 4;
// Distance between DCS' map origin to object in meters on north-south axis.
double v = 5;
}
/**
* Position used in requests to DCS-gRPC.
*
* Latitude and Longitude are in Decimal Degrees format (e.g. 41.33 / 37.21).
* Negative values are used for West of the meridian and south of the equator.
*
* Altitude is given in meters above Mean Sea Level (MSL) and can be a decimal
* value.
*/
message InputPosition {
// Latitude in Decimal Degrees format
double lat = 1;
// Longitude in Decimal Degrees format
double lon = 2;
// Altitude in Meters above Mean Sea Level (MSL)
double alt = 3;
}
/**
* This type is returned if an object category cannot be determined
*
* The base object includes the `getName()` function so even for an unknown type
* we _should_ be able to get the name
*/
message Unknown {
string name = 1;
}
/**
* An instance of a DCS Unit
*
* A unit is an "active" unit in a DCS mission. This means it has an attached AI
* that moves and shoots. Units include aircraft, ground units, ships, weapons
* etc.
*/
message Unit {
// The DCS generated ID
uint32 id = 1;
// The name of the unit as assigned in the mission editor
string name = 2;
// The DCS assigned callsign if one exists. e.g. "Enfield 11"
string callsign = 3;
// The coalition the unit belongs to
Coalition coalition = 4;
// The DCS type-name of the unit. e.g "MiG-29A", "ZSU_57_2" or "Hawk ln"
string type = 5;
// The position of the unit
Position position = 6;
// The orientation of the unit in both 2D and 3D space
Orientation orientation = 7;
// The velocity of the unit in both 2D and 3D space
Velocity velocity = 8;
// The name of the player if one is in control of the unit
optional string player_name = 9;
// The group that the unit belongs to
Group group = 10;
// The number of this unit in the group. Does not change as units are
// destroyed
uint32 number_in_group = 11;
}
/**
* An instance of a DCS group
*/
message Group {
uint32 id = 1; // The DCS generated ID
string name = 2; // The name of the group as assigned in the mission editor
Coalition coalition = 3; // The coalition of the group
GroupCategory category = 4; // The group category.
}
/**
* Group category enumerator.
*/
enum GroupCategory {
GROUP_CATEGORY_UNSPECIFIED = 0;
GROUP_CATEGORY_AIRPLANE = 1;
GROUP_CATEGORY_HELICOPTER = 2;
GROUP_CATEGORY_GROUND = 3;
GROUP_CATEGORY_SHIP = 4;
GROUP_CATEGORY_TRAIN = 5;
}
/**
* An instance of a DCS weapon
*
* These weapons include everything from autocannon HE shells up to massive
* ship-killer missiles
*/
message Weapon {
// The DCS generated ID
uint32 id = 1;
// The DCS type-name of the weapon. e.g "Matra_S530D", "HAWK_RAKETA" or
// "weapons.shells.53-UOR-281U"
string type = 2;
// The position of the Weapon
Position position = 3;
// The orientation of the unit in both 2D and 3D space
Orientation orientation = 4;
// The velocity of the unit in both 2D and 3D space
Velocity velocity = 5;
}
/**
* An instance of a DCS static object
*
* These objects are often buildings but can also be vehicles that have no AI or
* other game behaviour aside from being destroyable
*/
message Static {
// The DCS generated ID
uint32 id = 1;
// The DCS type-name of the static
string type = 2;
// The name of the static
string name = 3;
// The coalition the static belongs to
Coalition coalition = 4;
// The position of the static
Position position = 5;
}
/**
* An instance of a DCS scenery object
*/
message Scenery {
// The id of the scenery
uint32 id = 1;
// The DCS type-name of the scenery
string type = 2;
// The position of the scenery
Position position = 3;
}
/**
* An instance of a DCS Airfield
*
*/
message Airbase {
// Information about the unit, if the airbase is one (e.g. in case of a
// carrier).
optional Unit unit = 1;
// TODO: Fill this in
string name = 2;
// TODO: Fill this in
string callsign = 3;
// The coalition the unit belongs to. This can change mid-mission if an
// airfield is captured
Coalition coalition = 4;
// The position of the center point of the airfield.
Position position = 6;
// What category the airfield belongs to.
AirbaseCategory category = 7;
// TODO: Fill this in
string display_name = 8;
}
/**
* An instance of a DCS Cargo object
*/
message Cargo {
}
/*
* The initiator of an event
*
* The initiator of an event. For things like shooting events it is usually a
* vehicle but it can be almost anything depending on the event
*/
message Initiator {
oneof initiator {
Unknown unknown = 1;
Unit unit = 2;
Weapon weapon = 3;
Static static = 4;
Scenery scenery = 5;
Airbase airbase = 6;
Cargo cargo = 7;
}
}
/*
* The target of an event
*
* The target of an event. For things like shooting events it is usually a
* vehicle but it can be almost anything depending on the event
*/
message Target {
oneof target {
Unknown unknown = 1;
Unit unit = 2;
Weapon weapon = 3;
Static static = 4;
Scenery scenery = 5;
Airbase airbase = 6;
Cargo cargo = 7;
}
}
/*
* A MarkPanel
*
* A MarkPanel visible on the F10 map. These can be used for reference by
* players but can also be used by things like Jester for setting waypoints
*/
message MarkPanel {
// The id of the mark panel.
uint32 id = 1;
// The time in seconds relative to the mission start the mark got created.
double time = 2;
// The unit of the player that created the mark. Not set if the player isn't
// controlling any unit anymore (disconnected, spectator, game master, ...).
optional Unit initiator = 3;
// If set, the mark is only visible for the specified coalition.
optional Coalition coalition = 4;
// The ID of the group the player was in when creating the mark panel. This
// will still be set even if the player isn't controlling the unit in that
// group anymore.
optional uint32 group_id = 5;
// The text content of the mark.
optional string text = 6;
// The position of the mark.
Position position = 7;
}
/**
* A vector in a right-handed coordinate system where +x is north, -x south, +z
* is east, -z west, +y up and -y down.
*/
message Vector {
double x = 1;
double y = 2;
double z = 3;
}
/**
* The orientation of an object in 3D space.
*/
message Orientation {
// The heading the nose of the object points to on a flat world.
double heading = 1;
// Yaw in degrees - clockwise relative to the true north (this is similar to
// the heading, just corrected by the projection error when going from a flat
// to a spherical world).
double yaw = 2;
// Pitch in degrees - positive when taking-off.
double pitch = 3;
// Roll in degrees - positive when rolling the aircraft to the right.
double roll = 4;
// The normalized direction the object is pointing to.
Vector forward = 5;
// The normalized direction the three line (right wing) is pointing to.
Vector right = 6;
// The normalized up vector (orthogonal to forward and right).
Vector up = 7;
}
/**
* The orientation of an object in 3D space.
*/
message Velocity {
// The heading the object is moving to (use `orientation.heading` to get the
// heading the nose is pointing to).
double heading = 1;
// The horizontal speed of the unit. If it is doing mach one straight up then
// the speed will be 0
double speed = 2;
// The direction the object is traveling to, and speed (magnitude of the
// vector) the object is traveling with.
Vector velocity = 3;
}
/**
* An instance of a contact in a DCS AI controller's detection table
*
* This is a target that the AI controller has detected and is actively tracking
*
*/
message Contact {
// The DCS generated ID
uint32 id = 1;
// Can the sensor see the contact
bool visible = 2;
// Does the controller know the distance to the contact?
bool distance = 3;
// Either the basic information, or Unit or Weapon export object
oneof target {
Unknown object = 4;
Unit unit = 5;
Weapon weapon = 6;
}
}

View File

@@ -0,0 +1,51 @@
syntax = "proto3";
package dcs.controller.v0;
import "dcs/common/v0/common.proto";
option csharp_namespace = "RurouniJones.Dcs.Grpc.V0.Controller";
option go_package = "github.com/DCS-gRPC/go-bindings/dcs/v0/controller";
service ControllerService {
// https://wiki.hoggitworld.com/view/DCS_option_alarmState
rpc SetAlarmState(SetAlarmStateRequest) returns (SetAlarmStateResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_getDetectedTargets
rpc GetDetectedTargets(GetDetectedTargetsRequest)
returns (GetDetectedTargetsResponse) {}
}
message SetAlarmStateRequest {
enum AlarmState {
ALARM_STATE_UNSPECIFIED = 0;
ALARM_STATE_AUTO = 1;
ALARM_STATE_GREEN = 2;
ALARM_STATE_RED = 3;
}
oneof name {
string group_name = 1;
string unit_name = 2;
}
AlarmState alarm_state = 3;
}
message SetAlarmStateResponse {
}
message GetDetectedTargetsRequest {
enum DetectionType {
DETECTION_TYPE_UNSPECIFIED = 0;
DETECTION_TYPE_VISUAL = 1;
DETECTION_TYPE_OPTIC = 2;
DETECTION_TYPE_RADAR = 4;
DETECTION_TYPE_IRST = 8;
DETECTION_TYPE_RWR = 16;
DETECTION_TYPE_DLINK = 32;
}
string unit_name = 1;
optional bool include_object = 2;
optional DetectionType detection_type = 3;
}
message GetDetectedTargetsResponse {
repeated dcs.common.v0.Contact contacts = 1;
}

View File

@@ -0,0 +1,91 @@
syntax = "proto3";
package dcs.custom.v0;
option csharp_namespace = "RurouniJones.Dcs.Grpc.V0.Custom";
option go_package = "github.com/DCS-gRPC/go-bindings/dcs/v0/custom";
// The Custom service is for APIs that do not map to the "standard library" of
// DCS APIs provided by Eagle Dynamics.
//
// Expect to find APIs here that may be useful for mission frameworks etc.
service CustomService {
// DCT Function
rpc RequestMissionAssignment(RequestMissionAssignmentRequest)
returns (RequestMissionAssignmentResponse) {}
// DCT Function
rpc JoinMission(JoinMissionRequest) returns (JoinMissionResponse) {}
// DCT Function
rpc AbortMission(AbortMissionRequest) returns (AbortMissionResponse) {}
// DCT Function
rpc GetMissionStatus(GetMissionStatusRequest)
returns (GetMissionStatusResponse) {}
// Evaluate some Lua inside of the mission and return the result as a JSON
// string. Disabled by default.
rpc Eval(EvalRequest) returns (EvalResponse) {}
/**
* Calculates the magnetic declination at the given position using the
* International Geomagnetic Reference Field (IGRF) model. The result is not
* always exactly the same as what DCS seem to use, but it is very close (DCS
* doesn't expose its declination).
*/
rpc GetMagneticDeclination(GetMagneticDeclinationRequest)
returns (GetMagneticDeclinationResponse) {}
}
message RequestMissionAssignmentRequest {
string unit_name = 1;
string mission_type = 2;
}
message RequestMissionAssignmentResponse {
}
message JoinMissionRequest {
string unit_name = 1;
int32 mission_code = 2;
}
message JoinMissionResponse {
}
message AbortMissionRequest {
string unit_name = 1;
}
message AbortMissionResponse {
}
message GetMissionStatusRequest {
string unit_name = 1;
}
message GetMissionStatusResponse {
}
message EvalRequest {
string lua = 1;
}
message EvalResponse {
string json = 1;
}
message GetMagneticDeclinationRequest {
/// Latitude in Decimal Degrees format
double lat = 1;
/// Longitude in Decimal Degrees format
double lon = 2;
/// Altitude in Meters above Mean Sea Level (MSL)
double alt = 3;
}
message GetMagneticDeclinationResponse {
/// Magnetic declination in degrees. A negative value is an westerly
/// declination, while a positive value is a easterly declination. `True
/// North` + `declination` = `Magnetic North`
double declination = 1;
}

View File

@@ -0,0 +1,18 @@
syntax = "proto3";
package dcs;
import "dcs/atmosphere/v0/atmosphere.proto";
import "dcs/coalition/v0/coalition.proto";
import "dcs/common/v0/common.proto";
import "dcs/controller/v0/controller.proto";
import "dcs/custom/v0/custom.proto";
import "dcs/group/v0/group.proto";
import "dcs/hook/v0/hook.proto";
import "dcs/mission/v0/mission.proto";
import "dcs/net/v0/net.proto";
import "dcs/srs/v0/srs.proto";
import "dcs/timer/v0/timer.proto";
import "dcs/trigger/v0/trigger.proto";
import "dcs/unit/v0/unit.proto";
import "dcs/world/v0/world.proto";

View File

@@ -0,0 +1,42 @@
syntax = "proto3";
package dcs.group.v0;
import "dcs/common/v0/common.proto";
option csharp_namespace = "RurouniJones.Dcs.Grpc.V0.Group";
option go_package = "github.com/DCS-gRPC/go-bindings/dcs/v0/group";
// https://wiki.hoggitworld.com/view/DCS_Class_Group
service GroupService {
// https://wiki.hoggitworld.com/view/DCS_func_getUnits
rpc GetUnits(GetUnitsRequest) returns (GetUnitsResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_activate
rpc Activate(ActivateRequest) returns (ActivateResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_destroy
rpc Destroy(DestroyRequest) returns (DestroyResponse) {}
}
message GetUnitsRequest {
string group_name = 1;
// Whether the response should include only active units (`true`), only
// inactive units (`false`), or all units (`nil`).
optional bool active = 2;
}
message GetUnitsResponse {
repeated dcs.common.v0.Unit units = 1;
}
message ActivateRequest {
string group_name = 1;
}
message ActivateResponse {
}
message DestroyRequest {
string group_name = 1;
}
message DestroyResponse {
}

View File

@@ -0,0 +1,239 @@
syntax = "proto3";
package dcs.hook.v0;
option csharp_namespace = "RurouniJones.Dcs.Grpc.V0.Hook";
option go_package = "github.com/DCS-gRPC/go-bindings/dcs/v0/hook";
// APis that are part of the hook environment
service HookService {
// https://wiki.hoggitworld.com/view/DCS_func_getMissionName
rpc GetMissionName(GetMissionNameRequest) returns (GetMissionNameResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_getMissionFilename
rpc GetMissionFilename(GetMissionFilenameRequest)
returns (GetMissionFilenameResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_getMissionDescription
rpc GetMissionDescription(GetMissionDescriptionRequest)
returns (GetMissionDescriptionResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_getPause
rpc GetPaused(GetPausedRequest) returns (GetPausedResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_setPause
rpc SetPaused(SetPausedRequest) returns (SetPausedResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_stopMission
rpc StopMission(StopMissionRequest) returns (StopMissionResponse) {}
// Reload the currently running mission
rpc ReloadCurrentMission(ReloadCurrentMissionRequest)
returns (ReloadCurrentMissionResponse) {}
// Load the next mission in the server mission list. Note that it does
// not loop back to the first mission once the end of the mission list
// has been reached
rpc LoadNextMission(LoadNextMissionRequest)
returns (LoadNextMissionResponse) {}
// Load a specific mission file. This does not need to be in the mission
// list.
rpc LoadMission(LoadMissionRequest)
returns (LoadMissionResponse) {}
// Evaluate some Lua inside of the hook environment and return the result as a
// JSON string. Disabled by default.
rpc Eval(EvalRequest) returns (EvalResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_exitProcess
rpc ExitProcess(ExitProcessRequest) returns (ExitProcessResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_isMultiplayer
rpc IsMultiplayer(IsMultiplayerRequest) returns (IsMultiplayerResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_isServer
rpc IsServer(IsServerRequest) returns (IsServerResponse) {}
// Bans a player that is currently connected to the server
rpc BanPlayer(BanPlayerRequest) returns (BanPlayerResponse) {}
// Unbans a player via their globally unique ID
rpc UnbanPlayer(UnbanPlayerRequest) returns (UnbanPlayerResponse) {}
// Get a list of all the banned players
rpc GetBannedPlayers(GetBannedPlayersRequest)
returns (GetBannedPlayersResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_getUnitType
rpc GetUnitType(GetUnitTypeRequest) returns (GetUnitTypeResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_getRealTime
rpc GetRealTime(GetRealTimeRequest) returns (GetRealTimeResponse) {}
// Get a count of ballistics objects
rpc GetBallisticsCount(GetBallisticsCountRequest)
returns (GetBallisticsCountResponse) {}
}
message GetMissionNameRequest {
}
message GetMissionNameResponse {
string name = 1;
}
message GetMissionFilenameRequest {
}
message GetMissionFilenameResponse {
string name = 1;
}
message GetMissionDescriptionRequest {
}
message GetMissionDescriptionResponse {
string description = 1;
}
message GetPausedRequest {
}
message GetPausedResponse {
bool paused = 1;
}
message SetPausedRequest {
bool paused = 1;
}
message SetPausedResponse {
}
message ReloadCurrentMissionRequest {
}
message ReloadCurrentMissionResponse {
}
message LoadNextMissionRequest {
}
message LoadNextMissionResponse {
// Was the next mission successfully loaded. SHOULD return false when the
// end of the mission list has been reached but DCS appears to always
// return true
bool loaded = 1;
}
message LoadMissionRequest {
// The full path to the .miz file to be loaded
string file_name = 1;
}
message LoadMissionResponse {
}
message StopMissionRequest {
}
message StopMissionResponse {
}
message EvalRequest {
string lua = 1;
}
message EvalResponse {
string json = 1;
}
message ExitProcessRequest {
}
message ExitProcessResponse {
}
message IsMultiplayerRequest {
}
message IsMultiplayerResponse {
bool multiplayer = 1;
}
message IsServerRequest {
}
message IsServerResponse {
bool server = 1;
}
message BanPlayerRequest {
// The session ID of the player
uint32 id = 1;
// The period of the ban in seconds
uint32 period = 2;
// The reason for the ban
string reason = 3;
}
message BanPlayerResponse {
// Was the player successfully banned
bool banned = 1;
}
message UnbanPlayerRequest {
// The globally unique ID of the player
string ucid = 1;
}
message UnbanPlayerResponse {
// Was the player successfully unbanned
bool unbanned = 1;
}
message GetBannedPlayersRequest {
}
message GetBannedPlayersResponse {
repeated BanDetails bans = 1;
}
message BanDetails {
// The globally unique ID of the player
string ucid = 1;
// The IP address the user had when they were banned
string ip_address = 2;
// The Name of the player at the time of the ban
string player_name = 3;
// The reason given for the ban
string reason = 4;
// When the ban was issued in unixtime
uint64 banned_from = 5;
// When the ban will expire in unixtime
uint64 banned_until = 6;
}
message GetUnitTypeRequest {
// The slot or unit ID of the unit to retrieve the type of
string id = 1;
}
message GetUnitTypeResponse {
// Type of unit (e.g. "F-14B")
string type = 1;
}
message GetRealTimeRequest {
}
message GetRealTimeResponse {
// The current time in a mission relative to the DCS start time
double time = 1;
}
message GetBallisticsCountRequest {
}
message GetBallisticsCountResponse {
uint32 count = 1;
}

View File

@@ -0,0 +1,780 @@
syntax = "proto3";
package dcs.mission.v0;
import "dcs/common/v0/common.proto";
import "google/protobuf/struct.proto";
option csharp_namespace = "RurouniJones.Dcs.Grpc.V0.Mission";
option go_package = "github.com/DCS-gRPC/go-bindings/dcs/v0/mission";
// the "path" field in mission command requests and responses is a repeated
// string however "paths" doesn't make sense. therefore we will disable
// the linter pluralization checks for this file.
// protolint:disable REPEATED_FIELD_NAMES_PLURALIZED
// Contains the streaming APIs that streaming information out of the DCS server.
service MissionService {
// Streams DCS game generated Events.
// See https://wiki.hoggitworld.com/view/Category:Events
rpc StreamEvents(StreamEventsRequest) returns (stream StreamEventsResponse) {}
// Streams unit updates
// Provides similar functionality as Tacview but at a much lower update rate
// so puts less load on the server. Suitable for things like online maps but
// not as a Tacview replacement.
rpc StreamUnits(StreamUnitsRequest) returns (stream StreamUnitsResponse) {}
// Returns the mission's in-game starttime as an ISO 8601 formatted datetime
// string.
rpc GetScenarioStartTime(GetScenarioStartTimeRequest)
returns (GetScenarioStartTimeResponse) {}
// Returns the mission's in-game current time as an ISO 8601 formatted
// datetime string.
rpc GetScenarioCurrentTime(GetScenarioCurrentTimeRequest)
returns (GetScenarioCurrentTimeResponse) {}
// Adds a new mission command
// See https://wiki.hoggitworld.com/view/DCS_func_addCommand
rpc AddMissionCommand(AddMissionCommandRequest)
returns (AddMissionCommandResponse) {}
// Adds a new command sub menu
// See https://wiki.hoggitworld.com/view/DCS_func_addSubMenu
rpc AddMissionCommandSubMenu(AddMissionCommandSubMenuRequest)
returns (AddMissionCommandSubMenuResponse) {}
// Removes a registered mission command.
// See https://wiki.hoggitworld.com/view/DCS_func_removeItem
rpc RemoveMissionCommandItem(RemoveMissionCommandItemRequest)
returns (RemoveMissionCommandItemResponse) {}
// Adds a new coalition command
// See https://wiki.hoggitworld.com/view/DCS_func_addCommandForCoalition
rpc AddCoalitionCommand(AddCoalitionCommandRequest)
returns (AddCoalitionCommandResponse) {}
// Adds a new coalition command sub menu
// See https://wiki.hoggitworld.com/view/DCS_func_addSubMenuForCoalition
rpc AddCoalitionCommandSubMenu(AddCoalitionCommandSubMenuRequest)
returns (AddCoalitionCommandSubMenuResponse) {}
// Removes a registered coalition command.
// See https://wiki.hoggitworld.com/view/DCS_func_removeItemForCoalition
rpc RemoveCoalitionCommandItem(RemoveCoalitionCommandItemRequest)
returns (RemoveCoalitionCommandItemResponse) {}
// Adds a new group command
// See https://wiki.hoggitworld.com/view/DCS_func_addCommandForGroup
rpc AddGroupCommand(AddGroupCommandRequest)
returns (AddGroupCommandResponse) {}
// Adds a new group command sub menu
// See https://wiki.hoggitworld.com/view/DCS_func_addSubMenuForGroup
rpc AddGroupCommandSubMenu(AddGroupCommandSubMenuRequest)
returns (AddGroupCommandSubMenuResponse) {}
// Removes a group coalition command.
// See https://wiki.hoggitworld.com/view/DCS_func_removeItemForGroup
rpc RemoveGroupCommandItem(RemoveGroupCommandItemRequest)
returns (RemoveGroupCommandItemResponse) {}
// Returns an ID for the current session.
// The ID will change upon mission change or server restart.
rpc GetSessionId(GetSessionIdRequest)
returns (GetSessionIdResponse) {}
}
message StreamEventsRequest {
}
// The DCS Event information. Contains event information and a timestamp.
message StreamEventsResponse {
// Occurs when a unit fires a weapon (but no machine gun- or autocannon-based
// weapons - those are handled by [ShootingStartEvent]).
message ShotEvent {
// The object that fired the weapon.
dcs.common.v0.Initiator initiator = 1;
// The weapon that has been fired.
dcs.common.v0.Weapon weapon = 2;
}
// Occurs when an object is hit by a weapon.
message HitEvent {
// The object that fired the weapon. Not set when for example fyling an
// aircraft into a building (building will be the target and weapon_name the
// name of the aircraft).
optional dcs.common.v0.Initiator initiator = 1;
// The weapon that the target has been hit with.
dcs.common.v0.Weapon weapon = 2;
// The object that has been hit.
dcs.common.v0.Target target = 3;
// The weapon the target got hit by.
optional string weapon_name = 4;
}
// Occurs when an aircraft takes off from an airbase, farp, or ship.
message TakeoffEvent {
// The object that took off.
dcs.common.v0.Initiator initiator = 1;
// The airbase, farp or ship the unit took off from.
dcs.common.v0.Airbase place = 2;
}
// Occurs when an aircraft lands at an airbase, farp or ship.
message LandEvent {
// The object that landed.
dcs.common.v0.Initiator initiator = 1;
// The airbase, farp or ship the unit landed at.
dcs.common.v0.Airbase place = 2;
}
// Occurs when an aircraft crashes into the ground and is completely
// destroyed.
message CrashEvent {
// The object that crashed.
dcs.common.v0.Initiator initiator = 1;
}
// Occurs when a pilot ejects from its aircraft.
message EjectionEvent {
// The unit a pilot ejected from.
dcs.common.v0.Initiator initiator = 1;
// The ejection seat.
dcs.common.v0.Target target = 3;
}
// Occurs when an aircraft connects with a tanker and begins taking on fuel.
message RefuelingEvent {
// The object that is receiving fuel.
dcs.common.v0.Initiator initiator = 1;
}
// Occurs when an object is completely destroyed.
message DeadEvent {
// The object that has been destroyed.
dcs.common.v0.Initiator initiator = 1;
}
// Occurs when a pilot of an aircraft is killed. Can occur either if the
// player is alive and crashes (in this case both this and the [CrashEvent]
// event will be fired) or if a weapon kills the pilot without completely
// destroying the plane.
message PilotDeadEvent {
// The unit the pilot has died in.
dcs.common.v0.Initiator initiator = 1;
}
// Occurs when a ground unit captures either an airbase or a farp.
message BaseCaptureEvent {
// The object that captured the base.
dcs.common.v0.Initiator initiator = 1;
// The airbase that was captured, can be a FARP or Airbase
dcs.common.v0.Airbase place = 2;
}
// Occurs when the mission starts.
message MissionStartEvent {
}
// Occurs when the mission stops.
message MissionEndEvent {
}
// Occurs when an aircraft is finished taking fuel.
message RefuelingStopEvent {
// he unit that was receiving fuel.
dcs.common.v0.Initiator initiator = 1;
}
// Occurs when any object is spawned into the mission.
message BirthEvent {
// The object that was spawned.
dcs.common.v0.Initiator initiator = 1;
// The airbase, farp or ship the unit took off from.
optional dcs.common.v0.Airbase place = 2;
}
// Occurs e.g. when a player controlled aircraft blacks out.
message HumanFailureEvent {
// The unit the system failure occurred in.
dcs.common.v0.Initiator initiator = 1;
}
// Occurs when a system on an aircraft fails. This can be due to damage or due
// to random failures set up in the mission editor.
message DetailedFailureEvent {
// The target the failure occurred for.
dcs.common.v0.Target target = 1;
}
// Occurs when any aircraft starts its engines.
message EngineStartupEvent {
// The object that starts its engines.
dcs.common.v0.Initiator initiator = 1;
// The airbase, farp or ship the unit started their engine at.
dcs.common.v0.Airbase place = 2;
}
message EngineShutdownEvent {
// Occurs when any aircraft shuts down its engines.
dcs.common.v0.Initiator initiator = 1;
// The airbase, farp or ship the unit shut down their engine at.
dcs.common.v0.Airbase place = 2;
}
// Occurs when a player takes direct control of a unit.
message PlayerEnterUnitEvent {
// The unit the player took control of.
dcs.common.v0.Initiator initiator = 1;
}
// Occurs when a player relieves direct control of a unit.
message PlayerLeaveUnitEvent {
// The unit the player relieves control of.
dcs.common.v0.Initiator initiator = 1;
}
// Occurs when a unit begins firing a machine gun- or autocannon-based weapon
// (weapons with a high rate of fire). Other weapons are handled by
// [ShotEvent].
message ShootingStartEvent {
// The object that started firing.
dcs.common.v0.Initiator initiator = 1;
// The name of the shoot weapon.
string weapon_name = 2;
}
// Occurs when a unit stops firing a machine gun- or autocannon-based weapon.
// Event will always correspond with a [ShootingStartEvent] event.
message ShootingEndEvent {
// The object that was shooting and has no stopped firing.
dcs.common.v0.Initiator initiator = 1;
// The name of the shoot weapon.
string weapon_name = 2;
}
// Occurs when marks get added to the mission by players or scripting
// functions.
message MarkAddEvent {
// The object that added the mark.
dcs.common.v0.Initiator initiator = 1;
oneof visibility {
// The group the mark's visibility is restricted for.
uint64 group_id = 2;
// The coalition the mark's visibility is restricted for.
dcs.common.v0.Coalition coalition = 3;
}
// The mark's id.
uint32 id = 4;
// The position the mark has been added at.
dcs.common.v0.Position position = 5;
// The mark's label.
string text = 6;
}
// Occurs when marks got changed.
message MarkChangeEvent {
// The object that changed the mark.
dcs.common.v0.Initiator initiator = 1;
oneof visibility {
// The group the mark's visibility is restricted for.
uint64 group_id = 2;
// The coalition the mark's visibility is restricted for.
dcs.common.v0.Coalition coalition = 3;
}
// The mark's id.
uint32 id = 4;
// The position of the changed mark.
dcs.common.v0.Position position = 5;
// The mark's label.
string text = 6;
}
// Occurs when marks get removed.
message MarkRemoveEvent {
// The object that removed the mark.
dcs.common.v0.Initiator initiator = 1;
oneof visibility {
// The group the mark's visibility is restricted for.
uint64 group_id = 2;
// The coalition the mark's visibility is restricted for.
dcs.common.v0.Coalition coalition = 3;
}
// The mark's id.
uint32 id = 4;
// The position the mark has been removed from.
dcs.common.v0.Position position = 5;
// The mark's label.
string text = 6;
}
// Occurs when an object is killed by a weapon.
message KillEvent {
// The object that fired the weapon.
dcs.common.v0.Initiator initiator = 1;
// The weapon that the target has been killed with.
dcs.common.v0.Weapon weapon = 2;
// The object that has been killed.
dcs.common.v0.Target target = 3;
// The name of the weapon that killed the target (exists instead of weapon
// for weapons that trigger the shooting start and end events).
optional string weapon_name = 4;
}
// A score change (doesn't contain any useful information)
message ScoreEvent {
}
// A unit got destroyed.
message UnitLostEvent {
// The object that got destroyed weapon.
dcs.common.v0.Initiator initiator = 1;
}
// A pilot detached from their ejection seat.
message LandingAfterEjectionEvent {
// The ejected pilot.
dcs.common.v0.Initiator initiator = 1;
// The position the pilot landed at.
dcs.common.v0.Position place = 2;
}
// A pilot detached from their ejection seat.
message DiscardChairAfterEjectionEvent {
// The ejection seat.
dcs.common.v0.Initiator initiator = 1;
// The pilot.
dcs.common.v0.Target target = 2;
}
// Fired for each payload of an aircraft spawened midair.
message WeaponAddEvent {
// The object that got spawned.
dcs.common.v0.Initiator initiator = 1;
// The name of the payload.
string weapon_name = 2;
}
// Occurs when an aircraft receives an LSO rating after recovering on an
// aircraft carrier.
message LandingQualityMarkEvent {
// The aircraft that received the rating.
dcs.common.v0.Initiator initiator = 1;
// The rating.
string comment = 2;
// The ship the unit landed at.
dcs.common.v0.Airbase place = 3;
}
// Occurs when a chat message is sent on the server
message PlayerSendChatEvent {
// The player's id in the current server session.
uint32 player_id = 1;
// what was typed
string message = 2;
}
// fired when the player changes across to a slot
message PlayerChangeSlotEvent {
// The player's id in the current server session.
uint32 player_id = 1;
// The slot's coalition
dcs.common.v0.Coalition coalition = 2;
// The slot's identifier
string slot_id = 3;
}
/**
* Fired when a player connected to the server.
*/
message ConnectEvent {
// The player's IP and port.
string addr = 1;
// The name of the player.
string name = 2;
// The player's unique client identifier (used to ban a player).
string ucid = 3;
// The player's id in the current server session
// (used to for name/slot/... changes).
uint32 id = 4;
}
/**
* The reason a player disconnected for.
*/
enum DisconnectReason {
DISCONNECT_REASON_UNSPECIFIED = 0;
DISCONNECT_REASON_THATS_OKAY = 1;
DISCONNECT_REASON_INVALID_ADDRESS = 2;
DISCONNECT_REASON_CONNECT_FAILED = 3;
DISCONNECT_REASON_WRONG_VERSION = 4;
DISCONNECT_REASON_PROTOCOL_ERROR = 5;
DISCONNECT_REASON_TIMEOUT = 6;
DISCONNECT_REASON_INVALID_PASSWORD = 101;
DISCONNECT_REASON_BANNED = 102;
DISCONNECT_REASON_BAD_CALLSIGN = 103;
DISCONNECT_REASON_TAINTED_CLIENT = 104;
DISCONNECT_REASON_KICKED = 105;
DISCONNECT_REASON_REFUSED = 106;
DISCONNECT_REASON_DENIED_TRIAL_ONLY = 107;
}
/**
* Fired when a player disconnected from the server
* (not fired for the server's player).
*/
message DisconnectEvent {
// The player's id in the current server session.
uint32 id = 1;
// The reason a player disconnected for.
DisconnectReason reason = 2;
}
message MissionCommandEvent {
// A struct containing details of the command that was run by a player
google.protobuf.Struct details = 1;
}
message CoalitionCommandEvent {
// The coalition of the player who ran the command
dcs.common.v0.Coalition coalition = 1;
// A struct containing details of the command that was run by a player
google.protobuf.Struct details = 2;
}
message GroupCommandEvent {
// Details of the group to which the player who ran the command is a unit of
dcs.common.v0.Group group = 1;
// A struct containing details of the command that was run by a player
google.protobuf.Struct details = 2;
}
/**
* Fired every second containing simulation FPS information since the previous
* event.
*/
message SimulationFpsEvent {
// The average FPS since the last event.
double average = 1;
}
/**
* Fired for every TTS request that contains the `text_plain` field, for other
* clients to use e.g. for accessibility use-cases.
*/
message TtsEvent {
// The plain text that got transmitted.
string text = 1;
// The radio frequency in Hz the transmission got send to.
uint64 frequency = 2;
// The coalition of the transmission.
dcs.common.v0.Coalition coalition = 3;
// Custom name of the SRS client used for the transmission.
optional string srs_client_name = 4;
}
/**
* Fired every time a player occuping a unit connects to a frequency on SRS.
*/
message SrsConnectEvent {
// The unit that connected to a frequency in SRS.
dcs.common.v0.Unit unit = 1;
// The radio frequency in Hz the unit connected to.
uint64 frequency = 2;
}
/**
* Fired every time a player occuping a unit disconnects from a frequency on
* SRS. It is not fired when the player leaves the unit or the unit dies.
*/
message SrsDisconnectEvent {
// The unit that disconnected from a frequency in SRS.
dcs.common.v0.Unit unit = 1;
// The radio frequency in Hz the unit disconnected from.
uint64 frequency = 2;
}
// The event's mission time.
double time = 1;
oneof event {
ShotEvent shot = 4;
HitEvent hit = 5;
TakeoffEvent takeoff = 6;
LandEvent land = 7;
CrashEvent crash = 8;
EjectionEvent ejection = 9;
RefuelingEvent refueling = 10;
DeadEvent dead = 11;
PilotDeadEvent pilot_dead = 12;
BaseCaptureEvent base_capture = 13;
MissionStartEvent mission_start = 14;
MissionEndEvent mission_end = 15;
// @exclude 16 reserved for S_EVENT_TOOK_CONTROL
RefuelingStopEvent refueling_stop = 17;
BirthEvent birth = 18;
HumanFailureEvent human_failure = 19;
DetailedFailureEvent detailed_failure = 20;
EngineStartupEvent engine_startup = 21;
EngineShutdownEvent engine_shutdown = 22;
PlayerEnterUnitEvent player_enter_unit = 23;
PlayerLeaveUnitEvent player_leave_unit = 24;
// @exclude 25 reserved for S_EVENT_PLAYER_COMMENT
ShootingStartEvent shooting_start = 26;
ShootingEndEvent shooting_end = 27;
MarkAddEvent mark_add = 28;
MarkChangeEvent mark_change = 29;
MarkRemoveEvent mark_remove = 30;
KillEvent kill = 31;
ScoreEvent score = 32;
UnitLostEvent unit_lost = 33;
LandingAfterEjectionEvent landing_after_ejection = 34;
// @exclude 35 reserved for S_EVENT_PARATROOPER_LENDING
DiscardChairAfterEjectionEvent discard_chair_after_ejection = 36;
WeaponAddEvent weapon_add = 37;
// @exclude 38 reserved for S_EVENT_TRIGGER_ZONE
LandingQualityMarkEvent landing_quality_mark = 39;
// @exclude 40 reserved for S_EVENT_BDA
// The following events are additions on top of DCS's own event enum,
// which is why they start at 8192 to give DCS plenty of space for
// new built-in events.
ConnectEvent connect = 8192;
DisconnectEvent disconnect = 8193;
PlayerSendChatEvent player_send_chat = 8194;
PlayerChangeSlotEvent player_change_slot = 8195;
MissionCommandEvent mission_command = 8196;
CoalitionCommandEvent coalition_command = 8197;
GroupCommandEvent group_command = 8198;
SimulationFpsEvent simulation_fps = 8199;
TtsEvent tts = 8200;
SrsConnectEvent srs_connect = 8201;
SrsDisconnectEvent srs_disconnect = 8202;
}
}
message StreamUnitsRequest {
// The poll rate in seconds at which the gRPC server communicates with the DCS
// mission to retrieve the latest unit positions. The lower the `poll_rate`
// the higher the amount of requests send to to the DCS mission. Default: 5
optional uint32 poll_rate = 1;
// The maximum backoff in seconds which the gRPC postpones polling units that
// haven't moved recently. This is an optimization to dynamically reduce the
// poll rate for stationary units. Set it to the same value as `poll_rate` to
// disable the backoff. Default: 30
optional uint32 max_backoff = 2;
// The type of the unit to stream movements. Different categories of units
// would move at different speeds, which allows the stream to be configured
// with the appropriate polling rates. `GROUP_CATEGORY_UNSPECIFIED` would
// return all the units.
dcs.common.v0.GroupCategory category = 3;
}
message StreamUnitsResponse {
message UnitGone {
uint32 id = 1;
string name = 2;
}
double time = 1;
oneof update {
// The unit is either new or its position or attitude changed.
dcs.common.v0.Unit unit = 2;
// The unit does not exist anymore.
UnitGone gone = 3;
}
}
message GetScenarioStartTimeRequest {
}
message GetScenarioStartTimeResponse {
string datetime = 1;
}
message GetScenarioCurrentTimeRequest {
}
message GetScenarioCurrentTimeResponse {
string datetime = 1;
}
// MISSION COMMANDS
// GLOBAL
// Adds an F10 radio command visible to all players in all coalitions.
// When the player activates the command then a `missionCommand` event will be
// emitted to all connected DCS-gRPC clients for processing as they see fit.
message AddMissionCommandRequest {
// The name of the command that is displayed to the player.
// It will form the last entry in the returned path.
string name = 1;
// The menu path the command will appear under. This can be empty if you want
// the command to be on the first level under the F10 menu. This path must
// already have been created.
repeated string path = 2;
// A struct containing data that will be included in the emitted event to the
// DCS-gRPC clients
google.protobuf.Struct details = 3;
}
message AddMissionCommandResponse {
// The full path to the command, including the command name. Use this path to
// delete the command.
repeated string path = 1;
}
message AddMissionCommandSubMenuRequest {
// The name of the submenu that is displayed to the player.
// It will form the last entry in the returned path.
string name = 1;
// The menu path the submenu will appear under. This can be empty if you want
// the submenu to be on the first level under the F10 menu. This path must
// already have been created using this command. you cannot create a nested
// submenu tree in one command.
repeated string path = 2;
}
message AddMissionCommandSubMenuResponse {
// The full path to the submenu, including the submenu name. Use this path to
// add another submenu or command underneath it or delete the submenu.
repeated string path = 1;
}
message RemoveMissionCommandItemRequest {
// The full path to the menu item, which can be a submenu or a command, to be
// removed. Deleting a menu item will delete all children it may have.
repeated string path = 1;
}
message RemoveMissionCommandItemResponse {
}
// COALITION
// Adds an F10 radio command visible to all players in the specified coalition.
// When the player activates the command then a `coalitionCommand` event will
// be emitted to all connected DCS-gRPC clients for processing as they see fit.
// The emitted event will include the coalition.
message AddCoalitionCommandRequest {
// The coalition whose players will be able to see and run the command
dcs.common.v0.Coalition coalition = 1;
// The name of the command that is displayed to the player.
// It will form the last entry in the returned path.
string name = 2;
// The menu path the command will appear under. This can be empty if you want
// the command to be on the first level under the F10 menu. This path must
// already have been created.
repeated string path = 3;
// A struct containing data that will be included in the emitted event to the
// DCS-gRPC clients
google.protobuf.Struct details = 4;
}
message AddCoalitionCommandResponse {
// The full path to the command, including the command name. Use this path to
// delete the command.
repeated string path = 1;
}
message AddCoalitionCommandSubMenuRequest {
// The coalition whose players will be able to see the submenu
dcs.common.v0.Coalition coalition = 1;
// The name of the submenu that is displayed to the player.
// It will form the last entry in the returned path.
string name = 2;
// The menu path the submenu will appear under. This can be empty if you want
// the submenu to be on the first level under the F10 menu. This path must
// already have been created using this command. you cannot create a nested
// submenu tree in one command.
repeated string path = 3;
}
message AddCoalitionCommandSubMenuResponse {
// The full path to the submenu, including the submenu name. Use this path to
// add another submenu or command underneath it or delete the submenu.
repeated string path = 1;
}
message RemoveCoalitionCommandItemRequest {
// The coalition whose players will have the menu item removed
dcs.common.v0.Coalition coalition = 1;
// The full path to the menu item, which can be a submenu or a command, to be
// removed. Deleting a menu item will delete all children it may have.
repeated string path = 2;
}
message RemoveCoalitionCommandItemResponse {
}
// GROUP
// Adds an F10 radio command visible to all players in the specified group.
// When the player activates the command then a `groupCommand` event will
// be emitted to all connected DCS-gRPC clients for processing as they see fit.
// The emitted event will include the group name.
message AddGroupCommandRequest {
// The name of the group whose players will be able to see and execute the
// command. TODO (Figure out if this persists across spawns)
string group_name = 1;
// The name of the command that is displayed to the player.
// It will form the last entry in the returned path.
string name = 2;
// The menu path the command will appear under. This can be empty if you want
// the command to be on the first level under the F10 menu. This path must
// already have been created.
repeated string path = 3;
// A struct containing data that will be included in the emitted event to the
// DCS-gRPC clients
google.protobuf.Struct details = 4;
}
message AddGroupCommandResponse {
// The full path to the command, including the command name. Use this path to
// delete the command.
repeated string path = 1;
}
message AddGroupCommandSubMenuRequest {
// The name of the group whose players will be able to see the submenu
string group_name = 1;
// The name of the submenu that is displayed to the player.
// It will form the last entry in the returned path.
string name = 2;
// The menu path the submenu will appear under. This can be empty if you want
// the submenu to be on the first level under the F10 menu. This path must
// already have been created using this command. you cannot create a nested
// submenu tree in one command.
repeated string path = 3;
}
message AddGroupCommandSubMenuResponse {
// The full path to the submenu, including the submenu name. Use this path to
// add another submenu or command underneath it or delete the submenu.
repeated string path = 1;
}
message RemoveGroupCommandItemRequest {
// The group whose players will have the menu item removed
string group_name = 1;
// The full path to the menu item, which can be a submenu or a command, to be
// removed. Deleting a menu item will delete all children it may have.
repeated string path = 2;
}
message RemoveGroupCommandItemResponse {
}
message GetSessionIdRequest {
}
message GetSessionIdResponse {
int64 session_id = 1;
}

View File

@@ -0,0 +1,93 @@
syntax = "proto3";
package dcs.net.v0;
import "dcs/common/v0/common.proto";
option csharp_namespace = "RurouniJones.Dcs.Grpc.V0.Net";
option go_package = "github.com/DCS-gRPC/go-bindings/dcs/v0/net";
service NetService {
// https://wiki.hoggitworld.com/view/DCS_func_send_chat_to
rpc SendChatTo(SendChatToRequest) returns (SendChatToResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_send_chat
rpc SendChat(SendChatRequest) returns (SendChatResponse) {}
// returns a list of all connected players.
// https://wiki.hoggitworld.com/view/DCS_func_get_player_info
rpc GetPlayers(GetPlayersRequest) returns (GetPlayersResponse) {}
// Kick a specified player from the server with a message
// https://wiki.hoggitworld.com/view/DCS_func_kick
rpc KickPlayer(KickPlayerRequest) returns (KickPlayerResponse) {}
// Force a player into a slot / coalition.
// To move the player back into spectators, use the following pseudo:
// `ForcePlayerSlot({ player_id: ..., coalition: NEUTRAL, slot_id: "" })`
rpc ForcePlayerSlot(ForcePlayerSlotRequest)
returns (ForcePlayerSlotResponse) {}
}
message SendChatToRequest {
// the message to send in the chat
string message = 1;
// the target player of the direct message
uint32 target_player_id = 2;
}
message SendChatToResponse {
}
message SendChatRequest {
// the message to send in the chat
string message = 1;
// which coalition? DCS only supports ALL or NEUTRAL
// (only applicable to send_chat)
dcs.common.v0.Coalition coalition = 2;
}
message SendChatResponse {
}
message GetPlayersRequest {
}
message GetPlayersResponse {
message GetPlayerInfo {
// the player id
uint32 id = 1;
// player's online name
string name = 2;
// coalition which player is slotted in
dcs.common.v0.Coalition coalition = 3;
// the slot identifier
string slot = 4;
// the ping of the player
uint32 ping = 5;
// the connection ip address and port the client
// has established with the server
string remote_address = 6;
// the unique identifier for the player
string ucid = 7;
// abbreviated language (locale) e.g. "en"
string locale = 8;
}
// list of all the players connected to the server
repeated GetPlayerInfo players = 1;
}
message ForcePlayerSlotRequest {
uint32 player_id = 1;
dcs.common.v0.Coalition coalition = 2;
string slot_id = 3;
}
message ForcePlayerSlotResponse {
}
message KickPlayerRequest {
uint32 id = 1;
string message = 2;
}
message KickPlayerResponse {
}

View File

@@ -0,0 +1,106 @@
syntax = "proto3";
package dcs.srs.v0;
import "dcs/common/v0/common.proto";
option csharp_namespace = "RurouniJones.Dcs.Grpc.V0.Srs";
option go_package = "github.com/DCS-gRPC/go-bindings/dcs/v0/srs";
service SrsService {
// Synthesize text to speech and transmit it over SRS. By default, this blocks until a
// transmission completed (unless `async` is set to `true`). This can be used to prevent
// transmission to overlap each other, by not sending another transmission on the same frequency
// until you've received the response from the previous transmission on that frequency. However,
// it does not block or prevent any other client from transmitting over the same frequency at the
// same time.
rpc Transmit(TransmitRequest) returns (TransmitResponse) {}
// Retrieve a list of units (players) and their active frequencies that are connected to SRS.
rpc GetClients(GetClientsRequest) returns (GetClientsResponse) {}
}
message TransmitRequest {
// The text that is synthesized to speech and transmitted to SRS. Supports SSML tags (you should
// not wrap the text in the root `<speak>` tag though).
string ssml = 1;
// The plain text without any transformations made to it for the purpose of getting it spoken out
// as desired (no SSML tags, no FOUR NINER instead of 49, ...). Even though this field is
// optional, please consider providing it as it can be used to display the spoken text to players
// with hearing impairments.
optional string plaintext = 2;
// The radio frequency in Hz the transmission is send to. Example: 251000000
// for 251.00MHz.
uint64 frequency = 3;
// Name of the SRS client. Defaults to "DCS-gRPC".
optional string srs_client_name = 4;
// The origin of the transmission. Relevant if the SRS server has "Line of
// Sight" and/or "Distance Limit" enabled.
dcs.common.v0.InputPosition position = 5;
// The coalition of the transmission. Relevant if the SRS server has "Secure
// Coalition Radios" enabled. Only Blue and Red are supported, all other
// values will fallback to Spectator.
dcs.common.v0.Coalition coalition = 6;
// Whether to keep the request open until the whole transmission was sent. If
// enabled, you can send the next transmission after you've received the
// response for the previous one and be sure that they don't overlap (talk
// over each other). If disabled, you'll receive a response right away (kind
// of fire and forget). You can use the returned duration as a spacing between
// TTS requests to prevent the overlap of multiple playbacks yourself.
bool async =7;
message Aws {
// The voice the text is synthesized in, see:
// https://docs.aws.amazon.com/polly/latest/dg/voicelist.html
optional string voice = 1;
}
message Azure {
// The voice the text is synthesized in, see:
// https://learn.microsoft.com/azure/cognitive-services/speech-service/language-support
optional string voice = 1;
}
message GCloud {
// The voice the text is synthesized in, see:
// https://cloud.google.com/text-to-speech/docs/voices
optional string voice = 1;
}
message Windows {
// The voice the text is synthesized in.
optional string voice = 1;
}
// Optional TTS provider to be use. Defaults to the one configured in your
// config or to Windows' built-in TTS.
oneof provider {
Aws aws = 8;
Azure azure = 9;
GCloud gcloud = 10;
Windows win = 11;
}
}
message TransmitResponse {
// The duration in milliseconds it roughly takes to speak the transmission.
uint32 duration_ms = 1;
}
message GetClientsRequest {
}
message GetClientsResponse {
message Client {
// The unit that is connected to SRS.
dcs.common.v0.Unit unit = 1;
// The radio frequencies in Hz the unit is connected to.
repeated uint64 frequencies = 2;
}
repeated Client clients = 1;
}

View File

@@ -0,0 +1,46 @@
syntax = "proto3";
package dcs.timer.v0;
option csharp_namespace = "RurouniJones.Dcs.Grpc.V0.Timer";
option go_package = "github.com/DCS-gRPC/go-bindings/dcs/v0/timer";
// https://wiki.hoggitworld.com/view/DCS_singleton_timer
service TimerService {
// https://wiki.hoggitworld.com/view/DCS_func_getTime
rpc GetTime(GetTimeRequest) returns (GetTimeResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_getAbsTime
rpc GetAbsoluteTime(GetAbsoluteTimeRequest)
returns (GetAbsoluteTimeResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_getTime0
rpc GetTimeZero(GetTimeZeroRequest) returns (GetTimeZeroResponse) {}
}
message GetTimeRequest {
}
message GetTimeResponse {
double time = 1;
}
message GetAbsoluteTimeRequest {
}
message GetAbsoluteTimeResponse {
// The current time in seconds since 00:00 of the start date of the mission.
double time = 1;
uint32 day = 2;
uint32 month = 3;
int32 year = 4;
}
message GetTimeZeroRequest {
}
message GetTimeZeroResponse {
// The time in seconds since 00:00.
double time = 1;
uint32 day = 2;
uint32 month = 3;
int32 year = 4;
}

View File

@@ -0,0 +1,279 @@
syntax = "proto3";
package dcs.trigger.v0;
import "dcs/common/v0/common.proto";
option csharp_namespace = "RurouniJones.Dcs.Grpc.V0.Trigger";
option go_package = "github.com/DCS-gRPC/go-bindings/dcs/v0/trigger";
// https://wiki.hoggitworld.com/view/DCS_singleton_trigger
service TriggerService {
// https://wiki.hoggitworld.com/view/DCS_func_outText
rpc OutText(OutTextRequest) returns (OutTextResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_outTextForCoalition
rpc OutTextForCoalition(OutTextForCoalitionRequest)
returns (OutTextForCoalitionResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_outTextForGroup
rpc OutTextForGroup(OutTextForGroupRequest)
returns (OutTextForGroupResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_outTextForUnit
rpc OutTextForUnit(OutTextForUnitRequest)
returns (OutTextForUnitResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_getUserFlag
rpc GetUserFlag(GetUserFlagRequest) returns (GetUserFlagResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_setUserFlag
rpc SetUserFlag(SetUserFlagRequest) returns (SetUserFlagResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_markToAll
rpc MarkToAll(MarkToAllRequest) returns (MarkToAllResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_markToCoalition
rpc MarkToCoalition(MarkToCoalitionRequest)
returns (MarkToCoalitionResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_markToGroup
rpc MarkToGroup(MarkToGroupRequest) returns (MarkToGroupResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_markupToAll
rpc MarkupToAll(MarkupToAllRequest) returns (MarkupToAllResponse) {}
// Uses markupToAll under the hood but enforces a coalition to be specified
// https://wiki.hoggitworld.com/view/DCS_func_markupToAll
rpc MarkupToCoalition(MarkupToCoalitionRequest)
returns (MarkupToCoalitionResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_removeMark
rpc RemoveMark(RemoveMarkRequest) returns (RemoveMarkResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_explosion
rpc Explosion(ExplosionRequest) returns (ExplosionResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_smoke
rpc Smoke(SmokeRequest) returns (SmokeResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_illuminationBomb
rpc IlluminationBomb(IlluminationBombRequest)
returns (IlluminationBombResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_signalFlare
rpc SignalFlare(SignalFlareRequest) returns (SignalFlareResponse) {}
}
message OutTextRequest {
string text = 1;
int32 display_time = 2;
bool clear_view = 3;
}
message OutTextResponse {
}
message OutTextForCoalitionRequest {
string text = 1;
int32 display_time = 2;
bool clear_view = 3;
dcs.common.v0.Coalition coalition = 4;
}
message OutTextForCoalitionResponse {
}
message OutTextForGroupRequest {
string text = 1;
int32 display_time = 2;
bool clear_view = 3;
uint32 group_id = 4;
}
message OutTextForGroupResponse {
}
message OutTextForUnitRequest {
string text = 1;
int32 display_time = 2;
bool clear_view = 3;
uint32 unit_id = 4;
}
message OutTextForUnitResponse {
}
message GetUserFlagRequest {
string flag = 1;
}
message GetUserFlagResponse {
uint32 value = 1;
}
message SetUserFlagRequest {
string flag = 1;
uint32 value = 2;
}
message SetUserFlagResponse {
}
message MarkToAllRequest {
string text = 2;
dcs.common.v0.InputPosition position = 3;
bool read_only = 4;
string message = 5;
}
message MarkToAllResponse {
uint32 id = 1;
}
message MarkToCoalitionRequest {
uint32 id = 1;
string text = 2;
dcs.common.v0.InputPosition position = 3;
dcs.common.v0.Coalition coalition = 4;
bool read_only = 5;
string message = 6;
}
message MarkToCoalitionResponse {
uint32 id = 1;
}
message MarkToGroupRequest {
uint32 id = 1;
string text = 2;
dcs.common.v0.InputPosition position = 3;
uint32 group_id = 4;
bool read_only = 5;
string message = 6;
}
message MarkToGroupResponse {
uint32 id = 1;
}
message RemoveMarkRequest {
uint32 id = 1;
}
message RemoveMarkResponse {
}
message ExplosionRequest {
dcs.common.v0.InputPosition position = 1;
uint32 power = 2;
}
message ExplosionResponse {
}
message SmokeRequest {
enum SmokeColor {
SMOKE_COLOR_UNSPECIFIED = 0;
SMOKE_COLOR_GREEN = 1;
SMOKE_COLOR_RED = 2;
SMOKE_COLOR_WHITE = 3;
SMOKE_COLOR_ORANGE = 4;
SMOKE_COLOR_BLUE = 5;
}
// Altitude parameter will be ignored. Smoke always eminates from ground
// level which will be calculated server-side
dcs.common.v0.InputPosition position = 1;
SmokeColor color = 2;
}
message SmokeResponse {
}
message IlluminationBombRequest {
// The altitude of Illumination Bombs is meters above ground. Ground level
// will be calculated server-side
dcs.common.v0.InputPosition position = 1;
uint32 power = 2;
}
message IlluminationBombResponse {
}
message SignalFlareRequest {
enum FlareColor {
FLARE_COLOR_UNSPECIFIED = 0;
FLARE_COLOR_GREEN = 1;
FLARE_COLOR_RED = 2;
FLARE_COLOR_WHITE = 3;
FLARE_COLOR_YELLOW = 4;
}
// Altitude parameter will be ignored. Signal flares always fire from
// ground level which will be calculated server-side
dcs.common.v0.InputPosition position = 1;
FlareColor color = 2;
uint32 azimuth = 3;
}
message SignalFlareResponse {
}
enum LineType {
// protolint:disable:next ENUM_FIELD_NAMES_ZERO_VALUE_END_WITH
LINE_TYPE_NO_LINE = 0;
LINE_TYPE_SOLID = 1;
LINE_TYPE_DASHED = 2;
LINE_TYPE_DOTTED = 3;
LINE_TYPE_DOT_DASH = 4;
LINE_TYPE_LONG_DASH = 5;
LINE_TYPE_TWO_DASH = 6;
}
// Represents an RGBA color but instead of using 0-255 as the color
// values it uses 0 to 1. A red color with 50% transparency would be
// RGBA of 1, 0, 0, 0.5
message Color {
double red = 1;
double green = 2;
double blue = 3;
double alpha = 4;
}
enum Shape {
SHAPE_UNSPECIFIED = 0;
SHAPE_LINE = 1;
SHAPE_CIRCLE = 2;
SHAPE_RECT = 3;
SHAPE_ARROW = 4;
SHAPE_TEXT = 5;
SHAPE_QUAD = 6;
SHAPE_FREEFORM = 7;
}
message MarkupToAllRequest {
Shape shape = 1;
repeated dcs.common.v0.InputPosition points = 2;
Color border_color = 3;
Color fill_color = 4;
LineType line_type = 5;
bool read_only = 6;
string message = 7;
}
message MarkupToAllResponse {
uint32 id = 1;
}
message MarkupToCoalitionRequest {
Shape shape = 1;
dcs.common.v0.Coalition coalition = 2;
repeated dcs.common.v0.InputPosition points = 3;
Color border_color = 4;
Color fill_color = 5;
LineType line_type = 6;
bool read_only = 7;
string message = 8;
}
message MarkupToCoalitionResponse {
uint32 id = 1;
}

View File

@@ -0,0 +1,106 @@
syntax = "proto3";
package dcs.unit.v0;
import "dcs/common/v0/common.proto";
option csharp_namespace = "RurouniJones.Dcs.Grpc.V0.Unit";
option go_package = "github.com/DCS-gRPC/go-bindings/dcs/v0/unit";
// https://wiki.hoggitworld.com/view/DCS_Class_Unit
service UnitService {
// https://wiki.hoggitworld.com/view/DCS_func_getRadar
rpc GetRadar(GetRadarRequest) returns (GetRadarResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_getPoint
rpc GetPosition(GetPositionRequest) returns (GetPositionResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_getPlayerName
rpc GetPlayerName(GetPlayerNameRequest) returns (GetPlayerNameResponse) {}
rpc GetDescriptor(GetDescriptorRequest) returns (GetDescriptorResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_enableEmission
rpc SetEmission(SetEmissionRequest) returns (SetEmissionResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_getByName
rpc Get(GetRequest) returns (GetResponse) {}
/**
* Get information about the unit in 3D space, including its position,
* orientation and velocity.
*/
rpc GetTransform(GetTransformRequest) returns (GetTransformResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_destroy
rpc Destroy(DestroyRequest) returns (DestroyResponse) {}
}
message GetRadarRequest {
string name = 1;
}
message GetRadarResponse {
bool active = 1;
dcs.common.v0.Target target = 2;
}
message GetPositionRequest {
string name = 1;
}
message GetPositionResponse {
dcs.common.v0.Position position = 1;
}
message GetTransformRequest {
string name = 1;
}
message GetTransformResponse {
// Time in seconds since the scenario started.
double time = 1;
// The position of the unit
dcs.common.v0.Position position = 2;
// The orientation of the unit in both 2D and 3D space
dcs.common.v0.Orientation orientation = 3;
// The velocity of the unit in both 2D and 3D space
dcs.common.v0.Velocity velocity = 4;
}
message GetPlayerNameRequest {
string name = 1;
}
message GetPlayerNameResponse {
optional string player_name = 1;
}
message GetDescriptorRequest {
string name = 1;
}
// TODO fill these in as and when we need em
message GetDescriptorResponse {
repeated string attributes = 1;
}
message SetEmissionRequest {
string name = 1;
bool emitting = 2;
}
message SetEmissionResponse {
}
message GetRequest {
string name = 1;
}
message GetResponse {
dcs.common.v0.Unit unit = 1;
}
message DestroyRequest {
string name = 1;
}
message DestroyResponse {
}

View File

@@ -0,0 +1,39 @@
syntax = "proto3";
package dcs.world.v0;
import "dcs/common/v0/common.proto";
option csharp_namespace = "RurouniJones.Dcs.Grpc.V0.World";
option go_package = "github.com/DCS-gRPC/go-bindings/dcs/v0/world";
// https://wiki.hoggitworld.com/view/DCS_singleton_world
service WorldService {
// https://wiki.hoggitworld.com/view/DCS_func_getAirbases
rpc GetAirbases(GetAirbasesRequest) returns (GetAirbasesResponse) {}
// https://wiki.hoggitworld.com/view/DCS_func_getMarkPanels
rpc GetMarkPanels(GetMarkPanelsRequest) returns (GetMarkPanelsResponse) {}
// Returns the theatre (Map name) of the mission
rpc GetTheatre(GetTheatreRequest) returns (GetTheatreResponse) {}
}
message GetAirbasesRequest {
dcs.common.v0.Coalition coalition = 1;
}
message GetAirbasesResponse {
repeated dcs.common.v0.Airbase airbases = 1;
}
message GetMarkPanelsRequest {
}
message GetMarkPanelsResponse {
repeated dcs.common.v0.MarkPanel mark_panels = 1;
}
message GetTheatreRequest {
}
message GetTheatreResponse {
string theatre = 1;
}

View File

@@ -0,0 +1,3 @@
pub mod v0 {
tonic::include_proto!("dcs.atmosphere.v0");
}

View File

@@ -0,0 +1,3 @@
pub mod v0 {
tonic::include_proto!("dcs.coalition.v0");
}

View File

@@ -0,0 +1,151 @@
pub mod v0 {
use std::ops::Neg;
tonic::include_proto!("dcs.common.v0");
#[derive(Default, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct RawTransform {
pub position: Option<Position>,
pub position_north: Option<Vector>,
pub forward: Option<Vector>,
pub right: Option<Vector>,
pub up: Option<Vector>,
pub velocity: Option<Vector>,
}
pub(crate) struct Transform {
pub position: Position,
pub orientation: Orientation,
pub velocity: Velocity,
}
impl From<RawTransform> for Transform {
fn from(raw: RawTransform) -> Self {
let RawTransform {
position,
position_north,
forward,
right,
up,
velocity,
} = raw;
let position = position.unwrap_or_default();
let position_north = position_north.unwrap_or_default();
let forward = forward.unwrap_or_default();
let right = right.unwrap_or_default();
let up = up.unwrap_or_default();
let velocity = velocity.unwrap_or_default();
let projection_error =
(position_north.z - position.u).atan2(position_north.x - position.v);
let heading = forward.z.atan2(forward.x);
let orientation = Orientation {
heading: {
let heading = heading.to_degrees();
if heading < 0.0 {
heading + 360.0
} else {
heading
}
},
yaw: (heading - projection_error).to_degrees(),
roll: right.y.asin().neg().to_degrees(),
pitch: forward.y.asin().to_degrees(),
forward: Some(forward),
right: Some(right),
up: Some(up),
};
let velocity = Velocity {
heading: {
let heading = velocity.z.atan2(velocity.x).to_degrees();
if heading < 0.0 {
heading + 360.0
} else {
heading
}
},
speed: (velocity.x.powi(2) + velocity.z.powi(2)).sqrt(),
velocity: Some(velocity),
};
Transform {
position,
orientation,
velocity,
}
}
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct UnitIntermediate {
id: u32,
name: String,
callsign: String,
coalition: i32,
r#type: String,
player_name: Option<String>,
group: Option<Group>,
number_in_group: u32,
raw_transform: Option<RawTransform>,
}
impl From<UnitIntermediate> for Unit {
fn from(i: UnitIntermediate) -> Self {
let UnitIntermediate {
id,
name,
callsign,
coalition,
r#type,
player_name,
group,
number_in_group,
raw_transform,
} = i;
let transform = Transform::from(raw_transform.unwrap_or_default());
Unit {
id,
name,
callsign,
coalition,
r#type,
position: Some(transform.position),
orientation: Some(transform.orientation),
velocity: Some(transform.velocity),
player_name,
group,
number_in_group,
}
}
}
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct WeaponIntermediate {
id: u32,
r#type: String,
raw_transform: Option<RawTransform>,
}
impl From<WeaponIntermediate> for Weapon {
fn from(i: WeaponIntermediate) -> Self {
let WeaponIntermediate {
id,
r#type,
raw_transform,
} = i;
let transform = Transform::from(raw_transform.unwrap_or_default());
Weapon {
id,
r#type,
position: Some(transform.position),
orientation: Some(transform.orientation),
velocity: Some(transform.velocity),
}
}
}
}

View File

@@ -0,0 +1,3 @@
pub mod v0 {
tonic::include_proto!("dcs.controller.v0");
}

View File

@@ -0,0 +1,3 @@
pub mod v0 {
tonic::include_proto!("dcs.custom.v0");
}

View File

@@ -0,0 +1,3 @@
pub mod v0 {
tonic::include_proto!("dcs.group.v0");
}

View File

@@ -0,0 +1,3 @@
pub mod v0 {
tonic::include_proto!("dcs.hook.v0");
}

View File

@@ -0,0 +1,3 @@
pub mod v0 {
tonic::include_proto!("dcs.mission.v0");
}

View File

@@ -0,0 +1,278 @@
// Current recommendation as of
// https://github.com/tokio-rs/prost/issues/661#issuecomment-1156606409
#![allow(clippy::derive_partial_eq_without_eq)]
#![allow(clippy::large_enum_variant)]
// use crate::{call::Element, dcs::net::v0::SendChatRequest};
// use std::{cmp::Ordering, collections::HashMap, sync::Arc, time::Duration};
// use tokio::{sync::RwLock, time::sleep};
// use self::{
// common::v0::Coalition,
// mission::v0::{
// mission_service_client::MissionServiceClient, stream_units_response::Update,
// StreamUnitsRequest,
// },
// net::v0::net_service_client::NetServiceClient,
// };
pub mod atmosphere;
pub mod coalition;
pub mod common;
pub mod controller;
pub mod custom;
pub mod group;
pub mod hook;
pub mod mission;
pub mod net;
pub mod srs;
pub mod timer;
pub mod trigger;
pub mod unit;
mod utils;
pub mod world;
// // In the general part of the world DCS is set in, the compass drift is about 5 degrees
// // This should probably be configurable as it depends on the map.
// // (I just remember reading somewhere that the heading drift is about 5°, cant find proof now but in testing it lines up)
// // https://en.wikipedia.org/wiki/Magnetic_declination
// // https://github.com/DCS-gRPC/rust-server/issues/197
// const COMPASS_DRIFT: i32 = -5;
// #[derive(Debug, Clone)]
// pub struct Position {
// pub lat: f64,
// pub long: f64,
// pub altitude: f64,
// }
// impl Position {
// pub fn distance_to_km(&self, other: &Position) -> f64 {
// let radius_of_earth_in_km: f64 = 6371.0;
// let delta_lat = (other.lat - self.lat).to_radians();
// let delta_long = (other.long - self.long).to_radians();
// let a = (delta_lat / 2.0).sin() * (delta_lat / 2.0).sin()
// + self.lat.to_radians().cos()
// * other.lat.to_radians().cos()
// * (delta_long / 2.0).sin()
// * (delta_long / 2.0).sin();
// let c = 2.0 * (a.sqrt().atan2((1.0 - a).sqrt()));
// radius_of_earth_in_km * c
// }
// pub fn distance_to_nmi(&self, other: &Position) -> f64 {
// self.distance_to_km(other) * 0.5399568035
// }
// pub fn get_bearing_to(&self, other: &Position) -> i32 {
// let delta_long = (other.long - self.long).to_radians();
// let y =
// (other.long.to_radians() - self.long.to_radians()).sin() * other.lat.to_radians().cos();
// let x = self.lat.to_radians().cos() * other.lat.to_radians().sin()
// - self.lat.to_radians().sin() * other.lat.to_radians().cos() * delta_long.cos();
// (((y.atan2(x).to_degrees() + 360.0) % 360.0) as i32) + COMPASS_DRIFT
// }
// pub fn feet(&self) -> f64 {
// self.altitude * 3.28084
// }
// pub fn angels(&self) -> i32 {
// (self.feet() / 1000.0).round() as i32
// }
// }
// #[derive(Debug, Clone)]
// pub struct Unit {
// pub id: u32,
// pub name: String,
// pub player: Option<String>,
// pub callsign: String,
// pub group_name: Option<String>,
// pub coalition: i32,
// pub r#type: String,
// pub position: Position,
// pub heading: f64,
// pub speed: f64,
// }
// #[derive(Clone)]
// pub struct RpcClient {
// base_url: String,
// units: Arc<RwLock<HashMap<u32, Unit>>>,
// }
// impl RpcClient {
// pub fn new(base_url: &str) -> Self {
// let units = Arc::new(RwLock::new(HashMap::new()));
// let db = units.clone();
// let url = base_url.to_owned();
// tokio::spawn(async move {
// loop {
// let Ok(mut client) = MissionServiceClient::connect(url.clone()).await else {
// eprintln!("Unable to connect to DCS. Make sure Dcs-gRPC is installed and available, retry in 5 seconds..");
// sleep(Duration::from_secs(5)).await; // retry in 5 seconds
// continue;
// };
// let Ok(mut stream) = client
// .stream_units(StreamUnitsRequest {
// poll_rate: Some(10),
// max_backoff: Some(30),
// category: common::v0::GroupCategory::Airplane as i32,
// })
// .await
// else {
// continue;
// };
// let mut message = stream.get_mut().message().await;
// while let Ok(Some(next)) = &message {
// if let Some(update) = &next.update {
// match update {
// Update::Gone(unit) => {
// db.write().await.remove(&unit.id);
// }
// Update::Unit(unit) => {
// let group_name = unit.group.as_ref().map(|g| g.name.clone());
// let lat = unit.position.as_ref().map_or(0.0, |p| p.lat);
// let long = unit.position.as_ref().map_or(0.0, |p| p.lon);
// let altitude = unit.position.as_ref().map_or(0.0, |p| p.alt);
// let heading = unit.orientation.as_ref().map_or(0.0, |o| o.heading);
// let speed = unit.velocity.as_ref().map_or(0.0, |v| v.speed);
// // println!("updated: {}", &unit.callsign);
// db.write().await.insert(
// unit.id,
// Unit {
// id: unit.id,
// name: unit.name.clone(),
// player: unit.player_name.clone(),
// callsign: unit.callsign.clone(),
// group_name,
// coalition: unit.coalition,
// r#type: unit.r#type.clone(),
// position: Position {
// lat,
// long,
// altitude,
// },
// heading,
// speed,
// },
// );
// }
// }
// }
// message = stream.get_mut().message().await;
// }
// // if we get here, something went wrong, reconnect
// eprintln!("Connection lost");
// }
// });
// Self {
// base_url: base_url.to_string(),
// units,
// }
// }
// // Change to TransmitMessage when it becomes available in gRPC
// // https://github.com/DCS-gRPC/rust-server/blob/main/STATUS.md?plain=1#L519
// pub async fn send_text_message(&self, message: &str) {
// let Ok(mut client) = NetServiceClient::connect(self.base_url.clone()).await else {
// return;
// };
// let request = SendChatRequest {
// message: message.to_string(),
// coalition: Coalition::All as i32,
// // target_player_id: player_id,
// };
// client.send_chat(request).await.ok();
// }
// pub async fn get_unit_by_element(&self, element: &Element) -> Option<Unit> {
// // Return a unit, do the math in the command code
// let units = self.units.read().await;
// units
// .iter()
// .filter(|u| u.1.coalition == Coalition::Blue as i32)
// .map(|u| u.1)
// .find(|e| {
// e.callsign.to_lowercase()
// == format!(
// "{}{}{}",
// element.squadron.to_string(),
// element.group,
// element.unit
// )
// .to_lowercase()
// })
// .cloned()
// }
// pub async fn get_unit_by_pilot(&self, pilot: &str) -> Option<Unit> {
// // Return a unit, do the math in the command code
// let units = self.units.read().await;
// let pilot = pilot.to_string();
// units
// .iter()
// .filter(|u| u.1.coalition == Coalition::Blue as i32)
// .map(|u| u.1)
// .find(|e| {
// let Some(player) = &e.player else {
// return false;
// };
// player == &pilot
// })
// .cloned()
// }
// pub async fn get_nearest_hostile(&self, unit: &Unit) -> Option<Unit> {
// let units = self.units.read().await;
// units
// .iter()
// .filter(|u| u.1.coalition == Coalition::Red as i32) // only reds
// .map(|u| u.1)
// .min_by(|a, b| {
// let a_pos = &a.position; // some hostile A position
// let b_pos = &b.position; // some hostile B position
// let a_dist = a_pos.distance_to_km(&unit.position);
// let b_dist = b_pos.distance_to_km(&unit.position);
// a_dist.partial_cmp(&b_dist).unwrap()
// })
// .cloned()
// }
// pub async fn get_hostiles_in_range_from(&self, unit: &Unit, distance: f64) -> Vec<Unit> {
// let units = self.units.read().await;
// units
// .iter()
// .filter(|u| u.1.coalition == Coalition::Red as i32) // only reds
// .map(|u| u.1)
// .filter(|a| {
// let a_pos = &a.position; // some hostile A position
// let a_dist = a_pos.distance_to_nmi(&unit.position);
// a_dist.partial_cmp(&distance).unwrap() == Ordering::Less
// })
// .cloned()
// .collect::<Vec<_>>()
// }
// }

View File

@@ -0,0 +1,3 @@
pub mod v0 {
tonic::include_proto!("dcs.net.v0");
}

View File

@@ -0,0 +1,3 @@
pub mod v0 {
tonic::include_proto!("dcs.srs.v0");
}

View File

@@ -0,0 +1,3 @@
pub mod v0 {
tonic::include_proto!("dcs.timer.v0");
}

View File

@@ -0,0 +1,3 @@
pub mod v0 {
tonic::include_proto!("dcs.trigger.v0");
}

View File

@@ -0,0 +1,28 @@
pub mod v0 {
use crate::dcs::common::v0::{RawTransform, Transform};
tonic::include_proto!("dcs.unit.v0");
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct GetTransformResponseIntermediate {
time: f64,
raw_transform: Option<RawTransform>,
}
impl From<GetTransformResponseIntermediate> for GetTransformResponse {
fn from(i: GetTransformResponseIntermediate) -> Self {
let GetTransformResponseIntermediate {
time,
raw_transform,
} = i;
let transform = Transform::from(raw_transform.unwrap_or_default());
GetTransformResponse {
time,
position: Some(transform.position),
orientation: Some(transform.orientation),
velocity: Some(transform.velocity),
}
}
}
}

View File

@@ -0,0 +1,411 @@
/// Methods that can be used to serialize and deserialize [prost_types::Struct].
pub mod proto_struct {
use std::collections::BTreeMap;
use std::fmt;
use prost_types::value::Kind;
use prost_types::{ListValue, Struct, Value};
use serde::de::{MapAccess, Visitor};
use serde::ser::{SerializeMap, SerializeSeq};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub fn serialize<S>(data: &Option<Struct>, se: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if let Some(data) = data {
StructSe(data).serialize(se)
} else {
se.serialize_unit()
}
}
pub fn deserialize<'de, D>(de: D) -> Result<Option<Struct>, D::Error>
where
D: Deserializer<'de>,
{
Ok(Option::<StructDe>::deserialize(de)?.map(|s| s.0))
}
/// Serializable Wrapper around [prost_types::Struct].
struct StructSe<'a>(&'a Struct);
impl<'a> Serialize for StructSe<'a> {
fn serialize<S>(&self, se: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = se.serialize_map(Some(self.0.fields.len()))?;
for (key, val) in &self.0.fields {
map.serialize_key(&key)?;
map.serialize_value(&ValueSe(val))?;
}
map.end()
}
}
/// Deserializable Wrapper around [prost_types::Struct].
struct StructDe(Struct);
impl<'de> Deserialize<'de> for StructDe {
fn deserialize<D>(de: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
de.deserialize_map(StructVisitor)
}
}
/// Serializable Wrapper around [prost_types::Value].
struct ValueSe<'a>(&'a Value);
impl<'a> Serialize for ValueSe<'a> {
fn serialize<S>(&self, se: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if let Some(kind) = &self.0.kind {
match kind {
Kind::NullValue(_) => se.serialize_unit(),
Kind::NumberValue(v) => v.serialize(se),
Kind::StringValue(v) => v.serialize(se),
Kind::BoolValue(v) => v.serialize(se),
Kind::StructValue(v) => StructSe(v).serialize(se),
Kind::ListValue(v) => {
let mut seq = se.serialize_seq(Some(v.values.len()))?;
for val in &v.values {
seq.serialize_element(&ValueSe(val))?
}
seq.end()
}
}
} else {
se.serialize_none()
}
}
}
/// Serializable Wrapper around [prost_types::Value].
struct ValueDe(Value);
impl<'de> Deserialize<'de> for ValueDe {
fn deserialize<D>(de: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
de.deserialize_any(ValueVisitor)
}
}
struct StructVisitor;
impl<'de> Visitor<'de> for StructVisitor {
type Value = StructDe;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("google.protobuf.Struct")
}
fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut fields = BTreeMap::new();
while let Some((key, value)) = access.next_entry::<String, ValueDe>()? {
fields.insert(key, value.0);
}
Ok(StructDe(Struct { fields }))
}
}
struct ValueVisitor;
impl ValueVisitor {
fn visit_number<E>(self, v: impl TryInto<f64>) -> Result<ValueDe, E>
where
E: serde::de::Error,
{
v.try_into()
.map(|v| {
ValueDe(Value {
kind: Some(Kind::NumberValue(v)),
})
})
.map_err(|_| {
serde::de::Error::invalid_type(serde::de::Unexpected::Other("f64"), &self)
})
}
}
impl<'de> Visitor<'de> for ValueVisitor {
type Value = ValueDe;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("google.protobuf.Value")
}
fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(ValueDe(Value {
kind: Some(Kind::BoolValue(v)),
}))
}
fn visit_i8<E>(self, v: i8) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_number(v)
}
fn visit_i16<E>(self, v: i16) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_number(v)
}
fn visit_i32<E>(self, v: i32) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_number(v)
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
i32::try_from(v)
.map_err(|_| {
serde::de::Error::invalid_type(serde::de::Unexpected::Other("i64"), &self)
})
.and_then(|v| self.visit_number(v))
}
fn visit_i128<E>(self, v: i128) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
i32::try_from(v)
.map_err(|_| {
serde::de::Error::invalid_type(serde::de::Unexpected::Other("i64"), &self)
})
.and_then(|v| self.visit_number(v))
}
fn visit_u8<E>(self, v: u8) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_number(v)
}
fn visit_u16<E>(self, v: u16) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_number(v)
}
fn visit_u32<E>(self, v: u32) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_number(v)
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
u32::try_from(v)
.map_err(|_| {
serde::de::Error::invalid_type(serde::de::Unexpected::Other("i64"), &self)
})
.and_then(|v| self.visit_number(v))
}
fn visit_u128<E>(self, v: u128) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
u32::try_from(v)
.map_err(|_| {
serde::de::Error::invalid_type(serde::de::Unexpected::Other("i64"), &self)
})
.and_then(|v| self.visit_number(v))
}
fn visit_f32<E>(self, v: f32) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_number(v)
}
fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
self.visit_number(v)
}
fn visit_char<E>(self, v: char) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(ValueDe(Value {
kind: Some(Kind::StringValue(v.to_string())),
}))
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(ValueDe(Value {
kind: Some(Kind::StringValue(v.to_string())),
}))
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(ValueDe(Value {
kind: Some(Kind::NullValue(0)),
}))
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let mut values = Vec::with_capacity(seq.size_hint().unwrap_or(0));
while let Some(val) = seq.next_element::<ValueDe>()? {
values.push(val.0);
}
Ok(ValueDe(Value {
kind: Some(Kind::ListValue(ListValue { values })),
}))
}
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
Ok(ValueDe(Value {
kind: Some(Kind::StructValue(StructVisitor.visit_map(map)?.0)),
}))
}
}
#[cfg(test)]
mod tests {
use prost_types::value::Kind;
use prost_types::{ListValue, Struct, Value};
#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
struct Message {
#[serde(with = "crate::dcs::utils::proto_struct")]
details: Option<Struct>,
}
fn create_message(key: impl Into<String>, kind: Kind) -> Message {
Message {
details: Some(Struct {
fields: [(key.into(), Value { kind: Some(kind) })]
.into_iter()
.collect(),
}),
}
}
#[test]
fn test_null() {
let m = create_message("null", Kind::NullValue(0));
let json = serde_json::to_string(&m).unwrap();
assert_eq!(json, r#"{"details":{"null":null}}"#);
assert_eq!(serde_json::from_str::<Message>(&json).unwrap(), m);
}
#[test]
fn test_number() {
let m = create_message("number", Kind::NumberValue(42.2223));
let json = serde_json::to_string(&m).unwrap();
assert_eq!(json, r#"{"details":{"number":42.2223}}"#);
assert_eq!(serde_json::from_str::<Message>(&json).unwrap(), m);
}
#[test]
fn test_string() {
let m = create_message("string", Kind::StringValue("dcs-grpc".to_string()));
let json = serde_json::to_string(&m).unwrap();
assert_eq!(json, r#"{"details":{"string":"dcs-grpc"}}"#);
assert_eq!(serde_json::from_str::<Message>(&json).unwrap(), m);
}
#[test]
fn test_bool() {
let m = create_message("bool", Kind::BoolValue(true));
let json = serde_json::to_string(&m).unwrap();
assert_eq!(json, r#"{"details":{"bool":true}}"#);
assert_eq!(serde_json::from_str::<Message>(&json).unwrap(), m);
}
#[test]
fn test_struct() {
let m = create_message(
"nested",
Kind::StructValue(Struct {
fields: [
(
"number".to_string(),
Value {
kind: Some(Kind::NumberValue(42.0)),
},
),
(
"string".to_string(),
Value {
kind: Some(Kind::StringValue("dcs-grpc".to_string())),
},
),
]
.into_iter()
.collect(),
}),
);
let json = serde_json::to_string(&m).unwrap();
assert_eq!(
json,
r#"{"details":{"nested":{"number":42.0,"string":"dcs-grpc"}}}"#
);
assert_eq!(serde_json::from_str::<Message>(&json).unwrap(), m);
}
#[test]
fn test_list() {
let m = create_message(
"list",
Kind::ListValue(ListValue {
values: vec![
Value {
kind: Some(Kind::NumberValue(42.0)),
},
Value {
kind: Some(Kind::StringValue("dcs-grpc".to_string())),
},
],
}),
);
let json = serde_json::to_string(&m).unwrap();
assert_eq!(json, r#"{"details":{"list":[42.0,"dcs-grpc"]}}"#);
assert_eq!(serde_json::from_str::<Message>(&json).unwrap(), m);
}
}
}

View File

@@ -0,0 +1,3 @@
pub mod v0 {
tonic::include_proto!("dcs.world.v0");
}

View File

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

View File

@@ -0,0 +1,16 @@
[package]
name = "guardian_core"
version.workspace = true
edition.workspace = true
repository.workspace = true
license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bevy.workspace = true
dcs-grpc.workspace = true
tokio.workspace = true
async-compat = "0.2.1"
crossbeam-channel = "0.5.9"
tonic = "0.10"

View File

@@ -0,0 +1,28 @@
mod mission;
mod text;
mod voice;
use crate::GrpcBaseUrl;
pub use self::mission::*;
pub use self::text::*;
use bevy::app::{App, Plugin, ScheduleRunnerPlugin};
pub use dcs_grpc::dcs::common::v0::{Coalition, Unit};
use std::time::Duration;
pub struct DcsPlugin;
impl Plugin for DcsPlugin {
fn build(&self, app: &mut App) {
app.world.get_resource_or_insert_with(GrpcBaseUrl::default);
// Make the app loop forever at 60fps.
app.add_plugins(ScheduleRunnerPlugin::run_loop(Duration::from_secs_f64(
1.0 / 60.0,
)));
app.add_plugins(MissionPlugin);
app.add_plugins(TextPlugin);
}
}

View File

@@ -0,0 +1,251 @@
use std::time::Duration;
use crate::{GrpcBaseUrl, TokioResource};
use bevy::{
app::{App, Plugin, PreStartup, PreUpdate},
core::Name,
ecs::{
component::Component,
entity::Entity,
event::{Event, EventReader, EventWriter},
system::{Commands, Query, Res},
},
};
use crossbeam_channel::{unbounded, Receiver};
use dcs_grpc::dcs::{
common::v0::{Coalition, GroupCategory::Airplane, Unit},
mission::v0::{
mission_service_client::MissionServiceClient, stream_units_response::Update,
StreamUnitsRequest,
},
};
pub struct MissionPlugin;
impl Plugin for MissionPlugin {
fn build(&self, app: &mut App) {
app.add_event::<UnitUpdatedEvent>();
app.add_event::<UnitGoneEvent>();
app.add_systems(PreStartup, connect_to_grpc);
app.add_systems(
PreUpdate,
(consume_stream_message, update_units, despawn_units),
);
}
}
fn connect_to_grpc(mut commands: Commands, tokio: Res<TokioResource>, url: Res<GrpcBaseUrl>) {
let handle = &tokio.0;
let (tx, task) = unbounded();
let url = url.to_string();
handle.spawn(async move {
loop {
let Ok(mut client) = MissionServiceClient::connect(url.clone()).await else {
eprintln!("Connection failed: {}", url);
tokio::time::sleep(Duration::from_secs(5)).await;
continue;
};
let Ok(mut stream) = client
.stream_units(StreamUnitsRequest {
poll_rate: Some(10),
max_backoff: Some(30),
category: Airplane as i32,
})
.await
else {
return;
};
let mut message = stream.get_mut().message().await;
while let Ok(Some(next)) = &message {
if let Some(update) = &next.update {
tx.send(update.clone()).ok();
}
message = stream.get_mut().message().await;
}
eprintln!("Disconnected");
tokio::time::sleep(Duration::from_secs(5)).await;
}
});
commands.spawn(UnitsRequestMessage(task));
}
pub(crate) fn consume_stream_message(
requests: Query<&UnitsRequestMessage>,
mut ev_unit_updated: EventWriter<UnitUpdatedEvent>,
mut ev_unit_gone: EventWriter<UnitGoneEvent>,
) {
for stream in requests.iter() {
if let Ok(update) = stream.0.try_recv() {
match update {
Update::Gone(unit) => {
ev_unit_gone.send(UnitGoneEvent(unit.id));
}
Update::Unit(unit) => {
ev_unit_updated.send(UnitUpdatedEvent(unit));
}
}
}
}
}
pub(crate) fn update_units(
mut commands: Commands,
units: Query<(Entity, &Id)>,
mut ev: EventReader<UnitUpdatedEvent>,
) {
for event in ev.read() {
let event = &event.0;
let mut e: Option<_> = None;
for (ent, id) in units.iter() {
if id.0 == event.id {
e = commands.get_entity(ent);
break;
}
}
if e.is_none() {
e = Some(commands.spawn((Id(event.id), Callsign(event.callsign.clone()))));
}
let Some(mut e) = e else {
unreachable!();
};
if let Some(position) = &event.position {
e.insert(Position {
lat: position.lat,
long: position.lon,
altitude: position.alt,
});
}
if let Some(orientation) = &event.orientation {
e.insert(Heading(orientation.heading));
}
if let Some(playername) = &event.player_name {
e.insert(Player(playername.clone()));
}
if let Some(group) = &event.group {
e.insert(Group {
id: group.id,
unit: event.number_in_group,
});
}
e.insert(Name::new(event.name.clone()));
e.insert(UnitType(event.r#type.clone()));
e.insert(Side(
Coalition::try_from(event.coalition).expect("Coalition to be correct"),
));
}
}
pub(crate) fn despawn_units(
mut commands: Commands,
units: Query<(Entity, &Id)>,
mut ev: EventReader<UnitGoneEvent>,
) {
for event in ev.read() {
let gid = &event.0;
for (ent, id) in units.iter() {
if gid == &id.0 {
commands.entity(ent).despawn();
}
}
}
}
#[derive(Event)]
pub(crate) struct UnitUpdatedEvent(Unit);
#[derive(Event)]
pub(crate) struct UnitGoneEvent(u32);
#[derive(Component)]
pub(crate) struct UnitsRequestMessage(Receiver<Update>);
#[derive(Debug, Component)]
pub struct Id(pub u32);
#[derive(Debug, Component)]
pub struct Callsign(pub String);
#[derive(Debug, Component)]
pub struct UnitType(pub String);
#[derive(Debug, Component)]
pub struct Player(pub String);
#[derive(Debug, Component)]
pub struct Side(pub Coalition);
#[derive(Debug, Component)]
pub struct Position {
pub lat: f64,
pub long: f64,
pub altitude: f64,
}
#[derive(Debug, Component)]
pub struct Heading(pub f64);
#[derive(Debug, Component)]
pub struct Group {
pub id: u32,
pub unit: u32,
}
const COMPASS_DRIFT: i32 = -5;
impl Position {
pub fn distance_to_km(&self, other: &Position) -> f64 {
let radius_of_earth_in_km: f64 = 6371.0;
let delta_lat = (other.lat - self.lat).to_radians();
let delta_long = (other.long - self.long).to_radians();
let a = (delta_lat / 2.0).sin() * (delta_lat / 2.0).sin()
+ self.lat.to_radians().cos()
* other.lat.to_radians().cos()
* (delta_long / 2.0).sin()
* (delta_long / 2.0).sin();
let c = 2.0 * (a.sqrt().atan2((1.0 - a).sqrt()));
radius_of_earth_in_km * c
}
pub fn distance_to_nmi(&self, other: &Position) -> f64 {
self.distance_to_km(other) * 0.5399568035
}
pub fn get_bearing_to(&self, other: &Position) -> i32 {
let delta_long = (other.long - self.long).to_radians();
let y =
(other.long.to_radians() - self.long.to_radians()).sin() * other.lat.to_radians().cos();
let x = self.lat.to_radians().cos() * other.lat.to_radians().sin()
- self.lat.to_radians().sin() * other.lat.to_radians().cos() * delta_long.cos();
(((y.atan2(x).to_degrees() + 360.0) % 360.0) as i32) + COMPASS_DRIFT
}
pub fn feet(&self) -> f64 {
self.altitude * 3.28084
}
pub fn angels(&self) -> i32 {
(self.feet() / 1000.0).round() as i32
}
}

View File

@@ -0,0 +1,134 @@
use std::{
borrow::Cow,
hash::{Hash, Hasher},
};
use bevy::{
app::{Plugin, PostUpdate},
ecs::{
component::Component,
entity::Entity,
query::Added,
reflect::ReflectComponent,
system::{Commands, Query, Res},
},
reflect::{std_traits::ReflectDefault, Reflect},
utils::AHasher,
};
use dcs_grpc::dcs::{
common::v0::Coalition,
net::v0::{net_service_client::NetServiceClient, SendChatRequest},
};
use crate::{GrpcBaseUrl, TokioResource};
pub struct TextPlugin;
impl Plugin for TextPlugin {
fn build(&self, app: &mut bevy::prelude::App) {
app.add_systems(PostUpdate, send_text_message);
}
}
fn send_text_message(
mut commands: Commands,
query: Query<(Entity, &TextMessage), Added<TextMessage>>,
url: Res<GrpcBaseUrl>,
tokio: Res<TokioResource>,
) {
let url = url.to_string();
for (ent, msg) in query.iter() {
if msg.as_str().is_empty() {
commands.entity(ent).remove::<TextMessage>().despawn();
continue;
}
let message = msg.clone();
let url = url.clone();
tokio.0.spawn(async move {
let Ok(mut client) = NetServiceClient::connect(url.to_string()).await else {
return;
};
let request = SendChatRequest {
message: message.to_string(),
coalition: Coalition::All as i32,
// target_player_id: player_id,
};
client.send_chat(request).await.ok();
});
commands.entity(ent).remove::<TextMessage>().despawn();
}
}
#[derive(Reflect, Component, Clone)]
#[reflect(Component, Default, Debug)]
pub struct TextMessage {
hash: u64,
message: Cow<'static, str>,
}
impl Default for TextMessage {
fn default() -> Self {
TextMessage::new("")
}
}
impl TextMessage {
/// Creates a new [`TextMessage`] from any string-like type.
///
/// The internal hash will be computed immediately.
pub fn new(message: impl Into<Cow<'static, str>>) -> Self {
let message = message.into();
let mut message = TextMessage { message, hash: 0 };
message.update_hash();
message
}
/// Sets the entity's message.
///
/// The internal hash will be re-computed.
#[inline(always)]
pub fn set(&mut self, message: impl Into<Cow<'static, str>>) {
*self = TextMessage::new(message);
}
/// Updates the message of the entity in place.
///
/// This will allocate a new string if the message was previously
/// created from a borrow.
#[inline(always)]
pub fn mutate<F: FnOnce(&mut String)>(&mut self, f: F) {
f(self.message.to_mut());
self.update_hash();
}
/// Gets the message of the entity as a `&str`.
#[inline(always)]
pub fn as_str(&self) -> &str {
&self.message
}
fn update_hash(&mut self) {
let mut hasher = AHasher::default();
self.message.hash(&mut hasher);
self.hash = hasher.finish();
}
}
impl std::fmt::Display for TextMessage {
#[inline(always)]
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Display::fmt(&self.message, f)
}
}
impl std::fmt::Debug for TextMessage {
#[inline(always)]
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Debug::fmt(&self.message, f)
}
}

View File

@@ -0,0 +1,92 @@
use std::{
borrow::Cow,
hash::{Hash, Hasher},
};
use bevy::{
app::Plugin,
ecs::{component::Component, reflect::ReflectComponent},
reflect::{std_traits::ReflectDefault, Reflect},
utils::AHasher,
};
pub struct SrsPlugin;
impl Plugin for SrsPlugin {
fn build(&self, app: &mut bevy::prelude::App) {
todo!()
}
}
fn connect_srs() {
// will emit strings as commands
}
#[derive(Reflect, Component, Clone)]
#[reflect(Component, Default, Debug)]
pub struct VoiceMessage {
hash: u64,
message: Cow<'static, str>,
}
impl Default for VoiceMessage {
fn default() -> Self {
VoiceMessage::new("")
}
}
impl VoiceMessage {
/// Creates a new [`VoiceMessage`] from any string-like type.
///
/// The internal hash will be computed immediately.
pub fn new(message: impl Into<Cow<'static, str>>) -> Self {
let message = message.into();
let mut message = VoiceMessage { message, hash: 0 };
message.update_hash();
message
}
/// Sets the entity's message.
///
/// The internal hash will be re-computed.
#[inline(always)]
pub fn set(&mut self, message: impl Into<Cow<'static, str>>) {
*self = VoiceMessage::new(message);
}
/// Updates the message of the entity in place.
///
/// This will allocate a new string if the message was previously
/// created from a borrow.
#[inline(always)]
pub fn mutate<F: FnOnce(&mut String)>(&mut self, f: F) {
f(self.message.to_mut());
self.update_hash();
}
/// Gets the message of the entity as a `&str`.
#[inline(always)]
pub fn as_str(&self) -> &str {
&self.message
}
fn update_hash(&mut self) {
let mut hasher = AHasher::default();
self.message.hash(&mut hasher);
self.hash = hasher.finish();
}
}
impl std::fmt::Display for VoiceMessage {
#[inline(always)]
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Display::fmt(&self.message, f)
}
}
impl std::fmt::Debug for VoiceMessage {
#[inline(always)]
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Debug::fmt(&self.message, f)
}
}

View File

@@ -0,0 +1,79 @@
use std::{
borrow::Cow,
hash::{Hash, Hasher},
};
use bevy::{
ecs::{reflect::ReflectResource, system::Resource},
reflect::{std_traits::ReflectDefault, Reflect},
utils::AHasher,
};
#[derive(Reflect, Resource, Clone)]
#[reflect(Resource, Default, Debug)]
pub struct GrpcBaseUrl {
hash: u64,
url: Cow<'static, str>,
}
impl Default for GrpcBaseUrl {
fn default() -> Self {
GrpcBaseUrl::new("http://127.0.0.1:50051/")
}
}
impl GrpcBaseUrl {
/// Creates a new [`GrpcBaseUrl`] from any string-like type.
///
/// The internal hash will be computed immediately.
pub fn new(url: impl Into<Cow<'static, str>>) -> Self {
let url = url.into();
let mut url = GrpcBaseUrl { url, hash: 0 };
url.update_hash();
url
}
/// Sets the entity's url.
///
/// The internal hash will be re-computed.
#[inline(always)]
pub fn set(&mut self, url: impl Into<Cow<'static, str>>) {
*self = GrpcBaseUrl::new(url);
}
/// Updates the url of the entity in place.
///
/// This will allocate a new string if the url was previously
/// created from a borrow.
#[inline(always)]
pub fn mutate<F: FnOnce(&mut String)>(&mut self, f: F) {
f(self.url.to_mut());
self.update_hash();
}
/// Gets the url of the entity as a `&str`.
#[inline(always)]
pub fn as_str(&self) -> &str {
&self.url
}
fn update_hash(&mut self) {
let mut hasher = AHasher::default();
self.url.hash(&mut hasher);
self.hash = hasher.finish();
}
}
impl std::fmt::Display for GrpcBaseUrl {
#[inline(always)]
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Display::fmt(&self.url, f)
}
}
impl std::fmt::Debug for GrpcBaseUrl {
#[inline(always)]
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Debug::fmt(&self.url, f)
}
}

View File

@@ -0,0 +1,24 @@
mod dcs;
mod grpc_base_url;
pub use dcs::*;
pub use grpc_base_url::*;
use bevy::app::PluginGroup;
use bevy::app::PluginGroupBuilder;
use bevy::ecs::system::Resource;
use tokio::runtime::Handle;
pub struct DefaultPlugins;
impl PluginGroup for DefaultPlugins {
fn build(self) -> PluginGroupBuilder {
#[allow(unused_mut)]
let mut group = PluginGroupBuilder::start::<Self>().add(DcsPlugin);
group
}
}
#[derive(Resource)]
pub struct TokioResource(pub Handle);