Skip to main content

Minimal Integration

A minimal IPC implementation, followed by the bits you actually want to know.

Getting Started

Snowcloak.Ipc is a ready-to-go integration, available on Nuget. Install through your IDE's package manager, or manually.

<ItemGroup>
<PackageReference Include="Snowcloak.Ipc" Version="1.4.0" />
</ItemGroup>

Construct the IPC with your own IDalamudPluginInterface - the wrapper derives the key from your plugin's InternalName and creates a registration token.

You should subscribe to wrapper events before registering, attempt an immediate connection, and reconnect when Snowcloak announces that its providers are available:

using Dalamud.Plugin;
using Dalamud.Plugin.Ipc.Exceptions;
using Snowcloak.Ipc;

public sealed class SnowcloakIntegration : IDisposable
{
private readonly SnowcloakIpc _ipc;
private ExtensionGrant? _grant;

private const SnowcloakIpcCapability RequestedPermissions =
SnowcloakIpcCapability.ReadPairData |
SnowcloakIpcCapability.TransmitExtensionData |
SnowcloakIpcCapability.ReceiveExtensionData;

public SnowcloakIntegration(IDalamudPluginInterface pluginInterface)
{
_ipc = new SnowcloakIpc(pluginInterface);
_ipc.Available += Connect;
_ipc.Unavailable += OnUnavailable;
_ipc.PermissionsChanged += OnPermissionsChanged;
_ipc.RemoteDataStateChanged += OnRemoteDataStateChanged;
Connect();
}

public bool IsAvailable { get; private set; }

private void Connect()
{
try
{
if (!_ipc.TryGetApiVersion(out var version)
|| version.Major != 1
|| version.Minor < 4)
{
OnUnavailable();
return;
}

_grant = _ipc.Register(RequestedPermissions);
IsAvailable = _grant.Accepted;
}
catch (Exception exception) when (exception is IpcNotReadyError or IpcTypeMismatchError)
{
OnUnavailable();
}
}

private void OnPermissionsChanged(ExtensionGrant grant)
{
_grant = grant;
}

private void OnUnavailable()
{
IsAvailable = false;
_grant = null;
}

private void OnRemoteDataStateChanged(RemoteExtensionDataState state)
{
// Validate and apply or clear your own state here.
}

public void Dispose()
{
try
{
_ipc.UnregisterWithResult();
}
catch (Exception exception) when (exception is IpcNotReadyError or IpcTypeMismatchError)
{
}

_ipc.Available -= Connect;
_ipc.Unavailable -= OnUnavailable;
_ipc.PermissionsChanged -= OnPermissionsChanged;
_ipc.RemoteDataStateChanged -= OnRemoteDataStateChanged;
_ipc.Dispose();
}
}

Calling Connect() immediately is important - if Snowcloak was already loaded, its earlier Available event will not be replayed. The event exists so an IPC-consuming plugin can recover later if either Snowcloak or itself is reloaded.

Registration and Permissions

Registration and approval are separate states.

When a plugin first requests a permission, or expands its request, Snowcloak will alert the user and request approval. Users are not required to approve immediately, so your plugin should be able to tolerate being in limbo across game restarts. Each permission can be granted or revoked independently.


var requested =
SnowcloakIpcCapability.ReadPairData |
SnowcloakIpcCapability.ReadPairDataOutOfRange |
SnowcloakIpcCapability.ReceiveExtensionData |
SnowcloakIpcCapability.ReceiveExtensionDataOutOfRange;

ExtensionGrant grant = ipc.Register(requested);

if (!grant.Accepted)
{
// No slot was registered. Show `grant.Reason` and disable the integration.
return;
}

if (grant.PendingPermissions != SnowcloakIpcCapability.None)
{
// Registration succeeded, but the user has not approved every request.
}

bool canReadVisiblePairs = grant.Allows(SnowcloakIpcCapability.ReadPairData);
bool canReadAllPairs = grant.Allows(
SnowcloakIpcCapability.ReadPairData |
SnowcloakIpcCapability.ReadPairDataOutOfRange);

Plugins can open their own integration settings directly:

SnowcloakOperationResult result = ipc.OpenPluginIntegrations();

It's recommended to think about whether you should really be doing that, and it's probably best not to without user interaction.

When a user approves or revokes a permission, it'll fire the PermissionsChanged event. Replace your cached grant with the supplied value and immediately stop any features whose permission was revoked. Manual querying is possible using GetGrant().

ExtensionGrant.Accepted means that Snowcloak registered the plugin for the current load. It does not mean that any permissions have been approved. Use Allows for every optional feature.

ExtensionGrant contains:

PropertyMeaning
AcceptedWhether this load owns the plugin identity's current registration.
ReasonRejection or diagnostic reason, when present.
PluginKeySnowcloak's normalised key for the registered Dalamud plugin identity; null when registration was rejected.
MaxBytesPerPluginMaximum UTF-8 byte count of this plugin's extension-data value. Primarily useful if you're granted an exception to the 4KB rule.
MaxTotalBytesAggregate extension-data capacity across all registered plugins. Primarily useful if you're granted an exception to the 4KB rule.
MaxRegisteredPluginsMaximum number of simultaneously registered plugin identities.
MinPushIntervalMsMinimum interval between extension-data publications.
RequestedPermissionsComplete capability mask requested by this registration.
GrantedPermissionsRequested capabilities currently granted by the user.
PendingPermissionsRequested capabilities not currently granted. This is computed as RequestedPermissions & ~GrantedPermissions.

Allows(capability) returns true only when every bit in capability is granted.

Permissions table

PermissionAllows
ReadPairDataSelf-identity, handled object indices, visible pair lookup and visible pair/application events.
ReadPairDataOutOfRangeGetAllPairs, out-of-range GetPairByUid, out-of-range application state and lifecycle events. Requires ReadPairData as well.
ReadProfileDataCached profile summaries and profile-update events.
OpenProfileWindowOpening Snowcloak's profile window for a known pair.
ApplyMcdfApplying an MCDF file to a GPose-range actor through Snowcloak.
TransmitExtensionDataPublishing this plugin's data slot.
ReceiveExtensionDataReceiving this plugin's matching data for visible pairs. If not set, data will be dropped as if the user didn't have it installed.
ReceiveExtensionDataOutOfRangeReceiving matching data for online, unpaused pairs outside object range. Requires ReceiveExtensionData as well.
OpenPairRequestWindowOpening a Snowcloak pair-request confirmation for a visible target.

You should only request permissions actually used by the plugin. SnowcloakIpcCapability.All is useful for reference or diagnostic purposes and is used by an internal testing plugin, but is a poor default for a normal integration.

RegisterExtension() is a convenience method whch requests only TransmitExtensionData | ReceiveExtensionData. Use Register(...) when a plugin needs any other capability.

Quick Event Reference

EventDelegate signatureRequired permissionMeaning and response
AvailableActionNoneSnowcloak has registered its IPC providers. Attempt an immediate version check and registration. This does not mean Snowcloak is connected to its server.
UnavailableActionNoneSnowcloak is unloading or has removed its IPC providers. Clear cached Snowcloak state and wait for Available.
ConnectionStateChangedAction<SnowcloakConnectionState>NoneSnowcloak's server connection moved to Disconnected, Connecting or Connected. Re-query current state after connection.
HandledCharactersChangedActionReadPairDataThe set returned by GetHandledObjectIndices() changed. Re-query it.
ProfileUpdatedAction<string>ReadProfileDataThe cached profile for the supplied uid changed or was invalidated. Re-query GetProfileSummary(uid).
PairVisibilityChangedAction<string, ushort, bool>ReadPairDataSupplies uid, objectIndex and isVisible. Re-query the pair. When isVisible is false, the index identifies the object that disappeared and must not be retained.
PairDataAppliedAction<string, ushort>ReadPairDataSupplies uid and objectIndex after Snowcloak finishes applying appearance data to that visible character. It is unrelated to extension-data delivery.
PairAddedAction<string>Pair-read permissions applicable to the pairThe supplied uid entered the accessible pair snapshot. Re-query GetPairByUid(uid) or the appropriate pair collection.
PairRemovedAction<string>Pair-read permissions applicable to the previous pair stateThe supplied uid left the accessible pair snapshot. Remove cached pair, profile, application and extension state for it.
PairStateChangedAction<string>Pair-read permissions applicable to the pairOne or more PairInfo properties changed for the supplied uid. Re-query rather than inferring which property changed.
ApplicationStatusChangedAction<PairApplicationStatus>Pair-read permissions applicable to the pairSupplies the complete current application status. A non-visible pair requires both ReadPairData and ReadPairDataOutOfRange.
ExtensionDataAppliedAction<string, ushort, string?>ReceiveExtensionDataLegacy visible-object projection supplying uid, objectIndex and data. A null value clears the projection. Prefer RemoteDataStateChanged.
LocalDataStatusChangedAction<ExtensionPublicationStatus>TransmitExtensionDataSupplies the complete current publication status for this plugin's local value.
RemoteDataStateChangedAction<RemoteExtensionDataState>Receive permissions applicable to the pairSupplies the complete availability and data state for this plugin's remote value. AvailableOutOfRange requires both receive permissions.
PermissionsChangedAction<ExtensionGrant>Current registrationSupplies a replacement grant after the user changes this plugin's permissions. Replace the cached grant and stop features whose capabilities were revoked.

Available, Unavailable and ConnectionStateChanged are subscribed when SnowcloakIpc is constructed. The remaining plugin-specific event channels are created after Register returns an accepted grant. They are recreated when the plugin registers again after a Snowcloak reload.

Pair lifecycle events do not carry PairInfo; they carry an invalidation key or visibility scalars. ApplicationStatusChanged, LocalDataStatusChanged, RemoteDataStateChanged and PermissionsChanged carry complete typed records.

Events are not replayed and ordering between different event types is not a contract. Subscribe before registration, query the initial snapshots immediately after registration, and treat later events as prompts to replace or re-query state. Event callbacks can run on framework, SignalR or background threads.

SnowcloakConnectionState values are:

StateMeaning
DisconnectedSnowcloak has no active server connection.
ConnectingSnowcloak is establishing or re-establishing its server connection.
ConnectedSnowcloak has an active authenticated server connection.

Lifecycle, errors, and threading

Some tips that didn't really fit elsewhere in the docs:

  • Subscribe to Available and Unavailable, but also attempt registration immediately.
  • Re-register after Snowcloak reloads. The wrapper's per-load token and plugin-specific event channels are recreated by Register.
  • Clear cached Snowcloak state on Unavailable and Disconnected.
  • Re-query after PermissionsChanged, pair lifecycle events and connection changes.
  • Call UnregisterWithResult during normal plugin disposal, then unsubscribe events and dispose the wrapper.
  • Expect IpcNotReadyError when Snowcloak is absent or between reload states and IpcTypeMismatchError when a consumer uses an incompatible signature.
  • Do not assume IPC callbacks run on the framework thread. Snowcloak can publish events from framework, SignalR and background refresh paths. Protect shared collections and marshal game or UI work onto the appropriate thread.
  • Keep event handlers quick. Record the new state, schedule heavier work, and return.
  • Never retain an object index after visibility is lost. Resolve it again from a current snapshot.
  • Never treat publication success, appearance application and extension-data delivery as the same lifecycle event.