├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE ├── README.md ├── src ├── ArgParser.cpp ├── ArgParser.h ├── CMakeLists.txt ├── CameraModule.cpp ├── CameraModule.h ├── Timer.hpp ├── Tools.h ├── ToolsCommon.cpp ├── ToolsUnix.cpp ├── ToolsWindows.cpp └── main.cpp └── test ├── ArgParser.test.cpp ├── CMakeLists.txt └── ExampleReplayInterloperBotScreenMovement.SC2Replay /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X trash 2 | .DS_Store 3 | 4 | # Build sandbox. 5 | build 6 | 7 | # IDE - VSCode 8 | .vscode/ 9 | 10 | # Clangd cache 11 | .cache/ 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "cpp-sc2"] 2 | path = cpp-sc2 3 | url = https://github.com/alkurbatov/cpp-sc2.git 4 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.10) 2 | 3 | project(Observer) 4 | 5 | # Specify output directories. 6 | set(EXECUTABLE_OUTPUT_PATH "${PROJECT_BINARY_DIR}/bin") 7 | 8 | # Build with c++14 support required by sc2api. 9 | # Build with c++17 support required by ToolsCommon. 10 | set(CMAKE_CXX_STANDARD 17) 11 | 12 | # Disable building of examples in the cpp-sc2 submodule. 13 | set(BUILD_API_EXAMPLES OFF CACHE INTERNAL "" FORCE) 14 | 15 | # Disable building of tests in the cpp-sc2 submodule. 16 | set(BUILD_API_TESTS OFF CACHE INTERNAL "" FORCE) 17 | 18 | # Search for Google test suite. 19 | find_package(GTest REQUIRED) 20 | include_directories(${GTEST_INCLUDE_DIRS}) 21 | 22 | if (MSVC) 23 | # Setup MSVC parallelized builds. 24 | add_compile_options(/MP) 25 | 26 | # Use statically linked runtime. 27 | set(CMAKE_MSVC_RUNTIME_LIBRARY MultiThreaded$<$:Debug>) 28 | endif () 29 | 30 | add_subdirectory("cpp-sc2") 31 | add_subdirectory("src") 32 | add_subdirectory("test") 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aiarena/SC2AutoObserver/47da21292f9649c6c238302ff11204081d98b184/LICENSE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sc2AutoObserver 2 | 3 | ## About 4 | Starcraft 2 replays auto observer inspired by the [SSCAIT-ObserverModule](https://github.com/Plankton555/SSCAIT-ObserverModule). 5 | The Sc2AutoObserver is used to view replays on the [SC2 AI Arena 24/7 livestream](https://www.twitch.tv/aiarenastream) 6 | 7 | ## Build requirements 8 | 1. Download (the password is iagreetotheeula) at least one of the following map packs: 9 | * [Ladder 2019 Season 3](http://blzdistsc2-a.akamaihd.net/MapPacks/Ladder2019Season3.zip) 10 | 11 | 2. Put the downloaded maps into the Maps folder (create it if the folder doesn't exist): 12 | * Windows: C:\Program Files\StarCraft II\Maps 13 | * OS X: /Applications/StarCraft II/Maps 14 | 15 | 3. Download and install [CMake >= 3.10](https://cmake.org/download/). 16 | 17 | 4. A compiler with C++17 support. 18 | 19 | 5. Install [Google test >= 1.10.0](https://github.com/google/googletest) 20 | 21 | 5. Windows: Download and install [Visual Studio](https://www.visualstudio.com/downloads/) 22 | 23 | 6. OS X: Install XCode. 24 | 25 | ## Build instructions 26 | 27 | ### Windows (Visual Studio) 28 | ```bat 29 | :: Clone the project. 30 | $ git clone --recursive git@gitlab.com:aiarena/sc2autoobserver.git 31 | $ cd sc2autoobserver 32 | 33 | :: Create build directory. 34 | $ mkdir build 35 | $ cd build 36 | 37 | :: Generate VS solution. 38 | $ cmake ../ -G "Visual Studio 15 2017 Win64" 39 | 40 | :: Build the project using Visual Studio. 41 | $ start Observer.sln 42 | 43 | :: Launch the observer. 44 | $ bin/Observer.exe --Path "" --Speed ` 45 | ``` 46 | 47 | ### OS X (Xcode) 48 | ```bash 49 | # Clone the project. 50 | $ git clone --recursive git@gitlab.com:aiarena/sc2autoobserver.git && cd sc2autoobserve 51 | 52 | # Create build directory. 53 | $ mkdir build && cd build 54 | 55 | # Generate a Makefile. 56 | # Use 'cmake -DCMAKE_BUILD_TYPE=Debug ../' if debuginfo is needed 57 | # Debug build also contains additional debug features and chat commands support. 58 | $ cmake ../ -G Xcode 59 | 60 | # Build the project using Xcode. 61 | $ open Observer.xcodeproj/ 62 | 63 | # Launch the observer. 64 | $ ./bin/Observer --Path "" --Speed ` 65 | ``` 66 | 67 | ## Load replays from an older SC2 versions 68 | To load replays from older an older SC2 version, one should additionally specify game version hash and path to the older SC2 executable, e.g. for 4.10 version: 69 | ### Windows 70 | ```bat 71 | $ bin/Observer.exe --Path "C:\Users\lladdy\Documents\358809_TyrZ_DoogieHowitzer_IceandChromeLE.SC2Replay" -- -d "B89B5D6FA7CBF6452E721311BFBC6CB2" -e "D:\Battle.net\StarCraft II\Versions\Base75689\SC2.exe" 72 | ``` 73 | ### OS X 74 | ```bash 75 | $ ./bin/Observer --Path "/Users/alkurbatov/Downloads/358809_TyrZ_DoogieHowitzer_IceandChromeLE.SC2Replay" -- -d "B89B5D6FA7CBF6452E721311BFBC6CB2" -e "/Applications/StarCraft II/Versions/Base75689/SC2.app/Contents/MacOS/SC2" 76 | ``` 77 | 78 | ## License 79 | Copyright (c) 2017 Daniel K�hntopp 80 | 81 | Licensed under the [MIT license](LICENSE). 82 | -------------------------------------------------------------------------------- /src/ArgParser.cpp: -------------------------------------------------------------------------------- 1 | #include "ArgParser.h" 2 | 3 | #include 4 | 5 | void splitInputOptions(int argc, char* argv[], std::vector* observer_options, std::vector* sc2_options) { 6 | const char* delimiter = "--"; 7 | 8 | observer_options->push_back(argv[0]); 9 | sc2_options->push_back(argv[0]); 10 | 11 | std::vector* marker = observer_options; 12 | 13 | for (int i = 1; i < argc; ++i) { 14 | if (strcmp(argv[i], delimiter) == 0) { 15 | marker = sc2_options; 16 | continue; 17 | } 18 | 19 | marker->push_back(argv[i]); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/ArgParser.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | void splitInputOptions(int argc, char* argv[], std::vector* observer_options, std::vector* sc2_options); 6 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(Observer) 2 | 3 | target_sources(Observer 4 | PRIVATE 5 | ArgParser.h 6 | ArgParser.cpp 7 | CameraModule.h 8 | CameraModule.cpp 9 | main.cpp 10 | Timer.hpp 11 | Tools.h 12 | ToolsCommon.cpp 13 | $<$:ToolsWindows.cpp> 14 | $<$:ToolsUnix.cpp> 15 | ) 16 | 17 | include_directories(SYSTEM 18 | ${PROJECT_SOURCE_DIR}/cpp-sc2/include 19 | ${PROJECT_SOURCE_DIR}/cpp-sc2/contrib/protobuf/src 20 | ${PROJECT_BINARY_DIR}/cpp-sc2/generated 21 | "." 22 | ) 23 | 24 | if (MSVC) 25 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4 /WX-") 26 | elseif (CMAKE_CXX_COMPILER_ID MATCHES "Clang") 27 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Weverything \ 28 | -Wno-c++98-compat -Wno-c++98-compat-pedantic -Wno-padded \ 29 | -Wno-switch-enum -Wno-weak-vtables -Wno-exit-time-destructors \ 30 | -Wno-float-equal -Wno-global-constructors" 31 | ) 32 | endif () 33 | 34 | set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG") 35 | 36 | link_directories(${PROJECT_BINARY_DIR}/cpp-sc2/bin) 37 | 38 | target_link_libraries(Observer 39 | sc2api sc2utils 40 | ) 41 | 42 | if (APPLE) 43 | target_link_libraries(Observer "-framework Carbon") 44 | endif () 45 | -------------------------------------------------------------------------------- /src/CameraModule.cpp: -------------------------------------------------------------------------------- 1 | #include "CameraModule.h" 2 | #include 3 | #include "sc2api/sc2_api.h" 4 | #include "sc2api/sc2_unit_filters.h" 5 | #include "sc2api/sc2_proto_interface.h" 6 | 7 | // Radius to detect groups of army units 8 | const float armyBlobRadius = 10.0f; 9 | // Movement speed of camera 10 | const float moveFactor = 0.1f; 11 | // Only smooth movemnt when closer as... 12 | const float cameraJumpThreshold = 30.0f; 13 | // When is a unit near a start location 14 | const float nearStartLocationDistance = 50.0f; 15 | // Camera distance to map (zoom). 0 means default. 16 | const float cameraDistance = 50.0f; 17 | 18 | CameraModule::CameraModule(sc2::Client * const bot) : 19 | m_initialized(false), 20 | m_client(bot), 21 | cameraMoveTime(200), 22 | cameraMoveTimeMin(75), 23 | watchScoutWorkerUntil(7500), 24 | lastMoved(0), 25 | lastMovedPriority(0), 26 | lastMovedPosition(sc2::Point2D(0, 0)), 27 | cameraFocusPosition(sc2::Point2D(0, 0)), 28 | cameraFocusUnit(nullptr), 29 | followUnit(false) 30 | { 31 | } 32 | 33 | void CameraModule::onStart() 34 | { 35 | if (m_client->Control() && m_client->Control()->GetObservation()) 36 | { 37 | if (lastMoved) 38 | { 39 | lastMoved = 0; 40 | lastMovedPriority = 0; 41 | lastMovedPosition = sc2::Point2D(0, 0); 42 | cameraFocusUnit = nullptr; 43 | followUnit = false; 44 | } 45 | setPlayerIds(); 46 | setPlayerStartLocations(); 47 | cameraFocusPosition = (*m_startLocations.begin()).second; 48 | currentCameraPosition = (*m_startLocations.begin()).second; 49 | m_initialized = true; 50 | } 51 | } 52 | 53 | void CameraModule::onFrame() 54 | { 55 | // Sometimes the first GetObservation() fails... 56 | if (!m_initialized) 57 | { 58 | onStart(); 59 | return; 60 | } 61 | moveCameraFallingNuke(); 62 | moveCameraNukeDetect(); 63 | // moveCameraIsUnderAttack(); 64 | moveCameraIsAttacking(); 65 | if (m_client->Observation()->GetGameLoop() <= watchScoutWorkerUntil) 66 | { 67 | moveCameraScoutWorker(); 68 | } 69 | moveCameraDrop(); 70 | moveCameraArmy(); 71 | 72 | updateCameraPosition(); 73 | } 74 | 75 | void CameraModule::moveCameraFallingNuke() 76 | { 77 | const int prio = 6; 78 | if (!shouldMoveCamera(prio)) 79 | { 80 | return; 81 | } 82 | for (auto & unit : m_client->Observation()->GetUnits()) 83 | { 84 | if (unit->unit_type == sc2::UNIT_TYPEID::TERRAN_NUKE) 85 | { 86 | moveCamera(unit, prio); 87 | return; 88 | } 89 | } 90 | } 91 | 92 | void CameraModule::moveCameraNukeDetect() 93 | { 94 | const int prio = 5; 95 | if (!shouldMoveCamera(prio)) 96 | { 97 | return; 98 | } 99 | for (auto & effects : m_client->Observation()->GetEffects()) 100 | { 101 | if (effects.effect_id == uint32_t(7)) // 7 = NukePersistent NOT TESTED YET 102 | { 103 | moveCamera(effects.positions.front(), prio); 104 | return; 105 | } 106 | } 107 | } 108 | 109 | void CameraModule::moveCameraIsUnderAttack() 110 | { 111 | const int prio = 4; 112 | if (!shouldMoveCamera(prio)) 113 | { 114 | return; 115 | } 116 | 117 | for (auto & unit : m_client->Observation()->GetUnits()) 118 | { 119 | if (isUnderAttack(unit)) 120 | { 121 | moveCamera(unit, prio); 122 | } 123 | } 124 | } 125 | 126 | 127 | void CameraModule::moveCameraIsAttacking() 128 | { 129 | const int prio = 4; 130 | if (!shouldMoveCamera(prio)) 131 | { 132 | return; 133 | } 134 | 135 | for (auto & unit : m_client->Observation()->GetUnits()) 136 | { 137 | if (isAttacking(unit)) 138 | { 139 | moveCamera(unit, prio); 140 | } 141 | } 142 | } 143 | 144 | void CameraModule::moveCameraScoutWorker() 145 | { 146 | const int highPrio = 2; 147 | const int lowPrio = 0; 148 | if (!shouldMoveCamera(lowPrio)) 149 | { 150 | return; 151 | } 152 | 153 | for (auto & unit : m_client->Observation()->GetUnits()) 154 | { 155 | if (!IsWorkerType(unit->unit_type)) 156 | { 157 | continue; 158 | } 159 | if (isNearOpponentStartLocation(unit->pos, unit->owner)) 160 | { 161 | moveCamera(unit, highPrio); 162 | } 163 | else if (!isNearOwnStartLocation(unit->pos, unit->owner)) 164 | { 165 | moveCamera(unit, lowPrio); 166 | } 167 | } 168 | } 169 | 170 | void CameraModule::moveCameraDrop() 171 | { 172 | const int prio = 3; 173 | if (!shouldMoveCamera(prio)) 174 | { 175 | return; 176 | } 177 | for (auto & unit : m_client->Observation()->GetUnits()) 178 | { 179 | if ((unit->unit_type.ToType() == sc2::UNIT_TYPEID::ZERG_OVERLORDTRANSPORT || unit->unit_type.ToType() == sc2::UNIT_TYPEID::TERRAN_MEDIVAC || unit->unit_type.ToType() == sc2::UNIT_TYPEID::PROTOSS_WARPPRISM) 180 | && isNearOpponentStartLocation(unit->pos, unit->owner) && unit->cargo_space_taken > 0) 181 | { 182 | moveCamera(unit, prio); 183 | } 184 | } 185 | } 186 | 187 | void CameraModule::moveCameraArmy() 188 | { 189 | int prio = 1; 190 | if (!shouldMoveCamera(prio)) 191 | { 192 | return; 193 | } 194 | // Double loop, check if army units are close to each other 195 | 196 | sc2::Point2D bestPos; 197 | const sc2::Unit * bestPosUnit = nullptr; 198 | int mostUnitsNearby = 0; 199 | 200 | for (auto & unit : m_client->Observation()->GetUnits()) 201 | { 202 | if (!isArmyUnitType(unit->unit_type.ToType()) || unit->display_type != sc2::Unit::DisplayType::Visible || !(unit->owner == 1 || unit->owner == 2)) 203 | { 204 | continue; 205 | } 206 | sc2::Point2D uPos = unit->pos; 207 | 208 | int nrUnitsNearby = 0; 209 | for (auto & nearbyUnit : m_client->Observation()->GetUnits()) 210 | { 211 | if (!isArmyUnitType(nearbyUnit->unit_type.ToType()) || unit->display_type != sc2::Unit::DisplayType::Visible || !(unit->owner == 1 || unit->owner == 2) || Dist(unit->pos, nearbyUnit->pos) > armyBlobRadius ) 212 | { 213 | continue; 214 | } 215 | nrUnitsNearby++; 216 | } 217 | if (nrUnitsNearby > mostUnitsNearby) { 218 | mostUnitsNearby = nrUnitsNearby; 219 | bestPos = uPos; 220 | bestPosUnit = unit; 221 | } 222 | } 223 | 224 | if (mostUnitsNearby > 1) { 225 | moveCamera(bestPosUnit, prio); 226 | } 227 | } 228 | 229 | void CameraModule::moveCameraUnitCreated(const sc2::Unit * unit) 230 | { 231 | if (!m_initialized) 232 | { 233 | return; 234 | } 235 | int prio; 236 | if (isBuilding(unit->unit_type)) 237 | { 238 | prio = 2; 239 | } 240 | else 241 | { 242 | prio = 1; 243 | } 244 | if (!shouldMoveCamera(prio) || unit->unit_type.ToType() == sc2::UNIT_TYPEID::TERRAN_KD8CHARGE || unit->unit_type.ToType() == sc2::UNIT_TYPEID::ZERG_LARVA || unit->unit_type.ToType() == sc2::UNIT_TYPEID::PROTOSS_INTERCEPTOR) 245 | { 246 | return; 247 | } 248 | else if (!IsWorkerType(unit->unit_type)) 249 | { 250 | moveCamera(unit, prio); 251 | } 252 | } 253 | 254 | bool CameraModule::shouldMoveCamera(const int priority) const 255 | { 256 | unsigned int elapsedFrames = m_client->Observation()->GetGameLoop() - lastMoved; 257 | bool isTimeToMove = elapsedFrames >= cameraMoveTime; 258 | bool isTimeToMoveIfHigherPrio = elapsedFrames >= cameraMoveTimeMin; 259 | bool isHigherPrio = lastMovedPriority < priority || (followUnit && !cameraFocusUnit->is_alive); 260 | // camera should move IF: enough time has passed OR (minimum time has passed AND new prio is higher) 261 | return isTimeToMove || (isHigherPrio && isTimeToMoveIfHigherPrio); 262 | } 263 | 264 | void CameraModule::moveCamera(const sc2::Point2D pos, const int priority) 265 | { 266 | if (!shouldMoveCamera(priority)) 267 | { 268 | return; 269 | } 270 | if (followUnit == false && cameraFocusPosition == pos) 271 | { 272 | // don't register a camera move if the position is the same 273 | return; 274 | } 275 | 276 | cameraFocusPosition = pos; 277 | lastMovedPosition = cameraFocusPosition; 278 | lastMoved = m_client->Observation()->GetGameLoop(); 279 | lastMovedPriority = priority; 280 | followUnit = false; 281 | } 282 | 283 | void CameraModule::moveCamera(const sc2::Unit * unit, int priority) 284 | { 285 | if (!shouldMoveCamera(priority)) 286 | { 287 | return; 288 | } 289 | if (followUnit == true && cameraFocusUnit == unit) { 290 | // don't register a camera move if we follow the same unit 291 | return; 292 | } 293 | 294 | cameraFocusUnit = unit; 295 | lastMovedPosition = cameraFocusUnit->pos; 296 | lastMoved = m_client->Observation()->GetGameLoop(); 297 | lastMovedPriority = priority; 298 | followUnit = true; 299 | } 300 | 301 | void CameraModule::updateCameraPosition() 302 | { 303 | if (followUnit && isValidPos(cameraFocusUnit->pos)) 304 | { 305 | cameraFocusPosition = cameraFocusUnit->pos; 306 | } 307 | // We only do smooth movement, if the focus is nearby. 308 | float dist = Dist(currentCameraPosition, cameraFocusPosition); 309 | if (dist > cameraJumpThreshold) 310 | { 311 | currentCameraPosition = cameraFocusPosition; 312 | } 313 | else if (dist > 0.1f) 314 | { 315 | currentCameraPosition = currentCameraPosition + sc2::Point2D( 316 | moveFactor*(cameraFocusPosition.x - currentCameraPosition.x), 317 | moveFactor*(cameraFocusPosition.y - currentCameraPosition.y)); 318 | } 319 | else 320 | { 321 | return; 322 | } 323 | if (isValidPos(currentCameraPosition)) 324 | { 325 | updateCameraPositionExcecute(); 326 | } 327 | } 328 | 329 | // Utility 330 | 331 | //At the moment there is no flag for being under attack 332 | bool CameraModule::isUnderAttack(const sc2::Unit *) const 333 | { 334 | return false; 335 | } 336 | 337 | bool CameraModule::isAttacking(const sc2::Unit * attacker) const 338 | { 339 | if (!isArmyUnitType(attacker->unit_type.ToType()) || attacker->display_type != sc2::Unit::DisplayType::Visible || !(attacker->owner == 1 || attacker->owner == 2)) 340 | { 341 | return false; 342 | } 343 | // Option A 344 | // return unit->orders.size()>0 && unit->orders.front().ability_id.ToType() == sc2::ABILITY_ID::ATTACK_ATTACK; 345 | // Option B 346 | // return unit->weapon_cooldown > 0.0f; 347 | // Option C 348 | // Not sure if observer can see the "private" fields of player units. 349 | // So we just assume: if it is in range and an army unit -> it attacks 350 | std::vector weapons = m_client->Observation()->GetUnitTypeData()[attacker->unit_type].weapons; 351 | float rangeAir = -1.0; 352 | float rangeGround = -1.0; 353 | for (auto & weapon : weapons) 354 | { 355 | if (weapon.type == sc2::Weapon::TargetType::Any) 356 | { 357 | rangeAir = weapon.range; 358 | rangeGround = weapon.range; 359 | } 360 | else if (weapon.type == sc2::Weapon::TargetType::Air) 361 | { 362 | rangeAir = weapon.range; 363 | } 364 | else if (weapon.type == sc2::Weapon::TargetType::Ground) 365 | { 366 | rangeGround = weapon.range; 367 | } 368 | } 369 | int enemyID = getOpponent(attacker->owner); 370 | for (auto & unit : m_client->Observation()->GetUnits()) 371 | { 372 | if (unit->display_type != sc2::Unit::DisplayType::Visible || unit->owner != enemyID || !(attacker->owner == 1 || attacker->owner == 2)) 373 | { 374 | continue; 375 | } 376 | if (unit->is_flying) 377 | { 378 | if (Dist(attacker->pos, unit->pos) < rangeAir) 379 | { 380 | return true; 381 | } 382 | } 383 | else 384 | { 385 | if (Dist(attacker->pos, unit->pos) < rangeGround) 386 | { 387 | return true; 388 | } 389 | } 390 | } 391 | return false; 392 | } 393 | 394 | bool CameraModule::IsWorkerType(const sc2::UNIT_TYPEID type) const 395 | { 396 | switch (type) 397 | { 398 | case sc2::UNIT_TYPEID::TERRAN_SCV: return true; 399 | case sc2::UNIT_TYPEID::TERRAN_MULE: return true; 400 | case sc2::UNIT_TYPEID::PROTOSS_PROBE: return true; 401 | case sc2::UNIT_TYPEID::ZERG_DRONE: return true; 402 | case sc2::UNIT_TYPEID::ZERG_DRONEBURROWED: return true; 403 | default: return false; 404 | } 405 | } 406 | 407 | bool CameraModule::isNearOpponentStartLocation(const sc2::Point2D pos, const int player) const 408 | { 409 | return isNearOwnStartLocation(pos, getOpponent(player)); 410 | } 411 | 412 | bool CameraModule::isNearOwnStartLocation(const sc2::Point2D pos, const int player) const 413 | { 414 | return Dist(pos, m_startLocations.at(player)) < nearStartLocationDistance; 415 | } 416 | 417 | bool CameraModule::isArmyUnitType(const sc2::UNIT_TYPEID type) const 418 | { 419 | if (IsWorkerType(type)) { return false; } 420 | if (type == sc2::UNIT_TYPEID::ZERG_OVERLORD) { return false; } // Excluded here the overlord transport etc to count them as army unit 421 | if (isBuilding(type)) { return false; } 422 | if (type == sc2::UNIT_TYPEID::ZERG_EGG) { return false; } 423 | if (type == sc2::UNIT_TYPEID::ZERG_LARVA) { return false; } 424 | if (type == sc2::UNIT_TYPEID::PROTOSS_INTERCEPTOR) { return false; } 425 | 426 | return true; 427 | } 428 | 429 | bool CameraModule::isBuilding(const sc2::UNIT_TYPEID type) const 430 | { 431 | switch (type) 432 | { 433 | // Terran 434 | case sc2::UNIT_TYPEID::TERRAN_ARMORY: 435 | case sc2::UNIT_TYPEID::TERRAN_BARRACKS: 436 | case sc2::UNIT_TYPEID::TERRAN_BARRACKSFLYING: 437 | case sc2::UNIT_TYPEID::TERRAN_BARRACKSREACTOR: 438 | case sc2::UNIT_TYPEID::TERRAN_BARRACKSTECHLAB: 439 | case sc2::UNIT_TYPEID::TERRAN_BUNKER: 440 | case sc2::UNIT_TYPEID::TERRAN_COMMANDCENTER: 441 | case sc2::UNIT_TYPEID::TERRAN_COMMANDCENTERFLYING: 442 | case sc2::UNIT_TYPEID::TERRAN_ENGINEERINGBAY: 443 | case sc2::UNIT_TYPEID::TERRAN_FACTORY: 444 | case sc2::UNIT_TYPEID::TERRAN_FACTORYFLYING: 445 | case sc2::UNIT_TYPEID::TERRAN_FACTORYREACTOR: 446 | case sc2::UNIT_TYPEID::TERRAN_FACTORYTECHLAB: 447 | case sc2::UNIT_TYPEID::TERRAN_FUSIONCORE: 448 | case sc2::UNIT_TYPEID::TERRAN_GHOSTACADEMY: 449 | case sc2::UNIT_TYPEID::TERRAN_MISSILETURRET: 450 | case sc2::UNIT_TYPEID::TERRAN_ORBITALCOMMAND: 451 | case sc2::UNIT_TYPEID::TERRAN_ORBITALCOMMANDFLYING: 452 | case sc2::UNIT_TYPEID::TERRAN_PLANETARYFORTRESS: 453 | case sc2::UNIT_TYPEID::TERRAN_REFINERY: 454 | case sc2::UNIT_TYPEID::TERRAN_SENSORTOWER: 455 | case sc2::UNIT_TYPEID::TERRAN_STARPORT: 456 | case sc2::UNIT_TYPEID::TERRAN_STARPORTFLYING: 457 | case sc2::UNIT_TYPEID::TERRAN_STARPORTREACTOR: 458 | case sc2::UNIT_TYPEID::TERRAN_STARPORTTECHLAB: 459 | case sc2::UNIT_TYPEID::TERRAN_SUPPLYDEPOT: 460 | case sc2::UNIT_TYPEID::TERRAN_SUPPLYDEPOTLOWERED: 461 | case sc2::UNIT_TYPEID::TERRAN_REACTOR: 462 | case sc2::UNIT_TYPEID::TERRAN_TECHLAB: 463 | 464 | // Zerg 465 | case sc2::UNIT_TYPEID::ZERG_BANELINGNEST: 466 | case sc2::UNIT_TYPEID::ZERG_CREEPTUMOR: 467 | case sc2::UNIT_TYPEID::ZERG_CREEPTUMORBURROWED: 468 | case sc2::UNIT_TYPEID::ZERG_CREEPTUMORQUEEN: 469 | case sc2::UNIT_TYPEID::ZERG_EVOLUTIONCHAMBER: 470 | case sc2::UNIT_TYPEID::ZERG_EXTRACTOR: 471 | case sc2::UNIT_TYPEID::ZERG_GREATERSPIRE: 472 | case sc2::UNIT_TYPEID::ZERG_HATCHERY: 473 | case sc2::UNIT_TYPEID::ZERG_HIVE: 474 | case sc2::UNIT_TYPEID::ZERG_HYDRALISKDEN: 475 | case sc2::UNIT_TYPEID::ZERG_INFESTATIONPIT: 476 | case sc2::UNIT_TYPEID::ZERG_LAIR: 477 | case sc2::UNIT_TYPEID::ZERG_LURKERDENMP: 478 | case sc2::UNIT_TYPEID::ZERG_NYDUSCANAL: 479 | case sc2::UNIT_TYPEID::ZERG_NYDUSNETWORK: 480 | case sc2::UNIT_TYPEID::ZERG_SPAWNINGPOOL: 481 | case sc2::UNIT_TYPEID::ZERG_SPINECRAWLER: 482 | case sc2::UNIT_TYPEID::ZERG_SPINECRAWLERUPROOTED: 483 | case sc2::UNIT_TYPEID::ZERG_SPIRE: 484 | case sc2::UNIT_TYPEID::ZERG_SPORECRAWLER: 485 | case sc2::UNIT_TYPEID::ZERG_SPORECRAWLERUPROOTED: 486 | case sc2::UNIT_TYPEID::ZERG_ULTRALISKCAVERN: 487 | 488 | // Protoss 489 | case sc2::UNIT_TYPEID::PROTOSS_ASSIMILATOR: 490 | case sc2::UNIT_TYPEID::PROTOSS_CYBERNETICSCORE: 491 | case sc2::UNIT_TYPEID::PROTOSS_DARKSHRINE: 492 | case sc2::UNIT_TYPEID::PROTOSS_FLEETBEACON: 493 | case sc2::UNIT_TYPEID::PROTOSS_FORGE: 494 | case sc2::UNIT_TYPEID::PROTOSS_GATEWAY: 495 | case sc2::UNIT_TYPEID::PROTOSS_NEXUS: 496 | case sc2::UNIT_TYPEID::PROTOSS_PHOTONCANNON: 497 | case sc2::UNIT_TYPEID::PROTOSS_PYLON: 498 | case sc2::UNIT_TYPEID::PROTOSS_PYLONOVERCHARGED: 499 | case sc2::UNIT_TYPEID::PROTOSS_ROBOTICSBAY: 500 | case sc2::UNIT_TYPEID::PROTOSS_ROBOTICSFACILITY: 501 | case sc2::UNIT_TYPEID::PROTOSS_STARGATE: 502 | case sc2::UNIT_TYPEID::PROTOSS_TEMPLARARCHIVE: 503 | case sc2::UNIT_TYPEID::PROTOSS_TWILIGHTCOUNCIL: 504 | case sc2::UNIT_TYPEID::PROTOSS_WARPGATE: 505 | case sc2::UNIT_TYPEID::PROTOSS_SHIELDBATTERY: 506 | return true; 507 | default: return false; 508 | } 509 | } 510 | 511 | bool CameraModule::isValidPos(const sc2::Point2D pos) const 512 | { 513 | // Maybe playable width/height? 514 | return pos.x >= 0 && pos.y >= 0 && pos.x < m_client->Observation()->GetGameInfo().width && pos.y < m_client->Observation()->GetGameInfo().height; 515 | } 516 | 517 | float CameraModule::Dist(const sc2::Unit * A, const sc2::Unit * B) const 518 | { 519 | return std::sqrt(std::pow(A->pos.x - B->pos.x, 2) + std::pow(A->pos.y - B->pos.y, 2)); 520 | } 521 | 522 | float CameraModule::Dist(const sc2::Point2D A, const sc2::Point2D B) const 523 | { 524 | return std::sqrt(std::pow(A.x - B.x, 2) + std::pow(A.y - B.y, 2)); 525 | } 526 | 527 | 528 | void CameraModule::setPlayerStartLocations() 529 | { 530 | m_startLocations.clear(); 531 | sc2::Units bases = m_client->Observation()->GetUnits(sc2::IsUnits({ sc2::UNIT_TYPEID::TERRAN_COMMANDCENTER, sc2::UNIT_TYPEID::ZERG_HATCHERY, sc2::UNIT_TYPEID::PROTOSS_NEXUS })); 532 | // If we are not an observer 533 | // Assumes 2 player map 534 | if (bases.size() == 1) 535 | { 536 | m_startLocations[bases.front()->owner] = bases.front()->pos; 537 | m_startLocations[getOpponent(bases.front()->owner)] = getInvertedPos(bases.front()->pos); 538 | } 539 | else 540 | { 541 | for (auto & unit : bases) 542 | { 543 | m_startLocations[unit->owner] = unit->pos; 544 | } 545 | } 546 | } 547 | 548 | void CameraModule::setPlayerIds() 549 | { 550 | for (auto & player : m_client->Observation()->GetGameInfo().player_info) 551 | { 552 | if (player.player_type != sc2::PlayerType::Observer) 553 | { 554 | m_playerIDs.push_back(player.player_id); 555 | } 556 | } 557 | } 558 | 559 | int CameraModule::getOpponent(const int player) const 560 | { 561 | for (auto i : m_playerIDs) 562 | { 563 | if (i != player) 564 | { 565 | return i; 566 | } 567 | } 568 | return -1; 569 | } 570 | 571 | const sc2::Point2D CameraModule::getInvertedPos(const sc2::Point2D pos) const 572 | { 573 | return sc2::Point2D(m_client->Observation()->GetGameInfo().width - pos.x, m_client->Observation()->GetGameInfo().width - pos.y); 574 | } 575 | 576 | CameraModule::~CameraModule() 577 | { 578 | } 579 | 580 | ///////////////////////// For observers 581 | CameraModuleObserver::CameraModuleObserver(sc2::ReplayObserver * const observer) :CameraModule(observer), m_observer(observer) 582 | { 583 | } 584 | 585 | void CameraModuleObserver::onStart() 586 | { 587 | // This should work once it is implemented on the proto side. 588 | sc2::GameRequestPtr request = m_observer->Control()->Proto().MakeRequest(); 589 | SC2APIProtocol::RequestObserverAction* obsRequest = request->mutable_obs_action(); 590 | SC2APIProtocol::ObserverAction* action = obsRequest->add_actions(); 591 | SC2APIProtocol::ActionObserverPlayerPerspective * player_perspective = action->mutable_player_perspective(); 592 | player_perspective->set_player_id(0); // 0 = everyone 593 | m_client->Control()->Proto().SendRequest(request); 594 | m_client->Control()->WaitForResponse(); 595 | CameraModule::onStart(); 596 | } 597 | 598 | void CameraModuleObserver::updateCameraPositionExcecute() 599 | { 600 | m_observer->ObserverAction()->CameraMove(currentCameraPosition, cameraDistance); 601 | } 602 | 603 | ///////////////////////// For agents 604 | CameraModuleAgent::CameraModuleAgent(sc2::Agent * const agent) :CameraModule(agent) 605 | { 606 | } 607 | 608 | 609 | void CameraModuleAgent::updateCameraPositionExcecute() 610 | { 611 | sc2::GameRequestPtr camera_request = m_client->Control()->Proto().MakeRequest(); 612 | SC2APIProtocol::RequestAction* request_action = camera_request->mutable_action(); 613 | SC2APIProtocol::Action* action = request_action->add_actions(); 614 | SC2APIProtocol::ActionRaw* action_raw = action->mutable_action_raw(); 615 | SC2APIProtocol::ActionRawCameraMove* camera_move = action_raw->mutable_camera_move(); 616 | 617 | SC2APIProtocol::Point* point = camera_move->mutable_center_world_space(); 618 | point->set_x(currentCameraPosition.x); 619 | point->set_y(currentCameraPosition.y); 620 | 621 | m_client->Control()->Proto().SendRequest(camera_request); 622 | m_client->Control()->WaitForResponse(); 623 | //m_client->Debug()->DebugMoveCamera(currentCameraPosition); 624 | //m_client->Debug()->SendDebug(); 625 | } 626 | -------------------------------------------------------------------------------- /src/CameraModule.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "sc2api/sc2_api.h" 3 | 4 | class CameraModule 5 | { 6 | protected: 7 | 8 | //Sometimes getObservation fails when starting a game. 9 | bool m_initialized; 10 | 11 | sc2::Client * m_client; 12 | std::vector m_playerIDs; 13 | std::map m_startLocations; 14 | 15 | unsigned int cameraMoveTime; 16 | unsigned int cameraMoveTimeMin; 17 | uint32_t watchScoutWorkerUntil; 18 | 19 | unsigned int lastMoved; 20 | int lastMovedPriority; 21 | sc2::Point2D lastMovedPosition; 22 | sc2::Point2D currentCameraPosition; 23 | sc2::Point2D cameraFocusPosition; 24 | const sc2::Unit * cameraFocusUnit; 25 | bool followUnit; 26 | 27 | void moveCamera(const sc2::Point2D pos, const int priority); 28 | void moveCamera(const sc2::Unit * unit, int priority); 29 | void moveCameraIsAttacking(); 30 | void moveCameraIsUnderAttack(); 31 | void moveCameraScoutWorker(); 32 | void moveCameraFallingNuke(); 33 | void moveCameraNukeDetect(); 34 | void moveCameraDrop(); 35 | void moveCameraArmy(); 36 | void updateCameraPosition(); 37 | bool shouldMoveCamera(const int priority) const; 38 | bool isNearOpponentStartLocation(const sc2::Point2D pos, const int player) const; 39 | bool isNearOwnStartLocation(const sc2::Point2D pos, int player) const; 40 | bool isArmyUnitType(const sc2::UNIT_TYPEID type) const; 41 | bool isBuilding(const sc2::UNIT_TYPEID type) const; 42 | bool isValidPos(const sc2::Point2D pos) const; 43 | bool isUnderAttack(const sc2::Unit * unit) const; 44 | bool isAttacking(const sc2::Unit * unit) const; 45 | bool IsWorkerType(const sc2::UNIT_TYPEID type) const; 46 | float Dist(const sc2::Unit * A, const sc2::Unit * B) const; 47 | float Dist(const sc2::Point2D A, const sc2::Point2D B) const; 48 | void setPlayerStartLocations(); 49 | void setPlayerIds(); 50 | int getOpponent(const int player) const; 51 | const sc2::Point2D getInvertedPos(const sc2::Point2D) const; 52 | 53 | //Depending on whether we have an agent or an observer we need to use different functions to move the camera 54 | virtual void updateCameraPositionExcecute() = 0; 55 | 56 | public: 57 | explicit CameraModule(sc2::Client * const bot); 58 | void onStart(); 59 | void onFrame(); 60 | void moveCameraUnitCreated(const sc2::Unit * unit); 61 | 62 | virtual ~CameraModule(); 63 | }; 64 | 65 | class CameraModuleObserver : public CameraModule 66 | { 67 | private: 68 | sc2::ReplayObserver * const m_observer; 69 | void updateCameraPositionExcecute() override; 70 | public: 71 | CameraModuleObserver(sc2::ReplayObserver * const observer); 72 | void onStart(); 73 | }; 74 | 75 | class CameraModuleAgent : public CameraModule 76 | { 77 | void updateCameraPositionExcecute() override; 78 | public: 79 | CameraModuleAgent(sc2::Agent * const observer); 80 | }; 81 | -------------------------------------------------------------------------------- /src/Timer.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #ifdef _WIN32 // Windows system specific 8 | #include 9 | #else // Unix based system specific 10 | #include 11 | #endif 12 | 13 | #include 14 | 15 | class Timer 16 | { 17 | double startTimeInMicroSec; // starting time in micro-second 18 | double endTimeInMicroSec; // ending time in micro-second 19 | int stopped; // stop flag 20 | #ifdef _WIN32 21 | LARGE_INTEGER frequency; // ticks per second 22 | LARGE_INTEGER startCount; // 23 | LARGE_INTEGER endCount; // 24 | #else 25 | timeval startCount; // 26 | timeval endCount; // 27 | #endif 28 | 29 | public: 30 | 31 | Timer() 32 | { 33 | #ifdef _WIN32 34 | QueryPerformanceFrequency(&frequency); 35 | startCount.QuadPart = 0; 36 | endCount.QuadPart = 0; 37 | #else 38 | startCount.tv_sec = startCount.tv_usec = 0; 39 | endCount.tv_sec = endCount.tv_usec = 0; 40 | #endif 41 | 42 | stopped = 0; 43 | startTimeInMicroSec = 0; 44 | endTimeInMicroSec = 0; 45 | 46 | start(); 47 | } 48 | 49 | ~Timer() {} // default destructor 50 | 51 | void start() 52 | { 53 | stopped = 0; // reset stop flag 54 | 55 | #ifdef _WIN32 56 | QueryPerformanceCounter(&startCount); 57 | #else 58 | gettimeofday(&startCount, nullptr); 59 | #endif 60 | } 61 | 62 | void stop() 63 | { 64 | stopped = 1; // set timer stopped flag 65 | 66 | #ifdef _WIN32 67 | QueryPerformanceCounter(&endCount); 68 | #else 69 | gettimeofday(&endCount, nullptr); 70 | #endif 71 | } 72 | 73 | double getElapsedTimeInMicroSec() 74 | { 75 | #ifdef _WIN32 76 | if (!stopped) 77 | QueryPerformanceCounter(&endCount); 78 | 79 | startTimeInMicroSec = startCount.QuadPart * (1000000.0 / frequency.QuadPart); 80 | endTimeInMicroSec = endCount.QuadPart * (1000000.0 / frequency.QuadPart); 81 | #else 82 | if (!stopped) 83 | gettimeofday(&endCount, nullptr); 84 | 85 | startTimeInMicroSec = (startCount.tv_sec * 1000000.0) + startCount.tv_usec; 86 | endTimeInMicroSec = (endCount.tv_sec * 1000000.0) + endCount.tv_usec; 87 | #endif 88 | 89 | return endTimeInMicroSec - startTimeInMicroSec; 90 | } 91 | 92 | double getElapsedTimeInMilliSec() 93 | { 94 | return this->getElapsedTimeInMicroSec() * 0.001; 95 | } 96 | 97 | 98 | double getElapsedTimeInSec() 99 | { 100 | return this->getElapsedTimeInMicroSec() * 0.000001; 101 | } 102 | 103 | double getElapsedTime() 104 | { 105 | return this->getElapsedTimeInSec(); 106 | } 107 | 108 | template 109 | static void TimeFunction(std::function function, ...) 110 | { 111 | Timer t; 112 | t.start(); 113 | va_list args; 114 | va_start(args, function); 115 | va_end(args); 116 | 117 | double ms = t.getElapsedTimeInMilliSec(); 118 | std::cout << "Function ran in " << ms << "ms\n"; 119 | } 120 | }; 121 | -------------------------------------------------------------------------------- /src/Tools.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | 7 | bool loadReplayPaths(const std::string& path, std::vector& dst); 8 | 9 | void pressDKey(); 10 | -------------------------------------------------------------------------------- /src/ToolsCommon.cpp: -------------------------------------------------------------------------------- 1 | #include "Tools.h" 2 | 3 | #include 4 | 5 | namespace fs = std::filesystem; 6 | 7 | 8 | bool loadReplayPaths(const std::string& path, std::vector& dst) 9 | { 10 | if (fs::path(path).extension() == ".SC2Replay") { 11 | dst.push_back(path); 12 | return false; 13 | } 14 | 15 | for (const auto& entry : fs::directory_iterator(path)) { 16 | if (entry.path().extension() != ".SC2Replay") 17 | continue; 18 | 19 | dst.push_back(entry.path().string()); 20 | } 21 | 22 | return true; 23 | } 24 | -------------------------------------------------------------------------------- /src/ToolsUnix.cpp: -------------------------------------------------------------------------------- 1 | #include "Tools.h" 2 | 3 | void pressDKey() 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /src/ToolsWindows.cpp: -------------------------------------------------------------------------------- 1 | #include "Tools.h" 2 | 3 | #include 4 | 5 | 6 | void pressDKey() 7 | { 8 | auto hwnd = FindWindow(NULL, TEXT("StarCraft II")); 9 | if (hwnd != 0) 10 | { 11 | SetForegroundWindow(hwnd); 12 | SetFocus(hwnd); 13 | SetActiveWindow(hwnd); 14 | INPUT ip; 15 | ip.type = INPUT_KEYBOARD; 16 | ip.ki.wScan = 0; // hardware scan code for key 17 | ip.ki.time = 0; 18 | ip.ki.dwExtraInfo = 0; 19 | 20 | // Press the "D" key 21 | ip.ki.wVk = 0x44; // virtual-key code for the "D" key 22 | ip.ki.dwFlags = 0; // 0 for key press 23 | SendInput(1, &ip, sizeof(INPUT)); 24 | 25 | // Release the "D" key. 26 | ip.ki.dwFlags = KEYEVENTF_KEYUP; // KEYEVENTF_KEYUP for key release 27 | SendInput(1, &ip, sizeof(INPUT)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "ArgParser.h" 2 | #include "CameraModule.h" 3 | #include "Timer.hpp" 4 | #include "Tools.h" 5 | 6 | #include "sc2api/sc2_api.h" 7 | #include "sc2api/sc2_gametypes.h" 8 | #include "sc2utils/sc2_manage_process.h" 9 | #include "sc2utils/sc2_arg_parser.h" 10 | 11 | #include 12 | #include 13 | 14 | class Replay : public sc2::ReplayObserver 15 | { 16 | public: 17 | CameraModuleObserver m_cameraModule; 18 | float m_speed; 19 | long m_delay; 20 | bool m_toggle_production_tab; 21 | std::map < sc2::Tag, bool> alreadySeen; 22 | 23 | Replay(float speed, long delay, bool toggle_production_tab): 24 | sc2::ReplayObserver(), m_cameraModule(this), 25 | m_speed(speed), m_delay(delay), 26 | m_toggle_production_tab(toggle_production_tab) 27 | { 28 | } 29 | 30 | void OnGameStart() final 31 | { 32 | /* For some reason it is not a good idea to already do this on start. 33 | for (const auto & unit : Observation()->GetUnits()) 34 | { 35 | alreadySeen[unit->tag] = true; 36 | } 37 | */ 38 | m_cameraModule.onStart(); 39 | 40 | if (m_toggle_production_tab) 41 | pressDKey(); 42 | } 43 | 44 | void OnUnitCreated(const sc2::Unit* unit) final 45 | { 46 | m_cameraModule.moveCameraUnitCreated(unit); 47 | } 48 | 49 | void OnUnitEnterVision(const sc2::Unit*) final 50 | { 51 | // Does not get called. 52 | } 53 | 54 | void OnStep() final 55 | { 56 | Timer t; 57 | t.start(); 58 | Observation()->GetChatMessages(); 59 | for (const auto & unit : Observation()->GetUnits()) 60 | { 61 | if (!alreadySeen[unit->tag]) 62 | { 63 | alreadySeen[unit->tag] = true; 64 | OnUnitCreated(unit); 65 | } 66 | } 67 | m_cameraModule.onFrame(); 68 | while (t.getElapsedTimeInMilliSec() < 1000.0 / 22.4 * static_cast(m_speed)) 69 | { 70 | } 71 | } 72 | 73 | void OnGameEnd() final 74 | { 75 | std::cout << "Game end." << std::endl; 76 | 77 | Timer t; 78 | t.start(); 79 | while (t.getElapsedTimeInMilliSec() < m_delay) 80 | { 81 | } 82 | } 83 | }; 84 | 85 | 86 | int main(int argc, char* argv[]) 87 | { 88 | std::vector observer_options; 89 | std::vector sc2_options; 90 | splitInputOptions(argc, argv, &observer_options, &sc2_options); 91 | 92 | sc2::ArgParser arg_parser(observer_options[0]); 93 | arg_parser.AddOptions({ 94 | { "-p", "--Path", "Path to a single SC2 replay or directory with replay files", true }, 95 | { "-s", "--Speed", "Replay speed", false}, 96 | { "-d", "--Delay", "Delay after game in ms.", false}, 97 | { "-t", "--Toggle", "Toggle the Production tab on game start (Windows only).", false}, 98 | }); 99 | arg_parser.Parse(static_cast(observer_options.size()), &observer_options[0]); 100 | 101 | std::string replayPath; 102 | if (!arg_parser.Get("Path", replayPath)) 103 | { 104 | std::cout << "Please provide the path to a single SC2 replay or directory with replay files via --Path." << std::endl; 105 | return 1; 106 | } 107 | float speed; 108 | std::string speedString; 109 | if (arg_parser.Get("Speed", speedString)) 110 | { 111 | speed = strtof(speedString.c_str(), nullptr); 112 | } 113 | else 114 | { 115 | speed = 4.0f; 116 | std::cout << "Using default speed: 4x" << std::endl; 117 | } 118 | 119 | long delay; 120 | std::string delayString; 121 | if (arg_parser.Get("Delay", delayString)) 122 | { 123 | delay = strtol(delayString.c_str(), nullptr, 10); 124 | } 125 | else 126 | { 127 | delay = 3000; 128 | std::cout << "Using default delay: 3000ms" << std::endl; 129 | } 130 | 131 | std::string dummy; 132 | bool toggle_production_tab = arg_parser.Get("Toggle", dummy); 133 | 134 | std::vector replayFiles; 135 | unsigned long replayIndex = 0; 136 | sc2::Coordinator coordinator; 137 | if (!coordinator.LoadSettings(static_cast(sc2_options.size()), &sc2_options[0])) { 138 | return 1; 139 | } 140 | Replay replayObserver(speed, delay, toggle_production_tab); 141 | coordinator.AddReplayObserver(&replayObserver); 142 | coordinator.SetReplayPerspective(0); 143 | //coordinator.SetRealtime(true); 144 | coordinator.SetFullScreen(1); 145 | while (true) 146 | { 147 | bool isDirectory = loadReplayPaths(replayPath, replayFiles); 148 | if (replayIndex == replayFiles.size()) 149 | { 150 | std::cout << "There are no more replays at " << replayPath << "\\*" << std::endl << "I will wait for 30 seconds and try again." << std::endl; 151 | sc2::SleepFor(30000); 152 | continue; 153 | } 154 | const std::string replayFile = replayFiles[replayIndex]; 155 | if (!coordinator.SetReplayPath(replayFile)) 156 | { 157 | std::cout << "Could not read the replay: " << replayFile << std::endl; 158 | std::cout << "Please provide the replay path as command line argument." << std::endl; 159 | return 1; 160 | } 161 | if (!coordinator.HasReplays()) 162 | { 163 | std::cout << "Could not read the replay: " << replayFile << std::endl; 164 | std::cout << "Please provide the replay path as command line argument." << std::endl; 165 | return 1; 166 | } 167 | while (coordinator.Update() && !coordinator.AllGamesEnded()) 168 | { 169 | } 170 | if (!isDirectory) 171 | { 172 | break; 173 | } 174 | ++replayIndex; 175 | } 176 | coordinator.LeaveGame(); 177 | } 178 | -------------------------------------------------------------------------------- /test/ArgParser.test.cpp: -------------------------------------------------------------------------------- 1 | #include "src/ArgParser.h" 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | TEST(splitInputOptions, ProperlySplitsObserverAndSC2Args) { 9 | std::vector observer_options; 10 | std::vector sc2_options; 11 | 12 | const char* argv [] = { 13 | "./bin/Observer", 14 | "--Path", 15 | "/Users/alkurbatov/Downloads/358809_TyrZ_DoogieHowitzer_IceandChromeLE.SC2Replay", 16 | "--", 17 | "-d", 18 | "B89B5D6FA7CBF6452E721311BFBC6CB2", 19 | "-e", 20 | "/Applications/StarCraft II/Versions/Base75689/SC2.app/Contents/MacOS/SC2" 21 | }; 22 | int argc = sizeof argv / sizeof argv[0]; 23 | 24 | splitInputOptions(argc, const_cast(argv), &observer_options, &sc2_options); 25 | 26 | ASSERT_THAT( 27 | observer_options, 28 | testing::ElementsAre( 29 | "./bin/Observer", 30 | "--Path", 31 | "/Users/alkurbatov/Downloads/358809_TyrZ_DoogieHowitzer_IceandChromeLE.SC2Replay" 32 | ) 33 | ); 34 | 35 | ASSERT_THAT( 36 | sc2_options, 37 | testing::ElementsAre( 38 | "./bin/Observer", 39 | "-d", 40 | "B89B5D6FA7CBF6452E721311BFBC6CB2", 41 | "-e", 42 | "/Applications/StarCraft II/Versions/Base75689/SC2.app/Contents/MacOS/SC2" 43 | ) 44 | ); 45 | } 46 | 47 | TEST(splitInputOptions, DoesntSplitAnythingIfDelimiterNotProvided) { 48 | std::vector observer_options; 49 | std::vector sc2_options; 50 | 51 | const char* argv [] = { 52 | "./bin/Observer", 53 | "--Path", 54 | "/Users/alkurbatov/Downloads/358809_TyrZ_DoogieHowitzer_IceandChromeLE.SC2Replay" 55 | }; 56 | int argc = sizeof argv / sizeof argv[0]; 57 | 58 | splitInputOptions(argc, const_cast(argv), &observer_options, &sc2_options); 59 | 60 | ASSERT_THAT( 61 | observer_options, 62 | testing::ElementsAre( 63 | "./bin/Observer", 64 | "--Path", 65 | "/Users/alkurbatov/Downloads/358809_TyrZ_DoogieHowitzer_IceandChromeLE.SC2Replay" 66 | ) 67 | ); 68 | 69 | ASSERT_THAT( 70 | sc2_options, 71 | testing::ElementsAre( 72 | "./bin/Observer" 73 | ) 74 | ); 75 | } 76 | 77 | TEST(splitInputOptions, ReturnsObserverArgsIfDelimiterPassedWithoutSC2Options) { 78 | std::vector observer_options; 79 | std::vector sc2_options; 80 | 81 | const char* argv [] = { 82 | "./bin/Observer", 83 | "--Path", 84 | "/Users/alkurbatov/Downloads/358809_TyrZ_DoogieHowitzer_IceandChromeLE.SC2Replay", 85 | "--" 86 | }; 87 | int argc = sizeof argv / sizeof argv[0]; 88 | 89 | splitInputOptions(argc, const_cast(argv), &observer_options, &sc2_options); 90 | 91 | ASSERT_THAT( 92 | observer_options, 93 | testing::ElementsAre( 94 | "./bin/Observer", 95 | "--Path", 96 | "/Users/alkurbatov/Downloads/358809_TyrZ_DoogieHowitzer_IceandChromeLE.SC2Replay" 97 | ) 98 | ); 99 | 100 | ASSERT_THAT( 101 | sc2_options, 102 | testing::ElementsAre( 103 | "./bin/Observer" 104 | ) 105 | ); 106 | } 107 | 108 | TEST(splitInputOptions, ReturnsSC2ArgsIfDelimiterPassedWithoutObserverOptions) { 109 | std::vector observer_options; 110 | std::vector sc2_options; 111 | 112 | const char* argv [] = { 113 | "./bin/Observer", 114 | "--", 115 | "-d", 116 | "B89B5D6FA7CBF6452E721311BFBC6CB2" 117 | }; 118 | int argc = sizeof argv / sizeof argv[0]; 119 | 120 | splitInputOptions(argc, const_cast(argv), &observer_options, &sc2_options); 121 | 122 | ASSERT_THAT( 123 | observer_options, 124 | testing::ElementsAre( 125 | "./bin/Observer" 126 | ) 127 | ); 128 | 129 | ASSERT_THAT( 130 | sc2_options, 131 | testing::ElementsAre( 132 | "./bin/Observer", 133 | "-d", 134 | "B89B5D6FA7CBF6452E721311BFBC6CB2" 135 | ) 136 | ); 137 | } 138 | 139 | TEST(splitInputOptions, DoesntFailIfNoOptionsSpecified) { 140 | std::vector observer_options; 141 | std::vector sc2_options; 142 | 143 | const char* argv [] = { 144 | "./bin/Observer" 145 | }; 146 | int argc = sizeof argv / sizeof argv[0]; 147 | 148 | splitInputOptions(argc, const_cast(argv), &observer_options, &sc2_options); 149 | 150 | ASSERT_THAT( 151 | observer_options, 152 | testing::ElementsAre( 153 | "./bin/Observer" 154 | ) 155 | ); 156 | 157 | ASSERT_THAT( 158 | sc2_options, 159 | testing::ElementsAre( 160 | "./bin/Observer" 161 | ) 162 | ); 163 | } 164 | -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include(GoogleTest) 2 | 3 | include_directories(${PROJECT_SOURCE_DIR}) 4 | 5 | add_executable( 6 | TestArgParser 7 | ${PROJECT_SOURCE_DIR}/src/ArgParser.h 8 | ${PROJECT_SOURCE_DIR}/src/ArgParser.cpp 9 | ArgParser.test.cpp) 10 | target_link_libraries(TestArgParser GTest::Main) 11 | gtest_discover_tests(TestArgParser) 12 | -------------------------------------------------------------------------------- /test/ExampleReplayInterloperBotScreenMovement.SC2Replay: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aiarena/SC2AutoObserver/47da21292f9649c6c238302ff11204081d98b184/test/ExampleReplayInterloperBotScreenMovement.SC2Replay --------------------------------------------------------------------------------