Skip to content

07. Selection detection part 2

phu54321 edited this page Feb 10, 2018 · 6 revisions

The material used in this chapter can be downloaded at here.

You can skip reading this chapter and just move on to the next one. I will not assume that you have read this chapter for the rest of the course.

I will assume that the reader of this chapter has a basic understanding of C, and have a solid understanding of EUD.

Moving a pawn. We should again detect what square does the selected scourge exists. I haven't acknowledged this issue. We should re-implement the selection detection. In chapter 5, we discussed two ways of getting a selected piece's position.

  • 'Unitnode chunk' holds unit's positions. We have a pointer to the chunk, so just read the coordinates from the chunk.
  • In-memory board can hold each unit's index.

In chapter 5 we followed the latter way. Unfortunately for the scourge, the latter way doesn't seem too probable. So I think for scourge selection detection we should follow the former way of selection detection. So here we will follow the former way.

Internally unitnode chunk follows this structure. In here we need to access the 'position' member. This code has been brought to you by BWAPI project. See here for the source.

class CUnit
{
// v-- POSSIBLE SHARED BULLET/UNIT STRUCTURE BEGIN
// CLink<CUnit> link;
/*0x000*/ BW::CUnit*    prev;
/*0x004*/ BW::CUnit*    next;      /**< Pointer to next unit in the unit linked list, we use
                                      *   it to iterate units.
                                      *   @see BW#BWXFN_UnitNodeTable_FirstElement

/**
 * Skipping the details...
 */

/*0x026*/ u8            _unknown_0x026;
/*0x027*/ u8            flingyMovementType;
/*0x028*/ BW::Position  position;         // Current position of the unit
/*0x02C*/ point         halt;             // @todo Unknown    // Either this or current_speed is officially called "xDX, xDY" (no POINT struct)
/*0x034*/ u32           flingyTopSpeed;

/**
 * Skipping the details...
 */
};

We need to read the position member of this struct. According to the Position.h, BW::Position is defined as the followings.

namespace BW {
    struct Position {
        u16 x, y;
    }
}

Given a CUnit address ptr, we should

  1. Read position value, an 4-byte dword located at ptr + 0x028. Since you can only read in the 4-byte boundary in EUD, you should first read an entire position member instead of reading x and y separately.
  2. Split position struct to x and y.

ptr is shorthand for 'pointer'.

This corresponds to the following code.

const position = dwread_epd(EPD(ptr + 0x028));
const x, y = dwbreak(position)[[0, 1]];

dwread_epd_safe should be used when reading read-only memory. dwread_epd can be used for reading writable memory. This is faster than the safe variant.

You've probably already heard about dwread_epd. dwbreak breaks a single dword to words and bytes, hence dw + break. For example, dwbreak converts dword 0xAABBCCDD to 6 values, 0xCCDD, 0xAABB, 0xDD, 0xCC, 0xBB, 0xAA. The order return value may seem a bit chaotic, but it actually follows the little endian rule. Think it like this. Given a dword memory layout DD CC BB AA, dwbreak breaks this memory layout to 2 words and 4 bytes, DD CC, BB AA, DD, CC, BB, AA. [[0, 1]] means 'select the 0th and 1st value from 6 return values'. Position structure has memory layout like XX XX YY YY, so dwbreak(position)[[0, 1]] means 'dwbreak the position value and return only XX XX and YY YY part, which is what we exactly have wanted.

EPD(ptr) internally is equivalent to (ptr - 0x58A364) / 4 in epScript. In short, multiplication and division take a lot of time, so we need to eliminate that as much as possible.

euddraft uses special variable table structure to optimize addition and subtraction, so these two operations can be performed in three triggers only. However multiplication and division are quite complex, so they require at least 66 triggers each. This is a 32 magnitude faster than doing multiplication and division using a naive SetDeaths and Deaths conditions, but this speed is still unacceptable for most epScript usages.

Luckily for most cases, when we have ptr we also have epd. So we can avoid division for the most time. Let's see how we can eliminate the needs for division on calculating EPD(ptr + 0x028). You can quickly find out that this is equivalent to epd + (0x028) / 4. So, the code for getting x, y coordinates for Unitnode chunk epd is equivalent to:

const position = dwread_epd(epd + (0x028 / 4));
const x, y = dwbreak(position)[[0, 1]];

0x028 / 4 is a constant expression, and euddraft is smart enough to calculate this in compile-time rather than inside StarCraft.

So we can retrieve the position of a unit via CUnit address. In chapter 5, we wrote the following code to retrieve the unitnode chunk address of the selected piece.

    const selectedUnitPointer = dwread_epd_safe(EPD(0x6284E8));
    if (selectedUnitPointer != 0) {
        const selectedUnitIndex = (selectedUnitPointer - 0x59CCA8) / 336;
        /* ... */
    }

We actually don't need to calculate any unitnode indexes here. Just get the coordinates directly!

function getSelectedUnit() {
    const unitptr, unitepd = dwepdread_epd_safe(EPD(0x6284E8));
    if (unitptr != 0) {
        const unitpos = dwread_epd(unitepd + (0x028 / 4));
        const unitX, unitY = dwbreak(unitpos)[[0, 1]];
        // Convert raw coordinates to board cell position
        const unitTileX = (unitX - 768) / 64;
        const unitTileY = (unitY - 768) / 64;

        const unitType = dwread_epd(unitepd + (0x064 / 4));
        return unitType, unitTileX, unitTileY;
    }
    else {
        return -1, -1, -1;
    }
}

getSelectedUnit returns -1, -1, -1 when no units are selected. We functionified the logic because we will be using it multiple times. It's always a good practice to make recurring logics to functions. Also we for future use we also read unitType from CUnit data.

Next, we need to modify afterTriggerExec a bit.

function afterTriggerExec() {
    const unitType, selX, selY = getSelectedUnit();
    if (selX != -1 && tile(selX, selY) != 0) {
        SetResources(AllPlayers, SetTo, selX, Ore);
        SetResources(AllPlayers, SetTo, selY, Gas);
        RemoveUnit("Zerg Scourge", P7);
        RemoveUnit("Zerg Scourge", P8);
        getPossiblePawnDestination(getTileOccupiedPlayer(selX, selY), selX, selY);
    }
    else {
        SetResources(AllPlayers, SetTo, 10000, OreAndGas);
    }
    SetInvincibility(Enable, "(any unit)", AllPlayers, 'Anywhere');
}

We check tile(selX, selY) != 0, because when a scourge is selected, there may be no piece under that scourge, and we don't want to show a movable position of that non-existing piece.

We also don't need to store unit indexes for each piece. Modify placePiece function a bit.

function placePiece(x, y, unitType, player) {
    move1x1Loc(x, y);
    CreateUnit(1, unitType, '1x1', player);
    setTile(x, y, unitType + player * 1000);
}

Also, remove some unused functions, and we're ready to go. The result looks the same as the previous chapter.

That's all for this chapter. See you next!