Arcademia.Leaderboards 1.2.0

dotnet add package Arcademia.Leaderboards --version 1.2.0
                    
NuGet\Install-Package Arcademia.Leaderboards -Version 1.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Arcademia.Leaderboards" Version="1.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Arcademia.Leaderboards" Version="1.2.0" />
                    
Directory.Packages.props
<PackageReference Include="Arcademia.Leaderboards" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Arcademia.Leaderboards --version 1.2.0
                    
#r "nuget: Arcademia.Leaderboards, 1.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Arcademia.Leaderboards@1.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Arcademia.Leaderboards&version=1.2.0
                    
Install as a Cake Addin
#tool nuget:?package=Arcademia.Leaderboards&version=1.2.0
                    
Install as a Cake Tool

Arcademia Leaderboards SDK (.NET)

Submit high scores from your game and let players claim them to their Arcademia account by scanning a QR code. Your code never has to care whether it's running on an arcade cabinet or on your own PC, it just works either way.

This is the plain .NET build of the SDK, for anything that isn't Unity: Godot (C# scripting), MonoGame, Stride, a custom engine, or any .NET tool. If you're building in Unity, use the Unity package instead, it has the identical API.

Full platform docs (including the raw HTTP API for non-.NET engines): https://manager.arcademia.ac/docs

Install

dotnet add package Arcademia.Leaderboards

Targets netstandard2.0, so it works on .NET Framework 4.6.1+, .NET 6/8/9+, and Mono.

Two modes, one API

Launcher (Live) Sandbox
When Game was started by the Arcademia launcher on an arcade machine Anywhere else: your dev machine, a build you're testing, CI
Auth Nothing you set. The launcher and the machine's credentials handle it Your game's API key (apiKey in config)
Scores land on The live, public leaderboard The Test area only. Visible to you in the dashboard, never public
Claiming Shows a QR code on the cabinet Gives you a link to open in your browser instead of a QR code

You don't choose the mode yourself. ArcademiaLeaderboards figures it out automatically by checking for environment variables the launcher sets on the game process before starting it. Write your gameplay code once and it behaves correctly in both places.

Quick start

using Arcademia.Leaderboards;

async void OnGameOver(long finalScore)
{
    var result = await ArcademiaLeaderboards.SubmitScoreAsync("highscore", finalScore);

    if (result.Success)
        Console.WriteLine($"Saved (#{result.Rank}), mode = {result.Mode}");
    else
        Console.WriteLine($"Score not saved: {result.Message}");
}

That covers a minimal integration. Everything below is optional.

Typed name or account

Once a score is submitted, the player can put a name on it in one of two ways. Offer them the choice and use whichever they pick:

  1. Type a name in your game. Pass it to SubmitScoreAsync, or call SetPlayerNameAsync afterwards if you submitted first.
  2. Save it to their Arcademia account. Call RequestClaimAsync. The player scans a QR code on the cabinet and signs in on their phone, and the call returns their account's username so your game can show it.

They don't need to do both. A claimed score always shows the account's username, so there's no point asking for a typed name as well.

var result = await ArcademiaLeaderboards.SubmitScoreAsync("highscore", finalScore);

if (playerChoseAccount)
{
    var claim = await ArcademiaLeaderboards.RequestClaimAsync(result.ScoreId);
    if (claim.Success)
    {
        ShowName(claim.PlayerName);
        return;
    }
}

var typed = await AskForNameInGame();
var named = await ArcademiaLeaderboards.SetPlayerNameAsync(result.ScoreId, typed);
ShowName(named.Success ? named.PlayerName : typed);

If the player cancels the QR code or it times out, the score is still there under the default name "Player", so you can fall back to your own name entry as the example does.

When you read scores back, each BoardScore tells you which kind of name it has. Claimed is true when the name is a verified Arcademia username, and false when it's a name someone typed in a game. You could use it to put a badge next to verified players, for example.

Configuration

The SDK needs an API base URL and an API key. Grab both from your game's Leaderboards tab in the Arcademia dashboard (request access, then generate a key, there's more detail in the dashboard itself). The key only matters in sandbox mode. On the machine it's ignored in favour of the launcher/machine credentials, so it's safe to ship inside your build (see Shipping the key below, and the full docs for the full security explanation).

Option A: arcademia.json (recommended, since you can change it without a rebuild)

Place an arcademia.json file next to your built executable (the SDK looks in AppContext.BaseDirectory):

{
  "apiBase": "https://manager.arcademia.ac",
  "apiKey": "arc_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}

This gets read automatically the first time you call anything on ArcademiaLeaderboards. You can leave apiBase out entirely to use the default, which is the live API. You'd only override it if you're pointing at a private or staging deployment. In Godot, set your export template to copy this file alongside the exported binary.

Option B: Configure() in code

ArcademiaLeaderboards.Configure(new ArcademiaSettings
{
    apiBase = "https://manager.arcademia.ac",
    apiKey  = "arc_xxxxxxxx_...",
});

Call this before anything else if you'd rather set the key at runtime, say from a settings menu, instead of shipping arcademia.json. Calling Configure on the machine is harmless too; the key just gets ignored there.

API reference

ArcademiaLeaderboards.Mode (returns ArcademiaMode)

Launcher or Sandbox. Read-only, set automatically on first use.

ArcademiaLeaderboards.SessionId (returns string)

The current play session id when running via the launcher, otherwise null. This is informational only, you never need to pass it yourself.

PingAsync() (returns Task<PingResult>)

A sanity check: confirms connectivity and, in sandbox mode, that the API key is valid. Worth calling once on startup.

var ping = await ArcademiaLeaderboards.PingAsync();
if (!ping.Success) Console.WriteLine(ping.Message);

SubmitScoreAsync(boardSlug, value, playerName = null, metadataJson = null, scoreId = null) (returns Task<ScoreResult>)

Submits a score to the named board.

  • boardSlug: from the dashboard, e.g. "highscore" or "time-trial".
  • value: a whole number (long). For time-based boards, submit milliseconds, the dashboard formats it back for display.
  • playerName: free text, any characters, up to 32. Server-side profanity filtering applies. Defaults to "Player" if you leave it out. Leave it out if the player might claim the score instead (see Typed name or account above).
  • metadataJson: an optional JSON object string (max 2 KB), e.g. "{\"level\":\"3-2\",\"character\":\"fox\"}". It's stored with the score and comes back as BoardScore.Metadata whenever your game reads scores, so you can show things like the level or character next to each entry. See Metadata below.
  • scoreId: normally you can leave this null and a Guid gets generated for you. Passing your own lets you safely retry a submission (say, after a network blip) without creating a duplicate, since the server deduplicates by this id.
var result = await ArcademiaLeaderboards.SubmitScoreAsync(
    "highscore", 15230, "REX", "{\"level\":\"3-2\"}");

result.Status is one of "submitted", "queued", "rejected", or "error". If the player's offline on the machine, the launcher queues the score and flushes it once connectivity returns, so result.Status will be "queued" rather than "submitted". Both count as success from your game's point of view, there's nothing extra to handle.

Hang on to result.ScoreId if you plan to offer a claim next.

SetPlayerNameAsync(scoreId, playerName) (returns Task<NameResult>)

Sets or changes the name on a score submitted earlier in the same play session. Use it when the player types their name after the score was submitted, or after a claim was cancelled or timed out.

var named = await ArcademiaLeaderboards.SetPlayerNameAsync(result.ScoreId, "MAL");
if (!named.Success) Console.WriteLine(named.Message);

The same name rules apply as for SubmitScoreAsync, and named.PlayerName is the name as it was saved. Status is "saved", "queued" (the score is still waiting to upload on a cabinet that's offline, and the new name will go with it), "rejected" or "error". Scores that have been claimed can't be renamed, since they already show the account's username.

Lets the player save a score to their Arcademia account instead of typing a name. On a cabinet, the launcher shows a QR code. The call won't return until the player scans it, cancels, or about five minutes pass, so call it from an async void handler rather than your main update loop.

var claim = await ArcademiaLeaderboards.RequestClaimAsync(result.ScoreId);
switch (claim.Status)
{
    case "saved":     ShowMessage($"Saved as {claim.PlayerName}!"); break;
    case "cancelled": ShowMessage("Cancelled.");                   break;
    case "expired":   ShowMessage("Timed out.");                   break;
    default:          ShowMessage("Couldn't save right now.");     break;
}

When the claim succeeds, claim.PlayerName is the player's Arcademia username. Show that in your game rather than asking for a name.

In sandbox mode there's no cabinet to show a QR code on, so you get a link instead. It's passed to onClaimLink and also written to the log. Open it in your browser, sign in, and save the score, and the call returns just like it would on a cabinet. You can pass a CancellationToken to give up early, which cancels the link.

var claim = await ArcademiaLeaderboards.RequestClaimAsync(
    result.ScoreId,
    url => Console.WriteLine("Claim it here: " + url));

GetScoresAsync(boardSlug, query = null) (returns Task<ScoresResult>)

Loads a leaderboard to show in your game. You decide what comes back: which group of players to rank against, which positions to include, and whether to include the current player's own position with the players either side of them.

There are four scopes (LeaderboardScope):

Scope Ranks against
Local Scores set on the cabinet the game is running on
Institutional Scores from every cabinet at the same institution
Country Scores from every cabinet in the same country
Global Every Arcademia cabinet

The cabinet, institution and country are worked out on the server from the play session, so there's nothing to pass in for them. The country comes from the site the cabinet belongs to.

Build a request with ScoreQuery:

var query = ScoreQuery.For(LeaderboardScope.Country)
    .Top(10)
    .AroundPlayer(result.ScoreId, before: 2, after: 2);

var board = await ArcademiaLeaderboards.GetScoresAsync("highscore", query);
if (board.Success)
{
    foreach (var s in board.Scores)
        Console.WriteLine($"#{s.Rank} {s.PlayerName} {s.Value}");

    if (board.Player != null)
        Console.WriteLine($"You are #{board.Player.Rank} of {board.Total}");
}
Method What it does
ScoreQuery.For(scope) Start a query for a scope. new ScoreQuery() defaults to Global.
.Top(n) Ranks 1 to n. Top(0) asks for no ranked list at all.
.Range(from, to) / .Rank(n) Add a block of positions or a single one. Chain as many as you like, e.g. .Top(3).Rank(10).Range(50, 55).
.WithRanks("1-3,10,50-55") Same thing written as a string. "none" means no ranked list.
.NoRanks() Skip the ranked list, handy when you only want the player's position.
.AroundPlayer(scoreId, before, after) Find the player's position using the ScoreId from SubmitScoreAsync, plus up to 50 scores either side.
.EveryScore() Rank every submission instead of each player's best.
.WithScope(scope) Copy the query with a different scope.

If you leave the ranks out, you get the top of the board up to its display cap (10 on a cabinet, 25 in sandbox, if no cap is set). A request can cover up to 200 positions.

ScoresResult gives you Scores (the positions you asked for, in rank order), Player (the player's own row, or null if you didn't pass a score id or it isn't in this scope), Around (the player and their neighbours, in rank order), Total (how many ranked entries the scope has), and Scope. Each BoardScore has Rank, PlayerName, Value, AchievedAt, Claimed, IsPlayer, MachineName, SiteName, Country and Metadata. Claimed is true when PlayerName is a verified Arcademia username, and false when it was typed in a game.

For a quick top ten there's a shorter overload:

var local = await ArcademiaLeaderboards.GetScoresAsync("highscore", LeaderboardScope.Local, 10);

GetScoresForScopesAsync(boardSlug, query = null, params scopes) (returns Task<Dictionary<LeaderboardScope, ScoresResult>>)

Runs the same query once per scope, which is what you want for a leaderboard screen with a tab per scope. Leave scopes empty to get all four.

var tabs = await ArcademiaLeaderboards.GetScoresForScopesAsync(
    "highscore",
    new ScoreQuery().Top(10).AroundPlayer(result.ScoreId, 1, 1));

ShowTab("This cabinet", tabs[LeaderboardScope.Local]);
ShowTab("Everyone",     tabs[LeaderboardScope.Global]);

Each scope is its own request and can fail on its own, so check Success on each result.

In sandbox mode these calls read your test scores. Test scores don't come from a cabinet, so every scope returns the same list, but the request and the result look exactly the same as they will on a cabinet.

Metadata

Anything you pass as metadataJson when submitting comes back on every score you read, as BoardScore.Metadata. It's the same JSON object as a string (the server may tidy up the spacing), or null if the score had none. Turn it back into your own type to use it:

class RunInfo { public string level { get; set; } public string character { get; set; } }

foreach (var s in board.Scores)
{
    var info = string.IsNullOrEmpty(s.Metadata) ? null : JsonSerializer.Deserialize<RunInfo>(s.Metadata);
    Console.WriteLine($"#{s.Rank} {s.PlayerName} {s.Value} on {info?.level}");
}

In "best per player" mode, each player's row is their best score, so its metadata comes from that run. Metadata is only returned to your game. It isn't shown on the public Arcademia leaderboard pages.

GetTestScoresAsync(boardSlug, limit = 25, offset = 0) (returns Task<TestScoresResult>)

Reads back scores from the sandbox test area, i.e. whatever you or another dev submitted while not on a cabinet. Only works in sandbox mode (it returns Success = false on the machine, since the concept doesn't apply there). Handy for a debug overlay while you're developing.

Shipping the key

Your compiled build, including an arcademia.json next to it if you're using one, is something a player (or a curious developer) can open up. That's expected, and it's safe. The API key only grants writes to the sandbox test area, it can never write to a live leaderboard. A live write has to come from an arcade machine with an open play session for your game, verified server-side, which is something a key alone can never fake, stolen or not. The full explanation, including exactly what a malicious actor can and can't do with a leaked key, is on the online docs' Security model page.

Godot notes

Godot's C# support runs on Mono/.NET, so this package works there like any other NuGet dependency: add it via your .csproj (Godot generates one for a C#-enabled project), and call it from any script the same way as the example above. SubmitScoreAsync/RequestClaimAsync are async Task methods, so await them from an async Godot callback (e.g. a signal handler) rather than blocking the main thread.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.2.0 52 9/24/2026
1.1.0 61 9/23/2026
1.0.0 57 9/22/2026