Brood War API – The Comprehensive Guide: Buildings, and placement

Index for the Comprehensive Guide posts

Whee! After some vacation, I’m back to writing this guide! This article is a bit all over the place, but thematically kinda-sorta fits together. Well, you have to get the book, I guess! Let’s get into it.

(Chapter: An inside look into StarCraft)

First, additions to the previous part.

Some additions to the psionic matrix:

The psionic matrix is not a rectangle, but rather an ellipse. There is a 5×8 array describing the shape, which visually corresponds to the bottom right quadrant of the pylon range. Mirroring this (not rotating) gives the full shape of the pylon coverage. The matrix looks like this:

static const bool psi_field_mask[5][8] = {
    { 1, 1, 1, 1, 1, 1, 1, 1 },
    { 1, 1, 1, 1, 1, 1, 1, 1 },
    { 1, 1, 1, 1, 1, 1, 1, 0 },
    { 1, 1, 1, 1, 1, 1, 0, 0 },
    { 1, 1, 1, 0, 0, 0, 0, 0 }
};

(Continuing the section about morphing buldings)

Normally, a Zerg Drone’s health will be the same before beginning morphing, and after cancelling morphing. The exception is building an Extractor, since behind the scenes, the game destroys the drone (and the Geyser!), then creates the morphing Extractor. If the Extractor is canceled, a new drone gets created, this time with full health. This can be used to heal up Drones quickly, although it costs minerals to do so.

Addons

Some Terran buildings can have addons (Nuclear Silo and Comsat Station for Command Centers, Machine Shop for Factories, Control Tower for Starports, Covert Ops and Physics Lab for Science Facilities). These can enable training certain units, or unlock certain researches, or provide special abilites. Addons can be disconnected via lifting or destroying the parent building. After disconnecting, they no longer provide their function, and any Terran player can connect a building to them, they become neutral. Addons occupy 2×2 building tiles, and always one tile to the right from the parent building’s bottom right tile.

Training units

Some Terran and Protoss buildings can train units. These units have a training queue, where up to five units can be placed. The resources for training are deducted at the point of issuing the training order. When training of a unit is complete, the unit is placed to the first available position around the building where it can fit. It is usually right next to the training building, but not always. An example when it’s not placed there:

The code that dictates the unit placement is the following (explanation further below):

bool place_completed_unit(unit_t* u) {
	if (!us_hidden(u)) return true;
	bool res;
	xy pos;
	std::tie(res, pos) = find_unit_placement(u, u->sprite->position, false);
	if (!res) {
		display_last_error_for_player(u->owner);
		return false;
	}
	move_unit(u, pos);
	unit_t* turret = unit_turret(u);
	if (turret) {
		add_completed_unit(turret, 1, false);
		u_set_status_flag(turret, unit_t::status_flag_completed);
		turret->hp = turret->unit_type->hitpoints;
		move_unit(turret, pos);
	}
	return true;
}

If the unit can’t be placed, the message “Building exit is blocked” is displayed, the training is canceled, and the full cost of the unit is refunded.

Finding unit placement

And then there is a really long code segment for finding the unit placement (find_unit_placement). This method is used elsewhere as well – I will reference this section in the future. It can be observed that completed units are placed in a kinda-sorta counter-clockwise fashion, starting from one tile below the building’s bottom left corner). Let’s take a look at it. (Code a little below)

What does this method do? The return values of the method is a true/false value and a position – if and where the unit can be placed. It can be called two different ways, by providing a rectangle in the arguments (rect bounds), and without it. If it’s called without, the method is executed with a rectangle that has a top left corner 128 pixels (to the top and left, naturally) from the unit’s position, and a bottom right corner 127 pixels from the same position. It is mostly called without, so this is the basic placement rectangle. There is one exception: when a gas harvester exits a refinery, the target position is not the unit’s position.

First, the method trims down the bounds to be inside of the map (You don’t want your unit outside of that). There is a lambda expression called blocking_unit_pred, which is declared here. This returns true, if the checked unit is a Starport, Robotics Facility, or Stargate, and the unit being placed is a flyer created at the respective building. If the checked unit is none of those, then the checked and the placed units’ flying property is compared. If they are equal (both are flyers or both are not), this returns true. Otherwise, false. Keep in mind that this method ( find_unit_placement) is used in multiple places, not just when training units.

After this definition, the first search is performed. The unit’s inner bounding box is searched for another blocking unit. If no blocking unit is found, and the unit being placed is a flyer or can fit the target position, then the unit is placed there. If there is no blocking unit, but the unit can’t fit the position and it is not a flyer, and the terrain_displaces_unit variable is set to false, the unit cannot be placed.

The only place where the terrain_displaces_unit is set to true is when searching for a gas exit position. (So if the unit being placed is not blocked by another unit, it’s either placed in the target position or blocked by terrain, unless it is searching for a gas exit)

Then an inner find method gets declared, which will be used later. This takes three parameters, a position, a width, and a height. The width and height parameters are twice the base width and height. The base width and height are set to 8 by default, but if there is a blocking unit, they are set to the sum of the following values (for width/height, respectively):

  • The distance from the left/top side to the center of the unit being placed
  • The distance from the center to the right/bottom side of the blocking unit
  • 2 extra pixels.

The position is the unit’s position by default, (which is the same as the training building/transport/other source unit), but not always. If there is a blocking unit, it is modified by the blocking unit’s dimension to the right plus the target unit’s dimension to the left + 2 pixels.

The inner find is executed until just before the search rectangle would exceed the basic placement rectangle in all directions. Until then, the base width/height is increased by 16 pixels, so the search area in turn is 32 pixels larger in each direction. The method searches along the boundary of the area each time, and checks the four directions in the following order:

  • From the bottom left corner to the right
  • From the bottom right corner to the top
  • From the top right corner to the left
  • From the top left corner to the bottom

If the search rectangle is more than twice the width of the unit being placed, then the search rectangle’s left side is shortened by the width of the unit being placed. This is why units are first placed with a left side roughly matching the left side of the first blocking unit.

Each time, if it finds a blocking unit, the search box (which is the size of the unit to be placed) is placed past the blocking units in the target direction. I know this explanation is a bit dense, so here is an example image:

The yellow rectangle represents the basic placement rectangle. The large red rectagles are the ever-increasing search areas of the inner find. The green rectangles are the starting positions. of the inner find method. The purple is the first position the method finds (in that case, the blocking unit is the command center), while the orange squares are the positions where the starting SCVs initially placed – this method is used for that as well.

And behold, the gloriously confusing code segment!

std::pair<bool, xy> find_unit_placement(const unit_t* u, xy pos, rect bounds, bool terrain_displaces_unit) const {
	if (bounds.from.x < 0) bounds.from.x = 0;
	if (bounds.from.y < 0) bounds.from.y = 0;
	if (bounds.to.x >= (int)game_st.map_width) bounds.to.x = (int)game_st.map_width - 1;
	if (bounds.to.y >= (int)game_st.map_height - 32) bounds.to.y = (int)game_st.map_height - 32 - 1;
	auto blocking_unit_pred = [&](const unit_t* target) {
		if (unit_dead(target)) return false;
		if (u_no_collide(u) || u_no_collide(target)) return false;
		if (ut_powerup(u)) {
			return u_grounded_building(target) || ut_powerup(target);
		}
		if (unit_is(target, UnitTypes::Terran_Starport)) {
			if (unit_is(u, UnitTypes::Terran_Wraith)) return true;
			if (unit_is(u, UnitTypes::Terran_Dropship)) return true;
			if (unit_is(u, UnitTypes::Terran_Science_Vessel)) return true;
			if (unit_is(u, UnitTypes::Terran_Battlecruiser)) return true;
			if (unit_is(u, UnitTypes::Terran_Valkyrie)) return true;
		}
		if (unit_is(target, UnitTypes::Protoss_Robotics_Facility)) {
			if (unit_is(u, UnitTypes::Protoss_Shuttle)) return true;
			if (unit_is(u, UnitTypes::Protoss_Observer)) return true;
		}
		if (unit_is(target, UnitTypes::Protoss_Stargate)) {
			if (unit_is(u, UnitTypes::Protoss_Scout)) return true;
			if (unit_is(u, UnitTypes::Protoss_Carrier)) return true;
			if (unit_is(u, UnitTypes::Protoss_Arbiter)) return true;
			if (unit_is(u, UnitTypes::Protoss_Corsair)) return true;
		}
		return u_flying(u) == u_flying(target);
	};
       const unit_t* blocking_unit = find_unit(unit_inner_bounding_box(u, pos), blocking_unit_pred);
	if (!blocking_unit && is_in_bounds(unit_inner_bounding_box(u, pos), {{0, 0}, {(int)game_st.map_width, (int)game_st.map_height - 32}})) {
		if (u_flying(u) || unit_type_can_fit_at(u->unit_type, pos)) return {true, pos};
		if (!terrain_displaces_unit) {
			st.last_error = 60;
			return {false, {}};
		}
	}

	xy find_result;

	auto find = [&](xy pos, int width, int height) {
		rect bb;
		bb.from.x = pos.x / 8u * 8u;
		bb.from.y = pos.y / 8u * 8u;
		bb.to.x = (pos.x + width + 7) / 8u * 8u;
		bb.to.y = (pos.y + height + 7) / 8u * 8u;
		if (bb.from.x < bounds.from.x) bb.from.x = bounds.from.x;
		if (bb.from.y < bounds.from.y) bb.from.y = bounds.from.y;
		if (bb.to.x > bounds.to.x) bb.to.x = bounds.to.x;
		if (bb.to.y > bounds.to.y) bb.to.y = bounds.to.y;

		if (width > (u->unit_type->dimensions.from.x + u->unit_type->dimensions.to.x + 1) * 2) {
			bb.from.x += (u->unit_type->dimensions.from.x + u->unit_type->dimensions.to.x + 1 + 7) / 8u * 8u;
		}

		rect search_bb = unit_inner_bounding_box(u, {bb.from.x, bb.to.y});

		for (int x = bb.from.x; x <= bb.to.x;) {
			if (is_inner_bb_move_target_in_valid_bounds(search_bb)) {
			const unit_t* n = find_unit(search_bb, blocking_unit_pred);
				if (n) {
					int inc = n->sprite->position.x + n->unit_type->dimensions.to.x + 1 - search_bb.from.x;
					inc += (8 - ((x + inc) & 7)) & 7;
					search_bb.from.x += inc;
					search_bb.to.x += inc;
					x += inc;
					continue;
				} else {
					xy pos{x, bb.to.y};
					if (is_reachable(u, pos)) {
						if (u_flying(u) || unit_type_can_fit_at(u->unit_type, pos)) {
							find_result = pos;
							return true;
						}
					}
				}
			}
			search_bb.from.x += 8;
			search_bb.to.x += 8;
			x += 8;
		}

		search_bb = unit_inner_bounding_box(u, {bb.to.x, bb.to.y});

		for (int y = bb.to.y; y >= bb.from.y;) {
			if (is_inner_bb_move_target_in_valid_bounds(search_bb)) {
			const unit_t* n = find_unit(search_bb, blocking_unit_pred);
				if (n) {
					int dec = search_bb.to.y - (n->sprite->position.y - n->unit_type->dimensions.from.y - 1);
					dec += (8 - ((y - dec) & 7)) & 7;
					search_bb.from.y -= dec;
					search_bb.to.y -= dec;
					y -= dec;
					continue;
				} else {
					xy pos{bb.to.x, y};
					if (is_reachable(u, pos)) {
						if (u_flying(u) || unit_type_can_fit_at(u->unit_type, pos)) {
							find_result = pos;
							return true;
						}
					}
				}
			}
			search_bb.from.y -= 8;
			search_bb.to.y -= 8;
			y -= 8;
		}

		if (width > (u->unit_type->dimensions.from.x + u->unit_type->dimensions.to.x + 1) * 2) {
			bb.from.x -= (u->unit_type->dimensions.from.x + u->unit_type->dimensions.to.x + 1 + 7) / 8u * 8u;
		}

		search_bb = unit_inner_bounding_box(u, {bb.to.x, bb.from.y});

		for (int x = bb.to.x; x >= bb.from.x;) {
			if (is_inner_bb_move_target_in_valid_bounds(search_bb)) {
			const unit_t* n = find_unit(search_bb, blocking_unit_pred);
				if (n) {
					int dec = search_bb.to.x - (n->sprite->position.x - n->unit_type->dimensions.from.x - 1);
					dec += (8 - ((x - dec) & 7)) & 7;
					search_bb.from.x -= dec;
					search_bb.to.x -= dec;
					x -= dec;
					continue;
				} else {
					xy pos{x, bb.from.y};
					if (is_reachable(u, pos)) {
						if (u_flying(u) || unit_type_can_fit_at(u->unit_type, pos)) {
							find_result = pos;
							return true;
						}
					}
				}
			}
			search_bb.from.x -= 8;
			search_bb.to.x -= 8;
			x -= 8;
		}

		search_bb = unit_inner_bounding_box(u, {bb.from.x, bb.from.y});

		for (int y = bb.from.y; y <= bb.to.y;) {
			if (is_inner_bb_move_target_in_valid_bounds(search_bb)) {
			const unit_t* n = find_unit(search_bb, blocking_unit_pred);
				if (n) {
					int inc = n->sprite->position.y + n->unit_type->dimensions.to.y + 1 - search_bb.from.y;
					inc += (8 - ((y + inc) & 7)) & 7;
					search_bb.from.y += inc;
					search_bb.to.y += inc;
					y += inc;
					continue;
				} else {
					xy pos{bb.from.x, y};
					if (is_reachable(u, pos)) {
						if (u_flying(u) || unit_type_can_fit_at(u->unit_type, pos)) {
							find_result = pos;
							return true;
						}
					}
				}
			}
			search_bb.from.y += 8;
			search_bb.to.y += 8;
			y += 8;
		}

		return false;
	};


	int width = 8;
	int height = 8;
	if (blocking_unit) {
		int new_width = u->unit_type->dimensions.from.x + blocking_unit->unit_type->dimensions.to.x + 2;
		int new_height = u->unit_type->dimensions.from.y + blocking_unit->unit_type->dimensions.to.y + 2;
		if (new_width > width) width = new_width;
		if (new_height > height) height = new_height;
	}
	while (true) {
		xy npos = pos - xy(width, height);
		if (npos.x < bounds.from.x && npos.y < bounds.from.y && pos.x + width > bounds.to.x && pos.y + height > bounds.to.y)  break;
		if (find(npos, width * 2, height * 2)) return {true, find_result};
		width += 16;
		height += 16;
	}
	st.last_error = 60;
	return {false, {}};
}

std::pair<bool, xy> find_unit_placement(const unit_t* u, xy pos, bool terrain_displaces_unit) const {
	return find_unit_placement(u, pos, {pos - xy(128, 128), pos + xy(127, 127)}, terrain_displaces_unit);
}

Lifting off and landing buildings

Some Terran buildings (and Infested Command Centers) can lift off from the ground, then move as flyers. Only buildings that are not training units, or researching can be ordered to lift off. While lifted off, they can’t produce units, they can turn and move (albeit quite slowly). The lifting off is an iscript animation, therefore takes varying amount of frames per unit type.

Consequently, the unit can land in a proper spot – building placement rules apply just the same as when creating the buildings (so, Infested Command Centers can land on creep)

Destroyed buildings

If a building is destroyed, the following things can happen:

  • If any addon is connected, those will be unusable.
  • If any research or upgrade is in progress by that building, it will be cancelled, and 3/4 of the resource cost of it refunded.
  • If there are any units in the building’s training queue, then the cost of those are fully refunded, regardless of the progress on those units.
  • If the building was a creep provider, the creep provided by it starts to recede. Other creep providers can still sustain the same area though.

Individual unit abilities

In this section, I will examine every unit, with their unique upgrades, damage types, and quirks. Most of this information has been found out with surprising accuracy throughout the years by dedicated players – but seeing the code always helps, and can clear up some common misunderstandings. In the appendix, I will also provide their relevant game data (attack speed in frames, training time and such).

Terran units – Buildings

Command Center: The main building (resource depot) for Terran. It can train SCVs, and build addons. Can lift off.

Comsat Station: Addon for the Command Center. It has one ability, the Scanner Sweep, which reveals an area of the map in a 320 pixel radius, and cloaked units within that area. It gets destroyed after 163 frames. This is dictated by this quirky piece of (iscript) code:

ScannerSweepInit:
    sprol              380 0 0    # Unknown546 (thingy\eveCast.grp)
    wait               6
    sprol              380 32 32    # Unknown546 (thingy\eveCast.grp)
    wait               2
    sprol              380 48 5    # Unknown546 (thingy\eveCast.grp)
    wait               5
    sprol              380 32 224    # Unknown546 (thingy\eveCast.grp)
    wait               2
    sprol              380 251 208    # Unknown546 (thingy\eveCast.grp)
    wait               2
    sprol              380 224 224    # Unknown546 (thingy\eveCast.grp)
    wait               5
    sprol              380 208 254    # Unknown546 (thingy\eveCast.grp)
    wait               3
    sprol              380 224 32    # Unknown546 (thingy\eveCast.grp)
    wait               5
    sprol              380 3 48    # Unknown546 (thingy\eveCast.grp)
    wait               63
    wait               63
    sigorder           4
    goto               ScannerSweepLocal00

We see that the sum of wait times is 156 frames, and since it is subject to the periodic check delay, that means the unit dies at the 163rd frame. However, the vision provided with the scanner sweep can stay longer, since the vision update is not necessarily executed in this frame. It can be up to 99 frames late, so one sweep will provide between 163 and 262 frames of vision.

Inside the game code, the Scanner Sweep is implemented as a unit that cannot be interacted with.

void order_CastScannerSweep(unit_t* u) {
	auto energy_cost = fp8::integer(get_tech_type(TechTypes::Scanner_Sweep)->energy_cost);
	if (u->energy < energy_cost) {
		// todo: callback for error
		order_done(u);
		return;
	}
const unit_type_t* scan_type = get_unit_type(UnitTypes::Spell_Scanner_Sweep);
	xy pos = restrict_move_target_to_valid_bounds(scan_type, u->order_target.pos);
	unit_t* scan = create_unit(scan_type, pos, u->owner);
	if (scan) {
		finish_building_unit(scan);
		complete_unit(scan);
		order_done(u);
		u->energy -= energy_cost;
		play_sound(388, u);
	} else {
		display_last_error_for_player(u->owner);
		order_done(u);
	}
}

Nuclear Silo: Enables the player to build Nuclear Missiles. Only one missile can be built per silo. These can be launched by a Terran Ghost. Nuclear missiles are also implemented as units (There was a bug/exploit in earlier versions of StarCraft, where the nuke could be controlled. This has been patched out in version 1.15.1).

The nuke’s damage is the 2/3 of the unit’s maximum hp plus maximum shields, but minimum 500. Armor and shields do protect against this damage, although that rarely matters. The explosion is an area of effect attack with a splash radius (see Damage section)

if (b->weapon_type->hit_type == weapon_type_t::hit_type_nuclear_missile) {
	int damage = max_visible_hp_plus_shields(target) * 2 / 3;
	if (damage < 500) damage = 500;
	return fp8::integer(damage);
}

Barracks: Trains Terran infantry units (Marine, Firebat, Medic, Ghost). Can lift off.

Supply Depot: Provides 8 supply, and cannot lift off. Often used for walling.

Bunker: Defensive building that can fit 4 Terran infantry units. (Marine, Firebat, Medic, Ghost, SCV). Units inside the bunker get +64 pixels to their weapon range, and cannot be damaged while the Bunker exists. Abilities cannot be used inside, but their effect will continue inside the Bunker- for example, Stim Packs will still enhance attack speed. Medics cannot heal in Bunkers, and units inside are not considered to be on the map, so area of effect damage have no effect on them.

And that’s where I stop this piece of the BWAPI Guide! This part took a looong time to write, especially deciphering the building placement logic. Feel free to point out mistakes/typos, and ask if something is not clear. In the meantime, you can subscribe to the mailing list for updates, or follow me on my social media channels (Facebook, Twitter, YouTube), or even buy some Undermind merch, or consider giving me one dollarydoo on Patreon. Thanks for reading!

Leave a Reply