Initial Commit
This commit is contained in:
16
crates/guardian_core/Cargo.toml
Normal file
16
crates/guardian_core/Cargo.toml
Normal 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"
|
||||
28
crates/guardian_core/src/dcs.rs
Normal file
28
crates/guardian_core/src/dcs.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
251
crates/guardian_core/src/dcs/mission.rs
Normal file
251
crates/guardian_core/src/dcs/mission.rs
Normal 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
|
||||
}
|
||||
}
|
||||
134
crates/guardian_core/src/dcs/text.rs
Normal file
134
crates/guardian_core/src/dcs/text.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
92
crates/guardian_core/src/dcs/voice.rs
Normal file
92
crates/guardian_core/src/dcs/voice.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
79
crates/guardian_core/src/grpc_base_url.rs
Normal file
79
crates/guardian_core/src/grpc_base_url.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
24
crates/guardian_core/src/lib.rs
Normal file
24
crates/guardian_core/src/lib.rs
Normal 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);
|
||||
Reference in New Issue
Block a user