Skip to content

05. Selecting a pawn

phu54321 edited this page Feb 10, 2018 · 10 revisions

Table of Contents generated with DocToc

The material used in this section can be downloaded on here.

Before moving on, I've fixed a minor issue on the chess board. Thanks for Oh_Man (@ Staredit.net) for pointing it out. White queen always stands on the white square, and the black queen always stands on the black square. I've flipped the board color accordingly.

Throughout the chapter 5-8, we will implement pawn moving. 'Moving pawn' consists of four stages.

  1. (Chapter 5) User selects a pawn
  2. (Chapter 6) Game displays where the pawn can go
  3. (Chapter 7, 8) The user selects the destination, and the pawn moves there.
  4. (Chapter 8) If necessary, delete the opponent piece on the destination.

See the TOC to see where the chapter 7 go.

User selects a pawn

When a user selects a pawn, our script should recognize it. For so we will implement a unit selection detection.

Unit selection basics - Pointer

You're now learning a pointer. This is one of the hardest concepts to learn in C programming language. Your reading speed may slow down, but that's normal.

Units are stored in memory space as an array. On EUDDB this is called 'Unitnode Table'. See EUDDB reference for more information.

Simply put, Unitnode table is an array of 336byte chunks. Every ingame unit occupies some 336byte space in there. There are 1700 unit chunks in unitnode chunk table. 0th unit's unitnode chunk takes address from 0x59CCA8 to 0x59CDF7 (0x59CDF7 - 0x59CCA8 + 1 = 336). 1st unit's unitnode chunk takes address from 0x59CDF8 to 0x59CF47 (0x59CF47 - 0x59CDF8 + 1 = 336). This continues until the 1699th unit. In total, 1700 units (0th-1699th) takes space from 0x59CCA8 to 0x6283E7.

StarCraft has several values having addresses as a value. We will use address 0x6284E8 for unit selection. This address holds a value, that is an address of starting address of some 336-byte chunk in the Unitnode Table.

Address 0x6284E8 holds some value between 0x59CCA8 and 0x628298. If the value of 0x6284E8 is 0x59CCA8, that means the 0th indexed unit is selected. If the value of 0x6284E8 is 0x617D68 (which is 0x59CCA8 + 336 * 1500), that means the 1501th indexed unit is selected. So If you read the value of 0x6284E8, then you can know what unit has been selected. You can know the selected unit's index number, and later we will use this index value to find the coordinate of the selected unit.

Like in this case, we call memory holding address as value pointer. In this case, Memory at 0x6284E8 is a pointer to the first selected unit of P1.

Getting selected unit's index

Address 0x6284E8 only applies to Player 1's selection. In fact, 'selected unit pointer' is also an array. 'Player x's selected unit' pointer is located at (0x6284E8 + 48 * x).

A player can select 12 units. Normally a pointer needs 4 bytes of memory. So a player needs 48 bytes of memory to hold a pointer to its selected units. This explains why the number 48 appeared. You don't need to understand or memorize this note to read the rest of this chapter.

In our chess, Player 1 plays and other human players just watches, so we just have to handle only Player 1's selections. We will ignore the player number for now.

To get the current selection, you can use dwread_epd_safe function. This function reads value from memory via EPD player. ::

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.

const selectedUnitPointer = dwread_epd_safe(EPD(0x6284E8));

EPD(x) means EPD player id of the address [x]. This line means

  • Read a value from 0x6284E8
  • Assign that to selectedUnitPointer

To get the index of the selected unit, we can use the following formula::

if (selectedUnitPointer != 0) {
    const selectedUnitIndex = (selectedUnitPointer - 0x59CCA8) / 336;
}

Quite simple. If no units are selected, then selectedUnitPointer becomes 0. In pointer 0 means 'No object address stored here'. if(A) { B; } means that B will be executed if condition A are met. In this case, if selectedUnitPointer != (is not equal to) 0, then the script calculates the Unitnode Table index of the unit pointer.

So now we have a selected unit index. Let's check out how much can we do with this snippet. Erase the old hello world code and paste this code instead.

function afterTriggerExec() {
    const selectedUnitPointer = dwread_epd_safe(EPD(0x6284E8));
    if (selectedUnitPointer != 0) {
        const selectedUnitIndex = (selectedUnitPointer - 0x59CCA8) / 336;
        SetResources(AllPlayers, SetTo, selectedUnitIndex, Ore);
    }
    else {
        SetResources(AllPlayers, SetTo, 10000, Ore);
    }
}

We won't care if the selected unit is pawn or not. We will postpone the piece type checks until we implement the knights. Let's just assume that every selectable piece is a pawn.

Everything under else happens when conditions in if are not met. So this code means

  • If a unit is selected, set all player's mineral to the selected unit's index.
  • If not, (selected no units), set all player's mineral to 10000.

So here's the result.

Getting the selected unit's unitnode index

Why 1682? StarCraft counts index in backward. First unit's index is 0, Second unit's index is 1699. Third unit's index is 1698, .... 1682 means that this is the 18th created unit. Ask blizzard why.

That already looks like a big jump.

Translating unit index to unit position

So how can we translate unit index to a unit position? There are two ways.

  • '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 this chapter, we will use the second way. The first way requires more cleverness. Here's a quick magic. This function returns the address to 'next generated unit'.

function getNextGeneratedUnitPtr() {
    return dwread_epd(EPD(0x628438));
}

This is an offset I've discovered. Just think it as a magic.

So, using this function we will modify our 'placePiece' function to crank in the unit index value in our in-memory board.

function placePiece(x, y, unitType, player) {
    move1x1Loc(x, y);
    const unitPtr = getNextGeneratedUnitPtr();
    const unitIndex = (unitPtr - 0x59CCA8) / 336;
    CreateUnit(1, unitType, '1x1', player);
    setTile(x, y, unitType + player * 1000 + unitIndex * 10000);
}

If you find player * 1000 + unitIndex * 10000 disgusting, I also feel so. We will clean this mess soon.

Now we have a unit index for each coordinate, so we can know what cell contains specific unit index by searching the in-memory board.

function getUnitPlace(index) {
    for(var y = 0 ; y < 8 ; y++) {
        for(var x = 0 ; x < 8 ; x++) {
            if(tile(x, y) / 10000 == index) {
                return x, y;
            }
        }
    }
}

Three new things. for, var, and return with multiple things. In epScript, a function can return a multiple values. That's all. for(var y = 0 ; y < 8 ; y++) means 'Repeat for y = 0, 1, 2, 3, 4, 5, 6, 7'. Same goes for x. So the above code means,

  • For all (x, y) in (0~7)*(0~7),
    • If unit index stored in in-memory board equals to the index given to function,
      • return the coordinate of the cell.

We can now use this function to get X and Y coordinate of the selected unit. Just modify afterTriggerExec function a bit.

    function afterTriggerExec() {
        const selectedUnitPointer = dwread_epd_safe(EPD(0x6284E8));
        if (selectedUnitPointer != 0) {
            const selectedUnitIndex = (selectedUnitPointer - 0x59CCA8) / 336;
            const x, y = getUnitPlace(selectedUnitIndex);
            SetResources(AllPlayers, SetTo, x, Ore);
            SetResources(AllPlayers, SetTo, y, Gas);
        }
        else {
            SetResources(AllPlayers, SetTo, 10000, OreAndGas);
        }
    }

Getting the selected unit's coordinate

Okay. This is cool. We got a coordinate of selected piece. That's all for this chapter. See you again in the next chapter!