├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ └── rust.yml ├── .gitignore ├── .rustfmt.toml ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── debug-bot.rs ├── events.rs ├── ex_main │ └── mod.rs ├── reaper-rush.rs ├── runner.rs ├── speed-mining.rs ├── worker-rush.rs └── zerg-rush.rs ├── generate_ids.py ├── misc └── cost.md ├── sc2-macro ├── Cargo.toml ├── macro │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── src │ └── lib.rs └── src ├── action.rs ├── api.rs ├── bot.rs ├── client.rs ├── consts.rs ├── debug.rs ├── distance ├── mod.rs └── rayon.rs ├── game_data.rs ├── game_info.rs ├── game_state.rs ├── geometry.rs ├── ids ├── ability_id.rs ├── buff_id.rs ├── effect_id.rs ├── impls.rs ├── mod.rs ├── unit_typeid.rs └── upgrade_id.rs ├── lib.rs ├── paths.rs ├── pixel_map.rs ├── player.rs ├── ramp.rs ├── score.rs ├── unit.rs ├── units ├── iter.rs ├── mod.rs └── rayon.rs └── utils └── mod.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | 7 | [*.rs] 8 | indent_style = tab 9 | indent_size = 4 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, windows-latest] 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: actions-rs/toolchain@v1 21 | with: 22 | profile: minimal 23 | toolchain: stable 24 | override: true 25 | - name: Cache cargo registry 26 | uses: actions/cache@v2 27 | with: 28 | path: ~/.cargo/registry 29 | key: ${{ runner.os }}-${{ steps.rustup.outputs.rustc_hash }}-dev-registry-${{ hashFiles('**/Cargo.lock') }} 30 | - name: Cache cargo index 31 | uses: actions/cache@v2 32 | with: 33 | path: ~/.cargo/git 34 | key: ${{ runner.os }}-${{ steps.rustup.outputs.rustc_hash }}-dev-index-${{ hashFiles('**/Cargo.lock') }} 35 | - name: Cache cargo build 36 | uses: actions/cache@v2 37 | with: 38 | path: | 39 | target 40 | key: ${{ runner.os }}-${{ steps.rustup.outputs.rustc_hash }}-dev-target-${{ hashFiles('**/Cargo.lock') }} 41 | - name: Build library 42 | run: cargo build --lib --verbose 43 | # - name: Run tests 44 | # run: cargo test --verbose 45 | 46 | fmt: 47 | name: Rustfmt 48 | runs-on: ubuntu-latest 49 | steps: 50 | - uses: actions/checkout@v2 51 | - uses: actions-rs/toolchain@v1 52 | with: 53 | profile: minimal 54 | toolchain: nightly 55 | override: true 56 | - run: rustup component add rustfmt --toolchain nightly 57 | - uses: actions-rs/cargo@v1 58 | with: 59 | toolchain: nightly 60 | command: fmt 61 | args: --all -- --check 62 | 63 | clippy: 64 | name: Clippy 65 | runs-on: ubuntu-latest 66 | steps: 67 | - uses: actions/checkout@v2 68 | - uses: actions-rs/toolchain@v1 69 | with: 70 | profile: minimal 71 | toolchain: stable 72 | override: true 73 | - run: rustup component add clippy 74 | - uses: actions-rs/cargo@v1 75 | with: 76 | command: clippy 77 | args: -- -D warnings 78 | 79 | examples: 80 | name: Examples 81 | runs-on: ubuntu-latest 82 | steps: 83 | - uses: actions/checkout@v2 84 | - uses: actions-rs/toolchain@v1 85 | with: 86 | profile: minimal 87 | toolchain: stable 88 | override: true 89 | - run: rustup component add clippy 90 | - uses: actions-rs/cargo@v1 91 | with: 92 | command: clippy 93 | args: --examples -- -D warnings 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | */target 3 | */*/target 4 | Cargo.lock 5 | /.idea 6 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 110 2 | hard_tabs = true 3 | edition = "2018" 4 | ignore = ["src/lib.rs", "src/ids"] 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-sc2" 3 | version = "1.2.0" 4 | authors = ["Armageddon "] 5 | edition = "2021" 6 | description = "Rust implementation of StarCraft II API" 7 | repository = "https://github.com/UltraMachine/rust-sc2" 8 | readme = "README.md" 9 | license = "MIT" 10 | 11 | [dependencies] 12 | sc2-proto = "0.2.3" 13 | # sc2-proto = { path = "../sc2-proto-rs" } 14 | sc2-macro = { path = "sc2-macro", version = "1" } 15 | tungstenite = { version = "^0.17.0", default-features = false } 16 | protobuf = "^2.17.0" 17 | ndarray = "^0.15.1" 18 | num-traits = "^0.2.12" 19 | num-derive = "^0.3.1" 20 | itertools = "^0.10.0" 21 | lazy_static = "^1.4.0" 22 | maplit = "^1.0.2" 23 | rand = "^0.8.0" 24 | log = "^0.4.11" 25 | rustc-hash = "^1.1.0" 26 | rayon = { version = "^1.3.1", optional = true } 27 | parking_lot = { version = "^0.12.0", optional = true } 28 | indexmap = "^1.5.1" 29 | serde = { version = "^1.0.114", features = ["derive"], optional = true } 30 | lazy-init = "^0.5.0" 31 | once_cell = "^1.8.0" 32 | dirs = "^4.0.0" 33 | 34 | [target.'cfg(windows)'.dependencies] 35 | regex = "^1.3.9" 36 | 37 | [dev-dependencies] 38 | clap = { version = "4", features = ["derive"] } 39 | 40 | [features] 41 | protoc = ["sc2-proto/protoc-rust"] 42 | enemies_cache = [] 43 | wine_sc2 = [] 44 | rayon = ["dep:rayon", "indexmap/rayon", "ndarray/rayon"] 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Armageddon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | **Table of Contents** 3 | 4 | - [rust-sc2](#rust-sc2) 5 | - [Getting started](#getting-started) 6 | - [Rust](#rust) 7 | - [StarCraft II](#starcraft-ii) 8 | - [Installation](#installation) 9 | - [Windows and macOS](#windows-and-macos) 10 | - [Linux](#linux) 11 | - [Headfull (Lutris and Wine)](#headfull-lutris-and-wine) 12 | - [Headless (no graphics)](#headless-no-graphics) 13 | - [Example](#example) 14 | - [Running Example](#running-example) 15 | - [Headfull](#headfull) 16 | - [Headless](#headless) 17 | - [Runnint the advanced examples](#running-the-advanced-examples) 18 | - [Optional features](#optional-features) 19 | - [Making bot step by step](#making-bot-step-by-step) 20 | 21 | 22 | 23 | 24 | # rust-sc2 25 | [![crates.io](https://img.shields.io/crates/v/rust-sc2.svg)](https://crates.io/crates/rust-sc2) 26 | [![Documentation](https://docs.rs/rust-sc2/badge.svg)](https://docs.rs/rust-sc2) 27 | 28 | Rust implementation of StarCraft II API 29 | 30 | The library aims to be simple and easy to use, being very fast and functional at the same time. However, it provides both high and low level abstractions. This lib is inspired by [python-sc2](https://github.com/BurnySc2/python-sc2) lib, so people might find it easy to switch to rust-sc2. It was originally created because other rust libs were old, not functional and low level. 31 | 32 | Feel free to ask questions in `#rust` channel of [Starcraft 2 AI Discord](https://discord.gg/Emm5Ztz) server 33 | 34 | # Getting started 35 | ## Rust 36 | [Install latest stable Rust](https://www.rust-lang.org/tools/install) 37 | (Older versions may also work, but compatibility is not guaranteed) 38 | 39 | Create your project 40 | 41 | `cargo new ` 42 | 43 | Add to dependencies in Cargo.toml: 44 | ```toml 45 | [dependencies] 46 | rust-sc2 = "1" 47 | ``` 48 | Or if you want developer version directly from github: 49 | ```toml 50 | [dependencies] 51 | rust-sc2 = { git = "https://github.com/UltraMachine/rust-sc2" } 52 | ``` 53 | Or if you want to use a local version: 54 | ```toml 55 | [dependencies] 56 | rust-sc2 = { path = "/path/to/rust-sc2" } 57 | ``` 58 | 59 | **NOTE:** *Version of this library on crates.io is outdated and lacks many features. Unfortunately, I can't update it yet, so it's highly recommended to use github version for now.* 60 | 61 | ## StarCraft II 62 | 63 | ### Installation 64 | #### Windows and macOS 65 | 66 | Install SC2 through [Battle.net](https://www.blizzard.com/en-us/apps/battle.net/desktop). 67 | 68 | #### Linux 69 | ##### Headfull (Lutris and Wine) 70 | 71 | 1. Install Lutris from your package manager 72 | 2. [Install Battle.net dependencies](https://github.com/lutris/docs/blob/master/Battle.Net.md). (Wine and Vulkan drivers) 73 | 3. [Install SC2 through Lutris](https://lutris.net/games/starcraft-ii/) 74 | 75 | ##### Headless (no graphics) 76 | 77 | 1. Download most recent [Linux Package](https://github.com/Blizzard/s2client-proto#linux-packages) (Maps will come with the zip) 78 | 2. Unzip to ~/StarCraftII (you'll need the End User License Agreement Password above the Linux Packages link) 79 | 3. Move your `.SC2Map` files up out of their `LadderXXXXSeasonX` directory to `Maps` directory. (Since there are multiple versions of the same map, there is no way of knowing which one you want.) 80 | 81 | 82 | # Example 83 | The simplest competetive bot in less than 30 lines. Copy this into your `/path/to/project/main.rs` 84 | ```rust 85 | use rust_sc2::prelude::*; 86 | 87 | #[bot] 88 | #[derive(Default)] 89 | struct WorkerRush; 90 | impl Player for WorkerRush { 91 | fn get_player_settings(&self) -> PlayerSettings { 92 | PlayerSettings::new(Race::Protoss) 93 | } 94 | fn on_start(&mut self) -> SC2Result<()> { 95 | for worker in &self.units.my.workers { 96 | worker.attack(Target::Pos(self.enemy_start), false); 97 | } 98 | Ok(()) 99 | } 100 | } 101 | 102 | fn main() -> SC2Result<()> { 103 | let mut bot = WorkerRush::default(); 104 | run_vs_computer( 105 | &mut bot, 106 | Computer::new(Race::Random, Difficulty::Medium, None), 107 | "EternalEmpireLE", 108 | Default::default(), 109 | ) 110 | } 111 | ``` 112 | Note: The Linux client doesn't have the map `EternalEmpireLE` so you'll need to download it, or reference another map from the `LadderXXXXSeasonX` directories. 113 | 114 | ## Running Example 115 | ### Headfull 116 | 1. `export SC2PATH="/home//Games/starcraft-ii/drive_c/Program Files (x86)/StarCraft II"` 117 | 2. Make sure you have this snippet in your project's **Cargo.toml**: 118 | ```toml 119 | [features] 120 | wine_sc2 = ["rust-sc2/wine_sc2"] 121 | 122 | ``` 123 | 3. `cargo run --features wine_sc2` 124 | 125 | ### Headless 126 | 1. `export SC2PATH=/abs/path/to/StarCraftII` 127 | 2. `cargo run` 128 | 129 | ## Running the advanced examples 130 | There are more advanced examples in the [`examples`](https://github.com/UltraMachine/rust-sc2/tree/master/examples) folder. To run one of these examples on your own machine, say the Reaper Rush one, clone this repository, navigate to the root folder and run the command 131 | ``` 132 | cargo run --example reaper-rush -- local 133 | ``` 134 | In addition to `local` (or `human` if you want to play against your own bot), these examples take several arguments. For a full list in either case, run the commands 135 | ``` 136 | cargo run --example reaper-rush -- local --help 137 | cargo run --example reaper-rush -- human --help 138 | ``` 139 | 140 | 141 | ## Optional features 142 | - `"rayon"` - enables parallelism and makes all types threadsafe 143 | - `"serde"` - adds implementation of `Serialize`, `Deserialize` to ids, Race, GameResult, ... 144 | - `"wine_sc2"` - allows you to run headful SC2 through Lutris and Wine 145 | 146 | ## Making bot step by step 147 | First of all, import rust-sc2 lib: 148 | ```rust 149 | use rust_sc2::prelude::*; 150 | ``` 151 | Create your bot's struct (Can be Unit or C-like): 152 | ```rust 153 | #[bot] 154 | struct MyBot; 155 | ``` 156 | ```rust 157 | #[bot] 158 | struct MyBot { 159 | /* fields here */ 160 | } 161 | ``` 162 | Then implement `Player` trait for your bot: 163 | ```rust 164 | // You mustn't call any of these methods by hands, they're for API only 165 | impl Player for MyBot { 166 | // Must be implemented 167 | fn get_player_settings(&self) -> PlayerSettings { 168 | // Race can be Terran, Zerg, Protoss or Random 169 | PlayerSettings::new(Race::Random) 170 | } 171 | 172 | // Methods below aren't necessary to implement (Empty by default) 173 | 174 | // Called once on first step 175 | fn on_start(&mut self) -> SC2Result<()> { 176 | /* your awesome code here */ 177 | } 178 | 179 | // Called on every game step 180 | fn on_step(&mut self, iteration: usize) -> SC2Result<()> { 181 | /* your awesome code here */ 182 | } 183 | 184 | // Called once on last step 185 | // "result" says if your bot won or lost game 186 | fn on_end(&self, result: GameResult) -> SC2Result<()> { 187 | /* your awesome code here */ 188 | } 189 | 190 | // Called on different events, see more in `examples/events.rs` 191 | fn on_event(&mut self, event: Event) -> SC2Result<()> { 192 | /* your awesome code here */ 193 | } 194 | } 195 | ``` 196 | Also you might want to add method to construct it: 197 | ```rust 198 | impl MyBot { 199 | // It's necessary to have #[bot_new] here 200 | #[bot_new] 201 | fn new() -> Self { 202 | Self { 203 | /* initializing fields */ 204 | } 205 | } 206 | } 207 | ``` 208 | If your bot implements `Default` you can simply call `MyBot::default()`, but if you want more control over initializer: 209 | ```rust 210 | impl MyBot { 211 | // You don't need #[bot_new] here, because of "..Default::default()" 212 | fn new() -> Self { 213 | Self { 214 | /* initializing fields */ 215 | ..Default::default() 216 | } 217 | } 218 | } 219 | ``` 220 | The rest is to run it: 221 | ```rust 222 | fn main() -> SC2Result<()> { 223 | let mut bot = MyBot::new(); 224 | run_vs_computer( 225 | &mut bot, 226 | Computer::new( 227 | Race::Random, 228 | Difficulty::VeryEasy, 229 | None, // AI Build (random here) 230 | ), 231 | "EternalEmpireLE", // Map name 232 | LaunchOptions::default(), 233 | ) 234 | } 235 | ``` 236 | -------------------------------------------------------------------------------- /examples/debug-bot.rs: -------------------------------------------------------------------------------- 1 | use rust_sc2::{geometry::Point3, prelude::*}; 2 | 3 | mod ex_main; 4 | 5 | #[bot] 6 | #[derive(Default)] 7 | struct DebugAI; 8 | 9 | impl Player for DebugAI { 10 | fn on_step(&mut self, _iteration: usize) -> SC2Result<()> { 11 | // Debug expansion locations 12 | for exp in self.expansions.clone() { 13 | let (loc, center) = (exp.loc, exp.center); 14 | let z = self.get_z_height(loc) + 1.5; 15 | self.debug.draw_sphere(loc.to3(z), 0.6, Some((255, 128, 255))); 16 | let z = self.get_z_height(center) + 1.5; 17 | self.debug.draw_sphere(center.to3(z), 0.5, Some((255, 128, 64))); 18 | } 19 | 20 | // Debug unit types 21 | self.units 22 | .all 23 | .iter() 24 | .map(|u| (format!("{:?}", u.type_id()), u.position3d())) 25 | .collect::>() 26 | .into_iter() 27 | .for_each(|(s, pos)| self.debug.draw_text_world(&s, pos, Some((255, 128, 128)), None)); 28 | Ok(()) 29 | } 30 | 31 | fn get_player_settings(&self) -> PlayerSettings { 32 | PlayerSettings::new(self.race) 33 | } 34 | } 35 | 36 | fn main() -> SC2Result<()> { 37 | ex_main::main(DebugAI::default()) 38 | } 39 | -------------------------------------------------------------------------------- /examples/events.rs: -------------------------------------------------------------------------------- 1 | use rust_sc2::prelude::*; 2 | 3 | // Example of how to use events 4 | 5 | #[bot] 6 | #[derive(Default)] 7 | struct EmptyBot; 8 | impl Player for EmptyBot { 9 | fn get_player_settings(&self) -> PlayerSettings { 10 | PlayerSettings::new(Race::Random) 11 | } 12 | 13 | // Use it like here 14 | fn on_event(&mut self, event: Event) -> SC2Result<()> { 15 | match event { 16 | Event::UnitDestroyed(_tag, alliance) => { 17 | match alliance { 18 | Some(Alliance::Own) => { /* your code here */ } 19 | Some(Alliance::Neutral) => { /* your code here */ } 20 | // Enemy 21 | _ => { /* your code here */ } 22 | } 23 | } 24 | Event::UnitCreated(tag) => { 25 | if let Some(_u) = self.units.my.units.get(tag) { /* your code here */ } 26 | } 27 | Event::ConstructionStarted(tag) => { 28 | if let Some(_u) = self.units.my.structures.get(tag) { /* your code here */ } 29 | } 30 | Event::ConstructionComplete(tag) => { 31 | if let Some(_u) = self.units.my.structures.get(tag) { /* your code here */ } 32 | } 33 | Event::RandomRaceDetected(_race) => { /* your code here */ } 34 | } 35 | Ok(()) 36 | } 37 | } 38 | 39 | fn main() -> SC2Result<()> { 40 | let mut bot = EmptyBot::default(); 41 | 42 | run_vs_computer( 43 | &mut bot, 44 | Computer::new(Race::Random, Difficulty::VeryEasy, None), 45 | "EverDreamLE", 46 | Default::default(), 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /examples/ex_main/mod.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | use rust_sc2::bot::Bot; 3 | use rust_sc2::prelude::*; 4 | use std::ops::{Deref, DerefMut}; 5 | 6 | #[derive(Parser)] 7 | #[clap(version, author)] 8 | struct Args { 9 | #[clap(long = "LadderServer")] 10 | ladder_server: Option, 11 | #[clap(long = "OpponentId")] 12 | opponent_id: Option, 13 | #[clap(long = "GamePort")] 14 | host_port: Option, 15 | #[clap(long = "StartPort")] 16 | player_port: Option, 17 | #[clap(long = "RealTime")] 18 | realtime: bool, 19 | 20 | /// Set game step for bot 21 | #[clap(short = 's', long = "step", default_value = "2")] 22 | game_step: u32, 23 | 24 | #[clap(subcommand)] 25 | command: Option, 26 | } 27 | 28 | #[derive(Subcommand)] 29 | enum Command { 30 | /// Run local game vs Computer 31 | Local { 32 | /// Set path to map, relative to "Starcraft II/Maps" directory. 33 | /// See `https://github.com/UltraMachine/rust-sc2/blob/master/src/paths.rs#L38-L67` 34 | #[clap(short, long)] 35 | map: String, 36 | /// Set opponent race 37 | #[clap(short, long, default_value = "Random")] 38 | race: Race, 39 | /// Set opponent diffuculty 40 | #[clap(short, long)] 41 | difficulty: Option, 42 | /// Set opponent build 43 | #[clap(short = 'b', long)] 44 | ai_build: Option, 45 | /// Set sc2 version 46 | #[clap(long)] 47 | sc2_version: Option, 48 | /// Set path to save replay 49 | #[clap(long)] 50 | save_replay: Option, 51 | /// Enable realtime mode 52 | #[clap(long)] 53 | realtime: bool, 54 | }, 55 | /// Run game Human vs Bot 56 | Human { 57 | /// Set path to map, relative to "Starcraft II/Maps" directory. 58 | /// See `https://github.com/UltraMachine/rust-sc2/blob/master/src/paths.rs#L38-L67` 59 | #[clap(short, long)] 60 | map: String, 61 | /// Set human race 62 | #[clap(short, long, default_value = "Random")] 63 | race: Race, 64 | /// Set human name 65 | #[clap(short, long)] 66 | name: Option, 67 | /// Set sc2 version 68 | #[clap(long)] 69 | sc2_version: Option, 70 | /// Set path to save replay 71 | #[clap(long)] 72 | save_replay: Option, 73 | }, 74 | } 75 | 76 | pub(crate) fn main(mut bot: impl Player + DerefMut + Deref) -> SC2Result<()> { 77 | let args = Args::parse(); 78 | 79 | if args.game_step == 0 { 80 | panic!("game_step must be X >= 1") 81 | } 82 | bot.set_game_step(args.game_step); 83 | 84 | match args.command { 85 | Some(Command::Local { 86 | map, 87 | race, 88 | difficulty, 89 | ai_build, 90 | sc2_version, 91 | save_replay, 92 | realtime, 93 | }) => run_vs_computer( 94 | &mut bot, 95 | Computer::new(race, difficulty.unwrap_or(Difficulty::VeryEasy), ai_build), 96 | &map, 97 | LaunchOptions { 98 | sc2_version: sc2_version.as_deref(), 99 | realtime, 100 | save_replay_as: save_replay.as_deref(), 101 | }, 102 | ), 103 | Some(Command::Human { 104 | map, 105 | race, 106 | name, 107 | sc2_version, 108 | save_replay, 109 | }) => run_vs_human( 110 | &mut bot, 111 | PlayerSettings { 112 | race, 113 | name: name.as_deref(), 114 | ..Default::default() 115 | }, 116 | &map, 117 | LaunchOptions { 118 | sc2_version: sc2_version.as_deref(), 119 | realtime: true, 120 | save_replay_as: save_replay.as_deref(), 121 | }, 122 | ), 123 | None => run_ladder_game( 124 | &mut bot, 125 | args.ladder_server.as_deref().unwrap_or("127.0.0.1"), 126 | args.host_port.expect("GamePort must be specified"), 127 | args.player_port.expect("StartPort must be specified"), 128 | args.opponent_id.as_deref(), 129 | ), 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /examples/reaper-rush.rs: -------------------------------------------------------------------------------- 1 | use rust_sc2::prelude::*; 2 | use std::{cmp::Ordering, collections::HashSet}; 3 | 4 | mod ex_main; 5 | 6 | #[bot] 7 | #[derive(Default)] 8 | struct ReaperRushAI { 9 | reapers_retreat: HashSet, 10 | last_loop_distributed: u32, 11 | } 12 | 13 | impl Player for ReaperRushAI { 14 | fn on_start(&mut self) -> SC2Result<()> { 15 | if let Some(townhall) = self.units.my.townhalls.first() { 16 | // Setting rallypoint for command center 17 | townhall.smart(Target::Pos(self.start_center), false); 18 | 19 | // Ordering scv on initial 50 minerals 20 | townhall.train(UnitTypeId::SCV, false); 21 | self.subtract_resources(UnitTypeId::SCV, true); 22 | } 23 | 24 | // Splitting workers to closest mineral crystals 25 | for u in &self.units.my.workers { 26 | if let Some(mineral) = self.units.mineral_fields.closest(u) { 27 | u.gather(mineral.tag(), false); 28 | } 29 | } 30 | 31 | Ok(()) 32 | } 33 | 34 | fn on_step(&mut self, _iteration: usize) -> SC2Result<()> { 35 | self.distribute_workers(); 36 | self.build(); 37 | self.train(); 38 | self.execute_micro(); 39 | Ok(()) 40 | } 41 | 42 | fn get_player_settings(&self) -> PlayerSettings { 43 | PlayerSettings::new(Race::Terran).with_name("RustyReapers") 44 | } 45 | } 46 | 47 | impl ReaperRushAI { 48 | const DISTRIBUTION_DELAY: u32 = 8; 49 | 50 | fn distribute_workers(&mut self) { 51 | if self.units.my.workers.is_empty() { 52 | return; 53 | } 54 | let mut idle_workers = self.units.my.workers.idle(); 55 | 56 | // Check distribution delay if there aren't any idle workers 57 | let game_loop = self.state.observation.game_loop(); 58 | let last_loop = &mut self.last_loop_distributed; 59 | if idle_workers.is_empty() && *last_loop + Self::DISTRIBUTION_DELAY > game_loop { 60 | return; 61 | } 62 | *last_loop = game_loop; 63 | 64 | // Distribute 65 | let mineral_fields = &self.units.mineral_fields; 66 | if mineral_fields.is_empty() { 67 | return; 68 | } 69 | let bases = self.units.my.townhalls.ready(); 70 | if bases.is_empty() { 71 | return; 72 | } 73 | 74 | let mut deficit_minings = Units::new(); 75 | let mut deficit_geysers = Units::new(); 76 | 77 | // Distributing mineral workers 78 | for base in &bases { 79 | match base.assigned_harvesters().cmp(&base.ideal_harvesters()) { 80 | Ordering::Less => (0..(base.ideal_harvesters().unwrap() 81 | - base.assigned_harvesters().unwrap())) 82 | .for_each(|_| { 83 | deficit_minings.push(base.clone()); 84 | }), 85 | Ordering::Greater => { 86 | let local_minerals = mineral_fields 87 | .iter() 88 | .closer(11.0, base) 89 | .map(|m| m.tag()) 90 | .collect::>(); 91 | 92 | idle_workers.extend( 93 | self.units 94 | .my 95 | .workers 96 | .filter(|u| { 97 | u.target_tag().map_or(false, |target_tag| { 98 | local_minerals.contains(&target_tag) 99 | || (u.is_carrying_minerals() && target_tag == base.tag()) 100 | }) 101 | }) 102 | .iter() 103 | .take( 104 | (base.assigned_harvesters().unwrap() - base.ideal_harvesters().unwrap()) 105 | as usize, 106 | ) 107 | .cloned(), 108 | ); 109 | } 110 | _ => {} 111 | } 112 | } 113 | 114 | // Distributing gas workers 115 | self.units 116 | .my 117 | .gas_buildings 118 | .iter() 119 | .ready() 120 | .filter(|g| g.vespene_contents().map_or(false, |vespene| vespene > 0)) 121 | .for_each( 122 | |gas| match gas.assigned_harvesters().cmp(&gas.ideal_harvesters()) { 123 | Ordering::Less => (0..(gas.ideal_harvesters().unwrap() 124 | - gas.assigned_harvesters().unwrap())) 125 | .for_each(|_| { 126 | deficit_geysers.push(gas.clone()); 127 | }), 128 | Ordering::Greater => { 129 | idle_workers.extend( 130 | self.units 131 | .my 132 | .workers 133 | .filter(|u| { 134 | u.target_tag().map_or(false, |target_tag| { 135 | target_tag == gas.tag() 136 | || (u.is_carrying_vespene() 137 | && target_tag == bases.closest(gas).unwrap().tag()) 138 | }) 139 | }) 140 | .iter() 141 | .take( 142 | (gas.assigned_harvesters().unwrap() - gas.ideal_harvesters().unwrap()) 143 | as usize, 144 | ) 145 | .cloned(), 146 | ); 147 | } 148 | _ => {} 149 | }, 150 | ); 151 | 152 | // Distributing idle workers 153 | let minerals_near_base = if idle_workers.len() > deficit_minings.len() + deficit_geysers.len() { 154 | let minerals = mineral_fields.filter(|m| bases.iter().any(|base| base.is_closer(11.0, *m))); 155 | if minerals.is_empty() { 156 | None 157 | } else { 158 | Some(minerals) 159 | } 160 | } else { 161 | None 162 | }; 163 | 164 | for u in &idle_workers { 165 | if let Some(closest) = deficit_geysers.closest(u) { 166 | let tag = closest.tag(); 167 | deficit_geysers.remove(tag); 168 | u.gather(tag, false); 169 | } else if let Some(closest) = deficit_minings.closest(u) { 170 | u.gather( 171 | mineral_fields 172 | .closer(11.0, closest) 173 | .max(|m| m.mineral_contents().unwrap_or(0)) 174 | .unwrap() 175 | .tag(), 176 | false, 177 | ); 178 | let tag = closest.tag(); 179 | deficit_minings.remove(tag); 180 | } else if u.is_idle() { 181 | if let Some(mineral) = minerals_near_base.as_ref().and_then(|ms| ms.closest(u)) { 182 | u.gather(mineral.tag(), false); 183 | } 184 | } 185 | } 186 | } 187 | 188 | fn get_builder(&self, pos: Point2, mineral_tags: &[u64]) -> Option<&Unit> { 189 | self.units 190 | .my 191 | .workers 192 | .iter() 193 | .filter(|u| { 194 | !(u.is_constructing() 195 | || u.is_returning() || u.is_carrying_resource() 196 | || (u.is_gathering() && u.target_tag().map_or(true, |tag| !mineral_tags.contains(&tag)))) 197 | }) 198 | .closest(pos) 199 | } 200 | fn build(&mut self) { 201 | if self.minerals < 75 { 202 | return; 203 | } 204 | 205 | let mineral_tags = self 206 | .units 207 | .mineral_fields 208 | .iter() 209 | .map(|u| u.tag()) 210 | .collect::>(); 211 | let main_base = self.start_location.towards(self.game_info.map_center, 8.0); 212 | 213 | if self.counter().count(UnitTypeId::Refinery) < 2 214 | && self.counter().ordered().count(UnitTypeId::Refinery) == 0 215 | && self.can_afford(UnitTypeId::Refinery, false) 216 | { 217 | let start_location = self.start_location; 218 | if let Some(geyser) = self.find_gas_placement(start_location) { 219 | if let Some(builder) = self.get_builder(geyser.position(), &mineral_tags) { 220 | builder.build_gas(geyser.tag(), false); 221 | self.subtract_resources(UnitTypeId::Refinery, false); 222 | } 223 | } 224 | } 225 | 226 | if self.supply_left < 3 227 | && self.supply_cap < 200 228 | && self.counter().ordered().count(UnitTypeId::SupplyDepot) == 0 229 | && self.can_afford(UnitTypeId::SupplyDepot, false) 230 | { 231 | if let Some(location) = 232 | self.find_placement(UnitTypeId::SupplyDepot, main_base, Default::default()) 233 | { 234 | if let Some(builder) = self.get_builder(location, &mineral_tags) { 235 | builder.build(UnitTypeId::SupplyDepot, location, false); 236 | self.subtract_resources(UnitTypeId::SupplyDepot, false); 237 | return; 238 | } 239 | } 240 | } 241 | 242 | if self.counter().all().count(UnitTypeId::Barracks) < 4 243 | && self.can_afford(UnitTypeId::Barracks, false) 244 | { 245 | if let Some(location) = self.find_placement( 246 | UnitTypeId::Barracks, 247 | main_base, 248 | PlacementOptions { 249 | step: 4, 250 | ..Default::default() 251 | }, 252 | ) { 253 | if let Some(builder) = self.get_builder(location, &mineral_tags) { 254 | builder.build(UnitTypeId::Barracks, location, false); 255 | self.subtract_resources(UnitTypeId::Barracks, false); 256 | } 257 | } 258 | } 259 | } 260 | 261 | fn train(&mut self) { 262 | if self.minerals < 50 || self.supply_left == 0 { 263 | return; 264 | } 265 | 266 | if self.supply_workers < 22 && self.can_afford(UnitTypeId::SCV, true) { 267 | if let Some(cc) = self 268 | .units 269 | .my 270 | .townhalls 271 | .iter() 272 | .find(|u| u.is_ready() && u.is_almost_idle()) 273 | { 274 | cc.train(UnitTypeId::SCV, false); 275 | self.subtract_resources(UnitTypeId::SCV, true); 276 | } 277 | } 278 | 279 | if self.can_afford(UnitTypeId::Reaper, true) { 280 | if let Some(barracks) = self 281 | .units 282 | .my 283 | .structures 284 | .iter() 285 | .find(|u| u.type_id() == UnitTypeId::Barracks && u.is_ready() && u.is_almost_idle()) 286 | { 287 | barracks.train(UnitTypeId::Reaper, false); 288 | self.subtract_resources(UnitTypeId::Reaper, true); 289 | } 290 | } 291 | } 292 | 293 | fn throw_mine(&self, reaper: &Unit, target: &Unit) -> bool { 294 | if reaper.has_ability(AbilityId::KD8ChargeKD8Charge) 295 | && reaper.in_ability_cast_range(AbilityId::KD8ChargeKD8Charge, target, 0.0) 296 | { 297 | reaper.command( 298 | AbilityId::KD8ChargeKD8Charge, 299 | Target::Pos(target.position()), 300 | false, 301 | ); 302 | true 303 | } else { 304 | false 305 | } 306 | } 307 | fn execute_micro(&mut self) { 308 | // Lower ready depots 309 | self.units 310 | .my 311 | .structures 312 | .iter() 313 | .of_type(UnitTypeId::SupplyDepot) 314 | .ready() 315 | .for_each(|s| s.use_ability(AbilityId::MorphSupplyDepotLower, false)); 316 | 317 | // Reapers micro 318 | let reapers = self.units.my.units.of_type(UnitTypeId::Reaper); 319 | if reapers.is_empty() { 320 | return; 321 | } 322 | 323 | let targets = { 324 | let ground_targets = self.units.enemy.all.ground(); 325 | let ground_attackers = ground_targets.filter(|e| e.can_attack_ground()); 326 | if ground_attackers.is_empty() { 327 | ground_targets 328 | } else { 329 | ground_attackers 330 | } 331 | }; 332 | 333 | for u in &reapers { 334 | let is_retreating = self.reapers_retreat.contains(&u.tag()); 335 | if is_retreating { 336 | if u.health_percentage().unwrap() > 0.75 { 337 | self.reapers_retreat.remove(&u.tag()); 338 | } 339 | } else if u.health_percentage().unwrap() < 0.5 { 340 | self.reapers_retreat.insert(u.tag()); 341 | } 342 | 343 | match targets.closest(u) { 344 | Some(closest) => { 345 | if self.throw_mine(u, closest) { 346 | return; 347 | } 348 | if is_retreating || u.on_cooldown() { 349 | match targets 350 | .iter() 351 | .filter(|t| t.in_range(u, t.speed() + if is_retreating { 2.0 } else { 0.5 })) 352 | .closest(u) 353 | { 354 | Some(closest_attacker) => { 355 | let flee_position = { 356 | let pos = u.position().towards(closest_attacker.position(), -u.speed()); 357 | if self.is_pathable(pos) { 358 | pos 359 | } else { 360 | *u.position() 361 | .neighbors8() 362 | .iter() 363 | .filter(|p| self.is_pathable(**p)) 364 | .furthest(closest_attacker) 365 | .unwrap_or(&self.start_location) 366 | } 367 | }; 368 | u.move_to(Target::Pos(flee_position), false); 369 | } 370 | None => { 371 | if !(is_retreating || u.in_range(closest, 0.0)) { 372 | u.move_to(Target::Pos(closest.position()), false); 373 | } 374 | } 375 | } 376 | } else { 377 | match targets.iter().in_range_of(u, 0.0).min_by_key(|t| t.hits()) { 378 | Some(target) => u.attack(Target::Tag(target.tag()), false), 379 | None => u.move_to(Target::Pos(closest.position()), false), 380 | } 381 | } 382 | } 383 | None => { 384 | let pos = if is_retreating { 385 | u.position() 386 | } else { 387 | self.enemy_start 388 | }; 389 | u.move_to(Target::Pos(pos), false); 390 | } 391 | } 392 | } 393 | } 394 | } 395 | 396 | fn main() -> SC2Result<()> { 397 | ex_main::main(ReaperRushAI::default()) 398 | } 399 | -------------------------------------------------------------------------------- /examples/runner.rs: -------------------------------------------------------------------------------- 1 | use rust_sc2::prelude::*; 2 | 3 | #[bot] 4 | #[derive(Default)] 5 | struct EmptyBot; 6 | impl Player for EmptyBot { 7 | fn get_player_settings(&self) -> PlayerSettings { 8 | PlayerSettings::new(Race::Random) 9 | } 10 | } 11 | 12 | // Example of how to use runner 13 | fn main() -> SC2Result<()> { 14 | let mut bot = EmptyBot::default(); 15 | 16 | // Bot vs Computer 17 | // 1. Initialize runner 18 | let mut runner = RunnerSingle::new( 19 | &mut bot, 20 | Computer::new(Race::Random, Difficulty::VeryEasy, None), 21 | "EverDreamLE", 22 | Some("4.10"), // Client version can be specified, otherwise will be used latest available version 23 | ); 24 | 25 | // 2. Configure runner 26 | runner.set_map("EternalEmpireLE"); 27 | runner.computer = Computer::new(Race::Protoss, Difficulty::VeryHard, Some(AIBuild::Air)); 28 | runner.realtime = true; // Default: false 29 | runner.save_replay_as = Some("path/to/replay/MyReplay.SC2Replay"); // Default: None == don't save replay 30 | 31 | // 3. Launch SC2 32 | runner.launch()?; 33 | 34 | // 4. Run games 35 | // Run game once 36 | runner.run_game()?; 37 | 38 | const MAPS: &[&str] = &["EverDreamLE", "GoldenWallLE", "IceandChromeLE"]; 39 | const RACES: &[Race] = &[Race::Zerg, Race::Terran, Race::Protoss]; 40 | const DIFFICULTIES: &[Difficulty] = &[Difficulty::Easy, Difficulty::Medium, Difficulty::Hard]; 41 | 42 | // Run multiple times 43 | for i in 0..3 { 44 | // Configuration can be changed between games 45 | runner.set_map(MAPS[i]); 46 | runner.computer.race = RACES[i]; 47 | runner.computer.difficulty = DIFFICULTIES[i]; 48 | 49 | runner.run_game()?; 50 | } 51 | 52 | // Better to close runner manually before launching other 53 | runner.close(); 54 | 55 | let mut other = RunnerSingle::new( 56 | &mut bot, 57 | Computer::new(Race::Random, Difficulty::VeryEasy, None), 58 | "Flat32", 59 | None, 60 | ); 61 | other.run_game()?; 62 | other.close(); 63 | 64 | // Human vs Bot 65 | // 1. Initialize runner 66 | let mut runner = RunnerMulti::new( 67 | &mut bot, 68 | PlayerSettings::new(Race::Random).with_name("Name"), 69 | "PillarsofGoldLE", 70 | None, 71 | ); 72 | 73 | // 2. Configure runner 74 | runner.set_map("PillarsofGoldLE"); 75 | runner.human_settings = PlayerSettings::new(Race::Random).with_name("Name"); 76 | runner.realtime = false; 77 | runner.save_replay_as = None; 78 | 79 | // 3. Launch SC2 80 | runner.launch()?; 81 | 82 | // 4. Run games 83 | runner.run_game()?; 84 | 85 | // Runners dropped here: 86 | // Any launched sc2 clients will be closed automatically 87 | 88 | Ok(()) 89 | } 90 | -------------------------------------------------------------------------------- /examples/speed-mining.rs: -------------------------------------------------------------------------------- 1 | use rust_sc2::prelude::*; 2 | use std::collections::{HashMap, HashSet}; 3 | 4 | mod ex_main; 5 | 6 | #[bot] 7 | #[derive(Default)] 8 | struct LightningMcQueen { 9 | base_indices: HashMap, // (base tag, expansion index) 10 | assigned: HashMap>, // (mineral, workers) 11 | free_workers: HashSet, // tags of workers which aren't assigned to any work 12 | harvesters: HashMap, // (worker, (target mineral, nearest townhall)) 13 | } 14 | 15 | impl Player for LightningMcQueen { 16 | fn get_player_settings(&self) -> PlayerSettings { 17 | PlayerSettings::new(self.race).raw_crop_to_playable_area(true) 18 | } 19 | 20 | fn on_event(&mut self, event: Event) -> SC2Result<()> { 21 | match event { 22 | Event::UnitCreated(tag) => { 23 | if let Some(u) = self.units.my.units.get(tag) { 24 | if u.type_id() == self.race_values.worker { 25 | self.free_workers.insert(tag); 26 | } 27 | } 28 | } 29 | Event::ConstructionComplete(tag) => { 30 | if let Some(u) = self.units.my.structures.get(tag) { 31 | if u.type_id() == self.race_values.start_townhall { 32 | if let Some(idx) = self 33 | .expansions 34 | .iter() 35 | .enumerate() 36 | .find(|(_, exp)| exp.base == Some(tag)) 37 | .map(|(idx, _)| idx) 38 | { 39 | self.base_indices.insert(tag, idx); 40 | } 41 | } 42 | } 43 | } 44 | Event::UnitDestroyed(tag, alliance) => { 45 | let remove_mineral = |bot: &mut LightningMcQueen, tag| { 46 | if let Some(ws) = bot.assigned.remove(&tag) { 47 | for w in ws { 48 | bot.harvesters.remove(&w); 49 | bot.free_workers.insert(w); 50 | } 51 | } 52 | }; 53 | 54 | match alliance { 55 | Some(Alliance::Own) => { 56 | // townhall destroyed 57 | if let Some(idx) = self.base_indices.remove(&tag) { 58 | let exp = &self.expansions[idx]; 59 | for m in exp.minerals.clone() { 60 | remove_mineral(self, m); 61 | } 62 | // harvester died 63 | } else if let Some((m, _)) = self.harvesters.remove(&tag) { 64 | self.assigned.entry(m).and_modify(|ws| { 65 | ws.remove(&tag); 66 | }); 67 | // free worker died 68 | } else { 69 | self.free_workers.remove(&tag); 70 | } 71 | } 72 | // mineral mined out 73 | Some(Alliance::Neutral) => remove_mineral(self, tag), 74 | _ => {} 75 | } 76 | } 77 | _ => {} 78 | } 79 | Ok(()) 80 | } 81 | 82 | fn on_step(&mut self, _iteration: usize) -> SC2Result<()> { 83 | self.assign_roles(); 84 | self.execute_micro(); 85 | Ok(()) 86 | } 87 | } 88 | 89 | impl LightningMcQueen { 90 | fn assign_roles(&mut self) { 91 | let mut to_harvest = vec![]; 92 | // iterator of (mineral tag, nearest base tag) 93 | let mut harvest_targets = self.base_indices.iter().flat_map(|(b, i)| { 94 | self.expansions[*i] 95 | .minerals 96 | .iter() 97 | .map(|m| (m, 2 - self.assigned.get(m).map_or(0, |ws| ws.len()))) 98 | .flat_map(move |(m, c)| vec![(*m, *b); c]) 99 | }); 100 | 101 | for w in &self.free_workers { 102 | if let Some(t) = harvest_targets.next() { 103 | to_harvest.push((*w, t)); 104 | } else { 105 | break; 106 | } 107 | } 108 | 109 | for (w, t) in to_harvest { 110 | self.free_workers.remove(&w); 111 | self.harvesters.insert(w, t); 112 | self.assigned.entry(t.0).or_default().insert(w); 113 | } 114 | } 115 | fn execute_micro(&mut self) { 116 | let (gather_ability, return_ability) = match self.race { 117 | Race::Terran => (AbilityId::HarvestGatherSCV, AbilityId::HarvestReturnSCV), 118 | Race::Zerg => (AbilityId::HarvestGatherDrone, AbilityId::HarvestReturnDrone), 119 | Race::Protoss => (AbilityId::HarvestGatherProbe, AbilityId::HarvestReturnProbe), 120 | _ => unreachable!(), 121 | }; 122 | let mut mineral_moving = HashSet::new(); 123 | 124 | for u in &self.units.my.workers { 125 | if let Some((mineral_tag, base_tag)) = self.harvesters.get(&u.tag()) { 126 | let is_collides = || { 127 | let range = (u.radius() + u.distance_per_step()) * 2.0; 128 | !self.assigned[mineral_tag].iter().all(|&w| { 129 | w == u.tag() 130 | || mineral_moving.contains(&w) 131 | || u.is_further(range, &self.units.my.workers[w]) 132 | }) 133 | }; 134 | 135 | match u.orders().first().map(|ord| (ord.ability, ord.target)) { 136 | // moving 137 | Some((AbilityId::MoveMove, Target::Pos(current_target))) => { 138 | let mineral = &self.units.mineral_fields[*mineral_tag]; 139 | let range = mineral.radius() + u.distance_per_step(); 140 | // moving towards mineral 141 | if current_target.is_closer(range, mineral) { 142 | // execute gather ability if close enough or colliding with other workers 143 | if u.is_closer(u.radius() + range, mineral) || is_collides() { 144 | u.smart(Target::Tag(mineral.tag()), false); 145 | mineral_moving.insert(u.tag()); 146 | } 147 | // otherwise keep moving 148 | continue; 149 | } else { 150 | let base = &self.units.my.townhalls[*base_tag]; 151 | let range = base.radius() + u.distance_per_step(); 152 | // moving towards base 153 | if current_target.is_closer(range, base) { 154 | // execute return ability if close enough or colliding with other workers 155 | if u.is_closer(u.radius() + range, base) || is_collides() { 156 | u.smart(Target::Tag(base.tag()), false); 157 | mineral_moving.insert(u.tag()); 158 | } 159 | // otherwise keep moving 160 | continue; 161 | } 162 | } 163 | } 164 | // gathering 165 | Some((ability, Target::Tag(t))) if ability == gather_ability && t == *mineral_tag => { 166 | let mineral = &self.units.mineral_fields[*mineral_tag]; 167 | // execute move ability if far away from mineral and not colliding with other workers 168 | if u.is_further(u.radius() + mineral.radius() + u.distance_per_step(), mineral) 169 | && !is_collides() 170 | { 171 | let base = &self.units.my.townhalls[*base_tag]; 172 | u.move_to( 173 | Target::Pos(mineral.position().towards(base.position(), mineral.radius())), 174 | false, 175 | ); 176 | // otherwise keep gathering 177 | } else { 178 | mineral_moving.insert(u.tag()); 179 | } 180 | continue; 181 | } 182 | // returning 183 | Some((ability, Target::Tag(t))) if ability == return_ability && t == *base_tag => { 184 | let base = &self.units.my.townhalls[*base_tag]; 185 | // execute move ability if far away from base and not colliding with other workers 186 | if u.is_further(u.radius() + base.radius() + u.distance_per_step(), base) 187 | && !is_collides() 188 | { 189 | u.move_to( 190 | Target::Pos(base.position().towards(u.position(), base.radius())), 191 | false, 192 | ); 193 | // otherwise keep returning 194 | } else { 195 | mineral_moving.insert(u.tag()); 196 | } 197 | continue; 198 | } 199 | _ => {} 200 | } 201 | 202 | // execute default ability if worker is doing something it shouldn't do 203 | if u.is_carrying_resource() { 204 | u.return_resource(false); 205 | } else { 206 | u.gather(*mineral_tag, false); 207 | } 208 | mineral_moving.insert(u.tag()); 209 | } 210 | } 211 | } 212 | } 213 | 214 | fn main() -> SC2Result<()> { 215 | ex_main::main(LightningMcQueen::default()) 216 | } 217 | -------------------------------------------------------------------------------- /examples/worker-rush.rs: -------------------------------------------------------------------------------- 1 | use rust_sc2::prelude::*; 2 | 3 | mod ex_main; 4 | 5 | #[bot] 6 | #[derive(Default)] 7 | struct WorkerRushAI { 8 | mineral_forward: u64, 9 | mineral_back: u64, 10 | } 11 | 12 | impl Player for WorkerRushAI { 13 | fn on_start(&mut self) -> SC2Result<()> { 14 | if let Some(townhall) = self.units.my.townhalls.first() { 15 | townhall.train(UnitTypeId::Probe, false); 16 | } 17 | 18 | if let Some(closest) = self.units.mineral_fields.closest(self.enemy_start) { 19 | self.mineral_forward = closest.tag(); 20 | } 21 | if let Some(closest) = self.units.mineral_fields.closest(self.start_location) { 22 | self.mineral_back = closest.tag(); 23 | } 24 | 25 | Ok(()) 26 | } 27 | 28 | fn on_step(&mut self, _iteration: usize) -> SC2Result<()> { 29 | let ground_attackers = self 30 | .units 31 | .enemy 32 | .units 33 | .filter(|u| !u.is_flying() && u.can_attack_ground() && u.is_closer(45.0, self.enemy_start)); 34 | if !ground_attackers.is_empty() { 35 | for u in &self.units.my.workers { 36 | let closest = ground_attackers.closest(u).unwrap(); 37 | if u.shield() > Some(5) { 38 | if !u.on_cooldown() { 39 | u.attack(Target::Tag(closest.tag()), false); 40 | } else { 41 | u.gather(self.mineral_back, false); 42 | } 43 | } else if u.in_range_of(closest, 2.0) { 44 | u.gather(self.mineral_back, false); 45 | } else { 46 | u.gather(self.mineral_forward, false); 47 | } 48 | } 49 | } else { 50 | let ground_structures = self 51 | .units 52 | .enemy 53 | .structures 54 | .filter(|u| !u.is_flying() && u.is_closer(45.0, self.enemy_start)); 55 | if !ground_structures.is_empty() { 56 | for u in &self.units.my.workers { 57 | u.attack(Target::Tag(ground_structures.closest(u).unwrap().tag()), false); 58 | } 59 | } else { 60 | for u in &self.units.my.workers { 61 | u.gather(self.mineral_forward, false); 62 | } 63 | } 64 | } 65 | Ok(()) 66 | } 67 | 68 | fn get_player_settings(&self) -> PlayerSettings { 69 | PlayerSettings::new(Race::Protoss).with_name("RustyWorkers") 70 | } 71 | } 72 | 73 | fn main() -> SC2Result<()> { 74 | ex_main::main(WorkerRushAI::default()) 75 | } 76 | -------------------------------------------------------------------------------- /examples/zerg-rush.rs: -------------------------------------------------------------------------------- 1 | use rust_sc2::prelude::*; 2 | use std::cmp::Ordering; 3 | 4 | mod ex_main; 5 | 6 | #[bot] 7 | #[derive(Default)] 8 | struct ZergRushAI { 9 | last_loop_distributed: u32, 10 | } 11 | 12 | impl Player for ZergRushAI { 13 | fn on_start(&mut self) -> SC2Result<()> { 14 | // Setting rallypoint for hatchery 15 | if let Some(townhall) = self.units.my.townhalls.first() { 16 | townhall.command(AbilityId::RallyWorkers, Target::Pos(self.start_center), false); 17 | } 18 | 19 | // Splitting workers to closest mineral crystals 20 | for u in &self.units.my.workers { 21 | if let Some(mineral) = self.units.mineral_fields.closest(u) { 22 | u.gather(mineral.tag(), false); 23 | } 24 | } 25 | 26 | // Ordering drone on initial 50 minerals 27 | if let Some(larva) = self.units.my.larvas.first() { 28 | larva.train(UnitTypeId::Drone, false); 29 | } 30 | self.subtract_resources(UnitTypeId::Drone, true); 31 | 32 | Ok(()) 33 | } 34 | 35 | fn on_step(&mut self, _iteration: usize) -> SC2Result<()> { 36 | self.distribute_workers(); 37 | self.upgrades(); 38 | self.build(); 39 | self.order_units(); 40 | self.execute_micro(); 41 | Ok(()) 42 | } 43 | 44 | fn get_player_settings(&self) -> PlayerSettings { 45 | PlayerSettings::new(Race::Zerg).with_name("RustyLings") 46 | } 47 | } 48 | 49 | impl ZergRushAI { 50 | const DISTRIBUTION_DELAY: u32 = 8; 51 | 52 | fn distribute_workers(&mut self) { 53 | if self.units.my.workers.is_empty() { 54 | return; 55 | } 56 | let mut idle_workers = self.units.my.workers.idle(); 57 | let bases = self.units.my.townhalls.ready(); 58 | 59 | // Check distribution delay if there aren't any idle workers 60 | let game_loop = self.state.observation.game_loop(); 61 | let last_loop = &mut self.last_loop_distributed; 62 | if idle_workers.is_empty() && *last_loop + Self::DISTRIBUTION_DELAY + bases.len() as u32 > game_loop { 63 | return; 64 | } 65 | *last_loop = game_loop; 66 | 67 | // Distribute 68 | let mineral_fields = &self.units.mineral_fields; 69 | if mineral_fields.is_empty() { 70 | return; 71 | } 72 | if bases.is_empty() { 73 | return; 74 | } 75 | 76 | let mut deficit_minings = Units::new(); 77 | let mut deficit_geysers = Units::new(); 78 | 79 | // Distributing mineral workers 80 | let mineral_tags = mineral_fields.iter().map(|m| m.tag()).collect::>(); 81 | for base in &bases { 82 | match base.assigned_harvesters().cmp(&base.ideal_harvesters()) { 83 | Ordering::Less => (0..(base.ideal_harvesters().unwrap() 84 | - base.assigned_harvesters().unwrap())) 85 | .for_each(|_| { 86 | deficit_minings.push(base.clone()); 87 | }), 88 | Ordering::Greater => { 89 | let local_minerals = mineral_fields 90 | .iter() 91 | .closer(11.0, base) 92 | .map(|m| m.tag()) 93 | .collect::>(); 94 | 95 | idle_workers.extend( 96 | self.units 97 | .my 98 | .workers 99 | .iter() 100 | .filter(|u| { 101 | u.target_tag().map_or(false, |target_tag| { 102 | local_minerals.contains(&target_tag) 103 | || (u.is_carrying_minerals() && target_tag == base.tag()) 104 | }) 105 | }) 106 | .take( 107 | (base.assigned_harvesters().unwrap() - base.ideal_harvesters().unwrap()) 108 | as usize, 109 | ) 110 | .cloned(), 111 | ); 112 | } 113 | _ => {} 114 | } 115 | } 116 | 117 | // Distributing gas workers 118 | let speed_upgrade = UpgradeId::Zerglingmovementspeed; 119 | let has_enough_gas = self.can_afford_upgrade(speed_upgrade) 120 | || self.has_upgrade(speed_upgrade) 121 | || self.is_ordered_upgrade(speed_upgrade); 122 | let target_gas_workers = Some(if has_enough_gas { 0 } else { 3 }); 123 | 124 | self.units.my.gas_buildings.iter().ready().for_each(|gas| { 125 | match gas.assigned_harvesters().cmp(&target_gas_workers) { 126 | Ordering::Less => { 127 | idle_workers.extend(self.units.my.workers.filter(|u| { 128 | u.target_tag() 129 | .map_or(false, |target_tag| mineral_tags.contains(&target_tag)) 130 | })); 131 | (0..(gas.ideal_harvesters().unwrap() - gas.assigned_harvesters().unwrap())).for_each( 132 | |_| { 133 | deficit_geysers.push(gas.clone()); 134 | }, 135 | ); 136 | } 137 | Ordering::Greater => { 138 | idle_workers.extend( 139 | self.units 140 | .my 141 | .workers 142 | .iter() 143 | .filter(|u| { 144 | u.target_tag().map_or(false, |target_tag| { 145 | target_tag == gas.tag() 146 | || (u.is_carrying_vespene() 147 | && target_tag == bases.closest(gas).unwrap().tag()) 148 | }) 149 | }) 150 | .take( 151 | (gas.assigned_harvesters().unwrap() - gas.ideal_harvesters().unwrap()) 152 | as usize, 153 | ) 154 | .cloned(), 155 | ); 156 | } 157 | _ => {} 158 | } 159 | }); 160 | 161 | // Distributing idle workers 162 | let minerals_near_base = if idle_workers.len() > deficit_minings.len() + deficit_geysers.len() { 163 | let minerals = mineral_fields.filter(|m| bases.iter().any(|base| base.is_closer(11.0, *m))); 164 | if minerals.is_empty() { 165 | None 166 | } else { 167 | Some(minerals) 168 | } 169 | } else { 170 | None 171 | }; 172 | 173 | for u in &idle_workers { 174 | if let Some(closest) = deficit_geysers.closest(u) { 175 | let tag = closest.tag(); 176 | deficit_geysers.remove(tag); 177 | u.gather(tag, false); 178 | } else if let Some(closest) = deficit_minings.closest(u) { 179 | u.gather( 180 | mineral_fields 181 | .closer(11.0, closest) 182 | .max(|m| m.mineral_contents().unwrap_or(0)) 183 | .unwrap() 184 | .tag(), 185 | false, 186 | ); 187 | let tag = closest.tag(); 188 | deficit_minings.remove(tag); 189 | } else if u.is_idle() { 190 | if let Some(mineral) = minerals_near_base.as_ref().and_then(|ms| ms.closest(u)) { 191 | u.gather(mineral.tag(), false); 192 | } 193 | } 194 | } 195 | } 196 | 197 | fn order_units(&mut self) { 198 | // Can't order units without resources 199 | if self.minerals < 50 { 200 | return; 201 | } 202 | 203 | // Order one queen per each base 204 | let queen = UnitTypeId::Queen; 205 | if self.counter().all().count(queen) < self.units.my.townhalls.len() && self.can_afford(queen, true) { 206 | if let Some(townhall) = self.units.my.townhalls.first() { 207 | townhall.train(queen, false); 208 | self.subtract_resources(queen, true); 209 | } 210 | } 211 | 212 | // Can't order units without larva 213 | if self.units.my.larvas.is_empty() { 214 | return; 215 | } 216 | 217 | let over = UnitTypeId::Overlord; 218 | if self.supply_left < 3 219 | && self.supply_cap < 200 220 | && self.counter().ordered().count(over) == 0 221 | && self.can_afford(over, false) 222 | { 223 | if let Some(larva) = self.units.my.larvas.pop() { 224 | larva.train(over, false); 225 | self.subtract_resources(over, false); 226 | } 227 | } 228 | 229 | let drone = UnitTypeId::Drone; 230 | if (self.supply_workers as usize) < 96.min(self.counter().all().count(UnitTypeId::Hatchery) * 16) 231 | && self.can_afford(drone, true) 232 | { 233 | if let Some(larva) = self.units.my.larvas.pop() { 234 | larva.train(drone, false); 235 | self.subtract_resources(drone, true); 236 | } 237 | } 238 | 239 | let zergling = UnitTypeId::Zergling; 240 | if self.can_afford(zergling, true) { 241 | if let Some(larva) = self.units.my.larvas.pop() { 242 | larva.train(zergling, false); 243 | self.subtract_resources(zergling, true); 244 | } 245 | } 246 | } 247 | 248 | fn get_builder(&self, pos: Point2, mineral_tags: &[u64]) -> Option<&Unit> { 249 | self.units 250 | .my 251 | .workers 252 | .iter() 253 | .filter(|u| { 254 | !(u.is_constructing() 255 | || u.is_returning() || u.is_carrying_resource() 256 | || (u.is_gathering() && u.target_tag().map_or(true, |tag| !mineral_tags.contains(&tag)))) 257 | }) 258 | .closest(pos) 259 | } 260 | fn build(&mut self) { 261 | if self.minerals < 75 { 262 | return; 263 | } 264 | 265 | let mineral_tags = self 266 | .units 267 | .mineral_fields 268 | .iter() 269 | .map(|u| u.tag()) 270 | .collect::>(); 271 | 272 | let pool = UnitTypeId::SpawningPool; 273 | if self.counter().all().count(pool) == 0 && self.can_afford(pool, false) { 274 | let place = self.start_location.towards(self.game_info.map_center, 6.0); 275 | if let Some(location) = self.find_placement(pool, place, Default::default()) { 276 | if let Some(builder) = self.get_builder(location, &mineral_tags) { 277 | builder.build(pool, location, false); 278 | self.subtract_resources(pool, false); 279 | } 280 | } 281 | } 282 | 283 | let extractor = UnitTypeId::Extractor; 284 | if self.counter().all().count(extractor) == 0 && self.can_afford(extractor, false) { 285 | let start = self.start_location; 286 | if let Some(geyser) = self.find_gas_placement(start) { 287 | if let Some(builder) = self.get_builder(geyser.position(), &mineral_tags) { 288 | builder.build_gas(geyser.tag(), false); 289 | self.subtract_resources(extractor, false); 290 | } 291 | } 292 | } 293 | 294 | let hatchery = UnitTypeId::Hatchery; 295 | if self.can_afford(hatchery, false) { 296 | if let Some(exp) = self.get_expansion() { 297 | if let Some(builder) = self.get_builder(exp.loc, &mineral_tags) { 298 | builder.build(hatchery, exp.loc, false); 299 | self.subtract_resources(hatchery, false); 300 | } 301 | } 302 | } 303 | } 304 | 305 | fn upgrades(&mut self) { 306 | let speed_upgrade = UpgradeId::Zerglingmovementspeed; 307 | if !(self.has_upgrade(speed_upgrade) || self.is_ordered_upgrade(speed_upgrade)) 308 | && self.can_afford_upgrade(speed_upgrade) 309 | { 310 | if let Some(pool) = self 311 | .units 312 | .my 313 | .structures 314 | .iter() 315 | .find(|s| s.type_id() == UnitTypeId::SpawningPool) 316 | { 317 | pool.research(speed_upgrade, false); 318 | self.subtract_upgrade_cost(speed_upgrade); 319 | } 320 | } 321 | } 322 | 323 | fn execute_micro(&self) { 324 | // Injecting Larva 325 | let mut queens = self.units.my.units.filter(|u| { 326 | u.type_id() == UnitTypeId::Queen 327 | && !u.is_using(AbilityId::EffectInjectLarva) 328 | && u.has_ability(AbilityId::EffectInjectLarva) 329 | }); 330 | if !queens.is_empty() { 331 | self.units 332 | .my 333 | .townhalls 334 | .iter() 335 | .filter(|h| { 336 | !h.has_buff(BuffId::QueenSpawnLarvaTimer) 337 | || h.buff_duration_remain().unwrap() * 20 > h.buff_duration_max().unwrap() 338 | }) 339 | .for_each(|h| { 340 | if let Some(queen) = queens.closest(h) { 341 | queen.command(AbilityId::EffectInjectLarva, Target::Tag(h.tag()), false); 342 | let tag = queen.tag(); 343 | queens.remove(tag); 344 | } 345 | }); 346 | } 347 | 348 | let zerglings = self.units.my.units.of_type(UnitTypeId::Zergling); 349 | if zerglings.is_empty() { 350 | return; 351 | } 352 | 353 | // Check if speed upgrade is >80% ready 354 | let speed_upgrade = UpgradeId::Zerglingmovementspeed; 355 | let speed_upgrade_is_almost_ready = 356 | self.has_upgrade(speed_upgrade) || self.upgrade_progress(speed_upgrade) >= 0.8; 357 | 358 | // Attacking with zerglings or defending our locations 359 | let targets = if speed_upgrade_is_almost_ready { 360 | self.units.enemy.all.ground() 361 | } else { 362 | self.units 363 | .enemy 364 | .all 365 | .filter(|e| !e.is_flying() && self.units.my.townhalls.iter().any(|h| h.is_closer(25.0, *e))) 366 | }; 367 | if !targets.is_empty() { 368 | for u in &zerglings { 369 | if let Some(target) = targets 370 | .iter() 371 | .in_range_of(u, 0.0) 372 | .min_by_key(|t| t.hits()) 373 | .or_else(|| targets.closest(u)) 374 | { 375 | u.attack(Target::Pos(target.position()), false); 376 | } 377 | } 378 | } else { 379 | let target = if speed_upgrade_is_almost_ready { 380 | self.enemy_start 381 | } else { 382 | self.start_location.towards(self.start_center, -8.0) 383 | }; 384 | for u in &zerglings { 385 | u.move_to(Target::Pos(target), false); 386 | } 387 | } 388 | } 389 | } 390 | 391 | fn main() -> SC2Result<()> { 392 | ex_main::main(ZergRushAI::default()) 393 | } 394 | -------------------------------------------------------------------------------- /generate_ids.py: -------------------------------------------------------------------------------- 1 | from json import load 2 | from pathlib import Path 3 | from sys import argv 4 | 5 | HEAD = """\ 6 | #![allow(deprecated)] 7 | 8 | #[cfg(feature = "serde")] 9 | use serde::{Serialize, Deserialize}; 10 | """ 11 | DERIVES = """\ 12 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 13 | #[derive(Debug, FromPrimitive, ToPrimitive, Copy, Clone, PartialEq, Eq, Hash)]\ 14 | """ 15 | ENUM_NAMES = ("UnitTypeId", "AbilityId", "UpgradeId", "BuffId", "EffectId") 16 | FILE_NAMES = ("unit_typeid", "ability_id", "upgrade_id", "buff_id", "effect_id") 17 | 18 | MIMICS = { 19 | "UnitTypeId": { 20 | "Lurker": "LurkerMP", 21 | "LurkerBurrowed": "LurkerMPBurrowed", 22 | "LurkerDen": "LurkerDenMP", 23 | "LurkerEgg": "LurkerMPEgg", 24 | }, 25 | "UpgradeId": { 26 | "TerranVehicleArmorsLevel1": "TerranVehicleAndShipArmorsLevel1", 27 | "TerranVehicleArmorsLevel2": "TerranVehicleAndShipArmorsLevel2", 28 | "TerranVehicleArmorsLevel3": "TerranVehicleAndShipArmorsLevel3", 29 | "TerranShipArmorsLevel1": "TerranVehicleAndShipArmorsLevel1", 30 | "TerranShipArmorsLevel2": "TerranVehicleAndShipArmorsLevel2", 31 | "TerranShipArmorsLevel3": "TerranVehicleAndShipArmorsLevel3", 32 | "MarineStimpack": "Stimpack", 33 | "CombatShield": "ShieldWall", 34 | "JackhammerConcussionGrenades": "PunisherGrenades", 35 | "InfernalPreIgniters": "HighCapacityBarrels", 36 | "HellionCampaignInfernalPreIgniter": "HighCapacityBarrels", 37 | "TransformationServos": "SmartServos", 38 | "CycloneRapidFireLaunchers": "CycloneLockOnDamageUpgrade", 39 | "MagFieldLaunchers": "CycloneLockOnDamageUpgrade", 40 | "PermanentCloakGhost": "PersonalCloaking", 41 | "YamatoCannon": "BattlecruiserEnableSpecializations", 42 | }, 43 | } 44 | 45 | 46 | def mimic(id, enum): 47 | try: 48 | id = MIMICS[enum][id] 49 | except KeyError: 50 | return "" 51 | return f'\t#[deprecated(note = "Use `{enum}::{id}` instead.")]\n' 52 | 53 | 54 | def parse_simple(d, data): 55 | units = {} 56 | for v in data[d]: 57 | key = v["name"] 58 | 59 | if not key: 60 | continue 61 | 62 | key_to_insert = key.replace(" ", "").replace("_", "").replace("@", "") 63 | if key_to_insert[0].isdigit(): 64 | key_to_insert = "_" + key_to_insert 65 | if key_to_insert in units: 66 | index = 2 67 | tmp = f"{key_to_insert}{index}" 68 | while tmp in units: 69 | index += 1 70 | tmp = f"{key_to_insert}{index}" 71 | key_to_insert = tmp 72 | key_to_insert = key_to_insert[0].upper() + key_to_insert[1:] 73 | units[key_to_insert] = v["id"] 74 | 75 | return units 76 | 77 | 78 | def parse_data(data, version=None): 79 | units = parse_simple("Units", data) 80 | upgrades = parse_simple("Upgrades", data) 81 | effects = parse_simple("Effects", data) 82 | buffs = parse_simple("Buffs", data) 83 | 84 | abilities = {} 85 | for v in data["Abilities"]: 86 | key = v["buttonname"] 87 | remapid = v.get("remapid") 88 | 89 | if (not key) and (remapid is None): 90 | assert v["buttonname"] == "" 91 | continue 92 | 93 | if not key: 94 | if v["friendlyname"] != "": 95 | key = v["friendlyname"] 96 | else: 97 | exit(f"Not mapped: {v !r}") 98 | 99 | key = key.replace(" ", "").replace("_", "").replace("@", "") 100 | if "name" in v: 101 | key = f'{v["name"].replace(" ", "").replace("_", "").replace("@", "")}{key}' 102 | 103 | if "friendlyname" in v: 104 | key = v["friendlyname"].replace(" ", "").replace("_", "").replace("@", "") 105 | 106 | if key[0].isdigit(): 107 | key = "_" + key 108 | 109 | key = key[0].upper() + key[1:] 110 | key = key.replace("ResearchResearch", "Research") 111 | 112 | if key in abilities and v["index"] == 0: 113 | # print(f"{key} has value 0 and id {v['id']}, overwriting {key}: {abilities[key]}") 114 | # Commented out to try to fix: 3670 is not a valid AbilityId 115 | abilities[key] = v["id"] 116 | pass 117 | else: 118 | abilities[key] = v["id"] 119 | 120 | # fixes for wrong ids 121 | # if version == "4.10": 122 | # upgrades["EnhancedShockwaves"] = 296 123 | # abilities["GhostAcademyResearchEnhancedShockwaves"] = 822 124 | # elif version is None: 125 | if version is None: 126 | abilities["TerranBuildRefinery"] = 320 127 | elif version == "linux505": 128 | units["AssimilatorRich"] = 1980 129 | units["ExtractorRich"] = 1981 130 | units["AccelerationZoneSmall"] = 1985 131 | units["AccelerationZoneMedium"] = 1986 132 | units["AccelerationZoneLarge"] = 1987 133 | 134 | upgrades["TempestGroundAttackUpgrade"] = 296 135 | upgrades["EnhancedShockwaves"] = 297 136 | 137 | abilities["FleetBeaconResearchVoidRaySpeedUpgrade"] = 48 138 | abilities["FleetBeaconResearchTempestResearchGroundAttackUpgrade"] = 49 139 | abilities["FleetBeaconResearchTempestResearchGroundAttackUpgrade"] = 49 140 | abilities["GhostAcademyResearchEnhancedShockwaves"] = 822 141 | abilities["LurkerDenResearchLurkerRange"] = 3710 142 | abilities["BatteryOverchargeBatteryOvercharge"] = 3815 143 | abilities["AmorphousArmorcloudAmorphousArmorcloud"] = 3817 144 | 145 | buffs["AccelerationZoneTemporalField"] = 290 146 | buffs["AmorphousArmorcloud"] = 296 147 | buffs["BatteryOvercharge"] = 298 148 | 149 | return ( 150 | units, 151 | abilities, 152 | upgrades, 153 | buffs, 154 | effects, 155 | ) 156 | 157 | 158 | def gen_enum(enum, name): 159 | return ( 160 | f"{DERIVES}\npub enum {name} {{\n" 161 | + "".join( 162 | mimic(k, name) + f"\t{k} = {v},\n" 163 | for k, v in sorted(enum.items(), key=lambda x: x[1]) 164 | ) 165 | + "}\n" 166 | ) 167 | 168 | 169 | def generate(): 170 | mod = [ 171 | [ 172 | "//! Auto generated with `generate_ids.py` script from `stableid.json`", 173 | "//! ids of units, ablities, upgrades, buffs and effects.", 174 | "#![allow(missing_docs)]", 175 | ], 176 | [], 177 | [], 178 | ["mod impls;"], 179 | ] 180 | enums_latest = parse_data( 181 | load((Path.home() / "Documents" / "StarCraft II" / "stableid.json").open()) 182 | ) 183 | enums_linux = enums_latest 184 | # parse_data( 185 | # load( 186 | # (Path.home() / "Documents" / "StarCraft II" / "stableid_4.10.json").open() 187 | # ), 188 | # version="linux505", 189 | # ) 190 | 191 | for name, file, enum, enum_linux in zip( 192 | ENUM_NAMES, FILE_NAMES, enums_latest, enums_linux 193 | ): 194 | if enum == enum_linux: 195 | generated = f"{HEAD}\n{gen_enum(enum, name)}" 196 | else: 197 | generated = ( 198 | f'{HEAD}\n#[cfg(target_os = "windows")]\n' 199 | + gen_enum(enum, name) 200 | + '\n#[cfg(target_os = "linux")]\n' 201 | + gen_enum(enum_linux, name) 202 | ) 203 | 204 | (Path.cwd() / "src" / "ids" / f"{file}.rs").write_text(generated) 205 | mod[1].append(f"mod {file};") 206 | mod[2].append(f"pub use {file}::{name};") 207 | (Path.cwd() / "src" / "ids" / "mod.rs").write_text( 208 | "\n\n".join("\n".join(part) for part in mod) + "\n" 209 | ) 210 | 211 | 212 | if __name__ == "__main__": 213 | generate() 214 | -------------------------------------------------------------------------------- /misc/cost.md: -------------------------------------------------------------------------------- 1 | | UnitTypeId | Minerals | Vespene | Supply | Time | 2 | |-------------------------|----------|---------|--------|-----------| 3 | | SCV | 50 | 0 | 1 | 272 | 4 | | Marine | 50 | 0 | 1 | 400 | 5 | | Marauder | 100 | 25 | 2 | 480 | 6 | | Reaper | 50 | 50 | 1 | 720 | 7 | | Ghost | 150 | 125 | 2 | 640 | 8 | | Hellion | 100 | 0 | 2 | 480 | 9 | | HellionTank | 100 | 0 | 2 | 480 | 10 | | SiegeTank | 150 | 125 | 3 | 720 | 11 | | SiegeTankSieged | 150 | 125 | 3 | 68.66797 | 12 | | Cyclone | 150 | 100 | 3 | 720 | 13 | | WidowMine | 75 | 25 | 2 | 480 | 14 | | WidowMineBurrowed | 75 | 25 | 2 | 52 | 15 | | Thor | 300 | 200 | 6 | 960 | 16 | | ThorAP | 300 | 200 | 6 | 42 | 17 | | VikingFighter | 150 | 75 | 2 | 672 | 18 | | VikingAssault | 150 | 75 | 2 | 41.441406 | 19 | | Medivac | 100 | 100 | 2 | 672 | 20 | | Liberator | 150 | 150 | 3 | 960 | 21 | | LiberatorAG | 150 | 150 | 3 | 64.66797 | 22 | | Raven | 100 | 200 | 2 | 960 | 23 | | Banshee | 150 | 100 | 3 | 960 | 24 | | Battlecruiser | 400 | 300 | 6 | 1440 | 25 | | Larva | 0 | 0 | 0 | 0 | 26 | | Egg | 0 | 0 | 0 | 0 | 27 | | Drone | 50 | 0 | 1 | 272 | 28 | | DroneBurrowed | 50 | 0 | 1 | 23.328125 | 29 | | Queen | 150 | 0 | 2 | 800 | 30 | | QueenBurrowed | 150 | 0 | 2 | 15.332031 | 31 | | Zergling | 25 | 0 | 0.5 | 384 | 32 | | ZerglingBurrowed | 25 | 0 | 0.5 | 24.291016 | 33 | | BanelingCocoon | 50 | 25 | 0.5 | 0 | 34 | | Baneling | 50 | 25 | 0.5 | 320 | 35 | | BanelingBurrowed | 50 | 25 | 0.5 | 18.96289 | 36 | | Roach | 75 | 25 | 2 | 432 | 37 | | RoachBurrowed | 75 | 25 | 2 | 9.691406 | 38 | | RavagerCocoon | 0 | 0 | 2 | 0 | 39 | | Ravager | 100 | 100 | 3 | 196 | 40 | | RavagerBurrowed | 100 | 100 | 3 | 9.691406 | 41 | | Hydralisk | 100 | 50 | 2 | 528 | 42 | | HydraliskBurrowed | 100 | 50 | 2 | 24.291016 | 43 | | LurkerMPEgg | 0 | 0 | 2 | 0 | 44 | | LurkerMP | 150 | 150 | 3 | 553.3281 | 45 | | LurkerMPBurrowed | 150 | 150 | 3 | 42 | 46 | | Infestor | 100 | 150 | 2 | 800 | 47 | | InfestorBurrowed | 100 | 150 | 2 | 10.962891 | 48 | | SwarmHostMP | 100 | 75 | 3 | 640 | 49 | | SwarmHostBurrowedMP | 100 | 75 | 3 | 42 | 50 | | Ultralisk | 300 | 200 | 6 | 880 | 51 | | UltraliskBurrowed | 300 | 200 | 6 | 34 | 52 | | LocustMP | 0 | 0 | 0 | 0 | 53 | | LocustMPFlying | 0 | 0 | 0 | 8 | 54 | | Broodling | 0 | 0 | 0 | 0 | 55 | | Changeling | 0 | 0 | 0 | 0 | 56 | | ChangelingZealot | 0 | 0 | 0 | 8 | 57 | | ChangelingMarine | 0 | 0 | 0 | 8 | 58 | | ChangelingMarineShield | 0 | 0 | 0 | 8 | 59 | | ChangelingZergling | 0 | 0 | 0 | 8 | 60 | | ChangelingZerglingWings | 0 | 0 | 0 | 8 | 61 | | InfestorTerran | 0 | 0 | 0 | 78 | 62 | | InfestorTerranBurrowed | 0 | 0 | 0 | 24.291016 | 63 | | NydusCanal | 75 | 75 | 0 | 320 | 64 | | Overlord | 100 | 0 | 0 | 400 | 65 | | OverlordCocoon | 150 | 100 | 0 | 0 | 66 | | OverlordTransport | 100 | 0 | 0 | 266.6797 | 67 | | TransportOverlordCocoon | 150 | 100 | 0 | 0 | 68 | | Overseer | 150 | 50 | 0 | 266.6797 | 69 | | OverseerSiegeMode | 150 | 50 | 0 | 12 | 70 | | Mutalisk | 100 | 100 | 2 | 528 | 71 | | Corruptor | 150 | 100 | 2 | 640 | 72 | | BroodLordCocoon | 300 | 250 | 2 | 0 | 73 | | BroodLord | 300 | 250 | 4 | 541.34766 | 74 | | Viper | 100 | 200 | 3 | 640 | 75 | | Probe | 50 | 0 | 1 | 272 | 76 | | Zealot | 100 | 0 | 2 | 608 | 77 | | Stalker | 125 | 50 | 2 | 672 | 78 | | Sentry | 50 | 100 | 2 | 592 | 79 | | Adept | 100 | 25 | 2 | 672 | 80 | | AdeptPhaseShift | 0 | 0 | 2 | 0 | 81 | | HighTemplar | 50 | 150 | 2 | 880 | 82 | | DarkTemplar | 125 | 125 | 2 | 880 | 83 | | Immortal | 275 | 100 | 4 | 880 | 84 | | Colossus | 300 | 200 | 6 | 1200 | 85 | | Disruptor | 150 | 150 | 3 | 800 | 86 | | Archon | 175 | 275 | 4 | 0 | 87 | | Observer | 25 | 75 | 1 | 480 | 88 | | ObserverSiegeMode | 25 | 75 | 1 | 12 | 89 | | WarpPrism | 250 | 0 | 2 | 800 | 90 | | WarpPrismPhasing | 250 | 0 | 2 | 24 | 91 | | Phoenix | 150 | 100 | 2 | 560 | 92 | | VoidRay | 200 | 150 | 4 | 832 | 93 | | Oracle | 150 | 150 | 3 | 832 | 94 | | Carrier | 350 | 250 | 6 | 1440 | 95 | | Interceptor | 15 | 0 | 0 | 0 | 96 | | Tempest | 250 | 175 | 5 | 960 | 97 | | Mothership | 400 | 400 | 8 | 2560 | 98 | | CommandCenter | 400 | 0 | 0 | 1600 | 99 | | CommandCenterFlying | 400 | 0 | 0 | 32 | 100 | | PlanetaryFortress | 550 | 150 | 0 | 800 | 101 | | OrbitalCommand | 550 | 0 | 0 | 560 | 102 | | OrbitalCommandFlying | 550 | 0 | 0 | 32 | 103 | | SupplyDepot | 100 | 0 | 0 | 480 | 104 | | SupplyDepotLowered | 100 | 0 | 0 | 20.800781 | 105 | | SupplyDepotDrop | 0 | 0 | 0 | 0 | 106 | | Refinery | 75 | 0 | 0 | 480 | 107 | | Barracks | 150 | 0 | 0 | 1040 | 108 | | BarracksFlying | 150 | 0 | 0 | 32 | 109 | | EngineeringBay | 125 | 0 | 0 | 560 | 110 | | Bunker | 100 | 0 | 0 | 640 | 111 | | SensorTower | 125 | 100 | 0 | 400 | 112 | | MissileTurret | 100 | 0 | 0 | 400 | 113 | | Factory | 150 | 100 | 0 | 960 | 114 | | FactoryFlying | 150 | 100 | 0 | 32 | 115 | | GhostAcademy | 150 | 50 | 0 | 640 | 116 | | Starport | 150 | 100 | 0 | 800 | 117 | | StarportFlying | 150 | 100 | 0 | 32 | 118 | | Armory | 150 | 100 | 0 | 1040 | 119 | | FusionCore | 150 | 150 | 0 | 1040 | 120 | | TechLab | 50 | 25 | 0 | 2 | 121 | | BarracksTechLab | 50 | 25 | 0 | 400 | 122 | | FactoryTechLab | 50 | 25 | 0 | 400 | 123 | | StarportTechLab | 50 | 25 | 0 | 400 | 124 | | Reactor | 50 | 50 | 0 | 2 | 125 | | BarracksReactor | 50 | 50 | 0 | 800 | 126 | | FactoryReactor | 50 | 50 | 0 | 800 | 127 | | StarportReactor | 50 | 50 | 0 | 800 | 128 | | Hatchery | 350 | 0 | 0 | 1600 | 129 | | SpineCrawler | 150 | 0 | 0 | 800 | 130 | | SporeCrawler | 125 | 0 | 0 | 480 | 131 | | Extractor | 75 | 0 | 0 | 480 | 132 | | SpawningPool | 250 | 0 | 0 | 1040 | 133 | | EvolutionChamber | 125 | 0 | 0 | 560 | 134 | | RoachWarren | 200 | 0 | 0 | 880 | 135 | | BanelingNest | 150 | 50 | 0 | 960 | 136 | | CreepTumor | 0 | 0 | 0 | 240 | 137 | | CreepTumorBurrowed | 0 | 0 | 0 | 18.960938 | 138 | | CreepTumorQueen | 0 | 0 | 0 | 240 | 139 | | CreepTumorMissile | 0 | 0 | 0 | 0 | 140 | | Lair | 500 | 100 | 0 | 1280 | 141 | | HydraliskDen | 150 | 100 | 0 | 640 | 142 | | LurkerDenMP | 150 | 150 | 0 | 1280 | 143 | | InfestationPit | 150 | 100 | 0 | 800 | 144 | | Spire | 250 | 200 | 0 | 1600 | 145 | | NydusNetwork | 200 | 150 | 0 | 800 | 146 | | Hive | 700 | 250 | 0 | 1600 | 147 | | GreaterSpire | 350 | 350 | 0 | 1600 | 148 | | UltraliskCavern | 200 | 200 | 0 | 1040 | 149 | | Nexus | 400 | 0 | 0 | 1600 | 150 | | Pylon | 100 | 0 | 0 | 400 | 151 | | Assimilator | 75 | 0 | 0 | 480 | 152 | | Gateway | 150 | 0 | 0 | 1040 | 153 | | Forge | 150 | 0 | 0 | 720 | 154 | | CyberneticsCore | 150 | 0 | 0 | 800 | 155 | | PhotonCannon | 150 | 0 | 0 | 640 | 156 | | ShieldBattery | 100 | 0 | 0 | 640 | 157 | | RoboticsFacility | 150 | 100 | 0 | 1040 | 158 | | WarpGate | 150 | 0 | 0 | 160 | 159 | | Stargate | 150 | 150 | 0 | 960 | 160 | | TwilightCouncil | 150 | 100 | 0 | 800 | 161 | | RoboticsBay | 150 | 150 | 0 | 1040 | 162 | | FleetBeacon | 300 | 200 | 0 | 960 | 163 | | TemplarArchive | 150 | 200 | 0 | 800 | 164 | | DarkShrine | 150 | 150 | 0 | 1600 | 165 | -------------------------------------------------------------------------------- /sc2-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sc2-macro" 3 | version = "1.0.0" 4 | authors = ["Armageddon "] 5 | edition = "2018" 6 | description = "Procedural macros for rust-sc2 API" 7 | repository = "https://github.com/UltraMachine/rust-sc2" 8 | readme = "../README.md" 9 | license-file = "../LICENSE" 10 | 11 | [dependencies] 12 | sc2-proc-macro = { path = "macro", version = "1" } 13 | -------------------------------------------------------------------------------- /sc2-macro/macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sc2-proc-macro" 3 | version = "1.0.0" 4 | authors = ["Armageddon "] 5 | edition = "2018" 6 | description = "Procedural macros for rust-sc2 API" 7 | repository = "https://github.com/UltraMachine/rust-sc2" 8 | readme = "../../README.md" 9 | license-file = "../../LICENSE" 10 | 11 | [lib] 12 | proc-macro = true 13 | 14 | [dependencies] 15 | syn = { version = "^1.0.33", features = ["full"] } 16 | quote = "^1.0.7" 17 | proc-macro2 = "^1.0.18" 18 | regex = "^1.3.9" 19 | -------------------------------------------------------------------------------- /sc2-macro/macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate quote; 3 | 4 | use proc_macro::TokenStream; 5 | use regex::Regex; 6 | use syn::{ 7 | parse_macro_input, Attribute, Data, DeriveInput, Expr, Fields, ItemEnum, ItemFn, ItemStruct, Meta, 8 | NestedMeta, Stmt, 9 | }; 10 | 11 | #[proc_macro_attribute] 12 | pub fn bot(_attr: TokenStream, item: TokenStream) -> TokenStream { 13 | // let attr = parse_macro_input!(attr as AttributeArgs); 14 | let item = parse_macro_input!(item as ItemStruct); 15 | 16 | let name = item.ident; 17 | let vis = item.vis; 18 | let attrs = item.attrs; 19 | let generics = item.generics; 20 | let fields = match item.fields { 21 | Fields::Named(named_fields) => { 22 | let named = named_fields.named; 23 | quote! {#named} 24 | } 25 | Fields::Unnamed(_) => panic!("#[bot] is not allowed for tuple structs"), 26 | unit => quote! {#unit}, 27 | }; 28 | 29 | let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); 30 | 31 | TokenStream::from(quote! { 32 | #(#attrs)* 33 | #vis struct #name#generics { 34 | _bot: rust_sc2::bot::Bot, 35 | #fields 36 | } 37 | impl #impl_generics std::ops::Deref for #name #ty_generics #where_clause { 38 | type Target = rust_sc2::bot::Bot; 39 | 40 | fn deref(&self) -> &Self::Target { 41 | &self._bot 42 | } 43 | } 44 | impl #impl_generics std::ops::DerefMut for #name #ty_generics #where_clause { 45 | fn deref_mut(&mut self) -> &mut Self::Target { 46 | &mut self._bot 47 | } 48 | } 49 | }) 50 | } 51 | 52 | #[proc_macro_attribute] 53 | pub fn bot_new(_attr: TokenStream, item: TokenStream) -> TokenStream { 54 | // let attr = parse_macro_input!(attr as AttributeArgs); 55 | let item = parse_macro_input!(item as ItemFn); 56 | 57 | let vis = item.vis; 58 | let signature = item.sig; 59 | let blocks = item.block.stmts.iter().map(|s| match s { 60 | Stmt::Expr(Expr::Struct(struct_expr)) => { 61 | let path = &struct_expr.path; 62 | let rest = match &struct_expr.rest { 63 | Some(expr) => quote! {#expr}, 64 | None => quote! {}, 65 | }; 66 | let fields = struct_expr.fields.iter(); 67 | 68 | quote! { 69 | #path { 70 | _bot: Default::default(), 71 | #(#fields,)* 72 | ..#rest 73 | } 74 | } 75 | } 76 | n => quote! {#n}, 77 | }); 78 | 79 | TokenStream::from(quote! { 80 | #vis #signature { 81 | #(#blocks)* 82 | } 83 | }) 84 | } 85 | 86 | #[proc_macro_derive(FromStr, attributes(enum_from_str))] 87 | pub fn enum_from_str_derive(input: TokenStream) -> TokenStream { 88 | let item = parse_macro_input!(input as DeriveInput); 89 | if let Data::Enum(data) = item.data { 90 | let name = item.ident; 91 | let variants = data.variants.iter().map(|v| &v.ident); 92 | // let variants2 = variants.clone().map(|v| format!("{}::{}", name, v)); 93 | 94 | let additional_attributes = |a: &Attribute| { 95 | if a.path.is_ident("enum_from_str") { 96 | if let Meta::List(list) = a.parse_meta().unwrap() { 97 | return list.nested.iter().any(|n| { 98 | if let NestedMeta::Meta(Meta::Path(path)) = n { 99 | path.is_ident("use_primitives") 100 | } else { 101 | false 102 | } 103 | }); 104 | } else { 105 | unreachable!("No options found in attribute `enum_from_str`") 106 | } 107 | } 108 | false 109 | }; 110 | let other_cases = if item.attrs.iter().any(additional_attributes) { 111 | quote! { 112 | n => { 113 | if let Ok(num) = n.parse() { 114 | if let Some(result) = Self::from_i64(num) { 115 | return Ok(result); 116 | } 117 | } 118 | return Err(sc2_macro::ParseEnumError); 119 | } 120 | } 121 | } else { 122 | quote! {_ => return Err(sc2_macro::ParseEnumError)} 123 | }; 124 | TokenStream::from(quote! { 125 | impl std::str::FromStr for #name { 126 | type Err = sc2_macro::ParseEnumError; 127 | 128 | fn from_str(s: &str) -> Result { 129 | Ok(match s { 130 | #( 131 | stringify!(#variants) => Self::#variants, 132 | // #variants2 => Self::#variants, 133 | 134 | )* 135 | #other_cases, 136 | }) 137 | } 138 | } 139 | }) 140 | } else { 141 | panic!("Can only derive FromStr for enums") 142 | } 143 | } 144 | 145 | #[proc_macro_attribute] 146 | pub fn variant_checkers(_attr: TokenStream, item: TokenStream) -> TokenStream { 147 | // let attr = parse_macro_input!(attr as AttributeArgs); 148 | let item = parse_macro_input!(item as ItemEnum); 149 | 150 | let name = &item.ident; 151 | let variants = item.variants.iter().map(|v| &v.ident); 152 | let re = Regex::new(r"[A-Z0-9]{1}[a-z0-9]*").unwrap(); 153 | let snake_variants = variants.clone().map(|v| { 154 | format_ident!( 155 | "is_{}", 156 | re.find_iter(&v.to_string()) 157 | .map(|m| m.as_str().to_ascii_lowercase()) 158 | .collect::>() 159 | .join("_") 160 | ) 161 | }); 162 | 163 | TokenStream::from(quote! { 164 | #item 165 | impl #name { 166 | #( 167 | #[inline] 168 | pub fn #snake_variants(self) -> bool { 169 | matches!(self, Self::#variants) 170 | } 171 | )* 172 | } 173 | }) 174 | } 175 | -------------------------------------------------------------------------------- /sc2-macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fmt}; 2 | 3 | pub use sc2_proc_macro::{bot, bot_new, variant_checkers, FromStr}; 4 | 5 | #[derive(Debug, PartialEq, Eq)] 6 | pub struct ParseEnumError; 7 | 8 | impl fmt::Display for ParseEnumError { 9 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 10 | write!(f, "failed to parse enum") 11 | } 12 | } 13 | 14 | impl Error for ParseEnumError {} 15 | -------------------------------------------------------------------------------- /src/api.rs: -------------------------------------------------------------------------------- 1 | //! Contains nice wrapper around SC2 API. 2 | 3 | use crate::{ 4 | bot::{Locked, Rl}, 5 | client::{SC2Result, WS}, 6 | }; 7 | use protobuf::Message; 8 | use sc2_proto::sc2api::{Request, Response}; 9 | use tungstenite::Message::Binary; 10 | 11 | /// SC2 API. Can be accessed through [`self.api()`](crate::bot::Bot::api). 12 | pub struct API(Rl); 13 | impl API { 14 | pub(crate) fn new(ws: WS) -> API { 15 | API(Rl::new(ws)) 16 | } 17 | 18 | /// Sends request and returns a response. 19 | pub fn send(&self, req: Request) -> SC2Result { 20 | let mut ws = self.0.write_lock(); 21 | 22 | ws.write_message(Binary(req.write_to_bytes()?))?; 23 | 24 | let msg = ws.read_message()?; 25 | 26 | let mut res = Response::new(); 27 | res.merge_from_bytes(msg.into_data().as_slice())?; 28 | Ok(res) 29 | } 30 | 31 | /// Sends request, waits for the response, but ignores it (useful when response is empty). 32 | pub fn send_request(&self, req: Request) -> SC2Result<()> { 33 | let mut ws = self.0.write_lock(); 34 | ws.write_message(Binary(req.write_to_bytes()?))?; 35 | let _ = ws.read_message()?; 36 | Ok(()) 37 | } 38 | 39 | /// Sends request, but doesn't wait for the response (use only when more control required, 40 | /// in common cases prefered to use [`send`] or [`send_request`]). 41 | /// 42 | /// [`send`]: Self::send 43 | /// [`send_request`]: Self::send_request 44 | pub fn send_only(&self, req: Request) -> SC2Result<()> { 45 | self.0.write_lock().write_message(Binary(req.write_to_bytes()?))?; 46 | Ok(()) 47 | } 48 | /// Waits for a response (useful only after [`send_only`]). 49 | /// 50 | /// [`send_only`]: Self::send_only 51 | pub fn wait_response(&self) -> SC2Result { 52 | let msg = self.0.write_lock().read_message()?; 53 | 54 | let mut res = Response::new(); 55 | res.merge_from_bytes(msg.into_data().as_slice())?; 56 | Ok(res) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/debug.rs: -------------------------------------------------------------------------------- 1 | //! Items for interacting with Debug API. 2 | 3 | use crate::{ 4 | geometry::{Point2, Point3}, 5 | ids::UnitTypeId, 6 | IntoProto, 7 | }; 8 | use num_traits::ToPrimitive; 9 | use rustc_hash::FxHashSet; 10 | use sc2_proto::debug::{ 11 | DebugBox, DebugCommand as ProtoDebugCommand, DebugDraw as ProtoDebugDraw, DebugEndGame_EndResult, 12 | DebugGameState as ProtoDebugGameState, DebugLine, DebugSetUnitValue_UnitValue, DebugSphere, DebugText, 13 | }; 14 | 15 | type Color = (u32, u32, u32); 16 | type ScreenPos = (f32, f32); 17 | 18 | /// Helper struct for interacting with Debug API. 19 | /// Can be accessed through [`debug`] field of bot. 20 | /// 21 | /// [`debug`]: crate::bot::Bot::debug 22 | #[derive(Default)] 23 | pub struct Debugger { 24 | debug_commands: Vec, 25 | debug_drawings: Vec, 26 | kill_tags: FxHashSet, 27 | } 28 | impl Debugger { 29 | pub(crate) fn get_commands(&mut self) -> &[DebugCommand] { 30 | let commands = &mut self.debug_commands; 31 | 32 | if !self.debug_drawings.is_empty() { 33 | commands.push(DebugCommand::Draw(self.debug_drawings.drain(..).collect())); 34 | } 35 | if !self.kill_tags.is_empty() { 36 | commands.push(DebugCommand::KillUnit(self.kill_tags.drain().collect())); 37 | } 38 | 39 | commands 40 | } 41 | pub(crate) fn clear_commands(&mut self) { 42 | self.debug_commands.clear(); 43 | } 44 | 45 | fn draw_text(&mut self, text: &str, pos: DebugPos, color: Option, size: Option) { 46 | self.debug_drawings 47 | .push(DebugDraw::Text(text.to_string(), pos, color, size)); 48 | } 49 | /// Draws text in game world with 3d coordinates. 50 | pub fn draw_text_world(&mut self, text: &str, pos: Point3, color: Option, size: Option) { 51 | self.draw_text(text, DebugPos::World(pos), color, size); 52 | } 53 | /// Draws text in game window with 2d coordinates, where (0, 0) is left upper corner. 54 | pub fn draw_text_screen( 55 | &mut self, 56 | text: &str, 57 | pos: Option, 58 | color: Option, 59 | size: Option, 60 | ) { 61 | self.draw_text(text, DebugPos::Screen(pos.unwrap_or((0.0, 0.0))), color, size); 62 | } 63 | /// Draws line in game world from `p0` to `p1`. 64 | pub fn draw_line(&mut self, p0: Point3, p1: Point3, color: Option) { 65 | self.debug_drawings.push(DebugDraw::Line(p0, p1, color)); 66 | } 67 | /// Draws box in game world with corners `p0` and `p1`. 68 | pub fn draw_box(&mut self, p0: Point3, p1: Point3, color: Option) { 69 | self.debug_drawings.push(DebugDraw::Box(p0, p1, color)); 70 | } 71 | /// Draws cube in game world with given half size of edge. 72 | pub fn draw_cube(&mut self, pos: Point3, half_edge: f32, color: Option) { 73 | let offset = Point3::new(half_edge, half_edge, half_edge); 74 | self.debug_drawings 75 | .push(DebugDraw::Box(pos - offset, pos + offset, color)); 76 | } 77 | /// Draws sphere in game world with given radius. 78 | pub fn draw_sphere(&mut self, pos: Point3, radius: f32, color: Option) { 79 | self.debug_drawings.push(DebugDraw::Sphere(pos, radius, color)); 80 | } 81 | /// Spawns units using given commands in format: (unit type, owner's player id, position, count). 82 | pub fn create_units<'a, T>(&mut self, cmds: T) 83 | where 84 | T: IntoIterator, Point2, u32)>, 85 | { 86 | self.debug_commands.extend( 87 | cmds.into_iter() 88 | .copied() 89 | .map(|(type_id, owner, pos, count)| DebugCommand::CreateUnit(type_id, owner, pos, count)), 90 | ); 91 | } 92 | /// Kills units with given tags. 93 | pub fn kill_units<'a, T: IntoIterator>(&mut self, tags: T) { 94 | self.kill_tags.extend(tags); 95 | } 96 | /// Sets values for units using given commands in format: (unit tag, value type, value). 97 | pub fn set_unit_values<'a, T>(&mut self, cmds: T) 98 | where 99 | T: IntoIterator, 100 | { 101 | self.debug_commands.extend( 102 | cmds.into_iter() 103 | .copied() 104 | .map(|(tag, unit_value, value)| DebugCommand::SetUnitValue(tag, unit_value, value)), 105 | ); 106 | } 107 | /// Ends game with Victory for bot 108 | pub fn win_game(&mut self) { 109 | self.debug_commands.push(DebugCommand::EndGame(true)); 110 | } 111 | /// Ends game with Defeat for bot 112 | pub fn end_game(&mut self) { 113 | self.debug_commands.push(DebugCommand::EndGame(false)); 114 | } 115 | /// Disables fog of war, makes all map visible 116 | pub fn show_map(&mut self) { 117 | self.debug_commands 118 | .push(DebugCommand::GameState(DebugGameState::ShowMap)); 119 | } 120 | /// Gives ability to control enemy units 121 | pub fn control_enemy(&mut self) { 122 | self.debug_commands 123 | .push(DebugCommand::GameState(DebugGameState::ControlEnemy)); 124 | } 125 | /// Disables supply usage 126 | pub fn cheat_supply(&mut self) { 127 | self.debug_commands 128 | .push(DebugCommand::GameState(DebugGameState::Food)); 129 | } 130 | /// Makes free all units, structures and upgrades 131 | pub fn cheat_free_build(&mut self) { 132 | self.debug_commands 133 | .push(DebugCommand::GameState(DebugGameState::Free)); 134 | } 135 | /// Gives 5000 minerals and gas to the bot 136 | pub fn cheat_resources(&mut self) { 137 | self.debug_commands 138 | .push(DebugCommand::GameState(DebugGameState::AllResources)); 139 | } 140 | /// Gives 5000 minerals to the bot 141 | pub fn cheat_minerals(&mut self) { 142 | self.debug_commands 143 | .push(DebugCommand::GameState(DebugGameState::Minerals)); 144 | } 145 | /// Gives 5000 gas to the bot 146 | pub fn cheat_gas(&mut self) { 147 | self.debug_commands 148 | .push(DebugCommand::GameState(DebugGameState::Gas)); 149 | } 150 | /// Makes all bot's units invincible and significantly increases their damage 151 | pub fn cheat_god(&mut self) { 152 | self.debug_commands 153 | .push(DebugCommand::GameState(DebugGameState::God)); 154 | } 155 | /// Removes cooldown of abilities of bot's units 156 | pub fn cheat_cooldown(&mut self) { 157 | self.debug_commands 158 | .push(DebugCommand::GameState(DebugGameState::Cooldown)); 159 | } 160 | /// Removes all tech requirements for bot 161 | pub fn cheat_tech_tree(&mut self) { 162 | self.debug_commands 163 | .push(DebugCommand::GameState(DebugGameState::TechTree)); 164 | } 165 | /// First use: researches all upgrades for units and sets level 1 of damage and armor upgrades 166 | /// 167 | /// Second use: sets level 2 of damage and armor upgrades 168 | /// 169 | /// Third use: sets level 3 of damage and armor upgrades 170 | /// 171 | /// Fourth use: disables all upgrades researched with this command 172 | pub fn cheat_upgrades(&mut self) { 173 | self.debug_commands 174 | .push(DebugCommand::GameState(DebugGameState::Upgrade)); 175 | } 176 | /// Significantly increases speed of making units, structures and upgrades 177 | pub fn cheat_fast_build(&mut self) { 178 | self.debug_commands 179 | .push(DebugCommand::GameState(DebugGameState::FastBuild)); 180 | } 181 | } 182 | 183 | #[derive(Debug, Clone)] 184 | pub(crate) enum DebugCommand { 185 | Draw(Vec), 186 | GameState(DebugGameState), 187 | CreateUnit(UnitTypeId, Option, Point2, u32), 188 | KillUnit(Vec), 189 | // TestProcess, 190 | // SetScore, 191 | EndGame(bool), 192 | SetUnitValue(u64, UnitValue, u32), 193 | } 194 | impl IntoProto for &DebugCommand { 195 | fn into_proto(self) -> ProtoDebugCommand { 196 | let mut proto = ProtoDebugCommand::new(); 197 | match self { 198 | DebugCommand::Draw(cmds) => proto.set_draw(cmds.into_proto()), 199 | DebugCommand::GameState(cmd) => proto.set_game_state(cmd.into_proto()), 200 | DebugCommand::CreateUnit(type_id, owner, pos, count) => { 201 | let unit = proto.mut_create_unit(); 202 | unit.set_unit_type(type_id.to_u32().unwrap()); 203 | if let Some(owner) = owner { 204 | unit.set_owner(*owner as i32); 205 | } 206 | unit.set_pos(pos.into_proto()); 207 | unit.set_quantity(*count); 208 | } 209 | DebugCommand::KillUnit(tags) => proto.mut_kill_unit().set_tag(tags.to_vec()), 210 | DebugCommand::EndGame(win) => { 211 | let end_game = proto.mut_end_game(); 212 | if *win { 213 | end_game.set_end_result(DebugEndGame_EndResult::DeclareVictory); 214 | } 215 | } 216 | DebugCommand::SetUnitValue(tag, unit_value, value) => { 217 | let cmd = proto.mut_unit_value(); 218 | cmd.set_unit_tag(*tag); 219 | cmd.set_unit_value(unit_value.into_proto()); 220 | cmd.set_value(*value as f32); 221 | } 222 | } 223 | proto 224 | } 225 | } 226 | 227 | impl IntoProto for &[DebugDraw] { 228 | fn into_proto(self) -> ProtoDebugDraw { 229 | let mut cmds = ProtoDebugDraw::new(); 230 | for drawing in self { 231 | match drawing { 232 | DebugDraw::Text(text, pos, color, size) => { 233 | let mut proto_text = DebugText::new(); 234 | proto_text.set_text(text.to_string()); 235 | match pos { 236 | DebugPos::Screen((x, y)) => { 237 | let pos = proto_text.mut_virtual_pos(); 238 | pos.set_x(*x); 239 | pos.set_y(*y); 240 | } 241 | DebugPos::World(p) => proto_text.set_world_pos(p.into_proto()), 242 | } 243 | if let Some((r, g, b)) = color { 244 | let proto_color = proto_text.mut_color(); 245 | proto_color.set_r(*r); 246 | proto_color.set_g(*g); 247 | proto_color.set_b(*b); 248 | } 249 | if let Some(s) = size { 250 | proto_text.set_size(*s); 251 | } 252 | cmds.mut_text().push(proto_text); 253 | } 254 | DebugDraw::Line(p0, p1, color) => { 255 | let mut proto_line = DebugLine::new(); 256 | let line = proto_line.mut_line(); 257 | line.set_p0(p0.into_proto()); 258 | line.set_p1(p1.into_proto()); 259 | if let Some((r, g, b)) = color { 260 | let proto_color = proto_line.mut_color(); 261 | proto_color.set_r(*r); 262 | proto_color.set_g(*g); 263 | proto_color.set_b(*b); 264 | } 265 | cmds.mut_lines().push(proto_line); 266 | } 267 | DebugDraw::Box(p0, p1, color) => { 268 | let mut proto_box = DebugBox::new(); 269 | proto_box.set_min(p0.into_proto()); 270 | proto_box.set_max(p1.into_proto()); 271 | if let Some((r, g, b)) = color { 272 | let proto_color = proto_box.mut_color(); 273 | proto_color.set_r(*r); 274 | proto_color.set_g(*g); 275 | proto_color.set_b(*b); 276 | } 277 | cmds.mut_boxes().push(proto_box); 278 | } 279 | DebugDraw::Sphere(pos, radius, color) => { 280 | let mut proto_sphere = DebugSphere::new(); 281 | proto_sphere.set_p(pos.into_proto()); 282 | proto_sphere.set_r(*radius); 283 | if let Some((r, g, b)) = color { 284 | let proto_color = proto_sphere.mut_color(); 285 | proto_color.set_r(*r); 286 | proto_color.set_g(*g); 287 | proto_color.set_b(*b); 288 | } 289 | cmds.mut_spheres().push(proto_sphere); 290 | } 291 | } 292 | } 293 | cmds 294 | } 295 | } 296 | 297 | #[derive(Debug, Clone)] 298 | pub(crate) enum DebugPos { 299 | Screen(ScreenPos), // Coordinates on screen (0..1, 0..1) 300 | World(Point3), // Position in game world 301 | } 302 | 303 | #[derive(Debug, Clone)] 304 | pub(crate) enum DebugDraw { 305 | Text(String, DebugPos, Option, Option), 306 | Line(Point3, Point3, Option), 307 | Box(Point3, Point3, Option), 308 | Sphere(Point3, f32, Option), 309 | } 310 | 311 | /// Value type used in [`set_unit_values`](Debugger::set_unit_values) commands. 312 | #[allow(missing_docs)] 313 | #[derive(Debug, Clone, Copy)] 314 | pub enum UnitValue { 315 | Energy, 316 | Health, 317 | Shield, 318 | } 319 | impl IntoProto for UnitValue { 320 | fn into_proto(self) -> DebugSetUnitValue_UnitValue { 321 | match self { 322 | UnitValue::Energy => DebugSetUnitValue_UnitValue::Energy, 323 | UnitValue::Health => DebugSetUnitValue_UnitValue::Life, 324 | UnitValue::Shield => DebugSetUnitValue_UnitValue::Shields, 325 | } 326 | } 327 | } 328 | 329 | #[derive(Debug, Clone, Copy)] 330 | pub(crate) enum DebugGameState { 331 | ShowMap, 332 | ControlEnemy, 333 | Food, 334 | Free, 335 | AllResources, 336 | God, 337 | Minerals, 338 | Gas, 339 | Cooldown, 340 | TechTree, 341 | Upgrade, 342 | FastBuild, 343 | } 344 | impl IntoProto for DebugGameState { 345 | fn into_proto(self) -> ProtoDebugGameState { 346 | match self { 347 | DebugGameState::ShowMap => ProtoDebugGameState::show_map, 348 | DebugGameState::ControlEnemy => ProtoDebugGameState::control_enemy, 349 | DebugGameState::Food => ProtoDebugGameState::food, 350 | DebugGameState::Free => ProtoDebugGameState::free, 351 | DebugGameState::AllResources => ProtoDebugGameState::all_resources, 352 | DebugGameState::God => ProtoDebugGameState::god, 353 | DebugGameState::Minerals => ProtoDebugGameState::minerals, 354 | DebugGameState::Gas => ProtoDebugGameState::gas, 355 | DebugGameState::Cooldown => ProtoDebugGameState::cooldown, 356 | DebugGameState::TechTree => ProtoDebugGameState::tech_tree, 357 | DebugGameState::Upgrade => ProtoDebugGameState::upgrade, 358 | DebugGameState::FastBuild => ProtoDebugGameState::fast_build, 359 | } 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /src/distance/mod.rs: -------------------------------------------------------------------------------- 1 | //! Traits for comparing distance between points and units. 2 | #![allow(clippy::wrong_self_convention)] 3 | 4 | use crate::{geometry::Point2, units::iter::filter_fold}; 5 | use std::{cmp::Ordering, vec::IntoIter}; 6 | 7 | #[cfg(feature = "rayon")] 8 | pub mod rayon; 9 | 10 | /// Basic trait for comparing distance. 11 | pub trait Distance: Into { 12 | /// Calculates squared euclidean distance from `self` to `other`. 13 | fn distance_squared>(self, other: P) -> f32 { 14 | let a = self.into(); 15 | let b = other.into(); 16 | 17 | let dx = a.x - b.x; 18 | let dy = a.y - b.y; 19 | 20 | dx * dx + dy * dy 21 | } 22 | 23 | /// Calculates euclidean distance from `self` to `other`. 24 | #[inline] 25 | fn distance>(self, other: P) -> f32 { 26 | self.distance_squared(other).sqrt() 27 | } 28 | /// Checks if distance between `self` and `other` is less than given `distance`. 29 | #[inline] 30 | fn is_closer>(self, distance: f32, other: P) -> bool { 31 | self.distance_squared(other) < distance * distance 32 | } 33 | /// Checks if distance between `self` and `other` is greater than given `distance`. 34 | #[inline] 35 | fn is_further>(self, distance: f32, other: P) -> bool { 36 | self.distance_squared(other) > distance * distance 37 | } 38 | } 39 | 40 | impl> Distance for T {} 41 | 42 | #[inline] 43 | fn cmp(a: &T, b: &T) -> Ordering { 44 | a.partial_cmp(b).unwrap() 45 | } 46 | 47 | #[inline] 48 | fn dist_to(target: P) -> impl Fn(&T, &T) -> Ordering 49 | where 50 | T: Distance + Copy, 51 | P: Into + Copy, 52 | { 53 | let f = move |u: &T| u.distance_squared(target); 54 | move |a, b| f(a).partial_cmp(&f(b)).unwrap() 55 | } 56 | 57 | /// Helper trait for iterators of items implementing [`Distance`]. 58 | pub trait DistanceIterator: Iterator + Sized 59 | where 60 | Self::Item: Distance + Copy, 61 | { 62 | /// Filters all items closer than given `distance` to `target`. 63 | fn closer>(self, distance: f32, target: T) -> Closer { 64 | Closer::new(self, distance, target.into()) 65 | } 66 | /// Filters all items further than given `distance` to `target`. 67 | fn further>(self, distance: f32, target: T) -> Further { 68 | Further::new(self, distance, target.into()) 69 | } 70 | 71 | /// Returns closest to `target` item in iterator. 72 | fn closest>(self, target: T) -> Option { 73 | let target = target.into(); 74 | self.min_by(dist_to(target)) 75 | } 76 | /// Returns furthest to `target` item in iterator. 77 | fn furthest>(self, target: T) -> Option { 78 | let target = target.into(); 79 | self.max_by(dist_to(target)) 80 | } 81 | 82 | /// Returns distance to closest to `target` item in iterator. 83 | fn closest_distance>(self, target: T) -> Option { 84 | self.closest_distance_squared(target).map(|dist| dist.sqrt()) 85 | } 86 | /// Returns distance to furthest to `target` item in iterator. 87 | fn furthest_distance>(self, target: T) -> Option { 88 | self.furthest_distance_squared(target).map(|dist| dist.sqrt()) 89 | } 90 | 91 | /// Returns squared distance to closest to `target` item in iterator. 92 | fn closest_distance_squared>(self, target: T) -> Option { 93 | let target = target.into(); 94 | self.map(|u| u.distance_squared(target)).min_by(cmp) 95 | } 96 | /// Returns squared distance to furthest to `target` item in iterator. 97 | fn furthest_distance_squared>(self, target: T) -> Option { 98 | let target = target.into(); 99 | self.map(|u| u.distance_squared(target)).max_by(cmp) 100 | } 101 | 102 | /// Returns iterator of items sorted by distance to `target`. 103 | /// 104 | /// This sort is stable (i.e., does not reorder equal elements) and `O(n * log(n))` worst-case. 105 | /// 106 | /// When applicable, unstable sorting is preferred because it is generally faster than stable sorting 107 | /// and it doesn't allocate auxiliary memory. See [`sort_unstable_by_distance`](Self::sort_unstable_by_distance). 108 | fn sort_by_distance>(self, target: T) -> IntoIter { 109 | let mut v: Vec<_> = self.collect(); 110 | let target = target.into(); 111 | v.sort_by(dist_to(target)); 112 | v.into_iter() 113 | } 114 | /// Returns iterator of items sorted by distance to `target`. 115 | /// 116 | /// This sort is unstable (i.e., may reorder equal elements), 117 | /// in-place (i.e., does not allocate), and `O(n * log(n))` worst-case. 118 | fn sort_unstable_by_distance>(self, target: T) -> IntoIter { 119 | let mut v: Vec<_> = self.collect(); 120 | let target = target.into(); 121 | v.sort_unstable_by(dist_to(target)); 122 | v.into_iter() 123 | } 124 | } 125 | 126 | /// Helper trait for sorting by distance `slice` and `Vec` of elements implementing [`Distance`]. 127 | pub trait DistanceSlice { 128 | /// Sorts slice by distance to target. 129 | /// 130 | /// This sort is stable (i.e., does not reorder equal elements) and `O(n * log(n))` worst-case. 131 | /// 132 | /// When applicable, unstable sorting is preferred because it is generally faster than stable sorting 133 | /// and it doesn't allocate auxiliary memory. See [`sort_unstable_by_distance`](Self::sort_unstable_by_distance). 134 | fn sort_by_distance>(&mut self, target: P); 135 | /// Sorts slice by distance to target. 136 | /// 137 | /// This sort is unstable (i.e., may reorder equal elements), 138 | /// in-place (i.e., does not allocate), and `O(n * log(n))` worst-case. 139 | fn sort_unstable_by_distance>(&mut self, target: P); 140 | } 141 | 142 | /// Helper trait for iterator of points, used to find center of these points. 143 | pub trait Center: Iterator + Sized 144 | where 145 | Self::Item: Into, 146 | { 147 | /// Returns center of all iterated points or `None` if iterator is empty. 148 | fn center(self) -> Option { 149 | let (sum, len) = self.fold((Point2::default(), 0), |(sum, len), p| (sum + p.into(), len + 1)); 150 | if len > 0 { 151 | Some(sum / len as f32) 152 | } else { 153 | None 154 | } 155 | } 156 | } 157 | 158 | // Implementations 159 | impl Center for I 160 | where 161 | I: Iterator + Sized, 162 | I::Item: Into, 163 | { 164 | } 165 | 166 | impl DistanceIterator for I 167 | where 168 | I: Iterator + Sized, 169 | I::Item: Distance + Copy, 170 | { 171 | } 172 | 173 | impl DistanceSlice for [T] { 174 | fn sort_by_distance>(&mut self, target: P) { 175 | let target = target.into(); 176 | self.sort_by(dist_to(target)) 177 | } 178 | fn sort_unstable_by_distance>(&mut self, target: P) { 179 | let target = target.into(); 180 | self.sort_unstable_by(dist_to(target)) 181 | } 182 | } 183 | 184 | // Macros to generate iterator implementation here 185 | 186 | macro_rules! iterator_methods { 187 | () => { 188 | #[inline] 189 | fn next(&mut self) -> Option { 190 | let pred = self.predicate(); 191 | self.iter.find(pred) 192 | } 193 | 194 | #[inline] 195 | fn size_hint(&self) -> (usize, Option) { 196 | (0, self.iter.size_hint().1) 197 | } 198 | 199 | #[inline] 200 | fn count(self) -> usize { 201 | let pred = self.predicate(); 202 | self.iter.map(|u| pred(&u) as usize).sum() 203 | } 204 | 205 | #[inline] 206 | fn fold(self, init: Acc, fold: Fold) -> Acc 207 | where 208 | Fold: FnMut(Acc, Self::Item) -> Acc, 209 | { 210 | let pred = self.predicate(); 211 | self.iter.fold(init, filter_fold(pred, fold)) 212 | } 213 | }; 214 | } 215 | 216 | macro_rules! double_ended_iterator_methods { 217 | () => { 218 | #[inline] 219 | fn next_back(&mut self) -> Option { 220 | let pred = self.predicate(); 221 | self.iter.rfind(pred) 222 | } 223 | 224 | #[inline] 225 | fn rfold(self, init: Acc, fold: Fold) -> Acc 226 | where 227 | Fold: FnMut(Acc, Self::Item) -> Acc, 228 | { 229 | let pred = self.predicate(); 230 | self.iter.rfold(init, filter_fold(pred, fold)) 231 | } 232 | }; 233 | } 234 | 235 | macro_rules! impl_simple_iterator { 236 | ($name:ident) => { 237 | impl Iterator for $name 238 | where 239 | I: Iterator, 240 | I::Item: Distance + Copy, 241 | { 242 | type Item = I::Item; 243 | 244 | iterator_methods!(); 245 | } 246 | 247 | impl DoubleEndedIterator for $name 248 | where 249 | I: DoubleEndedIterator, 250 | I::Item: Distance + Copy, 251 | { 252 | double_ended_iterator_methods!(); 253 | } 254 | }; 255 | } 256 | 257 | // Iterator adaptors here 258 | 259 | /// An iterator that filters items closer than given distance to target. 260 | #[derive(Clone)] 261 | pub struct Closer { 262 | iter: I, 263 | distance: f32, 264 | target: Point2, 265 | } 266 | impl Closer { 267 | fn new(iter: I, distance: f32, target: Point2) -> Self { 268 | Self { 269 | iter, 270 | distance, 271 | target, 272 | } 273 | } 274 | 275 | fn predicate(&self) -> impl Fn(&T) -> bool { 276 | let distance = self.distance; 277 | let target = self.target; 278 | move |u| u.is_closer(distance, target) 279 | } 280 | } 281 | impl_simple_iterator!(Closer); 282 | 283 | /// An iterator that filters items further than given distance to target. 284 | #[derive(Clone)] 285 | pub struct Further { 286 | iter: I, 287 | distance: f32, 288 | target: Point2, 289 | } 290 | impl Further { 291 | fn new(iter: I, distance: f32, target: Point2) -> Self { 292 | Self { 293 | iter, 294 | distance, 295 | target, 296 | } 297 | } 298 | 299 | fn predicate(&self) -> impl Fn(&T) -> bool { 300 | let distance = self.distance; 301 | let target = self.target; 302 | move |u| u.is_further(distance, target) 303 | } 304 | } 305 | impl_simple_iterator!(Further); 306 | -------------------------------------------------------------------------------- /src/distance/rayon.rs: -------------------------------------------------------------------------------- 1 | //! Parallelism for iterators over elements implementing [`Distance`](super::Distance). 2 | 3 | use super::{cmp, dist_to, Distance}; 4 | use crate::geometry::Point2; 5 | use rayon::{iter::plumbing::*, prelude::*, vec::IntoIter as IntoParIter}; 6 | 7 | /// Helper trait for parallel iterators implementing [`Distance`]. 8 | pub trait ParDistanceIterator: ParallelIterator 9 | where 10 | Self::Item: Distance + Copy, 11 | { 12 | /// Filters all items closer than given `distance` to `target`. 13 | fn closer>(self, distance: f32, target: T) -> Closer { 14 | Closer::new(self, distance, target.into()) 15 | } 16 | /// Filters all items further than given `distance` to `target`. 17 | fn further>(self, distance: f32, target: T) -> Further { 18 | Further::new(self, distance, target.into()) 19 | } 20 | 21 | /// Returns closest to `target` item in iterator. 22 | fn closest>(self, target: T) -> Option { 23 | let target = target.into(); 24 | self.min_by(dist_to(target)) 25 | } 26 | /// Returns furthest to `target` item in iterator. 27 | fn furthest>(self, target: T) -> Option { 28 | let target = target.into(); 29 | self.max_by(dist_to(target)) 30 | } 31 | 32 | /// Returns distance to closest to `target` item in iterator. 33 | fn closest_distance>(self, target: T) -> Option { 34 | self.closest_distance_squared(target).map(|dist| dist.sqrt()) 35 | } 36 | /// Returns distance to furthest to target item in iterator. 37 | fn furthest_distance>(self, target: T) -> Option { 38 | self.furthest_distance_squared(target).map(|dist| dist.sqrt()) 39 | } 40 | 41 | /// Returns squared distance to closest to `target` item in iterator. 42 | fn closest_distance_squared>(self, target: T) -> Option { 43 | let target = target.into(); 44 | self.map(|u| u.distance_squared(target)).min_by(cmp) 45 | } 46 | /// Returns squared distance to furthest to target item in iterator. 47 | fn furthest_distance_squared>(self, target: T) -> Option { 48 | let target = target.into(); 49 | self.map(|u| u.distance_squared(target)).max_by(cmp) 50 | } 51 | 52 | /// Returns iterator of items sorted by distance to `target`. 53 | /// 54 | /// This sort is stable (i.e. does not reorder equal elements) and `O(n log n)` worst-case. 55 | /// 56 | /// When applicable, unstable sorting is preferred because it is generally faster than stable sorting 57 | /// and it doesn't allocate auxiliary memory. See [`sort_unstable_by_distance`](Self::sort_unstable_by_distance). 58 | fn sort_by_distance>(self, target: T) -> IntoParIter { 59 | let mut v = Vec::from_par_iter(self); 60 | let target = target.into(); 61 | v.par_sort_by(dist_to(target)); 62 | v.into_par_iter() 63 | } 64 | /// Returns iterator of items sorted by distance to target. 65 | /// 66 | /// This sort is unstable (i.e. may reorder equal elements), 67 | /// in-place (i.e. does not allocate), and `O(n log n)` worst-case. 68 | fn sort_unstable_by_distance>(self, target: T) -> IntoParIter { 69 | let mut v = Vec::from_par_iter(self); 70 | let target = target.into(); 71 | v.par_sort_unstable_by(dist_to(target)); 72 | v.into_par_iter() 73 | } 74 | } 75 | 76 | /// Helper trait for parallel sorting by distance `slice` and `Vec` of elements implementing [`Distance`]. 77 | pub trait ParDistanceSlice: ParallelSliceMut 78 | where 79 | T: Distance + Copy + Send, 80 | { 81 | /// Sorts slice in parallel by distance to target. 82 | /// 83 | /// This sort is stable (i.e. does not reorder equal elements) and `O(n log n)` worst-case. 84 | /// 85 | /// When applicable, unstable sorting is preferred because it is generally faster than stable sorting 86 | /// and it doesn't allocate auxiliary memory. 87 | /// See [`par_sort_unstable_by_distance`](Self::par_sort_unstable_by_distance). 88 | fn par_sort_by_distance>(&mut self, target: P) { 89 | let target = target.into(); 90 | self.par_sort_by(dist_to(target)) 91 | } 92 | /// Sorts slice in parallel by distance to target. 93 | /// 94 | /// This sort is unstable (i.e. may reorder equal elements), 95 | /// in-place (i.e. does not allocate), and `O(n log n)` worst-case. 96 | fn par_sort_unstable_by_distance>(&mut self, target: P) { 97 | let target = target.into(); 98 | self.par_sort_unstable_by(dist_to(target)) 99 | } 100 | } 101 | 102 | /// Helper trait for parallel iterator of points, used to find center of these points. 103 | pub trait ParCenter: ParallelIterator 104 | where 105 | Self::Item: Into, 106 | { 107 | /// Returns center of all iterated points or `None` if iterator is empty. 108 | fn center(self) -> Option { 109 | let (sum, len) = self.map(|p| (p.into(), 1)).reduce( 110 | || (Point2::default(), 0), 111 | |(sum1, len1), (sum2, len2)| (sum1 + sum2, len1 + len2), 112 | ); 113 | if len > 0 { 114 | Some(sum / len as f32) 115 | } else { 116 | None 117 | } 118 | } 119 | } 120 | 121 | impl ParCenter for I 122 | where 123 | I: ParallelIterator, 124 | I::Item: Into, 125 | { 126 | } 127 | 128 | impl ParDistanceIterator for I 129 | where 130 | I: ParallelIterator, 131 | I::Item: Distance + Copy, 132 | { 133 | } 134 | 135 | impl ParDistanceSlice for [T] {} 136 | 137 | // Macros to generate parallel iterator implementation here 138 | 139 | macro_rules! iterator_methods { 140 | () => { 141 | fn drive_unindexed(self, consumer: C) -> C::Result 142 | where 143 | C: UnindexedConsumer, 144 | { 145 | let pred = self.predicate(); 146 | self.iter 147 | .drive_unindexed(FilterConsumer::new(consumer, &pred)) 148 | } 149 | }; 150 | } 151 | 152 | macro_rules! impl_simple_iterator { 153 | ($name:ident) => { 154 | impl ParallelIterator for $name 155 | where 156 | I: ParallelIterator, 157 | I::Item: Distance + Copy, 158 | { 159 | type Item = I::Item; 160 | 161 | iterator_methods!(); 162 | } 163 | }; 164 | } 165 | 166 | // Consumer implementation 167 | 168 | struct FilterConsumer<'p, C, P> { 169 | base: C, 170 | filter_op: &'p P, 171 | } 172 | 173 | impl<'p, C, P> FilterConsumer<'p, C, P> { 174 | fn new(base: C, filter_op: &'p P) -> Self { 175 | FilterConsumer { base, filter_op } 176 | } 177 | } 178 | 179 | impl<'p, T, C, P: 'p> Consumer for FilterConsumer<'p, C, P> 180 | where 181 | C: Consumer, 182 | P: Fn(&T) -> bool + Sync, 183 | T: Distance + Copy, 184 | { 185 | type Folder = FilterFolder<'p, C::Folder, P>; 186 | type Reducer = C::Reducer; 187 | type Result = C::Result; 188 | 189 | fn split_at(self, index: usize) -> (Self, Self, C::Reducer) { 190 | let (left, right, reducer) = self.base.split_at(index); 191 | ( 192 | FilterConsumer::new(left, self.filter_op), 193 | FilterConsumer::new(right, self.filter_op), 194 | reducer, 195 | ) 196 | } 197 | 198 | fn into_folder(self) -> Self::Folder { 199 | FilterFolder { 200 | base: self.base.into_folder(), 201 | filter_op: self.filter_op, 202 | } 203 | } 204 | 205 | fn full(&self) -> bool { 206 | self.base.full() 207 | } 208 | } 209 | 210 | impl<'p, T, C, P: 'p> UnindexedConsumer for FilterConsumer<'p, C, P> 211 | where 212 | C: UnindexedConsumer, 213 | P: Fn(&T) -> bool + Sync, 214 | T: Distance + Copy, 215 | { 216 | fn split_off_left(&self) -> Self { 217 | FilterConsumer::new(self.base.split_off_left(), self.filter_op) 218 | } 219 | 220 | fn to_reducer(&self) -> Self::Reducer { 221 | self.base.to_reducer() 222 | } 223 | } 224 | 225 | struct FilterFolder<'p, C, P> { 226 | base: C, 227 | filter_op: &'p P, 228 | } 229 | 230 | impl<'p, C, P, T> Folder for FilterFolder<'p, C, P> 231 | where 232 | C: Folder, 233 | P: Fn(&T) -> bool + 'p, 234 | T: Distance + Copy, 235 | { 236 | type Result = C::Result; 237 | 238 | fn consume(self, item: T) -> Self { 239 | let filter_op = self.filter_op; 240 | if filter_op(&item) { 241 | let base = self.base.consume(item); 242 | FilterFolder { base, filter_op } 243 | } else { 244 | self 245 | } 246 | } 247 | 248 | fn complete(self) -> Self::Result { 249 | self.base.complete() 250 | } 251 | 252 | fn full(&self) -> bool { 253 | self.base.full() 254 | } 255 | } 256 | 257 | // Parallel Iterator adaptors here 258 | 259 | /// An iterator that filters items closer than given distance to target. 260 | #[derive(Clone)] 261 | pub struct Closer { 262 | iter: I, 263 | distance: f32, 264 | target: Point2, 265 | } 266 | impl Closer { 267 | fn new(iter: I, distance: f32, target: Point2) -> Self { 268 | Self { 269 | iter, 270 | distance, 271 | target, 272 | } 273 | } 274 | 275 | fn predicate(&self) -> impl Fn(&T) -> bool { 276 | let distance = self.distance; 277 | let target = self.target; 278 | move |u| u.is_closer(distance, target) 279 | } 280 | } 281 | impl_simple_iterator!(Closer); 282 | 283 | /// An iterator that filters items further than given distance to target. 284 | #[derive(Clone)] 285 | pub struct Further { 286 | iter: I, 287 | distance: f32, 288 | target: Point2, 289 | } 290 | impl Further { 291 | fn new(iter: I, distance: f32, target: Point2) -> Self { 292 | Self { 293 | iter, 294 | distance, 295 | target, 296 | } 297 | } 298 | 299 | fn predicate(&self) -> impl Fn(&T) -> bool { 300 | let distance = self.distance; 301 | let target = self.target; 302 | move |u| u.is_further(distance, target) 303 | } 304 | } 305 | impl_simple_iterator!(Further); 306 | -------------------------------------------------------------------------------- /src/game_data.rs: -------------------------------------------------------------------------------- 1 | //! Information about units, ablities, upgrades, buffs and effects provided by API stored here. 2 | #![allow(missing_docs)] 3 | 4 | use crate::{ 5 | ids::{AbilityId, BuffId, EffectId, UnitTypeId, UpgradeId}, 6 | player::Race, 7 | FromProto, TryFromProto, 8 | }; 9 | use num_traits::FromPrimitive; 10 | use rustc_hash::FxHashMap; 11 | use sc2_proto::{ 12 | data::{ 13 | AbilityData as ProtoAbilityData, AbilityData_Target, Attribute as ProtoAttribute, 14 | BuffData as ProtoBuffData, EffectData as ProtoEffectData, UnitTypeData as ProtoUnitTypeData, 15 | UpgradeData as ProtoUpgradeData, Weapon as ProtoWeapon, Weapon_TargetType, 16 | }, 17 | sc2api::ResponseData, 18 | }; 19 | #[cfg(feature = "serde")] 20 | use serde::{Deserialize, Serialize}; 21 | 22 | /// All the data about different ids stored here. 23 | /// Can be accessed through [`game_data`](crate::bot::Bot::game_data) field. 24 | #[derive(Default, Clone)] 25 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 26 | pub struct GameData { 27 | /// Information about abilities mapped to `AbilityId`s. 28 | pub abilities: FxHashMap, 29 | /// Information about units mapped to `UnitTypeId`s. 30 | pub units: FxHashMap, 31 | /// Information about upgrades mapped to `UpgradeId`s. 32 | pub upgrades: FxHashMap, 33 | /// Information about buffs mapped to `BuffId`s. 34 | pub buffs: FxHashMap, 35 | /// Information about effects mapped to `EffectId`s. 36 | pub effects: FxHashMap, 37 | } 38 | impl FromProto for GameData { 39 | fn from_proto(data: ResponseData) -> Self { 40 | Self { 41 | abilities: data 42 | .get_abilities() 43 | .iter() 44 | .filter_map(|a| AbilityData::try_from_proto(a).map(|data| (data.id, data))) 45 | .collect(), 46 | units: data 47 | .get_units() 48 | .iter() 49 | .filter_map(|u| UnitTypeData::try_from_proto(u).map(|data| (data.id, data))) 50 | .collect(), 51 | upgrades: data 52 | .get_upgrades() 53 | .iter() 54 | .filter_map(|u| UpgradeData::try_from_proto(u).map(|data| (data.id, data))) 55 | .collect(), 56 | buffs: data 57 | .get_buffs() 58 | .iter() 59 | .filter_map(|b| BuffData::try_from_proto(b).map(|data| (data.id, data))) 60 | .collect(), 61 | effects: data 62 | .get_effects() 63 | .iter() 64 | .filter_map(|e| EffectData::try_from_proto(e).map(|data| (data.id, data))) 65 | .collect(), 66 | } 67 | } 68 | } 69 | 70 | /// Cost of an item (`UnitTypeId` or `UpgradeId`) in resources, supply and time. 71 | #[derive(Debug, Default)] 72 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 73 | pub struct Cost { 74 | pub minerals: u32, 75 | pub vespene: u32, 76 | pub supply: f32, 77 | pub time: f32, 78 | } 79 | 80 | /// Possible target of ability, needed when giving commands to units. 81 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 82 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 83 | pub enum AbilityTarget { 84 | None, 85 | Point, 86 | Unit, 87 | PointOrUnit, 88 | PointOrNone, 89 | } 90 | impl FromProto for AbilityTarget { 91 | fn from_proto(target: AbilityData_Target) -> Self { 92 | match target { 93 | AbilityData_Target::None => AbilityTarget::None, 94 | AbilityData_Target::Point => AbilityTarget::Point, 95 | AbilityData_Target::Unit => AbilityTarget::Unit, 96 | AbilityData_Target::PointOrUnit => AbilityTarget::PointOrUnit, 97 | AbilityData_Target::PointOrNone => AbilityTarget::PointOrNone, 98 | } 99 | } 100 | } 101 | 102 | /// Differents attributes of units. 103 | #[variant_checkers] 104 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] 105 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 106 | pub enum Attribute { 107 | Light, 108 | Armored, 109 | Biological, 110 | Mechanical, 111 | Robotic, 112 | Psionic, 113 | Massive, 114 | Structure, 115 | Hover, 116 | Heroic, 117 | Summoned, 118 | } 119 | impl FromProto for Attribute { 120 | fn from_proto(attribute: ProtoAttribute) -> Self { 121 | match attribute { 122 | ProtoAttribute::Light => Attribute::Light, 123 | ProtoAttribute::Armored => Attribute::Armored, 124 | ProtoAttribute::Biological => Attribute::Biological, 125 | ProtoAttribute::Mechanical => Attribute::Mechanical, 126 | ProtoAttribute::Robotic => Attribute::Robotic, 127 | ProtoAttribute::Psionic => Attribute::Psionic, 128 | ProtoAttribute::Massive => Attribute::Massive, 129 | ProtoAttribute::Structure => Attribute::Structure, 130 | ProtoAttribute::Hover => Attribute::Hover, 131 | ProtoAttribute::Heroic => Attribute::Heroic, 132 | ProtoAttribute::Summoned => Attribute::Summoned, 133 | } 134 | } 135 | } 136 | 137 | /// Possible target of unit's weapon. 138 | #[variant_checkers] 139 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] 140 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 141 | pub enum TargetType { 142 | Ground, 143 | Air, 144 | Any, 145 | } 146 | impl FromProto for TargetType { 147 | fn from_proto(target_type: Weapon_TargetType) -> Self { 148 | match target_type { 149 | Weapon_TargetType::Ground => TargetType::Ground, 150 | Weapon_TargetType::Air => TargetType::Air, 151 | Weapon_TargetType::Any => TargetType::Any, 152 | } 153 | } 154 | } 155 | 156 | /// Weapon's characteristic. 157 | #[derive(Clone)] 158 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 159 | pub struct Weapon { 160 | /// Possible targets. 161 | pub target: TargetType, 162 | /// Usual damage. 163 | pub damage: u32, 164 | /// Addidional damage vs units with specific attribute. 165 | pub damage_bonus: Vec<(Attribute, u32)>, 166 | /// Number of attacks per use. 167 | pub attacks: u32, 168 | /// Maximum range. 169 | pub range: f32, 170 | /// Cooldown (in seconds * game speed). 171 | pub speed: f32, 172 | } 173 | impl FromProto<&ProtoWeapon> for Weapon { 174 | fn from_proto(weapon: &ProtoWeapon) -> Self { 175 | Self { 176 | target: TargetType::from_proto(weapon.get_field_type()), 177 | damage: weapon.get_damage() as u32, 178 | damage_bonus: weapon 179 | .get_damage_bonus() 180 | .iter() 181 | .map(|db| (Attribute::from_proto(db.get_attribute()), db.get_bonus() as u32)) 182 | .collect(), 183 | attacks: weapon.get_attacks(), 184 | range: weapon.get_range(), 185 | speed: weapon.get_speed(), 186 | } 187 | } 188 | } 189 | 190 | /// Information about specific ability. 191 | #[derive(Clone)] 192 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 193 | pub struct AbilityData { 194 | pub id: AbilityId, 195 | pub link_name: String, 196 | pub link_index: u32, 197 | pub button_name: Option, 198 | pub friendly_name: Option, 199 | pub hotkey: Option, 200 | pub remaps_to_ability_id: Option, 201 | /// Ability is available in current game version. 202 | pub available: bool, 203 | /// Possible target of ability, needed when giving commands to units. 204 | pub target: AbilityTarget, 205 | /// Ability can be used on minimap. 206 | pub allow_minimap: bool, 207 | /// Ability can be autocasted. 208 | pub allow_autocast: bool, 209 | /// Ability is used to construct a building. 210 | pub is_building: bool, 211 | /// Half of the building size. 212 | pub footprint_radius: Option, 213 | pub is_instant_placement: bool, 214 | /// Maximum range to target of the ability. 215 | pub cast_range: Option, 216 | } 217 | impl TryFromProto<&ProtoAbilityData> for AbilityData { 218 | fn try_from_proto(a: &ProtoAbilityData) -> Option { 219 | Some(Self { 220 | id: AbilityId::from_u32(a.get_ability_id())?, 221 | link_name: a.get_link_name().to_string(), 222 | link_index: a.get_link_index(), 223 | button_name: a.button_name.as_ref().cloned(), 224 | friendly_name: a.friendly_name.as_ref().cloned(), 225 | hotkey: a.hotkey.as_ref().cloned(), 226 | remaps_to_ability_id: a.remaps_to_ability_id.and_then(AbilityId::from_u32), 227 | available: a.get_available(), 228 | target: AbilityTarget::from_proto(a.get_target()), 229 | allow_minimap: a.get_allow_minimap(), 230 | allow_autocast: a.get_allow_autocast(), 231 | is_building: a.get_is_building(), 232 | footprint_radius: a.footprint_radius, 233 | is_instant_placement: a.get_is_instant_placement(), 234 | cast_range: a.cast_range, 235 | }) 236 | } 237 | } 238 | 239 | /// Information about specific unit type. 240 | #[derive(Clone)] 241 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 242 | pub struct UnitTypeData { 243 | pub id: UnitTypeId, 244 | pub name: String, 245 | /// Unit is available in current game version. 246 | pub available: bool, 247 | /// Space usage in transports and bunkers. 248 | pub cargo_size: u32, 249 | pub mineral_cost: u32, 250 | pub vespene_cost: u32, 251 | pub food_required: f32, 252 | pub food_provided: f32, 253 | /// Ability used to produce unit or `None` if unit can't be produced. 254 | pub ability: Option, 255 | /// Race of unit. 256 | pub race: Race, 257 | pub build_time: f32, 258 | /// Unit contains vespene (i.e. is vespene geyser). 259 | pub has_vespene: bool, 260 | /// Unit contains minerals (i.e. is mineral field). 261 | pub has_minerals: bool, 262 | pub sight_range: f32, 263 | pub tech_alias: Vec, 264 | pub unit_alias: Option, 265 | pub tech_requirement: Option, 266 | pub require_attached: bool, 267 | pub attributes: Vec, 268 | pub movement_speed: f32, 269 | pub armor: i32, 270 | pub weapons: Vec, 271 | } 272 | impl UnitTypeData { 273 | pub fn cost(&self) -> Cost { 274 | Cost { 275 | minerals: self.mineral_cost, 276 | vespene: self.vespene_cost, 277 | supply: self.food_required, 278 | time: self.build_time, 279 | } 280 | } 281 | } 282 | impl TryFromProto<&ProtoUnitTypeData> for UnitTypeData { 283 | fn try_from_proto(u: &ProtoUnitTypeData) -> Option { 284 | Some(Self { 285 | id: UnitTypeId::from_u32(u.get_unit_id())?, 286 | name: u.get_name().to_string(), 287 | available: u.get_available(), 288 | cargo_size: u.get_cargo_size(), 289 | mineral_cost: u.get_mineral_cost(), 290 | vespene_cost: u.get_vespene_cost(), 291 | food_required: u.get_food_required(), 292 | food_provided: u.get_food_provided(), 293 | ability: u.ability_id.and_then(AbilityId::from_u32), 294 | race: Race::from_proto(u.get_race()), 295 | build_time: u.get_build_time(), 296 | has_vespene: u.get_has_vespene(), 297 | has_minerals: u.get_has_minerals(), 298 | sight_range: u.get_sight_range(), 299 | tech_alias: u 300 | .get_tech_alias() 301 | .iter() 302 | .filter_map(|a| UnitTypeId::from_u32(*a)) 303 | .collect(), 304 | unit_alias: u.unit_alias.and_then(UnitTypeId::from_u32), 305 | tech_requirement: u.tech_requirement.and_then(UnitTypeId::from_u32), 306 | require_attached: u.get_require_attached(), 307 | attributes: u 308 | .get_attributes() 309 | .iter() 310 | .map(|&a| Attribute::from_proto(a)) 311 | .collect(), 312 | movement_speed: u.get_movement_speed(), 313 | armor: u.get_armor() as i32, 314 | weapons: u.get_weapons().iter().map(Weapon::from_proto).collect(), 315 | }) 316 | } 317 | } 318 | 319 | /// Information about specific upgrade. 320 | #[derive(Clone)] 321 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 322 | pub struct UpgradeData { 323 | pub id: UpgradeId, 324 | /// Ability used to research the upgrade. 325 | pub ability: AbilityId, 326 | pub name: String, 327 | pub mineral_cost: u32, 328 | pub vespene_cost: u32, 329 | pub research_time: f32, 330 | } 331 | impl UpgradeData { 332 | pub fn cost(&self) -> Cost { 333 | Cost { 334 | minerals: self.mineral_cost, 335 | vespene: self.vespene_cost, 336 | supply: 0.0, 337 | time: self.research_time, 338 | } 339 | } 340 | } 341 | impl TryFromProto<&ProtoUpgradeData> for UpgradeData { 342 | fn try_from_proto(u: &ProtoUpgradeData) -> Option { 343 | Some(Self { 344 | id: UpgradeId::from_u32(u.get_upgrade_id())?, 345 | ability: AbilityId::from_u32(u.get_ability_id())?, 346 | name: u.get_name().to_string(), 347 | mineral_cost: u.get_mineral_cost(), 348 | vespene_cost: u.get_vespene_cost(), 349 | research_time: u.get_research_time(), 350 | }) 351 | } 352 | } 353 | 354 | /// Information about specific buff. 355 | #[derive(Clone)] 356 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 357 | pub struct BuffData { 358 | pub id: BuffId, 359 | pub name: String, 360 | } 361 | impl TryFromProto<&ProtoBuffData> for BuffData { 362 | fn try_from_proto(b: &ProtoBuffData) -> Option { 363 | Some(Self { 364 | id: BuffId::from_u32(b.get_buff_id())?, 365 | name: b.get_name().to_string(), 366 | }) 367 | } 368 | } 369 | 370 | /// Information about specific effect. 371 | #[derive(Clone)] 372 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 373 | pub struct EffectData { 374 | pub id: EffectId, 375 | pub name: String, 376 | pub friendly_name: String, 377 | pub radius: f32, 378 | /// Targets affected by this effect. 379 | pub target: TargetType, 380 | /// `true` if effect affects allied units. 381 | pub friendly_fire: bool, 382 | } 383 | impl TryFromProto<&ProtoEffectData> for EffectData { 384 | fn try_from_proto(e: &ProtoEffectData) -> Option { 385 | EffectId::from_u32(e.get_effect_id()).map(|id| Self { 386 | id, 387 | name: e.get_name().to_string(), 388 | friendly_name: e.get_friendly_name().to_string(), 389 | radius: e.get_radius(), 390 | target: match id { 391 | EffectId::Null 392 | | EffectId::PsiStormPersistent 393 | | EffectId::ScannerSweep 394 | | EffectId::NukePersistent 395 | | EffectId::RavagerCorrosiveBileCP => TargetType::Any, 396 | _ => TargetType::Ground, 397 | }, 398 | friendly_fire: matches!( 399 | id, 400 | EffectId::PsiStormPersistent | EffectId::NukePersistent | EffectId::RavagerCorrosiveBileCP 401 | ), 402 | }) 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /src/game_info.rs: -------------------------------------------------------------------------------- 1 | //! Constant information about map, populated on first step. 2 | 3 | use crate::{ 4 | bot::Rs, 5 | geometry::{Point2, Rect, Size}, 6 | pixel_map::{ByteMap, PixelMap}, 7 | player::{AIBuild, Difficulty, PlayerType, Race}, 8 | FromProto, 9 | }; 10 | use rustc_hash::FxHashMap; 11 | use sc2_proto::sc2api::ResponseGameInfo; 12 | use std::path::Path; 13 | 14 | /// Structure where all map information stored. 15 | #[derive(Default, Clone)] 16 | pub struct GameInfo { 17 | /// Map name bot playing on, which depends on sc2 localization language. 18 | pub map_name: String, 19 | /// Map name bot playing on, which depends on file name. 20 | pub map_name_path: String, 21 | /// Mods used on that map. 22 | pub mod_names: Vec, 23 | /// Path to the map on current computer. 24 | pub local_map_path: String, 25 | /// Players on this map, mapped by their ids. 26 | pub players: FxHashMap, 27 | /// Full size of the map. 28 | pub map_size: Size, 29 | /// Grid with information about pathable tiles on that map. 30 | pub pathing_grid: PixelMap, 31 | /// Grid with information about terrain height on that map. 32 | pub terrain_height: Rs, 33 | /// Grid with information about buildable tiles on that map. 34 | pub placement_grid: PixelMap, 35 | /// Usually maps have some unplayable area around it, where units can't exist. 36 | /// This rectangle is only playble area on that map. 37 | pub playable_area: Rect, 38 | /// All starting locations of opponents. 39 | pub start_locations: Vec, 40 | /// Center of the map. 41 | pub map_center: Point2, 42 | } 43 | impl FromProto for GameInfo { 44 | fn from_proto(game_info: ResponseGameInfo) -> Self { 45 | let start_raw = game_info.get_start_raw(); 46 | let map_size = start_raw.get_map_size(); 47 | let area = start_raw.get_playable_area(); 48 | let area_p0 = area.get_p0(); 49 | let area_p1 = area.get_p1(); 50 | let area_p0_x = area_p0.get_x(); 51 | let area_p0_y = area_p0.get_y(); 52 | let area_p1_x = area_p1.get_x(); 53 | let area_p1_y = area_p1.get_y(); 54 | let local_map_path = game_info.get_local_map_path().to_string(); 55 | Self { 56 | map_name: game_info.get_map_name().to_string(), 57 | mod_names: game_info.get_mod_names().to_vec(), 58 | map_name_path: Path::new(&local_map_path) 59 | .file_stem() 60 | .unwrap_or_default() 61 | .to_str() 62 | .unwrap() 63 | .to_string(), 64 | local_map_path, 65 | players: game_info 66 | .get_player_info() 67 | .iter() 68 | .map(|i| { 69 | let id = i.get_player_id(); 70 | ( 71 | id, 72 | PlayerInfo { 73 | id, 74 | player_type: PlayerType::from_proto(i.get_field_type()), 75 | race_requested: Race::from_proto(i.get_race_requested()), 76 | race_actual: i.race_actual.map(Race::from_proto), 77 | difficulty: i.difficulty.map(Difficulty::from_proto), 78 | ai_build: i.ai_build.map(AIBuild::from_proto), 79 | player_name: i.player_name.as_ref().cloned(), 80 | }, 81 | ) 82 | }) 83 | .collect(), 84 | map_size: Size::new(map_size.get_x() as usize, map_size.get_y() as usize), 85 | pathing_grid: PixelMap::from_proto(start_raw.get_pathing_grid()), 86 | terrain_height: Rs::new(ByteMap::from_proto(start_raw.get_terrain_height())), 87 | placement_grid: PixelMap::from_proto(start_raw.get_placement_grid()), 88 | playable_area: Rect::new( 89 | area_p0_x as usize, 90 | area_p0_y as usize, 91 | area_p1_x as usize, 92 | area_p1_y as usize, 93 | ), 94 | start_locations: start_raw 95 | .get_start_locations() 96 | .iter() 97 | .map(Point2::from_proto) 98 | .collect(), 99 | map_center: Point2::new( 100 | (area_p0_x + (area_p1_x - area_p0_x) / 2) as f32, 101 | (area_p0_y + (area_p1_y - area_p0_y) / 2) as f32, 102 | ), 103 | } 104 | } 105 | } 106 | 107 | /// Information about player. 108 | #[derive(Clone)] 109 | pub struct PlayerInfo { 110 | /// Player id. 111 | pub id: u32, 112 | /// Player type, can be `Participant`, `Computer` or `Observer`. 113 | pub player_type: PlayerType, 114 | /// Requested race, can be random. 115 | pub race_requested: Race, 116 | /// Actual race, it's never random, `None` for opponents. 117 | pub race_actual: Option, 118 | /// Difficulty, populated only for computer opponents. 119 | pub difficulty: Option, 120 | /// AI Build, populated only for computer opponents. 121 | pub ai_build: Option, 122 | /// In-game name of player. 123 | pub player_name: Option, 124 | } 125 | -------------------------------------------------------------------------------- /src/geometry.rs: -------------------------------------------------------------------------------- 1 | //! Things you liked (hated) at school, now in SC2. 2 | //! 3 | //! Countains various geometric primitives with useful helper methods. 4 | 5 | use crate::{distance::Distance, unit::Radius, FromProto, IntoProto}; 6 | use sc2_proto::common::{Point, Point2D}; 7 | use std::{ 8 | hash::{Hash, Hasher}, 9 | iter::Sum, 10 | ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}, 11 | }; 12 | 13 | /// Size of 2D rectangle. 14 | #[allow(missing_docs)] 15 | #[derive(Debug, Default, Copy, Clone)] 16 | pub struct Size { 17 | pub x: usize, 18 | pub y: usize, 19 | } 20 | impl Size { 21 | /// Constructs new `Size` structure with given `x` and `y` size. 22 | pub fn new(x: usize, y: usize) -> Self { 23 | Self { x, y } 24 | } 25 | } 26 | 27 | /// Rectangle from (x0, y0) to (x1, y1). 28 | #[allow(missing_docs)] 29 | #[derive(Debug, Default, Copy, Clone)] 30 | pub struct Rect { 31 | pub x0: usize, 32 | pub y0: usize, 33 | pub x1: usize, 34 | pub y1: usize, 35 | } 36 | impl Rect { 37 | /// Constructs new rectangle with given coordinates. 38 | pub fn new(x0: usize, y0: usize, x1: usize, y1: usize) -> Self { 39 | Self { x0, y0, x1, y1 } 40 | } 41 | } 42 | 43 | /// Point on 2D grid, the most frequently used geometric primitive. 44 | #[allow(missing_docs)] 45 | #[derive(Debug, Default, Copy, Clone)] 46 | pub struct Point2 { 47 | pub x: f32, 48 | pub y: f32, 49 | } 50 | 51 | #[allow(clippy::len_without_is_empty)] 52 | impl Point2 { 53 | /// Constructs new 2D Point with given coordinates. 54 | pub fn new(x: f32, y: f32) -> Self { 55 | Self { x, y } 56 | } 57 | /// Returns new point with offset towards `other` on given distance. 58 | pub fn towards(self, other: Self, offset: f32) -> Self { 59 | self + (other - self) / self.distance(other) * offset 60 | } 61 | /// Returns new point with offset towards given angle on given distance. 62 | pub fn towards_angle(self, angle: f32, offset: f32) -> Self { 63 | self.offset(offset * angle.cos(), offset * angle.sin()) 64 | } 65 | /// Returns new point with given offset. 66 | pub fn offset(self, x: f32, y: f32) -> Self { 67 | Self { 68 | x: self.x + x, 69 | y: self.y + y, 70 | } 71 | } 72 | /// Returns points where circles with centers `self` and `other`, 73 | /// and given radius intersect, or `None` if they aren't intersect. 74 | pub fn circle_intersection(self, other: Self, radius: f32) -> Option<[Self; 2]> { 75 | if self == other { 76 | return None; 77 | } 78 | 79 | let vec_to_center = (other - self) / 2.0; 80 | let half_distance = vec_to_center.len(); 81 | 82 | if radius < half_distance { 83 | return None; 84 | } 85 | 86 | let remaining_distance = (radius * radius - half_distance * half_distance).sqrt(); 87 | let stretch_factor = remaining_distance / half_distance; 88 | 89 | let center = self + vec_to_center; 90 | let vec_stretched = vec_to_center * stretch_factor; 91 | Some([ 92 | center + vec_stretched.rotate90(true), 93 | center + vec_stretched.rotate90(false), 94 | ]) 95 | } 96 | 97 | /// Returns squared length of the vector. 98 | pub fn len_squared(self) -> f32 { 99 | self.x.powi(2) + self.y.powi(2) 100 | } 101 | /// Returns length of the vector. 102 | pub fn len(self) -> f32 { 103 | self.len_squared().sqrt() 104 | } 105 | /// Normalizes the vector. 106 | pub fn normalize(self) -> Self { 107 | self / self.len() 108 | } 109 | /// Rotates the vector on given angle. 110 | pub fn rotate(self, angle: f32) -> Self { 111 | let (s, c) = angle.sin_cos(); 112 | let (x, y) = (self.x, self.y); 113 | Self { 114 | x: c * x - s * y, 115 | y: s * x + c * y, 116 | } 117 | } 118 | /// Fast rotation of the vector on 90 degrees. 119 | pub fn rotate90(self, clockwise: bool) -> Self { 120 | if clockwise { 121 | Self::new(self.y, -self.x) 122 | } else { 123 | Self::new(-self.y, self.x) 124 | } 125 | } 126 | /// Dot product. 127 | pub fn dot(self, other: Self) -> f32 { 128 | self.x * other.x + self.y * other.y 129 | } 130 | 131 | /// Returns rounded point. 132 | pub fn round(self) -> Self { 133 | Self { 134 | x: self.x.round(), 135 | y: self.y.round(), 136 | } 137 | } 138 | /// Returns point rounded to closest lower integer. 139 | pub fn floor(self) -> Self { 140 | Self { 141 | x: self.x.floor(), 142 | y: self.y.floor(), 143 | } 144 | } 145 | /// Returns point rounded to closest greater integer. 146 | pub fn ceil(self) -> Self { 147 | Self { 148 | x: self.x.ceil(), 149 | y: self.y.ceil(), 150 | } 151 | } 152 | /// Returns point with absolute coordinates. 153 | pub fn abs(self) -> Self { 154 | Self { 155 | x: self.x.abs(), 156 | y: self.y.abs(), 157 | } 158 | } 159 | /// Returns 4 closest neighbors of point. 160 | pub fn neighbors4(self) -> [Self; 4] { 161 | [ 162 | self.offset(1.0, 0.0), 163 | self.offset(-1.0, 0.0), 164 | self.offset(0.0, 1.0), 165 | self.offset(0.0, -1.0), 166 | ] 167 | } 168 | /// Returns 4 closest diagonal neighbors of point. 169 | pub fn neighbors4diagonal(self) -> [Self; 4] { 170 | [ 171 | self.offset(1.0, 1.0), 172 | self.offset(-1.0, -1.0), 173 | self.offset(1.0, -1.0), 174 | self.offset(-1.0, 1.0), 175 | ] 176 | } 177 | /// Returns 8 closest neighbors of point. 178 | pub fn neighbors8(self) -> [Self; 8] { 179 | [ 180 | self.offset(1.0, 0.0), 181 | self.offset(-1.0, 0.0), 182 | self.offset(0.0, 1.0), 183 | self.offset(0.0, -1.0), 184 | self.offset(1.0, 1.0), 185 | self.offset(-1.0, -1.0), 186 | self.offset(1.0, -1.0), 187 | self.offset(-1.0, 1.0), 188 | ] 189 | } 190 | /// Returns tuple with point's coordinates. 191 | pub fn as_tuple(self) -> (f32, f32) { 192 | (self.x, self.y) 193 | } 194 | /// Converts 2D Point to 3D Point using given `z` value. 195 | pub fn to3(self, z: f32) -> Point3 { 196 | Point3 { 197 | x: self.x, 198 | y: self.y, 199 | z, 200 | } 201 | } 202 | } 203 | 204 | impl PartialEq for Point2 { 205 | fn eq(&self, other: &Self) -> bool { 206 | // (self.x - other.x).abs() < f32::EPSILON && (self.y - other.y).abs() < f32::EPSILON 207 | self.x as i32 == other.x as i32 && self.y as i32 == other.y as i32 208 | } 209 | } 210 | impl Eq for Point2 {} 211 | impl Hash for Point2 { 212 | fn hash(&self, state: &mut H) { 213 | (self.x as i32).hash(state); 214 | (self.y as i32).hash(state); 215 | } 216 | } 217 | 218 | impl From<&Point2> for Point2 { 219 | #[inline] 220 | fn from(p: &Point2) -> Self { 221 | *p 222 | } 223 | } 224 | impl From for (usize, usize) { 225 | #[inline] 226 | fn from(p: Point2) -> Self { 227 | (p.x as usize, p.y as usize) 228 | } 229 | } 230 | impl From<(usize, usize)> for Point2 { 231 | #[inline] 232 | fn from((x, y): (usize, usize)) -> Self { 233 | Self { 234 | x: x as f32 + 0.5, 235 | y: y as f32 + 0.5, 236 | } 237 | } 238 | } 239 | impl From for (isize, isize) { 240 | #[inline] 241 | fn from(p: Point2) -> Self { 242 | (p.x as isize, p.y as isize) 243 | } 244 | } 245 | impl From<(isize, isize)> for Point2 { 246 | #[inline] 247 | fn from((x, y): (isize, isize)) -> Self { 248 | Self { 249 | x: x as f32 + 0.5, 250 | y: y as f32 + 0.5, 251 | } 252 | } 253 | } 254 | impl From<(f32, f32)> for Point2 { 255 | #[inline] 256 | fn from((x, y): (f32, f32)) -> Self { 257 | Self { x, y } 258 | } 259 | } 260 | impl From for (f32, f32) { 261 | #[inline] 262 | fn from(p: Point2) -> Self { 263 | p.as_tuple() 264 | } 265 | } 266 | 267 | impl Add for Point2 { 268 | type Output = Self; 269 | 270 | fn add(self, other: Self) -> Self { 271 | Self { 272 | x: self.x + other.x, 273 | y: self.y + other.y, 274 | } 275 | } 276 | } 277 | impl Sub for Point2 { 278 | type Output = Self; 279 | 280 | fn sub(self, other: Self) -> Self { 281 | Self { 282 | x: self.x - other.x, 283 | y: self.y - other.y, 284 | } 285 | } 286 | } 287 | impl Mul for Point2 { 288 | type Output = Self; 289 | 290 | fn mul(self, other: Self) -> Self { 291 | Self { 292 | x: self.x * other.x, 293 | y: self.y * other.y, 294 | } 295 | } 296 | } 297 | impl Div for Point2 { 298 | type Output = Self; 299 | 300 | fn div(self, other: Self) -> Self { 301 | Self { 302 | x: self.x / other.x, 303 | y: self.y / other.y, 304 | } 305 | } 306 | } 307 | impl AddAssign for Point2 { 308 | fn add_assign(&mut self, other: Self) { 309 | self.x += other.x; 310 | self.y += other.y; 311 | } 312 | } 313 | impl SubAssign for Point2 { 314 | fn sub_assign(&mut self, other: Self) { 315 | self.x -= other.x; 316 | self.y -= other.y; 317 | } 318 | } 319 | impl MulAssign for Point2 { 320 | fn mul_assign(&mut self, other: Self) { 321 | self.x *= other.x; 322 | self.y *= other.y; 323 | } 324 | } 325 | impl DivAssign for Point2 { 326 | fn div_assign(&mut self, other: Self) { 327 | self.x /= other.x; 328 | self.y /= other.y; 329 | } 330 | } 331 | impl Add for Point2 { 332 | type Output = Self; 333 | 334 | fn add(self, other: f32) -> Self { 335 | Self { 336 | x: self.x + other, 337 | y: self.y + other, 338 | } 339 | } 340 | } 341 | impl Sub for Point2 { 342 | type Output = Self; 343 | 344 | fn sub(self, other: f32) -> Self { 345 | Self { 346 | x: self.x - other, 347 | y: self.y - other, 348 | } 349 | } 350 | } 351 | impl Mul for Point2 { 352 | type Output = Self; 353 | 354 | fn mul(self, other: f32) -> Self { 355 | Self { 356 | x: self.x * other, 357 | y: self.y * other, 358 | } 359 | } 360 | } 361 | impl Div for Point2 { 362 | type Output = Self; 363 | 364 | fn div(self, other: f32) -> Self { 365 | Self { 366 | x: self.x / other, 367 | y: self.y / other, 368 | } 369 | } 370 | } 371 | impl AddAssign for Point2 { 372 | fn add_assign(&mut self, other: f32) { 373 | self.x += other; 374 | self.y += other; 375 | } 376 | } 377 | impl SubAssign for Point2 { 378 | fn sub_assign(&mut self, other: f32) { 379 | self.x -= other; 380 | self.y -= other; 381 | } 382 | } 383 | impl MulAssign for Point2 { 384 | fn mul_assign(&mut self, other: f32) { 385 | self.x *= other; 386 | self.y *= other; 387 | } 388 | } 389 | impl DivAssign for Point2 { 390 | fn div_assign(&mut self, other: f32) { 391 | self.x /= other; 392 | self.y /= other; 393 | } 394 | } 395 | impl Neg for Point2 { 396 | type Output = Self; 397 | 398 | fn neg(self) -> Self { 399 | Self { 400 | x: -self.x, 401 | y: -self.y, 402 | } 403 | } 404 | } 405 | impl Sum for Point2 { 406 | fn sum>(iter: I) -> Self { 407 | iter.fold(Default::default(), Add::add) 408 | } 409 | } 410 | 411 | impl FromProto<&Point2D> for Point2 { 412 | fn from_proto(p: &Point2D) -> Self { 413 | Self { 414 | x: p.get_x(), 415 | y: p.get_y(), 416 | } 417 | } 418 | } 419 | impl FromProto<&Point> for Point2 { 420 | fn from_proto(p: &Point) -> Self { 421 | Self { 422 | x: p.get_x(), 423 | y: p.get_y(), 424 | } 425 | } 426 | } 427 | impl IntoProto for Point2 { 428 | fn into_proto(self) -> Point2D { 429 | let mut pos = Point2D::new(); 430 | pos.set_x(self.x); 431 | pos.set_y(self.y); 432 | pos 433 | } 434 | } 435 | 436 | /// Point in 3D game world. 437 | #[allow(missing_docs)] 438 | #[derive(Debug, Default, Copy, Clone)] 439 | pub struct Point3 { 440 | pub x: f32, 441 | pub y: f32, 442 | pub z: f32, 443 | } 444 | impl Point3 { 445 | /// Constructs new 3D Point with given coordinates. 446 | pub fn new(x: f32, y: f32, z: f32) -> Self { 447 | Self { x, y, z } 448 | } 449 | /// Returns new point with given offset. 450 | pub fn offset(self, x: f32, y: f32, z: f32) -> Self { 451 | Self { 452 | x: self.x + x, 453 | y: self.y + y, 454 | z: self.z + z, 455 | } 456 | } 457 | /// Returns rounded point. 458 | pub fn round(self) -> Self { 459 | Self { 460 | x: (self.x + 0.5) as i32 as f32, 461 | y: (self.y + 0.5) as i32 as f32, 462 | z: (self.z + 0.5) as i32 as f32, 463 | } 464 | } 465 | /// Returns tuple with point's coordinates. 466 | pub fn as_tuple(self) -> (f32, f32, f32) { 467 | (self.x, self.y, self.z) 468 | } 469 | /// Converts 3D Point to 2D Point. 470 | pub fn to2(self) -> Point2 { 471 | Point2 { x: self.x, y: self.y } 472 | } 473 | } 474 | 475 | impl From for Point2 { 476 | #[inline] 477 | fn from(p3: Point3) -> Self { 478 | p3.to2() 479 | } 480 | } 481 | 482 | impl From<(f32, f32, f32)> for Point3 { 483 | #[inline] 484 | fn from((x, y, z): (f32, f32, f32)) -> Self { 485 | Self { x, y, z } 486 | } 487 | } 488 | impl From for (f32, f32, f32) { 489 | #[inline] 490 | fn from(p3: Point3) -> Self { 491 | p3.as_tuple() 492 | } 493 | } 494 | 495 | impl Add for Point3 { 496 | type Output = Self; 497 | 498 | fn add(self, other: Self) -> Self { 499 | Self { 500 | x: self.x + other.x, 501 | y: self.y + other.y, 502 | z: self.z + other.z, 503 | } 504 | } 505 | } 506 | impl Sub for Point3 { 507 | type Output = Self; 508 | 509 | fn sub(self, other: Self) -> Self { 510 | Self { 511 | x: self.x - other.x, 512 | y: self.y - other.y, 513 | z: self.z - other.z, 514 | } 515 | } 516 | } 517 | impl Mul for Point3 { 518 | type Output = Self; 519 | 520 | fn mul(self, other: Self) -> Self { 521 | Self { 522 | x: self.x * other.x, 523 | y: self.y * other.y, 524 | z: self.z * other.z, 525 | } 526 | } 527 | } 528 | impl Div for Point3 { 529 | type Output = Self; 530 | 531 | fn div(self, other: Self) -> Self { 532 | Self { 533 | x: self.x / other.x, 534 | y: self.y / other.y, 535 | z: self.z / other.z, 536 | } 537 | } 538 | } 539 | impl Add for Point3 { 540 | type Output = Self; 541 | 542 | fn add(self, other: f32) -> Self { 543 | Self { 544 | x: self.x + other, 545 | y: self.y + other, 546 | z: self.z + other, 547 | } 548 | } 549 | } 550 | impl Sub for Point3 { 551 | type Output = Self; 552 | 553 | fn sub(self, other: f32) -> Self { 554 | Self { 555 | x: self.x - other, 556 | y: self.y - other, 557 | z: self.z - other, 558 | } 559 | } 560 | } 561 | impl Mul for Point3 { 562 | type Output = Self; 563 | 564 | fn mul(self, other: f32) -> Self { 565 | Self { 566 | x: self.x * other, 567 | y: self.y * other, 568 | z: self.z * other, 569 | } 570 | } 571 | } 572 | impl Div for Point3 { 573 | type Output = Self; 574 | 575 | fn div(self, other: f32) -> Self { 576 | Self { 577 | x: self.x / other, 578 | y: self.y / other, 579 | z: self.z / other, 580 | } 581 | } 582 | } 583 | impl Sum for Point3 { 584 | fn sum>(iter: I) -> Self { 585 | iter.fold(Default::default(), Add::add) 586 | } 587 | } 588 | impl FromProto<&Point> for Point3 { 589 | fn from_proto(p: &Point) -> Self { 590 | Self { 591 | x: p.get_x(), 592 | y: p.get_y(), 593 | z: p.get_z(), 594 | } 595 | } 596 | } 597 | impl IntoProto for Point3 { 598 | fn into_proto(self) -> Point { 599 | let mut pos = Point::new(); 600 | pos.set_x(self.x); 601 | pos.set_y(self.y); 602 | pos.set_z(self.z); 603 | pos 604 | } 605 | } 606 | 607 | impl Radius for Point2 {} 608 | impl Radius for &Point2 {} 609 | impl Radius for Point3 {} 610 | impl Radius for &Point3 {} 611 | -------------------------------------------------------------------------------- /src/ids/buff_id.rs: -------------------------------------------------------------------------------- 1 | #![allow(deprecated)] 2 | 3 | #[cfg(feature = "serde")] 4 | use serde::{Serialize, Deserialize}; 5 | 6 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 7 | #[derive(Debug, FromPrimitive, ToPrimitive, Copy, Clone, PartialEq, Eq, Hash)] 8 | pub enum BuffId { 9 | Null = 0, 10 | Radar25 = 1, 11 | Tauntb = 2, 12 | DisableAbils = 3, 13 | TransientMorph = 4, 14 | GravitonBeam = 5, 15 | GhostCloak = 6, 16 | BansheeCloak = 7, 17 | PowerUserWarpable = 8, 18 | VortexBehaviorEnemy = 9, 19 | Corruption = 10, 20 | QueenSpawnLarvaTimer = 11, 21 | GhostHoldFire = 12, 22 | GhostHoldFireB = 13, 23 | Leech = 14, 24 | LeechDisableAbilities = 15, 25 | EMPDecloak = 16, 26 | FungalGrowth = 17, 27 | GuardianShield = 18, 28 | SeekerMissileTimeout = 19, 29 | TimeWarpProduction = 20, 30 | Ethereal = 21, 31 | NeuralParasite = 22, 32 | NeuralParasiteWait = 23, 33 | StimpackMarauder = 24, 34 | SupplyDrop = 25, 35 | _250mmStrikeCannons = 26, 36 | Stimpack = 27, 37 | PsiStorm = 28, 38 | CloakFieldEffect = 29, 39 | Charging = 30, 40 | AIDangerBuff = 31, 41 | VortexBehavior = 32, 42 | Slow = 33, 43 | TemporalRiftUnit = 34, 44 | SheepBusy = 35, 45 | Contaminated = 36, 46 | TimeScaleConversionBehavior = 37, 47 | BlindingCloudStructure = 38, 48 | CollapsibleRockTowerConjoinedSearch = 39, 49 | CollapsibleRockTowerRampDiagonalConjoinedSearch = 40, 50 | CollapsibleTerranTowerConjoinedSearch = 41, 51 | CollapsibleTerranTowerRampDiagonalConjoinedSearch = 42, 52 | DigesterCreepSprayVision = 43, 53 | InvulnerabilityShield = 44, 54 | MineDroneCountdown = 45, 55 | MothershipStasis = 46, 56 | MothershipStasisCaster = 47, 57 | MothershipCoreEnergizeVisual = 48, 58 | OracleRevelation = 49, 59 | GhostSnipeDoT = 50, 60 | NexusPhaseShift = 51, 61 | NexusInvulnerability = 52, 62 | RoughTerrainSearch = 53, 63 | RoughTerrainSlow = 54, 64 | OracleCloakField = 55, 65 | OracleCloakFieldEffect = 56, 66 | ScryerFriendly = 57, 67 | SpectreShield = 58, 68 | ViperConsumeStructure = 59, 69 | RestoreShields = 60, 70 | MercenaryCycloneMissiles = 61, 71 | MercenarySensorDish = 62, 72 | MercenaryShield = 63, 73 | Scryer = 64, 74 | StunRoundInitialBehavior = 65, 75 | BuildingShield = 66, 76 | LaserSight = 67, 77 | ProtectiveBarrier = 68, 78 | CorruptorGroundAttackDebuff = 69, 79 | BattlecruiserAntiAirDisable = 70, 80 | BuildingStasis = 71, 81 | Stasis = 72, 82 | ResourceStun = 73, 83 | MaximumThrust = 74, 84 | ChargeUp = 75, 85 | CloakUnit = 76, 86 | NullField = 77, 87 | Rescue = 78, 88 | Benign = 79, 89 | LaserTargeting = 80, 90 | Engage = 81, 91 | CapResource = 82, 92 | BlindingCloud = 83, 93 | DoomDamageDelay = 84, 94 | EyeStalk = 85, 95 | BurrowCharge = 86, 96 | Hidden = 87, 97 | MineDroneDOT = 88, 98 | MedivacSpeedBoost = 89, 99 | ExtendBridgeExtendingBridgeNEWide8Out = 90, 100 | ExtendBridgeExtendingBridgeNWWide8Out = 91, 101 | ExtendBridgeExtendingBridgeNEWide10Out = 92, 102 | ExtendBridgeExtendingBridgeNWWide10Out = 93, 103 | ExtendBridgeExtendingBridgeNEWide12Out = 94, 104 | ExtendBridgeExtendingBridgeNWWide12Out = 95, 105 | PhaseShield = 96, 106 | Purify = 97, 107 | VoidSiphon = 98, 108 | OracleWeapon = 99, 109 | AntiAirWeaponSwitchCooldown = 100, 110 | ArbiterMPStasisField = 101, 111 | ImmortalOverload = 102, 112 | CloakingFieldTargeted = 103, 113 | LightningBomb = 104, 114 | OraclePhaseShift = 105, 115 | ReleaseInterceptorsCooldown = 106, 116 | ReleaseInterceptorsTimedLifeWarning = 107, 117 | ReleaseInterceptorsWanderDelay = 108, 118 | ReleaseInterceptorsBeacon = 109, 119 | ArbiterMPCloakFieldEffect = 110, 120 | PurificationNova = 111, 121 | CorruptionBombDamage = 112, 122 | CorsairMPDisruptionWeb = 113, 123 | DisruptorPush = 114, 124 | LightofAiur = 115, 125 | LockOn = 116, 126 | Overcharge = 117, 127 | OverchargeDamage = 118, 128 | OverchargeSpeedBoost = 119, 129 | SeekerMissile = 120, 130 | TemporalField = 121, 131 | VoidRaySwarmDamageBoost = 122, 132 | VoidMPImmortalReviveSupressed = 123, 133 | DevourerMPAcidSpores = 124, 134 | DefilerMPConsume = 125, 135 | DefilerMPDarkSwarm = 126, 136 | DefilerMPPlague = 127, 137 | QueenMPEnsnare = 128, 138 | OracleStasisTrapTarget = 129, 139 | SelfRepair = 130, 140 | AggressiveMutation = 131, 141 | ParasiticBomb = 132, 142 | ParasiticBombUnitKU = 133, 143 | ParasiticBombSecondaryUnitSearch = 134, 144 | AdeptDeathCheck = 135, 145 | LurkerHoldFire = 136, 146 | LurkerHoldFireB = 137, 147 | TimeStopStun = 138, 148 | SlaynElementalGrabStun = 139, 149 | PurificationNovaPost = 140, 150 | DisableInterceptors = 141, 151 | BypassArmorDebuffOne = 142, 152 | BypassArmorDebuffTwo = 143, 153 | BypassArmorDebuffThree = 144, 154 | ChannelSnipeCombat = 145, 155 | TempestDisruptionBlastStunBehavior = 146, 156 | GravitonPrison = 147, 157 | InfestorDisease = 148, 158 | SSLightningProjector = 149, 159 | PurifierPlanetCrackerCharge = 150, 160 | SpectreCloaking = 151, 161 | WraithCloak = 152, 162 | PsytrousOxide = 153, 163 | BansheeCloakCrossSpectrumDampeners = 154, 164 | SSBattlecruiserHunterSeekerTimeout = 155, 165 | SSStrongerEnemyBuff = 156, 166 | SSTerraTronArmMissileTargetCheck = 157, 167 | SSMissileTimeout = 158, 168 | SSLeviathanBombCollisionCheck = 159, 169 | SSLeviathanBombExplodeTimer = 160, 170 | SSLeviathanBombMissileTargetCheck = 161, 171 | SSTerraTronCollisionCheck = 162, 172 | SSCarrierBossCollisionCheck = 163, 173 | SSCorruptorMissileTargetCheck = 164, 174 | SSInvulnerable = 165, 175 | SSLeviathanTentacleMissileTargetCheck = 166, 176 | SSLeviathanTentacleMissileTargetCheckInverted = 167, 177 | SSLeviathanTentacleTargetDeathDelay = 168, 178 | SSLeviathanTentacleMissileScanSwapDelay = 169, 179 | SSPowerUpDiagonal2 = 170, 180 | SSBattlecruiserCollisionCheck = 171, 181 | SSTerraTronMissileSpinnerMissileLauncher = 172, 182 | SSTerraTronMissileSpinnerCollisionCheck = 173, 183 | SSTerraTronMissileLauncher = 174, 184 | SSBattlecruiserMissileLauncher = 175, 185 | SSTerraTronStun = 176, 186 | SSVikingRespawn = 177, 187 | SSWraithCollisionCheck = 178, 188 | SSScourgeMissileTargetCheck = 179, 189 | SSScourgeDeath = 180, 190 | SSSwarmGuardianCollisionCheck = 181, 191 | SSFighterBombMissileDeath = 182, 192 | SSFighterDroneDamageResponse = 183, 193 | SSInterceptorCollisionCheck = 184, 194 | SSCarrierCollisionCheck = 185, 195 | SSMissileTargetCheckVikingDrone = 186, 196 | SSMissileTargetCheckVikingStrong1 = 187, 197 | SSMissileTargetCheckVikingStrong2 = 188, 198 | SSPowerUpHealth1 = 189, 199 | SSPowerUpHealth2 = 190, 200 | SSPowerUpStrong = 191, 201 | SSPowerupMorphToBomb = 192, 202 | SSPowerupMorphToHealth = 193, 203 | SSPowerupMorphToSideMissiles = 194, 204 | SSPowerupMorphToStrongerMissiles = 195, 205 | SSCorruptorCollisionCheck = 196, 206 | SSScoutCollisionCheck = 197, 207 | SSPhoenixCollisionCheck = 198, 208 | SSScourgeCollisionCheck = 199, 209 | SSLeviathanCollisionCheck = 200, 210 | SSScienceVesselCollisionCheck = 201, 211 | SSTerraTronSawCollisionCheck = 202, 212 | SSLightningProjectorCollisionCheck = 203, 213 | ShiftDelay = 204, 214 | BioStasis = 205, 215 | PersonalCloakingFree = 206, 216 | EMPDrain = 207, 217 | MindBlastStun = 208, 218 | _330mmBarrageCannons = 209, 219 | VoodooShield = 210, 220 | SpectreCloakingFree = 211, 221 | UltrasonicPulseStun = 212, 222 | Irradiate = 213, 223 | NydusWormLavaInstantDeath = 214, 224 | PredatorCloaking = 215, 225 | PsiDisruption = 216, 226 | MindControl = 217, 227 | QueenKnockdown = 218, 228 | ScienceVesselCloakField = 219, 229 | SporeCannonMissile = 220, 230 | ArtanisTemporalRiftUnit = 221, 231 | ArtanisCloakingFieldEffect = 222, 232 | ArtanisVortexBehavior = 223, 233 | Incapacitated = 224, 234 | KarassPsiStorm = 225, 235 | DutchMarauderSlow = 226, 236 | JumpStompStun = 227, 237 | JumpStompFStun = 228, 238 | RaynorMissileTimedLife = 229, 239 | PsionicShockwaveHeightAndStun = 230, 240 | ShadowClone = 231, 241 | AutomatedRepair = 232, 242 | Slimed = 233, 243 | RaynorTimeBombMissile = 234, 244 | RaynorTimeBombUnit = 235, 245 | TychusCommandoStimPack = 236, 246 | ViralPlasma = 237, 247 | Napalm = 238, 248 | BurstCapacitorsDamageBuff = 239, 249 | ColonyInfestation = 240, 250 | Domination = 241, 251 | EMPBurst = 242, 252 | HybridCZergyRoots = 243, 253 | HybridFZergyRoots = 244, 254 | LockdownB = 245, 255 | SpectreLockdownB = 246, 256 | VoodooLockdown = 247, 257 | ZeratulStun = 248, 258 | BuildingScarab = 249, 259 | VortexBehaviorEradicator = 250, 260 | GhostBlast = 251, 261 | HeroicBuff03 = 252, 262 | CannonRadar = 253, 263 | SSMissileTargetCheckViking = 254, 264 | SSMissileTargetCheck = 255, 265 | SSMaxSpeed = 256, 266 | SSMaxAcceleration = 257, 267 | SSPowerUpDiagonal1 = 258, 268 | Water = 259, 269 | DefensiveMatrix = 260, 270 | TestAttribute = 261, 271 | TestVeterancy = 262, 272 | ShredderSwarmDamageApply = 263, 273 | CorruptorInfesting = 264, 274 | MercGroundDropDelay = 265, 275 | MercGroundDrop = 266, 276 | MercAirDropDelay = 267, 277 | SpectreHoldFire = 268, 278 | SpectreHoldFireB = 269, 279 | ItemGravityBombs = 270, 280 | CarryMineralFieldMinerals = 271, 281 | CarryHighYieldMineralFieldMinerals = 272, 282 | CarryHarvestableVespeneGeyserGas = 273, 283 | CarryHarvestableVespeneGeyserGasProtoss = 274, 284 | CarryHarvestableVespeneGeyserGasZerg = 275, 285 | PermanentlyCloaked = 276, 286 | RavenScramblerMissile = 277, 287 | RavenShredderMissileTimeout = 278, 288 | RavenShredderMissileTint = 279, 289 | RavenShredderMissileArmorReduction = 280, 290 | ChronoBoostEnergyCost = 281, 291 | NexusShieldRechargeOnPylonBehavior = 282, 292 | NexusShieldRechargeOnPylonBehaviorSecondaryOnTarget = 283, 293 | InfestorEnsnare = 284, 294 | InfestorEnsnareMakePrecursorReheightSource = 285, 295 | NexusShieldOvercharge = 286, 296 | ParasiticBombDelayTimedLife = 287, 297 | Transfusion = 288, 298 | AccelerationZoneTemporalField = 289, 299 | AccelerationZoneFlyingTemporalField = 290, 300 | InhibitorZoneFlyingTemporalField = 291, 301 | DummyBuff000 = 292, 302 | InhibitorZoneTemporalField = 293, 303 | ResonatingGlaivesPhaseShift = 294, 304 | NeuralParasiteChildren = 295, 305 | AmorphousArmorcloud = 296, 306 | RavenShredderMissileArmorReductionUISubtruct = 297, 307 | BatteryOvercharge = 298, 308 | DummyBuff001 = 299, 309 | DummyBuff002 = 300, 310 | DummyBuff003 = 301, 311 | DummyBuff004 = 302, 312 | DummyBuff005 = 303, 313 | OnCreepQueen = 304, 314 | LoadOutSprayTracker = 305, 315 | CloakField = 306, 316 | TakenDamage = 307, 317 | RavenScramblerMissileCarrier = 308, 318 | } 319 | -------------------------------------------------------------------------------- /src/ids/effect_id.rs: -------------------------------------------------------------------------------- 1 | #![allow(deprecated)] 2 | 3 | #[cfg(feature = "serde")] 4 | use serde::{Serialize, Deserialize}; 5 | 6 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 7 | #[derive(Debug, FromPrimitive, ToPrimitive, Copy, Clone, PartialEq, Eq, Hash)] 8 | pub enum EffectId { 9 | Null = 0, 10 | PsiStormPersistent = 1, 11 | GuardianShieldPersistent = 2, 12 | TemporalFieldGrowingBubbleCreatePersistent = 3, 13 | TemporalFieldAfterBubbleCreatePersistent = 4, 14 | ThermalLancesForward = 5, 15 | ScannerSweep = 6, 16 | NukePersistent = 7, 17 | LiberatorTargetMorphDelayPersistent = 8, 18 | LiberatorTargetMorphPersistent = 9, 19 | BlindingCloudCP = 10, 20 | RavagerCorrosiveBileCP = 11, 21 | LurkerMP = 12, 22 | } 23 | -------------------------------------------------------------------------------- /src/ids/impls.rs: -------------------------------------------------------------------------------- 1 | use super::{AbilityId, UnitTypeId}; 2 | 3 | impl UnitTypeId { 4 | #[inline] 5 | pub fn is_worker(self) -> bool { 6 | matches!(self, UnitTypeId::SCV | UnitTypeId::Drone | UnitTypeId::Probe) 7 | } 8 | #[rustfmt::skip::macros(matches)] 9 | #[inline] 10 | pub fn is_townhall(self) -> bool { 11 | matches!( 12 | self, 13 | UnitTypeId::CommandCenter 14 | | UnitTypeId::OrbitalCommand 15 | | UnitTypeId::PlanetaryFortress 16 | | UnitTypeId::CommandCenterFlying 17 | | UnitTypeId::OrbitalCommandFlying 18 | | UnitTypeId::Hatchery 19 | | UnitTypeId::Lair 20 | | UnitTypeId::Hive 21 | | UnitTypeId::Nexus 22 | ) 23 | } 24 | #[rustfmt::skip::macros(matches)] 25 | #[inline] 26 | pub fn is_addon(self) -> bool { 27 | matches!( 28 | self, 29 | UnitTypeId::TechLab 30 | | UnitTypeId::Reactor 31 | | UnitTypeId::BarracksTechLab 32 | | UnitTypeId::BarracksReactor 33 | | UnitTypeId::FactoryTechLab 34 | | UnitTypeId::FactoryReactor 35 | | UnitTypeId::StarportTechLab 36 | | UnitTypeId::StarportReactor 37 | ) 38 | } 39 | #[rustfmt::skip::macros(matches)] 40 | #[inline] 41 | pub fn is_melee(self) -> bool { 42 | matches!( 43 | self, 44 | UnitTypeId::SCV 45 | | UnitTypeId::Drone 46 | | UnitTypeId::DroneBurrowed 47 | | UnitTypeId::Probe 48 | | UnitTypeId::Zergling 49 | | UnitTypeId::ZerglingBurrowed 50 | | UnitTypeId::BanelingCocoon 51 | | UnitTypeId::Baneling 52 | | UnitTypeId::BanelingBurrowed 53 | | UnitTypeId::Broodling 54 | | UnitTypeId::Zealot 55 | | UnitTypeId::DarkTemplar 56 | | UnitTypeId::Ultralisk 57 | | UnitTypeId::UltraliskBurrowed 58 | | UnitTypeId::HellionTank 59 | ) 60 | } 61 | #[rustfmt::skip::macros(matches)] 62 | #[inline] 63 | pub fn is_structure(self) -> bool { 64 | matches!( 65 | self, 66 | UnitTypeId::CommandCenter 67 | | UnitTypeId::CommandCenterFlying 68 | | UnitTypeId::PlanetaryFortress 69 | | UnitTypeId::OrbitalCommand 70 | | UnitTypeId::OrbitalCommandFlying 71 | | UnitTypeId::SupplyDepot 72 | | UnitTypeId::SupplyDepotLowered 73 | | UnitTypeId::SupplyDepotDrop 74 | | UnitTypeId::Refinery 75 | | UnitTypeId::Barracks 76 | | UnitTypeId::BarracksFlying 77 | | UnitTypeId::EngineeringBay 78 | | UnitTypeId::Bunker 79 | | UnitTypeId::SensorTower 80 | | UnitTypeId::MissileTurret 81 | | UnitTypeId::Factory 82 | | UnitTypeId::FactoryFlying 83 | | UnitTypeId::GhostAcademy 84 | | UnitTypeId::Starport 85 | | UnitTypeId::StarportFlying 86 | | UnitTypeId::Armory 87 | | UnitTypeId::FusionCore 88 | | UnitTypeId::TechLab 89 | | UnitTypeId::BarracksTechLab 90 | | UnitTypeId::FactoryTechLab 91 | | UnitTypeId::StarportTechLab 92 | | UnitTypeId::Reactor 93 | | UnitTypeId::BarracksReactor 94 | | UnitTypeId::FactoryReactor 95 | | UnitTypeId::StarportReactor 96 | | UnitTypeId::Hatchery 97 | | UnitTypeId::SpineCrawler 98 | | UnitTypeId::SporeCrawler 99 | | UnitTypeId::Extractor 100 | | UnitTypeId::SpawningPool 101 | | UnitTypeId::EvolutionChamber 102 | | UnitTypeId::RoachWarren 103 | | UnitTypeId::BanelingNest 104 | | UnitTypeId::CreepTumor 105 | | UnitTypeId::CreepTumorBurrowed 106 | | UnitTypeId::CreepTumorQueen 107 | | UnitTypeId::CreepTumorMissile 108 | | UnitTypeId::Lair 109 | | UnitTypeId::HydraliskDen 110 | | UnitTypeId::LurkerDenMP 111 | | UnitTypeId::InfestationPit 112 | | UnitTypeId::Spire 113 | | UnitTypeId::NydusNetwork 114 | | UnitTypeId::Hive 115 | | UnitTypeId::GreaterSpire 116 | | UnitTypeId::UltraliskCavern 117 | | UnitTypeId::Nexus 118 | | UnitTypeId::Pylon 119 | | UnitTypeId::Assimilator 120 | | UnitTypeId::Gateway 121 | | UnitTypeId::Forge 122 | | UnitTypeId::CyberneticsCore 123 | | UnitTypeId::PhotonCannon 124 | | UnitTypeId::ShieldBattery 125 | | UnitTypeId::RoboticsFacility 126 | | UnitTypeId::WarpGate 127 | | UnitTypeId::Stargate 128 | | UnitTypeId::TwilightCouncil 129 | | UnitTypeId::RoboticsBay 130 | | UnitTypeId::FleetBeacon 131 | | UnitTypeId::TemplarArchive 132 | | UnitTypeId::DarkShrine 133 | ) 134 | } 135 | #[rustfmt::skip::macros(matches)] 136 | #[inline] 137 | pub fn is_unit(self) -> bool { 138 | matches!( 139 | self, 140 | UnitTypeId::SCV 141 | | UnitTypeId::Marine 142 | | UnitTypeId::Marauder 143 | | UnitTypeId::Reaper 144 | | UnitTypeId::Ghost 145 | | UnitTypeId::Hellion 146 | | UnitTypeId::HellionTank 147 | | UnitTypeId::SiegeTank 148 | | UnitTypeId::SiegeTankSieged 149 | | UnitTypeId::Cyclone 150 | | UnitTypeId::WidowMine 151 | | UnitTypeId::WidowMineBurrowed 152 | | UnitTypeId::Thor 153 | | UnitTypeId::ThorAP 154 | | UnitTypeId::VikingFighter 155 | | UnitTypeId::VikingAssault 156 | | UnitTypeId::Medivac 157 | | UnitTypeId::Liberator 158 | | UnitTypeId::LiberatorAG 159 | | UnitTypeId::Raven 160 | | UnitTypeId::Banshee 161 | | UnitTypeId::Battlecruiser 162 | | UnitTypeId::Larva 163 | | UnitTypeId::Egg 164 | | UnitTypeId::Drone 165 | | UnitTypeId::DroneBurrowed 166 | | UnitTypeId::Queen 167 | | UnitTypeId::QueenBurrowed 168 | | UnitTypeId::Zergling 169 | | UnitTypeId::ZerglingBurrowed 170 | | UnitTypeId::BanelingCocoon 171 | | UnitTypeId::Baneling 172 | | UnitTypeId::BanelingBurrowed 173 | | UnitTypeId::Roach 174 | | UnitTypeId::RoachBurrowed 175 | | UnitTypeId::RavagerCocoon 176 | | UnitTypeId::Ravager 177 | | UnitTypeId::RavagerBurrowed 178 | | UnitTypeId::Hydralisk 179 | | UnitTypeId::HydraliskBurrowed 180 | | UnitTypeId::LurkerMPEgg 181 | | UnitTypeId::LurkerMP 182 | | UnitTypeId::LurkerMPBurrowed 183 | | UnitTypeId::Infestor 184 | | UnitTypeId::InfestorBurrowed 185 | | UnitTypeId::SwarmHostMP 186 | | UnitTypeId::SwarmHostBurrowedMP 187 | | UnitTypeId::Ultralisk 188 | | UnitTypeId::UltraliskBurrowed 189 | | UnitTypeId::LocustMP 190 | | UnitTypeId::LocustMPFlying 191 | | UnitTypeId::Broodling 192 | | UnitTypeId::Changeling 193 | | UnitTypeId::ChangelingZealot 194 | | UnitTypeId::ChangelingMarine 195 | | UnitTypeId::ChangelingMarineShield 196 | | UnitTypeId::ChangelingZergling 197 | | UnitTypeId::ChangelingZerglingWings 198 | | UnitTypeId::InfestorTerran 199 | | UnitTypeId::InfestorTerranBurrowed 200 | | UnitTypeId::NydusCanal 201 | | UnitTypeId::Overlord 202 | | UnitTypeId::OverlordCocoon 203 | | UnitTypeId::OverlordTransport 204 | | UnitTypeId::TransportOverlordCocoon 205 | | UnitTypeId::Overseer 206 | | UnitTypeId::OverseerSiegeMode 207 | | UnitTypeId::Mutalisk 208 | | UnitTypeId::Corruptor 209 | | UnitTypeId::BroodLordCocoon 210 | | UnitTypeId::BroodLord 211 | | UnitTypeId::Viper 212 | | UnitTypeId::Probe 213 | | UnitTypeId::Zealot 214 | | UnitTypeId::Stalker 215 | | UnitTypeId::Sentry 216 | | UnitTypeId::Adept 217 | | UnitTypeId::AdeptPhaseShift 218 | | UnitTypeId::HighTemplar 219 | | UnitTypeId::DarkTemplar 220 | | UnitTypeId::Immortal 221 | | UnitTypeId::Colossus 222 | | UnitTypeId::Disruptor 223 | | UnitTypeId::Archon 224 | | UnitTypeId::Observer 225 | | UnitTypeId::ObserverSiegeMode 226 | | UnitTypeId::WarpPrism 227 | | UnitTypeId::WarpPrismPhasing 228 | | UnitTypeId::Phoenix 229 | | UnitTypeId::VoidRay 230 | | UnitTypeId::Oracle 231 | | UnitTypeId::Carrier 232 | | UnitTypeId::Interceptor 233 | | UnitTypeId::Tempest 234 | | UnitTypeId::Mothership 235 | ) 236 | } 237 | } 238 | 239 | impl AbilityId { 240 | #[inline] 241 | pub fn is_constructing(self) -> bool { 242 | matches!( 243 | self, 244 | // Terran 245 | AbilityId::TerranBuildCommandCenter 246 | | AbilityId::TerranBuildSupplyDepot 247 | | AbilityId::TerranBuildRefinery 248 | | AbilityId::TerranBuildBarracks 249 | | AbilityId::TerranBuildEngineeringBay 250 | | AbilityId::TerranBuildMissileTurret 251 | | AbilityId::TerranBuildBunker 252 | | AbilityId::TerranBuildSensorTower 253 | | AbilityId::TerranBuildGhostAcademy 254 | | AbilityId::TerranBuildFactory 255 | | AbilityId::TerranBuildStarport 256 | | AbilityId::TerranBuildArmory 257 | | AbilityId::TerranBuildFusionCore 258 | // Protoss 259 | | AbilityId::ProtossBuildNexus 260 | | AbilityId::ProtossBuildPylon 261 | | AbilityId::ProtossBuildAssimilator 262 | | AbilityId::ProtossBuildGateway 263 | | AbilityId::ProtossBuildForge 264 | | AbilityId::ProtossBuildFleetBeacon 265 | | AbilityId::ProtossBuildTwilightCouncil 266 | | AbilityId::ProtossBuildPhotonCannon 267 | | AbilityId::ProtossBuildStargate 268 | | AbilityId::ProtossBuildTemplarArchive 269 | | AbilityId::ProtossBuildDarkShrine 270 | | AbilityId::ProtossBuildRoboticsBay 271 | | AbilityId::ProtossBuildRoboticsFacility 272 | | AbilityId::ProtossBuildCyberneticsCore 273 | | AbilityId::BuildShieldBattery 274 | // Zerg 275 | | AbilityId::ZergBuildHatchery 276 | | AbilityId::ZergBuildCreepTumor 277 | | AbilityId::ZergBuildExtractor 278 | | AbilityId::ZergBuildSpawningPool 279 | | AbilityId::ZergBuildEvolutionChamber 280 | | AbilityId::ZergBuildHydraliskDen 281 | | AbilityId::ZergBuildSpire 282 | | AbilityId::ZergBuildUltraliskCavern 283 | | AbilityId::ZergBuildInfestationPit 284 | | AbilityId::ZergBuildNydusNetwork 285 | | AbilityId::ZergBuildBanelingNest 286 | | AbilityId::BuildLurkerDen 287 | | AbilityId::ZergBuildRoachWarren 288 | | AbilityId::ZergBuildSpineCrawler 289 | | AbilityId::ZergBuildSporeCrawler 290 | ) 291 | } 292 | #[inline] 293 | pub fn is_constructing_scv(self) -> bool { 294 | matches!( 295 | self, 296 | AbilityId::TerranBuildCommandCenter 297 | | AbilityId::TerranBuildSupplyDepot 298 | | AbilityId::TerranBuildRefinery 299 | | AbilityId::TerranBuildBarracks 300 | | AbilityId::TerranBuildEngineeringBay 301 | | AbilityId::TerranBuildMissileTurret 302 | | AbilityId::TerranBuildBunker 303 | | AbilityId::TerranBuildSensorTower 304 | | AbilityId::TerranBuildGhostAcademy 305 | | AbilityId::TerranBuildFactory 306 | | AbilityId::TerranBuildStarport 307 | | AbilityId::TerranBuildArmory 308 | | AbilityId::TerranBuildFusionCore 309 | ) 310 | } 311 | #[inline] 312 | pub fn is_constructing_drone(self) -> bool { 313 | matches!( 314 | self, 315 | AbilityId::ZergBuildHatchery 316 | | AbilityId::ZergBuildCreepTumor 317 | | AbilityId::ZergBuildExtractor 318 | | AbilityId::ZergBuildSpawningPool 319 | | AbilityId::ZergBuildEvolutionChamber 320 | | AbilityId::ZergBuildHydraliskDen 321 | | AbilityId::ZergBuildSpire 322 | | AbilityId::ZergBuildUltraliskCavern 323 | | AbilityId::ZergBuildInfestationPit 324 | | AbilityId::ZergBuildNydusNetwork 325 | | AbilityId::ZergBuildBanelingNest 326 | | AbilityId::BuildLurkerDen 327 | | AbilityId::ZergBuildRoachWarren 328 | | AbilityId::ZergBuildSpineCrawler 329 | | AbilityId::ZergBuildSporeCrawler 330 | ) 331 | } 332 | #[inline] 333 | pub fn is_constructing_probe(self) -> bool { 334 | matches!( 335 | self, 336 | AbilityId::ProtossBuildNexus 337 | | AbilityId::ProtossBuildPylon 338 | | AbilityId::ProtossBuildAssimilator 339 | | AbilityId::ProtossBuildGateway 340 | | AbilityId::ProtossBuildForge 341 | | AbilityId::ProtossBuildFleetBeacon 342 | | AbilityId::ProtossBuildTwilightCouncil 343 | | AbilityId::ProtossBuildPhotonCannon 344 | | AbilityId::ProtossBuildStargate 345 | | AbilityId::ProtossBuildTemplarArchive 346 | | AbilityId::ProtossBuildDarkShrine 347 | | AbilityId::ProtossBuildRoboticsBay 348 | | AbilityId::ProtossBuildRoboticsFacility 349 | | AbilityId::ProtossBuildCyberneticsCore 350 | | AbilityId::BuildShieldBattery 351 | ) 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /src/ids/mod.rs: -------------------------------------------------------------------------------- 1 | //! Auto generated with `generate_ids.py` script from `stableid.json` 2 | //! ids of units, ablities, upgrades, buffs and effects. 3 | #![allow(missing_docs)] 4 | 5 | mod unit_typeid; 6 | mod ability_id; 7 | mod upgrade_id; 8 | mod buff_id; 9 | mod effect_id; 10 | 11 | pub use unit_typeid::UnitTypeId; 12 | pub use ability_id::AbilityId; 13 | pub use upgrade_id::UpgradeId; 14 | pub use buff_id::BuffId; 15 | pub use effect_id::EffectId; 16 | 17 | mod impls; 18 | -------------------------------------------------------------------------------- /src/ids/upgrade_id.rs: -------------------------------------------------------------------------------- 1 | #![allow(deprecated)] 2 | 3 | #[cfg(feature = "serde")] 4 | use serde::{Serialize, Deserialize}; 5 | 6 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 7 | #[derive(Debug, FromPrimitive, ToPrimitive, Copy, Clone, PartialEq, Eq, Hash)] 8 | pub enum UpgradeId { 9 | Null = 0, 10 | CarrierLaunchSpeedUpgrade = 1, 11 | GlialReconstitution = 2, 12 | TunnelingClaws = 3, 13 | ChitinousPlating = 4, 14 | HiSecAutoTracking = 5, 15 | TerranBuildingArmor = 6, 16 | TerranInfantryWeaponsLevel1 = 7, 17 | TerranInfantryWeaponsLevel2 = 8, 18 | TerranInfantryWeaponsLevel3 = 9, 19 | NeosteelFrame = 10, 20 | TerranInfantryArmorsLevel1 = 11, 21 | TerranInfantryArmorsLevel2 = 12, 22 | TerranInfantryArmorsLevel3 = 13, 23 | ReaperSpeed = 14, 24 | Stimpack = 15, 25 | ShieldWall = 16, 26 | PunisherGrenades = 17, 27 | SiegeTech = 18, 28 | HighCapacityBarrels = 19, 29 | BansheeCloak = 20, 30 | MedivacCaduceusReactor = 21, 31 | RavenCorvidReactor = 22, 32 | HunterSeeker = 23, 33 | DurableMaterials = 24, 34 | PersonalCloaking = 25, 35 | GhostMoebiusReactor = 26, 36 | #[deprecated(note = "Use `UpgradeId::TerranVehicleAndShipArmorsLevel1` instead.")] 37 | TerranVehicleArmorsLevel1 = 27, 38 | #[deprecated(note = "Use `UpgradeId::TerranVehicleAndShipArmorsLevel2` instead.")] 39 | TerranVehicleArmorsLevel2 = 28, 40 | #[deprecated(note = "Use `UpgradeId::TerranVehicleAndShipArmorsLevel3` instead.")] 41 | TerranVehicleArmorsLevel3 = 29, 42 | TerranVehicleWeaponsLevel1 = 30, 43 | TerranVehicleWeaponsLevel2 = 31, 44 | TerranVehicleWeaponsLevel3 = 32, 45 | #[deprecated(note = "Use `UpgradeId::TerranVehicleAndShipArmorsLevel1` instead.")] 46 | TerranShipArmorsLevel1 = 33, 47 | #[deprecated(note = "Use `UpgradeId::TerranVehicleAndShipArmorsLevel2` instead.")] 48 | TerranShipArmorsLevel2 = 34, 49 | #[deprecated(note = "Use `UpgradeId::TerranVehicleAndShipArmorsLevel3` instead.")] 50 | TerranShipArmorsLevel3 = 35, 51 | TerranShipWeaponsLevel1 = 36, 52 | TerranShipWeaponsLevel2 = 37, 53 | TerranShipWeaponsLevel3 = 38, 54 | ProtossGroundWeaponsLevel1 = 39, 55 | ProtossGroundWeaponsLevel2 = 40, 56 | ProtossGroundWeaponsLevel3 = 41, 57 | ProtossGroundArmorsLevel1 = 42, 58 | ProtossGroundArmorsLevel2 = 43, 59 | ProtossGroundArmorsLevel3 = 44, 60 | ProtossShieldsLevel1 = 45, 61 | ProtossShieldsLevel2 = 46, 62 | ProtossShieldsLevel3 = 47, 63 | ObserverGraviticBooster = 48, 64 | GraviticDrive = 49, 65 | ExtendedThermalLance = 50, 66 | HighTemplarKhaydarinAmulet = 51, 67 | PsiStormTech = 52, 68 | ZergMeleeWeaponsLevel1 = 53, 69 | ZergMeleeWeaponsLevel2 = 54, 70 | ZergMeleeWeaponsLevel3 = 55, 71 | ZergGroundArmorsLevel1 = 56, 72 | ZergGroundArmorsLevel2 = 57, 73 | ZergGroundArmorsLevel3 = 58, 74 | ZergMissileWeaponsLevel1 = 59, 75 | ZergMissileWeaponsLevel2 = 60, 76 | ZergMissileWeaponsLevel3 = 61, 77 | Overlordspeed = 62, 78 | Overlordtransport = 63, 79 | Burrow = 64, 80 | Zerglingattackspeed = 65, 81 | Zerglingmovementspeed = 66, 82 | Hydraliskspeed = 67, 83 | ZergFlyerWeaponsLevel1 = 68, 84 | ZergFlyerWeaponsLevel2 = 69, 85 | ZergFlyerWeaponsLevel3 = 70, 86 | ZergFlyerArmorsLevel1 = 71, 87 | ZergFlyerArmorsLevel2 = 72, 88 | ZergFlyerArmorsLevel3 = 73, 89 | InfestorEnergyUpgrade = 74, 90 | CentrificalHooks = 75, 91 | BattlecruiserEnableSpecializations = 76, 92 | BattlecruiserBehemothReactor = 77, 93 | ProtossAirWeaponsLevel1 = 78, 94 | ProtossAirWeaponsLevel2 = 79, 95 | ProtossAirWeaponsLevel3 = 80, 96 | ProtossAirArmorsLevel1 = 81, 97 | ProtossAirArmorsLevel2 = 82, 98 | ProtossAirArmorsLevel3 = 83, 99 | WarpGateResearch = 84, 100 | Haltech = 85, 101 | Charge = 86, 102 | BlinkTech = 87, 103 | AnabolicSynthesis = 88, 104 | ObverseIncubation = 89, 105 | VikingJotunBoosters = 90, 106 | OrganicCarapace = 91, 107 | InfestorPeristalsis = 92, 108 | AbdominalFortitude = 93, 109 | HydraliskSpeedUpgrade = 94, 110 | BanelingBurrowMove = 95, 111 | CombatDrugs = 96, 112 | StrikeCannons = 97, 113 | #[deprecated(note = "Use `UpgradeId::SmartServos` instead.")] 114 | TransformationServos = 98, 115 | PhoenixRangeUpgrade = 99, 116 | TempestRangeUpgrade = 100, 117 | NeuralParasite = 101, 118 | LocustLifetimeIncrease = 102, 119 | UltraliskBurrowChargeUpgrade = 103, 120 | OracleEnergyUpgrade = 104, 121 | RestoreShields = 105, 122 | ProtossHeroShipWeapon = 106, 123 | ProtossHeroShipDetector = 107, 124 | ProtossHeroShipSpell = 108, 125 | ReaperJump = 109, 126 | IncreasedRange = 110, 127 | ZergBurrowMove = 111, 128 | AnionPulseCrystals = 112, 129 | TerranVehicleAndShipWeaponsLevel1 = 113, 130 | TerranVehicleAndShipWeaponsLevel2 = 114, 131 | TerranVehicleAndShipWeaponsLevel3 = 115, 132 | TerranVehicleAndShipArmorsLevel1 = 116, 133 | TerranVehicleAndShipArmorsLevel2 = 117, 134 | TerranVehicleAndShipArmorsLevel3 = 118, 135 | FlyingLocusts = 119, 136 | RoachSupply = 120, 137 | ImmortalRevive = 121, 138 | DrillClaws = 122, 139 | CycloneLockOnRangeUpgrade = 123, 140 | CycloneAirUpgrade = 124, 141 | LiberatorMorph = 125, 142 | AdeptShieldUpgrade = 126, 143 | LurkerRange = 127, 144 | ImmortalBarrier = 128, 145 | AdeptKillBounce = 129, 146 | AdeptPiercingAttack = 130, 147 | CinematicMode = 131, 148 | CursorDebug = 132, 149 | #[deprecated(note = "Use `UpgradeId::CycloneLockOnDamageUpgrade` instead.")] 150 | MagFieldLaunchers = 133, 151 | EvolveGroovedSpines = 134, 152 | EvolveMuscularAugments = 135, 153 | BansheeSpeed = 136, 154 | MedivacRapidDeployment = 137, 155 | RavenRecalibratedExplosives = 138, 156 | MedivacIncreaseSpeedBoost = 139, 157 | LiberatorAGRangeUpgrade = 140, 158 | DarkTemplarBlinkUpgrade = 141, 159 | RavagerRange = 142, 160 | RavenDamageUpgrade = 143, 161 | CycloneLockOnDamageUpgrade = 144, 162 | AresClassWeaponsSystemViking = 145, 163 | AutoHarvester = 146, 164 | HybridCPlasmaUpgradeHard = 147, 165 | HybridCPlasmaUpgradeInsane = 148, 166 | InterceptorLimit4 = 149, 167 | InterceptorLimit6 = 150, 168 | _330mmBarrageCannons = 151, 169 | NotPossibleSiegeMode = 152, 170 | NeoSteelFrame = 153, 171 | NeoSteelAndShrikeTurretIconUpgrade = 154, 172 | OcularImplants = 155, 173 | CrossSpectrumDampeners = 156, 174 | OrbitalStrike = 157, 175 | ClusterBomb = 158, 176 | ShapedHull = 159, 177 | SpectreTooltipUpgrade = 160, 178 | UltraCapacitors = 161, 179 | VanadiumPlating = 162, 180 | CommandCenterReactor = 163, 181 | RegenerativeBioSteel = 164, 182 | CellularReactors = 165, 183 | BansheeCloakedDamage = 166, 184 | DistortionBlasters = 167, 185 | EMPTower = 168, 186 | SupplyDepotDrop = 169, 187 | HiveMindEmulator = 170, 188 | FortifiedBunkerCarapace = 171, 189 | Predator = 172, 190 | ScienceVessel = 173, 191 | DualFusionWelders = 174, 192 | AdvancedConstruction = 175, 193 | AdvancedMedicTraining = 176, 194 | ProjectileAccelerators = 177, 195 | ReinforcedSuperstructure = 178, 196 | MULE = 179, 197 | OrbitalRelay = 180, 198 | Razorwire = 181, 199 | AdvancedHealingAI = 182, 200 | TwinLinkedFlameThrowers = 183, 201 | NanoConstructor = 184, 202 | CerberusMines = 185, 203 | Hyperfluxor = 186, 204 | TriLithiumPowerCells = 187, 205 | #[deprecated(note = "Use `UpgradeId::PersonalCloaking` instead.")] 206 | PermanentCloakGhost = 188, 207 | PermanentCloakSpectre = 189, 208 | UltrasonicPulse = 190, 209 | SurvivalPods = 191, 210 | EnergyStorage = 192, 211 | FullBoreCanisterAmmo = 193, 212 | CampaignJotunBoosters = 194, 213 | MicroFiltering = 195, 214 | ParticleCannonAir = 196, 215 | VultureAutoRepair = 197, 216 | PsiDisruptor = 198, 217 | ScienceVesselEnergyManipulation = 199, 218 | ScienceVesselPlasmaWeaponry = 200, 219 | ShowGatlingGun = 201, 220 | TechReactor = 202, 221 | TechReactorAI = 203, 222 | TerranDefenseRangeBonus = 204, 223 | X88TNapalmUpgrade = 205, 224 | HurricaneMissiles = 206, 225 | MechanicalRebirth = 207, 226 | #[deprecated(note = "Use `UpgradeId::Stimpack` instead.")] 227 | MarineStimpack = 208, 228 | DarkTemplarTactics = 209, 229 | ClusterWarheads = 210, 230 | CloakDistortionField = 211, 231 | DevastatorMissiles = 212, 232 | DistortionThrusters = 213, 233 | DynamicPowerRouting = 214, 234 | ImpalerRounds = 215, 235 | KineticFields = 216, 236 | BurstCapacitors = 217, 237 | HailstormMissilePods = 218, 238 | RapidDeployment = 219, 239 | ReaperStimpack = 220, 240 | ReaperD8Charge = 221, 241 | Tychus05BattlecruiserPenetration = 222, 242 | ViralPlasma = 223, 243 | FirebatJuggernautPlating = 224, 244 | MultilockTargetingSystems = 225, 245 | TurboChargedEngines = 226, 246 | DistortionSensors = 227, 247 | #[deprecated(note = "Use `UpgradeId::HighCapacityBarrels` instead.")] 248 | InfernalPreIgniters = 228, 249 | #[deprecated(note = "Use `UpgradeId::HighCapacityBarrels` instead.")] 250 | HellionCampaignInfernalPreIgniter = 229, 251 | NapalmFuelTanks = 230, 252 | AuxiliaryMedBots = 231, 253 | JuggernautPlating = 232, 254 | MarauderLifeBoost = 233, 255 | #[deprecated(note = "Use `UpgradeId::ShieldWall` instead.")] 256 | CombatShield = 234, 257 | ReaperU238Rounds = 235, 258 | MaelstromRounds = 236, 259 | SiegeTankShapedBlast = 237, 260 | TungstenSpikes = 238, 261 | BearclawNozzles = 239, 262 | NanobotInjectors = 240, 263 | StabilizerMedPacks = 241, 264 | HALORockets = 242, 265 | ScavengingSystems = 243, 266 | ExtraMines = 244, 267 | AresClassWeaponsSystem = 245, 268 | WhiteNapalm = 246, 269 | ViralMunitions = 247, 270 | #[deprecated(note = "Use `UpgradeId::PunisherGrenades` instead.")] 271 | JackhammerConcussionGrenades = 248, 272 | FireSuppressionSystems = 249, 273 | FlareResearch = 250, 274 | ModularConstruction = 251, 275 | ExpandedHull = 252, 276 | ShrikeTurret = 253, 277 | MicrofusionReactors = 254, 278 | WraithCloak = 255, 279 | SingularityCharge = 256, 280 | GraviticThrusters = 257, 281 | #[deprecated(note = "Use `UpgradeId::BattlecruiserEnableSpecializations` instead.")] 282 | YamatoCannon = 258, 283 | DefensiveMatrix = 259, 284 | DarkProtoss = 260, 285 | TerranInfantryWeaponsUltraCapacitorsLevel1 = 261, 286 | TerranInfantryWeaponsUltraCapacitorsLevel2 = 262, 287 | TerranInfantryWeaponsUltraCapacitorsLevel3 = 263, 288 | TerranInfantryArmorsVanadiumPlatingLevel1 = 264, 289 | TerranInfantryArmorsVanadiumPlatingLevel2 = 265, 290 | TerranInfantryArmorsVanadiumPlatingLevel3 = 266, 291 | TerranVehicleWeaponsUltraCapacitorsLevel1 = 267, 292 | TerranVehicleWeaponsUltraCapacitorsLevel2 = 268, 293 | TerranVehicleWeaponsUltraCapacitorsLevel3 = 269, 294 | TerranVehicleArmorsVanadiumPlatingLevel1 = 270, 295 | TerranVehicleArmorsVanadiumPlatingLevel2 = 271, 296 | TerranVehicleArmorsVanadiumPlatingLevel3 = 272, 297 | TerranShipWeaponsUltraCapacitorsLevel1 = 273, 298 | TerranShipWeaponsUltraCapacitorsLevel2 = 274, 299 | TerranShipWeaponsUltraCapacitorsLevel3 = 275, 300 | TerranShipArmorsVanadiumPlatingLevel1 = 276, 301 | TerranShipArmorsVanadiumPlatingLevel2 = 277, 302 | TerranShipArmorsVanadiumPlatingLevel3 = 278, 303 | HireKelmorianMinersPH = 279, 304 | HireDevilDogsPH = 280, 305 | HireSpartanCompanyPH = 281, 306 | HireHammerSecuritiesPH = 282, 307 | HireSiegeBreakersPH = 283, 308 | HireHelsAngelsPH = 284, 309 | HireDuskWingPH = 285, 310 | HireDukesRevenge = 286, 311 | ToshEasyMode = 287, 312 | VoidRaySpeedUpgrade = 288, 313 | SmartServos = 289, 314 | ArmorPiercingRockets = 290, 315 | #[deprecated(note = "Use `UpgradeId::CycloneLockOnDamageUpgrade` instead.")] 316 | CycloneRapidFireLaunchers = 291, 317 | RavenEnhancedMunitions = 292, 318 | DiggingClaws = 293, 319 | CarrierCarrierCapacity = 294, 320 | CarrierLeashRangeUpgrade = 295, 321 | TempestGroundAttackUpgrade = 296, 322 | EnhancedShockwaves = 297, 323 | MicrobialShroud = 298, 324 | SunderingImpact = 299, 325 | AmplifiedShielding = 300, 326 | PsionicAmplifiers = 301, 327 | SecretedCoating = 302, 328 | HurricaneThrusters = 303, 329 | InterferenceMatrix = 304, 330 | } 331 | -------------------------------------------------------------------------------- /src/paths.rs: -------------------------------------------------------------------------------- 1 | #[cfg(windows)] 2 | use regex::Regex; 3 | 4 | use dirs::home_dir; 5 | use std::{env, fs, path::Path}; 6 | 7 | pub fn get_path_to_sc2() -> String { 8 | match env::var_os("SC2PATH") { 9 | Some(path) => path 10 | .to_str() 11 | .unwrap() 12 | .replace('~', home_dir().unwrap().to_str().unwrap()), 13 | None => { 14 | #[cfg(windows)] 15 | { 16 | let file = fs::read_to_string(format!( 17 | "{}/Documents/StarCraft II/ExecuteInfo.txt", 18 | home_dir().unwrap().to_str().unwrap(), 19 | )) 20 | .expect("Can't read ExecuteInfo.txt"); 21 | let re = Regex::new(r"= (.*)\\Versions").unwrap().captures(&file).unwrap(); 22 | 23 | let path = Path::new(&re[1]); 24 | if path.exists() { 25 | return path.to_str().unwrap().replace('\\', "/"); 26 | } 27 | 28 | "C:/Program Files (x86)/StarCraft II".to_string() 29 | } 30 | #[cfg(target_os = "linux")] 31 | { 32 | format!("{}/StarCraftII", home_dir().unwrap().to_str().unwrap()) 33 | } 34 | #[cfg(target_os = "macos")] 35 | { 36 | "/Applications/Starcraft II".to_string() 37 | } 38 | } 39 | } 40 | } 41 | 42 | pub fn get_map_path(sc2_path: &str, map_name: &str) -> String { 43 | let maps = { 44 | let path = format!("{}/Maps", sc2_path); 45 | if fs::metadata(&path).is_ok() { 46 | path 47 | } else { 48 | let path = format!("{}/maps", sc2_path); 49 | if fs::metadata(&path).is_ok() { 50 | path 51 | } else { 52 | panic!("Can't find maps folder in: {}", sc2_path); 53 | } 54 | } 55 | }; 56 | let map_path = format!("{}/{}.SC2Map", maps, map_name); 57 | fs::metadata(&map_path).unwrap_or_else(|_| panic!("Map doesn't exists: {}", map_path)); 58 | if cfg!(feature = "wine_sc2") { 59 | // Normalize the path using winepath 60 | let mut path_cmd = std::process::Command::new("winepath"); 61 | path_cmd 62 | // Specify that we have a windows path 63 | .arg("-w") 64 | .arg(map_path); 65 | let output = path_cmd.output().expect("Failed to run winepath"); 66 | assert!(output.status.success()); 67 | std::str::from_utf8(&output.stdout).unwrap().trim().to_string() 68 | } else { 69 | map_path 70 | } 71 | } 72 | 73 | pub fn get_latest_base_version(sc2_path: &str) -> u32 { 74 | Path::new(&format!("{}/Versions", sc2_path)) 75 | .read_dir() 76 | .expect("Can't read `Versions` folder") 77 | .filter_map(|dir| { 78 | let dir = dir.unwrap(); 79 | dir.file_type().ok().filter(|ftype| ftype.is_dir()).and( 80 | dir.file_name() 81 | .to_str() 82 | .filter(|name| name.starts_with("Base")) 83 | .map(|name| name[4..].parse::().unwrap()), 84 | ) 85 | }) 86 | .max() 87 | .unwrap() 88 | } 89 | 90 | // Returns (Base version, Data hash) 91 | pub fn get_version_info(version: &str) -> (u32, &str) { 92 | match version { 93 | "5.0.2" => (81102, "DC0A1182FB4ABBE8E29E3EC13CF46F68"), 94 | "5.0.1" => (81009, "0D28678BC32E7F67A238F19CD3E0A2CE"), 95 | "5.0.0" | "5.0" | "5" => (80949, "9AE39C332883B8BF6AA190286183ED72"), 96 | "4.12.1" => (80188, "44DED5AED024D23177C742FC227C615A"), 97 | "4.12.0" | "4.12" => (79998, "B47567DEE5DC23373BFF57194538DFD3"), 98 | "4.11.4" => (78285, "69493AFAB5C7B45DDB2F3442FD60F0CF"), 99 | "4.11.3" => (77661, "A15B8E4247434B020086354F39856C51"), 100 | "4.11.2" => (77535, "FC43E0897FCC93E4632AC57CBC5A2137"), 101 | "4.11.1" => (77474, "F92D1127A291722120AC816F09B2E583"), 102 | "4.11.0" | "4.11" => (77379, "70E774E722A58287EF37D487605CD384"), 103 | "4.10.4" => (76811, "FF9FA4EACEC5F06DEB27BD297D73ED67"), 104 | "4.10.3" => (76114, "CDB276D311F707C29BA664B7754A7293"), 105 | "4.10.2" => (76052, "D0F1A68AA88BA90369A84CD1439AA1C3"), 106 | "4.10.1" => (75800, "DDFFF9EC4A171459A4F371C6CC189554"), 107 | "4.10.0" | "4.10" => (75689, "B89B5D6FA7CBF6452E721311BFBC6CB2"), 108 | "4.9.3" => (75025, "C305368C63621480462F8F516FB64374"), 109 | "4.9.2" => (74741, "614480EF79264B5BD084E57F912172FF"), 110 | "4.9.1" => (74456, "218CB2271D4E2FA083470D30B1A05F02"), 111 | "4.9.0" | "4.9" => (74071, "70C74A2DCA8A0D8E7AE8647CAC68ACCA"), 112 | "4.8.6" => (73620, "AA18FEAD6573C79EF707DF44ABF1BE61"), 113 | "4.8.5" => (73559, "B2465E73AED597C74D0844112D582595"), 114 | "4.8.4" => (73286, "CD040C0675FD986ED37A4CA3C88C8EB5"), 115 | "4.8.3" => (72282, "0F14399BBD0BA528355FF4A8211F845B"), 116 | "4.8.2" => (71663, "FE90C92716FC6F8F04B74268EC369FA5"), 117 | "4.8.1" => (71523, "FCAF3F050B7C0CC7ADCF551B61B9B91E"), 118 | "4.8.0" | "4.8" => (71061, "760581629FC458A1937A05ED8388725B"), 119 | v => panic!("Can't find info about version: {:?}", v), 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/pixel_map.rs: -------------------------------------------------------------------------------- 1 | //! Data structures, used to store map data. 2 | #![allow(missing_docs)] 3 | 4 | use crate::{geometry::Point2, FromProto}; 5 | use ndarray::Array2; 6 | use num_traits::FromPrimitive; 7 | use sc2_proto::common::ImageData; 8 | use std::{ 9 | fmt, 10 | ops::{Index, IndexMut}, 11 | }; 12 | 13 | /// 2-Dimensional Array of pixels, where each pixel is `Set` or is `Empty`. 14 | pub type PixelMap = Array2; 15 | /// 2-Dimensional Array of bytes. 16 | pub type ByteMap = Array2; 17 | /// 2-Dimensional Array that represents visibility. 18 | pub type VisibilityMap = Array2; 19 | 20 | impl Index for Array2 { 21 | type Output = T; 22 | 23 | #[inline] 24 | fn index(&self, pos: Point2) -> &Self::Output { 25 | &self[<(usize, usize)>::from(pos)] 26 | } 27 | } 28 | impl IndexMut for Array2 { 29 | #[inline] 30 | fn index_mut(&mut self, pos: Point2) -> &mut Self::Output { 31 | &mut self[<(usize, usize)>::from(pos)] 32 | } 33 | } 34 | 35 | fn to_binary(n: u8) -> impl Iterator { 36 | (0..8).rev().map(move |x| Pixel::from_u8((n >> x) & 1).unwrap()) 37 | } 38 | 39 | impl FromProto<&ImageData> for PixelMap { 40 | fn from_proto(grid: &ImageData) -> Self { 41 | let size = grid.get_size(); 42 | Array2::from_shape_vec( 43 | (size.get_y() as usize, size.get_x() as usize), 44 | grid.get_data().iter().flat_map(|n| to_binary(*n)).collect(), 45 | ) 46 | .expect("Can't create PixelMap") 47 | .reversed_axes() 48 | } 49 | } 50 | impl FromProto<&ImageData> for ByteMap { 51 | fn from_proto(grid: &ImageData) -> Self { 52 | let size = grid.get_size(); 53 | Array2::from_shape_vec( 54 | (size.get_y() as usize, size.get_x() as usize), 55 | grid.get_data().to_vec(), 56 | ) 57 | .expect("Can't create ByteMap") 58 | .reversed_axes() 59 | } 60 | } 61 | impl FromProto<&ImageData> for VisibilityMap { 62 | fn from_proto(grid: &ImageData) -> Self { 63 | let size = grid.get_size(); 64 | Array2::from_shape_vec( 65 | (size.get_y() as usize, size.get_x() as usize), 66 | grid.get_data() 67 | .iter() 68 | .map(|n| { 69 | Visibility::from_u8(*n) 70 | .unwrap_or_else(|| panic!("enum Visibility has no variant with value: {}", n)) 71 | }) 72 | .collect(), 73 | ) 74 | .expect("Can't create VisibilityMap") 75 | .reversed_axes() 76 | } 77 | } 78 | 79 | /// Base for the most 2d maps. 80 | #[variant_checkers] 81 | #[derive(FromPrimitive, ToPrimitive, Copy, Clone, PartialEq, Eq, Default)] 82 | pub enum Pixel { 83 | /// When pixel is set, this tile is obstacle (e.g. not pathable | not placeable) 84 | /// or has something on it (e.g. has creep). 85 | Set, 86 | /// When pixel is empty, this tile is free (e.g. pathable | placeable | no creep). 87 | #[default] 88 | Empty, 89 | } 90 | 91 | impl fmt::Debug for Pixel { 92 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 93 | match self { 94 | Pixel::Empty => 0.fmt(f), 95 | Pixel::Set => 1.fmt(f), 96 | } 97 | } 98 | } 99 | 100 | /// Base for visibility maps. 101 | #[variant_checkers] 102 | #[derive(Debug, FromPrimitive, ToPrimitive, Copy, Clone, PartialEq, Eq, Default)] 103 | pub enum Visibility { 104 | /// Position is hidden (i.e. weren't explored before) 105 | #[default] 106 | Hidden, 107 | /// Position is in fog of war (i.e. was explored before, but not visible now) 108 | Fogged, 109 | /// Position is visible now. 110 | Visible, 111 | /// Position is fully hidden (i.e. terrain isn't visible, only darkness; only in campain and custom maps). 112 | FullHidden, 113 | } 114 | impl Visibility { 115 | pub fn is_explored(self) -> bool { 116 | !matches!(self, Visibility::Hidden) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/player.rs: -------------------------------------------------------------------------------- 1 | //! Items representing various player's data. 2 | #![allow(missing_docs)] 3 | 4 | use crate::{FromProto, IntoProto}; 5 | use num_traits::FromPrimitive; 6 | use sc2_proto::{ 7 | common::Race as ProtoRace, 8 | sc2api::{ 9 | AIBuild as ProtoAIBuild, Difficulty as ProtoDifficulty, PlayerType as ProtoPlayerType, 10 | Result as ProtoGameResult, 11 | }, 12 | }; 13 | #[cfg(feature = "serde")] 14 | use serde::{Deserialize, Serialize}; 15 | 16 | /// Representation of game races (your gender in SC2). 17 | #[variant_checkers] 18 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 19 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, FromStr, Default)] 20 | pub enum Race { 21 | /// Brutal mens, who try to survive in this world. 22 | Terran, 23 | /// Ruthless insects of incredibly big size. What a nightmare? 24 | Zerg, 25 | /// Kinda high-tech guys, who build cannons and batteries near your base "just for scouting". 26 | Protoss, 27 | /// Use when you didn't decide your race yet or just want to play them all. 28 | #[default] 29 | Random, 30 | } 31 | impl FromProto for Race { 32 | fn from_proto(race: ProtoRace) -> Self { 33 | match race { 34 | ProtoRace::Terran => Race::Terran, 35 | ProtoRace::Zerg => Race::Zerg, 36 | ProtoRace::Protoss => Race::Protoss, 37 | ProtoRace::Random => Race::Random, 38 | ProtoRace::NoRace => Race::Random, 39 | } 40 | } 41 | } 42 | impl IntoProto for Race { 43 | fn into_proto(self) -> ProtoRace { 44 | match self { 45 | Race::Terran => ProtoRace::Terran, 46 | Race::Zerg => ProtoRace::Zerg, 47 | Race::Protoss => ProtoRace::Protoss, 48 | Race::Random => ProtoRace::Random, 49 | } 50 | } 51 | } 52 | 53 | /// Difficulty of in-game AI. 54 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 55 | #[derive(Debug, Copy, Clone, FromPrimitive, FromStr)] 56 | #[enum_from_str(use_primitives)] 57 | pub enum Difficulty { 58 | VeryEasy, 59 | Easy, 60 | Medium, 61 | MediumHard, 62 | Hard, 63 | Harder, 64 | VeryHard, 65 | CheatVision, 66 | CheatMoney, 67 | CheatInsane, 68 | } 69 | impl FromProto for Difficulty { 70 | fn from_proto(difficulty: ProtoDifficulty) -> Self { 71 | match difficulty { 72 | ProtoDifficulty::VeryEasy => Difficulty::VeryEasy, 73 | ProtoDifficulty::Easy => Difficulty::Easy, 74 | ProtoDifficulty::Medium => Difficulty::Medium, 75 | ProtoDifficulty::MediumHard => Difficulty::MediumHard, 76 | ProtoDifficulty::Hard => Difficulty::Hard, 77 | ProtoDifficulty::Harder => Difficulty::Harder, 78 | ProtoDifficulty::VeryHard => Difficulty::VeryHard, 79 | ProtoDifficulty::CheatVision => Difficulty::CheatVision, 80 | ProtoDifficulty::CheatMoney => Difficulty::CheatMoney, 81 | ProtoDifficulty::CheatInsane => Difficulty::CheatInsane, 82 | } 83 | } 84 | } 85 | impl IntoProto for Difficulty { 86 | fn into_proto(self) -> ProtoDifficulty { 87 | match self { 88 | Difficulty::VeryEasy => ProtoDifficulty::VeryEasy, 89 | Difficulty::Easy => ProtoDifficulty::Easy, 90 | Difficulty::Medium => ProtoDifficulty::Medium, 91 | Difficulty::MediumHard => ProtoDifficulty::MediumHard, 92 | Difficulty::Hard => ProtoDifficulty::Hard, 93 | Difficulty::Harder => ProtoDifficulty::Harder, 94 | Difficulty::VeryHard => ProtoDifficulty::VeryHard, 95 | Difficulty::CheatVision => ProtoDifficulty::CheatVision, 96 | Difficulty::CheatMoney => ProtoDifficulty::CheatMoney, 97 | Difficulty::CheatInsane => ProtoDifficulty::CheatInsane, 98 | } 99 | } 100 | } 101 | 102 | /// Strategy build of in-game AI. 103 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 104 | #[derive(Debug, Copy, Clone, FromStr, Default)] 105 | pub enum AIBuild { 106 | #[default] 107 | RandomBuild, 108 | Rush, 109 | Timing, 110 | Power, 111 | Macro, 112 | Air, 113 | } 114 | impl FromProto for AIBuild { 115 | fn from_proto(ai_build: ProtoAIBuild) -> Self { 116 | match ai_build { 117 | ProtoAIBuild::RandomBuild => AIBuild::RandomBuild, 118 | ProtoAIBuild::Rush => AIBuild::Rush, 119 | ProtoAIBuild::Timing => AIBuild::Timing, 120 | ProtoAIBuild::Power => AIBuild::Power, 121 | ProtoAIBuild::Macro => AIBuild::Macro, 122 | ProtoAIBuild::Air => AIBuild::Air, 123 | } 124 | } 125 | } 126 | impl IntoProto for AIBuild { 127 | fn into_proto(self) -> ProtoAIBuild { 128 | match self { 129 | AIBuild::RandomBuild => ProtoAIBuild::RandomBuild, 130 | AIBuild::Rush => ProtoAIBuild::Rush, 131 | AIBuild::Timing => ProtoAIBuild::Timing, 132 | AIBuild::Power => ProtoAIBuild::Power, 133 | AIBuild::Macro => ProtoAIBuild::Macro, 134 | AIBuild::Air => ProtoAIBuild::Air, 135 | } 136 | } 137 | } 138 | 139 | /// Type of the player, used when joining a game. 140 | #[derive(Copy, Clone, PartialEq, Eq)] 141 | pub enum PlayerType { 142 | /// Bot or Human. 143 | Participant, 144 | /// In-game AI (aka Computer). 145 | Computer, 146 | /// Player who just watch game or replay. 147 | Observer, 148 | } 149 | impl FromProto for PlayerType { 150 | fn from_proto(player_type: ProtoPlayerType) -> Self { 151 | match player_type { 152 | ProtoPlayerType::Participant => PlayerType::Participant, 153 | ProtoPlayerType::Computer => PlayerType::Computer, 154 | ProtoPlayerType::Observer => PlayerType::Observer, 155 | } 156 | } 157 | } 158 | impl IntoProto for PlayerType { 159 | fn into_proto(self) -> ProtoPlayerType { 160 | match self { 161 | PlayerType::Participant => ProtoPlayerType::Participant, 162 | PlayerType::Computer => ProtoPlayerType::Computer, 163 | PlayerType::Observer => ProtoPlayerType::Observer, 164 | } 165 | } 166 | } 167 | 168 | /// Computer opponent configuration used in [`run_vs_computer`](crate::client::run_vs_computer). 169 | pub struct Computer { 170 | pub race: Race, 171 | pub difficulty: Difficulty, 172 | pub ai_build: Option, 173 | } 174 | impl Computer { 175 | pub fn new(race: Race, difficulty: Difficulty, ai_build: Option) -> Self { 176 | Self { 177 | race, 178 | difficulty, 179 | ai_build, 180 | } 181 | } 182 | } 183 | 184 | /// Game result for bot passed to [`on_end`](crate::Player::on_end). 185 | #[variant_checkers] 186 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 187 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 188 | pub enum GameResult { 189 | Victory, 190 | Defeat, 191 | Tie, 192 | Undecided, 193 | } 194 | impl FromProto for GameResult { 195 | fn from_proto(player_type: ProtoGameResult) -> Self { 196 | match player_type { 197 | ProtoGameResult::Victory => GameResult::Victory, 198 | ProtoGameResult::Defeat => GameResult::Defeat, 199 | ProtoGameResult::Tie => GameResult::Tie, 200 | ProtoGameResult::Undecided => GameResult::Undecided, 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/ramp.rs: -------------------------------------------------------------------------------- 1 | //! Data structures for storing data of ramps on the map 2 | //! with methods for extracting useful info from them. 3 | 4 | use crate::{bot::Rs, distance::*, geometry::Point2, pixel_map::ByteMap}; 5 | use std::{ 6 | cmp::{Ordering, Reverse}, 7 | convert::TryInto, 8 | fmt, 9 | }; 10 | 11 | /// Structured collection of ramps. 12 | #[derive(Default)] 13 | pub struct Ramps { 14 | /// All ramps on the map. 15 | pub all: Vec, 16 | /// Ramp to your main base. 17 | pub my: Ramp, 18 | /// Ramp to opponent's main base. 19 | pub enemy: Ramp, 20 | } 21 | 22 | type Pos = (usize, usize); 23 | 24 | /// Ramp data structure with some helpful methods. 25 | /// All ramps stored in [`Ramps`] in [`ramps`](crate::bot::Bot::ramps) field of bot. 26 | #[derive(Default, Clone)] 27 | pub struct Ramp { 28 | /// All points which belong to this ramp. 29 | pub points: Vec, 30 | height: Rs, 31 | start_location: Point2, 32 | } 33 | impl Ramp { 34 | pub(crate) fn new(points: Vec, height: &Rs, start_location: Point2) -> Self { 35 | Self { 36 | points, 37 | height: Rs::clone(height), 38 | start_location, 39 | } 40 | } 41 | /// Returns only upper points of the ramp. 42 | pub fn upper(&self) -> Vec { 43 | let mut max = u8::MIN; 44 | let mut result = Vec::new(); 45 | 46 | for &p in &self.points { 47 | let h = self.height[p]; 48 | match h.cmp(&max) { 49 | Ordering::Greater => { 50 | max = h; 51 | result = vec![p]; 52 | } 53 | Ordering::Equal => result.push(p), 54 | _ => {} 55 | } 56 | } 57 | 58 | result 59 | } 60 | /// Returns only lower points of the ramp. 61 | pub fn lower(&self) -> Vec { 62 | let mut min = u8::MAX; 63 | let mut result = Vec::new(); 64 | 65 | for &p in &self.points { 66 | let h = self.height[p]; 67 | match h.cmp(&min) { 68 | Ordering::Less => { 69 | min = h; 70 | result = vec![p]; 71 | } 72 | Ordering::Equal => result.push(p), 73 | _ => {} 74 | } 75 | } 76 | 77 | result 78 | } 79 | /// Returns center of upper points of the ramp. 80 | pub fn top_center(&self) -> Option { 81 | let ps = self.upper(); 82 | if ps.is_empty() { 83 | None 84 | } else { 85 | // Some(ps.iter().sum::() / ps.len()) 86 | let (x, y) = ps.iter().fold((0, 0), |(ax, ay), (x, y)| (ax + x, ay + y)); 87 | Some((x / ps.len(), y / ps.len())) 88 | } 89 | } 90 | /// Returns center of lower points of the ramp. 91 | pub fn bottom_center(&self) -> Option { 92 | let ps = self.lower(); 93 | if ps.is_empty() { 94 | None 95 | } else { 96 | let (x, y) = ps.iter().fold((0, 0), |(ax, ay), (x, y)| (ax + x, ay + y)); 97 | Some((x / ps.len(), y / ps.len())) 98 | } 99 | } 100 | fn upper2_for_ramp_wall(&self) -> Option<[Pos; 2]> { 101 | let mut upper = self.upper(); 102 | if upper.len() > 5 { 103 | return None; 104 | } 105 | match upper.len().cmp(&2) { 106 | Ordering::Greater => self.bottom_center().and_then(|(center_x, center_y)| { 107 | upper.sort_unstable_by_key(|(x, y)| { 108 | let dx = x.abs_diff(center_x); 109 | let dy = y.abs_diff(center_y); 110 | Reverse(dx * dx + dy * dy) 111 | }); 112 | upper[..2].try_into().ok() 113 | }), 114 | Ordering::Equal => upper.as_slice().try_into().ok(), 115 | Ordering::Less => None, 116 | } 117 | } 118 | /// Returns correct positions to build corner supplies in terran wall. 119 | pub fn corner_depots(&self) -> Option<[Point2; 2]> { 120 | if let Some(ps) = self.upper2_for_ramp_wall() { 121 | let p1 = Point2::from(ps[0]); 122 | let p2 = Point2::from(ps[1]); 123 | 124 | let center = (p1 + p2) / 2.0; 125 | return center.circle_intersection(self.depot_in_middle()?, 5_f32.sqrt()); 126 | } 127 | None 128 | } 129 | /// Returns correct position to build barrack in terran wall without addon. 130 | pub fn barracks_in_middle(&self) -> Option { 131 | let upper_len = self.upper().len(); 132 | if upper_len != 2 && upper_len != 5 { 133 | return None; 134 | } 135 | if let Some(ps) = self.upper2_for_ramp_wall() { 136 | let p1 = Point2::from(ps[0]); 137 | let p2 = Point2::from(ps[1]); 138 | 139 | let intersects = p1.circle_intersection(p2, 5_f32.sqrt())?; 140 | let (x, y) = *self.lower().first()?; 141 | let lower = Point2::new(x as f32, y as f32); 142 | 143 | return intersects.iter().furthest(lower).copied(); 144 | } 145 | None 146 | } 147 | /// Returns correct position to build barrack in terran wall with addon. 148 | pub fn barracks_correct_placement(&self) -> Option { 149 | self.barracks_in_middle().map(|pos| { 150 | if self 151 | .corner_depots() 152 | .map_or(false, |depots| pos.x + 1.0 > depots[0].x.max(depots[1].x)) 153 | { 154 | pos 155 | } else { 156 | pos.offset(-2.0, 0.0) 157 | } 158 | }) 159 | } 160 | /// Returns correct position to build supply in middle of wall from 3 supplies. 161 | pub fn depot_in_middle(&self) -> Option { 162 | let upper_len = self.upper().len(); 163 | if upper_len != 2 && upper_len != 5 { 164 | return None; 165 | } 166 | if let Some(ps) = self.upper2_for_ramp_wall() { 167 | let p1 = Point2::from(ps[0]); 168 | let p2 = Point2::from(ps[1]); 169 | 170 | let intersects = p1.circle_intersection(p2, 1.581_138_8)?; // 2.5_f32.sqrt() 171 | let (x, y) = *self.lower().first()?; 172 | let lower = Point2::new(x as f32, y as f32); 173 | 174 | return intersects.iter().furthest(lower).copied(); 175 | } 176 | None 177 | } 178 | /// Returns correct position to build pylon in protoss wall. 179 | pub fn protoss_wall_pylon(&self) -> Option { 180 | let middle = self.depot_in_middle()?; 181 | Some(middle + (self.barracks_in_middle()? - middle) * 6.0) 182 | } 183 | /// Returns correct positions of 3x3 buildings in protoss wall. 184 | pub fn protoss_wall_buildings(&self) -> Option<[Point2; 2]> { 185 | let middle = self.depot_in_middle()?; 186 | let direction = self.barracks_in_middle()? - middle; 187 | 188 | let mut depots = self.corner_depots()?.to_vec(); 189 | let start = self.start_location; 190 | depots.sort_unstable_by(|d1, d2| { 191 | d1.distance_squared(start) 192 | .partial_cmp(&d2.distance_squared(start)) 193 | .unwrap() 194 | }); 195 | 196 | let wall1 = depots[1] + direction; 197 | Some([wall1, middle + direction + (middle - wall1) / 1.5]) 198 | } 199 | /// Returns correct position of unit to close protoss wall. 200 | pub fn protoss_wall_warpin(&self) -> Option { 201 | let middle = self.depot_in_middle()?; 202 | let direction = self.barracks_in_middle()? - middle; 203 | 204 | let mut depots = self.corner_depots()?.to_vec(); 205 | let start = self.start_location; 206 | depots.sort_unstable_by(|d1, d2| { 207 | d1.distance_squared(start) 208 | .partial_cmp(&d2.distance_squared(start)) 209 | .unwrap() 210 | }); 211 | 212 | Some(depots[0] - direction) 213 | } 214 | } 215 | impl fmt::Debug for Ramp { 216 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 217 | write!(f, "Ramp({:?})", self.points) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/score.rs: -------------------------------------------------------------------------------- 1 | //! SC2 Score interface. 2 | 3 | use crate::{FromProto, IntoSC2}; 4 | use sc2_proto::score::{CategoryScoreDetails, Score as ProtoScore, Score_ScoreType, VitalScoreDetails}; 5 | 6 | #[variant_checkers] 7 | #[derive(Clone, Default)] 8 | pub enum ScoreType { 9 | #[default] 10 | Curriculum, 11 | Melee, 12 | } 13 | impl FromProto for ScoreType { 14 | fn from_proto(score_type: Score_ScoreType) -> Self { 15 | match score_type { 16 | Score_ScoreType::Curriculum => ScoreType::Curriculum, 17 | Score_ScoreType::Melee => ScoreType::Melee, 18 | } 19 | } 20 | } 21 | 22 | #[derive(Default, Clone)] 23 | pub struct Category { 24 | pub none: f32, 25 | pub army: f32, 26 | pub economy: f32, 27 | pub technology: f32, 28 | pub upgrade: f32, 29 | } 30 | impl FromProto<&CategoryScoreDetails> for Category { 31 | fn from_proto(category: &CategoryScoreDetails) -> Self { 32 | Self { 33 | none: category.get_none(), 34 | army: category.get_army(), 35 | economy: category.get_economy(), 36 | technology: category.get_technology(), 37 | upgrade: category.get_upgrade(), 38 | } 39 | } 40 | } 41 | 42 | #[derive(Default, Clone)] 43 | pub struct Vital { 44 | pub life: f32, 45 | pub shields: f32, 46 | pub energy: f32, 47 | } 48 | impl FromProto<&VitalScoreDetails> for Vital { 49 | fn from_proto(vital: &VitalScoreDetails) -> Self { 50 | Self { 51 | life: vital.get_life(), 52 | shields: vital.get_shields(), 53 | energy: vital.get_energy(), 54 | } 55 | } 56 | } 57 | 58 | /// All kinds of scores stored here. 59 | /// 60 | /// Can be accessed through [state.observation.score](crate::game_state::Observation::score). 61 | #[derive(Default, Clone)] 62 | pub struct Score { 63 | pub score_type: ScoreType, 64 | pub total_score: i32, 65 | // score details 66 | pub idle_production_time: f32, 67 | pub idle_worker_time: f32, 68 | pub total_value_units: f32, 69 | pub total_value_structures: f32, 70 | pub killed_value_units: f32, 71 | pub killed_value_structures: f32, 72 | pub collected_minerals: f32, 73 | pub collected_vespene: f32, 74 | pub collection_rate_minerals: f32, 75 | pub collection_rate_vespene: f32, 76 | pub spent_minerals: f32, 77 | pub spent_vespene: f32, 78 | pub food_used: Category, 79 | pub killed_minerals: Category, 80 | pub killed_vespene: Category, 81 | pub lost_minerals: Category, 82 | pub lost_vespene: Category, 83 | pub friendly_fire_minerals: Category, 84 | pub friendly_fire_vespene: Category, 85 | pub used_minerals: Category, 86 | pub used_vespene: Category, 87 | pub total_used_minerals: Category, 88 | pub total_used_vespene: Category, 89 | pub total_damage_dealt: Vital, 90 | pub total_damage_taken: Vital, 91 | pub total_healed: Vital, 92 | pub current_apm: f32, 93 | pub current_effective_apm: f32, 94 | } 95 | impl FromProto<&ProtoScore> for Score { 96 | fn from_proto(score: &ProtoScore) -> Self { 97 | let details = score.get_score_details(); 98 | Self { 99 | score_type: score.get_score_type().into_sc2(), 100 | total_score: score.get_score(), 101 | idle_production_time: details.get_idle_production_time(), 102 | idle_worker_time: details.get_idle_worker_time(), 103 | total_value_units: details.get_total_value_units(), 104 | total_value_structures: details.get_total_value_structures(), 105 | killed_value_units: details.get_killed_value_units(), 106 | killed_value_structures: details.get_killed_value_structures(), 107 | collected_minerals: details.get_collected_minerals(), 108 | collected_vespene: details.get_collected_vespene(), 109 | collection_rate_minerals: details.get_collection_rate_minerals(), 110 | collection_rate_vespene: details.get_collection_rate_vespene(), 111 | spent_minerals: details.get_spent_minerals(), 112 | spent_vespene: details.get_spent_vespene(), 113 | food_used: details.get_food_used().into_sc2(), 114 | killed_minerals: details.get_killed_minerals().into_sc2(), 115 | killed_vespene: details.get_killed_vespene().into_sc2(), 116 | lost_minerals: details.get_lost_minerals().into_sc2(), 117 | lost_vespene: details.get_lost_vespene().into_sc2(), 118 | friendly_fire_minerals: details.get_friendly_fire_minerals().into_sc2(), 119 | friendly_fire_vespene: details.get_friendly_fire_vespene().into_sc2(), 120 | used_minerals: details.get_used_minerals().into_sc2(), 121 | used_vespene: details.get_used_vespene().into_sc2(), 122 | total_used_minerals: details.get_total_used_minerals().into_sc2(), 123 | total_used_vespene: details.get_total_used_vespene().into_sc2(), 124 | total_damage_dealt: details.get_total_damage_dealt().into_sc2(), 125 | total_damage_taken: details.get_total_damage_taken().into_sc2(), 126 | total_healed: details.get_total_healed().into_sc2(), 127 | current_apm: details.get_current_apm(), 128 | current_effective_apm: details.get_current_effective_apm(), 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/units/iter.rs: -------------------------------------------------------------------------------- 1 | //! Iterator adaptors for Units. 2 | 3 | use super::Container; 4 | use crate::{ids::UnitTypeId, unit::Unit}; 5 | use indexmap::map::IntoIter; 6 | use std::borrow::Borrow; 7 | 8 | /// Owned iterator over Units. 9 | pub struct IntoUnits(pub(super) IntoIter); 10 | 11 | impl Iterator for IntoUnits { 12 | type Item = Unit; 13 | 14 | #[inline] 15 | fn next(&mut self) -> Option { 16 | self.0.next().map(|x| x.1) 17 | } 18 | 19 | #[inline] 20 | fn size_hint(&self) -> (usize, Option) { 21 | self.0.size_hint() 22 | } 23 | 24 | #[inline] 25 | fn count(self) -> usize { 26 | self.0.len() 27 | } 28 | 29 | #[inline] 30 | fn nth(&mut self, n: usize) -> Option { 31 | self.0.nth(n).map(|x| x.1) 32 | } 33 | 34 | #[inline] 35 | fn last(mut self) -> Option { 36 | self.next_back() 37 | } 38 | } 39 | 40 | impl DoubleEndedIterator for IntoUnits { 41 | fn next_back(&mut self) -> Option { 42 | self.0.next_back().map(|x| x.1) 43 | } 44 | } 45 | 46 | impl ExactSizeIterator for IntoUnits { 47 | fn len(&self) -> usize { 48 | self.0.len() 49 | } 50 | } 51 | 52 | // Macros to generate iterator implementation here 53 | 54 | pub(crate) fn filter_fold( 55 | mut pred: impl FnMut(&T) -> bool, 56 | mut fold: impl FnMut(Acc, T) -> Acc, 57 | ) -> impl FnMut(Acc, T) -> Acc { 58 | move |acc, u| if pred(&u) { fold(acc, u) } else { acc } 59 | } 60 | 61 | macro_rules! iterator_methods { 62 | () => { 63 | #[inline] 64 | fn next(&mut self) -> Option { 65 | let pred = self.predicate(); 66 | self.iter.find(|u| pred(u.borrow())) 67 | } 68 | 69 | #[inline] 70 | fn size_hint(&self) -> (usize, Option) { 71 | (0, self.iter.size_hint().1) 72 | } 73 | 74 | #[inline] 75 | fn count(self) -> usize { 76 | let pred = self.predicate(); 77 | self.iter.map(|u| pred(u.borrow()) as usize).sum() 78 | } 79 | 80 | #[inline] 81 | fn fold(self, init: Acc, fold: Fold) -> Acc 82 | where 83 | Fold: FnMut(Acc, Self::Item) -> Acc, 84 | { 85 | let pred = self.predicate(); 86 | self.iter 87 | .fold(init, filter_fold(|u| pred(u.borrow()), fold)) 88 | } 89 | }; 90 | } 91 | 92 | macro_rules! double_ended_iterator_methods { 93 | () => { 94 | #[inline] 95 | fn next_back(&mut self) -> Option { 96 | let pred = self.predicate(); 97 | self.iter.rfind(|u| pred(u.borrow())) 98 | } 99 | 100 | #[inline] 101 | fn rfold(self, init: Acc, fold: Fold) -> Acc 102 | where 103 | Fold: FnMut(Acc, Self::Item) -> Acc, 104 | { 105 | let pred = self.predicate(); 106 | self.iter 107 | .rfold(init, filter_fold(|u| pred(u.borrow()), fold)) 108 | } 109 | }; 110 | } 111 | 112 | macro_rules! impl_simple_iterator { 113 | ($name:ident $(<$a:lifetime>)?) => { 114 | impl<$($a,)? I> Iterator for $name<$($a,)? I> 115 | where 116 | I: Iterator, 117 | I::Item: Borrow, 118 | { 119 | type Item = I::Item; 120 | 121 | iterator_methods!(); 122 | } 123 | 124 | impl<$($a,)? I> DoubleEndedIterator for $name<$($a,)? I> 125 | where 126 | I: DoubleEndedIterator, 127 | I::Item: Borrow, 128 | { 129 | double_ended_iterator_methods!(); 130 | } 131 | }; 132 | } 133 | 134 | macro_rules! make_simple_iterator { 135 | ($(#[$attr:meta])* $name:ident, $pred:expr) => { 136 | $(#[$attr])* 137 | #[derive(Clone)] 138 | pub struct $name { 139 | iter: I, 140 | } 141 | 142 | impl $name { 143 | pub(super) fn new(iter: I) -> Self { 144 | Self { iter } 145 | } 146 | 147 | fn predicate(&self) -> impl Fn(&Unit) -> bool { 148 | $pred 149 | } 150 | } 151 | 152 | impl_simple_iterator!($name); 153 | }; 154 | } 155 | 156 | // Iterator adaptors here 157 | 158 | /// An iterator that filters units with given tags. 159 | #[derive(Clone)] 160 | pub struct FindTags<'a, I, T> { 161 | iter: I, 162 | tags: &'a T, 163 | } 164 | impl<'a, I, T: Container> FindTags<'a, I, T> { 165 | pub(super) fn new(iter: I, tags: &'a T) -> Self { 166 | Self { iter, tags } 167 | } 168 | 169 | fn predicate(&self) -> impl Fn(&Unit) -> bool + 'a { 170 | let tags = self.tags; 171 | move |u| tags.contains(&u.tag()) 172 | } 173 | } 174 | 175 | impl<'a, I, T> Iterator for FindTags<'a, I, T> 176 | where 177 | I: Iterator, 178 | I::Item: Borrow, 179 | T: Container, 180 | { 181 | type Item = I::Item; 182 | 183 | iterator_methods!(); 184 | } 185 | 186 | impl<'a, I, T> DoubleEndedIterator for FindTags<'a, I, T> 187 | where 188 | I: DoubleEndedIterator, 189 | I::Item: Borrow, 190 | T: Container, 191 | { 192 | double_ended_iterator_methods!(); 193 | } 194 | 195 | /// An iterator that filters units of given type. 196 | #[derive(Clone)] 197 | pub struct OfType { 198 | iter: I, 199 | unit_type: UnitTypeId, 200 | } 201 | impl OfType { 202 | pub(super) fn new(iter: I, unit_type: UnitTypeId) -> Self { 203 | Self { iter, unit_type } 204 | } 205 | 206 | fn predicate(&self) -> impl Fn(&Unit) -> bool { 207 | let unit_type = self.unit_type; 208 | move |u| u.type_id() == unit_type 209 | } 210 | } 211 | impl_simple_iterator!(OfType); 212 | 213 | /// An iterator that filters out units of given type. 214 | #[derive(Clone)] 215 | pub struct ExcludeType { 216 | iter: I, 217 | unit_type: UnitTypeId, 218 | } 219 | impl ExcludeType { 220 | pub(super) fn new(iter: I, unit_type: UnitTypeId) -> Self { 221 | Self { iter, unit_type } 222 | } 223 | 224 | fn predicate(&self) -> impl Fn(&Unit) -> bool { 225 | let unit_type = self.unit_type; 226 | move |u| u.type_id() != unit_type 227 | } 228 | } 229 | impl_simple_iterator!(ExcludeType); 230 | 231 | /// An iterator that filters units of given types. 232 | #[derive(Clone)] 233 | pub struct OfTypes<'a, I, T> { 234 | iter: I, 235 | types: &'a T, 236 | } 237 | impl<'a, I, T: Container> OfTypes<'a, I, T> { 238 | pub(super) fn new(iter: I, types: &'a T) -> Self { 239 | Self { iter, types } 240 | } 241 | 242 | fn predicate(&self) -> impl Fn(&Unit) -> bool + 'a { 243 | let types = self.types; 244 | move |u| types.contains(&u.type_id()) 245 | } 246 | } 247 | 248 | impl<'a, I, T> Iterator for OfTypes<'a, I, T> 249 | where 250 | I: Iterator, 251 | I::Item: Borrow, 252 | T: Container, 253 | { 254 | type Item = I::Item; 255 | 256 | iterator_methods!(); 257 | } 258 | 259 | impl<'a, I, T> DoubleEndedIterator for OfTypes<'a, I, T> 260 | where 261 | I: DoubleEndedIterator, 262 | I::Item: Borrow, 263 | T: Container, 264 | { 265 | double_ended_iterator_methods!(); 266 | } 267 | 268 | /// An iterator that filters out units of given types. 269 | #[derive(Clone)] 270 | pub struct ExcludeTypes<'a, I, T> { 271 | iter: I, 272 | types: &'a T, 273 | } 274 | impl<'a, I, T: Container> ExcludeTypes<'a, I, T> { 275 | pub(super) fn new(iter: I, types: &'a T) -> Self { 276 | Self { iter, types } 277 | } 278 | 279 | fn predicate(&self) -> impl Fn(&Unit) -> bool + 'a { 280 | let types = self.types; 281 | move |u| !types.contains(&u.type_id()) 282 | } 283 | } 284 | 285 | impl<'a, I, T> Iterator for ExcludeTypes<'a, I, T> 286 | where 287 | I: Iterator, 288 | I::Item: Borrow, 289 | T: Container, 290 | { 291 | type Item = I::Item; 292 | 293 | iterator_methods!(); 294 | } 295 | 296 | impl<'a, I, T> DoubleEndedIterator for ExcludeTypes<'a, I, T> 297 | where 298 | I: DoubleEndedIterator, 299 | I::Item: Borrow, 300 | T: Container, 301 | { 302 | double_ended_iterator_methods!(); 303 | } 304 | 305 | make_simple_iterator!( 306 | /// An iterator that filters ground units. 307 | Ground, 308 | |u| !u.is_flying() 309 | ); 310 | 311 | make_simple_iterator!( 312 | /// An iterator that filters flying units. 313 | Flying, 314 | |u| u.is_flying() 315 | ); 316 | 317 | make_simple_iterator!( 318 | /// An iterator that filters ready units and structures. 319 | Ready, 320 | |u| u.is_ready() 321 | ); 322 | 323 | make_simple_iterator!( 324 | /// An iterator that filters units structures in-progress. 325 | NotReady, 326 | |u| !u.is_ready() 327 | ); 328 | 329 | make_simple_iterator!( 330 | /// An iterator that filters units with no orders. 331 | Idle, 332 | |u| u.is_idle() 333 | ); 334 | 335 | make_simple_iterator!( 336 | /// An iterator that filters units with no orders or almost finished orders. 337 | AlmostIdle, 338 | |u| u.is_almost_idle() 339 | ); 340 | 341 | make_simple_iterator!( 342 | /// An iterator that filters units with no orders (this also handles buildings with reactor). 343 | Unused, 344 | |u| u.is_unused() 345 | ); 346 | 347 | make_simple_iterator!( 348 | /// An iterator that filters units with no orders or almost finished orders 349 | /// (this also handles buildings with reactor). 350 | AlmostUnused, 351 | |u| u.is_almost_unused() 352 | ); 353 | 354 | make_simple_iterator!( 355 | /// An iterator that filters units units visible on current step. 356 | Visible, 357 | |u| u.is_visible() 358 | ); 359 | 360 | /// An iterator that filters units in attack range of given unit. 361 | #[derive(Clone)] 362 | pub struct InRangeOf<'a, I> { 363 | iter: I, 364 | unit: &'a Unit, 365 | gap: f32, 366 | } 367 | impl<'a, I> InRangeOf<'a, I> { 368 | pub(super) fn new(iter: I, unit: &'a Unit, gap: f32) -> Self { 369 | Self { iter, unit, gap } 370 | } 371 | 372 | fn predicate(&self) -> impl Fn(&Unit) -> bool + 'a { 373 | let unit = self.unit; 374 | let gap = self.gap; 375 | move |u| unit.in_range(u, gap) 376 | } 377 | } 378 | impl_simple_iterator!(InRangeOf<'a>); 379 | 380 | /// An iterator that filters units close enough to attack given unit. 381 | #[derive(Clone)] 382 | pub struct InRange<'a, I> { 383 | iter: I, 384 | unit: &'a Unit, 385 | gap: f32, 386 | } 387 | impl<'a, I> InRange<'a, I> { 388 | pub(super) fn new(iter: I, unit: &'a Unit, gap: f32) -> Self { 389 | Self { iter, unit, gap } 390 | } 391 | 392 | fn predicate(&self) -> impl Fn(&Unit) -> bool + 'a { 393 | let unit = self.unit; 394 | let gap = self.gap; 395 | move |u| u.in_range(unit, gap) 396 | } 397 | } 398 | impl_simple_iterator!(InRange<'a>); 399 | 400 | /// An iterator that filters units in attack range of given unit (this also handles range upgrades). 401 | #[derive(Clone)] 402 | pub struct InRealRangeOf<'a, I> { 403 | iter: I, 404 | unit: &'a Unit, 405 | gap: f32, 406 | } 407 | impl<'a, I> InRealRangeOf<'a, I> { 408 | pub(super) fn new(iter: I, unit: &'a Unit, gap: f32) -> Self { 409 | Self { iter, unit, gap } 410 | } 411 | 412 | fn predicate(&self) -> impl Fn(&Unit) -> bool + 'a { 413 | let unit = self.unit; 414 | let gap = self.gap; 415 | move |u| unit.in_real_range(u, gap) 416 | } 417 | } 418 | impl_simple_iterator!(InRealRangeOf<'a>); 419 | 420 | /// An iterator that filters units close enough to attack given unit (this also handles range upgrades). 421 | #[derive(Clone)] 422 | pub struct InRealRange<'a, I> { 423 | iter: I, 424 | unit: &'a Unit, 425 | gap: f32, 426 | } 427 | impl<'a, I> InRealRange<'a, I> { 428 | pub(super) fn new(iter: I, unit: &'a Unit, gap: f32) -> Self { 429 | Self { iter, unit, gap } 430 | } 431 | 432 | fn predicate(&self) -> impl Fn(&Unit) -> bool + 'a { 433 | let unit = self.unit; 434 | let gap = self.gap; 435 | move |u| u.in_real_range(unit, gap) 436 | } 437 | } 438 | impl_simple_iterator!(InRealRange<'a>); 439 | 440 | /// Helper trait for iterators over units. 441 | pub trait UnitsIterator: Iterator + Sized 442 | where 443 | Self::Item: Borrow, 444 | { 445 | /// Searches for unit with given tag and returns it if found. 446 | fn find_tag(mut self, tag: u64) -> Option { 447 | self.find(|u| u.borrow().tag() == tag) 448 | } 449 | /// Leaves only units with given tags. 450 | fn find_tags>(self, tags: &T) -> FindTags { 451 | FindTags::new(self, tags) 452 | } 453 | /// Leaves only units of given type. 454 | fn of_type(self, unit_type: UnitTypeId) -> OfType { 455 | OfType::new(self, unit_type) 456 | } 457 | /// Excludes units of given type. 458 | fn exclude_type(self, unit_type: UnitTypeId) -> ExcludeType { 459 | ExcludeType::new(self, unit_type) 460 | } 461 | /// Leaves only units of given types. 462 | fn of_types>(self, types: &T) -> OfTypes { 463 | OfTypes::new(self, types) 464 | } 465 | /// Excludes units of given types. 466 | fn exclude_types>(self, types: &T) -> ExcludeTypes { 467 | ExcludeTypes::new(self, types) 468 | } 469 | /// Leaves only non-flying units. 470 | fn ground(self) -> Ground { 471 | Ground::new(self) 472 | } 473 | /// Leaves only flying units. 474 | fn flying(self) -> Flying { 475 | Flying::new(self) 476 | } 477 | /// Leaves only ready structures. 478 | fn ready(self) -> Ready { 479 | Ready::new(self) 480 | } 481 | /// Leaves only structures in-progress. 482 | fn not_ready(self) -> NotReady { 483 | NotReady::new(self) 484 | } 485 | /// Leaves only units with no orders. 486 | fn idle(self) -> Idle { 487 | Idle::new(self) 488 | } 489 | /// Leaves only units with no orders or that almost finished their orders. 490 | fn almost_idle(self) -> AlmostIdle { 491 | AlmostIdle::new(self) 492 | } 493 | /// Leaves only units with no orders. 494 | /// Unlike [`idle`](Self::idle) this takes reactor on terran buildings into account. 495 | fn unused(self) -> Unused { 496 | Unused::new(self) 497 | } 498 | /// Leaves only units with no orders or that almost finished their orders. 499 | /// Unlike [`almost_idle`](Self::almost_idle) this takes reactor on terran buildings into account. 500 | fn almost_unused(self) -> AlmostUnused { 501 | AlmostUnused::new(self) 502 | } 503 | /// Leaves only units visible on current step. 504 | fn visible(self) -> Visible { 505 | Visible::new(self) 506 | } 507 | /// Leaves only units in attack range of given unit. 508 | fn in_range_of(self, unit: &Unit, gap: f32) -> InRangeOf { 509 | InRangeOf::new(self, unit, gap) 510 | } 511 | /// Leaves only units that are close enough to attack given unit. 512 | fn in_range(self, unit: &Unit, gap: f32) -> InRange { 513 | InRange::new(self, unit, gap) 514 | } 515 | /// Leaves only units in attack range of given unit. 516 | /// Unlike [`in_range_of`](Self::in_range_of) this takes range upgrades into account. 517 | fn in_real_range_of(self, unit: &Unit, gap: f32) -> InRealRangeOf { 518 | InRealRangeOf::new(self, unit, gap) 519 | } 520 | /// Leaves only units that are close enough to attack given unit. 521 | /// Unlike [`in_range`](Self::in_range) this takes range upgrades into account. 522 | fn in_real_range(self, unit: &Unit, gap: f32) -> InRealRange { 523 | InRealRange::new(self, unit, gap) 524 | } 525 | } 526 | 527 | impl UnitsIterator for I 528 | where 529 | I: Iterator + Sized, 530 | I::Item: Borrow, 531 | { 532 | } 533 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | //! Different utilites useful (or useless) in bot development. 2 | 3 | use indexmap::IndexSet; 4 | use rustc_hash::{FxHashMap, FxHashSet, FxHasher}; 5 | use std::hash::{BuildHasherDefault, Hash}; 6 | 7 | type FxIndexSet = IndexSet>; 8 | 9 | /// DBSCAN implementation in Rust. 10 | /// 11 | /// Inputs: 12 | /// - `data`: iterable collection of points. 13 | /// - `range_query`: function that should return neighbors of given point. 14 | /// - `min_points`: minimum neighbors required for point to not be marked as noise. 15 | /// 16 | /// Returns: (Clusters, Noise). 17 | pub fn dbscan<'a, DT, P, F>(data: DT, range_query: F, min_points: usize) -> (Vec>, FxHashSet

) 18 | where 19 | DT: IntoIterator, 20 | P: Eq + Hash + Clone + 'a, 21 | F: Fn(&P) -> FxIndexSet

, 22 | { 23 | let mut c = 0; 24 | let mut clusters = Vec::>::new(); 25 | let mut noise = FxHashSet::

::default(); 26 | let mut solved = FxHashSet::

::default(); 27 | for p in data { 28 | if solved.contains(p) { 29 | continue; 30 | } 31 | solved.insert(p.clone()); 32 | 33 | let neighbors = range_query(p); 34 | if neighbors.len() < min_points { 35 | noise.insert(p.clone()); 36 | } else { 37 | match clusters.get_mut(c) { 38 | Some(cluster) => cluster.push(p.clone()), 39 | None => clusters.push(vec![p.clone()]), 40 | } 41 | 42 | let mut seed_set = neighbors; 43 | while let Some(q) = seed_set.pop() { 44 | if noise.remove(&q) { 45 | clusters[c].push(q.clone()); 46 | } else if !solved.contains(&q) { 47 | clusters[c].push(q.clone()); 48 | solved.insert(q.clone()); 49 | let neighbors = range_query(&q); 50 | if neighbors.len() >= min_points { 51 | seed_set.extend(neighbors); 52 | } 53 | } 54 | } 55 | c += 1; 56 | } 57 | } 58 | (clusters, noise) 59 | } 60 | 61 | /// Generates `range_query` function for [`dbscan`]. 62 | /// 63 | /// Takes: 64 | /// - `data`: iterable collection of points (the same data should be passed in [`dbscan`]). 65 | /// - `distance`: function that should returns distance between 2 given points. 66 | /// - `epsilon`: maximum distance between neighbors. 67 | pub fn range_query<'a, DT, P, D, F>(data: DT, distance: F, epsilon: D) -> impl Fn(&P) -> FxIndexSet

68 | where 69 | DT: IntoIterator + Clone, 70 | P: Eq + Hash + Clone + 'a, 71 | D: PartialOrd, 72 | F: Fn(&P, &P) -> D, 73 | { 74 | move |q: &P| { 75 | data.clone() 76 | .into_iter() 77 | .filter(|p| distance(q, p) <= epsilon) 78 | .cloned() 79 | .collect() 80 | } 81 | } 82 | 83 | #[cfg(feature = "parking_lot")] 84 | use parking_lot::{RwLock, RwLockReadGuard}; 85 | #[cfg(not(feature = "parking_lot"))] 86 | use std::sync::{RwLock, RwLockReadGuard}; 87 | 88 | fn read(lock: &RwLock) -> RwLockReadGuard { 89 | #[cfg(feature = "parking_lot")] 90 | let reader = lock.read(); 91 | #[cfg(not(feature = "parking_lot"))] 92 | let reader = lock.read().unwrap(); 93 | 94 | reader 95 | } 96 | 97 | #[derive(Default)] 98 | pub struct CacheMap(RwLock>); 99 | impl CacheMap 100 | where 101 | K: Copy + Eq + Hash, 102 | V: Copy, 103 | { 104 | pub fn get_or_create(&self, k: &K, f: F) -> V 105 | where 106 | F: FnOnce() -> V, 107 | { 108 | let lock = read(&self.0); 109 | if let Some(res) = lock.get(k) { 110 | *res 111 | } else { 112 | drop(lock); 113 | 114 | #[cfg(feature = "parking_lot")] 115 | let mut lock = self.0.write(); 116 | #[cfg(not(feature = "parking_lot"))] 117 | let mut lock = self.0.write().unwrap(); 118 | 119 | let res = f(); 120 | lock.insert(*k, res); 121 | res 122 | } 123 | } 124 | pub fn get(&self, k: &K) -> Option { 125 | read(&self.0).get(k).copied() 126 | } 127 | } 128 | --------------------------------------------------------------------------------