Skip to content

Commit

Permalink
Added experimental headful mode
Browse files Browse the repository at this point in the history
  • Loading branch information
Bytekeeper committed Mar 12, 2022
1 parent 916da74 commit 242420a
Show file tree
Hide file tree
Showing 11 changed files with 454 additions and 232 deletions.
2 changes: 1 addition & 1 deletion build.bat
Original file line number Diff line number Diff line change
@@ -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
13 changes: 10 additions & 3 deletions game.toml
Original file line number Diff line number Diff line change
@@ -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
# 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
58 changes: 58 additions & 0 deletions src/botsetup.rs
Original file line number Diff line number Diff line change
@@ -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<Self> {
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<Self> {
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<Command>;
}
85 changes: 85 additions & 0 deletions src/bwapi.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::Race;
use shared_memory::*;
use std::io::Write;
use std::mem::size_of;

#[repr(C)]
Expand Down Expand Up @@ -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<String>,
// 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")
}
}
83 changes: 83 additions & 0 deletions src/bwheadless.rs
Original file line number Diff line number Diff line change
@@ -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<Command> {
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)
}
}
4 changes: 4 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ pub struct Cli {
map: Option<String>,
#[clap(subcommand)]
game_type: Option<GameType>,
#[clap(short, long)]
human_speed: bool,
}

pub enum Error {
Expand All @@ -48,6 +50,7 @@ impl TryFrom<Cli> for GameConfig {
name: name.to_string(),
player_name: None,
race: None,
headful: false,
})
.collect(),
),
Expand All @@ -57,6 +60,7 @@ impl TryFrom<Cli> for GameConfig {
game_name: None,
game_type,
human_host: matches!(cli.game_type.unwrap(), GameType::Human { .. }),
human_speed: cli.human_speed,
})
}
}
Expand Down
89 changes: 89 additions & 0 deletions src/injectory.rs
Original file line number Diff line number Diff line change
@@ -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<Command> {
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)
}
}
Loading

0 comments on commit 242420a

Please sign in to comment.