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
-MaxGameIDtoMaxGameID - 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
ErrGameIDTooLargeifgameIDexceedsMaxGameID - Returns
ErrGameIDTooSmallifgameIDis 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.godefines the bit layout usinggameIDSlotBit(16) andzoneSlotBit(8) constants to partition a 64-bit integer into game and zone segmentsBuildUIDvalidates game ID bounds againstMaxGameIDand packs both identifiers using left-shift and bitwise OR operationsSplitUIDreverses the encoding using right-shift and masking withzoneMaskto 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →