Skip to main content

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:

PropertyMeaning
PluginKeySnowcloak's normalised key for this registered plugin identity.
LocalRevisionRevision of the current local value. It advances whenever the value is changed or cleared.
AcknowledgedRevisionLatest local revision accepted as part of a server transmission.
StateCurrent ExtensionPublicationState.
PayloadBytesUTF-8 byte count of the current local value; zero when cleared.
LastChangedAtUtcTime the local value was last changed or cleared.
LastAttemptedAtUtcTime Snowcloak most recently attempted to include an unacknowledged revision in a transmission.
LastAcknowledgedAtUtcTime the server last accepted the current extension manifest.
LastFailureMost recent transmission failure reason, when present.

Publication states are:

StateMeaning
EmptyNo local value has been published for this registration.
DebouncedA new local revision is waiting for the minimum push interval.
PendingThe revision is ready for Snowcloak's next character-data publication.
TransmittingSnowcloak has included the unacknowledged revision in an attempted transmission.
AcknowledgedThe server accepted the manifest containing the current revision.
FailedTransmission 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:

PropertyMeaning
UidSnowcloak UID for the user sending data
ObjectIndexCurrent visible object index when the state is object-bound; otherwise null.
AvailabilityWhy data is present or absent.
DataThe remote plugin's data string when available; otherwise null.
BytesUTF-8 byte count of Data; zero when no value is present.
UpdatedAtUtcTime this client delivered or cleared the cached state, when recorded. This is not a timestamp supplied by the remote plugin.

Remote availability states

StateMeaning
NoDataThe pair is accessible, but its current manifest has no value for this plugin.
AvailableData is available and ObjectIndex identifies the visible character.
RevertedA previously delivered value was explicitly removed from the pair's extension section.
NotVisibleThe pair is outside object range or paused and the plugin lacks applicable out-of-range access.
AvailableOutOfRangeData is available for an online, unpaused pair outside object range. ObjectIndex is null.
OfflineThe 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.