diff --git a/README.md b/README.md index 069db61..83fbdba 100644 --- a/README.md +++ b/README.md @@ -4,4 +4,4 @@ Blazingly fast rust interface for the RLBot v5 socket api. ## Examples -see `crates/rlbot/examples` folder +see the [`rlbot/examples`](https://github.com/RLBot/rust-interface/tree/master/rlbot/examples) folder diff --git a/flatbuffers-schema b/flatbuffers-schema index 805c3cb..488b414 160000 --- a/flatbuffers-schema +++ b/flatbuffers-schema @@ -1 +1 @@ -Subproject commit 805c3cb5f0cd8aa13b940d98f9af59914aab0012 +Subproject commit 488b414e2a046c15cd394c1fd4788fdc9074cefe diff --git a/rlbot/examples/atba_agent/bot.toml b/rlbot/examples/atba_agent/bot.toml index 44d74c5..e0c4c6d 100644 --- a/rlbot/examples/atba_agent/bot.toml +++ b/rlbot/examples/atba_agent/bot.toml @@ -6,7 +6,8 @@ agent_id = "rlbot/rust-example/atba_agent" # loadout_file = "loadout.toml" run_command = "..\\..\\..\\target\\release\\examples\\atba_agent.exe" run_command_linux = "../../../target/release/examples/atba_agent" -# rust_interface::agents::run_agents supports "non-hivemind" multithreading using one connection +# rlbot::agents::BotAgent can run on multiple threads, sharing a process. +# If you want the bot to run as separate processes, set this field to false. hivemind = true [details] diff --git a/rlbot/examples/atba_agent/main.rs b/rlbot/examples/atba_agent/main.rs index a11e68e..7ec4ec5 100644 --- a/rlbot/examples/atba_agent/main.rs +++ b/rlbot/examples/atba_agent/main.rs @@ -2,24 +2,25 @@ use std::{f32::consts::PI, sync::Arc}; use rlbot::{ RLBotConnection, - agents::{Agent, run_agents}, + agents::{BotAgent, run_bot_agents}, flat::{ - ControllableInfo, ControllerState, FieldInfo, GamePacket, MatchConfiguration, PlayerInput, + ControllableInfo, ControllerState, FieldInfo, GamePacket, MatchConfiguration, PlayerClass, + PlayerInput, }, - util::{PacketQueue, RLBotEnvironment}, + util::{AgentEnvironment, PacketQueue}, }; #[allow(dead_code)] struct AtbaAgent { index: u32, - spawn_id: i32, + player_id: i32, team: u32, name: String, match_config: Arc, field_info: Arc, } -impl Agent for AtbaAgent { +impl BotAgent for AtbaAgent { fn new( team: u32, controllable_info: ControllableInfo, @@ -30,18 +31,20 @@ impl Agent for AtbaAgent { let name = match_config .player_configurations .iter() - .find_map(|player| { - if player.spawn_id == controllable_info.spawn_id { - Some(player.name.clone()) + .find(|player| player.player_id == controllable_info.identifier) + .map(|player| { + if let PlayerClass::CustomBot(custombot) = &player.variety { + &custombot.name } else { - None + unreachable!("We cannot be controlling anything other a custombot") } }) - .unwrap(); + .unwrap() + .clone(); Self { index: controllable_info.index, - spawn_id: controllable_info.spawn_id, + player_id: controllable_info.identifier, team, name, match_config, @@ -90,10 +93,10 @@ impl Agent for AtbaAgent { } fn main() { - let RLBotEnvironment { + let AgentEnvironment { server_addr, agent_id, - } = RLBotEnvironment::from_env(); + } = AgentEnvironment::from_env(); let agent_id = agent_id.unwrap_or_else(|| "rlbot/rust-example/atba_agent".into()); println!("Connecting"); @@ -108,8 +111,8 @@ fn main() { // all of the bots in a team. // Blocking. - run_agents::(agent_id.clone(), true, true, rlbot_connection) - .expect("run_agents crashed"); + run_bot_agents::(agent_id.clone(), true, true, rlbot_connection) + .expect("run_bot_agents crashed"); println!("Agent(s) with agent_id `{agent_id}` exited nicely"); } diff --git a/rlbot/examples/atba_hivemind/main.rs b/rlbot/examples/atba_hivemind/main.rs index b8bbca0..d984697 100644 --- a/rlbot/examples/atba_hivemind/main.rs +++ b/rlbot/examples/atba_hivemind/main.rs @@ -2,25 +2,25 @@ use std::f32::consts::PI; use rlbot::{ RLBotConnection, + agents::{HivemindAgent, run_hivemind_agent}, flat::{ ControllableTeamInfo, ControllerState, FieldInfo, GamePacket, MatchConfiguration, - PlayerInput, + PlayerClass, PlayerInput, }, - hivemind::{Hivemind, run_hivemind}, - util::{PacketQueue, RLBotEnvironment}, + util::{AgentEnvironment, PacketQueue}, }; #[allow(dead_code)] struct AtbaHivemind { indices: Vec, - spawn_ids: Vec, + player_ids: Vec, team: u32, names: Vec, match_config: MatchConfiguration, field_info: FieldInfo, } -impl Hivemind for AtbaHivemind { +impl HivemindAgent for AtbaHivemind { fn new( controllable_team_info: ControllableTeamInfo, match_config: MatchConfiguration, @@ -30,29 +30,30 @@ impl Hivemind for AtbaHivemind { let names = match_config .player_configurations .iter() - .filter_map(|player| { + .filter(|pconf| { controllable_team_info .controllables .iter() - .find_map(|controllable| { - if controllable.spawn_id == player.spawn_id { - Some(player.name.clone()) - } else { - None - } - }) + .any(|controllable| controllable.identifier == pconf.player_id) + }) + .map(|player| { + if let PlayerClass::CustomBot(custombot) = &player.variety { + custombot.name.clone() + } else { + unreachable!("We cannot be controlling anything other a custombot") + } }) .collect(); - let (indices, spawn_ids) = controllable_team_info + let (indices, player_ids) = controllable_team_info .controllables .iter() - .map(|controllable| (controllable.index, controllable.spawn_id)) + .map(|controllable| (controllable.index, controllable.identifier)) .unzip(); Self { indices, - spawn_ids, + player_ids, team: controllable_team_info.team, names, match_config, @@ -103,10 +104,10 @@ impl Hivemind for AtbaHivemind { } fn main() { - let RLBotEnvironment { + let AgentEnvironment { server_addr, agent_id, - } = RLBotEnvironment::from_env(); + } = AgentEnvironment::from_env(); let agent_id = agent_id.unwrap_or_else(|| "rlbot/rust-example/atba_hivemind".into()); println!("Connecting"); @@ -121,8 +122,8 @@ fn main() { // all of the bots in a team. // Blocking. - run_hivemind::(agent_id.clone(), true, true, rlbot_connection) - .expect("run_hivemind crashed"); + run_hivemind_agent::(agent_id.clone(), true, true, rlbot_connection) + .expect("run_hivemind_agent crashed"); println!("Hivemind with agent_id `{agent_id}` exited nicely"); } diff --git a/rlbot/examples/atba_raw/main.rs b/rlbot/examples/atba_raw/main.rs index 3197454..6bb8e2f 100644 --- a/rlbot/examples/atba_raw/main.rs +++ b/rlbot/examples/atba_raw/main.rs @@ -1,16 +1,16 @@ use std::f32::consts::PI; use rlbot::{ - Packet, RLBotConnection, - flat::{ConnectionSettings, ControllerState, PlayerInput}, - util::RLBotEnvironment, + RLBotConnection, + flat::{ConnectionSettings, ControllerState, CoreMessage, InitComplete, PlayerInput}, + util::AgentEnvironment, }; fn main() { - let RLBotEnvironment { + let AgentEnvironment { server_addr, agent_id, - } = RLBotEnvironment::from_env(); + } = AgentEnvironment::from_env(); let agent_id = agent_id.unwrap_or_else(|| "rlbot/rust-example/atba_raw".into()); let mut rlbot_connection = RLBotConnection::new(&server_addr).expect("connection"); @@ -31,7 +31,7 @@ fn main() { // Wait for ControllableTeamInfo to know which indices we control let controllable_team_info = loop { let packet = rlbot_connection.recv_packet().unwrap(); - if let Packet::ControllableTeamInfo(x) = packet { + if let CoreMessage::ControllableTeamInfo(x) = packet { break x; } @@ -48,10 +48,10 @@ fn main() { .first() .expect("controllables.len() = 1"); - rlbot_connection.send_packet(Packet::InitComplete).unwrap(); + rlbot_connection.send_packet(InitComplete {}).unwrap(); loop { - let Packet::GamePacket(game_packet) = packets_to_process + let CoreMessage::GamePacket(game_packet) = packets_to_process .pop() .unwrap_or_else(|| rlbot_connection.recv_packet().unwrap()) else { diff --git a/rlbot/examples/high_jump_script/main.rs b/rlbot/examples/high_jump_script/main.rs index 239b518..d6ca0ae 100644 --- a/rlbot/examples/high_jump_script/main.rs +++ b/rlbot/examples/high_jump_script/main.rs @@ -1,11 +1,11 @@ use rlbot::{ RLBotConnection, + agents::{ScriptAgent, run_script_agent}, flat::{ DesiredCarState, DesiredGameState, DesiredPhysics, FieldInfo, GamePacket, MatchConfiguration, MatchPhase, Vector3Partial, }, - scripts::{Script, run_script}, - util::{PacketQueue, RLBotEnvironment}, + util::{AgentEnvironment, PacketQueue}, }; #[allow(dead_code)] @@ -15,7 +15,7 @@ struct MyScript { prev_jumps: Vec, } -impl Script for MyScript { +impl ScriptAgent for MyScript { fn new( agent_id: String, match_config: MatchConfiguration, @@ -81,16 +81,16 @@ impl Script for MyScript { } fn main() { - let RLBotEnvironment { + let AgentEnvironment { server_addr, agent_id, - } = RLBotEnvironment::from_env(); + } = AgentEnvironment::from_env(); let agent_id = agent_id.unwrap_or_else(|| "rlbot/rust-example/high_jump_script".into()); let rlbot_connection = RLBotConnection::new(&server_addr).expect("connection"); // Blocking. - run_script::(agent_id.clone(), true, true, rlbot_connection) - .expect("run_script crashed"); + run_script_agent::(agent_id.clone(), true, true, rlbot_connection) + .expect("run_script_agent crashed"); println!("Script with agent_id `{agent_id}` exited nicely"); } diff --git a/rlbot/examples/packet_logger.rs b/rlbot/examples/packet_logger.rs index bad5e62..7008bc7 100644 --- a/rlbot/examples/packet_logger.rs +++ b/rlbot/examples/packet_logger.rs @@ -1,10 +1,10 @@ -use rlbot::{RLBotConnection, flat::ConnectionSettings, util::RLBotEnvironment}; +use rlbot::{RLBotConnection, flat::ConnectionSettings, util::AgentEnvironment}; fn main() { - let RLBotEnvironment { + let AgentEnvironment { server_addr, agent_id, - } = RLBotEnvironment::from_env(); + } = AgentEnvironment::from_env(); let agent_id = agent_id.unwrap_or_else(|| "rlbot/rust-packet-logger".into()); let mut rlbot_connection = RLBotConnection::new(&server_addr).expect("connection"); diff --git a/rlbot/examples/start_match.rs b/rlbot/examples/start_match.rs index 752908e..bfe45f3 100644 --- a/rlbot/examples/start_match.rs +++ b/rlbot/examples/start_match.rs @@ -3,8 +3,8 @@ use std::env::args; use rlbot::{ RLBotConnection, flat::{ - CustomBot, ExistingMatchBehavior, GameMode, Human, MatchConfiguration, MatchLengthMutator, - MutatorSettings, PlayerClass, PlayerConfiguration, + CustomBot, DebugRendering, ExistingMatchBehavior, GameMode, Human, MatchConfiguration, + MatchLengthMutator, MutatorSettings, PlayerClass, PlayerConfiguration, }, }; @@ -25,34 +25,29 @@ fn main() { let mut player_configurations = (0..bots_to_add) .map(|i| PlayerConfiguration { - variety: PlayerClass::CustomBot(Box::new(CustomBot {})), - name: format!("BOT{i}"), + variety: PlayerClass::CustomBot(Box::new(CustomBot { + name: format!("BOT{i}"), + root_dir: String::default(), + run_command: String::default(), + loadout: None, + agent_id: agent_id.clone(), + hivemind: true, + })), team: i % 2, - root_dir: String::default(), - run_command: String::default(), - loadout: None, - spawn_id: 0, // RLBotServer will set this - agent_id: agent_id.clone(), - hivemind: true, + player_id: 0, // RLBotServer will set this }) .collect::>(); // Also add a human player_configurations.push(PlayerConfiguration { variety: PlayerClass::Human(Box::new(Human {})), - name: String::default(), team: 1, - loadout: None, - spawn_id: Default::default(), - root_dir: String::default(), - agent_id: String::default(), - run_command: String::default(), - hivemind: Default::default(), + player_id: Default::default(), }); let match_configuration = MatchConfiguration { player_configurations, - game_mode: GameMode::Soccer, + game_mode: GameMode::Soccar, game_map_upk: "UtopiaStadium_P".into(), // mutatorSettings CANNOT be None, otherwise RLBot will crash (this is true for v4, maybe not v5) mutators: Some(Box::new(MutatorSettings { @@ -60,7 +55,7 @@ fn main() { ..Default::default() })), existing_match_behavior: ExistingMatchBehavior::Restart, - enable_rendering: true, + enable_rendering: DebugRendering::OnByDefault, enable_state_setting: true, ..Default::default() }; diff --git a/rlbot/src/agents.rs b/rlbot/src/agents/bot.rs similarity index 83% rename from rlbot/src/agents.rs rename to rlbot/src/agents/bot.rs index eb0f742..e74c741 100644 --- a/rlbot/src/agents.rs +++ b/rlbot/src/agents/bot.rs @@ -1,13 +1,11 @@ use std::{mem, sync::Arc, thread}; -use crate::{ - Packet, RLBotConnection, StartingInfo, - flat::*, - util::{PacketQueue, write_multiple_packets}, -}; +use crate::{RLBotConnection, StartingInfo, flat::*, util::PacketQueue}; + +use super::AgentError; #[allow(unused_variables)] -pub trait Agent { +pub trait BotAgent { // TODO: Maybe pass a struct? fn new( team: u32, @@ -21,14 +19,6 @@ pub trait Agent { fn on_ball_prediction(&mut self, ball_prediction: &BallPrediction) {} } -#[derive(thiserror::Error, Debug)] -pub enum AgentError { - #[error("Agent panicked")] - AgentPanic, - #[error("RLBot failed")] - PacketParseError(#[from] crate::RLBotError), -} - /// Run multiple agents with n agents per thread. They share a connection. /// Ok(()) means a successful exit; one of the bots received a None packet. /// @@ -39,7 +29,7 @@ pub enum AgentError { /// # Panics /// /// Panics if a thread can't be spawned for each agent. -pub fn run_agents( +pub fn run_bot_agents( // TODO: Maybe pass a struct? agent_id: String, wants_ball_predictions: bool, @@ -70,9 +60,9 @@ pub fn run_agents( let num_threads = controllable_team_info.controllables.len(); let mut threads = Vec::with_capacity(num_threads); - let (outgoing_sender, outgoing_recver) = kanal::unbounded::>(); + let (outgoing_sender, outgoing_recver) = kanal::unbounded::>(); for (i, controllable_info) in controllable_team_info.controllables.into_iter().enumerate() { - let (incoming_sender, incoming_recver) = kanal::unbounded::>(); + let (incoming_sender, incoming_recver) = kanal::unbounded::>(); let match_configuration = match_configuration.clone(); let field_info = field_info.clone(); @@ -86,7 +76,7 @@ pub fn run_agents( controllable_info.index, )) .spawn(move || { - run_agent::( + run_bot_agent::( incoming_recver, controllable_team_info.team, controllable_info, @@ -103,7 +93,7 @@ pub fn run_agents( // which we rely on for clean exiting drop(outgoing_sender); - let mut to_send: Vec> = vec![Vec::new(); num_threads]; + let mut to_send: Vec> = vec![Vec::new(); num_threads]; for reserved_packet_spot in &mut to_send { if let Ok(messages) = outgoing_recver.recv() { *reserved_packet_spot = messages; @@ -112,12 +102,11 @@ pub fn run_agents( } } - write_multiple_packets( - &mut connection, + connection.send_packets_enum( to_send .iter_mut() .flat_map(mem::take) - .chain([Packet::InitComplete]), + .chain([InitComplete {}.into()]), )?; // Main loop, broadcast packet to all of the bots, then wait for all of the outgoing vecs @@ -129,7 +118,7 @@ pub fn run_agents( let packet = Arc::new(packet); match &*packet { - Packet::None => { + CoreMessage::DisconnectSignal(_) => { for (incoming_sender, _) in &threads { if incoming_sender.send(packet.clone()).is_err() { return Err(AgentError::AgentPanic); @@ -138,15 +127,15 @@ pub fn run_agents( break 'main_loop; } - Packet::MatchComm(_) => { + CoreMessage::MatchComm(_) => { for (incoming_sender, _) in &threads { if incoming_sender.send(packet.clone()).is_err() { return Err(AgentError::AgentPanic); } } } - Packet::BallPrediction(_) => ball_prediction = Some(packet), - Packet::GamePacket(_) => game_packet = Some(packet), + CoreMessage::BallPrediction(_) => ball_prediction = Some(packet), + CoreMessage::GamePacket(_) => game_packet = Some(packet), _ => panic!("Unexpected packet: {packet:?}"), } } @@ -175,7 +164,7 @@ pub fn run_agents( } } - write_multiple_packets(&mut connection, to_send.iter_mut().flat_map(mem::take))?; + connection.send_packets_enum(to_send.iter_mut().flat_map(mem::take))?; } } @@ -186,13 +175,13 @@ pub fn run_agents( Ok(()) } -fn run_agent( - incoming_recver: kanal::Receiver>, +fn run_bot_agent( + incoming_recver: kanal::Receiver>, team: u32, controllable_info: ControllableInfo, match_configuration: Arc, field_info: Arc, - outgoing_sender: kanal::Sender>, + outgoing_sender: kanal::Sender>, ) { let mut outgoing_queue_local = PacketQueue::default(); let mut bot = T::new( @@ -213,18 +202,18 @@ fn run_agent( }; match &*packet { - Packet::None => break, - Packet::GamePacket(x) => bot.tick(x, &mut outgoing_queue_local), - Packet::MatchComm(x) => { + CoreMessage::DisconnectSignal(_) => break, + CoreMessage::GamePacket(x) => bot.tick(x, &mut outgoing_queue_local), + CoreMessage::MatchComm(x) => { bot.on_match_comm(x, &mut outgoing_queue_local); } - Packet::BallPrediction(x) => { + CoreMessage::BallPrediction(x) => { bot.on_ball_prediction(x); } _ => unreachable!(), /* The rest of the packets are only client -> server */ } - if matches!(*packet, Packet::GamePacket(_)) { + if matches!(*packet, CoreMessage::GamePacket(_)) { outgoing_sender .send(outgoing_queue_local.empty()) .expect("Couldn't send outgoing"); diff --git a/rlbot/src/hivemind.rs b/rlbot/src/agents/hivemind.rs similarity index 58% rename from rlbot/src/hivemind.rs rename to rlbot/src/agents/hivemind.rs index c224531..a12f08b 100644 --- a/rlbot/src/hivemind.rs +++ b/rlbot/src/agents/hivemind.rs @@ -1,15 +1,14 @@ use rlbot_flat::flat::{ - BallPrediction, ConnectionSettings, ControllableTeamInfo, FieldInfo, GamePacket, MatchComm, - MatchConfiguration, + BallPrediction, ConnectionSettings, ControllableTeamInfo, CoreMessage, FieldInfo, GamePacket, + InitComplete, MatchComm, MatchConfiguration, }; -use crate::{ - Packet, RLBotConnection, StartingInfo, - util::{PacketQueue, write_multiple_packets}, -}; +use crate::{RLBotConnection, StartingInfo, util::PacketQueue}; + +use super::AgentError; #[allow(unused_variables)] -pub trait Hivemind { +pub trait HivemindAgent { fn new( controllable_team_info: ControllableTeamInfo, match_configuration: MatchConfiguration, @@ -21,20 +20,12 @@ pub trait Hivemind { fn on_ball_prediction(&mut self, ball_prediction: BallPrediction) {} } -#[derive(thiserror::Error, Debug)] -pub enum HivemindError { - #[error("Hivemind panicked")] - HivemindPanic, - #[error("RLBot failed")] - PacketParseError(#[from] crate::RLBotError), -} - -pub fn run_hivemind( +pub fn run_hivemind_agent( agent_id: String, wants_ball_predictions: bool, wants_comms: bool, mut connection: RLBotConnection, -) -> Result<(), HivemindError> { +) -> Result<(), AgentError> { connection.send_packet(ConnectionSettings { agent_id, wants_ball_predictions, @@ -56,8 +47,8 @@ pub fn run_hivemind( &mut outgoing_queue, ); - outgoing_queue.push(Packet::InitComplete); - write_multiple_packets(&mut connection, outgoing_queue.empty().into_iter())?; + outgoing_queue.push(InitComplete {}); + connection.send_packets_enum(outgoing_queue.empty().into_iter())?; let mut ball_prediction = None; let mut game_packet = None; @@ -65,12 +56,12 @@ pub fn run_hivemind( connection.set_nonblocking(true)?; while let Ok(packet) = connection.recv_packet() { match packet { - Packet::None => break 'main_loop, - Packet::MatchComm(match_comm) => { - hivemind.on_match_comm(match_comm, &mut outgoing_queue); + CoreMessage::DisconnectSignal(_) => break 'main_loop, + CoreMessage::MatchComm(match_comm) => { + hivemind.on_match_comm(*match_comm, &mut outgoing_queue); } - Packet::BallPrediction(ball_pred) => ball_prediction = Some(ball_pred), - Packet::GamePacket(gp) => game_packet = Some(gp), + CoreMessage::BallPrediction(ball_pred) => ball_prediction = Some(ball_pred), + CoreMessage::GamePacket(gp) => game_packet = Some(gp), _ => panic!("Unexpected packet: {packet:?}"), } } @@ -78,12 +69,12 @@ pub fn run_hivemind( if let Some(game_packet) = game_packet.take() { if let Some(ball_prediction) = ball_prediction.take() { - hivemind.on_ball_prediction(ball_prediction); + hivemind.on_ball_prediction(*ball_prediction); } - hivemind.tick(game_packet, &mut outgoing_queue); + hivemind.tick(*game_packet, &mut outgoing_queue); - write_multiple_packets(&mut connection, outgoing_queue.empty().into_iter())?; + connection.send_packets_enum(outgoing_queue.empty().into_iter())?; } } diff --git a/rlbot/src/agents/mod.rs b/rlbot/src/agents/mod.rs new file mode 100644 index 0000000..6fb5c31 --- /dev/null +++ b/rlbot/src/agents/mod.rs @@ -0,0 +1,17 @@ +mod bot; +mod hivemind; +mod script; + +pub use { + bot::{BotAgent, run_bot_agents}, + hivemind::{HivemindAgent, run_hivemind_agent}, + script::{ScriptAgent, run_script_agent}, +}; + +#[derive(thiserror::Error, Debug)] +pub enum AgentError { + #[error("Agent panicked")] + AgentPanic, + #[error("RLBot failed")] + PacketParseError(#[from] crate::RLBotError), +} diff --git a/rlbot/src/scripts.rs b/rlbot/src/agents/script.rs similarity index 59% rename from rlbot/src/scripts.rs rename to rlbot/src/agents/script.rs index eb6d210..1c42127 100644 --- a/rlbot/src/scripts.rs +++ b/rlbot/src/agents/script.rs @@ -1,14 +1,14 @@ use rlbot_flat::flat::{ - BallPrediction, ConnectionSettings, FieldInfo, GamePacket, MatchComm, MatchConfiguration, + BallPrediction, ConnectionSettings, CoreMessage, FieldInfo, GamePacket, InitComplete, + MatchComm, MatchConfiguration, }; -use crate::{ - Packet, RLBotConnection, StartingInfo, - util::{PacketQueue, write_multiple_packets}, -}; +use crate::{RLBotConnection, StartingInfo, util::PacketQueue}; + +use super::AgentError; #[allow(unused_variables)] -pub trait Script { +pub trait ScriptAgent { fn new( agent_id: String, match_configuration: MatchConfiguration, @@ -20,20 +20,12 @@ pub trait Script { fn on_ball_prediction(&mut self, ball_prediction: BallPrediction) {} } -#[derive(thiserror::Error, Debug)] -pub enum ScriptError { - #[error("Script panicked")] - ScriptPanic, - #[error("RLBot failed")] - PacketParseError(#[from] crate::RLBotError), -} - -pub fn run_script( +pub fn run_script_agent( agent_id: String, wants_ball_predictions: bool, wants_comms: bool, mut connection: RLBotConnection, -) -> Result<(), ScriptError> { +) -> Result<(), AgentError> { connection.send_packet(ConnectionSettings { agent_id: agent_id.clone(), wants_ball_predictions, @@ -55,8 +47,8 @@ pub fn run_script( &mut outgoing_queue, ); - outgoing_queue.push(Packet::InitComplete); - write_multiple_packets(&mut connection, outgoing_queue.empty().into_iter())?; + outgoing_queue.push(InitComplete {}); + connection.send_packets_enum(outgoing_queue.empty().into_iter())?; let mut ball_prediction = None; let mut game_packet = None; @@ -64,12 +56,12 @@ pub fn run_script( connection.set_nonblocking(true)?; while let Ok(packet) = connection.recv_packet() { match packet { - Packet::None => break 'main_loop, - Packet::MatchComm(match_comm) => { - script.on_match_comm(match_comm, &mut outgoing_queue); + CoreMessage::DisconnectSignal(_) => break 'main_loop, + CoreMessage::MatchComm(match_comm) => { + script.on_match_comm(*match_comm, &mut outgoing_queue); } - Packet::BallPrediction(ball_pred) => ball_prediction = Some(ball_pred), - Packet::GamePacket(gp) => game_packet = Some(gp), + CoreMessage::BallPrediction(ball_pred) => ball_prediction = Some(ball_pred), + CoreMessage::GamePacket(gp) => game_packet = Some(gp), _ => panic!("Unexpected packet: {packet:?}"), } } @@ -77,12 +69,12 @@ pub fn run_script( if let Some(game_packet) = game_packet.take() { if let Some(ball_prediction) = ball_prediction.take() { - script.on_ball_prediction(ball_prediction); + script.on_ball_prediction(*ball_prediction); } - script.tick(game_packet, &mut outgoing_queue); + script.tick(*game_packet, &mut outgoing_queue); - write_multiple_packets(&mut connection, outgoing_queue.empty().into_iter())?; + connection.send_packets_enum(outgoing_queue.empty().into_iter())?; } } diff --git a/rlbot/src/lib.rs b/rlbot/src/lib.rs index f8d1993..a3bd586 100644 --- a/rlbot/src/lib.rs +++ b/rlbot/src/lib.rs @@ -8,21 +8,22 @@ use rlbot_flat::planus::{self, ReadAsRoot}; use thiserror::Error; pub mod agents; -pub mod hivemind; -pub mod scripts; +pub mod render; +pub mod state_builder; pub mod util; #[cfg(feature = "glam")] pub use rlbot_flat::glam; -pub use rlbot_flat::flat; +pub mod flat { + pub use rlbot_flat::RLBOT_FLATBUFFERS_SCHEMA_REV; + pub use rlbot_flat::flat::*; +} use flat::*; #[derive(Error, Debug)] pub enum PacketParseError { - #[error("Invalid data type: {0}")] - InvalidDataType(u16), #[error("Unpacking flatbuffer failed")] InvalidFlatbuffer(#[from] planus::Error), } @@ -33,153 +34,26 @@ pub enum RLBotError { Connection(#[from] std::io::Error), #[error("Parsing packet failed")] PacketParseError(#[from] PacketParseError), + #[error("Building packet failed")] + PacketBuildError(#[from] PacketBuildError), #[error("Invalid address, cannot parse")] InvalidAddrError(#[from] AddrParseError), } -#[allow(dead_code)] #[derive(Debug, Clone)] -pub enum Packet { - None, - GamePacket(GamePacket), - FieldInfo(FieldInfo), - StartCommand(StartCommand), - MatchConfiguration(MatchConfiguration), - PlayerInput(PlayerInput), - DesiredGameState(DesiredGameState), - RenderGroup(RenderGroup), - RemoveRenderGroup(RemoveRenderGroup), - MatchComm(MatchComm), - BallPrediction(BallPrediction), - ConnectionSettings(ConnectionSettings), - StopCommand(StopCommand), - SetLoadout(SetLoadout), - InitComplete, - ControllableTeamInfo(ControllableTeamInfo), +enum GenericMessage { + InterfaceMessage(InterfaceMessage), + CoreMessage(CoreMessage), } -macro_rules! gen_impl_from_flat_packet { - ($($x:ident),+) => { - $( - impl From<$x> for Packet { - fn from(x: $x) -> Self { - Packet::$x(x) - } - } - )+ - }; -} - -gen_impl_from_flat_packet!( - // None - GamePacket, - FieldInfo, - StartCommand, - MatchConfiguration, - PlayerInput, - DesiredGameState, - RenderGroup, - RemoveRenderGroup, - MatchComm, - BallPrediction, - ConnectionSettings, - StopCommand, - SetLoadout, - // InitComplete - ControllableTeamInfo -); - -impl Packet { - #[must_use] - pub const fn data_type(&self) -> u16 { - match *self { - Self::None => 0, - Self::GamePacket(_) => 1, - Self::FieldInfo(_) => 2, - Self::StartCommand(_) => 3, - Self::MatchConfiguration(_) => 4, - Self::PlayerInput(_) => 5, - Self::DesiredGameState(_) => 6, - Self::RenderGroup(_) => 7, - Self::RemoveRenderGroup(_) => 8, - Self::MatchComm(_) => 9, - Self::BallPrediction(_) => 10, - Self::ConnectionSettings(_) => 11, - Self::StopCommand(_) => 12, - Self::SetLoadout(_) => 13, - Self::InitComplete => 14, - Self::ControllableTeamInfo(_) => 15, - } - } - - pub fn build(self, builder: &mut planus::Builder) -> Vec { - // TODO: make this mess nicer - macro_rules! p { - ($($x:ident),+; $($y:ident),+) => { - match self { - $( - Self::$x => Vec::new(), - )+ - $( - Self::$y(x) => { - builder.clear(); - builder.finish(x, None).to_vec() - }, - )+ - } - }; - } - - p!( - // Empty payload: - None, InitComplete; - // Flatbuffer payload: - GamePacket, FieldInfo, StartCommand, MatchConfiguration, PlayerInput, - DesiredGameState, RenderGroup, RemoveRenderGroup, MatchComm, BallPrediction, - ConnectionSettings, StopCommand, SetLoadout, ControllableTeamInfo - ) +impl From for GenericMessage { + fn from(value: InterfaceMessage) -> Self { + GenericMessage::InterfaceMessage(value) } - - pub fn from_payload(data_type: u16, payload: &[u8]) -> Result { - // TODO: make this mess nicer - macro_rules! p { - ($e:ident) => { - Ok(Self::$e) - }; - ($e:ident, $x:ident) => { - Ok(Self::$e($x::read_as_root(payload)?.try_into().unwrap())) - }; - ($($n:literal, $($x:ident),+);+) => { - match data_type { - $( - $n => p!( - $($x),+ - ), - )+ - _ => Err(PacketParseError::InvalidDataType(data_type)), - } - }; - - } - - p!( - 0, None; - 1, GamePacket, GamePacketRef; - 2, FieldInfo, FieldInfoRef; - 3, StartCommand, StartCommandRef; - 4, MatchConfiguration, MatchConfigurationRef; - 5, PlayerInput, PlayerInputRef; - 6, DesiredGameState, DesiredGameStateRef; - 7, RenderGroup, RenderGroupRef; - 8, RemoveRenderGroup, RemoveRenderGroupRef; - 9, MatchComm, MatchCommRef; - 10, BallPrediction, BallPredictionRef; - 11, ConnectionSettings, ConnectionSettingsRef; - 12, StopCommand, StopCommandRef; - 13, SetLoadout, SetLoadoutRef; - 14, InitComplete; - 15, ControllableTeamInfo, ControllableTeamInfoRef - ) +} +impl From for GenericMessage { + fn from(value: CoreMessage) -> Self { + GenericMessage::CoreMessage(value) } } @@ -196,41 +70,51 @@ pub struct RLBotConnection { } impl RLBotConnection { - fn send_packet_enum(&mut self, packet: Packet) -> Result<(), RLBotError> { - let data_type_bin = packet.data_type().to_be_bytes().to_vec(); - let payload = packet.build(&mut self.builder); - let data_len_bin = u16::try_from(payload.len()) - .expect("Payload can't be greater than a u16") - .to_be_bytes() - .to_vec(); - - // Join so we make sure everything gets written in the right order - let joined = [data_type_bin, data_len_bin, payload].concat(); - - self.stream.write_all(&joined)?; + pub(crate) fn send_packets_enum( + &mut self, + packets: impl Iterator, + ) -> Result<(), RLBotError> { + let to_write = packets + // convert Packet to Vec that RLBotServer can understand + .flat_map(|x| { + build_packet_payload(GenericMessage::from(x), &mut self.builder) + .expect("failed to build packet") + }) + .collect::>(); + + self.stream.write_all(&to_write)?; self.stream.flush()?; + Ok(()) } - pub fn send_packet(&mut self, packet: impl Into) -> Result<(), RLBotError> { + fn send_packet_enum(&mut self, packet: InterfaceMessage) -> Result<(), RLBotError> { + self.stream + .write_all(&build_packet_payload(packet, &mut self.builder)?)?; + self.stream.flush()?; + Ok(()) + } + + pub fn send_packet(&mut self, packet: impl Into) -> Result<(), RLBotError> { self.send_packet_enum(packet.into()) } - pub fn recv_packet(&mut self) -> Result { - let mut buf = [0u8; 4]; + pub fn recv_packet(&mut self) -> Result { + let mut buf = [0u8; 2]; self.stream.read_exact(&mut buf)?; - let data_type = u16::from_be_bytes([buf[0], buf[1]]); - let data_len = u16::from_be_bytes([buf[2], buf[3]]); + let data_len = u16::from_be_bytes(buf); let buf = &mut self.recv_buf[0..data_len as usize]; self.stream.read_exact(buf)?; - let packet = Packet::from_payload(data_type, buf)?; + let packet_ref: CorePacketRef = + CorePacketRef::read_as_root(buf).map_err(PacketParseError::InvalidFlatbuffer)?; + let packet: CorePacket = packet_ref.try_into().unwrap(); - Ok(packet) + Ok(packet.message) } pub fn set_nonblocking(&self, nonblocking: bool) -> Result<(), RLBotError> { @@ -258,9 +142,9 @@ impl RLBotConnection { loop { let packet = self.recv_packet()?; match packet { - Packet::ControllableTeamInfo(x) => controllable_team_info = Some(x), - Packet::MatchConfiguration(x) => match_configuration = Some(x), - Packet::FieldInfo(x) => field_info = Some(x), + CoreMessage::ControllableTeamInfo(x) => controllable_team_info = Some(x), + CoreMessage::MatchConfiguration(x) => match_configuration = Some(x), + CoreMessage::FieldInfo(x) => field_info = Some(x), _ => {} } @@ -273,9 +157,37 @@ impl RLBotConnection { } Ok(StartingInfo { - controllable_team_info: controllable_team_info.unwrap(), - match_configuration: match_configuration.unwrap(), - field_info: field_info.unwrap(), + controllable_team_info: *controllable_team_info.unwrap(), + match_configuration: *match_configuration.unwrap(), + field_info: *field_info.unwrap(), }) } } + +#[derive(Error, Debug)] +pub enum PacketBuildError { + #[error("Payload too large {0}, couldn't fit in u16")] + PayloadTooLarge(usize), +} + +fn build_packet_payload( + packet: impl Into, + builder: &mut planus::Builder, +) -> Result, PacketBuildError> { + builder.clear(); + let payload = match packet.into() { + GenericMessage::InterfaceMessage(x) => { + let packet: InterfacePacket = x.into(); + builder.finish(packet, None) + } + GenericMessage::CoreMessage(x) => { + let packet: CorePacket = x.into(); + builder.finish(packet, None) + } + }; + let data_len_bin = u16::try_from(payload.len()) + .map_err(|_| PacketBuildError::PayloadTooLarge(payload.len()))? + .to_be_bytes() + .to_vec(); + Ok([data_len_bin, payload.to_vec()].concat()) +} diff --git a/rlbot/src/render.rs b/rlbot/src/render.rs new file mode 100644 index 0000000..f2833df --- /dev/null +++ b/rlbot/src/render.rs @@ -0,0 +1,208 @@ +use rlbot_flat::flat::{ + Color, Line3D, PolyLine3D, Rect2D, Rect3D, RenderAnchor, RenderGroup, RenderMessage, String2D, + String3D, TextHAlign, TextVAlign, Vector3, +}; + +#[rustfmt::skip] +pub mod colors { + use rlbot_flat::flat::Color; + pub const TRANSPARENT: Color = Color { r: 0, g: 0, b: 0, a: 0 }; + pub const BLACK: Color = Color { r: 0, g: 0, b: 0, a: 255 }; + pub const WHITE: Color = Color { r: 255, g: 255, b: 255, a: 255 }; + pub const RED: Color = Color { r: 255, g: 0, b: 0, a: 255 }; + pub const GREEN: Color = Color { r: 0, g: 128, b: 0, a: 255 }; + pub const BLUE: Color = Color { r: 0, g: 0, b: 255, a: 255 }; + pub const LIME: Color = Color { r: 0, g: 255, b: 0, a: 255 }; + pub const YELLOW: Color = Color { r: 255, g: 255, b: 0, a: 255 }; + pub const ORANGE: Color = Color { r: 255, g: 128, b: 0, a: 255 }; + pub const CYAN: Color = Color { r: 0, g: 255, b: 255, a: 255 }; + pub const PINK: Color = Color { r: 255, g: 0, b: 255, a: 255 }; + pub const PURPLE: Color = Color { r: 128, g: 0, b: 128, a: 255 }; + pub const TEAL: Color = Color { r: 0, g: 128, b: 128, a: 255 }; +} + +/// The Renderer allows of easy construction of [RenderGroup]s for in-game debug rendering. +/// When done, call [build] and queue the resulting [RenderGroup] in the packet queue. +/// +/// Example: +/// ```ignore +/// use rlbot::render::{Renderer}; +/// use rlbot::render::colors::{BLUE, GREEN, RED}; +/// let mut draw = Renderer::new(0); +/// draw.line_3d(car.pos, car.pos + car.forward() * 120., RED); +/// draw.line_3d(car.pos, car.pos + car.rightward() * 120., GREEN); +/// draw.line_3d(car.pos, car.pos + car.upward() * 120., BLUE); +/// packet_queue.push(draw.build()); +/// ``` +pub struct Renderer { + pub group: RenderGroup, +} + +impl Renderer { + /// Create a new Renderer. + /// Each render group must have a unique id. + /// Re-using an id will result in overwriting (watch out when using hiveminds). + pub fn new(group_id: i32) -> Self { + Self { + group: RenderGroup { + render_messages: vec![], + id: group_id, + }, + } + } + + /// Get the resulting [RenderGroup]. + pub fn build(self) -> RenderGroup { + self.group + } + + /// Add a [RenderMessage] to this group. + pub fn push(&mut self, message: impl Into) { + self.group.render_messages.push(message.into()); + } + + /// Draws a line between two anchors in 3d space. + pub fn line_3d( + &mut self, + start: impl Into, + end: impl Into, + color: Color, + ) { + self.group.render_messages.push( + Line3D { + start: Box::new(start.into()), + end: Box::new(end.into()), + color, + } + .into(), + ); + } + + /// Draws a line going through each of the provided points. + pub fn polyline_3d( + &mut self, + points: impl IntoIterator>, + color: Color, + ) { + self.group.render_messages.push( + PolyLine3D { + points: points.into_iter().map(|p| p.into()).collect(), + color, + } + .into(), + ); + } + + /// Draws text in 2d space. + /// X and y uses screen-space coordinates, i.e. 0.1 is 10% of the screen width/height. + /// Use `set_resolution` to change to pixel coordinates. + /// Characters of the font are 20 pixels tall and 10 pixels wide when `scale == 1.0`. + /// Consider using [push] and `..default()` when using multiple default values. + pub fn string_2d( + &mut self, + text: String, + x: f32, + y: f32, + scale: f32, + foreground: Color, + background: Color, + h_align: TextHAlign, + v_align: TextVAlign, + ) { + self.group.render_messages.push( + String2D { + text, + x, + y, + scale, + foreground, + background, + h_align, + v_align, + } + .into(), + ); + } + + /// Draws text anchored in 3d space. + /// Characters of the font are 20 pixels tall and 10 pixels wide when `scale == 1.0`. + /// Consider using [push] and `..default()` when using multiple default values. + pub fn string_3d( + &mut self, + text: String, + anchor: impl Into, + scale: f32, + foreground: Color, + background: Color, + h_align: TextHAlign, + v_align: TextVAlign, + ) { + self.group.render_messages.push( + String3D { + text, + anchor: Box::new(anchor.into()), + scale, + foreground, + background, + h_align, + v_align, + } + .into(), + ); + } + + /// Draws a rectangle anchored in 2d space. + /// X, y, width, and height uses screen-space coordinates, i.e. 0.1 is 10% of the screen width/height. + /// Use `set_resolution` to change to pixel coordinates. + /// Consider using [push] and `..default()` when using multiple default values. + pub fn rect_2d( + &mut self, + x: f32, + y: f32, + width: f32, + height: f32, + color: Color, + h_align: TextHAlign, + v_align: TextVAlign, + ) { + self.group.render_messages.push( + Rect2D { + x, + y, + width, + height, + color, + h_align, + v_align, + } + .into(), + ); + } + + /// Draws a rectangle anchored in 3d space. + /// Width and height are screen-space sizes, i.e. 0.1 is 10% of the screen width/height. + /// Use `set_resolution` to change to pixel coordinates. + /// The size does not change based on distance to the camera. + /// Consider using [push] and `..default()` when using multiple default values. + pub fn rect_3d( + &mut self, + anchor: impl Into, + width: f32, + height: f32, + color: Color, + h_align: TextHAlign, + v_align: TextVAlign, + ) { + self.group.render_messages.push( + Rect3D { + anchor: Box::new(anchor.into()), + width, + height, + color, + h_align, + v_align, + } + .into(), + ); + } +} diff --git a/rlbot/src/state_builder.rs b/rlbot/src/state_builder.rs new file mode 100644 index 0000000..6937ad8 --- /dev/null +++ b/rlbot/src/state_builder.rs @@ -0,0 +1,244 @@ +use rlbot_flat::flat::{ + DesiredBallState, DesiredCarState, DesiredGameState, DesiredMatchInfo, DesiredPhysics, Rotator, + RotatorPartial, Vector3, Vector3Partial, +}; + +/// Extension methods for easy construction of [DesiredGameState]. +/// +/// Example: +/// ```rust +/// use rlbot::state_builder::{DesiredCarStateExt, DesiredGameStateExt, DesiredPhysicsExt}; +/// use rlbot::flat::{DesiredGameState, Vector3}; +/// let mut dgs = DesiredGameState::default(); +/// dgs.mod_car(0, |c| { +/// c.set_location(Vector3::default()); +/// c.set_boost(100.); +/// }); +/// dgs.mod_balls((0..5).map(|i| (i, |b| { +/// b.set_location_z(0.); +/// b.set_velocity_z(0.); +/// }))); +/// ``` +pub trait DesiredGameStateExt { + fn mod_match_info(&mut self, build: impl FnOnce(&mut DesiredMatchInfo)); + + fn mod_car(&mut self, index: usize, build: impl FnOnce(&mut DesiredCarState)); + + fn mod_cars(&mut self, build: impl IntoIterator); + + fn mod_ball(&mut self, index: usize, build: impl FnOnce(&mut DesiredBallState)); + + fn mod_balls( + &mut self, + build: impl IntoIterator, + ); +} + +#[allow(dead_code)] +impl DesiredGameStateExt for DesiredGameState { + /// Modify the desired match info. + fn mod_match_info(&mut self, build: impl FnOnce(&mut DesiredMatchInfo)) { + build(self.match_info.get_or_insert_default()); + } + + /// Modify the desired car at the given index. + fn mod_car(&mut self, index: usize, build: impl FnOnce(&mut DesiredCarState)) { + if self.car_states.len() <= index { + self.car_states.resize(index + 1, Default::default()); + } + build(&mut self.car_states[index]); + } + + /// Modify all desired cars. + fn mod_cars( + &mut self, + build: impl IntoIterator, + ) { + for (i, func) in build { + if self.car_states.len() <= i { + self.car_states.resize(i + 1, Default::default()); + } + func(&mut self.car_states[i]); + } + } + + /// Modify the desired ball at the given index. + fn mod_ball(&mut self, index: usize, build: impl FnOnce(&mut DesiredBallState)) { + if self.ball_states.len() <= index { + self.ball_states.resize(index + 1, Default::default()); + } + build(&mut self.ball_states[index]); + } + + /// Modify all desired balls. + fn mod_balls( + &mut self, + build: impl IntoIterator, + ) { + for (i, func) in build { + if self.ball_states.len() <= i { + self.ball_states.resize(i + 1, Default::default()); + } + func(&mut self.ball_states[i]); + } + } +} + +/// Extension methods for easy construction of a [DesiredMatchInfo]. +pub trait DesiredMatchInfoExt { + fn set_gravity_z(&mut self, gravity: f32); + fn set_game_speed(&mut self, speed: f32); +} + +#[allow(dead_code)] +impl DesiredMatchInfoExt for DesiredMatchInfo { + /// Set the desired world gravity z. + fn set_gravity_z(&mut self, gravity_z: f32) { + self.world_gravity_z.get_or_insert_default().val = gravity_z; + } + + /// Set the desired game speed. + fn set_game_speed(&mut self, speed: f32) { + self.game_speed.get_or_insert_default().val = speed; + } +} + +/// Extension methods for easy construction of a [DesiredCarState]. +pub trait DesiredCarStateExt { + fn set_boost(&mut self, amount: f32); + fn mod_physics(&mut self, build: impl FnOnce(&mut DesiredPhysics)); +} + +impl DesiredCarStateExt for DesiredCarState { + /// Set the boost amount of this car. + fn set_boost(&mut self, amount: f32) { + self.boost_amount.get_or_insert_default().val = amount; + } + + /// Modify the physics of this car. + fn mod_physics(&mut self, build: impl FnOnce(&mut DesiredPhysics)) { + build(self.physics.get_or_insert_default()); + } +} + +/// Extension methods for easy construction of a [DesiredBallState]. +pub trait DesiredBallStateExt { + fn mod_physics(&mut self, build: impl FnOnce(&mut DesiredPhysics)); +} + +impl DesiredBallStateExt for DesiredBallState { + /// Modify the physics of this ball. + fn mod_physics(&mut self, build: impl FnOnce(&mut DesiredPhysics)) { + build(&mut self.physics) + } +} + +/// Extension methods for easy construction of [DesiredPhysics]. +pub trait DesiredPhysicsExt { + fn set_location(&mut self, loc: impl Into); + fn set_location_x(&mut self, x: f32); + fn set_location_y(&mut self, y: f32); + fn set_location_z(&mut self, z: f32); + fn set_velocity(&mut self, vel: impl Into); + fn set_velocity_x(&mut self, x: f32); + fn set_velocity_y(&mut self, y: f32); + fn set_velocity_z(&mut self, z: f32); + fn set_rotation(&mut self, rot: impl Into); + fn set_rotation_pitch(&mut self, pitch: f32); + fn set_rotation_yaw(&mut self, yaw: f32); + fn set_rotation_roll(&mut self, roll: f32); + fn set_angular_velocity(&mut self, ang_vel: impl Into); + fn set_angular_velocity_x(&mut self, x: f32); + fn set_angular_velocity_y(&mut self, y: f32); + fn set_angular_velocity_z(&mut self, z: f32); +} + +macro_rules! physics_path { + ( $self:ident slf ) => { + $self + }; + ( $self:ident physics) => { + $self.physics + }; + ( $self:ident optional_physics) => { + $self.physics.get_or_insert_default() + }; +} + +macro_rules! desired_physics_ext { + ( $t:ty; $p:ident ) => { + impl DesiredPhysicsExt for $t { + fn set_location(&mut self, loc: impl Into) { + let loc: Vector3Partial = loc.into().into(); + physics_path!(self $p).location.replace(loc.into()); + } + + fn set_location_x(&mut self, x: f32) { + physics_path!(self $p).location.get_or_insert_default().x.get_or_insert_default().val = x; + } + + fn set_location_y(&mut self, y: f32) { + physics_path!(self $p).location.get_or_insert_default().y.get_or_insert_default().val = y; + } + + fn set_location_z(&mut self, z: f32) { + physics_path!(self $p).location.get_or_insert_default().z.get_or_insert_default().val = z; + } + + fn set_velocity(&mut self, vel: impl Into) { + let vel: Vector3Partial = vel.into().into(); + physics_path!(self $p).velocity.replace(vel.into()); + } + + fn set_velocity_x(&mut self, x: f32) { + physics_path!(self $p).velocity.get_or_insert_default().x.get_or_insert_default().val = x; + } + + fn set_velocity_y(&mut self, y: f32) { + physics_path!(self $p).velocity.get_or_insert_default().y.get_or_insert_default().val = y; + } + + fn set_velocity_z(&mut self, z: f32) { + physics_path!(self $p).velocity.get_or_insert_default().z.get_or_insert_default().val = z; + } + + fn set_rotation(&mut self, rot: impl Into) { + let rot: RotatorPartial = rot.into().into(); + physics_path!(self $p).rotation.replace(rot.into()); + } + + fn set_rotation_pitch(&mut self, pitch: f32) { + physics_path!(self $p).rotation.get_or_insert_default().pitch.get_or_insert_default().val = pitch; + } + + fn set_rotation_yaw(&mut self, yaw: f32) { + physics_path!(self $p).rotation.get_or_insert_default().yaw.get_or_insert_default().val = yaw; + } + + fn set_rotation_roll(&mut self, roll: f32) { + physics_path!(self $p).rotation.get_or_insert_default().roll.get_or_insert_default().val = roll; + } + + fn set_angular_velocity(&mut self, ang_vel: impl Into) { + let ang_vel: Vector3Partial = ang_vel.into().into(); + physics_path!(self $p).angular_velocity.replace(ang_vel.into()); + } + + fn set_angular_velocity_x(&mut self, x: f32) { + physics_path!(self $p).angular_velocity.get_or_insert_default().x.get_or_insert_default().val = x; + } + + fn set_angular_velocity_y(&mut self, y: f32) { + physics_path!(self $p).angular_velocity.get_or_insert_default().y.get_or_insert_default().val = y; + } + + fn set_angular_velocity_z(&mut self, z: f32) { + physics_path!(self $p).angular_velocity.get_or_insert_default().z.get_or_insert_default().val = z; + } + } + }; +} + +desired_physics_ext!(DesiredPhysics; slf); +desired_physics_ext!(DesiredBallState; physics); +desired_physics_ext!(DesiredCarState; optional_physics); diff --git a/rlbot/src/util.rs b/rlbot/src/util.rs index e147b55..167f21e 100644 --- a/rlbot/src/util.rs +++ b/rlbot/src/util.rs @@ -1,15 +1,15 @@ -use std::{env, io::Write, mem}; +use std::{env, mem}; -use crate::{Packet, RLBotConnection, RLBotError}; +use rlbot_flat::flat::InterfaceMessage; -pub struct RLBotEnvironment { +pub struct AgentEnvironment { /// Will fallback to 127.0.0.1:23234 pub server_addr: String, /// No fallback and therefor Option<> pub agent_id: Option, } -impl RLBotEnvironment { +impl AgentEnvironment { // Reads from environment variables RLBOT_SERVER_ADDR/(RLBOT_SERVER_IP & RLBOT_SERVER_PORT) and RLBOT_AGENT_ID #[must_use] pub fn from_env() -> Self { @@ -32,7 +32,7 @@ impl RLBotEnvironment { /// A queue of packets to be sent to RLBotServer pub struct PacketQueue { - internal_queue: Vec, + internal_queue: Vec, } impl Default for PacketQueue { @@ -49,35 +49,11 @@ impl PacketQueue { } } - pub fn push(&mut self, packet: impl Into) { + pub fn push(&mut self, packet: impl Into) { self.internal_queue.push(packet.into()); } - pub(crate) fn empty(&mut self) -> Vec { + pub(crate) fn empty(&mut self) -> Vec { mem::take(&mut self.internal_queue) } } - -pub(crate) fn write_multiple_packets( - connection: &mut RLBotConnection, - packets: impl Iterator, -) -> Result<(), RLBotError> { - let to_write = packets - // convert Packet to Vec that RLBotServer can understand - .flat_map(|x| { - let data_type_bin = x.data_type().to_be_bytes().to_vec(); - let payload = x.build(&mut connection.builder); - let data_len_bin = u16::try_from(payload.len()) - .expect("Payload can't be greater than a u16") - .to_be_bytes() - .to_vec(); - - [data_type_bin, data_len_bin, payload].concat() - }) - .collect::>(); - - connection.stream.write_all(&to_write)?; - connection.stream.flush()?; - - Ok(()) -} diff --git a/rlbot_flat/Cargo.toml b/rlbot_flat/Cargo.toml index c487dc5..2af30f5 100644 --- a/rlbot_flat/Cargo.toml +++ b/rlbot_flat/Cargo.toml @@ -12,7 +12,9 @@ glam = { version = "0.30.0", optional = true } [build-dependencies] planus-translation = { git = "https://github.com/swz-git/planus", rev = "a0b1fbf" } planus-codegen = { git = "https://github.com/swz-git/planus", rev = "a0b1fbf" } +planus-types = { git = "https://github.com/swz-git/planus", rev = "a0b1fbf" } eyre = "0.6.12" +fluent-uri = "0.3.2" [features] default = ["glam"] diff --git a/rlbot_flat/codegen/mod.rs b/rlbot_flat/codegen/mod.rs index c9d87fb..71b2c2b 100644 --- a/rlbot_flat/codegen/mod.rs +++ b/rlbot_flat/codegen/mod.rs @@ -1,4 +1,9 @@ -use eyre::{Context, anyhow}; +use core::slice; +use eyre::{ContextCompat, anyhow}; +use planus_types::{ + ast::Docstrings, + intermediate::{AbsolutePath, Declaration, DeclarationKind, Declarations}, +}; use std::{ fs, io::Write, @@ -6,13 +11,13 @@ use std::{ time::Instant, }; -const SCHEMA_DIR: &str = "../flatbuffers-schema"; +const SCHEMA_DIR: &str = "../flatbuffers-schema/schema"; const OUT_FILE: &str = "./src/planus_flat.rs"; fn get_git_rev(dir: impl AsRef) -> Option { let output = std::process::Command::new("git") .current_dir(dir) - .args(&["rev-parse", "--short", "HEAD"]) + .args(["rev-parse", "--short", "HEAD"]) .output() .ok()?; @@ -26,25 +31,59 @@ pub fn main() -> eyre::Result<()> { Err(anyhow!("Couldn't find flatbuffers schema folder"))?; } - let cleaned_files = read_and_clean_schema_files(SCHEMA_DIR)?; - - let declarations = planus_translation::translate_files_from_memory_with_options( - &cleaned_files, - Default::default(), - ); + let rlbot_fbs_path = PathBuf::from(SCHEMA_DIR).join("rlbot.fbs"); + let mut declarations = planus_translation::translate_files(&[rlbot_fbs_path.as_path()]) + .context("planus translation failed")?; + + // Replace all links in docstrings with + for docstring in docstrings_deep_iter_mut(&mut declarations) { + let mut start = 0; + while start < docstring.len() { + let end = { + let (mut ch_iter, mut i_iter) = (docstring[start..].chars(), start..); + loop { + let next = ch_iter.next(); + if let Some(ch) = next + && !ch.is_whitespace() + { + i_iter.next(); + continue; + } + break i_iter.next().unwrap(); + } + }; + if let Ok(uri) = fluent_uri::Uri::parse(&docstring[start..end]) + && (uri.scheme().as_str() == "http" || uri.scheme().as_str() == "https") + { + docstring.insert(start, '<'); + docstring.insert(end + 1, '>'); + } + start = end + 1; + } + } let generated_planus = // No idea why planus renames RLBot to RlBot but this fixes it planus_codegen::generate_rust(&declarations)?.replace("RlBot", "RLBot"); + let generated_custom = generate_custom(declarations.declarations.iter().filter(|x| { + x.0.0 + .last() + .map(|s| s.ends_with("InterfaceMessage") || s.ends_with("CoreMessage")) + .unwrap_or(false) + }))?; + let now = Instant::now(); - let time_taken = format!( - "// @generated by build.rs, took {:?}\n// built from schema {}\n", + let header = format!( + "// @generated by build.rs, took {:?}\n\ + pub const RLBOT_FLATBUFFERS_SCHEMA_REV: &str = \"{}\";\n", now.duration_since(start_time), get_git_rev(SCHEMA_DIR).unwrap_or_else(|| "UNKNOWN".into()) ); let raw_out = &[ - time_taken.as_bytes(), + header.as_bytes(), + "////////// CUSTOM GENERATED //////////\n".as_bytes(), + generated_custom.as_bytes(), "////////// PLANUS GENERATED //////////\n".as_bytes(), generated_planus.as_bytes(), ] @@ -55,45 +94,74 @@ pub fn main() -> eyre::Result<()> { Ok(()) } -fn read_and_clean_schema_files( - schema_dir: impl AsRef, -) -> eyre::Result> { - let fbs_file_paths: Vec<_> = fs::read_dir(schema_dir) - .context("failed to read schema dir")? - .map(|x| x.unwrap().path()) - .filter(|x| x.is_file() && x.extension().map(|x| x.to_str()) == Some(Some("fbs"))) - .collect(); - - let include_all_str: String = fbs_file_paths - .iter() - // File paths to names - .map(|x| x.file_name().unwrap().to_str().unwrap().to_owned()) - // Include every file name - .map(|x| format!("include \"{x}\";")) - .collect(); - - Ok(fbs_file_paths - .into_iter() - .map(|fbs_file_path| { - let mut contents = fs::read_to_string(&fbs_file_path).expect("failed to read file"); - - // planus doesn't support multiple root_types - // removing them doesn't seem to do much - contents = contents.replace("root_type", "// root_type"); - - // comment all existing includes - contents = contents.replace("include \"", "// include \""); - - // include all files (since we're removing root_types the root_types aren't auto-included) - contents = include_all_str.clone() + &contents; - - ( - fbs_file_path - .strip_prefix(SCHEMA_DIR) - .expect("failed to strip SCHEMA_DIR prefix from file path") - .to_owned(), - contents, - ) +fn docstrings_iter_mut(d: &mut Docstrings) -> impl Iterator { + d.docstrings.iter_mut().map(|x| &mut x.value) +} + +/// Returns an iterator over all docstrings in declarations +fn docstrings_deep_iter_mut(declarations: &mut Declarations) -> impl Iterator { + // Top 10 most beautiful code OAT. + declarations.declarations.iter_mut().flat_map(|(_, decl)| { + docstrings_iter_mut(&mut decl.docstrings).chain({ + let it: Box> = match &mut decl.kind { + DeclarationKind::Table(t) => Box::new( + t.fields + .iter_mut() + .flat_map(|(_, field)| docstrings_iter_mut(&mut field.docstrings)), + ), + DeclarationKind::Struct(s) => Box::new( + s.fields + .iter_mut() + .flat_map(|(_, field)| docstrings_iter_mut(&mut field.docstrings)), + ), + DeclarationKind::Enum(e) => Box::new( + e.variants + .iter_mut() + .flat_map(|(_, variant)| docstrings_iter_mut(&mut variant.docstrings)), + ), + DeclarationKind::Union(u) => Box::new( + u.variants + .iter_mut() + .flat_map(|(_, variant)| docstrings_iter_mut(&mut variant.docstrings)), + ), + DeclarationKind::RpcService(_) => unimplemented!("RpcService"), + }; + it }) - .collect()) + }) +} + +/// Generate From for enum types. +fn generate_custom<'a>( + enum_decls: impl IntoIterator, +) -> eyre::Result { + let mut output = String::new(); + for (decl_path, decl) in enum_decls { + let DeclarationKind::Union(u) = &decl.kind else { + return Err(eyre::eyre!("DeclarationKind wasn't union")); + }; + output.push_str(&format!( + "// impl From for {}\n", + decl_path.0.join("::") + )); + + for variant in u.variants.keys() { + let from_t = [ + &decl_path.0[..decl_path.0.len() - 1], + slice::from_ref(variant), + ] + .concat() + .join("::"); + let for_t = decl_path.0.join("::"); + #[rustfmt::skip] + output.push_str(&format!( + "impl From<{from_t}> for {for_t} {{\ + fn from(value: {from_t}) -> Self {{\ + Self::{variant}(::std::boxed::Box::new(value))\ + }}\ + }}\n", // /*{decl_path:#?}*/\n/*{decl:#?}*/ + )); + } + } + Ok(output) } diff --git a/rlbot_flat/src/glam_compat.rs b/rlbot_flat/src/glam_compat.rs new file mode 100644 index 0000000..f05ec44 --- /dev/null +++ b/rlbot_flat/src/glam_compat.rs @@ -0,0 +1,52 @@ +pub use super::flat; +pub use glam; + +impl From for glam::Vec3 { + fn from(value: flat::Vector3) -> Self { + Self::new(value.x, value.y, value.z) + } +} + +impl From for glam::Vec3A { + fn from(value: flat::Vector3) -> Self { + Self::new(value.x, value.y, value.z) + } +} + +impl From for flat::Vector3 { + fn from(value: glam::Vec3) -> Self { + Self { + x: value.x, + y: value.y, + z: value.z, + } + } +} + +impl From for flat::Vector3 { + fn from(value: glam::Vec3A) -> Self { + Self { + x: value.x, + y: value.y, + z: value.z, + } + } +} + +impl From for flat::RenderAnchor { + fn from(value: glam::Vec3) -> Self { + Self { + world: value.into(), + relative: None, + } + } +} + +impl From for flat::RenderAnchor { + fn from(value: glam::Vec3A) -> Self { + Self { + world: value.into(), + relative: None, + } + } +} diff --git a/rlbot_flat/src/lib.rs b/rlbot_flat/src/lib.rs index 273e147..d9ded84 100644 --- a/rlbot_flat/src/lib.rs +++ b/rlbot_flat/src/lib.rs @@ -1,8 +1,13 @@ -#[allow(clippy::all, dead_code)] pub(crate) mod planus_flat; pub use planus; +pub use planus_flat::RLBOT_FLATBUFFERS_SCHEMA_REV; pub use planus_flat::rlbot::flat; +#[cfg(feature = "glam")] +mod glam_compat; +#[cfg(feature = "glam")] +pub use glam_compat::*; + impl From for flat::Float { fn from(value: f32) -> Self { Self { val: value } @@ -40,41 +45,60 @@ impl From for flat::DesiredPhysics { } } -#[cfg(feature = "glam")] -pub use glam; - -#[cfg(feature = "glam")] -impl From for glam::Vec3 { - fn from(value: flat::Vector3) -> Self { - Self::new(value.x, value.y, value.z) +impl From for flat::InterfacePacket { + fn from(message: flat::InterfaceMessage) -> Self { + flat::InterfacePacket { message } } } -#[cfg(feature = "glam")] -impl From for glam::Vec3A { - fn from(value: flat::Vector3) -> Self { - Self::new(value.x, value.y, value.z) +impl From for flat::CorePacket { + fn from(message: flat::CoreMessage) -> Self { + flat::CorePacket { message } } } -#[cfg(feature = "glam")] -impl From for flat::Vector3 { - fn from(value: glam::Vec3) -> Self { +macro_rules! from_render_message { + ( $( $t:ident ),* ) => { + $( + impl From for flat::RenderMessage { + fn from(value: flat::$t) -> Self { + Self { + variety: flat::RenderType::$t(Box::new(value)), + } + } + } + )* + }; +} + +from_render_message!(Line3D, PolyLine3D, String2D, String3D, Rect2D, Rect3D); + +impl From for flat::RenderAnchor { + fn from(value: flat::Vector3) -> Self { Self { - x: value.x, - y: value.y, - z: value.z, + world: value, + relative: None, } } } -#[cfg(feature = "glam")] -impl From for flat::Vector3 { - fn from(value: glam::Vec3A) -> Self { +impl From for flat::RenderAnchor { + fn from(value: flat::RelativeAnchor) -> Self { Self { - x: value.x, - y: value.y, - z: value.z, + world: flat::Vector3::default(), + relative: Some(value), } } } + +impl From for flat::RelativeAnchor { + fn from(value: flat::CarAnchor) -> Self { + flat::RelativeAnchor::CarAnchor(Box::new(value)) + } +} + +impl From for flat::RelativeAnchor { + fn from(value: flat::BallAnchor) -> Self { + flat::RelativeAnchor::BallAnchor(Box::new(value)) + } +}