How fabrica-util's xid.BuildUID Combines Zone IDs for Cross-Zone Player Session Management

fabrica-util's xid.BuildUID function packs a player ID and zone ID into a single 64-bit integer using bitwise operations, enabling atomic cross-zone session tracking without separate database columns.

The go-pantheon/fabrica-util repository provides distributed game server utilities for managing player sessions across multiple zones. By encoding both the player identity and their current zone location into one immutable identifier, the system eliminates race conditions during zone transitions while simplifying database indexing and key lookups.

Bit Layout Constants in xid/id.go

The encoding scheme is defined in xid/id.go using three constants that establish the bit boundaries for packing:

const (
    gameIDSlotBit = 16                     // lower 16 bits reserved
    zoneSlotBit   = 8                      // zone occupies next 8 bits
    zoneMask      = (1 << zoneBit) - 1     // 0xFF, limits a zone to 0‑255
)

This layout allocates the 64-bit signed integer as follows:

  • Game ID: Occupies bits 16 through 63 (48 bits), supporting values from -MaxGameID to MaxGameID
  • Zone ID: Occupies bits 8 through 15 (8 bits), supporting 0–255 zones via zoneMask
  • Lower 8 bits: Reserved (bits 0–7)

The gameIDSlotBit constant (16) defines the left-shift amount for the player identifier, while zoneSlotBit (8) positions the zone identifier in the upper middle byte.

Composing IDs with BuildUID

The BuildUID function validates inputs before performing bitwise composition. It first checks that gameID fits within the allowable range to prevent overflow conditions:

  • Returns ErrGameIDTooLarge if gameID exceeds MaxGameID
  • Returns ErrGameIDTooSmall if gameID is less than -MaxGameID

Upon validation, the function constructs the packed identifier by left-shifting the game ID by gameIDSlotBit (16) and the zone ID by zoneSlotBit (8), then combining them with a bitwise OR:

return (gameID << gameIDSlotBit) | int64(zone)<<zoneSlotBit, nil

This operation places the zone ID in bits 8–15 and the game ID in bits 16–63, producing a single int64 value that uniquely identifies both the player and their current zone location.

Decoding Sessions with SplitUID

To retrieve the original components from a packed UID, xid/id.go provides the SplitUID function. This reverses the bit manipulation through right-shift operations and masking:

gameID = uid >> gameIDSlotBit
zone   = uint8(uid >> zoneSlotBit & zoneMask)

The game ID is recovered by shifting right 16 bits, while the zone ID requires a right shift of 8 bits followed by an AND operation with zoneMask (0xFF) to isolate the lower 8 bits of that segment.

Practical Implementation Examples

The following example demonstrates creating a combined UID for player 12345 entering zone 17:

package main

import (
    "fmt"
    "github.com/go-pantheon/fabrica-util/xid"
)

func main() {
    // Combine player #12345 with zone 17
    uid, err := xid.BuildUID(12345, 17)
    if err != nil {
        panic(err)
    }
    fmt.Printf("Combined UID: %d\n", uid)

    // Decode the packed value
    gameID, zone := xid.SplitUID(uid)
    fmt.Printf("GameID: %d, Zone: %d\n", gameID, zone)
}

Error handling ensures out-of-range values are caught before packing:

// Attempt to pack an invalid game ID
uid, err := xid.BuildUID(xid.MaxGameID+1, 0)
if err != nil {
    fmt.Println("Could not build UID:", err) // Outputs: ErrGameIDTooLarge
}

Summary

  • xid/id.go defines the bit layout using gameIDSlotBit (16) and zoneSlotBit (8) constants to partition a 64-bit integer into game and zone segments
  • BuildUID validates game ID bounds against MaxGameID and packs both identifiers using left-shift and bitwise OR operations
  • SplitUID reverses the encoding using right-shift and masking with zoneMask to recover the original components
  • The packed UID format supports 48-bit game IDs and 8-bit zone IDs (0–255), enabling atomic 64-bit operations for distributed session management without separate columns

Frequently Asked Questions

What happens if the gameID exceeds the allowed range in BuildUID?

The function performs bounds checking before bit manipulation. If the game ID is greater than MaxGameID, it returns ErrGameIDTooLarge; if less than -MaxGameID, it returns ErrGameIDTooSmall. These exported errors from xid/id.go allow calling code to handle validation failures explicitly without panicking.

How many zones can fabrica-util support per player session?

The zone ID occupies 8 bits as defined by zoneSlotBit and filtered through zoneMask (0xFF), supporting exactly 256 unique zones (0 through 255). This limit is hardcoded in the bit layout constants within xid/id.go.

Can SplitUID decode any 64-bit integer created by BuildUID?

Yes, SplitUID is the mathematical inverse of BuildUID. Any valid UID produced by the packing function can be decomposed back into its original game ID and zone components without data loss, assuming the UID was created using the same constant definitions from the fabrica-util source.

Why does fabrica-util use bit packing instead of separate database columns?

Packing both identifiers into a single 64-bit integer allows atomic comparison and indexing operations across distributed systems. This design prevents race conditions when moving players between zones because the combined ID represents an immutable snapshot of player location, eliminating the need for transactional updates across multiple database columns or tables.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →