Satori Java Client Guide #

This client library guide will show you how to use the core Satori features in Java.

Prerequisites #

Before proceeding ensure that you have:

The Satori client is packaged as part of the Nakama Java SDK, but using Nakama is not required.

Full API documentation #

For the full API documentation please visit the API docs.

Installation #

The client is available from the Heroic Labs GitHub releases page.

There are a few different options for installing the Nakama Java SDK in your project depending on whether you’re using Gradle, Maven or relying on the JAR package directly.

Installing with Gradle #

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
repositories {
    maven {
        url 'https://jitpack.io'
    }
}

dependencies {
    implementation 'com.github.heroiclabs.nakama-java:nakama-java:<commit>'
    implementation 'com.github.heroiclabs.nakama-java:satori-java:<commit>'


 // or, depend on the fat Jar which bundles all of the Nakama Java dependencies into a single Jar.
 // implementation 'com.github.heroiclabs.nakama-java:nakama-java-all:<commit>'
 // implementation 'com.github.heroiclabs.nakama-java:satori-java-all:<commit>'
}

Installing with Maven #

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
  <repositories>
    <repository>
      <id>jitpack.io</id>
      <url>https://jitpack.io</url>
    </repository>
  </repositories>

  ...
  <dependencies>
    <dependency>
      <groupId>com.github.heroiclabs.nakama-java</groupId>
      <artifactId>nakama-java</artifactId>
      <version>_commit_</version>
      <type>jar</type>
    </dependency>
  </dependencies>

Or, if you would like to depend on a fat JAR with Maven:

1
2
3
4
5
6
7
8
  <dependencies>
    <dependency>
      <groupId>com.github.heroiclabs.nakama-java</groupId>
      <artifactId>nakama-java-all</artifactId>
      <version>_commit_</version>
      <type>jar</type>
    </dependency>
  </dependencies>

Installing with JAR #

1
2
3
4
5
6
7
8
  <dependencies>
    <dependency>
      <groupId>com.github.heroiclabs.nakama-java</groupId>
      <artifactId>nakama-java-all</artifactId>
      <version>_commit_</version>
      <type>jar</type>
    </dependency>
  </dependencies>

Updates #

To update to a newer version of the Nakama Java SDK, replace the version/commit reference in your Gradle/Maven configuration file.

Logging #

The Nakama Java SDK uses the SLF4J logging API. You can find more information on how to use this API and how to use different logging bindings by reading the SLF4J User Manual.

All examples in this guide use an SLF4J Logger which can be created as follows:

1
2
// Where App is the name of your class
Logger logger = LoggerFactory.getLogger(App.class);

For Android #

Android uses a permissions system which determines which platform services the application will request to use and ask permission for from the user. The client uses the network to communicate with the server so you must add the “INTERNET” permission.

1
<uses-permission android:name="android.permission.INTERNET"/>

Asynchronous programming #

Many of the Nakama APIs are asynchronous and non-blocking and are available in the Java SDK using ListenableFuture objects which are part of the Google Guava library.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Create a single thread executor
ExecutorService executor = Executors.newSingleThreadExecutor();

// Get a ListenableFuture with a Session result
ListenableFuture<Session> authFuture = client.authenticateDevice(deviceId);

// Setup the success and failure callbacks, specifying which executor to use
Futures.addCallback(authFuture, new FutureCallback<Session>() {
    @Override
    public void onSuccess(@NullableDecl Session session) {
        logger.debug("Authenticated user id: " + session.getUserId());
        executor.shutdown();
    }

    @Override
    public void onFailure(Throwable throwable) {
        logger.error(throwable.getMessage());
        executor.shutdown();
    }
}, executor);

// Wait for the executor to finish all tasks
try {
    executor.awaitTermination(5, TimeUnit.SECONDS);
}
catch (InterruptedException e) {
    logger.error(e.getMessage());
}

If you wish to chain asynchronous calls together, you can do so using AsyncFunction<> objects and the Futures.transformAsync function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Get a ListenableFuture with a Session result
ListenableFuture<Session> authFuture = client.authenticateDevice(deviceId);

// Create an AsyncFunction to get the Account of a user using a Session object
AsyncFunction<Session, Account> accountFunction = session -> client.getAccount(session);

// Get a ListenableFuture from Futures.transformAsync by first passing the original Future, followed by the extended Future and finally an exector
ListenableFuture<Account> accountFuture = Futures.transformAsync(authFuture, accountFunction, executor);

// Setup the success and failture callbacks as usual
Futures.addCallback(accountFuture, new FutureCallback<>() {
    @Override
    public void onSuccess(@NullableDecl Account account) {
        logger.debug("Got account for user id: " + account.getUser().getId());
        executor.shutdown();
    }

    @Override
    public void onFailure(Throwable throwable) {
        logger.error(throwable.getMessage());
        executor.shutdown();
    }
}, executor);

For brevity, the code samples in this guide will use the simpler but thread blocking .get() function instead.

1
Flag flag = client.getFlag(session, "FlagName").get();

Handling exceptions #

Network programming requires additional safeguarding against connection and payload issues.

As shown above, API calls use a callback pattern with both a success and a failure callback being provided. If an API call throws an exception it is handled in the onFailure callback and the exception details can be accessed in the throwable object.

Serializing and deserializing data #

When sending and receiving data across the network it will need to be serialized and deserialized appropriately. The two most common ways to do this are using JSON and Binary data.

Both examples will show how to serialize and deserialize the Map object below but can be used with any serializable object.

1
2
3
Map<String, String> data = new HashMap<>();
data.put("Key", "Value");
data.put("AnotherKey", "AnotherValue");

JSON #

Using the com.google.gson.Gson package.

1
2
3
4
5
// Serialize
String serialized = new Gson().toJson(data, data.getClass());

// Deserialize
Map<String, String> deserialized = new Gson().fromJson(serialized, new TypeToken<Map<String, String>>(){}.getType());

Binary #

Using the java.io.* package. Conversion to and from Base64 is only necessary if you wish to send and receive the serialized data as a String; otherwise you can serialize and deserialize using a byte[] array.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Serialize
String serialized = null;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

try {
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
    objectOutputStream.writeObject(data);
    byte[] bytes = byteArrayOutputStream.toByteArray();
    serialized = Base64.getEncoder().encodeToString(bytes);
} catch (IOException e) {
    e.printStackTrace();
}


// Deserialize
byte[] bytes = Base64.getDecoder().decode(serialized);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);

try {
    ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
    Map<String, String> deserialized = (Map<String, String>) objectInputStream.readObject();
} catch (IOException e) {
    e.printStackTrace();
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

Getting started #

Learn how to get started using the Satori Client to manage your live game.

Satori client #

The Satori Client connects to a Satori Server and is the entry point to access Satori features. It is recommended to have one client per server per game.

To create a client pass in your server connection details:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
String apiKey = "apiKey";
String host = "127.0.0.1";
int port = 7450;
boolean useSsl = true;

// Ensure you are using Client from the Satori namespace
Client client = new DefaultClient(apiKey, host, port, useSsl);

// Alternatively, you can create an HttpClient with the same parameters as opposed to the gRPC-based DefaultClient
Client client = new HttpClient(apiKey, host, port, useSsl);
API Keys
Create and rotate API keys in the Satori Settings page.

Configuring the request timeout length #

Each request to Satori from the client must complete in a certain period of time before it is considered to have timed out. You can configure how long this period is (in seconds) by setting the Timeout value on the client:

1
client.Timeout = 10;

Authentication #

Authenticate users using the Satori Client via their unique ID.

When authenticating, you can optionally pass in any desired defaultProperties and/or customProperties to be updated. If none are provided, the properties remain as they are on the server.

1
2
3
4
Map<String, String> defaultProperties = new HashMap<>();
Map<String, String> customProperties = new HashMap<>();

Session session = client.authenticate("identityId", defaultProperties, customProperties).get();

When authenticated the server responds with an auth token (JWT) which contains useful properties and gets deserialized into a Session object.

Session lifecycle #

Sessions expire after five (5) minutes by default. Expiring inactive sessions is good security practice.

Satori provides ways to restore sessions, for example when players re-launch the game, or refresh tokens to keep the session active while the game is being played.

Use the auth and refresh tokens on the session object to restore or refresh sessions.

Restore a session without having to re-authenticate:

1
2
3
4
String authToken = "<AuthToken>";
String refreshToken = "<RefreshToken>";

session = DefaultSession.restore(authToken, refreshToken);

Manually refreshing a session #

Sessions can be manually refreshed.

1
session = client.sessionRefresh(session).get();

Ending sessions #

Logout and end the current session:

1
client.authenticateLogout(session).get();

Experiments #

Satori Experiments allow you to test different game features and configurations in a live game.

List the current experiments for this user:

1
ExperimentList experiments = client.getAllExperiments(session).get();

You can also specify an array of experiment names you wish to get:

1
ExperimentList experiments = client.getExperiments(session, "ExperimentOne", "ExperimentTwo").get();

Feature flags #

Satori feature flags allow you to enable or disable features in a live game.

Get a single flag #

Get a single feature flag for this user:

1
Flag flag = client.getFlag(session, "FlagName").get();

You can also specify a default value for the flag if a value cannot be found:

1
Flag flag = client.getFlag(session, "FlagName", "DefaultValue").get();

Specifying a default value ensures no exception will be thrown if the network is unavailable, instead a flag with the specified default value will be returned.

Get a single default flag #

Get a single default flag for this user:

1
Flag flag = client.getFlagDefault("FlagName").get();

Similar to the getFlag method, you can also provide a default value for default flags:

1
Flag flag = client.getFlagDefault("FlagName", "DefaultValue").get();

Specifying a default value ensures no exception will be thrown if the network is unavailable, instead a flag with the specified default value will be returned.

Listing identity flags #

List the available feature flags for this user:

1
FlagList flagList = client.getFlags(session).get();

Listing default flags #

List all default feature flags:

1
FlagList flagList = client.getFlagsDefault().get();

Events #

Satori Events allow you to send data for a given user to the server for processing.

Metadata Limits
The maximum size of the metadata field is 4096 bytes.

Sending single events #

1
client.event(session, new Event("gameFinished", Instant.now(), "<Value>", null)).get();

Sending multiple events #

1
2
3
4
5
ArrayList<Event> events = new ArrayList<>();
events.add(new Event("adStarted", Instant.now(), "<Value>", null));
events.add(new Event("appLaunched", Instant.now(), "<Value>", null));

client.events(session, events).get();

Live events #

Satori Live Events allow you to deliver established features to your players on a custom schedule.

List all available live events:

1
LiveEventList liveEventsList = client.getLiveEvents(session).get();

Identities #

Satori Identities identify individual players of your game and can be enriched with custom properties.

List an identity’s properties #

1
Properties properties = client.listProperties(session).get();

Update an identity’s properties #

1
2
3
4
5
6
7
8
9
Map<String, String> defaultProperties = new HashMap<>();
defaultProperties.put("DefaultPropertyKey", "DefaultPropertyValue");
defaultProperties.put("AnotherDefaultPropertyKey", "AnotherDefaultPropertyValue");

Map<String, String> customProperties = new HashMap<>();
customProperties.put("CustomPropertyKey", "CustomPropertyValue");
customProperties.put("AnotherCustomPropertyKey", "AnotherCustomPropertyValue");

client.updateProperties(session, defaultProperties, customProperties).get();

You can immediately reevaluate the user’s audience memberships upon updating their properties by passing recompute as true:

1
2
3
boolean recompute = true;

client.updateProperties(session, defaultProperties, customProperties recompute).get();

Identifying a session with a new ID #

If you want to submit events to Satori before a user has authenticated with the game server backend (e.g. Nakama) and has a User ID, you should authenticate with Satori using a temporary ID, such as the device’s unique identifier or a randomly generated one.

1
2
String deviceId = "<DeviceUniqueIdentifier>";
Session session = client.authenticate(deviceId, defaultProperties, customProperties).get();

You can then submit events before the user has authenticated with the game backend.

1
client.event(session, new Event("gameLaunched", Instant.now(), "<Value>", null)).get();

The user would then authenticate with the game backend and retrieve their User ID.

1
2
com.heroiclabs.nakama.Session nakamaSession = nakamaClient.authenticateEmail("example@heroiclabs.com", "password").get();
String userId = nakamaSession.getUserId();

Once a user has successfully authenticated, you should then call identity to enrich the current session and return a new session that should be used for submitting future events.

1
Session newSession = client.identify(session, userId, defaultProperties, customProperties).get();

Note that the old session is no longer valid and cannot be used after this.

Deleting an identity #

1
client.deleteIdentity(session).get();