diff --git a/build.bat b/build.bat index ef8bc2a..d42bdb3 100644 --- a/build.bat +++ b/build.bat @@ -1,3 +1,3 @@ cargo build --release -releng\7za a -pshotgun -- bwaishotgun.7z target/release/bwaishotgun.exe bots SNP_DirectIP.snp bwheadless.exe game.toml shotgun.toml +releng\7za a -pshotgun -- bwaishotgun.7z target/release/bwaishotgun.exe bots tools NP_DirectIP.snp game.toml shotgun.toml releng\7za rn -pshotgun -- bwaishotgun.7z target/release/bwaishotgun.exe BWAIShotgun.exe diff --git a/game.toml b/game.toml index 8eca922..b267be8 100644 --- a/game.toml +++ b/game.toml @@ -1,10 +1,17 @@ -# Map path -map = 'C:\Something\StarCraft\map\SomeMap.scm' +# Map path - relative to Starcraft +map = 'maps\BroodWar\SomeMap.scm' # Game Type # Only Melee is supported currently, it takes a list of bots that should play and their respective name and race override # Ie. - this will run NitekatT 2 times, once it will play as protoss, the second instance will play using the bots preferred race (terran) game_type = { Melee = [{name = "NitekatT", race = "Protoss"}, {name = "NitekatT"}, {name = "MarineHell"}, {name = "ZergHell"}] } +# This one will run NiteKatT and ZergHell in a window, so you can observe +# Known bug: If the game is hosted by a headful bot, it will not be created automatically - you'll have to click 'create' +#game_type = { Melee = [{name = "NitekatT", race = "Protoss", headful = true}, {name = "MarineHell"}, {name = "ZergHell", headful = true}] } + # Want to join the fray? Uncomment this and open a game -# human_host = true \ No newline at end of file +# human_host = true + +# Only relevant, when not hosting: Uncomment to set the game speed to "fastest" instead of "as fast as possible" +# human_speed = true \ No newline at end of file diff --git a/src/botsetup.rs b/src/botsetup.rs new file mode 100644 index 0000000..16138cd --- /dev/null +++ b/src/botsetup.rs @@ -0,0 +1,58 @@ +use anyhow::bail; +use std::fs::read_dir; +use std::path::{Path, PathBuf}; +use std::process::Command; + +#[derive(Debug)] +pub enum Binary { + Dll(PathBuf), + Jar(PathBuf), + Exe(PathBuf), +} + +impl Binary { + pub(crate) fn from_path(path: &Path) -> Option { + path.extension() + .and_then(|ext| ext.to_str()) + .and_then(|ext| { + let mut ext = ext.to_string(); + ext.make_ascii_lowercase(); + let result = match ext.as_str() { + "dll" => Binary::Dll(path.to_path_buf()), + "jar" => Binary::Jar(path.to_path_buf()), + "exe" => Binary::Exe(path.to_path_buf()), + _ => return None, + }; + Some(result) + }) + } + + pub(crate) fn search(search_path: &Path) -> anyhow::Result { + let mut executable = None; + for file in read_dir(search_path)?.flatten() { + let path = file.path(); + if let Some(detected_binary) = Binary::from_path(&path) { + executable = Some(match (executable, detected_binary) { + (None, dll @ Binary::Dll(_)) | (Some(dll @ Binary::Dll(_)), Binary::Jar(_)) => { + dll + } + (None, jar @ Binary::Jar(_)) => jar, + (None, exe @ Binary::Exe(_)) + | (Some(Binary::Dll(_) | Binary::Jar(_)), exe @ Binary::Exe(_)) => exe, + _ => bail!( + "Found multiple binary candidates in '{}', please select one in 'bot.toml'", + search_path.to_string_lossy() + ), + }) + } + } + match executable { + None => bail!("No binary found in '{}'", search_path.to_string_lossy()), + Some(x) => Ok(x), + } + } +} + +pub trait LaunchBuilder { + fn build_command(&self, bot_binary: &Binary) -> anyhow::Result; +} diff --git a/src/bwapi.rs b/src/bwapi.rs index cc533c2..739de15 100644 --- a/src/bwapi.rs +++ b/src/bwapi.rs @@ -1,4 +1,6 @@ +use crate::Race; use shared_memory::*; +use std::io::Write; use std::mem::size_of; #[repr(C)] @@ -37,4 +39,87 @@ impl GameTableAccess { .as_ref() .map(|shmem| unsafe { *(shmem.as_ptr() as *const GameTable) }) } + + pub fn get_connected_client_count(&mut self) -> usize { + self.get_game_table() + .map(|table| { + table + .game_instances + .iter() + .filter(|it| it.is_connected) + .count() + }) + .unwrap_or(0) + } +} + +pub enum BwapiConnectMode { + Host { map: String, player_count: usize }, + Join, +} + +pub enum AutoMenu { + // Managed by bwheadless + Unused, + // Managed by BWAPI + injectory + AutoMenu { + name: String, + race: Race, + game_name: String, + connect_mode: BwapiConnectMode, + }, +} + +impl Default for AutoMenu { + fn default() -> Self { + Self::Unused + } +} + +/// Although BWAPI can manage multiple bots with one BWAPI.ini, we'll be using one per bot +#[derive(Default)] +pub struct BwapiIni { + pub ai_module: Option, + // default: 0 - full throttle + pub game_speed: i32, + pub auto_menu: AutoMenu, +} + +impl BwapiIni { + pub fn write(&self, out: &mut impl Write) -> std::io::Result<()> { + writeln!(out, "[ai]")?; + writeln!(out, "ai = {}", self.ai_module.as_deref().unwrap_or(""))?; + writeln!(out, "[auto_menu]")?; + match &self.auto_menu { + AutoMenu::Unused => (), + AutoMenu::AutoMenu { + name, + race, + game_name, + connect_mode, + } => { + writeln!(out, "auto_menu=LAN")?; + writeln!(out, "lan_mode=Local PC")?; + writeln!(out, "character_name={}", name)?; + writeln!(out, "race={}", race)?; + match connect_mode { + BwapiConnectMode::Host { map, player_count } => { + writeln!(out, "map={}", map)?; + writeln!(out, "wait_for_min_players={}", player_count)?; + writeln!(out, "wait_for_max_players={}", player_count)?; + } + BwapiConnectMode::Join => { + writeln!(out, "game={}", game_name)?; + } + } + } + } + writeln!( + out, + "save_replay = replays/$Y $b $d/%MAP%_%BOTRACE%%ALLYRACES%vs%ENEMYRACES%_$H$M$S.rep" + )?; + writeln!(out, "[starcraft]")?; + writeln!(out, "speed_override = {}", self.game_speed)?; + writeln!(out, "sound = OFF") + } } diff --git a/src/bwheadless.rs b/src/bwheadless.rs new file mode 100644 index 0000000..6f467be --- /dev/null +++ b/src/bwheadless.rs @@ -0,0 +1,83 @@ +use crate::botsetup::LaunchBuilder; +use crate::{tools_folder, Binary, BwapiIni, Race}; +use anyhow::{anyhow, ensure}; +use std::fs::File; +use std::path::PathBuf; +use std::process::Command; + +pub enum BwHeadlessConnectMode { + Host { map: String, player_count: usize }, + Join, +} + +pub struct BwHeadless { + pub starcraft_exe: PathBuf, + /// Folder containing bwapi-data/AI + pub bot_base_path: PathBuf, + pub bot_name: String, + pub race: Race, + pub game_name: String, + pub connect_mode: BwHeadlessConnectMode, +} + +impl LaunchBuilder for BwHeadless { + fn build_command(&self, bot_binary: &Binary) -> anyhow::Result { + ensure!( + self.starcraft_exe.exists(), + "Could not find 'StarCraft.exe'" + ); + let bwapi_data = self.bot_base_path.join("bwapi-data"); + ensure!( + bwapi_data.exists(), + "Missing '{}' - please read the instructions on how to setup a bot.", + bwapi_data.to_string_lossy() + ); + let bwapi_dll = bwapi_data.join("BWAPI.dll"); + ensure!( + bwapi_dll.exists(), + "Could not find '{}'", + bwapi_dll.to_string_lossy() + ); + + let bwheadless = tools_folder().join("bwheadless.exe"); + ensure!( + bwheadless.exists(), + r"Could not find '{}'. Please make sure to extract all files, or check your antivirus software.", + tools_folder().to_string_lossy() + ); + let bwapi_ini = bwapi_data.join("bwapi.ini"); + let mut bwapi_ini_file = File::create(&bwapi_ini)?; + BwapiIni { + ai_module: Some(match &bot_binary { + Binary::Dll(x) => x.to_string_lossy().to_string(), + Binary::Exe(_) | Binary::Jar(_) => "".to_string(), + }), + ..Default::default() + } + .write(&mut bwapi_ini_file)?; + + let mut cmd = Command::new(bwheadless); + cmd.arg("-e").arg(&self.starcraft_exe); + cmd.arg("-g").arg(&self.game_name); + cmd.arg("-r").arg(&self.race.to_string()); + cmd.arg("-l").arg(bwapi_dll); + cmd.arg("--installpath").arg(&self.bot_base_path); + cmd.arg("-n").arg(&self.bot_name); + // Newer versions of BWAPI no longer use the registry key (aka installpath) - but allow overriding the bwapi_ini location. + cmd.env("BWAPI_CONFIG_INI", &*bwapi_ini.to_string_lossy()); + cmd.current_dir(&self.bot_base_path); + let starcraft_path = self + .starcraft_exe + .parent() + .ok_or(anyhow!("Folder containing 'StarCraft.exe' not found"))?; + + match &self.connect_mode { + BwHeadlessConnectMode::Host { map, player_count } => { + cmd.arg("-m").arg(starcraft_path.join(map)); + cmd.arg("-h").arg(player_count.to_string()); + } + BwHeadlessConnectMode::Join => {} + } + Ok(cmd) + } +} diff --git a/src/cli.rs b/src/cli.rs index 015aa0b..4da202c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -22,6 +22,8 @@ pub struct Cli { map: Option, #[clap(subcommand)] game_type: Option, + #[clap(short, long)] + human_speed: bool, } pub enum Error { @@ -48,6 +50,7 @@ impl TryFrom for GameConfig { name: name.to_string(), player_name: None, race: None, + headful: false, }) .collect(), ), @@ -57,6 +60,7 @@ impl TryFrom for GameConfig { game_name: None, game_type, human_host: matches!(cli.game_type.unwrap(), GameType::Human { .. }), + human_speed: cli.human_speed, }) } } diff --git a/src/injectory.rs b/src/injectory.rs new file mode 100644 index 0000000..3975dec --- /dev/null +++ b/src/injectory.rs @@ -0,0 +1,89 @@ +use crate::botsetup::{Binary, LaunchBuilder}; +use crate::{tools_folder, AutoMenu, BwapiConnectMode, BwapiIni, Race}; +use anyhow::ensure; +use std::fs::File; +use std::path::PathBuf; +use std::process::Command; + +pub enum InjectoryConnectMode { + Host { map: String, player_count: usize }, + Join, +} + +pub struct Injectory { + pub starcraft_exe: PathBuf, + /// Folder containing bwapi-data/AI + pub bot_base_path: PathBuf, + pub player_name: String, + pub game_name: String, + pub race: Race, + pub connect_mode: InjectoryConnectMode, + pub wmode: bool, + pub game_speed: i32, +} + +impl LaunchBuilder for Injectory { + fn build_command(&self, bot_binary: &Binary) -> anyhow::Result { + ensure!( + self.starcraft_exe.exists(), + "Could not find 'StarCraft.exe'" + ); + let bwapi_data = self.bot_base_path.join("bwapi-data"); + ensure!( + bwapi_data.exists(), + "Missing '{}' - please read the instructions on how to setup a bot.", + bwapi_data.to_string_lossy() + ); + let bwapi_dll = bwapi_data.join("BWAPI.dll"); + ensure!( + bwapi_dll.exists(), + "Could not find '{}'", + bwapi_dll.to_string_lossy() + ); + let injectory = tools_folder().join("injectory_x86.exe"); + ensure!( + injectory.exists(), + r"Could not find '{}'. Please make sure to extract all files, or check your antivirus software.", + tools_folder().to_string_lossy() + ); + let bwapi_ini = bwapi_data.join("bwapi.ini"); + let mut bwapi_ini_file = File::create(&bwapi_ini)?; + BwapiIni { + ai_module: Some(match &bot_binary { + Binary::Dll(x) => x.to_string_lossy().to_string(), + Binary::Exe(_) | Binary::Jar(_) => "".to_string(), + }), + auto_menu: match &self.connect_mode { + InjectoryConnectMode::Host { map, player_count } => AutoMenu::AutoMenu { + name: self.player_name.clone(), + game_name: self.game_name.clone(), + race: self.race, + connect_mode: BwapiConnectMode::Host { + map: map.clone(), + player_count: *player_count, + }, + }, + InjectoryConnectMode::Join => AutoMenu::AutoMenu { + name: self.player_name.clone(), + game_name: self.game_name.clone(), + race: self.race, + connect_mode: BwapiConnectMode::Join, + }, + }, + game_speed: self.game_speed, + } + .write(&mut bwapi_ini_file)?; + let mut cmd = Command::new(injectory); + cmd.arg("-l").arg(&self.starcraft_exe); + cmd.arg("-i").arg(bwapi_dll); + if self.wmode { + cmd.arg(tools_folder().join("WMode.dll")); + } + cmd.arg("--wait-for-exit").arg("--kill-on-exit"); + // Newer versions of BWAPI no longer use the registry key (aka installpath) - but allow overriding the bwapi_ini location. + // Note that injectory does NOT do any registry trickery (bwheadless does) - so old bots (< 4.x) will most likely not work. + cmd.env("BWAPI_CONFIG_INI", &*bwapi_ini.to_string_lossy()); + cmd.current_dir(&self.bot_base_path); + Ok(cmd) + } +} diff --git a/src/main.rs b/src/main.rs index 898dced..1db4406 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,23 +1,27 @@ +mod botsetup; mod bwapi; +mod bwheadless; mod cli; +mod injectory; -use anyhow::bail; +use anyhow::{anyhow, bail}; use clap::Parser; use std::collections::HashSet; use std::error::Error; use std::fmt::{Debug, Display, Formatter}; -use std::fs::{create_dir_all, metadata, read, read_dir, File}; -use std::io::Write; -use std::ops::Deref; +use std::fs::{create_dir_all, metadata, read, File}; use std::path::{Path, PathBuf}; use std::process::{Child, Command}; use std::time::Duration; -use crate::bwapi::GameTableAccess; +use crate::botsetup::{Binary, LaunchBuilder}; +use crate::bwapi::{AutoMenu, BwapiConnectMode, BwapiIni, GameTableAccess}; +use crate::bwheadless::{BwHeadless, BwHeadlessConnectMode}; use crate::cli::Cli; +use crate::injectory::{Injectory, InjectoryConnectMode}; use registry::{Hive, Security}; use retry::delay::Fixed; -use retry::retry; +use retry::{retry, OperationResult}; use serde::de::Unexpected; use serde::{Deserialize, Deserializer}; @@ -46,6 +50,8 @@ pub struct BotConfig { pub name: String, pub player_name: Option, pub race: Option, + #[serde(default)] + pub headful: bool, } #[derive(Deserialize, Debug)] @@ -60,20 +66,18 @@ pub struct GameConfig { pub game_type: GameType, #[serde(default)] pub human_host: bool, + #[serde(default)] + pub human_speed: bool, } impl GameConfig { - fn load(config: &ShotgunConfig) -> Result { - let result: GameConfig = toml::from_slice( - read(base_folder().join("game.toml")) + fn load(config: &ShotgunConfig) -> anyhow::Result { + let result: GameConfig = + toml::from_slice(read(base_folder().join("game.toml"))?.as_slice()) .map_err(|e| e.to_string()) - .expect("'game.toml' not found") - .as_slice(), - ) - .map_err(|e| e.to_string()) - .expect("'game.toml' is invalid"); + .expect("'game.toml' is invalid"); if result.map.is_empty() && !result.human_host { - return Err("Map must be set for non-human hosted games".to_owned()); + bail!("Map must be set for non-human hosted games"); } let map_path_abs = Path::new(&result.map); let map_path_rel = config @@ -81,7 +85,7 @@ impl GameConfig { .expect("Could not find StarCraft installation") .join(&result.map); if map_path_abs.is_absolute() && !map_path_abs.exists() | !map_path_rel.exists() { - return Err(format!("Could not find map '{}'", result.map)); + bail!("Could not find map '{}'", result.map); } Ok(result) } @@ -134,25 +138,8 @@ impl Display for Race { } } -struct BwapiIni { - ai: String, -} - -impl BwapiIni { - fn write(&self, out: &mut impl Write) -> std::io::Result<()> { - writeln!(out, "[ai]")?; - writeln!(out, "ai = {}", self.ai)?; - writeln!(out, "[auto_menu]")?; - writeln!( - out, - "save_replay = replays/$Y $b $d/%MAP%_%BOTRACE%%ALLYRACES%vs%ENEMYRACES%_$H$M$S.rep" - )?; - writeln!(out, "[starcraft]")?; - writeln!(out, "speed_override = 0") - } -} #[derive(Debug)] -enum SGError { +pub enum SGError { MissingStarCraftExe(PathBuf), MultipleExecutables(PathBuf), ExecutableNotFound(PathBuf), @@ -184,67 +171,8 @@ impl Display for SGError { } } -struct BwHeadless { - starcraft_exe: PathBuf, -} - -impl BwHeadless { - fn new(starcraft_exe: &Path) -> Result { - if !&starcraft_exe.exists() { - return Err(SGError::MissingStarCraftExe(starcraft_exe.to_path_buf())); - } - Ok(Self { - starcraft_exe: starcraft_exe.to_path_buf(), - }) - } - - fn host_command( - &self, - map: &str, - player_count: usize, - game_name: &str, - ) -> std::io::Result { - let mut cmd = self.bwheadless_command()?; - cmd.arg("-m").arg(map); - cmd.arg("-h").arg(player_count.to_string()); - cmd.arg("-g").arg(game_name); - Ok(cmd) - } - - fn join_command(&self) -> std::io::Result { - self.bwheadless_command() - } - - fn bwheadless_command(&self) -> std::io::Result { - let mut bwheadless = base_folder(); - bwheadless.push("bwheadless.exe"); - let mut cmd = Command::new(bwheadless); - cmd.arg("-e").arg(&self.starcraft_exe); - Ok(cmd) - } - - fn add_bot_args( - cmd: &mut Command, - race: Race, - bot_name: &str, - bwapi_dll_path: &str, - bot_bwapi_path: &str, - ) { - cmd.arg("-r").arg(race.to_string()); - cmd.arg("-l").arg(bwapi_dll_path); - cmd.arg("--installpath").arg(&bot_bwapi_path); - cmd.arg("-n").arg(bot_name); - // Newer versions of BWAPI no longer use the registry key (aka installpath) - but allow overriding the bwapi_ini location - let ini_path = PathBuf::from(bot_bwapi_path).join("bwapi-data/bwapi.ini"); - cmd.env( - "BWAPI_CONFIG_INI", - ini_path.to_str().expect("Could not find bwapi.ini"), - ); - cmd.current_dir(bot_bwapi_path); - } -} - -fn base_folder() -> PathBuf { +/// bwaishotgun base folder +pub fn base_folder() -> PathBuf { std::env::current_exe() .expect("Could not find executable") .parent() @@ -252,51 +180,9 @@ fn base_folder() -> PathBuf { .to_owned() } -#[derive(Debug)] -enum Binary { - Dll(PathBuf), - Jar(PathBuf), - Exe(PathBuf), -} - -impl Binary { - fn from_path(path: &Path) -> Option { - path.extension() - .and_then(|ext| ext.to_str()) - .and_then(|ext| { - let mut ext = ext.to_string(); - ext.make_ascii_lowercase(); - let result = match ext.as_str() { - "dll" => Binary::Dll(path.to_path_buf()), - "jar" => Binary::Jar(path.to_path_buf()), - "exe" => Binary::Exe(path.to_path_buf()), - _ => return None, - }; - Some(result) - }) - } - - fn search(search_path: &Path) -> anyhow::Result { - let mut executable = None; - for file in read_dir(search_path)?.flatten() { - let path = file.path(); - if let Some(detected_binary) = Binary::from_path(&path) { - executable = Some(match (executable, detected_binary) { - (None, dll @ Binary::Dll(_)) | (Some(dll @ Binary::Dll(_)), Binary::Jar(_)) => { - dll - } - (None, jar @ Binary::Jar(_)) => jar, - (None, exe @ Binary::Exe(_)) - | (Some(Binary::Dll(_) | Binary::Jar(_)), exe @ Binary::Exe(_)) => exe, - _ => bail!(SGError::MultipleExecutables(search_path.to_path_buf())), - }) - } - } - match executable { - None => bail!(SGError::ExecutableNotFound(search_path.to_path_buf())), - Some(x) => Ok(x), - } - } +/// tools folder +pub fn tools_folder() -> PathBuf { + base_folder().join("tools") } pub struct BotProcess { @@ -309,9 +195,9 @@ pub struct PreparedBot { binary: Binary, race: Race, name: String, - bwapi_dll: PathBuf, working_dir: PathBuf, log_dir: PathBuf, + headful: bool, } impl PreparedBot { @@ -324,11 +210,9 @@ impl PreparedBot { let read_path = bwapi_data_path.join("read"); let write_path = bwapi_data_path.join("write"); let log_dir = path.join("logs"); - let bwapi_ini_path = bwapi_data_path.join("bwapi.ini"); create_dir_all(read_path).expect("Could not create read folder"); create_dir_all(write_path).expect("Could not create write folder"); create_dir_all(&log_dir).expect("Colud not create log folder"); - let mut bwapi_ini = File::create(bwapi_ini_path).expect("Could not create 'bwapi.ini'"); let bot_binary = definition .executable @@ -338,31 +222,23 @@ impl PreparedBot { Binary::search(ai_module_path.as_path()) .expect("Could not find bot binary in 'bwapi-data/AI'") }); - BwapiIni { - ai: match &bot_binary { - Binary::Dll(x) => x.to_string_lossy().to_string(), - Binary::Exe(_) | Binary::Jar(_) => "".to_string(), - }, - } - .write(&mut bwapi_ini) - .expect("Could not write 'bwapi.ini'"); - drop(bwapi_ini); + let race = config.race.unwrap_or(definition.race); Self { binary: bot_binary, - race: config.race.unwrap_or(definition.race), + race, name: config .player_name .clone() .unwrap_or_else(|| config.name.clone()), - bwapi_dll: bwapi_data_path.join("BWAPI.dll"), working_dir: path.to_path_buf(), log_dir, + headful: config.headful, } } } -fn main() { +fn main() -> anyhow::Result<()> { let config: ShotgunConfig = read(base_folder().join("shotgun.toml")) .map(|cfg| { toml::from_slice(cfg.as_slice()) @@ -389,9 +265,6 @@ fn main() { .get_starcraft_path() .expect("Could not find StarCraft installation"); let starcraft_exe = starcraft_path.join("StarCraft.exe"); - let bwheadless = BwHeadless::new(&starcraft_exe) - .map_err(|e| e.to_string()) - .expect("StarCraft.exe could not be found"); if let Ok(metadata) = metadata(starcraft_path.join("SNP_DirectIP.snp")) { if metadata.len() != 46100 { eprintln!("The 'SNP_DirectIP.snp' in your StarCraft installation might not support more than ~6 bots per game. Overwrite with the included 'SNP_DirectIP.snp' file to support more."); @@ -405,6 +278,7 @@ fn main() { .get_game_table() .iter() .flat_map(|table| table.game_instances.iter()) + .filter(|it| it.is_connected && it.server_process_id != 0) .map(|it| it.server_process_id) { eprintln!( @@ -458,59 +332,79 @@ fn main() { 0 } }); + let mut bot_names = HashSet::new(); for bot in prepared_bots.iter().map(|it| &it.name) { if !bot_names.insert(bot) { - println!("'{}' was added multiple times. All instances will use the same read/write/log folders and could fail to work properly.", bot); + println!("'{}' was added multiple times. All instances will use the same read/write/log folders and could fail to work properly. Also headful mode will not work as expected.", bot); } } let mut instances = vec![]; // If a human is going to host, no need to fire up a host - let mut host = game_config.human_host; + let mut host = !game_config.human_host; + // Game name is mutable, BWAPI can't create games with names differing from the player name in LAN + let mut game_name = game_config + .game_name + .as_deref() + .unwrap_or("shotgun") + .to_string(); for bot in prepared_bots { - let mut cmd = if !host { - host = true; - bwheadless.host_command( - &game_config.map, - player_count, - game_config.game_name.as_deref().unwrap_or("shotgun"), - ) + let bwapi_launcher: Box = if bot.headful { + if host { + // Headful + Host => All other bots need to join the game with this bots player name + game_name = bot.name.clone(); + } + Box::new(Injectory { + starcraft_exe: starcraft_exe.clone(), + bot_base_path: bot.working_dir.clone(), + player_name: bot.name.clone(), + game_name: game_name.clone(), + race: bot.race, + connect_mode: if host { + InjectoryConnectMode::Host { + map: game_config.map.clone(), + player_count, + } + } else { + InjectoryConnectMode::Join + }, + wmode: true, + game_speed: if game_config.human_speed { -1 } else { 0 }, + }) } else { - bwheadless.join_command() + Box::new(BwHeadless { + starcraft_exe: starcraft_exe.clone(), + bot_base_path: bot.working_dir.clone(), + bot_name: bot.name.clone(), + race: bot.race, + game_name: game_name.clone(), + connect_mode: if host { + BwHeadlessConnectMode::Host { + map: game_config.map.clone(), + player_count, + } + } else { + BwHeadlessConnectMode::Join + }, + }) + }; + if host { + println!("Hosting game with '{}'", bot.name); + } else { + println!("Joining game with '{}'", bot.name); } - .expect("Could not execute bwheadless"); - BwHeadless::add_bot_args( - &mut cmd, - bot.race, - &bot.name, - bot.bwapi_dll.to_string_lossy().deref(), - bot.working_dir.to_string_lossy().deref(), - ); + host = false; + let old_connected_client_count = if matches!(bot.binary, Binary::Exe(_) | Binary::Dll(_)) { - game_table_access - .get_game_table() - .map(|table| { - table - .game_instances - .iter() - .filter(|it| it.is_connected) - .count() - }) - .unwrap_or(0) + game_table_access.get_connected_client_count() } else { 0 }; - cmd.stdout( - File::create(bot.log_dir.join("game_out.log")) - .expect("Could not create game output log"), - ); - cmd.stderr( - File::create(bot.log_dir.join("game_err.log")) - .expect("Could not create game error log"), - ); - println!("Firing up {}", bot.name); - let bwheadless = cmd + let mut cmd = bwapi_launcher.build_command(&bot.binary)?; + cmd.stdout(File::create(bot.log_dir.join("game_out.log"))?) + .stderr(File::create(bot.log_dir.join("game_err.log"))?); + let mut bwapi_child = cmd .spawn() .expect("Could not run bwheadless (maybe deleted/blocked by a Virus Scanner?)"); @@ -524,65 +418,64 @@ fn main() { Binary::Jar(jar) => { let mut cmd = Command::new(config.java_path.as_deref().unwrap_or("java.exe")); - cmd.current_dir(Path::new(bot.working_dir.to_string_lossy().deref())); + cmd.current_dir(bot.working_dir); cmd.arg("-jar").arg(jar); cmd.stdout(bot_out_log); cmd.stderr(bot_err_log); - let child = cmd.spawn().expect("Could not execute bot binary"); + let mut child = cmd.spawn()?; // Wait up to 10 seconds before bailing retry(Fixed::from_millis(100).take(100), || { - let found = game_table_access.get_game_table().map(|table| { - table - .game_instances - .iter() - .filter(|game_instance| game_instance.is_connected) - .count() - > old_connected_client_count - }); - match found { - None => Err("Game table not found"), - Some(true) => Ok(()), - Some(false) => Err("Bot process not found in game table"), + let found = game_table_access.get_connected_client_count() + > old_connected_client_count; + if !matches!(bwapi_child.try_wait(), Ok(None)) { + OperationResult::Err("BWAPI process died") + } else if !matches!(child.try_wait(), Ok(None)) { + OperationResult::Err("Bot process died") + } else if found { + OperationResult::Ok(()) + } else { + OperationResult::Retry( + "Bot client executable did not connect to BWAPI server", + ) } }) - .expect("Bot failed to connect to BWAPI"); + .map_err(|e| anyhow!(e))?; bot_process = Some(child); // Give some time for } Binary::Exe(exe) => { let mut cmd = Command::new(exe); - cmd.current_dir(Path::new(bot.working_dir.to_string_lossy().deref())); + cmd.current_dir(bot.working_dir); cmd.stdout(bot_out_log); cmd.stderr(bot_err_log); - let child = cmd.spawn().expect("Could not execute bot binary"); + let mut child = cmd.spawn()?; // Wait up to 10 seconds before bailing retry(Fixed::from_millis(100).take(100), || { - let found = game_table_access.get_game_table().map(|table| { - table - .game_instances - .iter() - .filter(|game_instance| game_instance.is_connected) - .count() - > old_connected_client_count - }); - match found { - None => Err("Game table not found"), - Some(true) => Ok(()), - Some(false) => Err("Bot process not found in game table"), + let found = game_table_access.get_connected_client_count() + > old_connected_client_count; + if !matches!(bwapi_child.try_wait(), Ok(None)) { + OperationResult::Err("BWAPI process died") + } else if !matches!(child.try_wait(), Ok(None)) { + OperationResult::Err("Bot process died") + } else if found { + OperationResult::Ok(()) + } else { + OperationResult::Retry( + "Bot client executable did not connect to BWAPI server", + ) } }) - .expect("Bot failed to connect to BWAPI"); + .map_err(|e| anyhow!(e))?; bot_process = Some(child); } } - // TODO: Redirect stderr to stdout - bwheadless is very silent for a typical command prompt instances.push(BotProcess { - bwheadless, + bwheadless: bwapi_child, bot: bot_process, }); } @@ -601,10 +494,13 @@ fn main() { bot.kill().ok(); } instances.swap_remove(i); + println!("{} bots remaining", instances.len()); } } std::thread::sleep(Duration::from_secs(1)); } + println!("Done"); + Ok(()) } } } diff --git a/tools/WMode.dll b/tools/WMode.dll new file mode 100644 index 0000000..ffe2360 Binary files /dev/null and b/tools/WMode.dll differ diff --git a/bwheadless.exe b/tools/bwheadless.exe similarity index 100% rename from bwheadless.exe rename to tools/bwheadless.exe diff --git a/tools/injectory_x86.exe b/tools/injectory_x86.exe new file mode 100644 index 0000000..d73c31c Binary files /dev/null and b/tools/injectory_x86.exe differ