Skip to content

Mod Integration

artriy edited this page Jul 17, 2026 · 7 revisions

Mod Integration

Perfect Comms API 1.1 exposes a small public surface (PerfectComms.Api) so an Among Us role mod can add voice rules, private channels, relocated hearing, host settings, and overlay privacy without forking Perfect Comms.

Your mod references PerfectComms.dll as a soft dependency, registers only when Perfect Comms is present, and ships its own DLL. Perfect Comms never references your mod.


Why this exists

Before this API, adding a role-specific voice behavior meant editing the Perfect Comms role engine, snapshots, settings RPC, and host panel, then maintaining a fork. The public API keeps that logic in the role mod that owns the state.

Callbacks run locally on each client. That avoids a second network protocol only when the underlying role or modifier state is already synchronized by your mod and every relevant client has a compatible integration. Host-option values are the exception: Perfect Comms includes them in its existing host-settings snapshot. The host-object checks are for lobby compatibility, not hostile-client authentication; see Privacy.


30-second setup

1. Reference the DLL. Add PerfectComms.dll as a compile-time assembly reference. Do not bundle it with your mod; players install Perfect Comms separately.

2. Declare a soft dependency and isolate all API use. Keep Perfect Comms types out of your plugin's base type, fields, method signatures, and static initializers. Check the literal plugin id before entering a helper that touches the API:

using BepInEx;
using BepInEx.Unity.IL2CPP;

[BepInPlugin("com.me.mymod", "My Mod", "1.0.0")]
[BepInDependency(
    "com.edgetel.perfectcomms",
    BepInDependency.DependencyFlags.SoftDependency)]
public sealed class MyModPlugin : BasePlugin
{
    private const string PerfectCommsPluginId = "com.edgetel.perfectcomms";

    public override void Load()
    {
        if (!IL2CPPChainloader.Instance.Plugins.ContainsKey(PerfectCommsPluginId))
            return;

        PerfectCommsVoiceIntegration.Register();
    }
}

internal static class PerfectCommsVoiceIntegration
{
    private const string ModId = "com.me.mymod";

    internal static void Register()
    {
        // This method is entered only after the literal-id presence check.
        PerfectComms.Api.PerfectCommsApi.RegisterVoiceRule(ModId, ctx =>
            ctx.Phase == PerfectComms.Api.VoicePhaseKind.Meeting &&
            !ctx.IsDead &&
            MyRoles.IsGagged(ctx.Player)
                ? PerfectComms.Api.VoiceRuleResult.Mute("Gagged")
                : PerfectComms.Api.VoiceRuleResult.Pass);
    }

    // Call this before a dynamic unload or reload, if your mod supports one.
    internal static void Unregister()
        => PerfectComms.Api.PerfectCommsApi.Unregister(ModId);
}

The isolation matters: keep the presence probe in code that has no Perfect Comms API calls or types, then enter the integration helper only when the plugin is present. A soft dependency controls load ordering; it does not make an API reference in an eagerly resolved method or type safe. Using the literal id also keeps the probe independent of the optional API assembly.

The example targets API 1.1. A presence check does not prove that every method in the version you compiled against exists at runtime, so document and test your minimum supported Perfect Comms version.


The six primitives

Primitive What it currently does Networking
Gate Mutes a living player in Tasks, Meeting, or Exile None
Channel Gives matching players a working two-way private route with a selected audio shape None
Listener Origin Replaces or augments where the local player hears from during Tasks None
Host Options Adds declarative host booleans/enums and syncs their values Automatic
Mod Tab Gives your settings a tab in the host panel None
Overlay Privacy Dims, hides, or aliases identity-bearing voice indicators None

Ground rules

  • Callbacks must be cheap and throw-free. Audio-routing callbacks run at snapshot cadence, about 20 times per second per player. Overlay-privacy callbacks run at most once per rendered frame.
  • Audio callback failures are neutral. An exception becomes Pass, null, or false, so the failed callback adds no restriction. Overlay privacy is intentionally different: invalid or throwing privacy callbacks fail private.
  • Avoid hot-path allocations where practical.
  • Return Pass or null when your role does not apply. This lets built-in rules and other integrations decide.
  • Channel keys are namespaced by mod id internally, preventing cross-mod collisions.
  • Keep every key unique within your own mod. This includes host-option keys across boolean and enum options.
  • Unregister before dynamic reload/unload. Re-registering without PerfectCommsApi.Unregister(ModId) accumulates callbacks and settings.
  • Local evaluation is not state synchronization. Clients converge only when your mod already gives them the same relevant role state and compatible integration code.

Known limitations in API 1.1

  • Per-speaker VoiceRuleResult.Muffle is not routed. The verdict is stored, but it does not currently apply an audible low-pass effect. Use RegisterListenerFilter to muffle everything heard by the local listener, or a matching channel with VoiceAudioShape.Muffle.
  • TwoWay: false is not dependable directional routing. Use TwoWay: true; do not publish one-way channel behavior against API 1.1.
  • Proximity channels need an explicit origin. Distance falloff requires Shape: VoiceAudioShape.Proximity, a non-null Origin, and a task-phase listener position. Otherwise the route is flat volume.
  • LightRadius: -1 does not inherit the local player's radius. Any value less than or equal to zero disables the listener-origin vision limit. Pass a positive radius when you need that limit.
  • Gate mute has phase and life-state boundaries. It is enforced for living speakers in Tasks, Meeting, and Exile. It is not enforced in Lobby or for dead speakers.

Next

Clone this wiki locally