Extension Data
Moving bytes around.
Sending Data
Extension data is one UTF-8 string owned by one plugin identity. Snowcloak treats the contents of your payload as opaque - the server merely decides whether it's oversized or from a banned plugin.
The default hard limits are:
- 4 KiB of UTF-8 data per registered plugin;
- one slot per plugin identity
- 32 registered plugins; and
- 128 KiB across all registered plugins.
Certain plugins may be granted exceptions to this, and limits may be raised over time as usage and capacity becomes clearer. With that in mind, the IPC will report the current limits at grant time, which can be read to avoid duplicating constants:
if (grant.Allows(SnowcloakIpcCapability.TransmitExtensionData))
{
int myLimit = grant.MaxBytesPerPlugin;
int totalLimit = grant.MaxTotalBytes;
int slotLimit = grant.MaxRegisteredPlugins;
int minimumPushInterval = grant.MinPushIntervalMs;
}
It's recommended to use a versioned payload. JSON is convenient and used by a fair few of the default plugins, but any string format is valid. It's advised not to use base64, as it adds a ~33% overhead.
using System.Text;
using System.Text.Json;
public sealed record SharedStatus(int Version, string Text, string Mood);
var payload = JsonSerializer.Serialize(new SharedStatus(
Version: 1,
Text: "Looking for PvP!",
Mood: "Rambunctious"));
if (Encoding.UTF8.GetByteCount(payload) > grant.MaxBytesPerPlugin)
{
throw new InvalidOperationException("Extension payload is too large.");
}
SnowcloakOperationResult result = ipc.SetLocalDataWithResult(payload);
if (!result.Success && result.Code != SnowcloakOperationCode.Unchanged)
{
// Report result.Reason or disable the feature as appropriate.
}
SetLocalDataWithResult(null) or an empty string clears the local slot, and publishes its removal. Re-sending an identical
value returns Unchanged and does not create another revision.
Snowcloak enforces a two-second minimum publication interval at both client and server level, and performs debounce internally. Plugins do not need their own correctness debounce, though it's still polite to avoid needless calls.
A successful SetLocalDataWithResult records a local revision, but does not prove server acceptance. You can use publication
state to distinguish local edits from acknowledged transmission:
ipc.LocalDataStatusChanged += status =>
{
bool acknowledged =
status.State == ExtensionPublicationState.Acknowledged
&& status.AcknowledgedRevision == status.LocalRevision;
string? failure = status.LastFailure;
};
ExtensionPublicationStatus? current = ipc.GetLocalDataStatus();
ExtensionPublicationStatus contains:
| Property | Meaning |
|---|---|
PluginKey | Snowcloak's normalised key for this registered plugin identity. |
LocalRevision | Revision of the current local value. It advances whenever the value is changed or cleared. |
AcknowledgedRevision | Latest local revision accepted as part of a server transmission. |
State | Current ExtensionPublicationState. |
PayloadBytes | UTF-8 byte count of the current local value; zero when cleared. |
LastChangedAtUtc | Time the local value was last changed or cleared. |
LastAttemptedAtUtc | Time Snowcloak most recently attempted to include an unacknowledged revision in a transmission. |
LastAcknowledgedAtUtc | Time the server last accepted the current extension manifest. |
LastFailure | Most recent transmission failure reason, when present. |
Publication states are:
| State | Meaning |
|---|---|
Empty | No local value has been published for this registration. |
Debounced | A new local revision is waiting for the minimum push interval. |
Pending | The revision is ready for Snowcloak's next character-data publication. |
Transmitting | Snowcloak has included the unacknowledged revision in an attempted transmission. |
Acknowledged | The server accepted the manifest containing the current revision. |
Failed | Transmission of the current unacknowledged revision failed. LastFailure contains the reason when available. |
TransmitExtensionData permissions allow the value to be included in Snowcloak's extension section. A paired receiver running
the matching plugin may receive it while visible, or while out of range if that receiver's user grants the additional out-of-range receipt permission.
Receiving Data
Snowcloak provides a structured state API for data receipt:
private readonly Dictionary<string, SharedStatus> _statuses =
new(StringComparer.Ordinal);
private void OnRemoteDataStateChanged(RemoteExtensionDataState state)
{
if (state.Availability is RemoteExtensionDataAvailability.Available
or RemoteExtensionDataAvailability.AvailableOutOfRange)
{
if (TryParseStatus(state.Data, out var status))
{
_statuses[state.Uid] = status;
return;
}
}
_statuses.Remove(state.Uid);
}
private static bool TryParseStatus(string? value, out SharedStatus status)
{
status = null!;
if (string.IsNullOrWhiteSpace(value)
|| Encoding.UTF8.GetByteCount(value) > 4096)
{
return false;
}
try
{
var parsed = JsonSerializer.Deserialize<SharedStatus>(value);
if (parsed is null
|| parsed.Version != 1
|| parsed.Text.Length > 200
|| parsed.Mood.Length > 32
|| parsed.Text.Any(char.IsControl)
|| parsed.Mood.Any(char.IsControl))
{
return false;
}
status = parsed;
return true;
}
catch (JsonException)
{
return false;
}
}
On connection, registration, permission changes, or pair changes, enumerate the accessible pairs and query their current state. Tis ensures validity even when an event occured prior to subscription:
IReadOnlyList<PairInfo> pairs = grant.Allows(
SnowcloakIpcCapability.ReadPairData |
SnowcloakIpcCapability.ReadPairDataOutOfRange)
? ipc.GetAllPairs()
: ipc.GetVisiblePairs();
foreach (var pair in pairs)
{
RemoteExtensionDataState? state = ipc.GetRemoteDataState(pair.Uid);
if (state != null)
{
OnRemoteDataStateChanged(state);
}
}
GetRemoteData(uid) returns only the current data string and is useful for simple consumers. GetRemoteDataState(uid) is preferable - null data alone cannot explain why data is absent.
RemoteExtensionDataState contains:
| Property | Meaning |
|---|---|
Uid | Snowcloak UID for the user sending data |
ObjectIndex | Current visible object index when the state is object-bound; otherwise null. |
Availability | Why data is present or absent. |
Data | The remote plugin's data string when available; otherwise null. |
Bytes | UTF-8 byte count of Data; zero when no value is present. |
UpdatedAtUtc | Time this client delivered or cleared the cached state, when recorded. This is not a timestamp supplied by the remote plugin. |
Remote availability states
| State | Meaning |
|---|---|
NoData | The pair is accessible, but its current manifest has no value for this plugin. |
Available | Data is available and ObjectIndex identifies the visible character. |
Reverted | A previously delivered value was explicitly removed from the pair's extension section. |
NotVisible | The pair is outside object range or paused and the plugin lacks applicable out-of-range access. |
AvailableOutOfRange | Data is available for an online, unpaused pair outside object range. ObjectIndex is null. |
Offline | The pair is known but no longer online. Data is cleared. |
The older ExtensionDataApplied(uid, objectIndex, data) event is retained for visible-object integrations. It fires only for object-bound data. A null value clears the old visible-object projection; in IPC 1.4 it must not be interpreted by itself as proof that the remote plugin removed its value. Use RemoteDataStateChanged to distinguish Reverted, AvailableOutOfRange, NotVisible and Offline.