1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
//! This module contains code for interoperating with RLBot's BotManager.

use crate::{
    game::{ControllerState, GameTickPacket},
    init_with_options, InitOptions,
};
use std::{env, error::Error, path::PathBuf};

/// A bot that can run within the RLBot framework. Instances of `Bot` are used
/// by the [`run_bot`] function.
///
/// # Example
///
/// See [`examples/bot`] for a complete example.
///
/// [`examples/bot`]: https://github.com/whatisaphone/rlbot-rust/blob/master/examples/bot/main.rs
pub trait Bot {
    /// This method is called when the bot's player index changes. The player
    /// index is the index in the
    /// [`LiveDataPacket::GameCars`](ffi::LiveDataPacket::GameCars) array which
    /// is under this `Bot`'s control. This method is guaranteed to be called
    /// before the first call to [`tick`](Bot::tick).
    fn set_player_index(&mut self, index: usize);

    /// This is called whenever there is a new game state. Your car will be
    /// controlled according to the [`PlayerInput`](ffi::PlayerInput) you
    /// return.
    fn tick(&mut self, packet: &GameTickPacket) -> ControllerState;
}

/// Runs a bot under control of the RLBot framework.
///
/// This function assumes the app was launched by the framework. It will
/// establish a connection to the framework, enter a game loop, and never
/// return.
///
/// # Errors
///
/// This function returns an error if it cannot communicate with the framework.
///
/// # Example
///
/// ```no_run
/// struct MyBot;
///
/// impl rlbot::Bot for MyBot {
///     // ...
///     # fn set_player_index(&mut self, index: usize) { unimplemented!() }
///     # fn tick(&mut self, packet: &rlbot::GameTickPacket) -> rlbot::ControllerState { unimplemented!() }
/// }
///
/// rlbot::run_bot(MyBot);
/// ```
///
/// See [`examples/bot`] for a complete example.
///
/// [`examples/bot`]: https://github.com/whatisaphone/rlbot-rust/blob/master/examples/bot/main.rs
#[allow(deprecated)]
pub fn run_bot<B: Bot>(mut bot: B) -> Result<(), Box<dyn Error>> {
    let args = parse_framework_args()
        .map_err(|_| Box::<dyn Error>::from("could not parse framework arguments"))?
        .ok_or_else(|| Box::<dyn Error>::from("not launched by framework"))?;

    let player_index = args.player_index;

    let rlbot = init_with_options(args.into())?;

    bot.set_player_index(player_index as usize);

    let mut packets = rlbot.packeteer();
    loop {
        let packet = packets.next()?;
        let input = bot.tick(&packet);
        rlbot.update_player_input(player_index, &input)?;
    }
}

/// Parse the arguments passed by the RLBot framework.
///
/// This function returns:
///
/// * `Ok(Some(args))` – if the app was launched by the framework.
/// * `Ok(None)` – if the app was *not* launched by the framework.
/// * `Err(_)` – if it appears the app was launched by the framework, but we
///   could not understand the arguments.
pub fn parse_framework_args() -> Result<Option<FrameworkArgs>, ()> {
    parse_framework_command_line(env::args().skip(1))
}

fn parse_framework_command_line(
    mut args: impl Iterator<Item = String>,
) -> Result<Option<FrameworkArgs>, ()> {
    // Currently this only needs to interoperate with one caller – RLBot Python's
    // BaseSubprocessAgent. No public interface has been committed to, so we can
    // afford to be rigid and inflexible with the parsing.

    if args.next().as_ref().map(|s| &s[..]) != Some("--rlbot-version") {
        return Ok(None); // not launched by the framework
    }
    let rlbot_version = args.next().ok_or(())?;

    if args.next().as_ref().map(|s| &s[..]) != Some("--rlbot-dll-directory") {
        return Err(());
    }
    let rlbot_dll_directory = PathBuf::from(args.next().ok_or(())?);

    if args.next().as_ref().map(|s| &s[..]) != Some("--player-index") {
        return Err(());
    }
    let player_index = args.next().ok_or(())?.parse().map_err(|_| ())?;

    Ok(Some(FrameworkArgs {
        rlbot_version,
        rlbot_dll_directory,
        player_index,
        _non_exhaustive: (),
    }))
}

/// The arguments passed by the RLBot framework.
pub struct FrameworkArgs {
    /// The version of the RLBot framework used to launch the app. This is the
    /// same as the version shown when you run this Python code:
    ///
    /// ```python
    /// import rlbot
    /// print(rlbot.__version__)
    /// ```
    pub rlbot_version: String,

    /// The directory containing `RLBot_Core_Interface.dll` and
    /// `RLBot_Injector.exe`.
    pub rlbot_dll_directory: PathBuf,

    /// The index of the player you're controlling in the
    /// [`LiveDataPacket::GameCars`](ffi::LiveDataPacket::GameCars) array.
    pub player_index: i32,

    _non_exhaustive: (),
}

impl From<FrameworkArgs> for InitOptions {
    fn from(args: FrameworkArgs) -> Self {
        Self::new().rlbot_dll_directory(args.rlbot_dll_directory)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn pfcl(ss: Vec<&str>) -> Result<Option<FrameworkArgs>, ()> {
        parse_framework_command_line(ss.into_iter().map(str::to_string))
    }

    #[test]
    fn parse_framework_args() {
        let args = pfcl(vec![
            "--rlbot-version",
            "1.8.1",
            "--rlbot-dll-directory",
            "/tmp",
            "--player-index",
            "0",
        ])
        .unwrap()
        .unwrap();
        assert_eq!(args.rlbot_version, "1.8.1");
        assert_eq!(args.rlbot_dll_directory.to_str().unwrap(), "/tmp");
        assert_eq!(args.player_index, 0);
    }

    #[test]
    fn parse_empty_command_line() {
        let args = pfcl(vec![]).unwrap();
        assert!(args.is_none());
    }

    #[test]
    fn parse_non_matching_command_line() {
        let args = pfcl(vec!["--unrelated-argument"]).unwrap();
        assert!(args.is_none());
    }

    #[test]
    fn parse_error() {
        let args = pfcl(vec!["--rlbot-version"]);
        assert!(args.is_err());

        let args = pfcl(vec!["--rlbot-version", "1.8.1"]);
        assert!(args.is_err());
    }
}