omg so much has changed idk
This commit is contained in:
1026
Cargo.lock
generated
1026
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -29,7 +29,7 @@ opt-level = 1
|
|||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
bevy = { version = "0.12", features = [
|
bevy = { version = "0.12", default-features = false, features = [
|
||||||
# "dynamic_linking",
|
# "dynamic_linking",
|
||||||
"multi-threaded",
|
"multi-threaded",
|
||||||
"trace",
|
"trace",
|
||||||
@@ -46,7 +46,7 @@ tracing = "0.1.40"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bevy.workspace = true
|
bevy.workspace = true
|
||||||
clap.workspace = true
|
clap = { workspace = true, features = ["derive"] }
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
guardian_commands.workspace = true
|
guardian_commands.workspace = true
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
[srs]
|
[srs]
|
||||||
server = "127.0.0.1:5002"
|
server = "192.168.0.100:5003"
|
||||||
|
|
||||||
[grpc]
|
[grpc]
|
||||||
url = "http://127.0.0.1:50051/"
|
url = "http://192.168.0.100:50053/"
|
||||||
poll_rate = 10
|
poll_rate = 10
|
||||||
|
|
||||||
[stt]
|
[stt]
|
||||||
url = "http://192.168.0.2:3000/"
|
url = "http://192.168.0.2:3000/"
|
||||||
|
|
||||||
[channels.Overlord1-1]
|
[channels.Magic1-1]
|
||||||
frequency = 251250000
|
frequency = 266250000
|
||||||
modulation = "Am"
|
modulation = "Am"
|
||||||
voice = "David"
|
|
||||||
|
|||||||
@@ -83,6 +83,35 @@ pub enum Squadron {
|
|||||||
Wild,
|
Wild,
|
||||||
Ninja,
|
Ninja,
|
||||||
Jedi,
|
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 {
|
impl ToString for Squadron {
|
||||||
@@ -95,6 +124,7 @@ impl ToString for Squadron {
|
|||||||
Squadron::Dodge => "Dodge".to_string(),
|
Squadron::Dodge => "Dodge".to_string(),
|
||||||
Squadron::Ford => "Ford".to_string(),
|
Squadron::Ford => "Ford".to_string(),
|
||||||
Squadron::Chevy => "Chevy".to_string(),
|
Squadron::Chevy => "Chevy".to_string(),
|
||||||
|
Squadron::Hornet => "Hornet".to_string(),
|
||||||
Squadron::Pontiac => "Pontiac".to_string(),
|
Squadron::Pontiac => "Pontiac".to_string(),
|
||||||
Squadron::Viper => "Viper".to_string(),
|
Squadron::Viper => "Viper".to_string(),
|
||||||
Squadron::Venom => "Venom".to_string(),
|
Squadron::Venom => "Venom".to_string(),
|
||||||
@@ -108,6 +138,34 @@ impl ToString for Squadron {
|
|||||||
Squadron::Wild => "Wild".to_string(),
|
Squadron::Wild => "Wild".to_string(),
|
||||||
Squadron::Ninja => "Ninja".to_string(),
|
Squadron::Ninja => "Ninja".to_string(),
|
||||||
Squadron::Jedi => "Jedi".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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,6 +182,7 @@ impl std::str::FromStr for Squadron {
|
|||||||
"Dodge" => Ok(Squadron::Dodge),
|
"Dodge" => Ok(Squadron::Dodge),
|
||||||
"Ford" => Ok(Squadron::Ford),
|
"Ford" => Ok(Squadron::Ford),
|
||||||
"Chevy" => Ok(Squadron::Chevy),
|
"Chevy" => Ok(Squadron::Chevy),
|
||||||
|
"Hornet" => Ok(Squadron::Hornet),
|
||||||
"Pontiac" => Ok(Squadron::Pontiac),
|
"Pontiac" => Ok(Squadron::Pontiac),
|
||||||
"Viper" => Ok(Squadron::Viper),
|
"Viper" => Ok(Squadron::Viper),
|
||||||
"Venom" => Ok(Squadron::Venom),
|
"Venom" => Ok(Squadron::Venom),
|
||||||
@@ -137,7 +196,34 @@ impl std::str::FromStr for Squadron {
|
|||||||
"Wild" => Ok(Squadron::Wild),
|
"Wild" => Ok(Squadron::Wild),
|
||||||
"Ninja" => Ok(Squadron::Ninja),
|
"Ninja" => Ok(Squadron::Ninja),
|
||||||
"Jedi" => Ok(Squadron::Jedi),
|
"Jedi" => Ok(Squadron::Jedi),
|
||||||
_ => Err("Invalid squadron".into()),
|
"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())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -225,6 +311,7 @@ fn parse_element(input: &str) -> IResult<&str, Element> {
|
|||||||
"dodge" => Squadron::Dodge,
|
"dodge" => Squadron::Dodge,
|
||||||
"ford" => Squadron::Ford,
|
"ford" => Squadron::Ford,
|
||||||
"chevy" => Squadron::Chevy,
|
"chevy" => Squadron::Chevy,
|
||||||
|
"hornet" => Squadron::Hornet,
|
||||||
"pontiac" => Squadron::Pontiac,
|
"pontiac" => Squadron::Pontiac,
|
||||||
"viper" => Squadron::Viper,
|
"viper" => Squadron::Viper,
|
||||||
"venom" => Squadron::Venom,
|
"venom" => Squadron::Venom,
|
||||||
@@ -238,13 +325,41 @@ fn parse_element(input: &str) -> IResult<&str, Element> {
|
|||||||
"wild" => Squadron::Wild,
|
"wild" => Squadron::Wild,
|
||||||
"ninja" => Squadron::Ninja,
|
"ninja" => Squadron::Ninja,
|
||||||
"jedi" => Squadron::Jedi,
|
"jedi" => Squadron::Jedi,
|
||||||
_ => {
|
"hawg" => Squadron::Hawg,
|
||||||
return Err(NomError::Error(nom::error::Error {
|
"boar" => Squadron::Boar,
|
||||||
input: squadron_str,
|
"pig" => Squadron::Pig,
|
||||||
code: ErrorKind::Alpha,
|
"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 group = group_str.unwrap_or((None, 0, None)).1;
|
||||||
let unit = unit_str.unwrap_or((None, 0, None)).1;
|
let unit = unit_str.unwrap_or((None, 0, None)).1;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use bevy::{
|
|||||||
system::{ResMut, Resource, SystemMeta, SystemParam},
|
system::{ResMut, Resource, SystemMeta, SystemParam},
|
||||||
world::{unsafe_world_cell::UnsafeWorldCell, World},
|
world::{unsafe_world_cell::UnsafeWorldCell, World},
|
||||||
},
|
},
|
||||||
log::{debug, warn},
|
log::warn,
|
||||||
};
|
};
|
||||||
// use bevy::{input::keyboard::KeyboardInput, prelude::*};
|
// use bevy::{input::keyboard::KeyboardInput, prelude::*};
|
||||||
// use bevy_egui::egui::{self, Align, ScrollArea, TextEdit};
|
// use bevy_egui::egui::{self, Align, ScrollArea, TextEdit};
|
||||||
@@ -118,8 +118,6 @@ unsafe impl<T: Command> SystemParam for ConsoleCommand<T> {
|
|||||||
let clap_command = T::command().no_binary_name(true);
|
let clap_command = T::command().no_binary_name(true);
|
||||||
let arg_matches = clap_command.try_get_matches_from(command.args.iter());
|
let arg_matches = clap_command.try_get_matches_from(command.args.iter());
|
||||||
|
|
||||||
debug!("Trying to parse as `{}`.", command.command_name);
|
|
||||||
|
|
||||||
match arg_matches {
|
match arg_matches {
|
||||||
Ok(matches) => match T::from_arg_matches(&matches) {
|
Ok(matches) => match T::from_arg_matches(&matches) {
|
||||||
Ok(from_arg_matches) => {
|
Ok(from_arg_matches) => {
|
||||||
|
|||||||
@@ -35,11 +35,7 @@ thiserror = "1.0"
|
|||||||
symspell = "0.4.3"
|
symspell = "0.4.3"
|
||||||
simsearch = "0.2"
|
simsearch = "0.2"
|
||||||
|
|
||||||
[target.'cfg(target_os = "windows")'.dependencies.windows]
|
hound = "3.5.1"
|
||||||
version = "0.52"
|
dasp = { version = "0.11.0", features = ["all"] }
|
||||||
features = [
|
urlencoding = "2.1.3"
|
||||||
"Foundation",
|
samplerate = "0.2.4"
|
||||||
"Foundation_Collections",
|
|
||||||
"Storage_Streams",
|
|
||||||
"Media_SpeechSynthesis",
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ use bevy::{
|
|||||||
};
|
};
|
||||||
use crossbeam_channel::{unbounded, Receiver};
|
use crossbeam_channel::{unbounded, Receiver};
|
||||||
use dcs_grpc::dcs::{
|
use dcs_grpc::dcs::{
|
||||||
common::v0::{Coalition, GroupCategory::Airplane, Unit},
|
common::v0::{Coalition, GroupCategory, Unit},
|
||||||
mission::v0::{
|
mission::v0::{
|
||||||
mission_service_client::MissionServiceClient, stream_units_response::Update,
|
mission_service_client::MissionServiceClient, stream_units_response::Update,
|
||||||
StreamUnitsRequest,
|
StreamUnitsRequest,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
enum Response {
|
enum Response {
|
||||||
@@ -65,7 +65,7 @@ fn connect_to_grpc(mut commands: Commands, tokio: Res<TokioResource>, url: Res<G
|
|||||||
.stream_units(StreamUnitsRequest {
|
.stream_units(StreamUnitsRequest {
|
||||||
poll_rate,
|
poll_rate,
|
||||||
max_backoff: Some(30),
|
max_backoff: Some(30),
|
||||||
category: Airplane as i32,
|
category: GroupCategory::Airplane as i32,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
else {
|
else {
|
||||||
@@ -83,7 +83,7 @@ fn connect_to_grpc(mut commands: Commands, tokio: Res<TokioResource>, url: Res<G
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
debug!("Error from gRPC: {:?}", e); // verbose or debug log
|
error!("Error from gRPC: {:?}", e); // verbose or debug log
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,6 +91,7 @@ fn connect_to_grpc(mut commands: Commands, tokio: Res<TokioResource>, url: Res<G
|
|||||||
|
|
||||||
warn!("Disconnected from DCS: {}", url); // warn
|
warn!("Disconnected from DCS: {}", url); // warn
|
||||||
tx.send(Response::Disconnected).ok();
|
tx.send(Response::Disconnected).ok();
|
||||||
|
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -180,7 +181,22 @@ fn update_units(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(playername) = &event.player_name {
|
if let Some(playername) = &event.player_name {
|
||||||
|
let playername = playername.clone();
|
||||||
e.insert(Player::new(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 {
|
if let Some(group) = &event.group {
|
||||||
|
|||||||
@@ -15,13 +15,16 @@ use bevy::{
|
|||||||
reflect::{std_traits::ReflectDefault, Reflect},
|
reflect::{std_traits::ReflectDefault, Reflect},
|
||||||
utils::AHasher,
|
utils::AHasher,
|
||||||
};
|
};
|
||||||
use dcs_grpc::dcs::{
|
use dcs_grpc::dcs::trigger::v0::{
|
||||||
common::v0::Coalition,
|
trigger_service_client::TriggerServiceClient, OutTextForUnitRequest,
|
||||||
net::v0::{net_service_client::NetServiceClient, SendChatRequest},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
use crate::{components::GrpcBaseUrl, TokioResource};
|
use crate::{
|
||||||
|
components::{GrpcBaseUrl, Id},
|
||||||
|
TokioResource,
|
||||||
|
};
|
||||||
|
|
||||||
pub struct TextPlugin;
|
pub struct TextPlugin;
|
||||||
|
|
||||||
@@ -33,36 +36,38 @@ impl Plugin for TextPlugin {
|
|||||||
|
|
||||||
fn send_text_message(
|
fn send_text_message(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
query: Query<(Entity, &TextMessage), Added<TextMessage>>,
|
query: Query<(Entity, &Id, &TextMessage), Added<TextMessage>>,
|
||||||
url: Res<GrpcBaseUrl>,
|
url: Res<GrpcBaseUrl>,
|
||||||
tokio: Res<TokioResource>,
|
tokio: Res<TokioResource>,
|
||||||
) {
|
) {
|
||||||
let url = url.to_string();
|
for (ent, id, msg) in query.iter() {
|
||||||
|
let url = url.to_string();
|
||||||
|
|
||||||
for (ent, msg) in query.iter() {
|
|
||||||
if msg.as_str().is_empty() {
|
if msg.as_str().is_empty() {
|
||||||
commands.entity(ent).remove::<TextMessage>().despawn();
|
commands.entity(ent).remove::<TextMessage>();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let message = msg.clone();
|
let message = msg.clone();
|
||||||
let url = url.clone();
|
let url = url.clone();
|
||||||
|
let id: u32 = id.into();
|
||||||
tokio.0.spawn(async move {
|
tokio.0.spawn(async move {
|
||||||
let Ok(mut client) = NetServiceClient::connect(url.to_string()).await else {
|
let Ok(mut client) = TriggerServiceClient::connect(url.to_string()).await else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let request = SendChatRequest {
|
let request = OutTextForUnitRequest {
|
||||||
message: message.to_string(),
|
text: message.to_string(),
|
||||||
coalition: Coalition::All as i32,
|
display_time: 15,
|
||||||
// target_player_id: player_id,
|
clear_view: false,
|
||||||
|
unit_id: id,
|
||||||
};
|
};
|
||||||
|
|
||||||
info!("> {}", message);
|
info!("> {}", message);
|
||||||
client.send_chat(request).await.ok();
|
client.out_text_for_unit(request).await.ok();
|
||||||
});
|
});
|
||||||
|
|
||||||
commands.entity(ent).remove::<TextMessage>().despawn();
|
commands.entity(ent).remove::<TextMessage>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,8 @@ mod voice_codec;
|
|||||||
mod voice_command;
|
mod voice_command;
|
||||||
|
|
||||||
use guardian_commands::{call::parse_call, ConsoleCommandEntered, ConsoleConfiguration};
|
use guardian_commands::{call::parse_call, ConsoleCommandEntered, ConsoleConfiguration};
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use simsearch::SimSearch;
|
use simsearch::SimSearch;
|
||||||
use tracing::{error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
pub use voice_command::VoiceCommand;
|
pub use voice_command::VoiceCommand;
|
||||||
|
|
||||||
use crossbeam_channel::Receiver;
|
use crossbeam_channel::Receiver;
|
||||||
@@ -161,8 +160,8 @@ fn listen_srs(
|
|||||||
commands.entity(ent).insert(ClientHandler(client_handle));
|
commands.entity(ent).insert(ClientHandler(client_handle));
|
||||||
|
|
||||||
let frequency = radio.frequency;
|
let frequency = radio.frequency;
|
||||||
|
let modulation = radio.modulation;
|
||||||
let id = *id;
|
let id = *id;
|
||||||
let voice = radio.voice.clone();
|
|
||||||
let stt_url = stt_url.as_str().to_string();
|
let stt_url = stt_url.as_str().to_string();
|
||||||
radio.handle = Some(tokio.0.spawn(async move {
|
radio.handle = Some(tokio.0.spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
@@ -212,11 +211,7 @@ fn listen_srs(
|
|||||||
wav_audio_part: None,
|
wav_audio_part: None,
|
||||||
frequencies: vec![Frequency {
|
frequencies: vec![Frequency {
|
||||||
freq: frequency as f64,
|
freq: frequency as f64,
|
||||||
modulation: if frequency <= 87_995_000 {
|
modulation: modulation.into(),
|
||||||
voice_codec::Modulation::Fm
|
|
||||||
} else {
|
|
||||||
voice_codec::Modulation::Am
|
|
||||||
},
|
|
||||||
encryption: Encryption::None,
|
encryption: Encryption::None,
|
||||||
}],
|
}],
|
||||||
unit_id: id.into(),
|
unit_id: id.into(),
|
||||||
@@ -266,7 +261,7 @@ fn listen_srs(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(data) = voice_handle.recv() => {
|
Some(data) = voice_handle.recv() => {
|
||||||
frames.push(synthesize(data.as_str(), &voice).await?).await;
|
frames.push(synthesize("192.168.0.238:10200", data.as_str()).await?).await;
|
||||||
}
|
}
|
||||||
Some(data) = voice_stream.next() => {
|
Some(data) = voice_stream.next() => {
|
||||||
match data {
|
match data {
|
||||||
@@ -501,9 +496,12 @@ fn handle_voice_command(
|
|||||||
info!("Received: {}", raw);
|
info!("Received: {}", raw);
|
||||||
info!("Interpreted as: {}", cmd);
|
info!("Interpreted as: {}", cmd);
|
||||||
|
|
||||||
let Ok((_, call)) = parse_call(cmd) else {
|
let call = match parse_call(cmd) {
|
||||||
// no command, casual conversations?
|
Ok((_, call)) => call,
|
||||||
return;
|
Err(e) => {
|
||||||
|
debug!("Error parsing call: {:?}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let cmd = call.command;
|
let cmd = call.command;
|
||||||
@@ -520,21 +518,6 @@ fn handle_voice_command(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Voice(String);
|
|
||||||
|
|
||||||
impl Voice {
|
|
||||||
pub fn new(voice: impl Into<String>) -> Self {
|
|
||||||
Self(voice.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Voice {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "{}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct ClientHandler(Receiver<Client>);
|
pub struct ClientHandler(Receiver<Client>);
|
||||||
|
|
||||||
@@ -542,7 +525,6 @@ pub struct ClientHandler(Receiver<Client>);
|
|||||||
pub struct Radio {
|
pub struct Radio {
|
||||||
pub frequency: u64, // the way srs wants it
|
pub frequency: u64, // the way srs wants it
|
||||||
pub modulation: Modulation,
|
pub modulation: Modulation,
|
||||||
pub voice: Voice,
|
|
||||||
sguid: String,
|
sguid: String,
|
||||||
voice_sink: Option<Sender<VoiceMessage>>,
|
voice_sink: Option<Sender<VoiceMessage>>,
|
||||||
message_sink: Option<Sender<MessageRequest>>,
|
message_sink: Option<Sender<MessageRequest>>,
|
||||||
@@ -550,12 +532,11 @@ pub struct Radio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Radio {
|
impl Radio {
|
||||||
pub fn new(frequency: u64, modulation: impl Into<Modulation>, voice: Voice) -> Self {
|
pub fn new(frequency: u64, modulation: impl Into<Modulation>) -> Self {
|
||||||
let modulation = modulation.into();
|
let modulation = modulation.into();
|
||||||
Self {
|
Self {
|
||||||
frequency,
|
frequency,
|
||||||
modulation,
|
modulation,
|
||||||
voice,
|
|
||||||
sguid: create_sguid(),
|
sguid: create_sguid(),
|
||||||
voice_sink: None,
|
voice_sink: None,
|
||||||
message_sink: None,
|
message_sink: None,
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
|||||||
use bytes::{BufMut, BytesMut};
|
use bytes::{BufMut, BytesMut};
|
||||||
use tokio_util::codec::{Decoder, Encoder, LengthDelimitedCodec};
|
use tokio_util::codec::{Decoder, Encoder, LengthDelimitedCodec};
|
||||||
|
|
||||||
|
use super::message;
|
||||||
|
|
||||||
pub const GUID_LENGTH: usize = 22;
|
pub const GUID_LENGTH: usize = 22;
|
||||||
|
|
||||||
pub const PACKET_HEADER_LENGTH: usize = size_of::<u16>() // UInt16 Packet Length - 2 bytes
|
pub const PACKET_HEADER_LENGTH: usize = size_of::<u16>() // UInt16 Packet Length - 2 bytes
|
||||||
@@ -53,6 +55,20 @@ pub enum Modulation {
|
|||||||
Disabled,
|
Disabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<message::Modulation> for Modulation {
|
||||||
|
fn from(value: message::Modulation) -> Self {
|
||||||
|
match value {
|
||||||
|
super::Modulation::Am => Modulation::Am,
|
||||||
|
super::Modulation::Fm => Modulation::Fm,
|
||||||
|
super::Modulation::Intercom => Modulation::Intercom,
|
||||||
|
super::Modulation::Disabled => Modulation::Disabled,
|
||||||
|
super::Modulation::HaveQuick => Modulation::Disabled,
|
||||||
|
super::Modulation::Satcom => Modulation::Disabled,
|
||||||
|
super::Modulation::Mids => Modulation::Disabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Encryption {
|
pub enum Encryption {
|
||||||
None,
|
None,
|
||||||
|
|||||||
@@ -1,135 +1,155 @@
|
|||||||
// TODO: Look into SpeakNG to avoid the windows dependency https://crates.io/crates/espeakng
|
use std::io::Cursor;
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use bevy::log;
|
||||||
|
use dasp::interpolate::sinc::Sinc;
|
||||||
|
use dasp::ring_buffer;
|
||||||
|
use dasp::signal;
|
||||||
|
use dasp::Sample;
|
||||||
|
use dasp::Signal;
|
||||||
|
|
||||||
use tokio::sync::Mutex;
|
use hound::WavSpec;
|
||||||
use tracing::{debug, error, warn};
|
use hound::WavWriter;
|
||||||
use windows::core::HSTRING;
|
use tokio::io::AsyncReadExt;
|
||||||
use windows::Media::SpeechSynthesis::SpeechSynthesizer;
|
use tokio::io::AsyncWriteExt;
|
||||||
use windows::Storage::Streams::DataReader;
|
use tokio::net::TcpStream;
|
||||||
|
use tokio::net::ToSocketAddrs;
|
||||||
|
|
||||||
use crate::srs::Voice;
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct WinConfig {
|
pub struct Packet {
|
||||||
pub voice: Option<String>,
|
#[serde(rename = "type")]
|
||||||
|
pub r#type: String,
|
||||||
|
pub version: String,
|
||||||
|
pub data_length: Option<usize>,
|
||||||
|
pub payload_length: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WinConfig {
|
#[derive(Serialize)]
|
||||||
pub fn new(voice: &str) -> Self {
|
struct RequestData {
|
||||||
Self {
|
text: String,
|
||||||
voice: Some(voice.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static MUTEX: Mutex<()> = Mutex::const_new(());
|
#[derive(Serialize)]
|
||||||
|
struct Request {
|
||||||
|
r#type: String,
|
||||||
|
data: RequestData,
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn synthesize(text: &str, voice: &Voice) -> Result<Vec<Vec<u8>>, WinError> {
|
pub async fn synthesize<A: ToSocketAddrs>(
|
||||||
let config = WinConfig::new(&voice.to_string());
|
addr: A,
|
||||||
// Note, there does not seem to be a way to explicitly set 16000kHz, 16 audio bits per
|
text: &str,
|
||||||
// sample and mono channel.
|
) -> Result<Vec<Vec<u8>>, Box<dyn std::error::Error + Send + Sync + 'static>> {
|
||||||
|
let stream = TcpStream::connect(addr).await?;
|
||||||
|
|
||||||
// Prevent concurrent Windows TTS synthesis, as this might cause a crash.
|
let (mut read, mut write) = tokio::io::split(stream);
|
||||||
let lock = MUTEX.lock().await;
|
|
||||||
|
|
||||||
let mut voice_info = None;
|
let request = Request {
|
||||||
if let Some(voice) = &config.voice {
|
r#type: "synthesize".to_string(),
|
||||||
let all_voices = SpeechSynthesizer::AllVoices()?;
|
data: RequestData {
|
||||||
let len = all_voices.Size()? as usize;
|
text: text.to_string(),
|
||||||
for i in 0..len {
|
},
|
||||||
let v = all_voices.GetAt(i as u32)?;
|
|
||||||
let lang = v.Language()?.to_string();
|
|
||||||
if !lang.starts_with("en-") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = v.DisplayName()?.to_string();
|
|
||||||
if name.ends_with(voice) {
|
|
||||||
voice_info = Some(v);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// default to the first english voice in the list
|
|
||||||
let all_voices = SpeechSynthesizer::AllVoices()?;
|
|
||||||
let len = all_voices.Size()? as usize;
|
|
||||||
for i in 0..len {
|
|
||||||
let v = all_voices.GetAt(i as u32)?;
|
|
||||||
let lang = v.Language()?.to_string();
|
|
||||||
if lang.starts_with("en-") {
|
|
||||||
let name = v.DisplayName()?.to_string();
|
|
||||||
debug!("Using WIN voice: {}", name);
|
|
||||||
voice_info = Some(v);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if voice_info.is_none() {
|
|
||||||
error!("Could not find any english Windows TTS voice");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if voice_info.is_none() {
|
|
||||||
let all_voices = SpeechSynthesizer::AllVoices()?;
|
|
||||||
let len = all_voices.Size()? as usize;
|
|
||||||
warn!(
|
|
||||||
"Available WIN voices are (you don't have to include the `Microsoft` prefix in \
|
|
||||||
the name):"
|
|
||||||
);
|
|
||||||
for i in 0..len {
|
|
||||||
let v = all_voices.GetAt(i as u32)?;
|
|
||||||
let lang = v.Language()?.to_string();
|
|
||||||
if !lang.starts_with("en-") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = v.DisplayName()?.to_string();
|
|
||||||
warn!("- {} ({})", name, lang);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let synth = SpeechSynthesizer::new()?;
|
|
||||||
let lang = if let Some(info) = voice_info {
|
|
||||||
synth.SetVoice(&info)?;
|
|
||||||
info.Language()?.to_string().into()
|
|
||||||
} else {
|
|
||||||
Cow::Borrowed("en")
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// the DataReader is !Send, which is why we have to process it in a local set
|
let mut request = serde_json::to_string(&request)?;
|
||||||
let stream = synth
|
request.push('\n');
|
||||||
.SynthesizeSsmlToStreamAsync(&HSTRING::from(&format!(
|
|
||||||
r#"<speak version="1.0" xml:lang="{lang}">{text}</speak>"#
|
|
||||||
)))?
|
|
||||||
.await?;
|
|
||||||
let size = stream.Size()?;
|
|
||||||
|
|
||||||
let rd = DataReader::CreateDataReader(&stream.GetInputStreamAt(0)?)?;
|
write.write_all(request.as_bytes()).await?;
|
||||||
rd.LoadAsync(size as u32)?.await?;
|
|
||||||
|
|
||||||
let mut wav = vec![0u8; size as usize];
|
let mut str = String::new();
|
||||||
rd.ReadBytes(wav.as_mut_slice())?;
|
let mut pcm_buffer: Vec<u8> = Vec::new();
|
||||||
|
loop {
|
||||||
|
let Ok(c) = read.read_u8().await else {
|
||||||
|
log::error!("Unable to read byte");
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
drop(lock);
|
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();
|
||||||
|
|
||||||
Ok(wav_to_opus(wav.into()).await?)
|
if let Some(length) = packet.data_length {
|
||||||
}
|
let mut buf = (0..length).map(|_| 0u8).collect::<Vec<_>>();
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
if let Err(e) = read.read_exact(&mut buf).await {
|
||||||
pub enum WinError {
|
log::error!("Unable to read data: {:?}", e);
|
||||||
#[error("Calling WinRT API failed with error code {0}: {1}")]
|
break;
|
||||||
Win(i32, String),
|
}
|
||||||
#[error("Runtime error")]
|
};
|
||||||
Io(#[from] std::io::Error),
|
|
||||||
#[error("failed to encode audio data as opus")]
|
|
||||||
Opus(#[from] audiopus::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<windows::core::Error> for WinError {
|
if let Some(length) = packet.payload_length {
|
||||||
fn from(err: windows::core::Error) -> Self {
|
let mut buf = (0..length).map(|_| 0u8).collect::<Vec<_>>();
|
||||||
WinError::Win(err.code().0, err.message().to_string())
|
|
||||||
|
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> {
|
async fn wav_to_opus(wav: bytes::Bytes) -> Result<Vec<Vec<u8>>, audiopus::Error> {
|
||||||
@@ -160,3 +180,30 @@ async fn wav_to_opus(wav: bytes::Bytes) -> Result<Vec<Vec<u8>>, audiopus::Error>
|
|||||||
.await
|
.await
|
||||||
.unwrap()
|
.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())
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "nightly"
|
channel = "stable"
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ impl Braa {
|
|||||||
let bearing = split(&self.bearing.to_string(), 1);
|
let bearing = split(&self.bearing.to_string(), 1);
|
||||||
let range = split(&self.range.to_string(), 1);
|
let range = split(&self.range.to_string(), 1);
|
||||||
format!(
|
format!(
|
||||||
"BRA <break time=\"250ms\"/> {} <break /> {} <break /> {} thousand <break /> {}",
|
"BRA . . . {} . . . {} . . . {} thousand . . . {}",
|
||||||
bearing, range, self.angels, self.aspect
|
bearing, range, self.angels, self.aspect
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,13 +64,13 @@ pub fn bogey_dope(
|
|||||||
let (Some(n_position), Some(n_heading)) = n else {
|
let (Some(n_position), Some(n_heading)) = n else {
|
||||||
let message = format!("{}, {}, {}", p_callsign, a_callsign, "Skies are clear");
|
let message = format!("{}, {}, {}", p_callsign, a_callsign, "Skies are clear");
|
||||||
let voice_message = format!(
|
let voice_message = format!(
|
||||||
"{}, {}, {}",
|
"{} . . . {} . . . {}",
|
||||||
p_callsign.to_voice(),
|
p_callsign.to_voice(),
|
||||||
a_callsign.to_voice(),
|
a_callsign.to_voice(),
|
||||||
"Skies are clear"
|
"Skies are clear"
|
||||||
);
|
);
|
||||||
|
|
||||||
commands.spawn(TextMessage::new(message));
|
commands.entity(*pilot).insert(TextMessage::new(message));
|
||||||
commands
|
commands
|
||||||
.entity(*operator)
|
.entity(*operator)
|
||||||
.insert(VoiceMessage::new(voice_message));
|
.insert(VoiceMessage::new(voice_message));
|
||||||
@@ -80,13 +80,13 @@ pub fn bogey_dope(
|
|||||||
let bra = Braa::new(p_pos, n_position, n_heading);
|
let bra = Braa::new(p_pos, n_position, n_heading);
|
||||||
let message = format!("{}, {}, {}", p_callsign, a_callsign, bra);
|
let message = format!("{}, {}, {}", p_callsign, a_callsign, bra);
|
||||||
let voice_message = format!(
|
let voice_message = format!(
|
||||||
"{}, {}, {}",
|
"{} . . . {} . . . {}",
|
||||||
p_callsign.to_voice(),
|
p_callsign.to_voice(),
|
||||||
a_callsign.to_voice(),
|
a_callsign.to_voice(),
|
||||||
bra.to_voice()
|
bra.to_voice()
|
||||||
);
|
);
|
||||||
|
|
||||||
commands.spawn(TextMessage::new(message));
|
commands.entity(*pilot).insert(TextMessage::new(message));
|
||||||
commands
|
commands
|
||||||
.entity(*operator)
|
.entity(*operator)
|
||||||
.insert(VoiceMessage::new(voice_message));
|
.insert(VoiceMessage::new(voice_message));
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ pub fn clear_tripwire(
|
|||||||
a_callsign.to_voice(),
|
a_callsign.to_voice(),
|
||||||
);
|
);
|
||||||
|
|
||||||
commands.spawn(TextMessage::new(message));
|
commands.entity(*pilot).insert(TextMessage::new(message));
|
||||||
commands
|
commands
|
||||||
.entity(*operator)
|
.entity(*operator)
|
||||||
.insert(VoiceMessage::new(message_voice));
|
.insert(VoiceMessage::new(message_voice));
|
||||||
|
|||||||
@@ -44,13 +44,13 @@ pub fn radio_check(
|
|||||||
|
|
||||||
let message = format!("{}, {}, {}", p_callsign, a_callsign, "five by five");
|
let message = format!("{}, {}, {}", p_callsign, a_callsign, "five by five");
|
||||||
let voice_message = format!(
|
let voice_message = format!(
|
||||||
"{}, {}, {}",
|
"{} . . . {} . . . {}",
|
||||||
p_callsign.to_voice(),
|
p_callsign.to_voice(),
|
||||||
a_callsign.to_voice(),
|
a_callsign.to_voice(),
|
||||||
"five by five"
|
"five by five"
|
||||||
);
|
);
|
||||||
|
|
||||||
commands.spawn(TextMessage::new(message));
|
commands.entity(*pilot).insert(TextMessage::new(message));
|
||||||
commands
|
commands
|
||||||
.entity(*operator)
|
.entity(*operator)
|
||||||
.insert(VoiceMessage::new(voice_message));
|
.insert(VoiceMessage::new(voice_message));
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ pub fn set_tripwire(
|
|||||||
distance
|
distance
|
||||||
);
|
);
|
||||||
|
|
||||||
commands.spawn(TextMessage::new(message));
|
commands.entity(*pilot).insert(TextMessage::new(message));
|
||||||
commands
|
commands
|
||||||
.entity(*operator)
|
.entity(*operator)
|
||||||
.insert(VoiceMessage::new(message_voice));
|
.insert(VoiceMessage::new(message_voice));
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use std::{collections::HashMap, path::PathBuf};
|
|||||||
use bevy::ecs::system::Resource;
|
use bevy::ecs::system::Resource;
|
||||||
use guardian_core::{
|
use guardian_core::{
|
||||||
components::{GrpcBaseUrl, SrsSocketAddr, SttBaseUrl},
|
components::{GrpcBaseUrl, SrsSocketAddr, SttBaseUrl},
|
||||||
srs::{Modulation as SrsModulation, Voice},
|
srs::Modulation as SrsModulation,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -27,7 +27,6 @@ pub struct SrsConfig {
|
|||||||
pub struct ChannelConfig {
|
pub struct ChannelConfig {
|
||||||
pub frequency: u64,
|
pub frequency: u64,
|
||||||
pub modulation: Modulation, // 0=AM, 1=FM?
|
pub modulation: Modulation, // 0=AM, 1=FM?
|
||||||
pub voice: Voice,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<PathBuf> for Config {
|
impl TryFrom<PathBuf> for Config {
|
||||||
@@ -57,7 +56,6 @@ impl Config {
|
|||||||
ChannelConfig {
|
ChannelConfig {
|
||||||
frequency: 251000000,
|
frequency: 251000000,
|
||||||
modulation: Modulation::Am,
|
modulation: Modulation::Am,
|
||||||
voice: Voice::new("David"),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
44
src/main.rs
44
src/main.rs
@@ -3,6 +3,8 @@ mod commands;
|
|||||||
mod config;
|
mod config;
|
||||||
mod tripwire;
|
mod tripwire;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use bevy::{
|
use bevy::{
|
||||||
app::{App, Update},
|
app::{App, Update},
|
||||||
ecs::{
|
ecs::{
|
||||||
@@ -11,6 +13,7 @@ use bevy::{
|
|||||||
system::{Commands, Query, Res},
|
system::{Commands, Query, Res},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use clap::Parser;
|
||||||
use commands::CommandsPlugin;
|
use commands::CommandsPlugin;
|
||||||
use config::{Channels, Config};
|
use config::{Channels, Config};
|
||||||
use guardian_core::{
|
use guardian_core::{
|
||||||
@@ -19,8 +22,15 @@ use guardian_core::{
|
|||||||
DefaultPlugins, TokioResource,
|
DefaultPlugins, TokioResource,
|
||||||
};
|
};
|
||||||
use tokio::runtime::Handle;
|
use tokio::runtime::Handle;
|
||||||
|
use tracing::info;
|
||||||
use tripwire::TripwirePlugin;
|
use tripwire::TripwirePlugin;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
struct CommandLineArguments {
|
||||||
|
#[arg(short, long)]
|
||||||
|
config: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let subscriber = tracing_subscriber::FmtSubscriber::builder()
|
let subscriber = tracing_subscriber::FmtSubscriber::builder()
|
||||||
@@ -35,7 +45,21 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||||||
|
|
||||||
tracing::subscriber::set_global_default(subscriber)?;
|
tracing::subscriber::set_global_default(subscriber)?;
|
||||||
|
|
||||||
let config = Config::load("config.toml".into())?;
|
let args = CommandLineArguments::parse();
|
||||||
|
|
||||||
|
let config = match args.config {
|
||||||
|
Some(c) => {
|
||||||
|
if !c.try_exists()? {
|
||||||
|
return Err(format!("Config file {} not found", c.display()).into());
|
||||||
|
}
|
||||||
|
c
|
||||||
|
}
|
||||||
|
None => "config.toml".into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("Loading config {}", config.display());
|
||||||
|
|
||||||
|
let config = Config::load(config)?;
|
||||||
|
|
||||||
App::new()
|
App::new()
|
||||||
.insert_resource(config.srs.server)
|
.insert_resource(config.srs.server)
|
||||||
@@ -64,20 +88,8 @@ fn give_awacs_radio(
|
|||||||
|
|
||||||
bevy::log::info!("{} now has a radio", callsign);
|
bevy::log::info!("{} now has a radio", callsign);
|
||||||
|
|
||||||
commands.entity(ent).insert(Radio::new(
|
commands
|
||||||
channel.frequency,
|
.entity(ent)
|
||||||
channel.modulation,
|
.insert(Radio::new(channel.frequency, channel.modulation));
|
||||||
channel.voice.clone(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// // Initial player tripwire? get from some db so player doesnt need to redo it everytime
|
|
||||||
// fn add_tripwire(mut commands: Commands, players: Query<Entity, Added<Player>>) {
|
|
||||||
// for ent in players.iter() {
|
|
||||||
// commands.entity(ent).insert(Tripwire {
|
|
||||||
// range: 2.0,
|
|
||||||
// reported: vec![],
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ impl Plugin for TripwirePlugin {
|
|||||||
fn tripwire(
|
fn tripwire(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
awacs: Query<(Entity, &Callsign, &Radio), (With<Awacs>, With<Blue>, With<Radio>)>,
|
awacs: Query<(Entity, &Callsign, &Radio), (With<Awacs>, With<Blue>, With<Radio>)>,
|
||||||
mut players: Query<(&Callsign, &Position, &RadioInfo, &mut Tripwire), With<Player>>,
|
mut players: Query<(Entity, &Callsign, &Position, &RadioInfo, &mut Tripwire), With<Player>>,
|
||||||
npc: Query<(&Id, &Position, &Heading, &Velocity), (Without<Player>, With<Red>)>,
|
npc: Query<(&Id, &Position, &Heading, &Velocity), (Without<Player>, With<Red>)>,
|
||||||
) {
|
) {
|
||||||
// get the players radio frequencies
|
// get the players radio frequencies
|
||||||
@@ -34,7 +34,7 @@ fn tripwire(
|
|||||||
// broadcast message on that ferquency and as that awacs
|
// broadcast message on that ferquency and as that awacs
|
||||||
|
|
||||||
for (ent, awacs, radio) in awacs.iter() {
|
for (ent, awacs, radio) in awacs.iter() {
|
||||||
for (p_callsign, p_position, radioinfo, mut p_tripwire) in players.iter_mut() {
|
for (p_ent, p_callsign, p_position, radioinfo, mut p_tripwire) in players.iter_mut() {
|
||||||
// check if user is on the same frequency as awacs
|
// check if user is on the same frequency as awacs
|
||||||
if !radioinfo
|
if !radioinfo
|
||||||
.radios
|
.radios
|
||||||
@@ -56,13 +56,14 @@ fn tripwire(
|
|||||||
let bra = Braa::new(p_position, n_position, n_heading);
|
let bra = Braa::new(p_position, n_position, n_heading);
|
||||||
let message = format!("{}, {}, {}", p_callsign, awacs, bra);
|
let message = format!("{}, {}, {}", p_callsign, awacs, bra);
|
||||||
let voice_message = format!(
|
let voice_message = format!(
|
||||||
"{}, {}, {}",
|
"{} . . . {} . . . {}",
|
||||||
p_callsign.to_voice(),
|
p_callsign.to_voice(),
|
||||||
awacs.to_voice(),
|
awacs.to_voice(),
|
||||||
bra.to_voice()
|
bra.to_voice()
|
||||||
);
|
);
|
||||||
|
|
||||||
commands.spawn(TextMessage::new(message));
|
// commands.spawn((Id::new(player_id.into()), TextMessage::new(message)));
|
||||||
|
commands.entity(p_ent).insert(TextMessage::new(message));
|
||||||
commands
|
commands
|
||||||
.entity(ent)
|
.entity(ent)
|
||||||
.insert(VoiceMessage::new(voice_message));
|
.insert(VoiceMessage::new(voice_message));
|
||||||
|
|||||||
Reference in New Issue
Block a user