Configuration
Construction-Time vs Runtime Settings
Configuration comes in two tiers.
Settings in ClientConstructOptions are baked when the client is constructed and stay fixed for its lifetime; the network tunables additionally have setter and getter pairs on the client instance, so they can be read back and adjusted later.
| Setting | ClientConstructOptions Field |
Runtime Setter | Takes Effect |
|---|---|---|---|
| App id and version | AppId, AppVersion |
— | At construction. |
| Transport protocol | Protocol |
— | At construction. |
| Alternative ports | UseAlternativePorts |
— | At construction. |
| Region selection mode | RegionSelection |
— | At construction. |
| Lobby statistics push | AutoLobbyStats |
— | At construction. |
| Disconnect timeout | DisconnectTimeoutMs |
SetDisconnectTimeout() |
Immediately, also while connected. |
| Ping interval | PingIntervalMs |
SetPingInterval() |
Immediately, also while connected. |
| Sent count allowance | SentCountAllowance |
SetSentCountAllowance() |
Immediately, also while connected. |
| Quick resend attempts | QuickResendAttempts |
SetQuickResendAttempts() |
Immediately, also while connected. |
| CRC checksums | EnableCrc |
SetCrcEnabled() |
Only while disconnected. |
| Unreliable command limit | LimitOfUnreliableCommands |
SetLimitOfUnreliableCommands() |
Immediately, also while connected. |
| Auto-join lobby | — | SetAutoJoinLobby() |
On the next connect. |
| Traffic statistics | — | SetTrafficStatsEnabled() |
Immediately. |
Network Tunables
Six tunables shape the transport behavior.
DisconnectTimeout sets how long the connection may stay unresponsive before it counts as lost, PingInterval controls the keep-alive rate, SentCountAllowance is the resend budget for reliable commands, QuickResendAttempts accelerates early resends, CrcEnabled adds payload checksums on UDP and LimitOfUnreliableCommands caps the incoming unreliable command queue.
CRC checksums must be decided before the connection is established.
Bake EnableCrc in ClientConstructOptions or call SetCrcEnabled() while disconnected; while connected the setter is rejected and the value stays unchanged.
The other five tunables — DisconnectTimeout, PingInterval, SentCountAllowance, QuickResendAttempts and LimitOfUnreliableCommands — take effect immediately, even on a live connection.
The peer reads the current values on every service tick, so raising the disconnect timeout mid-session, for example, applies to the very next timeout check.
Three of the tunables are UDP-specific: SentCountAllowance, QuickResendAttempts and LimitOfUnreliableCommands have no effect on TCP or WebSocket connections.
DisconnectTimeout and PingInterval apply on every transport, and QuickResendAttempts is clamped to a maximum of 4.
Bake the tunables you know upfront:
C++
ClientConstructOptions options;
options.AppId = PHOTON_STR("your-app-id");
options.AppVersion = PHOTON_STR("1.0");
options.DisconnectTimeoutMs = 10000;
options.PingIntervalMs = 1000;
options.EnableCrc = true;
RealtimeClient client(options);
C
/* The construction-time tunables are not on the ABI, so the same values are
applied through the runtime setters right after creating the client. */
RealtimeHandle client = realtime_create("your-app-id", "1.0", queue);
realtime_set_disconnect_timeout(client, 10000);
realtime_set_ping_interval(client, 1000);
realtime_set_crc_enabled(client, 1); /* only accepted while disconnected */
/* Setting CRC right after realtime_create is safe because the client is
still disconnected at that point, which is the one window the setter
accepts. realtime_get_* counterparts read every value back.
SetSentCountAllowance is the one tunable without a C entry point, so the
resend budget keeps its SDK default over the ABI. */
Transport Protocols
ClientConstructOptions.Protocol selects the transport: UDP (the recommended default), TCP, WS or WSS.
ConnectionProtocol::Default resolves per platform — WebSocket on WASM builds, where raw UDP is unavailable, and UDP everywhere else.
Two related knobs sit next to the protocol choice.
UseAlternativePorts (construct-time) switches to Photon's alternative port range, which helps when restrictive firewalls block the standard ports, and ConnectOptions.TryUseDatagramEncryption enables DTLS encryption on UDP connections.
Server Time
GetServerTime() returns the synced Photon server clock in milliseconds — the value to use whenever clients need to agree on a point in time, such as round starts or ability cooldowns.
It is meaningful only after a successful connect.
The value is a 32-bit millisecond counter that wraps around roughly every 24.8 days. Compare timestamps with wrap-aware arithmetic instead of a plain less-than when sessions can run long.
FetchServerTimestamp() is not a getter: it requests a fresh clock synchronization from the server, which is worth doing after a long stall before trusting GetServerTime() again.
NetworkStats.ServerTimeMs is the same value as GetServerTime(), bundled into the statistics snapshot.
The master client schedules the round start a few seconds ahead and announces it, so every client counts down against the same server-time deadline:
C++
constexpr uint8_t roundStartEvent = 42;
auto subscription = client.SubscribeEvent([](uint8_t eventCode, int /*senderId*/, std::span<const uint8_t> data) {
if (eventCode != roundStartEvent || data.size() != sizeof(int))
{
return;
}
int startTime = 0;
std::memcpy(&startTime, data.data(), sizeof(startTime));
// Start the round once GetServerTime() reaches startTime.
});
const int startTime = client.GetServerTime() + 5000;
client.SendEvent(roundStartEvent, startTime);
C
#define ROUND_START_EVENT 42
/* Receiving, in the polled batch: */
if (e->type == RT_Event &&
e->counter == ROUND_START_EVENT &&
e->blobOffset >= 0 &&
e->blobLength == (int32_t)sizeof(int32_t))
{
int32_t startTime = 0;
memcpy(&startTime, blob + e->blobOffset, sizeof(startTime));
/* Start the round once realtime_server_time() reaches startTime. */
}
/* Sending, from the master client: */
int32_t startTime = realtime_server_time(client) + 5000;
realtime_send_event(client, ROUND_START_EVENT,
(const uint8_t*)&startTime, (int32_t)sizeof(startTime),
1, 0, 0, NULL, 0, 0, 0, 0, 0);
/* realtime_server_time is the direct counterpart of GetServerTime() and
wraps the same way, and realtime_fetch_server_timestamp requests a fresh
synchronization. The payload travels as raw bytes over the ABI, so both
sides agree on the width by using int32_t explicitly. */
Custom Operations
SendCustomOperation(opCode, params, reliable, channel, encrypt) is the escape hatch for calling custom server-side operations — typically a Photon Server plugin — by raw operation code.
The bool return only reports whether the request was queued for sending; the actual outcome arrives asynchronously.
Parameter keys are Photon parameter codes, not names: each RealtimeMap key must be a string holding the decimal form of a byte value between 0 and 255.
A non-string, non-numeric or out-of-range key makes the whole call return false and nothing is sent.
Values may be any type RealtimeValue carries.
The reply arrives through OnCustomOperationResponse(opCode, errorCode, errorString, data).
Match responses by opCode; an errorCode of 0 means success and the response parameters come back in data with their byte codes as decimal string keys, such as "1".
Send an operation and handle its response:
C++
constexpr uint8_t leaderboardOp = 7;
auto subscription = client.OnCustomOperationResponse.Subscribe(
[](uint8_t opCode, int errorCode, RealtimeCore::Common::StringViewType /*errorString*/, const RealtimeMap& /*data*/) {
if (opCode != leaderboardOp)
{
return;
}
if (errorCode != 0)
{
// The operation failed server-side; errorString explains why.
return;
}
// Success: read the results from data, keyed by decimal strings such as "1".
});
RealtimeMap params;
params.Set(PHOTON_STR("1"), PHOTON_STR("weekly")); // parameter code 1: board name
params.Set(PHOTON_STR("2"), static_cast<int32_t>(10)); // parameter code 2: entry count
if (!client.SendCustomOperation(leaderboardOp, params))
{
// Rejected locally: a key was not a valid parameter code.
}
C
#define LEADERBOARD_OP 7
/* Parameter keys are Photon parameter codes as decimal strings. */
rt_blob params = {0};
rt_begin(¶ms);
rt_put_str(¶ms, "1", "weekly"); /* parameter code 1: board name */
rt_put_i32(¶ms, "2", 10); /* parameter code 2: entry count */
rt_end(¶ms);
int32_t queued = realtime_send_custom_operation(client, LEADERBOARD_OP,
params.data, params.len,
1, /* reliable */
0, /* channelId */
0); /* encrypt */
rt_free(¶ms);
if (queued == 0)
{
/* Rejected locally: a key was not a valid parameter code. */
}
/* CustomOperationResponse (118): `origin` is the opCode, `counter` the error
code, and the blob is [string errorString][map data]. */
if (e->type == RT_CustomOperationResponse &&
e->origin == LEADERBOARD_OP &&
e->blobOffset >= 0)
{
rt_reader r = rt_read(blob + e->blobOffset, e->blobLength);
int32_t errorLen = 0;
const char* error = rt_get_str(&r, &errorLen);
if (e->counter != 0)
{
printf("leaderboard failed: %.*s\n", (int)errorLen, error);
}
else
{
/* Success: the map that follows holds the results, keyed by decimal
strings such as "1". Walk it as any other map blob. */
int32_t entries = rt_get_i32(&r);
for (int32_t i = 0; i < entries; ++i)
{
int32_t keyLen = 0;
const char* key = rt_get_str(&r, &keyLen);
uint8_t tag = rt_get_u8(&r);
rt_skip_value(&r, tag);
(void)key;
}
}
}
/* The parameter map is a map blob, so the same restriction applies as to
any other map over the ABI: only the tagged value types can be sent, and
a nested container cannot. The blob of the response holds the error
string first and the parameter map immediately after it, with no
separator between them. */