Coding Range

Here's what you need to know to write good SteamID code

February 6th, 2013

One of the first data types every programmer learns is an integer. It’s a number. Cool. Then you get into different types, number of bits or bytes in an integer, and fun things that happen when you overflow (e.g. set a ‘byte’ field to 256 and see what happens).

It’s a pretty basic concept in most programming languages, the difference between byte, short, int, long and long long. And depending on your language and/or compiler, short, int and long can be of different lengths. So I find it quite surprising when people blindly refer to numbers in a particular format as the format itself. Mostly with Steam IDs.

A Steam ID uniquely identifies most things on Steam. A user, a gameserver, a group, a group chat and so on. They come in three major formats:

  1. STEAM_0:1:23456 is an older textual format. The ‘0’, which should really be a ‘1’ (and is 1 in more recent games) specifies the Universe of Steam. In all cases where you don’t work for Valve, this is Universe 1 (Public). The next section is the lowest bit of the user’s account ID, and the last section is your account ID bitshifted 1 to the right (or just divided by two).

  2. [U:1:46913] is the more modern textual format. The U signifies that the account is a User or Individiaul. The 1 is the Universe, and the last section is the user’s account ID.

  3. 76561197960312641 is the 64-bit integer representation and the more commonly used representation. This is used in almost all APIs, both Steamworks and Steam Web APIs.

All three SteamIDs above refer to the exact same account. (It’s not my SteamID, I just picked it for the numerical sequence in the first representation.) It’s not a mysterous format, it’s actually documented quite well in the Steamworks and Source SDK header files. (Well, not the textual bit):

// 64 bits total
union SteamID_t
{
    struct SteamIDComponent_t
    {
        uint32              m_unAccountID : 32;         // unique account identifier
        unsigned int        m_unAccountInstance : 20;   // dynamic instance ID (used for multiseat type accounts only)
        unsigned int        m_EAccountType : 4;         // type of account - can't show as EAccountType, due to signed / unsigned difference
        EUniverse           m_EUniverse : 8;    // universe this account belongs to
    } m_comp;

    uint64 m_unAll64Bits;
} m_steamid;

64 bits, made up of four components: the ID, the instance, the type and the universe. For a normal user, the following generally holds true:

  • The universe field is 1, k_EUniversePublic.
  • The account type field is also 1, k_EAccountTypeIndividual
  • The instance field is usually 1. This was 2 for users logged in from a PS3, but I’m not sure if that still holds or not.

    Assert( ! ( ( k_EAccountTypeIndividual == eAccountType ) && ( 1 != unAccountInstance ) ) ); // enforce that for individual accounts, instance is always 1

Because of these effective constants, people have come up with really crappy ways to convert Steam IDs between the 64-bit representation and just the account ID representation (as used in some APIs such as IDota2Match). Looking at this, it’s pretty obvious:

  • The account ID is the lowest 32 bits of the 64-bit ID.
  • The 64-bit ID is the account ID binary OR’ed with either 0x110000100000000 or 0b100010000000000000000000100000000000000000000000000000000

Unfortunately, bad code spreads like a disease. Especially security-related code. Some people have the right idea but only one-way, other use the magic number 76561197960265728 without understanding what it means, and some just don’t cover all cases.

So please, the next time you copy and paste some code from a forum, ask yourself what it is that the code is actually doing. Because I can guarantee that I can supply most developers with a valid Steam ID that will break their code.

And pretty please stop calling them “64-bit Steam IDs” and “32-bit Steam IDs”. The latter is just incorrect.