Skip to content

API Integration

shmitzas edited this page Nov 10, 2026 · 1 revision

API Integration

How to build a plugin that adds a feature to VIPCore. The modules in the repository are the reference implementations.

Shared-interface key: VIPCore.Api.v1.


1. Reference the contract

VIPCore.Contract.dll ships in the VIPCore release under resources/exports/.

<Reference Include="VIPCore.Contract">
  <HintPath>lib\VIPCore.Contract.dll</HintPath>
  <Private>false</Private>
</Reference>

Private="false" is required. VIPCore exports the one copy every module binds to; a second copy is a second type identity for the same interface, and binding stops matching with no build error. The SwiftlyS2.VIPCore.Contract NuGet package is no longer published.


2. Acquire the API

Not in Load — the shared interface doesn't exist yet there.

private IVipCoreApiV1? _vipApi;
private bool _isFeatureRegistered;

public override void UseSharedInterface(IInterfaceManager interfaceManager)
{
    _vipApi = null;
    _isFeatureRegistered = false;

    if (interfaceManager.HasSharedInterface("VIPCore.Api.v1"))
        _vipApi = interfaceManager.GetSharedInterface<IVipCoreApiV1>("VIPCore.Api.v1");

    RegisterWhenReady();
}

private void RegisterWhenReady()
{
    if (_vipApi == null) return;

    if (_vipApi.IsCoreReady())
        RegisterVipFeatures();
    else
        _vipApi.OnCoreReady += RegisterVipFeatures;
}

Check HasSharedInterface first: GetSharedInterface throws when VIPCore isn't loaded. Check IsCoreReady() before subscribing to OnCoreReady, which has already fired when your module loads after VIPCore.


3. Register the feature

private const string FeatureKey = "vip.myfeature";

private void RegisterVipFeatures()
{
    if (_vipApi == null || _isFeatureRegistered) return;

    _vipApi.RegisterFeature(FeatureKey, FeatureType.Toggle, null,
        displayNameResolver: p => Core.Translation.GetPlayerLocalizer(p)["vip.myfeature"]);
    _vipApi.DescribeFeature(FeatureKey, p => Core.Translation.GetPlayerLocalizer(p)["vip.myfeature.description"]);

    _isFeatureRegistered = true;
    _vipApi.PlayerLoaded += OnPlayerLoaded;
}
  • The key is what operators put in a group's Values: lower case, dotted, starting vip..
  • The name and the description come from your resources/translations/, resolved per player.
  • Guard with a flag: the handshake runs again on every plugin load, and each run would subscribe again.

4. Read the group's settings

public class MyFeatureConfig
{
    public int Amount { get; set; } = 100;
    public List<string>? Items { get; set; }
}

var config = _vipApi.GetFeatureValue<MyFeatureConfig>(player, FeatureKey);

Null when the player isn't VIP or the group lacks the key; the class's defaults for a plain value such as "vip.myfeature": 1. Read it when you use it, and edits apply on save.

Rule Why
Settings are properties Fields aren't bound: the operator's values never arrive
A list defaults to null or empty The binder appends to a list that isn't, so the operator could add to your defaults but never remove one

5. Apply the feature

private void OnPlayerLoaded(IPlayer player, string group) => Apply(player);

private void Apply(IPlayer player)
{
    if (_vipApi == null || !player.IsValid || player.IsFakeClient) return;
    if (_vipApi.GetPlayerFeatureState(player, FeatureKey) != FeatureState.Enabled) return;

    var config = _vipApi.GetFeatureValue<MyFeatureConfig>(player, FeatureKey);
    // ...
}
  • Check the state is Enabled: PlayerHasFeature is also true for a player who switched it off.
  • PlayerLoaded fires when a VIP joins and when their grants change; PlayerRemoved when they lose their last group or leave. OnPlayerSpawn fires once, when a player joins, before their grants load — hook EventPlayerSpawn for spawns.
  • Events arrive on the game thread. Hand game calls made from anywhere else to Core.Scheduler.NextTick.
  • To react when the player switches the perk, pass onSelectItem to RegisterFeature.

6. Offer choices on Customize

A Selectable feature lists its choices on VIPCore's Customize page instead of a menu of its own:

_vipApi.RegisterFeature(FeatureKey, FeatureType.Selectable, null,
    displayNameResolver: p => Core.Translation.GetPlayerLocalizer(p)["vip.myfeature"]);
_vipApi.RegisterFeatureOptions(FeatureKey,
    options: p => [new VipFeatureOption("90", "90", IsDefault: true), new VipFeatureOption("110", "110")],
    current: p => CurrentId(p),        // the Id in use, or null for the default
    choose: (p, id) => Apply(p, id));  // apply the pick, and save it

Save the pick yourself with SetPlayerCookie, under a key of your own such as vip.myfeature.value, and restore it from PlayerLoaded. VIP_Fov and VIP_Tag do this.


7. Add a page of your own

_vipApi.RegisterMenuPage(new VipMenuPage("myplugin.page", 10,
    Label: p => Core.Translation.GetPlayerLocalizer(p)["vip.myfeature.page"],
    IsAvailable: p => _vipApi.GetPlayerFeatureState(p, FeatureKey) == FeatureState.Enabled,
    Build: p => BuildPage(p)));
  • OpenMenu(player, "myplugin.page") opens it from your own command, under the VIP menu, so back returns there.
  • Build it with your own Core.Menu. Give its rows the contract's tags — VipSectionRow, VipOptionRow, VipActionRow — and a renderer that reads them draws it like VIPCore's pages. See Row tags.
  • VIPCore's own page ids are refused.

8. Clean up

public override void Unload()
{
    if (_vipApi == null) return;

    _vipApi.OnCoreReady -= RegisterVipFeatures;
    _vipApi.PlayerLoaded -= OnPlayerLoaded;
    if (_isFeatureRegistered)
    {
        _vipApi.UnregisterMenuPage("myplugin.page");
        _vipApi.UnregisterFeature(FeatureKey);
    }
}

A handler or a page left behind is a delegate into an unloaded plugin.


Checklist

  • VIPCore.Contract referenced with Private="false"
  • Acquired in UseSharedInterface, after HasSharedInterface
  • Registered from IsCoreReady() or OnCoreReady, once
  • Name and description from your own translations
  • Settings are properties, and lists default to null or empty
  • Applied only when the state is Enabled
  • Game calls on the game thread
  • Picks saved with SetPlayerCookie, under your own key
  • Everything unsubscribed and unregistered in Unload

Clone this wiki locally