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:
| Property | Meaning |
|---|---|
Accepted | Whether this load owns the plugin identity's current registration. |
Reason | Rejection or diagnostic reason, when present. |
PluginKey | Snowcloak's normalised key for the registered Dalamud plugin identity; null when registration was rejected. |
MaxBytesPerPlugin | Maximum UTF-8 byte count of this plugin's extension-data value. Primarily useful if you're granted an exception to the 4KB rule. |
MaxTotalBytes | Aggregate extension-data capacity across all registered plugins. Primarily useful if you're granted an exception to the 4KB rule. |
MaxRegisteredPlugins | Maximum number of simultaneously registered plugin identities. |
MinPushIntervalMs | Minimum interval between extension-data publications. |
RequestedPermissions | Complete capability mask requested by this registration. |
GrantedPermissions | Requested capabilities currently granted by the user. |
PendingPermissions | Requested capabilities not currently granted. This is computed as RequestedPermissions & ~GrantedPermissions. |
Allows(capability) returns true only when every bit in capability is granted.
Permissions table
| Permission | Allows |
|---|---|
ReadPairData | Self-identity, handled object indices, visible pair lookup and visible pair/application events. |
ReadPairDataOutOfRange | GetAllPairs, out-of-range GetPairByUid, out-of-range application state and lifecycle events. Requires ReadPairData as well. |
ReadProfileData | Cached profile summaries and profile-update events. |
OpenProfileWindow | Opening Snowcloak's profile window for a known pair. |
ApplyMcdf | Applying an MCDF file to a GPose-range actor through Snowcloak. |
TransmitExtensionData | Publishing this plugin's data slot. |
ReceiveExtensionData | Receiving this plugin's matching data for visible pairs. If not set, data will be dropped as if the user didn't have it installed. |
ReceiveExtensionDataOutOfRange | Receiving matching data for online, unpaused pairs outside object range. Requires ReceiveExtensionData as well. |
OpenPairRequestWindow | Opening 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
| Event | Delegate signature | Required permission | Meaning and response |
|---|---|---|---|
Available | Action | None | Snowcloak has registered its IPC providers. Attempt an immediate version check and registration. This does not mean Snowcloak is connected to its server. |
Unavailable | Action | None | Snowcloak is unloading or has removed its IPC providers. Clear cached Snowcloak state and wait for Available. |
ConnectionStateChanged | Action<SnowcloakConnectionState> | None | Snowcloak's server connection moved to Disconnected, Connecting or Connected. Re-query current state after connection. |
HandledCharactersChanged | Action | ReadPairData | The set returned by GetHandledObjectIndices() changed. Re-query it. |
ProfileUpdated | Action<string> | ReadProfileData | The cached profile for the supplied uid changed or was invalidated. Re-query GetProfileSummary(uid). |
PairVisibilityChanged | Action<string, ushort, bool> | ReadPairData | Supplies uid, objectIndex and isVisible. Re-query the pair. When isVisible is false, the index identifies the object that disappeared and must not be retained. |
PairDataApplied | Action<string, ushort> | ReadPairData | Supplies uid and objectIndex after Snowcloak finishes applying appearance data to that visible character. It is unrelated to extension-data delivery. |
PairAdded | Action<string> | Pair-read permissions applicable to the pair | The supplied uid entered the accessible pair snapshot. Re-query GetPairByUid(uid) or the appropriate pair collection. |
PairRemoved | Action<string> | Pair-read permissions applicable to the previous pair state | The supplied uid left the accessible pair snapshot. Remove cached pair, profile, application and extension state for it. |
PairStateChanged | Action<string> | Pair-read permissions applicable to the pair | One or more PairInfo properties changed for the supplied uid. Re-query rather than inferring which property changed. |
ApplicationStatusChanged | Action<PairApplicationStatus> | Pair-read permissions applicable to the pair | Supplies the complete current application status. A non-visible pair requires both ReadPairData and ReadPairDataOutOfRange. |
ExtensionDataApplied | Action<string, ushort, string?> | ReceiveExtensionData | Legacy visible-object projection supplying uid, objectIndex and data. A null value clears the projection. Prefer RemoteDataStateChanged. |
LocalDataStatusChanged | Action<ExtensionPublicationStatus> | TransmitExtensionData | Supplies the complete current publication status for this plugin's local value. |
RemoteDataStateChanged | Action<RemoteExtensionDataState> | Receive permissions applicable to the pair | Supplies the complete availability and data state for this plugin's remote value. AvailableOutOfRange requires both receive permissions. |
PermissionsChanged | Action<ExtensionGrant> | Current registration | Supplies 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:
| State | Meaning |
|---|---|
Disconnected | Snowcloak has no active server connection. |
Connecting | Snowcloak is establishing or re-establishing its server connection. |
Connected | Snowcloak has an active authenticated server connection. |
Lifecycle, errors, and threading
Some tips that didn't really fit elsewhere in the docs:
- Subscribe to
AvailableandUnavailable, 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
UnavailableandDisconnected. - Re-query after
PermissionsChanged, pair lifecycle events and connection changes. - Call
UnregisterWithResultduring normal plugin disposal, then unsubscribe events and dispose the wrapper. - Expect
IpcNotReadyErrorwhen Snowcloak is absent or between reload states andIpcTypeMismatchErrorwhen 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.