no clue what the current status is

This commit is contained in:
2026-08-14 23:08:42 +02:00
parent b0efc215ba
commit bee5e0f11d
82 changed files with 3778 additions and 3777 deletions

0
crates/guardian_core/Cargo.toml Normal file → Executable file
View File

8
crates/guardian_core/src/components/awacs.rs Normal file → Executable file
View File

@@ -1,4 +1,4 @@
use bevy::ecs::component::Component;
#[derive(Component)]
pub struct Awacs;
use bevy::ecs::component::Component;
#[derive(Component)]
pub struct Awacs;

210
crates/guardian_core/src/components/callsign.rs Normal file → Executable file
View File

@@ -1,105 +1,105 @@
use std::{
borrow::Cow,
hash::{Hash, Hasher},
};
use bevy::{
ecs::{component::Component, reflect::ReflectComponent},
reflect::{std_traits::ReflectDefault, Reflect},
utils::AHasher,
};
#[derive(Reflect, Component, Clone)]
#[reflect(Component, Default, Debug)]
pub struct Callsign {
hash: u64,
value: Cow<'static, str>,
}
impl Default for Callsign {
fn default() -> Self {
Callsign::new("")
}
}
fn add_spaces_around_numbers(input: &str) -> String {
let mut result = String::new();
let mut chars = input.chars().peekable();
while let Some(c) = chars.next() {
result.push(c);
if let Some(next_char) = chars.peek() {
if next_char.is_numeric() {
result.push(' ');
}
}
}
result
}
impl Callsign {
/// Creates a new [`Callsign`] from any string-like type.
///
/// The internal hash will be computed immediately.
pub fn new(value: impl Into<Cow<'static, str>>) -> Self {
let value = value.into();
let mut value = Callsign { value, hash: 0 };
value.update_hash();
value
}
pub fn to_voice(&self) -> String {
let value: String = self.value.to_string();
let value = value.replace(' ', "");
let value = value.replace('-', "");
add_spaces_around_numbers(&value)
}
/// Sets the entity's value.
///
/// The internal hash will be re-computed.
#[inline(always)]
pub fn set(&mut self, value: impl Into<Cow<'static, str>>) {
*self = Callsign::new(value);
}
/// Updates the value of the entity in place.
///
/// This will allocate a new string if the value was previously
/// created from a borrow.
#[inline(always)]
pub fn mutate<F: FnOnce(&mut String)>(&mut self, f: F) {
f(self.value.to_mut());
self.update_hash();
}
/// Gets the value of the entity as a `&str`.
#[inline(always)]
pub fn as_str(&self) -> &str {
&self.value
}
fn update_hash(&mut self) {
let mut hasher = AHasher::default();
self.value.hash(&mut hasher);
self.hash = hasher.finish();
}
}
impl std::fmt::Display for Callsign {
#[inline(always)]
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Display::fmt(&self.value, f)
}
}
impl std::fmt::Debug for Callsign {
#[inline(always)]
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Debug::fmt(&self.value, f)
}
}
use std::{
borrow::Cow,
hash::{Hash, Hasher},
};
use bevy::{
ecs::{component::Component, reflect::ReflectComponent},
reflect::{std_traits::ReflectDefault, Reflect},
utils::AHasher,
};
#[derive(Reflect, Component, Clone)]
#[reflect(Component, Default, Debug)]
pub struct Callsign {
hash: u64,
value: Cow<'static, str>,
}
impl Default for Callsign {
fn default() -> Self {
Callsign::new("")
}
}
fn add_spaces_around_numbers(input: &str) -> String {
let mut result = String::new();
let mut chars = input.chars().peekable();
while let Some(c) = chars.next() {
result.push(c);
if let Some(next_char) = chars.peek() {
if next_char.is_numeric() {
result.push(' ');
}
}
}
result
}
impl Callsign {
/// Creates a new [`Callsign`] from any string-like type.
///
/// The internal hash will be computed immediately.
pub fn new(value: impl Into<Cow<'static, str>>) -> Self {
let value = value.into();
let mut value = Callsign { value, hash: 0 };
value.update_hash();
value
}
pub fn to_voice(&self) -> String {
let value: String = self.value.to_string();
let value = value.replace(' ', "");
let value = value.replace('-', "");
add_spaces_around_numbers(&value)
}
/// Sets the entity's value.
///
/// The internal hash will be re-computed.
#[inline(always)]
pub fn set(&mut self, value: impl Into<Cow<'static, str>>) {
*self = Callsign::new(value);
}
/// Updates the value of the entity in place.
///
/// This will allocate a new string if the value was previously
/// created from a borrow.
#[inline(always)]
pub fn mutate<F: FnOnce(&mut String)>(&mut self, f: F) {
f(self.value.to_mut());
self.update_hash();
}
/// Gets the value of the entity as a `&str`.
#[inline(always)]
pub fn as_str(&self) -> &str {
&self.value
}
fn update_hash(&mut self) {
let mut hasher = AHasher::default();
self.value.hash(&mut hasher);
self.hash = hasher.finish();
}
}
impl std::fmt::Display for Callsign {
#[inline(always)]
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Display::fmt(&self.value, f)
}
}
impl std::fmt::Debug for Callsign {
#[inline(always)]
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Debug::fmt(&self.value, f)
}
}

14
crates/guardian_core/src/components/group.rs Normal file → Executable file
View File

@@ -1,7 +1,7 @@
use bevy::ecs::component::Component;
#[derive(Debug, Component)]
pub struct Group {
pub id: u32,
pub unit: u32,
}
use bevy::ecs::component::Component;
#[derive(Debug, Component)]
pub struct Group {
pub id: u32,
pub unit: u32,
}

188
crates/guardian_core/src/components/grpc_base_url.rs Normal file → Executable file
View File

@@ -1,94 +1,94 @@
use std::{
borrow::Cow,
hash::{Hash, Hasher},
};
use bevy::{
ecs::{reflect::ReflectResource, system::Resource},
reflect::{std_traits::ReflectDefault, Reflect},
utils::AHasher,
};
use serde::{Deserialize, Serialize};
#[derive(Reflect, Resource, Clone, Deserialize, Serialize)]
#[reflect(Resource, Default, Debug)]
pub struct GrpcBaseUrl {
#[serde(skip)]
hash: u64,
/// The URL of the DCS-gRPC Server
url: Cow<'static, str>,
/// The poll rate for updating the mission situation in secononds.
/// Lower values might cause noticeable ingame lag spikes
poll_rate: Option<u32>,
}
impl Default for GrpcBaseUrl {
fn default() -> Self {
GrpcBaseUrl::new("http://127.0.0.1:50051/", Some(10))
}
}
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>>, poll_rate: Option<u32>) -> Self {
let url = url.into();
let mut url = GrpcBaseUrl {
url,
hash: 0,
poll_rate,
};
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, self.poll_rate);
}
/// 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
}
#[inline(always)]
pub fn poll_rate(&self) -> Option<u32> {
self.poll_rate
}
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)
}
}
use std::{
borrow::Cow,
hash::{Hash, Hasher},
};
use bevy::{
ecs::{reflect::ReflectResource, system::Resource},
reflect::{std_traits::ReflectDefault, Reflect},
utils::AHasher,
};
use serde::{Deserialize, Serialize};
#[derive(Reflect, Resource, Clone, Deserialize, Serialize)]
#[reflect(Resource, Default, Debug)]
pub struct GrpcBaseUrl {
#[serde(skip)]
hash: u64,
/// The URL of the DCS-gRPC Server
url: Cow<'static, str>,
/// The poll rate for updating the mission situation in secononds.
/// Lower values might cause noticeable ingame lag spikes
poll_rate: Option<u32>,
}
impl Default for GrpcBaseUrl {
fn default() -> Self {
GrpcBaseUrl::new("http://127.0.0.1:50051/", Some(10))
}
}
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>>, poll_rate: Option<u32>) -> Self {
let url = url.into();
let mut url = GrpcBaseUrl {
url,
hash: 0,
poll_rate,
};
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, self.poll_rate);
}
/// 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
}
#[inline(always)]
pub fn poll_rate(&self) -> Option<u32> {
self.poll_rate
}
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)
}
}

8
crates/guardian_core/src/components/heading.rs Normal file → Executable file
View File

@@ -1,4 +1,4 @@
use bevy::ecs::component::Component;
#[derive(Debug, Component)]
pub struct Heading(pub f64);
use bevy::ecs::component::Component;
#[derive(Debug, Component)]
pub struct Heading(pub f64);

44
crates/guardian_core/src/components/id.rs Normal file → Executable file
View File

@@ -1,22 +1,22 @@
use bevy::ecs::component::Component;
#[derive(Debug, Component, Copy, Clone, PartialEq)]
pub struct Id(u32);
impl Id {
pub fn new(value: u32) -> Self {
Self(value)
}
}
impl From<&Id> for u32 {
fn from(value: &Id) -> Self {
value.0
}
}
impl From<Id> for u32 {
fn from(value: Id) -> Self {
value.0
}
}
use bevy::ecs::component::Component;
#[derive(Debug, Component, Copy, Clone, PartialEq)]
pub struct Id(u32);
impl Id {
pub fn new(value: u32) -> Self {
Self(value)
}
}
impl From<&Id> for u32 {
fn from(value: &Id) -> Self {
value.0
}
}
impl From<Id> for u32 {
fn from(value: Id) -> Self {
value.0
}
}

54
crates/guardian_core/src/components/mod.rs Normal file → Executable file
View File

@@ -1,27 +1,27 @@
mod awacs;
mod callsign;
mod group;
mod grpc_base_url;
mod heading;
mod id;
mod player;
mod position;
mod side;
mod srs_socket_addr;
mod stt_base_url;
mod unit_type;
mod velocity;
pub use awacs::*;
pub use callsign::*;
pub use group::*;
pub use grpc_base_url::*;
pub use heading::*;
pub use id::*;
pub use player::*;
pub use position::*;
pub use side::*;
pub use srs_socket_addr::*;
pub use stt_base_url::*;
pub use unit_type::*;
pub use velocity::*;
mod awacs;
mod callsign;
mod group;
mod grpc_base_url;
mod heading;
mod id;
mod player;
mod position;
mod side;
mod srs_socket_addr;
mod stt_base_url;
mod unit_type;
mod velocity;
pub use awacs::*;
pub use callsign::*;
pub use group::*;
pub use grpc_base_url::*;
pub use heading::*;
pub use id::*;
pub use player::*;
pub use position::*;
pub use side::*;
pub use srs_socket_addr::*;
pub use stt_base_url::*;
pub use unit_type::*;
pub use velocity::*;

158
crates/guardian_core/src/components/player.rs Normal file → Executable file
View File

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

98
crates/guardian_core/src/components/position.rs Normal file → Executable file
View File

@@ -1,49 +1,49 @@
use bevy::ecs::component::Component;
#[derive(Debug, Component, Clone)]
pub struct Position {
pub lat: f64,
pub long: f64,
pub altitude: f64,
}
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
}
}
use bevy::ecs::component::Component;
#[derive(Debug, Component, Clone)]
pub struct Position {
pub lat: f64,
pub long: f64,
pub altitude: f64,
}
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
}
}

20
crates/guardian_core/src/components/side.rs Normal file → Executable file
View File

@@ -1,10 +1,10 @@
use bevy::ecs::component::Component;
#[derive(Debug, Component)]
pub struct All;
#[derive(Debug, Component)]
pub struct Neutral;
#[derive(Debug, Component)]
pub struct Red;
#[derive(Debug, Component)]
pub struct Blue;
use bevy::ecs::component::Component;
#[derive(Debug, Component)]
pub struct All;
#[derive(Debug, Component)]
pub struct Neutral;
#[derive(Debug, Component)]
pub struct Red;
#[derive(Debug, Component)]
pub struct Blue;

68
crates/guardian_core/src/components/srs_socket_addr.rs Normal file → Executable file
View File

@@ -1,34 +1,34 @@
use std::net::SocketAddr;
use bevy::ecs::system::Resource;
use serde::{Deserialize, Serialize};
#[derive(Debug, Resource, Clone, Deserialize, Serialize)]
pub struct SrsSocketAddr(
/// The server details for DCS-SRS
SocketAddr,
);
impl Default for SrsSocketAddr {
fn default() -> Self {
Self("127.0.0.1:5002".parse().unwrap())
}
}
impl SrsSocketAddr {
pub fn new(addr: SocketAddr) -> Self {
Self(addr)
}
}
impl From<SocketAddr> for SrsSocketAddr {
fn from(value: SocketAddr) -> Self {
Self(value)
}
}
impl From<SrsSocketAddr> for SocketAddr {
fn from(value: SrsSocketAddr) -> Self {
value.0
}
}
use std::net::SocketAddr;
use bevy::ecs::system::Resource;
use serde::{Deserialize, Serialize};
#[derive(Debug, Resource, Clone, Deserialize, Serialize)]
pub struct SrsSocketAddr(
/// The server details for DCS-SRS
SocketAddr,
);
impl Default for SrsSocketAddr {
fn default() -> Self {
Self("127.0.0.1:5002".parse().unwrap())
}
}
impl SrsSocketAddr {
pub fn new(addr: SocketAddr) -> Self {
Self(addr)
}
}
impl From<SocketAddr> for SrsSocketAddr {
fn from(value: SocketAddr) -> Self {
Self(value)
}
}
impl From<SrsSocketAddr> for SocketAddr {
fn from(value: SrsSocketAddr) -> Self {
value.0
}
}

164
crates/guardian_core/src/components/stt_base_url.rs Normal file → Executable file
View File

@@ -1,82 +1,82 @@
use std::{
borrow::Cow,
hash::{Hash, Hasher},
};
use bevy::{
ecs::{reflect::ReflectResource, system::Resource},
reflect::{std_traits::ReflectDefault, Reflect},
utils::AHasher,
};
use serde::{Deserialize, Serialize};
#[derive(Reflect, Resource, Clone, Deserialize, Serialize)]
#[reflect(Resource, Default, Debug)]
pub struct SttBaseUrl {
#[serde(skip)]
hash: u64,
/// The url to the [whisper-web](https://git.avii.nl/Guardian/whisper-web) instance
url: Cow<'static, str>,
}
impl Default for SttBaseUrl {
fn default() -> Self {
SttBaseUrl::new("http://127.0.0.1:3000/")
}
}
impl SttBaseUrl {
/// Creates a new [`SttBaseUrl`] 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 = SttBaseUrl { 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 = SttBaseUrl::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 SttBaseUrl {
#[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 SttBaseUrl {
#[inline(always)]
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Debug::fmt(&self.url, f)
}
}
use std::{
borrow::Cow,
hash::{Hash, Hasher},
};
use bevy::{
ecs::{reflect::ReflectResource, system::Resource},
reflect::{std_traits::ReflectDefault, Reflect},
utils::AHasher,
};
use serde::{Deserialize, Serialize};
#[derive(Reflect, Resource, Clone, Deserialize, Serialize)]
#[reflect(Resource, Default, Debug)]
pub struct SttBaseUrl {
#[serde(skip)]
hash: u64,
/// The url to the [whisper-web](https://git.avii.nl/Guardian/whisper-web) instance
url: Cow<'static, str>,
}
impl Default for SttBaseUrl {
fn default() -> Self {
SttBaseUrl::new("http://127.0.0.1:3000/")
}
}
impl SttBaseUrl {
/// Creates a new [`SttBaseUrl`] 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 = SttBaseUrl { 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 = SttBaseUrl::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 SttBaseUrl {
#[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 SttBaseUrl {
#[inline(always)]
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Debug::fmt(&self.url, f)
}
}

158
crates/guardian_core/src/components/unit_type.rs Normal file → Executable file
View File

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

28
crates/guardian_core/src/components/velocity.rs Normal file → Executable file
View File

@@ -1,14 +1,14 @@
use bevy::ecs::component::Component;
use dcs_grpc::dcs::common::v0::Vector;
#[derive(Debug, Component)]
pub struct Velocity(pub Vector);
impl Velocity {
pub fn speed(&self) -> f64 {
let x = self.0.x;
let y = self.0.y;
let z = self.0.z;
(x * x + y * y + z * z).sqrt()
}
}
use bevy::ecs::component::Component;
use dcs_grpc::dcs::common::v0::Vector;
#[derive(Debug, Component)]
pub struct Velocity(pub Vector);
impl Velocity {
pub fn speed(&self) -> f64 {
let x = self.0.x;
let y = self.0.y;
let z = self.0.z;
(x * x + y * y + z * z).sqrt()
}
}

58
crates/guardian_core/src/dcs.rs Normal file → Executable file
View File

@@ -1,29 +1,29 @@
pub mod mission;
pub mod text;
use crate::components::GrpcBaseUrl;
use mission::MissionPlugin;
use text::TextPlugin;
// Re-export
pub use dcs_grpc::dcs::common::v0::Coalition;
use bevy::app::{App, Plugin, ScheduleRunnerPlugin};
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);
}
}
pub mod mission;
pub mod text;
use crate::components::GrpcBaseUrl;
use mission::MissionPlugin;
use text::TextPlugin;
// Re-export
pub use dcs_grpc::dcs::common::v0::Coalition;
use bevy::app::{App, Plugin, ScheduleRunnerPlugin};
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);
}
}

514
crates/guardian_core/src/dcs/mission.rs Normal file → Executable file
View File

@@ -1,257 +1,257 @@
use std::time::Duration;
use crate::{components::*, TokioResource};
use bevy::{
app::{App, Plugin, PostUpdate, PreStartup, PreUpdate},
core::Name,
ecs::{
component::Component,
entity::Entity,
event::{Event, EventReader, EventWriter},
query::With,
system::{Commands, Query, Res},
},
};
use crossbeam_channel::{unbounded, Receiver};
use dcs_grpc::dcs::{
common::v0::{Coalition, GroupCategory, Unit},
mission::v0::{
mission_service_client::MissionServiceClient, stream_units_response::Update,
StreamUnitsRequest,
},
};
use tracing::{error, info, warn};
#[derive(Clone)]
enum Response {
Disconnected,
Update(Box<Update>),
}
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),
)
.add_systems(PostUpdate, cleanup_after_disconnect);
}
}
fn connect_to_grpc(mut commands: Commands, tokio: Res<TokioResource>, url: Res<GrpcBaseUrl>) {
let handle = &tokio.0;
let (tx, task) = unbounded();
let poll_rate = url.poll_rate();
let url = url.to_string();
handle.spawn(async move {
loop {
let Ok(mut client) = MissionServiceClient::connect(url.clone()).await else {
warn!("Connection failed: {}", url); // warn
tokio::time::sleep(Duration::from_secs(5)).await;
continue;
};
info!("Connected to DCS: {}", url);
let Ok(mut stream) = client
.stream_units(StreamUnitsRequest {
poll_rate,
max_backoff: Some(30),
category: GroupCategory::Airplane as i32,
})
.await
else {
return;
};
loop {
match stream.get_mut().message().await {
Ok(Some(next)) => {
if let Some(update) = &next.update {
tx.send(Response::Update(Box::new(update.clone()))).ok();
}
}
Ok(None) => {
break;
}
Err(e) => {
error!("Error from gRPC: {:?}", e); // verbose or debug log
break;
}
}
}
warn!("Disconnected from DCS: {}", url); // warn
tx.send(Response::Disconnected).ok();
tokio::time::sleep(Duration::from_secs(5)).await;
}
});
commands.spawn(UnitsRequestMessage(task));
}
fn cleanup_after_disconnect(
mut commands: Commands,
disconnected: Query<Entity, With<Disconnected>>,
entitiews: Query<Entity, With<Id>>,
) {
for d in disconnected.iter() {
// if there is one
commands.entity(d).despawn();
for ent in entitiews.iter() {
commands.entity(ent).despawn();
}
}
}
fn consume_stream_message(
mut commands: Commands,
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 {
Response::Update(boxed) => match *boxed {
Update::Gone(unit) => {
ev_unit_gone.send(UnitGoneEvent(unit.id));
}
Update::Unit(unit) => {
ev_unit_updated.send(UnitUpdatedEvent(unit));
}
},
Response::Disconnected => {
commands.spawn(Disconnected);
}
}
}
}
}
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::new(event.id) == id {
e = commands.get_entity(ent);
break;
}
}
if e.is_none() {
e = Some(commands.spawn((Id::new(event.id), Callsign::new(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(velocity) = &event.velocity {
if let Some(velocity) = &velocity.velocity {
e.insert(Velocity(velocity.clone()));
}
}
if let Some(playername) = &event.player_name {
let playername = playername.clone();
e.insert(Player::new(playername.clone()));
if let Some(index) = playername.find('|') {
let custom_callsign = playername[..index].trim().to_string();
// testing callsign validity
if guardian_commands::call::parse_call(&format!(
"Overlord {} radio check",
custom_callsign
))
.is_ok()
{
e.insert(Callsign::new(custom_callsign.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()));
if ["A-50", "E-3A", "E-2C", "KJ-2000"].contains(&event.r#type.as_str()) {
e.insert(Awacs);
}
e.insert(UnitType::new(event.r#type.clone()));
{
e.remove::<All>();
e.remove::<Neutral>();
e.remove::<Red>();
e.remove::<Blue>();
match Coalition::try_from(event.coalition).expect("Coalition to be correct") {
Coalition::All => e.insert(All),
Coalition::Neutral => e.insert(Neutral),
Coalition::Red => e.insert(Red),
Coalition::Blue => e.insert(Blue),
};
}
}
}
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 &Id::new(gid) == id {
commands.entity(ent).despawn();
}
}
}
}
#[derive(Event)]
struct UnitUpdatedEvent(Unit);
#[derive(Event)]
struct UnitGoneEvent(u32);
#[derive(Component)]
struct UnitsRequestMessage(Receiver<Response>);
#[derive(Component)]
struct Disconnected;
use std::time::Duration;
use crate::{components::*, TokioResource};
use bevy::{
app::{App, Plugin, PostUpdate, PreStartup, PreUpdate},
core::Name,
ecs::{
component::Component,
entity::Entity,
event::{Event, EventReader, EventWriter},
query::With,
system::{Commands, Query, Res},
},
};
use crossbeam_channel::{unbounded, Receiver};
use dcs_grpc::dcs::{
common::v0::{Coalition, GroupCategory, Unit},
mission::v0::{
mission_service_client::MissionServiceClient, stream_units_response::Update,
StreamUnitsRequest,
},
};
use tracing::{error, info, warn};
#[derive(Clone)]
enum Response {
Disconnected,
Update(Box<Update>),
}
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),
)
.add_systems(PostUpdate, cleanup_after_disconnect);
}
}
fn connect_to_grpc(mut commands: Commands, tokio: Res<TokioResource>, url: Res<GrpcBaseUrl>) {
let handle = &tokio.0;
let (tx, task) = unbounded();
let poll_rate = url.poll_rate();
let url = url.to_string();
handle.spawn(async move {
loop {
let Ok(mut client) = MissionServiceClient::connect(url.clone()).await else {
warn!("Connection failed: {}", url); // warn
tokio::time::sleep(Duration::from_secs(5)).await;
continue;
};
info!("Connected to DCS: {}", url);
let Ok(mut stream) = client
.stream_units(StreamUnitsRequest {
poll_rate,
max_backoff: Some(30),
category: GroupCategory::Airplane as i32,
})
.await
else {
return;
};
loop {
match stream.get_mut().message().await {
Ok(Some(next)) => {
if let Some(update) = &next.update {
tx.send(Response::Update(Box::new(update.clone()))).ok();
}
}
Ok(None) => {
break;
}
Err(e) => {
error!("Error from gRPC: {:?}", e); // verbose or debug log
break;
}
}
}
warn!("Disconnected from DCS: {}", url); // warn
tx.send(Response::Disconnected).ok();
tokio::time::sleep(Duration::from_secs(5)).await;
}
});
commands.spawn(UnitsRequestMessage(task));
}
fn cleanup_after_disconnect(
mut commands: Commands,
disconnected: Query<Entity, With<Disconnected>>,
entitiews: Query<Entity, With<Id>>,
) {
for d in disconnected.iter() {
// if there is one
commands.entity(d).despawn();
for ent in entitiews.iter() {
commands.entity(ent).despawn();
}
}
}
fn consume_stream_message(
mut commands: Commands,
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 {
Response::Update(boxed) => match *boxed {
Update::Gone(unit) => {
ev_unit_gone.send(UnitGoneEvent(unit.id));
}
Update::Unit(unit) => {
ev_unit_updated.send(UnitUpdatedEvent(unit));
}
},
Response::Disconnected => {
commands.spawn(Disconnected);
}
}
}
}
}
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::new(event.id) == id {
e = commands.get_entity(ent);
break;
}
}
if e.is_none() {
e = Some(commands.spawn((Id::new(event.id), Callsign::new(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(velocity) = &event.velocity {
if let Some(velocity) = &velocity.velocity {
e.insert(Velocity(velocity.clone()));
}
}
if let Some(playername) = &event.player_name {
let playername = playername.clone();
e.insert(Player::new(playername.clone()));
if let Some(index) = playername.find('|') {
let custom_callsign = playername[..index].trim().to_string();
// testing callsign validity
if guardian_commands::call::parse_call(&format!(
"Overlord {} radio check",
custom_callsign
))
.is_ok()
{
e.insert(Callsign::new(custom_callsign.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()));
if ["A-50", "E-3A", "E-2C", "KJ-2000"].contains(&event.r#type.as_str()) {
e.insert(Awacs);
}
e.insert(UnitType::new(event.r#type.clone()));
{
e.remove::<All>();
e.remove::<Neutral>();
e.remove::<Red>();
e.remove::<Blue>();
match Coalition::try_from(event.coalition).expect("Coalition to be correct") {
Coalition::All => e.insert(All),
Coalition::Neutral => e.insert(Neutral),
Coalition::Red => e.insert(Red),
Coalition::Blue => e.insert(Blue),
};
}
}
}
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 &Id::new(gid) == id {
commands.entity(ent).despawn();
}
}
}
}
#[derive(Event)]
struct UnitUpdatedEvent(Unit);
#[derive(Event)]
struct UnitGoneEvent(u32);
#[derive(Component)]
struct UnitsRequestMessage(Receiver<Response>);
#[derive(Component)]
struct Disconnected;

282
crates/guardian_core/src/dcs/text.rs Normal file → Executable file
View File

@@ -1,141 +1,141 @@
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::trigger::v0::{
trigger_service_client::TriggerServiceClient, OutTextForUnitRequest,
};
use tracing::info;
use crate::{
components::{GrpcBaseUrl, Id},
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, &Id, &TextMessage), Added<TextMessage>>,
url: Res<GrpcBaseUrl>,
tokio: Res<TokioResource>,
) {
for (ent, id, msg) in query.iter() {
let url = url.to_string();
if msg.as_str().is_empty() {
commands.entity(ent).remove::<TextMessage>();
continue;
}
let message = msg.clone();
let url = url.clone();
let id: u32 = id.into();
tokio.0.spawn(async move {
let Ok(mut client) = TriggerServiceClient::connect(url.to_string()).await else {
return;
};
let request = OutTextForUnitRequest {
text: message.to_string(),
display_time: 15,
clear_view: false,
unit_id: id,
};
info!("> {}", message);
client.out_text_for_unit(request).await.ok();
});
commands.entity(ent).remove::<TextMessage>();
}
}
#[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)
}
}
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::trigger::v0::{
trigger_service_client::TriggerServiceClient, OutTextForUnitRequest,
};
use tracing::info;
use crate::{
components::{GrpcBaseUrl, Id},
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, &Id, &TextMessage), Added<TextMessage>>,
url: Res<GrpcBaseUrl>,
tokio: Res<TokioResource>,
) {
for (ent, id, msg) in query.iter() {
let url = url.to_string();
if msg.as_str().is_empty() {
commands.entity(ent).remove::<TextMessage>();
continue;
}
let message = msg.clone();
let url = url.clone();
let id: u32 = id.into();
tokio.0.spawn(async move {
let Ok(mut client) = TriggerServiceClient::connect(url.to_string()).await else {
return;
};
let request = OutTextForUnitRequest {
text: message.to_string(),
display_time: 15,
clear_view: false,
unit_id: id,
};
info!("> {}", message);
client.out_text_for_unit(request).await.ok();
});
commands.entity(ent).remove::<TextMessage>();
}
}
#[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)
}
}

58
crates/guardian_core/src/lib.rs Normal file → Executable file
View File

@@ -1,29 +1,29 @@
pub mod components;
pub mod dcs;
pub mod srs;
mod tts;
use dcs::DcsPlugin;
use srs::SrsPlugin;
use bevy::{
app::{PluginGroup, PluginGroupBuilder},
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)
.add(SrsPlugin);
group
}
}
#[derive(Resource)]
pub struct TokioResource(pub Handle);
pub mod components;
pub mod dcs;
pub mod srs;
mod tts;
use dcs::DcsPlugin;
use srs::SrsPlugin;
use bevy::{
app::{PluginGroup, PluginGroupBuilder},
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)
.add(SrsPlugin);
group
}
}
#[derive(Resource)]
pub struct TokioResource(pub Handle);

3
crates/guardian_core/src/srs.rs Normal file → Executable file
View File

@@ -261,7 +261,8 @@ fn listen_srs(
}
}
Some(data) = voice_handle.recv() => {
frames.push(synthesize("192.168.0.238:10200", data.as_str()).await?).await;
// TODO: Make this configurable
frames.push(synthesize("192.168.1.3:10200", data.as_str()).await?).await;
}
Some(data) = voice_stream.next() => {
match data {

64
crates/guardian_core/src/srs/frame_queue.rs Normal file → Executable file
View File

@@ -1,32 +1,32 @@
// a queue we can push to at will
// we need to take an item and read async,stream?
// pull from the front, push to the back
use std::{collections::VecDeque, sync::Arc};
use tokio::sync::RwLock;
pub struct FrameQueue<T> {
backend: Arc<RwLock<VecDeque<T>>>,
}
impl<T> FrameQueue<T> {
pub fn new() -> Self {
Self {
backend: Arc::new(RwLock::new(VecDeque::new())),
}
}
pub async fn push(&self, item: T) {
self.backend.write().await.push_back(item);
}
pub async fn next(&self) -> Option<T> {
self.backend.write().await.pop_front()
}
}
impl<T> Default for FrameQueue<T> {
fn default() -> Self {
Self::new()
}
}
// a queue we can push to at will
// we need to take an item and read async,stream?
// pull from the front, push to the back
use std::{collections::VecDeque, sync::Arc};
use tokio::sync::RwLock;
pub struct FrameQueue<T> {
backend: Arc<RwLock<VecDeque<T>>>,
}
impl<T> FrameQueue<T> {
pub fn new() -> Self {
Self {
backend: Arc::new(RwLock::new(VecDeque::new())),
}
}
pub async fn push(&self, item: T) {
self.backend.write().await.push_back(item);
}
pub async fn next(&self) -> Option<T> {
self.backend.write().await.pop_front()
}
}
impl<T> Default for FrameQueue<T> {
fn default() -> Self {
Self::new()
}
}

0
crates/guardian_core/src/srs/message.rs Normal file → Executable file
View File

0
crates/guardian_core/src/srs/messages_codec.rs Normal file → Executable file
View File

160
crates/guardian_core/src/srs/voice.rs Normal file → Executable file
View File

@@ -1,80 +1,80 @@
use std::{
borrow::Cow,
hash::{Hash, Hasher},
};
use bevy::{
ecs::{component::Component, reflect::ReflectComponent},
reflect::{std_traits::ReflectDefault, Reflect},
utils::AHasher,
};
#[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("")
}
}
#[allow(unused)]
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)
}
}
use std::{
borrow::Cow,
hash::{Hash, Hasher},
};
use bevy::{
ecs::{component::Component, reflect::ReflectComponent},
reflect::{std_traits::ReflectDefault, Reflect},
utils::AHasher,
};
#[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("")
}
}
#[allow(unused)]
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)
}
}

0
crates/guardian_core/src/srs/voice_codec.rs Normal file → Executable file
View File

0
crates/guardian_core/src/srs/voice_command.rs Normal file → Executable file
View File

418
crates/guardian_core/src/tts.rs Normal file → Executable file
View File

@@ -1,209 +1,209 @@
use std::io::Cursor;
use bevy::log;
use dasp::interpolate::sinc::Sinc;
use dasp::ring_buffer;
use dasp::signal;
use dasp::Sample;
use dasp::Signal;
use hound::WavSpec;
use hound::WavWriter;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpStream;
use tokio::net::ToSocketAddrs;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Deserialize)]
pub struct Packet {
#[serde(rename = "type")]
pub r#type: String,
pub version: String,
pub data_length: Option<usize>,
pub payload_length: Option<usize>,
}
#[derive(Serialize)]
struct RequestData {
text: String,
}
#[derive(Serialize)]
struct Request {
r#type: String,
data: RequestData,
}
pub async fn synthesize<A: ToSocketAddrs>(
addr: A,
text: &str,
) -> Result<Vec<Vec<u8>>, Box<dyn std::error::Error + Send + Sync + 'static>> {
let stream = TcpStream::connect(addr).await?;
let (mut read, mut write) = tokio::io::split(stream);
let request = Request {
r#type: "synthesize".to_string(),
data: RequestData {
text: text.to_string(),
},
};
let mut request = serde_json::to_string(&request)?;
request.push('\n');
write.write_all(request.as_bytes()).await?;
let mut str = String::new();
let mut pcm_buffer: Vec<u8> = Vec::new();
loop {
let Ok(c) = read.read_u8().await else {
log::error!("Unable to read byte");
break;
};
if c == b'\n' {
let Ok(packet) = serde_json::from_str::<Packet>(&str) else {
log::error!("Unable to read packet from: {}", str);
break;
};
str = String::new();
if let Some(length) = packet.data_length {
let mut buf = (0..length).map(|_| 0u8).collect::<Vec<_>>();
if let Err(e) = read.read_exact(&mut buf).await {
log::error!("Unable to read data: {:?}", e);
break;
}
};
if let Some(length) = packet.payload_length {
let mut buf = (0..length).map(|_| 0u8).collect::<Vec<_>>();
if let Err(e) = read.read_exact(&mut buf).await {
log::error!("Unable to read data: {:?}", e);
break;
}
pcm_buffer.append(&mut buf);
}
match packet.r#type.as_str() {
"audio-start" => {
pcm_buffer.clear();
}
"audio-stop" => {
log::info!("Audio received, start post-processing");
break;
}
_ => {}
}
}
str.push(c.into());
}
let wav = to_wav(&pcm_buffer)?;
let Ok(reader) = hound::WavReader::new(&*wav) else {
log::error!("Error opening reader");
return Err("Error opening reader".into());
};
// pcm_buffer is 22050Hz, need to convert this to 16000Hz
let samples = reader
.into_samples()
.filter_map(Result::ok)
.map(i16::to_sample::<f64>);
let signal = signal::from_interleaved_samples_iter(samples);
let ring_buffer = ring_buffer::Fixed::from([[0.0]; 100]);
let sinc = Sinc::new(ring_buffer);
let new_signal = signal.from_hz_to_hz(sinc, 22050.0, 16000.0);
let spec = WavSpec {
channels: 1,
sample_rate: 16000,
bits_per_sample: 16,
sample_format: hound::SampleFormat::Int,
};
let new_wav: Vec<u8> = Vec::new();
let mut new_wav_cursor = Cursor::new(new_wav);
let mut writer = WavWriter::new(&mut new_wav_cursor, spec).unwrap();
for frame in new_signal.until_exhausted() {
writer.write_sample(frame[0].to_sample::<i16>()).unwrap();
}
writer.flush().unwrap();
drop(writer);
let wav = new_wav_cursor.into_inner();
let wav = bytes::Bytes::copy_from_slice(&wav);
Ok(wav_to_opus(wav).await?)
}
async fn wav_to_opus(wav: bytes::Bytes) -> Result<Vec<Vec<u8>>, audiopus::Error> {
use audiopus::coder::Encoder;
use audiopus::{Application, Channels, SampleRate};
tokio::task::spawn_blocking(move || {
let audio_stream = wav
.chunks(2)
.map(|bytes| i16::from_le_bytes(bytes.try_into().unwrap()))
.collect::<Vec<_>>();
const MONO_20MS: usize = 16000 /* 1 channel */ * 20 / 1000;
let enc = Encoder::new(SampleRate::Hz16000, Channels::Mono, Application::Voip)?;
let mut pos = 0;
let mut output = [0; 256];
let mut frames = Vec::new();
while pos + MONO_20MS < audio_stream.len() {
let len = enc.encode(&audio_stream[pos..(pos + MONO_20MS)], &mut output)?;
frames.push(output[..len].to_vec());
pos += MONO_20MS;
}
Ok::<_, audiopus::Error>(frames)
})
.await
.unwrap()
}
fn to_wav(data: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync + 'static>> {
let input_buffer: Vec<u8> = Vec::new();
let mut input_buffer_cursor = Cursor::new(input_buffer);
let input_spec = WavSpec {
channels: 1,
sample_rate: 22050,
bits_per_sample: 16,
sample_format: hound::SampleFormat::Int,
};
let mut input = hound::WavWriter::new(&mut input_buffer_cursor, input_spec)?;
let audio_stream = data
.chunks(2)
.map(|bytes| i16::from_le_bytes(bytes.try_into().unwrap()))
.collect::<Vec<_>>();
for frame in audio_stream {
input.write_sample(frame.to_sample::<i16>()).unwrap();
}
drop(input);
Ok(input_buffer_cursor.into_inner())
}
use std::io::Cursor;
use bevy::log;
use dasp::interpolate::sinc::Sinc;
use dasp::ring_buffer;
use dasp::signal;
use dasp::Sample;
use dasp::Signal;
use hound::WavSpec;
use hound::WavWriter;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpStream;
use tokio::net::ToSocketAddrs;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Deserialize)]
pub struct Packet {
#[serde(rename = "type")]
pub r#type: String,
pub version: String,
pub data_length: Option<usize>,
pub payload_length: Option<usize>,
}
#[derive(Serialize)]
struct RequestData {
text: String,
}
#[derive(Serialize)]
struct Request {
r#type: String,
data: RequestData,
}
pub async fn synthesize<A: ToSocketAddrs>(
addr: A,
text: &str,
) -> Result<Vec<Vec<u8>>, Box<dyn std::error::Error + Send + Sync + 'static>> {
let stream = TcpStream::connect(addr).await?;
let (mut read, mut write) = tokio::io::split(stream);
let request = Request {
r#type: "synthesize".to_string(),
data: RequestData {
text: text.to_string(),
},
};
let mut request = serde_json::to_string(&request)?;
request.push('\n');
write.write_all(request.as_bytes()).await?;
let mut str = String::new();
let mut pcm_buffer: Vec<u8> = Vec::new();
loop {
let Ok(c) = read.read_u8().await else {
log::error!("Unable to read byte");
break;
};
if c == b'\n' {
let Ok(packet) = serde_json::from_str::<Packet>(&str) else {
log::error!("Unable to read packet from: {}", str);
break;
};
str = String::new();
if let Some(length) = packet.data_length {
let mut buf = (0..length).map(|_| 0u8).collect::<Vec<_>>();
if let Err(e) = read.read_exact(&mut buf).await {
log::error!("Unable to read data: {:?}", e);
break;
}
};
if let Some(length) = packet.payload_length {
let mut buf = (0..length).map(|_| 0u8).collect::<Vec<_>>();
if let Err(e) = read.read_exact(&mut buf).await {
log::error!("Unable to read data: {:?}", e);
break;
}
pcm_buffer.append(&mut buf);
}
match packet.r#type.as_str() {
"audio-start" => {
pcm_buffer.clear();
}
"audio-stop" => {
log::info!("Audio received, start post-processing");
break;
}
_ => {}
}
}
str.push(c.into());
}
let wav = to_wav(&pcm_buffer)?;
let Ok(reader) = hound::WavReader::new(&*wav) else {
log::error!("Error opening reader");
return Err("Error opening reader".into());
};
// pcm_buffer is 22050Hz, need to convert this to 16000Hz
let samples = reader
.into_samples()
.filter_map(Result::ok)
.map(i16::to_sample::<f64>);
let signal = signal::from_interleaved_samples_iter(samples);
let ring_buffer = ring_buffer::Fixed::from([[0.0]; 100]);
let sinc = Sinc::new(ring_buffer);
let new_signal = signal.from_hz_to_hz(sinc, 22050.0, 16000.0);
let spec = WavSpec {
channels: 1,
sample_rate: 16000,
bits_per_sample: 16,
sample_format: hound::SampleFormat::Int,
};
let new_wav: Vec<u8> = Vec::new();
let mut new_wav_cursor = Cursor::new(new_wav);
let mut writer = WavWriter::new(&mut new_wav_cursor, spec).unwrap();
for frame in new_signal.until_exhausted() {
writer.write_sample(frame[0].to_sample::<i16>()).unwrap();
}
writer.flush().unwrap();
drop(writer);
let wav = new_wav_cursor.into_inner();
let wav = bytes::Bytes::copy_from_slice(&wav);
Ok(wav_to_opus(wav).await?)
}
async fn wav_to_opus(wav: bytes::Bytes) -> Result<Vec<Vec<u8>>, audiopus::Error> {
use audiopus::coder::Encoder;
use audiopus::{Application, Channels, SampleRate};
tokio::task::spawn_blocking(move || {
let audio_stream = wav
.chunks(2)
.map(|bytes| i16::from_le_bytes(bytes.try_into().unwrap()))
.collect::<Vec<_>>();
const MONO_20MS: usize = 16000 /* 1 channel */ * 20 / 1000;
let enc = Encoder::new(SampleRate::Hz16000, Channels::Mono, Application::Voip)?;
let mut pos = 0;
let mut output = [0; 256];
let mut frames = Vec::new();
while pos + MONO_20MS < audio_stream.len() {
let len = enc.encode(&audio_stream[pos..(pos + MONO_20MS)], &mut output)?;
frames.push(output[..len].to_vec());
pos += MONO_20MS;
}
Ok::<_, audiopus::Error>(frames)
})
.await
.unwrap()
}
fn to_wav(data: &[u8]) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync + 'static>> {
let input_buffer: Vec<u8> = Vec::new();
let mut input_buffer_cursor = Cursor::new(input_buffer);
let input_spec = WavSpec {
channels: 1,
sample_rate: 22050,
bits_per_sample: 16,
sample_format: hound::SampleFormat::Int,
};
let mut input = hound::WavWriter::new(&mut input_buffer_cursor, input_spec)?;
let audio_stream = data
.chunks(2)
.map(|bytes| i16::from_le_bytes(bytes.try_into().unwrap()))
.collect::<Vec<_>>();
for frame in audio_stream {
input.write_sample(frame.to_sample::<i16>()).unwrap();
}
drop(input);
Ok(input_buffer_cursor.into_inner())
}