no clue what the current status is
This commit is contained in:
0
crates/dcs-grpc/Cargo.toml
Normal file → Executable file
0
crates/dcs-grpc/Cargo.toml
Normal file → Executable file
1320
crates/dcs-grpc/LICENSE
Normal file → Executable 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
0
crates/dcs-grpc/build.rs
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/atmosphere/v0/atmosphere.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/atmosphere/v0/atmosphere.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/coalition/v0/coalition.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/coalition/v0/coalition.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/common/v0/common.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/common/v0/common.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/controller/v0/controller.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/controller/v0/controller.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/custom/v0/custom.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/custom/v0/custom.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/dcs.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/dcs.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/group/v0/group.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/group/v0/group.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/hook/v0/hook.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/hook/v0/hook.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/mission/v0/mission.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/mission/v0/mission.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/net/v0/net.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/net/v0/net.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/srs/v0/srs.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/srs/v0/srs.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/timer/v0/timer.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/timer/v0/timer.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/trigger/v0/trigger.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/trigger/v0/trigger.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/unit/v0/unit.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/unit/v0/unit.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/world/v0/world.proto
Normal file → Executable file
0
crates/dcs-grpc/protos/dcs/world/v0/world.proto
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/atmosphere.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/atmosphere.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/coalition.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/coalition.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/common.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/common.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/controller.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/controller.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/custom.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/custom.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/group.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/group.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/hook.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/hook.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/mission.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/mission.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/mod.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/mod.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/net.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/net.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/srs.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/srs.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/timer.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/timer.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/trigger.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/trigger.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/unit.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/unit.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/utils.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/utils.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/world.rs
Normal file → Executable file
0
crates/dcs-grpc/src/dcs/world.rs
Normal file → Executable file
0
crates/dcs-grpc/src/lib.rs
Normal file → Executable file
0
crates/dcs-grpc/src/lib.rs
Normal file → Executable file
0
crates/guardian_commands/Cargo.toml
Normal file → Executable file
0
crates/guardian_commands/Cargo.toml
Normal file → Executable file
902
crates/guardian_commands/src/call.rs
Normal file → Executable file
902
crates/guardian_commands/src/call.rs
Normal file → Executable 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
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
0
crates/guardian_commands/src/lib.rs
Normal file → Executable file
388
crates/guardian_commands/src/parser.rs
Normal file → Executable file
388
crates/guardian_commands/src/parser.rs
Normal file → Executable 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
0
crates/guardian_core/Cargo.toml
Normal file → Executable file
8
crates/guardian_core/src/components/awacs.rs
Normal file → Executable file
8
crates/guardian_core/src/components/awacs.rs
Normal file → Executable 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
210
crates/guardian_core/src/components/callsign.rs
Normal file → Executable 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
14
crates/guardian_core/src/components/group.rs
Normal file → Executable 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
188
crates/guardian_core/src/components/grpc_base_url.rs
Normal file → Executable 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
8
crates/guardian_core/src/components/heading.rs
Normal file → Executable 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
44
crates/guardian_core/src/components/id.rs
Normal file → Executable 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
54
crates/guardian_core/src/components/mod.rs
Normal file → Executable 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
158
crates/guardian_core/src/components/player.rs
Normal file → Executable 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
98
crates/guardian_core/src/components/position.rs
Normal file → Executable 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
20
crates/guardian_core/src/components/side.rs
Normal file → Executable 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
68
crates/guardian_core/src/components/srs_socket_addr.rs
Normal file → Executable 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
164
crates/guardian_core/src/components/stt_base_url.rs
Normal file → Executable 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
158
crates/guardian_core/src/components/unit_type.rs
Normal file → Executable 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
28
crates/guardian_core/src/components/velocity.rs
Normal file → Executable 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
58
crates/guardian_core/src/dcs.rs
Normal file → Executable 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
514
crates/guardian_core/src/dcs/mission.rs
Normal file → Executable 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
282
crates/guardian_core/src/dcs/text.rs
Normal file → Executable 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
58
crates/guardian_core/src/lib.rs
Normal file → Executable 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
3
crates/guardian_core/src/srs.rs
Normal file → Executable 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
64
crates/guardian_core/src/srs/frame_queue.rs
Normal file → Executable 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
0
crates/guardian_core/src/srs/message.rs
Normal file → Executable file
0
crates/guardian_core/src/srs/messages_codec.rs
Normal file → Executable file
0
crates/guardian_core/src/srs/messages_codec.rs
Normal file → Executable file
160
crates/guardian_core/src/srs/voice.rs
Normal file → Executable file
160
crates/guardian_core/src/srs/voice.rs
Normal file → Executable 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
0
crates/guardian_core/src/srs/voice_codec.rs
Normal file → Executable file
0
crates/guardian_core/src/srs/voice_command.rs
Normal file → Executable file
0
crates/guardian_core/src/srs/voice_command.rs
Normal file → Executable file
418
crates/guardian_core/src/tts.rs
Normal file → Executable file
418
crates/guardian_core/src/tts.rs
Normal file → Executable 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())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user