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

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

4610
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

37
Cargo.toml Normal file
View File

@@ -0,0 +1,37 @@
[workspace]
members = ["crates/*"]
[workspace.package]
version = "0.1.0"
edition = "2021"
repository = "https://git.avii.nl/git/guardian"
license = "MIT"
[package]
name = "guardian"
version.workspace = true
edition.workspace = true
repository.workspace = true
license.workspace = true
# Enable a small amount of optimization in debug mode
[profile.dev]
opt-level = 1
# Enable high optimizations for dependencies (incl. Bevy), but not for our code:
[profile.dev.package."*"]
opt-level = 3
[workspace.dependencies]
bevy = { version = "0.12", features = [
"dynamic_linking",
"multi-threaded",
"trace",
] }
tokio = { version = "1.34", features = ["macros", "rt-multi-thread", "signal"] }
dcs-grpc = { path = "./crates/dcs-grpc" }
[dependencies]
guardian_core = { path = "./crates/guardian_core" }
bevy.workspace = true
tokio.workspace = true

9
LICENSE Normal file
View File

@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2023 Avii
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

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);

2
rust-toolchain.toml Normal file
View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

158
src/main.rs Normal file
View File

@@ -0,0 +1,158 @@
use bevy::{
app::{App, Update},
ecs::{
component::Component,
entity::Entity,
query::{Added, With, Without},
system::{Commands, Query},
},
};
use guardian_core::*;
use tokio::runtime::Handle;
// Every incomming (voice) message is an event
// Systems can handle the events, eg bogey dope, set tripwire or radio check
// voice responses; srs should be a resource?
// |- srs plugin adds a resource to use to send messages i guess
// |-- or spawn an entity with a message commands.spawn((VoiceMessage("Hello World"), TextMessage("Hello World!")))
// |-- srs plugin will check if these components are Added<VoiceMessage> or w/e and handle accordingly
#[tokio::main]
async fn main() {
App::new()
.insert_resource(TokioResource(Handle::current()))
.add_plugins(DefaultPlugins)
.add_systems(Update, (add_tripwire, tripwire))
.run();
}
// this will be on a tripwire command event (from srs), instead of Added<Player>, not everyone may want it; opt-in over opt-out
fn add_tripwire(mut commands: Commands, players: Query<Entity, Added<Player>>) {
for ent in players.iter() {
commands.entity(ent).insert(Tripwire {
range: 30.0,
reported: vec![],
});
}
}
fn tripwire(
mut commands: Commands,
mut players: Query<(&Id, &Callsign, &Position, &mut Tripwire), With<Player>>,
npc: Query<(&Id, &Position, &Heading, &Side), Without<Player>>,
) {
for (p_id, p_callsign, p_position, mut p_tripwire) in players.iter_mut() {
let _p_id = &p_id.0;
let _p_callsign = &p_callsign.0;
for (n_id, n_position, n_heading, side) in npc.iter() {
if side.0 != Coalition::Red {
continue;
}
let n_id = &n_id.0;
let distance = n_position.distance_to_nmi(p_position);
if distance <= p_tripwire.range && !p_tripwire.reported.contains(n_id) {
p_tripwire.reported.push(*n_id);
let bra = Braa::new(p_position, n_position, n_heading);
commands.spawn(TextMessage::new(bra.to_text()));
} else if distance > p_tripwire.range && p_tripwire.reported.contains(n_id) {
p_tripwire.reported.retain(|b| b != n_id);
}
}
}
}
#[derive(Component)]
struct Tripwire {
range: f64,
reported: Vec<u32>,
}
#[derive(Clone)]
pub enum Aspect {
Hot,
Flank,
Cold,
}
impl std::fmt::Display for Aspect {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Hot => write!(f, "Hot"),
Self::Flank => write!(f, "Flank"),
Self::Cold => write!(f, "Cold"),
}
}
}
#[derive(Clone)]
pub struct Braa {
bearing: i32,
range: i32,
angels: i32,
aspect: Aspect,
}
impl Braa {
pub fn new(unit_a: &Position, unit_b: &Position, heading_b: &Heading) -> Self {
let bearing = unit_a.get_bearing_to(unit_b);
let range = unit_a.distance_to_nmi(unit_b).round() as i32;
let angels = unit_b.angels();
let aspect = {
let bearing = unit_b.get_bearing_to(unit_a);
let angle = (bearing - heading_b.0 as i32) % 360;
if (45..135).contains(&angle) {
Aspect::Flank
} else if (135..225).contains(&angle) {
Aspect::Cold
} else if (225..315).contains(&angle) {
Aspect::Flank
} else if !(45..=315).contains(&angle) {
Aspect::Hot
} else {
Aspect::Cold
}
};
Self {
bearing,
range,
angels,
aspect,
}
}
pub fn to_text(&self) -> String {
format!(
"BRA {}° for {}NM at {} thousand {}",
self.bearing, self.range, self.angels, self.aspect
)
}
pub fn to_voice(&self) -> String {
let bearing = split(&self.bearing.to_string(), 1);
let range = split(&self.range.to_string(), 1);
format!(
"BRA <break time=\"250ms\"/> {} <break /> {} <break /> {} thousand <break /> {}",
bearing, range, self.angels, self.aspect
)
}
}
fn split(input: &str, n: usize) -> String {
input
.chars()
.enumerate()
.flat_map(|(i, c)| {
if i != 0 && i % n == 0 {
Some(' ')
} else {
None
}
.into_iter()
.chain(std::iter::once(c))
})
.collect::<String>()
}