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
.gitignore vendored Normal file → Executable file
View File

0
Cargo.lock generated Normal file → Executable file
View File

0
Cargo.toml Normal file → Executable file
View File

18
LICENSE Normal file → Executable file
View File

@@ -1,9 +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.
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.

0
config.toml Normal file → Executable file
View File

0
crates/dcs-grpc/Cargo.toml Normal file → Executable file
View File

1320
crates/dcs-grpc/LICENSE Normal file → Executable file

File diff suppressed because it is too large Load Diff

0
crates/dcs-grpc/build.rs Normal file → Executable file
View File

View File

View File

0
crates/dcs-grpc/protos/dcs/common/v0/common.proto Normal file → Executable file
View File

View File

0
crates/dcs-grpc/protos/dcs/custom/v0/custom.proto Normal file → Executable file
View File

0
crates/dcs-grpc/protos/dcs/dcs.proto Normal file → Executable file
View File

0
crates/dcs-grpc/protos/dcs/group/v0/group.proto Normal file → Executable file
View File

0
crates/dcs-grpc/protos/dcs/hook/v0/hook.proto Normal file → Executable file
View File

0
crates/dcs-grpc/protos/dcs/mission/v0/mission.proto Normal file → Executable file
View File

0
crates/dcs-grpc/protos/dcs/net/v0/net.proto Normal file → Executable file
View File

0
crates/dcs-grpc/protos/dcs/srs/v0/srs.proto Normal file → Executable file
View File

0
crates/dcs-grpc/protos/dcs/timer/v0/timer.proto Normal file → Executable file
View File

0
crates/dcs-grpc/protos/dcs/trigger/v0/trigger.proto Normal file → Executable file
View File

0
crates/dcs-grpc/protos/dcs/unit/v0/unit.proto Normal file → Executable file
View File

0
crates/dcs-grpc/protos/dcs/world/v0/world.proto Normal file → Executable file
View File

0
crates/dcs-grpc/src/dcs/atmosphere.rs Normal file → Executable file
View File

0
crates/dcs-grpc/src/dcs/coalition.rs Normal file → Executable file
View File

0
crates/dcs-grpc/src/dcs/common.rs Normal file → Executable file
View File

0
crates/dcs-grpc/src/dcs/controller.rs Normal file → Executable file
View File

0
crates/dcs-grpc/src/dcs/custom.rs Normal file → Executable file
View File

0
crates/dcs-grpc/src/dcs/group.rs Normal file → Executable file
View File

0
crates/dcs-grpc/src/dcs/hook.rs Normal file → Executable file
View File

0
crates/dcs-grpc/src/dcs/mission.rs Normal file → Executable file
View File

0
crates/dcs-grpc/src/dcs/mod.rs Normal file → Executable file
View File

0
crates/dcs-grpc/src/dcs/net.rs Normal file → Executable file
View File

0
crates/dcs-grpc/src/dcs/srs.rs Normal file → Executable file
View File

0
crates/dcs-grpc/src/dcs/timer.rs Normal file → Executable file
View File

0
crates/dcs-grpc/src/dcs/trigger.rs Normal file → Executable file
View File

0
crates/dcs-grpc/src/dcs/unit.rs Normal file → Executable file
View File

0
crates/dcs-grpc/src/dcs/utils.rs Normal file → Executable file
View File

0
crates/dcs-grpc/src/dcs/world.rs Normal file → Executable file
View File

0
crates/dcs-grpc/src/lib.rs Normal file → Executable file
View File

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

902
crates/guardian_commands/src/call.rs Normal file → Executable file
View File

@@ -1,451 +1,451 @@
use std::fmt::Display;
use nom::branch::alt;
use nom::bytes::complete::{tag, tag_no_case};
use nom::character::complete::{alpha1, alphanumeric1, char, multispace0};
use nom::combinator::{map, opt, recognize};
use nom::error::ErrorKind;
use nom::sequence::tuple;
use nom::{Err as NomError, IResult};
#[derive(Debug)]
pub enum Awacs {
Overlord,
Magic,
Wizard,
Focus,
Darkstar,
}
#[derive(Debug)]
pub enum Receiver {
Awacs(Awacs),
Atc(String), //airfield name
}
impl Display for Receiver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Receiver::Awacs(Awacs::Overlord) => write!(f, "Overlord"),
Receiver::Awacs(Awacs::Magic) => write!(f, "Magic"),
Receiver::Awacs(Awacs::Wizard) => write!(f, "Wizard"),
Receiver::Awacs(Awacs::Focus) => write!(f, "Focus"),
Receiver::Awacs(Awacs::Darkstar) => write!(f, "Darkstar"),
Receiver::Atc(airfield) => write!(f, "{}", airfield.to_owned()),
}
}
}
// impl ToString for Receiver {
// fn to_string(&self) -> String {
// match self {
// Receiver::Awacs(Awacs::Overlord) => "Overlord".to_string(),
// Receiver::Awacs(Awacs::Darkstar) => "Darkstar".to_string(),
// Receiver::Atc(airfield) => airfield.to_owned(),
// }
// }
// }
fn parse_receiver(input: &str) -> IResult<&str, Receiver> {
alt((
map(tag_no_case("overlord"), |_| {
Receiver::Awacs(Awacs::Overlord)
}),
map(tag_no_case("magic"), |_| Receiver::Awacs(Awacs::Magic)),
map(tag_no_case("wizard"), |_| Receiver::Awacs(Awacs::Wizard)),
map(tag_no_case("focus"), |_| Receiver::Awacs(Awacs::Focus)),
map(tag_no_case("darkstar"), |_| {
Receiver::Awacs(Awacs::Darkstar)
}),
map(alphanumeric1, |e: &str| Receiver::Atc(e.to_owned())), // anything else = airfield? airfields _can_ contain spaces though.. soo w... hmm... parsing that is a bitch
))(input)
}
#[derive(Debug)]
pub enum Squadron {
Enfield,
Springfield,
Uzi,
Colt,
Dodge,
Ford,
Chevy,
Pontiac,
Viper,
Venom,
Lobo,
Cowboy,
Python,
Rattler,
Panther,
Wolf,
Weasel,
Wild,
Ninja,
Jedi,
Hawg,
Boar,
Pig,
Tusk,
ArmyAir,
Apache,
Crow,
Chaos,
Sioux,
Gatling,
Gunslinger,
Hammerhead,
Bootleg,
Palehorse,
Carnivore,
Saber,
Hornet,
Squid,
Ragin,
Roman,
Sting,
Jury,
Joker,
Ram,
Hawk,
Devil,
Check,
Snake,
Custom(String),
}
impl ToString for Squadron {
fn to_string(&self) -> String {
match self {
Squadron::Enfield => "Enfield".to_string(),
Squadron::Springfield => "Springfield".to_string(),
Squadron::Uzi => "Uzi".to_string(),
Squadron::Colt => "Colt".to_string(),
Squadron::Dodge => "Dodge".to_string(),
Squadron::Ford => "Ford".to_string(),
Squadron::Chevy => "Chevy".to_string(),
Squadron::Hornet => "Hornet".to_string(),
Squadron::Pontiac => "Pontiac".to_string(),
Squadron::Viper => "Viper".to_string(),
Squadron::Venom => "Venom".to_string(),
Squadron::Lobo => "Lobo".to_string(),
Squadron::Cowboy => "Cowboy".to_string(),
Squadron::Python => "Python".to_string(),
Squadron::Rattler => "Rattler".to_string(),
Squadron::Panther => "Panther".to_string(),
Squadron::Wolf => "Wolf".to_string(),
Squadron::Weasel => "Weasel".to_string(),
Squadron::Wild => "Wild".to_string(),
Squadron::Ninja => "Ninja".to_string(),
Squadron::Jedi => "Jedi".to_string(),
Squadron::Hawg => "Hawg".to_string(),
Squadron::Boar => "Boar".to_string(),
Squadron::Pig => "Pig".to_string(),
Squadron::Tusk => "Tusk".to_string(),
Squadron::ArmyAir => "ArmyAir".to_string(),
Squadron::Apache => "Apache".to_string(),
Squadron::Crow => "Crow".to_string(),
Squadron::Chaos => "Chaos".to_string(),
Squadron::Sioux => "Sioux".to_string(),
Squadron::Gatling => "Gatling".to_string(),
Squadron::Gunslinger => "Gunslinger".to_string(),
Squadron::Hammerhead => "Hammerhead".to_string(),
Squadron::Bootleg => "Bootleg".to_string(),
Squadron::Palehorse => "Palehorse".to_string(),
Squadron::Carnivore => "Carnivore".to_string(),
Squadron::Saber => "Saber".to_string(),
Squadron::Squid => "Squid".to_string(),
Squadron::Ragin => "Ragin".to_string(),
Squadron::Roman => "Roman".to_string(),
Squadron::Sting => "Sting".to_string(),
Squadron::Jury => "Jury".to_string(),
Squadron::Joker => "Joker".to_string(),
Squadron::Ram => "Ram".to_string(),
Squadron::Hawk => "Hawk".to_string(),
Squadron::Devil => "Devil".to_string(),
Squadron::Check => "Check".to_string(),
Squadron::Snake => "Snake".to_string(),
Squadron::Custom(c) => c.to_string(),
}
}
}
impl std::str::FromStr for Squadron {
type Err = Box<dyn std::error::Error + Send + Sync>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"Enfield" => Ok(Squadron::Enfield),
"Springfield" => Ok(Squadron::Springfield),
"Uzi" => Ok(Squadron::Uzi),
"Colt" => Ok(Squadron::Colt),
"Dodge" => Ok(Squadron::Dodge),
"Ford" => Ok(Squadron::Ford),
"Chevy" => Ok(Squadron::Chevy),
"Hornet" => Ok(Squadron::Hornet),
"Pontiac" => Ok(Squadron::Pontiac),
"Viper" => Ok(Squadron::Viper),
"Venom" => Ok(Squadron::Venom),
"Lobo" => Ok(Squadron::Lobo),
"Cowboy" => Ok(Squadron::Cowboy),
"Python" => Ok(Squadron::Python),
"Rattler" => Ok(Squadron::Rattler),
"Panther" => Ok(Squadron::Panther),
"Wolf" => Ok(Squadron::Wolf),
"Weasel" => Ok(Squadron::Weasel),
"Wild" => Ok(Squadron::Wild),
"Ninja" => Ok(Squadron::Ninja),
"Jedi" => Ok(Squadron::Jedi),
"Hawg" => Ok(Squadron::Hawg),
"Boar" => Ok(Squadron::Boar),
"Pig" => Ok(Squadron::Pig),
"Tusk" => Ok(Squadron::Tusk),
"ArmyAir" => Ok(Squadron::ArmyAir),
"Apache" => Ok(Squadron::Apache),
"Crow" => Ok(Squadron::Crow),
"Chaos" => Ok(Squadron::Chaos),
"Sioux" => Ok(Squadron::Sioux),
"Gatling" => Ok(Squadron::Gatling),
"Gunslinger" => Ok(Squadron::Gunslinger),
"Hammerhead" => Ok(Squadron::Hammerhead),
"Bootleg" => Ok(Squadron::Bootleg),
"Palehorse" => Ok(Squadron::Palehorse),
"Carnivore" => Ok(Squadron::Carnivore),
"Saber" => Ok(Squadron::Saber),
"Squid" => Ok(Squadron::Squid),
"Ragin" => Ok(Squadron::Ragin),
"Roman" => Ok(Squadron::Roman),
"Sting" => Ok(Squadron::Sting),
"Jury" => Ok(Squadron::Jury),
"Joker" => Ok(Squadron::Joker),
"Ram" => Ok(Squadron::Ram),
"Hawk" => Ok(Squadron::Hawk),
"Devil" => Ok(Squadron::Devil),
"Check" => Ok(Squadron::Check),
"Snake" => Ok(Squadron::Snake),
c => Ok(Squadron::Custom(c.to_string())),
}
}
}
#[derive(Debug)]
pub struct Element {
pub squadron: Squadron,
pub group: u8,
pub unit: u8,
}
impl Display for Element {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} {} {}",
self.squadron.to_string(),
self.group,
self.unit
)
}
}
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 std::str::FromStr for Element {
type Err = Box<dyn std::error::Error + Send + Sync>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let d = add_spaces_around_numbers(s);
let mut d = d.split(' ');
let squadron: Squadron = d.next().unwrap().parse()?;
let group: u8 = d.next().unwrap().parse()?;
let unit: u8 = d.next().unwrap().parse()?;
Ok(Element {
squadron,
group,
unit,
})
}
}
fn parse_element(input: &str) -> IResult<&str, Element> {
let mut parser = tuple((
alpha1, // Squadron name
opt(multispace0), // Optional whitespace
opt(tuple((
// Optional group
opt(multispace0),
parse_group_unit,
opt(multispace0),
))),
opt(char('-')),
opt(tuple((
// Optional unit
opt(multispace0),
parse_group_unit,
opt(multispace0),
))),
));
let (remainder, (squadron_str, _, group_str, _, unit_str)) = parser(input)?;
let squadron = match squadron_str.to_lowercase().as_str() {
"enfield" => Squadron::Enfield,
"springfield" => Squadron::Springfield,
"uzi" => Squadron::Uzi,
"colt" => Squadron::Colt,
"dodge" => Squadron::Dodge,
"ford" => Squadron::Ford,
"chevy" => Squadron::Chevy,
"hornet" => Squadron::Hornet,
"pontiac" => Squadron::Pontiac,
"viper" => Squadron::Viper,
"venom" => Squadron::Venom,
"lobo" => Squadron::Lobo,
"cowboy" => Squadron::Cowboy,
"python" => Squadron::Python,
"rattler" => Squadron::Rattler,
"panther" => Squadron::Panther,
"wolf" => Squadron::Wolf,
"weasel" => Squadron::Weasel,
"wild" => Squadron::Wild,
"ninja" => Squadron::Ninja,
"jedi" => Squadron::Jedi,
"hawg" => Squadron::Hawg,
"boar" => Squadron::Boar,
"pig" => Squadron::Pig,
"tusk" => Squadron::Tusk,
"armyair" => Squadron::ArmyAir,
"apache" => Squadron::Apache,
"crow" => Squadron::Crow,
"chaos" => Squadron::Chaos,
"sioux" => Squadron::Sioux,
"gatling" => Squadron::Gatling,
"gunslinger" => Squadron::Gunslinger,
"hammerhead" => Squadron::Hammerhead,
"bootleg" => Squadron::Bootleg,
"palehorse" => Squadron::Palehorse,
"carnivore" => Squadron::Carnivore,
"saber" => Squadron::Saber,
"squid" => Squadron::Squid,
"ragin" => Squadron::Ragin,
"roman" => Squadron::Roman,
"sting" => Squadron::Sting,
"jury" => Squadron::Jury,
"joker" => Squadron::Joker,
"ram" => Squadron::Ram,
"hawk" => Squadron::Hawk,
"devil" => Squadron::Devil,
"check" => Squadron::Check,
"snake" => Squadron::Snake,
c => Squadron::Custom(c.to_string()),
};
// _ => {
// return Err(NomError::Error(nom::error::Error {
// input: squadron_str,
// code: ErrorKind::Alpha,
// }))
// }
let group = group_str.unwrap_or((None, 0, None)).1;
let unit = unit_str.unwrap_or((None, 0, None)).1;
Ok((
remainder,
Element {
squadron,
group,
unit,
},
))
}
fn parse_group_unit(input: &str) -> IResult<&str, u8> {
let parse_digit = map(
recognize(alt((
char('0'),
char('1'),
char('2'),
char('3'),
char('4'),
char('5'),
char('6'),
char('7'),
char('8'),
char('9'),
))),
|s: &str| s.parse::<u8>().unwrap(),
);
let parse_word = alt((
map(tag("one"), |_| 1),
map(tag("two"), |_| 2),
map(tag("three"), |_| 3),
map(tag("four"), |_| 4),
map(tag("five"), |_| 5),
map(tag("six"), |_| 6),
map(tag("seven"), |_| 7),
map(tag("eight"), |_| 8),
map(tag("nine"), |_| 9),
));
alt((parse_digit, parse_word))(input)
}
// #[derive(Debug)]
// pub enum Unit {
// NauticalMiles,
// Kilometers,
// }
// #[derive(Debug)]
// pub enum Call {
// RadioCheck(Awacs, Element),
// Shopping(Awacs, Element),
// LocateAirfield(Awacs, Element, String),
// LocateFriendly(Awacs, Element, Element),
// Tripwire(Awacs, Element, Option<i32>, Unit),
// }
#[derive(Debug)]
pub struct Call {
pub receiver: Receiver,
pub element: Element,
pub command: String,
}
pub fn parse_call(input: &str) -> IResult<&str, Call> {
let mut parser = tuple((parse_receiver, multispace0, parse_element));
let (command, (receiver, _, element)) = parser(input)?;
if element.group == 0 || element.unit == 0 {
return Err(NomError::Error(nom::error::Error {
input: command,
code: ErrorKind::Digit,
}));
}
Ok((
"",
Call {
receiver,
element,
command: command.to_string(),
},
))
}
use std::fmt::Display;
use nom::branch::alt;
use nom::bytes::complete::{tag, tag_no_case};
use nom::character::complete::{alpha1, alphanumeric1, char, multispace0};
use nom::combinator::{map, opt, recognize};
use nom::error::ErrorKind;
use nom::sequence::tuple;
use nom::{Err as NomError, IResult};
#[derive(Debug)]
pub enum Awacs {
Overlord,
Magic,
Wizard,
Focus,
Darkstar,
}
#[derive(Debug)]
pub enum Receiver {
Awacs(Awacs),
Atc(String), //airfield name
}
impl Display for Receiver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Receiver::Awacs(Awacs::Overlord) => write!(f, "Overlord"),
Receiver::Awacs(Awacs::Magic) => write!(f, "Magic"),
Receiver::Awacs(Awacs::Wizard) => write!(f, "Wizard"),
Receiver::Awacs(Awacs::Focus) => write!(f, "Focus"),
Receiver::Awacs(Awacs::Darkstar) => write!(f, "Darkstar"),
Receiver::Atc(airfield) => write!(f, "{}", airfield.to_owned()),
}
}
}
// impl ToString for Receiver {
// fn to_string(&self) -> String {
// match self {
// Receiver::Awacs(Awacs::Overlord) => "Overlord".to_string(),
// Receiver::Awacs(Awacs::Darkstar) => "Darkstar".to_string(),
// Receiver::Atc(airfield) => airfield.to_owned(),
// }
// }
// }
fn parse_receiver(input: &str) -> IResult<&str, Receiver> {
alt((
map(tag_no_case("overlord"), |_| {
Receiver::Awacs(Awacs::Overlord)
}),
map(tag_no_case("magic"), |_| Receiver::Awacs(Awacs::Magic)),
map(tag_no_case("wizard"), |_| Receiver::Awacs(Awacs::Wizard)),
map(tag_no_case("focus"), |_| Receiver::Awacs(Awacs::Focus)),
map(tag_no_case("darkstar"), |_| {
Receiver::Awacs(Awacs::Darkstar)
}),
map(alphanumeric1, |e: &str| Receiver::Atc(e.to_owned())), // anything else = airfield? airfields _can_ contain spaces though.. soo w... hmm... parsing that is a bitch
))(input)
}
#[derive(Debug)]
pub enum Squadron {
Enfield,
Springfield,
Uzi,
Colt,
Dodge,
Ford,
Chevy,
Pontiac,
Viper,
Venom,
Lobo,
Cowboy,
Python,
Rattler,
Panther,
Wolf,
Weasel,
Wild,
Ninja,
Jedi,
Hawg,
Boar,
Pig,
Tusk,
ArmyAir,
Apache,
Crow,
Chaos,
Sioux,
Gatling,
Gunslinger,
Hammerhead,
Bootleg,
Palehorse,
Carnivore,
Saber,
Hornet,
Squid,
Ragin,
Roman,
Sting,
Jury,
Joker,
Ram,
Hawk,
Devil,
Check,
Snake,
Custom(String),
}
impl ToString for Squadron {
fn to_string(&self) -> String {
match self {
Squadron::Enfield => "Enfield".to_string(),
Squadron::Springfield => "Springfield".to_string(),
Squadron::Uzi => "Uzi".to_string(),
Squadron::Colt => "Colt".to_string(),
Squadron::Dodge => "Dodge".to_string(),
Squadron::Ford => "Ford".to_string(),
Squadron::Chevy => "Chevy".to_string(),
Squadron::Hornet => "Hornet".to_string(),
Squadron::Pontiac => "Pontiac".to_string(),
Squadron::Viper => "Viper".to_string(),
Squadron::Venom => "Venom".to_string(),
Squadron::Lobo => "Lobo".to_string(),
Squadron::Cowboy => "Cowboy".to_string(),
Squadron::Python => "Python".to_string(),
Squadron::Rattler => "Rattler".to_string(),
Squadron::Panther => "Panther".to_string(),
Squadron::Wolf => "Wolf".to_string(),
Squadron::Weasel => "Weasel".to_string(),
Squadron::Wild => "Wild".to_string(),
Squadron::Ninja => "Ninja".to_string(),
Squadron::Jedi => "Jedi".to_string(),
Squadron::Hawg => "Hawg".to_string(),
Squadron::Boar => "Boar".to_string(),
Squadron::Pig => "Pig".to_string(),
Squadron::Tusk => "Tusk".to_string(),
Squadron::ArmyAir => "ArmyAir".to_string(),
Squadron::Apache => "Apache".to_string(),
Squadron::Crow => "Crow".to_string(),
Squadron::Chaos => "Chaos".to_string(),
Squadron::Sioux => "Sioux".to_string(),
Squadron::Gatling => "Gatling".to_string(),
Squadron::Gunslinger => "Gunslinger".to_string(),
Squadron::Hammerhead => "Hammerhead".to_string(),
Squadron::Bootleg => "Bootleg".to_string(),
Squadron::Palehorse => "Palehorse".to_string(),
Squadron::Carnivore => "Carnivore".to_string(),
Squadron::Saber => "Saber".to_string(),
Squadron::Squid => "Squid".to_string(),
Squadron::Ragin => "Ragin".to_string(),
Squadron::Roman => "Roman".to_string(),
Squadron::Sting => "Sting".to_string(),
Squadron::Jury => "Jury".to_string(),
Squadron::Joker => "Joker".to_string(),
Squadron::Ram => "Ram".to_string(),
Squadron::Hawk => "Hawk".to_string(),
Squadron::Devil => "Devil".to_string(),
Squadron::Check => "Check".to_string(),
Squadron::Snake => "Snake".to_string(),
Squadron::Custom(c) => c.to_string(),
}
}
}
impl std::str::FromStr for Squadron {
type Err = Box<dyn std::error::Error + Send + Sync>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"Enfield" => Ok(Squadron::Enfield),
"Springfield" => Ok(Squadron::Springfield),
"Uzi" => Ok(Squadron::Uzi),
"Colt" => Ok(Squadron::Colt),
"Dodge" => Ok(Squadron::Dodge),
"Ford" => Ok(Squadron::Ford),
"Chevy" => Ok(Squadron::Chevy),
"Hornet" => Ok(Squadron::Hornet),
"Pontiac" => Ok(Squadron::Pontiac),
"Viper" => Ok(Squadron::Viper),
"Venom" => Ok(Squadron::Venom),
"Lobo" => Ok(Squadron::Lobo),
"Cowboy" => Ok(Squadron::Cowboy),
"Python" => Ok(Squadron::Python),
"Rattler" => Ok(Squadron::Rattler),
"Panther" => Ok(Squadron::Panther),
"Wolf" => Ok(Squadron::Wolf),
"Weasel" => Ok(Squadron::Weasel),
"Wild" => Ok(Squadron::Wild),
"Ninja" => Ok(Squadron::Ninja),
"Jedi" => Ok(Squadron::Jedi),
"Hawg" => Ok(Squadron::Hawg),
"Boar" => Ok(Squadron::Boar),
"Pig" => Ok(Squadron::Pig),
"Tusk" => Ok(Squadron::Tusk),
"ArmyAir" => Ok(Squadron::ArmyAir),
"Apache" => Ok(Squadron::Apache),
"Crow" => Ok(Squadron::Crow),
"Chaos" => Ok(Squadron::Chaos),
"Sioux" => Ok(Squadron::Sioux),
"Gatling" => Ok(Squadron::Gatling),
"Gunslinger" => Ok(Squadron::Gunslinger),
"Hammerhead" => Ok(Squadron::Hammerhead),
"Bootleg" => Ok(Squadron::Bootleg),
"Palehorse" => Ok(Squadron::Palehorse),
"Carnivore" => Ok(Squadron::Carnivore),
"Saber" => Ok(Squadron::Saber),
"Squid" => Ok(Squadron::Squid),
"Ragin" => Ok(Squadron::Ragin),
"Roman" => Ok(Squadron::Roman),
"Sting" => Ok(Squadron::Sting),
"Jury" => Ok(Squadron::Jury),
"Joker" => Ok(Squadron::Joker),
"Ram" => Ok(Squadron::Ram),
"Hawk" => Ok(Squadron::Hawk),
"Devil" => Ok(Squadron::Devil),
"Check" => Ok(Squadron::Check),
"Snake" => Ok(Squadron::Snake),
c => Ok(Squadron::Custom(c.to_string())),
}
}
}
#[derive(Debug)]
pub struct Element {
pub squadron: Squadron,
pub group: u8,
pub unit: u8,
}
impl Display for Element {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} {} {}",
self.squadron.to_string(),
self.group,
self.unit
)
}
}
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 std::str::FromStr for Element {
type Err = Box<dyn std::error::Error + Send + Sync>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let d = add_spaces_around_numbers(s);
let mut d = d.split(' ');
let squadron: Squadron = d.next().unwrap().parse()?;
let group: u8 = d.next().unwrap().parse()?;
let unit: u8 = d.next().unwrap().parse()?;
Ok(Element {
squadron,
group,
unit,
})
}
}
fn parse_element(input: &str) -> IResult<&str, Element> {
let mut parser = tuple((
alpha1, // Squadron name
opt(multispace0), // Optional whitespace
opt(tuple((
// Optional group
opt(multispace0),
parse_group_unit,
opt(multispace0),
))),
opt(char('-')),
opt(tuple((
// Optional unit
opt(multispace0),
parse_group_unit,
opt(multispace0),
))),
));
let (remainder, (squadron_str, _, group_str, _, unit_str)) = parser(input)?;
let squadron = match squadron_str.to_lowercase().as_str() {
"enfield" => Squadron::Enfield,
"springfield" => Squadron::Springfield,
"uzi" => Squadron::Uzi,
"colt" => Squadron::Colt,
"dodge" => Squadron::Dodge,
"ford" => Squadron::Ford,
"chevy" => Squadron::Chevy,
"hornet" => Squadron::Hornet,
"pontiac" => Squadron::Pontiac,
"viper" => Squadron::Viper,
"venom" => Squadron::Venom,
"lobo" => Squadron::Lobo,
"cowboy" => Squadron::Cowboy,
"python" => Squadron::Python,
"rattler" => Squadron::Rattler,
"panther" => Squadron::Panther,
"wolf" => Squadron::Wolf,
"weasel" => Squadron::Weasel,
"wild" => Squadron::Wild,
"ninja" => Squadron::Ninja,
"jedi" => Squadron::Jedi,
"hawg" => Squadron::Hawg,
"boar" => Squadron::Boar,
"pig" => Squadron::Pig,
"tusk" => Squadron::Tusk,
"armyair" => Squadron::ArmyAir,
"apache" => Squadron::Apache,
"crow" => Squadron::Crow,
"chaos" => Squadron::Chaos,
"sioux" => Squadron::Sioux,
"gatling" => Squadron::Gatling,
"gunslinger" => Squadron::Gunslinger,
"hammerhead" => Squadron::Hammerhead,
"bootleg" => Squadron::Bootleg,
"palehorse" => Squadron::Palehorse,
"carnivore" => Squadron::Carnivore,
"saber" => Squadron::Saber,
"squid" => Squadron::Squid,
"ragin" => Squadron::Ragin,
"roman" => Squadron::Roman,
"sting" => Squadron::Sting,
"jury" => Squadron::Jury,
"joker" => Squadron::Joker,
"ram" => Squadron::Ram,
"hawk" => Squadron::Hawk,
"devil" => Squadron::Devil,
"check" => Squadron::Check,
"snake" => Squadron::Snake,
c => Squadron::Custom(c.to_string()),
};
// _ => {
// return Err(NomError::Error(nom::error::Error {
// input: squadron_str,
// code: ErrorKind::Alpha,
// }))
// }
let group = group_str.unwrap_or((None, 0, None)).1;
let unit = unit_str.unwrap_or((None, 0, None)).1;
Ok((
remainder,
Element {
squadron,
group,
unit,
},
))
}
fn parse_group_unit(input: &str) -> IResult<&str, u8> {
let parse_digit = map(
recognize(alt((
char('0'),
char('1'),
char('2'),
char('3'),
char('4'),
char('5'),
char('6'),
char('7'),
char('8'),
char('9'),
))),
|s: &str| s.parse::<u8>().unwrap(),
);
let parse_word = alt((
map(tag("one"), |_| 1),
map(tag("two"), |_| 2),
map(tag("three"), |_| 3),
map(tag("four"), |_| 4),
map(tag("five"), |_| 5),
map(tag("six"), |_| 6),
map(tag("seven"), |_| 7),
map(tag("eight"), |_| 8),
map(tag("nine"), |_| 9),
));
alt((parse_digit, parse_word))(input)
}
// #[derive(Debug)]
// pub enum Unit {
// NauticalMiles,
// Kilometers,
// }
// #[derive(Debug)]
// pub enum Call {
// RadioCheck(Awacs, Element),
// Shopping(Awacs, Element),
// LocateAirfield(Awacs, Element, String),
// LocateFriendly(Awacs, Element, Element),
// Tripwire(Awacs, Element, Option<i32>, Unit),
// }
#[derive(Debug)]
pub struct Call {
pub receiver: Receiver,
pub element: Element,
pub command: String,
}
pub fn parse_call(input: &str) -> IResult<&str, Call> {
let mut parser = tuple((parse_receiver, multispace0, parse_element));
let (command, (receiver, _, element)) = parser(input)?;
if element.group == 0 || element.unit == 0 {
return Err(NomError::Error(nom::error::Error {
input: command,
code: ErrorKind::Digit,
}));
}
Ok((
"",
Call {
receiver,
element,
command: command.to_string(),
},
))
}

1098
crates/guardian_commands/src/console.rs Normal file → Executable file

File diff suppressed because it is too large Load Diff

0
crates/guardian_commands/src/lib.rs Normal file → Executable file
View File

388
crates/guardian_commands/src/parser.rs Normal file → Executable file
View File

@@ -1,194 +1,194 @@
use nom::{
bytes::complete::tag,
error::{Error, ErrorKind, ParseError},
sequence::delimited,
IResult,
};
// from https://github.com/getreu/parse-hyperlinks/blob/5af034d14aa72ffb9e705da13bf557a564b1bebf/parse-hyperlinks/src/lib.rs#L41
fn take_until_unbalanced(
opening_bracket: char,
closing_bracket: char,
) -> impl Fn(&str) -> IResult<&str, &str> {
move |i: &str| {
let mut index = 0;
let mut bracket_counter = 0;
while let Some(n) = &i[index..].find(&[opening_bracket, closing_bracket, '\\'][..]) {
index += n;
let mut it = i[index..].chars();
match it.next() {
Some('\\') => {
// Skip the escape char `\`.
index += '\\'.len_utf8();
// Skip also the following char.
if let Some(c) = it.next() {
index += c.len_utf8();
}
}
Some(c) if c == opening_bracket => {
bracket_counter += 1;
index += opening_bracket.len_utf8();
}
Some(c) if c == closing_bracket => {
// Closing bracket.
bracket_counter -= 1;
index += closing_bracket.len_utf8();
}
// Can not happen.
_ => unreachable!(),
};
// We found the unmatched closing bracket.
if bracket_counter == -1 {
// We do not consume it.
index -= closing_bracket.len_utf8();
return Ok((&i[index..], &i[0..index]));
};
}
if bracket_counter == 0 {
Ok(("", i))
} else {
Err(nom::Err::Error(Error::from_error_kind(
i,
ErrorKind::TakeUntil,
)))
}
}
}
fn split(input: &str) -> IResult<&str, &str> {
let mut parser = delimited(tag("["), take_until_unbalanced('[', ']'), tag("]"));
parser(input)
}
fn inner_parse(input: &str, depth: usize) -> Vec<String> {
let mut output = vec![];
let start_idx = input.find('[').unwrap_or(input.len());
if start_idx > 0 && depth == 0 {
output.push(input[..start_idx].to_string());
}
if start_idx == input.len() {
return output;
}
let input = &input[start_idx..];
let splitted = split(input).unwrap();
let variants: Vec<String> = splitted
.1
.split('|')
.map(|v| v.to_string())
.collect::<Vec<_>>();
let mut temp = vec![];
let mut temp_str = String::new();
for v in &variants {
if v.contains('[') || v.contains(']') {
temp_str.push_str(v);
temp_str.push('|');
} else {
let len = temp_str.len();
if len > 0 {
temp.push(temp_str[..len - 1].to_string());
temp.push(v.to_string());
}
temp_str.clear();
}
}
let variants = if temp.is_empty() { variants } else { temp };
// check if variants need expanding
let mut temp = vec![];
for v in variants {
if v.contains('[') {
let index = v.find('[').unwrap_or(0);
let prepend = &v[..index];
let remainder = inner_parse(&v, depth + 1);
for r in &remainder {
let l = format!("{}{}", prepend, r);
temp.push(l);
}
} else {
temp.push(v);
}
}
let remainder = splitted.0;
let backup = output.clone();
output.clear();
for variant in temp {
if remainder.contains('[') {
let index = remainder.find('[').unwrap_or(0);
let prepend = &remainder[..index];
let r2 = inner_parse(remainder, depth + 1);
if !backup.is_empty() {
for o in &backup {
for r in &r2 {
let l = format!("{}{}{}{}", o, variant, prepend, r);
output.push(l);
}
}
} else {
for r in &r2 {
let l = format!("{}{}{}", variant, prepend, r);
output.push(l);
}
}
} else if !backup.is_empty() {
for o in &backup {
output.push(format!("{}{}{}", o, variant, remainder));
}
} else {
output.push(format!("{}{}", variant, remainder));
}
}
output
}
pub fn parse(input: &str) -> Vec<String> {
inner_parse(input, 0)
}
#[test]
fn parse_test() {
// This works
assert_eq!(
parse("[Hello|Goodbye], World!"),
vec!["Hello, World!", "Goodbye, World!"]
);
assert_eq!(
parse("[Hello|Goodbye], [World|Moon]!"),
vec![
"Hello, World!",
"Hello, Moon!",
"Goodbye, World!",
"Goodbye, Moon!",
]
);
assert_eq!(
parse("[[Heading|Bearing] [to|for]|Where is] that"),
vec![
"Heading to that",
"Heading for that",
"Bearing to that",
"Bearing for that",
"Where is that",
]
);
assert_eq!(
parse("set [warning|tripwire]"),
vec!["set warning", "set tripwire"]
);
}
use nom::{
bytes::complete::tag,
error::{Error, ErrorKind, ParseError},
sequence::delimited,
IResult,
};
// from https://github.com/getreu/parse-hyperlinks/blob/5af034d14aa72ffb9e705da13bf557a564b1bebf/parse-hyperlinks/src/lib.rs#L41
fn take_until_unbalanced(
opening_bracket: char,
closing_bracket: char,
) -> impl Fn(&str) -> IResult<&str, &str> {
move |i: &str| {
let mut index = 0;
let mut bracket_counter = 0;
while let Some(n) = &i[index..].find(&[opening_bracket, closing_bracket, '\\'][..]) {
index += n;
let mut it = i[index..].chars();
match it.next() {
Some('\\') => {
// Skip the escape char `\`.
index += '\\'.len_utf8();
// Skip also the following char.
if let Some(c) = it.next() {
index += c.len_utf8();
}
}
Some(c) if c == opening_bracket => {
bracket_counter += 1;
index += opening_bracket.len_utf8();
}
Some(c) if c == closing_bracket => {
// Closing bracket.
bracket_counter -= 1;
index += closing_bracket.len_utf8();
}
// Can not happen.
_ => unreachable!(),
};
// We found the unmatched closing bracket.
if bracket_counter == -1 {
// We do not consume it.
index -= closing_bracket.len_utf8();
return Ok((&i[index..], &i[0..index]));
};
}
if bracket_counter == 0 {
Ok(("", i))
} else {
Err(nom::Err::Error(Error::from_error_kind(
i,
ErrorKind::TakeUntil,
)))
}
}
}
fn split(input: &str) -> IResult<&str, &str> {
let mut parser = delimited(tag("["), take_until_unbalanced('[', ']'), tag("]"));
parser(input)
}
fn inner_parse(input: &str, depth: usize) -> Vec<String> {
let mut output = vec![];
let start_idx = input.find('[').unwrap_or(input.len());
if start_idx > 0 && depth == 0 {
output.push(input[..start_idx].to_string());
}
if start_idx == input.len() {
return output;
}
let input = &input[start_idx..];
let splitted = split(input).unwrap();
let variants: Vec<String> = splitted
.1
.split('|')
.map(|v| v.to_string())
.collect::<Vec<_>>();
let mut temp = vec![];
let mut temp_str = String::new();
for v in &variants {
if v.contains('[') || v.contains(']') {
temp_str.push_str(v);
temp_str.push('|');
} else {
let len = temp_str.len();
if len > 0 {
temp.push(temp_str[..len - 1].to_string());
temp.push(v.to_string());
}
temp_str.clear();
}
}
let variants = if temp.is_empty() { variants } else { temp };
// check if variants need expanding
let mut temp = vec![];
for v in variants {
if v.contains('[') {
let index = v.find('[').unwrap_or(0);
let prepend = &v[..index];
let remainder = inner_parse(&v, depth + 1);
for r in &remainder {
let l = format!("{}{}", prepend, r);
temp.push(l);
}
} else {
temp.push(v);
}
}
let remainder = splitted.0;
let backup = output.clone();
output.clear();
for variant in temp {
if remainder.contains('[') {
let index = remainder.find('[').unwrap_or(0);
let prepend = &remainder[..index];
let r2 = inner_parse(remainder, depth + 1);
if !backup.is_empty() {
for o in &backup {
for r in &r2 {
let l = format!("{}{}{}{}", o, variant, prepend, r);
output.push(l);
}
}
} else {
for r in &r2 {
let l = format!("{}{}{}", variant, prepend, r);
output.push(l);
}
}
} else if !backup.is_empty() {
for o in &backup {
output.push(format!("{}{}{}", o, variant, remainder));
}
} else {
output.push(format!("{}{}", variant, remainder));
}
}
output
}
pub fn parse(input: &str) -> Vec<String> {
inner_parse(input, 0)
}
#[test]
fn parse_test() {
// This works
assert_eq!(
parse("[Hello|Goodbye], World!"),
vec!["Hello, World!", "Goodbye, World!"]
);
assert_eq!(
parse("[Hello|Goodbye], [World|Moon]!"),
vec![
"Hello, World!",
"Hello, Moon!",
"Goodbye, World!",
"Goodbye, Moon!",
]
);
assert_eq!(
parse("[[Heading|Bearing] [to|for]|Where is] that"),
vec![
"Heading to that",
"Heading for that",
"Bearing to that",
"Bearing for that",
"Where is that",
]
);
assert_eq!(
parse("set [warning|tripwire]"),
vec!["set warning", "set tripwire"]
);
}

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())
}

4
rust-toolchain.toml Normal file → Executable file
View File

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

194
src/braa.rs Normal file → Executable file
View File

@@ -1,97 +1,97 @@
use std::fmt::Display;
use guardian_core::components::{Heading, Position};
#[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 Display for Braa {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_text())
}
}
#[allow(unused)]
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 . . . {} . . . {} . . . {} thousand . . . {}",
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>()
}
use std::fmt::Display;
use guardian_core::components::{Heading, Position};
#[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 Display for Braa {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_text())
}
}
#[allow(unused)]
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 . . . {} . . . {} . . . {} thousand . . . {}",
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>()
}

186
src/commands/bogeydope.rs Normal file → Executable file
View File

@@ -1,93 +1,93 @@
use bevy::ecs::{
query::{With, Without},
system::{Commands, Query, Resource},
};
use clap::Parser;
use guardian_commands::{ConsoleCommand, NamedCommand};
use guardian_core::{components::*, dcs::text::TextMessage, srs::voice::VoiceMessage};
use tracing::debug;
use crate::braa::Braa;
#[derive(Parser, Debug)]
pub struct BogeyDope;
impl NamedCommand for BogeyDope {
fn name() -> &'static str {
"[shopping|bogey dope]"
}
}
impl Resource for BogeyDope {}
pub fn bogey_dope(
mut commands: Commands,
mut cmd: ConsoleCommand<BogeyDope>,
awacs: Query<&Callsign>,
player: Query<(&Callsign, &Position)>,
npc: Query<(&Position, &Heading, &Velocity), (Without<Player>, With<Red>)>,
) {
let Some(Ok(BogeyDope)) = cmd.take() else {
return;
};
let Some(pilot) = &cmd.pilot else {
return; // no pilot
};
let Some(operator) = &cmd.operator else {
return;
};
let Ok((p_callsign, p_pos)) = player.get(*pilot) else {
debug!("no player, died?");
return;
};
let Ok(a_callsign) = awacs.get(*operator) else {
debug!("no awacs, they died?");
return;
};
let mut distance: f64 = f64::MAX;
let mut n = (None, None);
for (n_pos, n_heading, n_velocity) in npc.iter() {
if n_velocity.speed() < 10.0 {
// ignore slow/on-ground units
continue;
}
let d = p_pos.distance_to_nmi(n_pos);
if d < distance {
distance = d;
n = (Some(n_pos), Some(n_heading));
}
}
let (Some(n_position), Some(n_heading)) = n else {
let message = format!("{}, {}, {}", p_callsign, a_callsign, "Skies are clear");
let voice_message = format!(
"{} . . . {} . . . {}",
p_callsign.to_voice(),
a_callsign.to_voice(),
"Skies are clear"
);
commands.entity(*pilot).insert(TextMessage::new(message));
commands
.entity(*operator)
.insert(VoiceMessage::new(voice_message));
return;
};
let bra = Braa::new(p_pos, n_position, n_heading);
let message = format!("{}, {}, {}", p_callsign, a_callsign, bra);
let voice_message = format!(
"{} . . . {} . . . {}",
p_callsign.to_voice(),
a_callsign.to_voice(),
bra.to_voice()
);
commands.entity(*pilot).insert(TextMessage::new(message));
commands
.entity(*operator)
.insert(VoiceMessage::new(voice_message));
}
use bevy::ecs::{
query::{With, Without},
system::{Commands, Query, Resource},
};
use clap::Parser;
use guardian_commands::{ConsoleCommand, NamedCommand};
use guardian_core::{components::*, dcs::text::TextMessage, srs::voice::VoiceMessage};
use tracing::debug;
use crate::braa::Braa;
#[derive(Parser, Debug)]
pub struct BogeyDope;
impl NamedCommand for BogeyDope {
fn name() -> &'static str {
"[shopping|bogey dope]"
}
}
impl Resource for BogeyDope {}
pub fn bogey_dope(
mut commands: Commands,
mut cmd: ConsoleCommand<BogeyDope>,
awacs: Query<&Callsign>,
player: Query<(&Callsign, &Position)>,
npc: Query<(&Position, &Heading, &Velocity), (Without<Player>, With<Red>)>,
) {
let Some(Ok(BogeyDope)) = cmd.take() else {
return;
};
let Some(pilot) = &cmd.pilot else {
return; // no pilot
};
let Some(operator) = &cmd.operator else {
return;
};
let Ok((p_callsign, p_pos)) = player.get(*pilot) else {
debug!("no player, died?");
return;
};
let Ok(a_callsign) = awacs.get(*operator) else {
debug!("no awacs, they died?");
return;
};
let mut distance: f64 = f64::MAX;
let mut n = (None, None);
for (n_pos, n_heading, n_velocity) in npc.iter() {
if n_velocity.speed() < 10.0 {
// ignore slow/on-ground units
continue;
}
let d = p_pos.distance_to_nmi(n_pos);
if d < distance {
distance = d;
n = (Some(n_pos), Some(n_heading));
}
}
let (Some(n_position), Some(n_heading)) = n else {
let message = format!("{}, {}, {}", p_callsign, a_callsign, "Skies are clear");
let voice_message = format!(
"{} . . . {} . . . {}",
p_callsign.to_voice(),
a_callsign.to_voice(),
"Skies are clear"
);
commands.entity(*pilot).insert(TextMessage::new(message));
commands
.entity(*operator)
.insert(VoiceMessage::new(voice_message));
return;
};
let bra = Braa::new(p_pos, n_position, n_heading);
let message = format!("{}, {}, {}", p_callsign, a_callsign, bra);
let voice_message = format!(
"{} . . . {} . . . {}",
p_callsign.to_voice(),
a_callsign.to_voice(),
bra.to_voice()
);
commands.entity(*pilot).insert(TextMessage::new(message));
commands
.entity(*operator)
.insert(VoiceMessage::new(voice_message));
}

120
src/commands/cleartripwire.rs Normal file → Executable file
View File

@@ -1,60 +1,60 @@
use bevy::ecs::system::{Commands, Query, Resource};
use clap::Parser;
use guardian_commands::{ConsoleCommand, NamedCommand};
use guardian_core::{components::*, dcs::text::TextMessage, srs::voice::VoiceMessage};
use tracing::debug;
use crate::tripwire::Tripwire;
#[derive(Parser, Debug)]
pub struct ClearTripwireCommand;
impl NamedCommand for ClearTripwireCommand {
fn name() -> &'static str {
"clear [tripwire|warning]"
}
}
impl Resource for ClearTripwireCommand {}
pub fn clear_tripwire(
mut commands: Commands,
mut cmd: ConsoleCommand<ClearTripwireCommand>,
callsign: Query<&Callsign>,
) {
let Some(Ok(ClearTripwireCommand)) = cmd.take() else {
return;
};
let Some(pilot) = &cmd.pilot else {
debug!("no pilot");
return;
};
let Some(operator) = &cmd.operator else {
debug!("no operator");
return;
};
let Ok(p_callsign) = callsign.get(*pilot) else {
debug!("no player, died?");
return;
};
let Ok(a_callsign) = callsign.get(*operator) else {
debug!("no awacs, they died?");
return;
};
commands.entity(*pilot).remove::<Tripwire>();
let message = format!("{}, {}, tripwire cleared", p_callsign, a_callsign);
let message_voice = format!(
"{}, {}, tripwire cleared",
p_callsign.to_voice(),
a_callsign.to_voice(),
);
commands.entity(*pilot).insert(TextMessage::new(message));
commands
.entity(*operator)
.insert(VoiceMessage::new(message_voice));
}
use bevy::ecs::system::{Commands, Query, Resource};
use clap::Parser;
use guardian_commands::{ConsoleCommand, NamedCommand};
use guardian_core::{components::*, dcs::text::TextMessage, srs::voice::VoiceMessage};
use tracing::debug;
use crate::tripwire::Tripwire;
#[derive(Parser, Debug)]
pub struct ClearTripwireCommand;
impl NamedCommand for ClearTripwireCommand {
fn name() -> &'static str {
"clear [tripwire|warning]"
}
}
impl Resource for ClearTripwireCommand {}
pub fn clear_tripwire(
mut commands: Commands,
mut cmd: ConsoleCommand<ClearTripwireCommand>,
callsign: Query<&Callsign>,
) {
let Some(Ok(ClearTripwireCommand)) = cmd.take() else {
return;
};
let Some(pilot) = &cmd.pilot else {
debug!("no pilot");
return;
};
let Some(operator) = &cmd.operator else {
debug!("no operator");
return;
};
let Ok(p_callsign) = callsign.get(*pilot) else {
debug!("no player, died?");
return;
};
let Ok(a_callsign) = callsign.get(*operator) else {
debug!("no awacs, they died?");
return;
};
commands.entity(*pilot).remove::<Tripwire>();
let message = format!("{}, {}, tripwire cleared", p_callsign, a_callsign);
let message_voice = format!(
"{}, {}, tripwire cleared",
p_callsign.to_voice(),
a_callsign.to_voice(),
);
commands.entity(*pilot).insert(TextMessage::new(message));
commands
.entity(*operator)
.insert(VoiceMessage::new(message_voice));
}

46
src/commands/mod.rs Normal file → Executable file
View File

@@ -1,23 +1,23 @@
use bevy::app::Plugin;
use guardian_commands::AddConsoleCommand;
mod bogeydope;
mod cleartripwire;
mod radiocheck;
mod tripwire;
use bogeydope::*;
use cleartripwire::*;
use radiocheck::*;
use tripwire::*;
pub struct CommandsPlugin;
impl Plugin for CommandsPlugin {
fn build(&self, app: &mut bevy::prelude::App) {
app.add_plugins(guardian_commands::CommandsPlugin);
app.add_console_command::<BogeyDope, _>(bogey_dope);
app.add_console_command::<RadioCheck, _>(radio_check);
app.add_console_command::<TripwireCommand, _>(set_tripwire);
app.add_console_command::<ClearTripwireCommand, _>(clear_tripwire);
}
}
use bevy::app::Plugin;
use guardian_commands::AddConsoleCommand;
mod bogeydope;
mod cleartripwire;
mod radiocheck;
mod tripwire;
use bogeydope::*;
use cleartripwire::*;
use radiocheck::*;
use tripwire::*;
pub struct CommandsPlugin;
impl Plugin for CommandsPlugin {
fn build(&self, app: &mut bevy::prelude::App) {
app.add_plugins(guardian_commands::CommandsPlugin);
app.add_console_command::<BogeyDope, _>(bogey_dope);
app.add_console_command::<RadioCheck, _>(radio_check);
app.add_console_command::<TripwireCommand, _>(set_tripwire);
app.add_console_command::<ClearTripwireCommand, _>(clear_tripwire);
}
}

114
src/commands/radiocheck.rs Normal file → Executable file
View File

@@ -1,57 +1,57 @@
use bevy::ecs::system::{Commands, Query, Resource};
use clap::Parser;
use guardian_commands::{ConsoleCommand, NamedCommand};
use guardian_core::{components::*, dcs::text::TextMessage, srs::voice::VoiceMessage};
use tracing::debug;
#[derive(Parser, Debug)]
pub struct RadioCheck;
impl NamedCommand for RadioCheck {
fn name() -> &'static str {
"radio check"
}
}
impl Resource for RadioCheck {}
pub fn radio_check(
mut commands: Commands,
mut cmd: ConsoleCommand<RadioCheck>,
callsign: Query<&Callsign>,
) {
let Some(Ok(RadioCheck)) = cmd.take() else {
return;
};
let Some(pilot) = &cmd.pilot else {
debug!("no pilot");
return;
};
let Some(operator) = &cmd.operator else {
debug!("no operator");
return;
};
let Ok(p_callsign) = callsign.get(*pilot) else {
debug!("no player, died?");
return;
};
let Ok(a_callsign) = callsign.get(*operator) else {
debug!("no awacs, they died?");
return;
};
let message = format!("{}, {}, {}", p_callsign, a_callsign, "five by five");
let voice_message = format!(
"{} . . . {} . . . {}",
p_callsign.to_voice(),
a_callsign.to_voice(),
"five by five"
);
commands.entity(*pilot).insert(TextMessage::new(message));
commands
.entity(*operator)
.insert(VoiceMessage::new(voice_message));
}
use bevy::ecs::system::{Commands, Query, Resource};
use clap::Parser;
use guardian_commands::{ConsoleCommand, NamedCommand};
use guardian_core::{components::*, dcs::text::TextMessage, srs::voice::VoiceMessage};
use tracing::debug;
#[derive(Parser, Debug)]
pub struct RadioCheck;
impl NamedCommand for RadioCheck {
fn name() -> &'static str {
"radio check"
}
}
impl Resource for RadioCheck {}
pub fn radio_check(
mut commands: Commands,
mut cmd: ConsoleCommand<RadioCheck>,
callsign: Query<&Callsign>,
) {
let Some(Ok(RadioCheck)) = cmd.take() else {
return;
};
let Some(pilot) = &cmd.pilot else {
debug!("no pilot");
return;
};
let Some(operator) = &cmd.operator else {
debug!("no operator");
return;
};
let Ok(p_callsign) = callsign.get(*pilot) else {
debug!("no player, died?");
return;
};
let Ok(a_callsign) = callsign.get(*operator) else {
debug!("no awacs, they died?");
return;
};
let message = format!("{}, {}, {}", p_callsign, a_callsign, "five by five");
let voice_message = format!(
"{} . . . {} . . . {}",
p_callsign.to_voice(),
a_callsign.to_voice(),
"five by five"
);
commands.entity(*pilot).insert(TextMessage::new(message));
commands
.entity(*operator)
.insert(VoiceMessage::new(voice_message));
}

206
src/commands/tripwire.rs Normal file → Executable file
View File

@@ -1,103 +1,103 @@
use bevy::ecs::system::{Commands, Query, Resource};
use clap::Parser;
use guardian_commands::{ConsoleCommand, NamedCommand};
use guardian_core::{components::*, dcs::text::TextMessage, srs::voice::VoiceMessage};
use tracing::debug;
use crate::tripwire::Tripwire;
#[derive(Parser, Debug)]
pub struct TripwireCommand;
impl NamedCommand for TripwireCommand {
fn name() -> &'static str {
"set [tripwire|warning]"
}
}
impl Resource for TripwireCommand {}
pub fn set_tripwire(
mut commands: Commands,
mut cmd: ConsoleCommand<TripwireCommand>,
callsign: Query<&Callsign>,
) {
let Some(Ok(TripwireCommand)) = cmd.take() else {
return;
};
let Some(pilot) = &cmd.pilot else {
debug!("no pilot");
return;
};
let Some(operator) = &cmd.operator else {
debug!("no operator");
return;
};
let Ok(p_callsign) = callsign.get(*pilot) else {
debug!("no player, died?");
return;
};
let Ok(a_callsign) = callsign.get(*operator) else {
debug!("no awacs, they died?");
return;
};
let Some(readback) = &cmd.raw else {
debug!("no readback");
return;
};
let mut parts = readback.split(' ');
// check the last part, if it's a number, interpret as miles
let Some(last) = parts.nth_back(0) else {
debug!("no last part");
return;
};
let distance = match last.parse::<i32>() {
Ok(distance) => distance, // a number is found, interpret as miles
Err(_) => {
let Some(maybe_number) = parts.nth_back(0) else {
// Report message garbled..
return;
};
match maybe_number.parse::<i32>() {
Ok(n) => {
if last == "kilometers" || last == "km" {
(n as f64 * 0.621371).round() as i32
} else {
n
}
}
Err(_) => {
// let message = format!("{}, {}. Unable to read, Say again.", caller, receiver);
return;
}
}
}
};
// clamp distance between 0 and 120
let distance = distance.min(120).max(0);
commands.entity(*pilot).insert(Tripwire::new(distance));
let message = format!(
"{}, {}, tripwire set to {} miles",
p_callsign, a_callsign, distance
);
let message_voice = format!(
"{}, {}, tripwire set to {} miles",
p_callsign.to_voice(),
a_callsign.to_voice(),
distance
);
commands.entity(*pilot).insert(TextMessage::new(message));
commands
.entity(*operator)
.insert(VoiceMessage::new(message_voice));
}
use bevy::ecs::system::{Commands, Query, Resource};
use clap::Parser;
use guardian_commands::{ConsoleCommand, NamedCommand};
use guardian_core::{components::*, dcs::text::TextMessage, srs::voice::VoiceMessage};
use tracing::debug;
use crate::tripwire::Tripwire;
#[derive(Parser, Debug)]
pub struct TripwireCommand;
impl NamedCommand for TripwireCommand {
fn name() -> &'static str {
"set [tripwire|warning]"
}
}
impl Resource for TripwireCommand {}
pub fn set_tripwire(
mut commands: Commands,
mut cmd: ConsoleCommand<TripwireCommand>,
callsign: Query<&Callsign>,
) {
let Some(Ok(TripwireCommand)) = cmd.take() else {
return;
};
let Some(pilot) = &cmd.pilot else {
debug!("no pilot");
return;
};
let Some(operator) = &cmd.operator else {
debug!("no operator");
return;
};
let Ok(p_callsign) = callsign.get(*pilot) else {
debug!("no player, died?");
return;
};
let Ok(a_callsign) = callsign.get(*operator) else {
debug!("no awacs, they died?");
return;
};
let Some(readback) = &cmd.raw else {
debug!("no readback");
return;
};
let mut parts = readback.split(' ');
// check the last part, if it's a number, interpret as miles
let Some(last) = parts.nth_back(0) else {
debug!("no last part");
return;
};
let distance = match last.parse::<i32>() {
Ok(distance) => distance, // a number is found, interpret as miles
Err(_) => {
let Some(maybe_number) = parts.nth_back(0) else {
// Report message garbled..
return;
};
match maybe_number.parse::<i32>() {
Ok(n) => {
if last == "kilometers" || last == "km" {
(n as f64 * 0.621371).round() as i32
} else {
n
}
}
Err(_) => {
// let message = format!("{}, {}. Unable to read, Say again.", caller, receiver);
return;
}
}
}
};
// clamp distance between 0 and 120
let distance = distance.min(120).max(0);
commands.entity(*pilot).insert(Tripwire::new(distance));
let message = format!(
"{}, {}, tripwire set to {} miles",
p_callsign, a_callsign, distance
);
let message_voice = format!(
"{}, {}, tripwire set to {} miles",
p_callsign.to_voice(),
a_callsign.to_voice(),
distance
);
commands.entity(*pilot).insert(TextMessage::new(message));
commands
.entity(*operator)
.insert(VoiceMessage::new(message_voice));
}

0
src/config.rs Normal file → Executable file
View File

0
src/main.rs Normal file → Executable file
View File

182
src/tripwire.rs Normal file → Executable file
View File

@@ -1,91 +1,91 @@
use bevy::{
app::{Plugin, Update},
ecs::{
component::Component,
entity::Entity,
query::{With, Without},
system::{Commands, Query},
},
};
use guardian_core::{
components::*,
dcs::text::TextMessage,
srs::{voice::VoiceMessage, Radio, RadioInfo},
};
use crate::braa::Braa;
pub struct TripwirePlugin;
impl Plugin for TripwirePlugin {
fn build(&self, app: &mut bevy::prelude::App) {
app.add_systems(Update, tripwire);
}
}
fn tripwire(
mut commands: Commands,
awacs: Query<(Entity, &Callsign, &Radio), (With<Awacs>, With<Blue>, With<Radio>)>,
mut players: Query<(Entity, &Callsign, &Position, &RadioInfo, &mut Tripwire), With<Player>>,
npc: Query<(&Id, &Position, &Heading, &Velocity), (Without<Player>, With<Red>)>,
) {
// get the players radio frequencies
// match the tuned frequency with one of the awacs'
// broadcast message on that ferquency and as that awacs
for (ent, awacs, radio) in awacs.iter() {
for (p_ent, p_callsign, p_position, radioinfo, mut p_tripwire) in players.iter_mut() {
// check if user is on the same frequency as awacs
if !radioinfo
.radios
.iter()
.any(|f| f.freq as u64 == radio.frequency && f.modulation == radio.modulation)
{
continue;
}
for (n_id, n_position, n_heading, n_velocity) in npc.iter() {
if n_velocity.speed() < 10.0 {
// ignore slow/on-ground units
continue;
}
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);
let message = format!("{}, {}, {}", p_callsign, awacs, bra);
let voice_message = format!(
"{} . . . {} . . . {}",
p_callsign.to_voice(),
awacs.to_voice(),
bra.to_voice()
);
// commands.spawn((Id::new(player_id.into()), TextMessage::new(message)));
commands.entity(p_ent).insert(TextMessage::new(message));
commands
.entity(ent)
.insert(VoiceMessage::new(voice_message));
} else if distance > p_tripwire.range && p_tripwire.reported.contains(n_id) {
p_tripwire.reported.retain(|b| b != n_id);
}
}
}
}
}
#[derive(Debug, Component)]
pub struct Tripwire {
range: f64,
reported: Vec<Id>,
}
impl Tripwire {
pub fn new(distance: i32) -> Self {
Self {
range: distance as f64,
reported: vec![],
}
}
}
use bevy::{
app::{Plugin, Update},
ecs::{
component::Component,
entity::Entity,
query::{With, Without},
system::{Commands, Query},
},
};
use guardian_core::{
components::*,
dcs::text::TextMessage,
srs::{voice::VoiceMessage, Radio, RadioInfo},
};
use crate::braa::Braa;
pub struct TripwirePlugin;
impl Plugin for TripwirePlugin {
fn build(&self, app: &mut bevy::prelude::App) {
app.add_systems(Update, tripwire);
}
}
fn tripwire(
mut commands: Commands,
awacs: Query<(Entity, &Callsign, &Radio), (With<Awacs>, With<Blue>, With<Radio>)>,
mut players: Query<(Entity, &Callsign, &Position, &RadioInfo, &mut Tripwire), With<Player>>,
npc: Query<(&Id, &Position, &Heading, &Velocity), (Without<Player>, With<Red>)>,
) {
// get the players radio frequencies
// match the tuned frequency with one of the awacs'
// broadcast message on that ferquency and as that awacs
for (ent, awacs, radio) in awacs.iter() {
for (p_ent, p_callsign, p_position, radioinfo, mut p_tripwire) in players.iter_mut() {
// check if user is on the same frequency as awacs
if !radioinfo
.radios
.iter()
.any(|f| f.freq as u64 == radio.frequency && f.modulation == radio.modulation)
{
continue;
}
for (n_id, n_position, n_heading, n_velocity) in npc.iter() {
if n_velocity.speed() < 10.0 {
// ignore slow/on-ground units
continue;
}
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);
let message = format!("{}, {}, {}", p_callsign, awacs, bra);
let voice_message = format!(
"{} . . . {} . . . {}",
p_callsign.to_voice(),
awacs.to_voice(),
bra.to_voice()
);
// commands.spawn((Id::new(player_id.into()), TextMessage::new(message)));
commands.entity(p_ent).insert(TextMessage::new(message));
commands
.entity(ent)
.insert(VoiceMessage::new(voice_message));
} else if distance > p_tripwire.range && p_tripwire.reported.contains(n_id) {
p_tripwire.reported.retain(|b| b != n_id);
}
}
}
}
}
#[derive(Debug, Component)]
pub struct Tripwire {
range: f64,
reported: Vec<Id>,
}
impl Tripwire {
pub fn new(distance: i32) -> Self {
Self {
range: distance as f64,
reported: vec![],
}
}
}