Mind Invaders Games logoMIND INVADERSGAME STUDIO

Documentation overview / Seasons, Group Data, Scheduled Events & Matchmaking Polling

Seasons, Group Data, Scheduled Events & Matchmaking Polling

On this page

The four things almost every live game re-implements by hand on top of a PlayFab wrapper. They ship inside the existing managers — no new feature toggles, nothing to enable beyond the feature the API already belongs to.

I want to… Call Feature toggle
Show last season's leaderboard PlayFabHub.Leaderboards.GetPreviousSeasonLeaderboard(…) leaderboards
Store a club activity feed / shared goals PlayFabHub.Social.UpdateGroupData(…) social
Show "Double XP weekend" while it is live PlayFabHub.LiveOps.GetActiveEvents(…) liveOps + playerData
Search for a match with a Cancel button PlayFabHub.Multiplayer.FindMatch(…) multiplayer
Fail a CloudScript call on { "error": … } PlayFabHub.CloudScript.ExecuteChecked<T>(…) cloudScript
Read a typed remote-config blob PlayFabHub.Data.GetTitleDataObject<T>(…) playerData

1. Seasons — statistic versions

Every PlayFab statistic carries a monotonically increasing version. Rolling the version is the seasonal reset: the old board is archived (and stays readable forever) and every player's value restarts at zero.

Reading

var lb = PlayFabHub.Leaderboards;

// Which season are we in?
lb.GetCurrentStatisticVersion("Trophies", r =>
{
    if (r.Ok) Debug.Log($"Season {r.Data.Version}, started {r.Data.ActivationTime:u}");
});

// Last season's final top 25 — resolves the previous version, then reads it.
lb.GetPreviousSeasonLeaderboard("Trophies", 0, 25, r => { /* r.Data.Leaderboard */ });

// Or drive it yourself from the full history.
lb.GetStatisticVersions("Trophies", r =>
{
    var active   = LeaderboardManager.ActiveVersion(r.Data.StatisticVersions);
    var previous = LeaderboardManager.PreviousVersion(r.Data.StatisticVersions);
    lb.GetLeaderboardForVersion("Trophies", previous.Version, 0, 25, page => { … });
});

// The player's own archived placement.
lb.GetMyStatisticForVersion("Trophies", previousVersion, r => { /* r.Data is int? */ });

ActiveVersion / PreviousVersion / FindValue are pure statics — no session required, so your season-rollover logic is unit-testable.

Resetting (server-owned, by design)

IncrementPlayerStatisticVersion exists only on the Admin and Server APIs. A client can never roll a version itself — otherwise any player could wipe a leaderboard. Two supported paths:

  1. Scheduled — set the statistic's Reset frequency in Game Manager and let PlayFab roll it. Nothing to call; just read the new version.
  2. On demand — deploy a CloudScript/Azure handler that authorizes the caller and calls server.IncrementPlayerStatisticVersion, then name it in PlayFabHubSettings ▸ Leaderboards ▸ Version reset function key.
// With no function key configured this fails locally with that explanation,
// instead of pretending a client-side reset exists.
PlayFabHub.Leaderboards.IncrementStatisticVersion("Trophies", r => { … });

2. Group data — shared documents on a clan/guild

A PlayFab group is a first-class entity, so the Data API works on it exactly as it does on a player: one group owns a set of named JSON documents that every member reads and (subject to the group's entity policy) writes. That is the supported home for activity feeds, weekly goals, MOTDs and guild settings — no CloudScript needed for the read path.

var social = PlayFabHub.Social;

// Read one typed document — missing/first-run resolves to your fallback, Ok.
social.GetGroupObject<ClubGoals>(groupId, "Goals", r => Render(r.Data), new ClubGoals());

// Write one document.
social.SetGroupObject(groupId, "Goals", new ClubGoals { weeklyWins = 50 });

// Write several at once, with optimistic concurrency (the right pattern for an
// append-only activity feed: read ProfileVersion, append, write it back).
social.GetGroupData(groupId, read =>
{
    var feed = EntityObjects.Read(read.Data, "Feed", new List<FeedEntry>());
    feed.Add(newEntry);

    social.UpdateGroupData(groupId,
        new Dictionary<string, object> { { "Feed", feed } },
        write => { /* !Ok when another member wrote first — re-read and retry */ },
        expectedProfileVersion: read.Data.ProfileVersion);
});

social.DeleteGroupObject(groupId, "Goals");

Access control. Reads/writes are authorized by the caller's entity token against the group's entity policy. Verify the effective group roles and entity-object policy with admin, member and non-member accounts. If a write returns Forbidden, review that specific permission or route the operation through an authorized server handler; do not open writes to everyone. Protected grants belong on a trusted backend. See feature setup.

Also new on the Groups surface: UpdateGroup (rename / repoint roles), ChangeMemberRole (promote an officer), ListMyMembershipOpportunities (pending invites + applications) and AcceptGroupInvitation.


3. Scheduled events — a Title Data window

LiveOpsManager reads one Title Data key (default LiveEvents, configurable) and filters it against the clock. The package owns only the scheduling contract; everything game-specific rides along in payload.

// Title Data ▸ LiveEvents — an array…
[
  { "id": "xp_weekend", "type": "boost",
    "startUtc": "2026-08-01T00:00:00Z", "endUtc": "2026-08-03T23:59:59Z",
    "priority": 10, "payload": { "multiplier": 2 } }
]

// …or a map keyed by id (the key fills in a missing "id").
{ "xp_weekend": { "type": "boost", "endUtc": "2026-08-03T23:59:59Z" } }

Times are UTC: ISO-8601 (with or without Z) or a Unix epoch in seconds or milliseconds. An empty/missing bound means open ended — no startUtc is already running, no endUtc never expires.

PlayFabHub.LiveOps.GetActiveEvents(r =>
{
    foreach (var e in r.Data)             // highest priority first
    {
        var boost = e.GetPayload<BoostPayload>();
        ShowBanner(e.id, e.TimeUntilEnd(DateTime.UtcNow));
    }
});

PlayFabHub.LiveOps.GetUpcomingEvents(r => { /* the "coming soon" rail */ });
PlayFabHub.LiveOps.GetTopActiveEvent(r => { /* one banner, or null */ });
  • Values come from the Player Data manager's Title Data cache, so a screen that asks every frame costs one request per titleDataCacheSeconds. Enable the playerData feature alongside liveOps (the call fails with that message if you don't).
  • A missing key or malformed JSON resolves to an empty list with Ok (one logged warning) — a bad live-config push must never take the main menu down.
  • eventWindowGraceSeconds widens both ends of every window to absorb device clock skew.
  • ParseEvents / ActiveAt / UpcomingAt are pure statics — unit-test your event schedule without a session.

Server authority. An event window read here decides what the UI shows. It must never be what decides a reward is owed — re-check the window inside the CloudScript handler that grants anything, or a device with a wound-forward clock claims the event early.


4. Matchmaking — ticket polling with cancel

CreateMatchmakingTicket + GetMatchmakingTicket are the raw calls; every consumer then writes the same interval/timeout/cancel loop. FindMatch is that loop.

MatchmakingPoll poll;

void StartSearch()
{
    poll = PlayFabHub.Multiplayer.FindMatch(
        queueName: null,                                   // null = config default
        onComplete: r =>
        {
            if (r.Ok) EnterMatch(r.Data);                  // players + team assignments
            else      ShowMessage(r.Error.Message);        // timeout / cancel / service error
        },
        onStatusChanged: status => label.text = status);   // "WaitingForPlayers", …
}

void OnCancelButton()  => poll?.Cancel();
void OnDestroy()       => poll?.Cancel();
  • The poll ends with exactly one onComplete call — matched, timed out, cancelled, or failed — so a UI can drive its state machine from that single edge without leaking a spinner.
  • Polling runs on unscaled time, so it keeps ticking on a paused (Time.timeScale = 0) matchmaking screen.
  • The interval is clamped to MultiplayerManager.MinTicketPollIntervalSeconds (6s) — PlayFab allows at most 10 ticket polls per minute per ticket.
  • Cancel() also cancels the ticket server-side (cancelTicketOnStop), so an abandoned search stops matching other players into a lobby nobody joins.
  • poll.Cancel() is safe before the ticket even exists: cancelling mid-create cancels the ticket the moment it comes back.
  • Already own the ticket? Use PollMatchmakingTicket(ticketId, queue, …).

Without a runner (edit mode, no play mode) the ticket is read exactly once and the poll completes with a local error explaining that.


5. Handler envelopes & typed remote config

Two small conveniences that remove the same hand-written block from every service class.

// { "ok": true, … } / { "error": "insufficient funds" } handled once.
// Field names come from CloudScriptConfig.errorField / okField — clear them to
// opt out, or rename them to match a contract you already ship.
PlayFabHub.CloudScript.ExecuteChecked<ClaimResult>("claimDailyReward", null, r =>
{
    if (!r.Ok) ShowMessage(r.Error.Message);   // carries the handler's own message
});

// Typed, cached remote config. Missing key or bad JSON → your fallback, Ok.
PlayFabHub.Data.GetTitleDataObject("LevelCurve", r => Apply(r.Data), new LevelCurve());
PlayFabHub.Data.GetTitleDataValue("MotdText", r => { /* r.Data is the raw string */ });
PlayFabHub.Data.InvalidateTitleDataCache();    // after a live push / on resume

// Entity objects, typed both ways (player entity).
PlayFabHub.Data.GetObject("Loadout", r => Apply(r.Data), new Loadout());
PlayFabHub.Data.SetObject("Loadout", loadout, expectedProfileVersion: version);
PlayFabHub.Data.DeleteObject("Loadout");

HubJson (serialize / deserialize / map a loose PlayFab object graph) and EntityObjects (read a named object out of a GetObjectsResponse) are public — use them for your own payloads rather than re-deriving PlayFab's serializer. Every Try* overload logs one warning and returns false instead of throwing.

HubError.IsLocal distinguishes "you asked for something impossible" (bad argument, disabled feature, unconfigured knob — never hit the wire) from "the service said no".


Mind Invaders Games · PlayFab Hub 1.0.0 · Updated 14 September 2026