Initial Commit

This commit is contained in:
AviiNL
2023-12-22 04:50:17 +01:00
parent 15f764aae8
commit f79b6b5dfb
41 changed files with 4379 additions and 270 deletions

97
src/braa.rs Normal file
View File

@@ -0,0 +1,97 @@
use std::fmt::Display;
use guardian_core::components::{Heading, Position};
#[derive(Clone)]
pub enum Aspect {
Hot,
Flank,
Cold,
}
impl std::fmt::Display for Aspect {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Hot => write!(f, "Hot"),
Self::Flank => write!(f, "Flank"),
Self::Cold => write!(f, "Cold"),
}
}
}
#[derive(Clone)]
pub struct Braa {
bearing: i32,
range: i32,
angels: i32,
aspect: Aspect,
}
impl Display for Braa {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_text())
}
}
#[allow(unused)]
impl Braa {
pub fn new(unit_a: &Position, unit_b: &Position, heading_b: &Heading) -> Self {
let bearing = unit_a.get_bearing_to(unit_b);
let range = unit_a.distance_to_nmi(unit_b).round() as i32;
let angels = unit_b.angels();
let aspect = {
let bearing = unit_b.get_bearing_to(unit_a);
let angle = (bearing - heading_b.0 as i32) % 360;
if (45..135).contains(&angle) {
Aspect::Flank
} else if (135..225).contains(&angle) {
Aspect::Cold
} else if (225..315).contains(&angle) {
Aspect::Flank
} else if !(45..=315).contains(&angle) {
Aspect::Hot
} else {
Aspect::Cold
}
};
Self {
bearing,
range,
angels,
aspect,
}
}
pub fn to_text(&self) -> String {
format!(
"BRA {}° for {}NM at {} thousand {}",
self.bearing, self.range, self.angels, self.aspect
)
}
pub fn to_voice(&self) -> String {
let bearing = split(&self.bearing.to_string(), 1);
let range = split(&self.range.to_string(), 1);
format!(
"BRA <break time=\"250ms\"/> {} <break /> {} <break /> {} thousand <break /> {}",
bearing, range, self.angels, self.aspect
)
}
}
fn split(input: &str, n: usize) -> String {
input
.chars()
.enumerate()
.flat_map(|(i, c)| {
if i != 0 && i % n == 0 {
Some(' ')
} else {
None
}
.into_iter()
.chain(std::iter::once(c))
})
.collect::<String>()
}

85
src/commands/bogeydope.rs Normal file
View File

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

21
src/commands/mod.rs Normal file
View File

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

View File

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

102
src/commands/tripwire.rs Normal file
View File

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

View File

@@ -1,158 +1,50 @@
mod braa;
mod commands;
mod tripwire;
use bevy::{
app::{App, Update},
ecs::{
component::Component,
entity::Entity,
query::{Added, With, Without},
query::{With, Without},
system::{Commands, Query},
},
};
use guardian_core::*;
use commands::CommandsPlugin;
use guardian_core::{
components::{Awacs, Callsign},
srs::{Modulation, Radio},
DefaultPlugins, TokioResource,
};
use tokio::runtime::Handle;
// Every incomming (voice) message is an event
// Systems can handle the events, eg bogey dope, set tripwire or radio check
// voice responses; srs should be a resource?
// |- srs plugin adds a resource to use to send messages i guess
// |-- or spawn an entity with a message commands.spawn((VoiceMessage("Hello World"), TextMessage("Hello World!")))
// |-- srs plugin will check if these components are Added<VoiceMessage> or w/e and handle accordingly
use tripwire::TripwirePlugin;
#[tokio::main]
async fn main() {
App::new()
.insert_resource(TokioResource(Handle::current()))
.add_plugins(DefaultPlugins)
.add_systems(Update, (add_tripwire, tripwire))
.add_plugins(TripwirePlugin)
.add_plugins(CommandsPlugin)
.add_systems(Update, give_awacs_radio)
.run();
}
// this will be on a tripwire command event (from srs), instead of Added<Player>, not everyone may want it; opt-in over opt-out
fn add_tripwire(mut commands: Commands, players: Query<Entity, Added<Player>>) {
for ent in players.iter() {
commands.entity(ent).insert(Tripwire {
range: 30.0,
reported: vec![],
});
}
}
fn tripwire(
fn give_awacs_radio(
mut commands: Commands,
mut players: Query<(&Id, &Callsign, &Position, &mut Tripwire), With<Player>>,
npc: Query<(&Id, &Position, &Heading, &Side), Without<Player>>,
units: Query<(Entity, &Callsign), (With<Awacs>, Without<Radio>)>,
) {
for (p_id, p_callsign, p_position, mut p_tripwire) in players.iter_mut() {
let _p_id = &p_id.0;
let _p_callsign = &p_callsign.0;
for (n_id, n_position, n_heading, side) in npc.iter() {
if side.0 != Coalition::Red {
continue;
}
for (ent, callsign) in units.iter() {
if callsign.as_str() == "Overlord1-1" {
commands
.entity(ent)
.insert(Radio::new(251000000, Modulation::Am));
}
let n_id = &n_id.0;
let distance = n_position.distance_to_nmi(p_position);
if distance <= p_tripwire.range && !p_tripwire.reported.contains(n_id) {
p_tripwire.reported.push(*n_id);
let bra = Braa::new(p_position, n_position, n_heading);
commands.spawn(TextMessage::new(bra.to_text()));
} else if distance > p_tripwire.range && p_tripwire.reported.contains(n_id) {
p_tripwire.reported.retain(|b| b != n_id);
}
if callsign.as_str() == "Magic1-1" {
commands
.entity(ent)
.insert(Radio::new(266000000, Modulation::Am));
}
}
}
#[derive(Component)]
struct Tripwire {
range: f64,
reported: Vec<u32>,
}
#[derive(Clone)]
pub enum Aspect {
Hot,
Flank,
Cold,
}
impl std::fmt::Display for Aspect {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Hot => write!(f, "Hot"),
Self::Flank => write!(f, "Flank"),
Self::Cold => write!(f, "Cold"),
}
}
}
#[derive(Clone)]
pub struct Braa {
bearing: i32,
range: i32,
angels: i32,
aspect: Aspect,
}
impl Braa {
pub fn new(unit_a: &Position, unit_b: &Position, heading_b: &Heading) -> Self {
let bearing = unit_a.get_bearing_to(unit_b);
let range = unit_a.distance_to_nmi(unit_b).round() as i32;
let angels = unit_b.angels();
let aspect = {
let bearing = unit_b.get_bearing_to(unit_a);
let angle = (bearing - heading_b.0 as i32) % 360;
if (45..135).contains(&angle) {
Aspect::Flank
} else if (135..225).contains(&angle) {
Aspect::Cold
} else if (225..315).contains(&angle) {
Aspect::Flank
} else if !(45..=315).contains(&angle) {
Aspect::Hot
} else {
Aspect::Cold
}
};
Self {
bearing,
range,
angels,
aspect,
}
}
pub fn to_text(&self) -> String {
format!(
"BRA {}° for {}NM at {} thousand {}",
self.bearing, self.range, self.angels, self.aspect
)
}
pub fn to_voice(&self) -> String {
let bearing = split(&self.bearing.to_string(), 1);
let range = split(&self.range.to_string(), 1);
format!(
"BRA <break time=\"250ms\"/> {} <break /> {} <break /> {} thousand <break /> {}",
bearing, range, self.angels, self.aspect
)
}
}
fn split(input: &str, n: usize) -> String {
input
.chars()
.enumerate()
.flat_map(|(i, c)| {
if i != 0 && i % n == 0 {
Some(' ')
} else {
None
}
.into_iter()
.chain(std::iter::once(c))
})
.collect::<String>()
}

96
src/tripwire.rs Normal file
View File

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