-
Notifications
You must be signed in to change notification settings - Fork 3
Mod Integration Examples
These examples show API 1.1 behavior that works today. Role and modifier checks such as GetRole<T>, GetModifier<T>, and MyRoles.* are placeholders for your own mod's compile-time API.
All examples share one registration class. Call Register() only after the literal plugin-id presence check shown in Mod Integration. Keep only the recipe methods you need:
using PerfectComms.Api;
using UnityEngine;
internal static class PerfectCommsVoiceIntegration
{
private const string Mod = "com.me.mymod";
internal static void Register()
{
PerfectCommsApi.RegisterModTab(Mod, "My Mod");
RegisterBlackmailer();
RegisterJailor();
RegisterPuppeteer();
RegisterMedium();
RegisterHiddenVoice();
RegisterHackerJam();
RegisterOverlayPrivacy();
}
internal static void Unregister()
=> PerfectCommsApi.Unregister(Mod);
// Paste the selected recipe methods below into this class.
}Call Unregister() before a dynamic unload or reload. Normal process exit needs no special cleanup.
Back to Mod Integration
Behavior: a currently blackmailed living player cannot talk during a meeting. A separate host option can keep the player muted during the following task round.
Primitive: Gate.
private static void RegisterBlackmailer()
{
PerfectCommsApi.RegisterHostOption(Mod,
new VoiceHostOption(
"MuteBlackmailed",
"Blackmailer: Mute in Meetings",
true));
PerfectCommsApi.RegisterHostOption(Mod,
new VoiceHostOption(
"MuteBlackmailedNextRound",
"Blackmailer: Mute Next Round",
false));
PerfectCommsApi.RegisterVoiceRule(Mod, ctx =>
{
if (ctx.IsDead)
return VoiceRuleResult.Pass;
// Current meeting state and persisted next-round state are separate.
// Do not put a current-modifier guard in front of both branches.
if (ctx.Phase == VoicePhaseKind.Meeting &&
ctx.GetOption("MuteBlackmailed") &&
ctx.Player.GetModifier<BlackmailedModifier>() != null)
{
return VoiceRuleResult.Mute("Blackmailed");
}
if (ctx.Phase == VoicePhaseKind.Tasks &&
ctx.GetOption("MuteBlackmailedNextRound") &&
MyRoles.WasBlackmailedLastMeeting(ctx.Player))
{
return VoiceRuleResult.Mute("Blackmailed");
}
return VoiceRuleResult.Pass;
});
}WasBlackmailedLastMeeting is your bookkeeping. Record the affected player when the meeting ends, then clear the state at the boundary your design requires.
Behavior: a jailed living player is muted in meetings until the Jailor's already-synchronized state allows them to speak.
Primitive: Gate.
private static void RegisterJailor()
{
PerfectCommsApi.RegisterHostOption(Mod,
new VoiceHostOption(
"MuteJailed",
"Jailor: Mute Jailee in Meetings",
true));
PerfectCommsApi.RegisterVoiceRule(Mod, ctx =>
{
if (ctx.Phase != VoicePhaseKind.Meeting ||
ctx.IsDead ||
!ctx.GetOption("MuteJailed"))
{
return VoiceRuleResult.Pass;
}
if (ctx.Player.GetModifier<JailedModifier>() == null)
return VoiceRuleResult.Pass;
return MyRoles.JailorAllowedVoice(ctx.Player)
? VoiceRuleResult.Pass
: VoiceRuleResult.Mute("Jailed");
});
}The Jailor decision is your mod's networked state. Perfect Comms does not synchronize it.
Behavior: during Tasks, a controlled living victim is muted and the local Puppeteer hears from the victim's position instead of their own.
Primitives: Gate and Listener Origin.
private static void RegisterPuppeteer()
{
PerfectCommsApi.RegisterHostOption(Mod,
new VoiceHostOption(
"MutePuppeteered",
"Puppeteer: Mute Controlled Victim",
true));
PerfectCommsApi.RegisterVoiceRule(Mod, ctx =>
ctx.Phase == VoicePhaseKind.Tasks &&
!ctx.IsDead &&
ctx.GetOption("MutePuppeteered") &&
ctx.Player.GetModifier<PuppeteerControlModifier>() != null
? VoiceRuleResult.Mute("Controlled")
: VoiceRuleResult.Pass);
PerfectCommsApi.RegisterListenerOrigin(Mod, local =>
{
// This state must become null outside the active task-phase control.
var victim = MyRoles.VictimControlledBy(local);
if (victim == null)
return null;
// Supply a real positive world-unit radius when vision should limit hearing.
float radius = MyRoles.HearingRadiusAt(victim);
return new VoiceListenerResult(
(Vector2)victim.transform.position,
radius,
VoiceListenerMode.Replace);
});
}Listener-origin callbacks receive only the local PlayerControl; they do not receive a VoiceRuleContext and cannot call GetOption. This example therefore ties relocation directly to your synchronized control state instead of registering an option it cannot read. API 1.1 also does not treat LightRadius: -1 as inheritance: any non-positive value disables the vision-radius limit.
Use VoiceListenerMode.Additive when the local player should hear from both their own body and the supplied origin.
Behavior: during Tasks, a living Medium and dead players share a private, full-volume, two-way radio route. The host can turn the route Off or set it to Both.
Primitive: Channel.
private static void RegisterMedium()
{
PerfectCommsApi.RegisterHostEnumOption(Mod,
new VoiceHostEnumOption(
"MediumVoice",
"Medium: Ghost Voice",
Default: 1,
Choices: new[] { "Off", "Both" }));
PerfectCommsApi.RegisterVoiceChannel(Mod, ctx =>
{
if (ctx.Phase != VoicePhaseKind.Tasks ||
ctx.GetEnumOption("MediumVoice") != 1)
{
return null;
}
bool isLivingMedium =
!ctx.IsDead &&
ctx.Player.GetRole<MediumRole>() != null;
bool isGhost = ctx.IsDead;
if (!isLivingMedium && !isGhost)
return null;
// Use a per-Medium key if your mod permits multiple simultaneous Mediums.
return new VoiceChannelResult(
"medium-seance",
TwoWay: true,
Shape: VoiceAudioShape.Radio);
});
}API 1.1 does not provide dependable one-way routing with TwoWay: false, so this recipe intentionally exposes only Off and Both. A Proximity-shaped channel is spatial only when it also supplies an explicit Origin and is evaluated with a task listener position; otherwise it is flat.
Behavior: a living hidden role cannot be heard during Tasks.
Primitive: Gate.
private static void RegisterHiddenVoice()
{
PerfectCommsApi.RegisterHostOption(Mod,
new VoiceHostOption(
"MuteHidden",
"Hidden Roles: Mute While Hidden",
true));
PerfectCommsApi.RegisterVoiceRule(Mod, ctx =>
{
if (ctx.Phase != VoicePhaseKind.Tasks ||
ctx.IsDead ||
!ctx.GetOption("MuteHidden"))
{
return VoiceRuleResult.Pass;
}
bool hidden =
ctx.Player.GetModifier<SwoopModifier>() != null ||
ctx.Player.GetModifier<VanishModifier>() != null;
return hidden
? VoiceRuleResult.Mute("Hidden")
: VoiceRuleResult.Pass;
});
}Behavior: living speakers are muted while a synchronized system effect is active in Tasks or Meetings.
Primitive: Gate, using phase-scoped global gates.
private static void RegisterHackerJam()
{
PerfectCommsApi.RegisterGlobalGate(
Mod,
VoicePhaseKind.Tasks,
() => MySystems.JamActive,
"Jammed");
PerfectCommsApi.RegisterGlobalGate(
Mod,
VoicePhaseKind.Meeting,
() => MySystems.JamActive,
"Jammed");
}The predicate must be cheap and must read state your mod already synchronizes.
Behavior: when your role system says a speaker's identity is hidden, suppress that speaker's identity-bearing voice indicator.
Primitive: Overlay Privacy.
private static void RegisterOverlayPrivacy()
{
PerfectCommsApi.RegisterHostOption(Mod,
new VoiceHostOption(
"HideHiddenVoiceIndicators",
"Hidden Roles: Hide Voice Indicators",
true));
PerfectCommsApi.RegisterOverlaySpeakerRule(Mod, ctx =>
ctx.GetOption("HideHiddenVoiceIndicators") &&
MyRoles.IsVoiceIdentityHidden(ctx.Speaker)
? VoiceOverlaySpeakerResult.HideSource
: VoiceOverlaySpeakerResult.Pass);
}Overlay-privacy callbacks compose restrictively and fail private if they throw or return invalid data. Keep the predicate cheap and deterministic.
- Role and modifier checks are your mod's API; Perfect Comms does not discover or synchronize them.
- Audio callbacks run at about 20 Hz per player. Overlay callbacks run at most once per rendered frame.
- Return
Passornullwhenever your behavior does not apply. - Audio callback exceptions fall back to a neutral result. Overlay privacy intentionally fails private.
- Clients converge only when they have compatible integration code and your underlying role state is already synchronized.
- Use unique option keys within your mod and register its tab once.
- API Reference - every type and method.
- Gate - supported mute behavior.
- Channels - working two-way custom routes.
- Listener Origin - task-phase hearing relocation.
- Host Options & Tabs - synchronized controls.
- Overlay Privacy - identity-bearing indicator policy.