Skip to content

Tutorial: Zone IDs

cooltrain7 edited this page Feb 5, 2022 · 1 revision

Zone IDs in the Census API By Pocok5

Working with the Census stream API, you likely noticed the zone_id field in a number of events. Most of the time, these appear to be small numbers (<100) that directly map to a continent in the zone collection. However, in the case of certain continents, the numbers are seemingly random and in the millions.

This happens due to the zone ID actually being two numbers merged into a single one: each continent has an ID denoting the type - and another number identifying the instance ID. This is needed because some continents may have multiple identical but separate copies running at the same time. It is currently most often seen with multiple Sanctuaries in case of high population in them (usually when outfits gather for a large event) and multiple Desolation instances holding concurrent Outfit Wars matches.

The zone ID number you get from events in the stream API appears to be a 32-bit integer, a number consisting of 4 bytes. However, it is actually two 2-byte numbers (short) stuck together to form said 32 bit int. The right (lower) two bytes contain the definition ID, that is, the zone type. The left (high) bytes contain the instance counter.

This counter is always 0 in case of a non-instanced zone like Indar, leaving only the definition ID to affect the number in its 32-bit form. For an instanced continent, the left bytes count up (apparently) linearly, one by one in order of a new instance of said zone type opening.

This counter is server-specific and reset on server restarts - we are yet to see a large enough gap between them to roll over 65535 instances of Koltyr, so no comment on what happens if the counter hits the ceiling.

How to separate instance and definition ID

For those familiar with the concepts of bit masking and bit shifting, the procedure to extract the two separate numbers is simple:

  1. Apply a mask of 0x0000FFFF (bitwise AND) to the raw zone ID to get the definition ID (continent type).
  2. Apply a mask of 0xFFFF0000 (bitwise AND) to the raw zone ID, then right shift the result 16 bits to get the instance ID.

However, in case you haven't had to familiarize yourself with bitwise operations yet, here is a quick guide:

Bit masking

Bit masking is essentially taking a number and a fixed constant (the mask) and applying one of several bitwise operators to it: AND, OR, XOR, etc. In this case, we will only need bitwise AND operations.

Think of AND bit masking as taking a paper with a number written on it and overlaying another paper on it with cutouts placed such that only certain numbers of the underlying paper show through.

The bitwise AND operator takes each bit of two numbers and applies the logic AND function to them:

A B Result
0 1 0
1 0 0
1 1 1

In other words, in places where the mask has a 1, the result will match the original number's corresponding bit, and where the mask is 0, the result will also have a 0.

Let's see this in practice with the zone ID "1638414":

The binary form:

00000000000110010000000000001110

The bitmask to get the continent type is 0x0000FFFF - that is simply a number with all bits in the high 2 bytes zeros and the low bytes ones:

00000000000000001111111111111111

Let's apply a bitwise AND operation (changed bits are marked)

00000000000110010000000000001110 &  
00000000000000001111111111111111  
00000000000000000000000000001110
           ^^  ^

The operation leaves us with the binary number 1110 (and quite a number of leading zeros). That number is 14 in decimal - which is indeed Koltyr's ID.

Bit shifting

Getting the ID of the specific instance of Koltyr is done similarly, but with a mask of 0xFFFF0000, a mirror image of the previous one, which zeroes the lower bytes and preserves the high 2 bytes only:

00000000000110010000000000001110 &  
11111111111111110000000000000000  
00000000000110010000000000000000
                            ^^^ 

The instance ID is indeed left there in the high bytes - the number 11001, which is 25 in decimal. However we are not done yet: while we can easily see the new number, as far as the computer is concerned this is still a number in the millions. After all, it has quite a few zeroes after it on the right side! We need to re-align the number 16 (binary) places to the right to restore the correct value. Bit shifting does just that:

00000000000110010000000000000000 >>16
00000000000000000000000000011001

The number now correctly represents 25 in the variable and the result can be used normally.

Known zone definition IDs

For reasons unknown, the REST API's collection is actually incomplete. Below is a list of known zones and their definition IDs.

Def. ID Name
2 Indar
4 Hossin
6 Amerish
8 Esamir
14 Koltyr
96 VR Training (NC)
97 VR Training (TR)
98 VR Training (VS)
344 Oshur
361 Desolation
362 Sanctuary
364 Tutorial

Example C# code

The code below handles extracting both definition and instance ID from zones and can be cast to/constructed from an integer straight from the Census events.

    public readonly struct ZoneId
    {
        public ZoneId(uint combinedId)
        {
            CombinedId = combinedId;
        }
        public ZoneId(uint definitionId, uint instanceId)
        {
            CombinedId = (instanceId << 16) | definitionId;
        }
        public uint CombinedId { get;}
        public uint DefinitionId
        {
            get
            {
                return CombinedId & 0xFFFF;
            }
        }
        public uint InstanceId
        {
            get
            {
                return (CombinedId & 0xFFFF0000) >> 16;
            }
        }

        public string FriendlyName { get {

                return DefinitionId switch
                {
                    14 => "Koltyr",
                    2 => "Indar",
                    4 => "Hossin",
                    6 => "Amerish",
                    8 => "Esamir",
                    96 => "VR Training (NC)",
                    97 => "VR Training (TR)",
                    98 => "VR Training (VS)",
                    344 => "Oshur",
                    361 => "Desolation",
                    362 => "Sanctuary",
                    364 => "Tutorial",
                    _ => $"Unknown ({DefinitionId})"
                };
        } }

        public override string ToString()
        {
            return $"[Definition: {DefinitionId}/{FriendlyName}, Instance: {InstanceId}]";
        }
        public static explicit operator uint(ZoneId zoneId)=>zoneId.CombinedId;
        public static explicit operator ZoneId(uint zoneId) => new ZoneId(zoneId);
    }