Repository navigation
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.
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.
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.
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, startingvip.. - 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.
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 |
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:PlayerHasFeatureis also true for a player who switched it off. -
PlayerLoadedfires when a VIP joins and when their grants change;PlayerRemovedwhen they lose their last group or leave.OnPlayerSpawnfires once, when a player joins, before their grants load — hookEventPlayerSpawnfor 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
onSelectItemtoRegisterFeature.
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 itSave 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.
_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.
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.
-
VIPCore.Contractreferenced withPrivate="false" - Acquired in
UseSharedInterface, afterHasSharedInterface - Registered from
IsCoreReady()orOnCoreReady, once - Name and description from your own translations
- Settings are properties, and lists default to
nullor 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