Skip to content

Commit

Permalink
Mostly LF3 support, also some code for sandboxing and TM support
Browse files Browse the repository at this point in the history
  • Loading branch information
Bytekeeper committed Apr 15, 2022
1 parent 5b3927a commit 8977a2e
Show file tree
Hide file tree
Showing 10 changed files with 122 additions and 22 deletions.
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@ Utility to quickly setup Starcraft Broodwar matches between 2 *or more* bots
Be aware that all bots will be executed directly, without any layer of isolation. If you need a more secure environment, either use sc-docker or setup BWAIShotgun in a VM.

# Usage
Have an installation of StarCraft Broodwar 1.16 (or get it [here](http://www.cs.mun.ca/~dchurchill/startcraft/scbw_bwapi440.zip)).


## Setup `BWAIshotgun`
Download the latest release of `bwaishotgun.z7` and unpack it.
Since Virus Scanners might interfere with download, it is password protected with the password `shotgun`.
The `bwheadless` file inside might trigger your Virus Scanner directly or indirectly (when bwaishotgun is started).

I built the binary using a [fork](https://github.com/Bytekeeper/bwheadless) of the origin [bwheadless](https://github.com/tscmoo/bwheadless),
feel free to check the code. It certainly does fishy things, which is to be expected as it heavily modifies StarCraft to run without UI etc.
I also modified it to run with "normal" game speed (LF3) - because most bots expect that.

## Setup the Game
Have an installation of StarCraft Broodwar 1.16 (or get it [here](http://www.cs.mun.ca/~dchurchill/startcraft/scbw_bwapi440.zip)).

Copy the `SNP_DirectIP.snp` (Local PC network inside the game) - the modified version of BWAIshotgun allows for 8 bots to play in a single game.

## Configure BWAIshotgun
Edit the `shotgun.toml` file. Many newer Java bots should run with any odd Java you have installed.
In that case, just leave the java setting open, and try the version on the `PATH`.

Expand All @@ -28,6 +31,13 @@ Place the `BWAPI.dll` inside, and the bot binary inside the `bwapi-data\AI` fold
To setup a game, edit the `game.toml` file. Add the absolute path of the map you want, and setup the bots.
The description of the `game_type` variable should be sufficient.

## Setup a sandbox
Ladders like SSCAIT and BASIL are using virtualization solutions.
You might want to protect your computer from malicious code in bots as well.
Consider setting up a sandbox (like [Sandboxie](https://sandboxie-plus.com/)) or a virtual machine.

## Running BWAIshotgun

Finally, run `bwaishotgun.exe` - it should show some info output of bots being started.
There is currently no timeout mechanism.
If the game does not stop after a few minutes, kill it and check the `logs` folder inside each bot folder for errors.
Expand Down
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 tools NP_DirectIP.snp game.toml shotgun.toml
releng\7za a -pshotgun -- bwaishotgun.7z target/release/bwaishotgun.exe bots tools SNP_DirectIP.snp game.toml shotgun.toml
releng\7za rn -pshotgun -- bwaishotgun.7z target/release/bwaishotgun.exe BWAIShotgun.exe
7 changes: 6 additions & 1 deletion src/botsetup.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::GameConfig;
use anyhow::bail;
use std::fs::read_dir;
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -54,5 +55,9 @@ impl Binary {
}

pub trait LaunchBuilder {
fn build_command(&self, bot_binary: &Binary) -> anyhow::Result<Command>;
fn build_command(
&self,
bot_binary: &Binary,
game_config: &GameConfig,
) -> anyhow::Result<Command>;
}
4 changes: 4 additions & 0 deletions src/bwapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ impl Default for AutoMenu {
#[derive(Default)]
pub struct BwapiIni {
pub ai_module: Option<String>,
pub tm_module: Option<String>,
// default: 0 - full throttle
pub game_speed: i32,
pub auto_menu: AutoMenu,
Expand All @@ -89,6 +90,9 @@ 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(""))?;
if let Some(tm) = &self.tm_module {
writeln!(out, "tournament = {}", tm)?;
}
writeln!(out, "[auto_menu]")?;
match &self.auto_menu {
AutoMenu::Unused => (),
Expand Down
12 changes: 9 additions & 3 deletions src/bwheadless.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::botsetup::LaunchBuilder;
use crate::{tools_folder, Binary, BwapiIni, Race};
use crate::{tools_folder, Binary, BwapiIni, GameConfig, Race, SandboxMode};
use anyhow::{anyhow, ensure};
use std::fs::File;
use std::path::PathBuf;
Expand All @@ -18,10 +18,15 @@ pub struct BwHeadless {
pub race: Race,
pub game_name: Option<String>,
pub connect_mode: BwHeadlessConnectMode,
pub sandbox: SandboxMode,
}

impl LaunchBuilder for BwHeadless {
fn build_command(&self, bot_binary: &Binary) -> anyhow::Result<Command> {
fn build_command(
&self,
bot_binary: &Binary,
game_config: &GameConfig,
) -> anyhow::Result<Command> {
ensure!(
self.starcraft_exe.exists(),
"Could not find 'StarCraft.exe'"
Expand Down Expand Up @@ -56,7 +61,7 @@ impl LaunchBuilder for BwHeadless {
}
.write(&mut bwapi_ini_file)?;

let mut cmd = Command::new(bwheadless);
let mut cmd = self.sandbox.wrap_executable(bwheadless);
cmd.arg("-e").arg(&self.starcraft_exe);
if let Some(game_name) = &self.game_name {
cmd.arg("-g").arg(game_name);
Expand All @@ -65,6 +70,7 @@ impl LaunchBuilder for BwHeadless {
cmd.arg("-l").arg(bwapi_dll);
cmd.arg("--installpath").arg(&self.bot_base_path);
cmd.arg("-n").arg(&self.bot_name);
cmd.arg("-gs").arg(game_config.latency_frames.to_string());
// 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);
Expand Down
1 change: 1 addition & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ impl TryFrom<Cli> for GameConfig {
game_type,
human_host: matches!(cli.game_type.unwrap(), GameType::Human { .. }),
human_speed: cli.human_speed,
latency_frames: 3,
})
}
}
Expand Down
12 changes: 9 additions & 3 deletions src/injectory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::process::Command;
use anyhow::ensure;

use crate::botsetup::{Binary, LaunchBuilder};
use crate::{tools_folder, AutoMenu, BwapiConnectMode, BwapiIni, Race};
use crate::{tools_folder, AutoMenu, BwapiConnectMode, BwapiIni, GameConfig, Race, SandboxMode};

pub enum InjectoryConnectMode {
Host { map: String, player_count: usize },
Expand All @@ -23,10 +23,15 @@ pub struct Injectory {
pub connect_mode: InjectoryConnectMode,
pub wmode: bool,
pub game_speed: i32,
pub sandbox: SandboxMode,
}

impl LaunchBuilder for Injectory {
fn build_command(&self, bot_binary: &Binary) -> anyhow::Result<Command> {
fn build_command(
&self,
bot_binary: &Binary,
_game_config: &GameConfig,
) -> anyhow::Result<Command> {
ensure!(
self.starcraft_exe.exists(),
"Could not find 'StarCraft.exe'"
Expand Down Expand Up @@ -74,6 +79,7 @@ impl LaunchBuilder for Injectory {
},
},
game_speed: self.game_speed,
tm_module: None,
}
.write(&mut bwapi_ini_file)?;

Expand All @@ -93,7 +99,7 @@ impl LaunchBuilder for Injectory {
}
*/

let mut cmd = Command::new(injectory);
let mut cmd = self.sandbox.wrap_executable(injectory);
cmd.arg("-l").arg(&self.starcraft_exe);
cmd.arg("-i").arg(bwapi_dll);
if self.wmode {
Expand Down
68 changes: 57 additions & 11 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod bwapi;
mod bwheadless;
mod cli;
mod injectory;
mod sandbox;

use anyhow::{anyhow, bail};
use clap::Parser;
Expand All @@ -11,7 +12,7 @@ use std::error::Error;
use std::fmt::{Debug, Display, Formatter};
use std::fs::{create_dir_all, metadata, read, File};
use std::path::{Path, PathBuf};
use std::process::{Child, Command};
use std::process::Child;
use std::time::Duration;

use crate::botsetup::{Binary, LaunchBuilder};
Expand All @@ -25,10 +26,29 @@ use retry::{retry, OperationResult};
use serde::de::Unexpected;
use serde::{Deserialize, Deserializer};

#[derive(Deserialize, Clone, Debug)]
pub enum SandboxMode {
Unconfigured,
NoSandbox,
Sandboxie {
executable: PathBuf,
box_name: String,
},
}

impl Default for SandboxMode {
fn default() -> Self {
// Should be Unconfigured if we ever support bot sandboxing
SandboxMode::NoSandbox
}
}

#[derive(Deserialize, Debug, Default)]
struct ShotgunConfig {
starcraft_path: Option<String>,
java_path: Option<String>,
#[serde(default)]
sandbox: SandboxMode,
}

impl ShotgunConfig {
Expand Down Expand Up @@ -68,6 +88,12 @@ pub struct GameConfig {
pub human_host: bool,
#[serde(default)]
pub human_speed: bool,
#[serde(default = "default_latency")]
pub latency_frames: u32,
}

fn default_latency() -> u32 {
3
}

impl GameConfig {
Expand Down Expand Up @@ -217,7 +243,13 @@ impl PreparedBot {
let bot_binary = definition
.executable
.as_deref()
.and_then(|s| Binary::from_path(path.join(s).as_path()))
.and_then(|s| {
// First try from bot path
Binary::from_path(path.join(s).as_path())
// Then from base path
.or_else(|| Binary::from_path(base_folder().join(s).as_path()))
})
// Lastly search
.unwrap_or_else(|| {
Binary::search(ai_module_path.as_path())
.expect("Could not find bot binary in 'bwapi-data/AI'")
Expand Down Expand Up @@ -250,6 +282,19 @@ fn main() -> anyhow::Result<()> {
ShotgunConfig::default()
});

if matches!(
config.sandbox,
SandboxMode::Unconfigured | SandboxMode::NoSandbox
) {
// Currently, we don't support bot sandboxing
// println!("You're running bots without a sandbox.");
if let SandboxMode::Unconfigured = config.sandbox {
eprintln!("If you are sure you don't want use a sandbox, please edit 'shotgun.toml' and set the sandbox to 'NoSandbox'.");
eprintln!("Will wait for 15 seconds (press ctrl+c to abort now, or wait and start the bots anyways).");
std::thread::sleep(Duration::from_secs(15));
}
}

let cli = Cli::parse();

let game_config: Result<GameConfig, cli::Error> = cli.try_into();
Expand Down Expand Up @@ -288,7 +333,7 @@ fn main() -> anyhow::Result<()> {
}

match game_config.game_type {
GameType::Melee(bots) => {
GameType::Melee(ref bots) => {
let bots: Vec<_> = bots
.iter()
.map(|cfg| {
Expand Down Expand Up @@ -375,6 +420,7 @@ fn main() -> anyhow::Result<()> {
},
wmode: true,
game_speed: if game_config.human_speed { -1 } else { 0 },
sandbox: config.sandbox.clone(),
})
} else {
Box::new(BwHeadless {
Expand All @@ -395,6 +441,7 @@ fn main() -> anyhow::Result<()> {
} else {
BwHeadlessConnectMode::Join
},
sandbox: config.sandbox.clone(),
})
};
if host {
Expand All @@ -410,26 +457,25 @@ fn main() -> anyhow::Result<()> {
} else {
0
};
let mut cmd = bwapi_launcher.build_command(&bot.binary)?;
let mut cmd = bwapi_launcher.build_command(&bot.binary, &game_config)?;
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?)");

let bot_out_log = File::create(bot.log_dir.join("bot_out.log"))
.expect("Could not create bot output log");
let bot_err_log = File::create(bot.log_dir.join("bot_err.log"))
.expect("Could not create bot error log");
let bot_out_log = File::create(bot.log_dir.join("bot_out.log"))?;
let bot_err_log = File::create(bot.log_dir.join("bot_err.log"))?;
let bot_process = match bot.binary {
Binary::Dll(_) => None,
Binary::Jar(jar) => {
let mut cmd =
Command::new(config.java_path.as_deref().unwrap_or("java.exe"));
let mut cmd = config
.sandbox
.wrap_executable(config.java_path.as_deref().unwrap_or("java.exe"));
cmd.arg("-jar").arg(jar);
Some(cmd)
}
Binary::Exe(exe) => Some(Command::new(exe)),
Binary::Exe(exe) => Some(config.sandbox.wrap_executable(exe)),
}
.map(|ref mut cmd| -> anyhow::Result<Child> {
cmd.current_dir(bot.working_dir);
Expand Down
22 changes: 22 additions & 0 deletions src/sandbox.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use crate::SandboxMode;
use std::ffi::OsStr;
use std::process::Command;

impl SandboxMode {
pub fn wrap_executable(&self, exe: impl AsRef<OsStr>) -> Command {
match self {
SandboxMode::Sandboxie {
executable,
box_name,
} => {
let mut cmd = Command::new(executable);
cmd.arg("/wait");
cmd.arg("/silent");
cmd.arg(format!("/box:{}", box_name));
cmd.arg(exe);
cmd
}
_ => Command::new(exe),
}
}
}
Binary file modified tools/bwheadless.exe
Binary file not shown.

0 comments on commit 8977a2e

Please sign in to comment.