Steam Leaderboard Grouping

How in the world?

3 min read
13 views
Beginner

So in Backseat Drivers we wanted to group players together (since the game is coop) on the leaderboard. Well this isn't something that's supported out of the box by Valve so I had to do some hacky stuff.

Screenshot 2025-09-06 023609.png

How?

So in the Steam API UploadLeaderboardScore it accepts a pScoreDetails (const int32*). Do you see where I'm going with this yet?

I take each player's SteamID, split it into two int32 values (low/high), and store them in sequence:
[0] = P1.low, [1] = P1.high, [2] = P2.low, [3] = P2.high

Screenshot 2025-09-06 023529.png
In this screenshot you can see that it stores just fine.

I wrote some simple C++ to handle this.

#include "SteamIdUtils.h"
#include "Misc/Char.h"
#include "Misc/AssertionMacros.h"

bool USteamIdUtils::IsAllDigits(const FString& S)
{
    if (S.IsEmpty()) return false;
    for (TCHAR C : S)
    {
        if (!FChar::IsDigit(C)) return false;
    }
    return true;
}

void USteamIdUtils::PackU64(uint64 V, int32& Lo, int32& Hi)
{
    Lo = static_cast<int32>(V & 0xFFFFFFFFull);
    Hi = static_cast<int32>((V >> 32) & 0xFFFFFFFFull);
}

uint64 USteamIdUtils::UnpackU64(int32 Lo, int32 Hi)
{
    uint64 LoU = static_cast<uint64>(static_cast<uint32>(Lo));
    uint64 HiU = static_cast<uint64>(static_cast<uint32>(Hi));
    return (HiU << 32) | LoU;
}

void USteamIdUtils::SplitSteamIdString(const FString& SteamIdStr, int32& OutLo, int32& OutHi, bool& bOk)
{
    OutLo = OutHi = 0;
    bOk = false;

    // Expect a decimal SteamID64 string like "7656119..."
    if (!IsAllDigits(SteamIdStr))
    {
        return; // Not a pure decimal string (e.g., "[U:1:123]" or "STEAM_1:1:...") — convert that first.
    }

    // Parse base-10 -> uint64
    uint64 Id64 = FCString::Strtoui64(*SteamIdStr, nullptr, 10);
    // Basic sanity: if the string wasn't "0", but parsed to 0, treat as failure
    if (Id64 == 0 && SteamIdStr != TEXT("0"))
    {
        return;
    }

    PackU64(Id64, OutLo, OutHi);
    bOk = true;
}

FString USteamIdUtils::MakeSteamIdString(int32 Lo, int32 Hi)
{
    uint64 Id64 = UnpackU64(Lo, Hi);
    return LexToString(Id64); // decimal string
}

void USteamIdUtils::Int32ArrayToSteamIdStrings(
    const TArray<int32>& In,
    TArray<FString>& Out,
    int32 OffsetInts,
    int32 NumIds,
    bool bSkipZero)
{
    UE_LOG(LogTemp, Verbose,
        TEXT("[SteamIdUtils] Int32ArrayToSteamIdStrings: In.Num=%d, OffsetInts=%d, NumIds=%d, bSkipZero=%s"),
        In.Num(), OffsetInts, NumIds,
        bSkipZero ? TEXT("true") : TEXT("false"));

    Out.Reset();

    // Bounds checks
    if (OffsetInts < 0 || OffsetInts >= In.Num())
    {
        UE_LOG(LogTemp, Error, TEXT("[SteamIdUtils] OffsetInts (%d) out of bounds (In.Num=%d). Aborting."),
               OffsetInts, In.Num());
        return;
    }

    const int32 Remaining = In.Num() - OffsetInts;
    if (Remaining < 2)
    {
        UE_LOG(LogTemp, Verbose, TEXT("[SteamIdUtils] Not enough ints to form a single pair. Remaining=%d."), Remaining);
        return;
    }

    if ((Remaining % 2) != 0)
    {
        UE_LOG(LogTemp, Verbose, TEXT("[SteamIdUtils] Odd count (%d) after Offset. Last int will be ignored."), Remaining);
    }

    int32 MaxPairs = Remaining / 2;
    if (NumIds >= 0)
    {
        if (NumIds > MaxPairs)
        {
            UE_LOG(LogTemp, Verbose, TEXT("[SteamIdUtils] Requested NumIds=%d exceeds available pairs=%d. Clamping."),
                   NumIds, MaxPairs);
        }
        MaxPairs = FMath::Min(MaxPairs, NumIds);
    }

    Out.Reserve(MaxPairs);

    int32 i = OffsetInts;
    for (int32 p = 0; p < MaxPairs; ++p, i += 2)
    {
        const int32 Lo = In[i];
        const int32 Hi = In[i + 1];

        // Combine low and high parts into 64-bit Steam ID
        const uint64 Id64 = UnpackU64(Lo, Hi);

        UE_LOG(LogTemp, Verbose, TEXT("[SteamIdUtils] Pair #%d Lo=%d Hi=%d -> uint64=%llu"),
               p, Lo, Hi, (unsigned long long)Id64);

        if (bSkipZero && Id64 == 0)
        {
            UE_LOG(LogTemp, Verbose, TEXT("[SteamIdUtils] Pair #%d resolved to 0 (skipped)."), p);
            continue;
        }

        const FString IdStr = LexToString(Id64);
        Out.Emplace(IdStr);
    }

    UE_LOG(LogTemp, Verbose, TEXT("[SteamIdUtils] Int32ArrayToSteamIdStrings: Produced %d SteamIDs from %d pairs."), Out.Num(), MaxPairs);
}

It works. haha.

until next time