[Include] Map Zones - Let's end a decade of bad practices...
#1

SA-MP Map Zones



This library does not bring anything gamechanging to the table, it's created to stop a decade long era of bad practices regarding map zones. An array of ~350 zones dumped (or manually converted?) from the game has been around for such a long time, but in that time I've never seen a satisfactory API for them. Let's look at an implementation from Emmet_'s South Central Roleplay.

Код:
stock GetLocation(Float:fX, Float:fY, Float:fZ)
{
    enum e_ZoneData
    {
        e_ZoneName[32 char],
        Float:e_ZoneArea[6]
    };
    new const g_arrZoneData[][e_ZoneData] =
    {
        // ...
    };
    new
        name[32] = "San Andreas";

    for (new i = 0; i != sizeof(g_arrZoneData); i ++)
    {
        if (
            (fX >= g_arrZoneData[i][e_ZoneArea][0] && fX <= g_arrZoneData[i][e_ZoneArea][3]) &&
            (fY >= g_arrZoneData[i][e_ZoneArea][1] && fY <= g_arrZoneData[i][e_ZoneArea][4]) &&
            (fZ >= g_arrZoneData[i][e_ZoneArea][2] && fZ <= g_arrZoneData[i][e_ZoneArea][5]))
        {
            strunpack(name, g_arrZoneData[i][e_ZoneName]);

            break;
        }
    }
    return name;
}

stock GetPlayerLocation(playerid)
{
    new
        Float:fX,
        Float:fY,
        Float:fZ,
        string[32],
        id = -1;

    if ((id = House_Inside(playerid)) != -1)
    {
        fX = HouseData[id][housePos][0];
        fY = HouseData[id][housePos][1];
        fZ = HouseData[id][housePos][2];
    }
    // ...
    else GetPlayerPos(playerid, fX, fY, fZ);

    format(string, 32, GetLocation(fX, fY, fZ));
    return string;
}


If you didn't get the reference, you should probably check out this repository. GetPlayerLocation most likely uses format to prevent this bug from occurring, but the risk is still there and arrays should never be returned in PAWN. Let's take a look at another implementation that even I used a long time ago.

Код:
stock GetPointZone(Float:x, Float:y, Float:z, zone[] = "San Andreas", len = sizeof(zone))
{
    for (new i, j = sizeof(Zones); i < j; i++)
    {
        if (x >= Zones[i][zArea][0] && x <= Zones[i][zArea][3] && y >= Zones[i][zArea][1] && y <= Zones[i][zArea][4] && z >= Zones[i][zArea][2] && z <= Zones[i][zArea][5])
        {
            strunpack(zone, Zones[i][zName], len);
            return 1;
        }
    }
    return 1;
}

stock GetPlayerZone(playerid, zone[], len = sizeof(zone))
{
    new Float:pos[3];
    GetPlayerPos(playerid, pos[0], pos[1], pos[2]);

    for (new i, j = sizeof(Zones); i < j; i++)
    {
        if (x >= Zones[i][zArea][0] && x <= Zones[i][zArea][3] && y >= Zones[i][zArea][1] && y <= Zones[i][zArea][4] && z >= Zones[i][zArea][2] && z <= Zones[i][zArea][5])
        {
            strunpack(zone, Zones[i][zName], len);
            return 1;
        }
    }
    return 1;
}
First of all, what do we see? A lot of code repetition. That's easy to fix in this case, but what if we also needed either the min/max position of the zone? We'd have to loop through the zones again or take a different approach. Which approach does this library take? Functions like GetMapZoneAtPoint and GetPlayerMapZone do not return the name of the zone, they return an identificator of it. The name or positions of the zone must be fetched using another function. In addition to that, I rebuilt the array of zones myself since the one used basically everywhere seems to be faulty according to this post.

Installation

Simply install to your project:

Код:
sampctl package install kristoisberg/samp-map-zones
Include in your code and begin using the library:

Код:
#include <map-zones>
Usage

Constants
  • INVALID_MAP_ZONE_ID = MapZone:-1
    • The return value of several functions when no map zone was matching the
      criteria.
  • MAX_MAP_ZONE_NAME = 27
    • The length of the longest map zone name including the null character.
  • MAX_MAP_ZONE_AREAS = 13
    • The most areas associated with a map zone.
Functions
  • MapZone:GetMapZoneAtPoint(Float:x, Float:y, Float:z)
    • Returns the ID of the map zone the point is in or INVALID_MAP_ZONE_ID if
      it isn't in any. Alias: GetMapZoneAtPoint3D.
  • MapZone:GetPlayerMapZone(playerid)
    • Returns the ID of the map zone the player is in or INVALID_MAP_ZONE_ID if
      it isn't in any. Alias: GetPlayerMapZone3D.
  • MapZone:GetVehicleMapZone(vehicleid)
    • Returns the ID of the map zone the vehicle is in or INVALID_MAP_ZONE_ID if
      it isn't in any. Alias: GetVehicleMapZone3D.
  • MapZone:GetMapZoneAtPoint2D(Float:x, Float:y)
    • Returns the ID of the map zone the point is in or INVALID_MAP_ZONE_ID if
      it isn't in any. Does not check the Z-coordinate.
  • MapZone:GetPlayerMapZone2D(playerid)
    • Returns the ID of the map zone the player is in or INVALID_MAP_ZONE_ID if
      it isn't in any. Does not check the Z-coordinate.
  • MapZone:GetVehicleMapZone2D(vehicleid)
    • Returns the ID of the map zone the vehicle is in or INVALID_MAP_ZONE_ID if
      it isn't in any. Does not check the Z-coordinate.
  • bool:IsValidMapZone(MapZone:id)
    • Returns true or false depending on if the map zone is valid or not.
  • bool:GetMapZoneName(MapZone:id, name[], size = sizeof(name))
    • Retrieves the name of the map zone. Returns true or false depending on
      if the map zone is valid or not.
  • bool:GetMapZoneSoundID(MapZone:id, &soundid)
    • Retrieves the sound ID of the map zone. Returns true or false depending
      on if the map zone is valid or not.
  • bool:GetMapZoneAreaCount(MapZone:id, &count)
    • Retrieves the count of areas associated with the map zone. Returns true or
      false depending on if the map zone is valid or not.
  • GetMapZoneAreaPos(MapZone:id, &Float:minX = 0.0, &Float:minY = 0.0, &Float:minZ = 0.0, &Float:maxX = 0.0, &Float:maxY = 0.0, &Float:maxZ = 0.0, start = 0)
    • Retrieves the coordinates of an area associated with the map zone. Returns
      the array index for the area or -1 if none were found. See the usage in
      in the examples section.
  • GetMapZoneCount()
    • Returns the count of map zones in the array. Could be used for iteration
      purposes.
Examples

Retrieving the location of a player

Код:
CMD:whereami(playerid) {
    new MapZone:zone = GetPlayerMapZone(playerid);

    if (zone == INVALID_MAP_ZONE_ID) {
        return SendClientMessage(playerid, 0xFFFFFFFF, "probably in the ocean, mate");
    }

    new name[MAX_MAP_ZONE_NAME], soundid;
    GetMapZoneName(zone, name);
    GetMapZoneSoundID(zone, soundid);

    new string[128];
    format(string, sizeof(string), "you are in %s", name);

    SendClientMessage(playerid, 0xFFFFFFFF, string);
    PlayerPlaySound(playerid, soundid, 0.0, 0.0, 0.0);
    return 1;
}
Iterating through areas associated with a map zone

Код:
new zone = ZONE_RICHMAN, index = -1, Float:minX, Float:minY, Float:minZ, Float:maxX, Float:maxY, Float:maxZ;

while ((index = GetMapZoneAreaPos(zone, minX, minY, minZ, maxX, maxY, maxZ, index + 1) != -1) {
    printf("%f %f %f %f %f %f", minX, minY, minZ, maxX, maxY, maxZ);
}
Extending

Код:
stock MapZone:GetPlayerOutsideMapZone(playerid) {
    new House:houseid = GetPlayerHouseID(playerid), Float:x, Float:y, Float:z;

    if (houseid != INVALID_HOUSE_ID) { // if the player is inside a house, get the exterior location of the house
        GetHouseExteriorPos(houseid, x, y, z);
    } else if (!GetPlayerPos(playerid, x, y, z)) { // the player isn't connected, presuming that GetPlayerHouseID returns INVALID_HOUSE_ID in that case 
        return INVALID_MAP_ZONE_ID;
    }

    return GetMapZoneAtPoint(x, y, z);
}
Testing

To test, simply run the package:

Код:
sampctl package run
Reply
#2

Quote:
Originally Posted by ******
Посмотреть сообщение
“stop bad practices”
  • Linear search.
  • Rectangular zones with multiple IDs for the same zone, instead of polygons.
  • IsPlayerConnected and IsValidVehicle instead of GetXPos return values.
  • Multiple copies of the same string literals.
  • No backwards-compatibility with the default compiler.
  • No const data.
  • No brackets on macro replacements.
  • Memes in the release topic.
I’m wondering which bad practices you DID stop?

Also, I’m not quite sure how you managed to miss the good solutions that have existed for a decade already…
Quote:
Originally Posted by ******
Посмотреть сообщение
Actually constructive critisism:
  • Put the locations and names in separate arrays, that way you avoid the massive duplication of strings, and their index will match the zone ID. I know people seem to prefer enums over multiple arrays (I’ve repeatedly seen comments to the effect of “use enums not two arrays”) but there are actually advantages to using multiple arrays instead.
  • Could be a good idea (since the IDs are now constants) to provide defines, then you can do things like:
if (GetPlayerZone(playerid) == ZONE_GROVE_STREET)

  • Searching rectangles is, admittedly, vastly simpler than searching polygons, but this can be solved with stored zone IDs, so your array data looks like:
{ZONE_OCEAN_FLATS, -2994.489990, 277.411010, -0.000091, -2867.850097, 458.411010, 200.000000},
{ZONE_OCEAN_FLATS, -2994.489990, -222.589004, -0.000106, -2593.439941, 277.411010, 200.000000},
{ZONE_OCEAN_FLATS, -2994.489990, -430.276000, -0.000122, -2831.889892, -222.589004, 200.000000},


Then you return the stored ID, not the array index, and multiple zones end up the same. For boxy shapes like this this is probably the best method TBH, even better than polygons if you could support them.
  • Much harder is getting rid of the linear search, but it can be done with careful arrangements of the data. Zones can be sorted in the x- and y- axes, making finding the zone given a co-ordinate almost O(1).
Thanks for the criticism and ideas. I've already taken care of the minor stuff, I'll release a new version in the near future, restructuring the data will take some time though. I'll also try to include sound IDs for the zone names, but the SFX files are a lot harder to parse than everything else I've parsed so far, so I might have to do that manually.

I've already addressed this in the Discord, but just in case I'll mention this here as well that I considered using polygons instead of rectangles for this (I should even have the required code in one of my older repos), but since some of the areas with same names have different Z-coordinates, it doesn't suit for this usecase since I'd like to keep the data the same as it was in the game files.
Reply
#3

This library has definitely become more than I ever expected it to be. Some major changes I made:
  • The data is now separated into two datasets. The first one consists of the names and sound IDs of the zones and the other one consists of coordinates of the areas associated with zones.
  • The areas are now stored by the minimum X-coordinate and GetMapZoneAtPoint now uses binary search to find the start point for the loop, decreasing the lookup times. Thanks to ****** for pointing out some mistakes I made when implementing binary search and giving me ideas for potential improvements in the future.
  • Added GetMapZoneSoundID function. Some (fewer than ten) zones are missing a sound ID in the game, I used the sound IDs of surrounding zones for them, for example “Robada Intersection” is referred to as “Tierra Robada”, “Pershing Square” as “Commerce”, etc. Thanks to spacemud for providing me an array of most sound IDs linked to area names, speeding up my work by at least an hour.
  • A constant is available for each zone, e.g. ZONE_ANGEL_PINE and ZONE_COMMERCE. The prefix is ZONE_ instead of MAP_ZONE_ so none of the zone names would need to be abbreviated.
  • Some changes to the API regarding coordinates. The readme (first post) is updated, further details are available there.
Reply
#4

A smaller update:
  • The amount of areas associated with each map zone is now saved in the array, improving the performance of GetMapZoneAreaCount, which looped through all areas before.
  • New constant MAX_MAP_ZONE_AREAS, which marks the maximum amount of areas associated with a map zone. Also, non-internal constants are now all documented in the readme.
  • Updated how GetMapZoneAreaPos works and has to be used. The example in the readme has been updated. The function essentially only has to loop through zones once instead of up to MAX_MAP_ZONE_AREAS times as before.
  • Added 2D variants for the zone-finding functions. 3D variants now have aliases with 3D suffixes.
Reply
#5

I reall hope the guys see the good work you have here.

And with the ****** critics it will better!
Reply
#6

Wow a nice include keep it up
Reply


Forum Jump:


Users browsing this thread: 1 Guest(s)