Skip to content
Margen67 edited this page Apr 21, 2021 · 3 revisions

Vanilla RCT2 uses game commands to perform changes to the state of the game. It is guessed that the structure of the game commands was to allow for multiplayer that was never implemented. Early in OpenRCT2 development the game commands were integrated into a new network system to allow for multiplayer. The design of the game commands has changed little since then.

Game commands are called with the game_do_command function. The function handles passing parameters to the correct actual game command function and clean up after calling the command. game_do_command parameters are all 7 32bit general purpose registers of the cpu and the actual command (which is normally in the esi register). There is also an overload of the function that doesn't use pointers.

uint32_t command, int32_t* eax, int32_t* ebx, int32_t* ecx, int32_t* edx, int32_t* esi, int32_t* edi, int32_t* ebp
  • eax is generally used for x coordinates
  • ebx the lowest 8 bits are GAME_COMMAND_FLAG's. Which control many features of the command.
  • ecx is generally used for y coordinates
  • edx is sometimes used for z coordinates
  • esi is generally used for the game command type but in one or two very rare cases it is used as a general purpose parameter.
  • edi general purpose param
  • ebp general purpose param

Each parameter is 32 bit and at times all 32 bits are in use. It is very common for multiple parameters to be squeezed into the 32bit int.

Below is a typical call to the function.

game_do_command(x, flags, y, z | (gSceneryPlaceRotation << 8), GAME_COMMAND_REMOVE_BANNER, 0, 0);

Note that z and rotation have been stuffed into edx. The command it will call is GAME_COMMAND_REMOVE_BANNER. Unused parameters have been set to zero.

The return value of game_do_command is the value in ebx. You can also receive the values in all of the other params by passing pointers to the game_do_command function. This is generally only used in very special situations.

Failure of a game command is indicated by returning MONEY32_UNDEFINED. Further detail on a failure can be found by querying gGameCommandErrorText, gGameCommandErrorTitle and the common string args global.

All game commands have at least two modes indicated by the state of the flags parameter in the lowest 8 bits of ebx. The first mode is Query the second is Execute. When GAME_COMMAND_FLAG_APPLY is set it calls the Execute otherwise it calls the Query. When you call game_do_command with APPLY the correct command will be called without the APPLY flag this is to first Query if its possible before actually Executing the command.

A typical command:

/**
 *
 *  rct2: 0x00663CCD
 */
void game_command_change_surface_style(
    int32_t* eax, int32_t* ebx, int32_t* ecx, int32_t* edx, [[maybe_unused]] int32_t* esi, int32_t* edi, int32_t* ebp)
{
    *ebx = map_change_surface_style(
        (int16_t)(*eax & 0xFFFF), (int16_t)(*ecx & 0xFFFF), (int16_t)(*edi & 0xFFFF), (int16_t)(*ebp & 0xFFFF), *edx & 0xFF,
        (*edx & 0xFF00) >> 8, *ebx & 0xFF);
}

This splits the information out of the unnamed parameters and passes it to a more appropriate function. Note that it saves the result into ebx. This is required so that it can indicate failure.

/**
 *
 *  rct2: 0x00663CCD
 */
static money32 map_change_surface_style(
    int32_t x0, int32_t y0, int32_t x1, int32_t y1, uint8_t surfaceStyle, uint8_t edgeStyle, uint8_t flags)
{
    gCommandExpenditureType = RCT_EXPENDITURE_TYPE_LANDSCAPING;

    x0 = std::max(x0, 32);
    y0 = std::max(y0, 32);
    x1 = std::min(x1, (int32_t)gMapSizeMaxXY);
    y1 = std::min(y1, (int32_t)gMapSizeMaxXY);

    int32_t xMid, yMid;

    xMid = (x0 + x1) / 2 + 16;
    yMid = (y0 + y1) / 2 + 16;

    int32_t heightMid = tile_element_height(xMid, yMid);

    gCommandPosition.x = xMid;
    gCommandPosition.y = yMid;
    gCommandPosition.z = heightMid;

    // Do nothing during pause
    if (game_is_paused() && !gCheatsBuildInPauseMode)
    {
        return 0;
    }
    ...continued below

This first part of the function is setting where the command took place and returning early if pause is pressed. Note how this function is incorrect and does not return MONEY32_UNDEFINED on failure. This is a mistake.

...continued
    // Do nothing if not in editor, sandbox mode or landscaping is forbidden
    if (!(gScreenFlags & SCREEN_FLAGS_SCENARIO_EDITOR) && !gCheatsSandboxMode
        && (gParkFlags & PARK_FLAGS_FORBID_LANDSCAPE_CHANGES))
    {
        return 0;
    }
...continued below

Again another mistake not returning correctly. This was maybe a bad example to choose!

    money32 surfaceCost = 0;
    money32 edgeCost = 0;
    for (int32_t x = x0; x <= x1; x += 32)
    {
        for (int32_t y = y0; y <= y1; y += 32)
        {
            if (x > 0x1FFF)
                continue;
            if (y > 0x1FFF)
                continue;

            if (!(gScreenFlags & SCREEN_FLAGS_SCENARIO_EDITOR) && !gCheatsSandboxMode)
            {
                if (!map_is_location_in_park({ x, y }))
                    continue;
            }

            auto surfaceElement = map_get_surface_element_at({ x, y })->AsSurface();
            if (surfaceElement == nullptr)
            {
                continue;
            }

            if (surfaceStyle != 0xFF)
            {
                uint8_t cur_terrain = surfaceElement->GetSurfaceStyle();

                if (surfaceStyle != cur_terrain)
                {
                    // Prevent network-originated value of surfaceStyle from causing
                    // invalid access.
                    uint8_t style = surfaceStyle & 0x1F;
                    auto& objManager = GetContext()->GetObjectManager();
                    const auto surfaceObj = static_cast<TerrainSurfaceObject*>(
                        objManager.GetLoadedObject(OBJECT_TYPE_TERRAIN_SURFACE, style));
                    if (surfaceObj != nullptr)
                    {
                        surfaceCost += surfaceObj->Price;
                    }

                    if (flags & GAME_COMMAND_FLAG_APPLY)
                    {

This is the part of the function that only happens during an execute. Note how no state is modified outside of this if.

                        surfaceElement->SetSurfaceStyle(surfaceStyle);

                        map_invalidate_tile_full(x, y);
                        footpath_remove_litter(x, y, tile_element_height(x, y));
                    }
                }
            }

            if (edgeStyle != 0xFF)
            {
                uint8_t currentEdge = surfaceElement->GetEdgeStyle();

                if (edgeStyle != currentEdge)
                {
                    edgeCost += 100;

                    if (flags & GAME_COMMAND_FLAG_APPLY)
                    {
                        surfaceElement->SetEdgeStyle(edgeStyle);
                        map_invalidate_tile_full(x, y);
                    }
                }
            }

            if (flags & GAME_COMMAND_FLAG_APPLY)
            {
                if (surfaceElement->CanGrassGrow() && (surfaceElement->GetGrassLength() & 7) != GRASS_LENGTH_CLEAR_0)
                {
                    surfaceElement->SetGrassLength(GRASS_LENGTH_CLEAR_0);
                    map_invalidate_tile_full(x, y);
                }
            }
        }
    }

The query section has iterated over every element to check to see if it could change it. The cost is still calculated but the actual change only happens during the Execute.

    if (flags & GAME_COMMAND_FLAG_APPLY && gGameCommandNestLevel == 1)
    {
        LocationXYZ16 coord;
        coord.x = ((x0 + x1) / 2) + 16;
        coord.y = ((y0 + y1) / 2) + 16;
        coord.z = tile_element_height(coord.x, coord.y);
        network_set_player_last_action_coord(network_get_player_index(game_command_playerid), coord);
    }

    return (gParkFlags & PARK_FLAGS_NO_MONEY) ? 0 : surfaceCost + edgeCost;
}

The final section of the function saves another location of where the function happened and then returns the cost of the action. Note how it returns zero if no money is on.

The flags make these functions hard to follow and passing parameters in register names is not intuitive. The lack of types mean failure is being passed in the same parameter as cost. There are also lots of repetition. Why for example does every function need to have NO_MONEY checks. It should all be handled centrally so that it is not forgotten. For that reason we created the new Game Actions framework.

Clone this wiki locally