HUD Output and Player State
This chapter covers the helpers that show feedback on the local player’s HUD (LocalHint, LocalBroadcast, CameraShake, Hud) and the player-state readouts that mods most often pair with them (Player.MovementState / IsCrouching / etc., PlayerStatsWrapper.Stamina, Round).
All four output helpers follow the same convention: static class, bool-returning entry points, no per-call allocations beyond what the underlying game systems require, and silent dropping of calls when the prerequisite singleton isn’t yet initialised. Mix them with direct IL2CPP access if you need finer control — none of them install patches or hooks.
LocalHint
Section titled “LocalHint”Anomaly.Client.Api.Features.LocalHint mirrors the surface of LabAPI’s server-side Player.SendHint but routes through the client’s HintDisplay.ShowLocal(Hint) — no server round-trip. Use it for mod menu prompts, custom keybind hints, error messages, and debug overlays.
using Anomaly.Client.Api.Features;using Il2CppHints;
// Plain text, default 3-second durationLocalHint.Show("Pickup detected.");
// Full hint construction with parameters and effectsLocalHint.Show( "Welcome, %nick%", parameters: new HintParameter[] { new StringHintParameter("nick", LocalPlayer.Instance.Nickname) }, effects: HintEffectPresets.FadeInAndOut(0.3f, 0.3f), durationSeconds: 5f);API:
public static bool Show(string text, float durationSeconds = 3f);public static bool Show(string text, HintParameter[] parameters, HintEffect[] effects, float durationSeconds = 3f);public static bool Show(Hint hint);public static bool Push(Hint hint);public static bool Pop();Push / Pop is a small stack you can use to layer a temporary hint over the active one — push the temporary hint, then pop to restore.
Show returns false when LocalPlayer.Instance == null (e.g. before ClientEvents.Ready) — the hint is silently dropped rather than thrown. Subscribe to hints from ClientEvents.Ready, not ClientEvents.Connected.
LocalBroadcast
Section titled “LocalBroadcast”Anomaly.Client.Api.Features.LocalBroadcast drives the client’s local broadcast queue directly via Broadcast.AddElement(...) — visually identical to a server broadcast, but no server cooperation.
using Anomaly.Client.Api.Features;
LocalBroadcast.Show("Round summary loaded.", durationSeconds: 8);LocalBroadcast.Show("[Admin] Server restart in 30s", durationSeconds: 30, LocalBroadcast.Flags.AdminChat);
// Drop everything currently queuedLocalBroadcast.Clear();API:
[System.Flags]public enum Flags : byte{ Normal = 0, Truncated = 1, AdminChat = 2,}
public static bool Show(string text, ushort durationSeconds, Flags flags = Flags.Normal);public static bool Clear();Truncated and AdminChat mirror the engine’s Broadcast.BroadcastFlags and produce the same visual treatment as server-driven broadcasts. Duration units are seconds, capped at 65535 by the underlying ushort.
CameraShake
Section titled “CameraShake”Anomaly.Client.Api.Features.CameraShake wraps CameraShakeController.Singleton so mods don’t reach into the IL2CPP-mapped class directly.
using Anomaly.Client.Api.Features;using Il2CppCameraShaking;
CameraShake.Apply(new RecoilShake(strength: 0.3f, duration: 0.15f));API:
public static bool IsAvailable { get; }public static bool Apply(IShakeEffect effect);Built-in effects in Il2CppCameraShaking (passable directly to Apply):
| Type | What it looks like |
|---|---|
LookatShake | Snap-toward then drift back — used internally for hit reactions |
RecoilShake | Short impulse — closest to a generic “small shake” |
TrackerShake | Subtle continuous noise — used for environmental tension |
ElevatorShake | Low-frequency vertical bob — used for elevator motion |
HeadbobShake | Periodic bob locked to footsteps |
Effects are reference-tracked by the controller and removed when their internal timer elapses — the helper is stateless, no removal call needed. Implement IShakeEffect to define a custom shake.
IsAvailable is true once the controller singleton is initialised; mods that fire shakes from ClientEvents.Ready or later can usually skip the check.
Hud (visibility)
Section titled “Hud (visibility)”Anomaly.Client.Api.Features.Hud controls local HUD visibility — globally or per-element. It exposes two complementary models intentionally kept distinct:
- Direct toggle (
SetVisible/SetElementVisible) — last call wins. Doesn’t compose across mods. - Push-based (
PushHide/PushHideElement) — returns anIDisposable; the element stays hidden as long as any handle is alive across all mods. Recommended for cinematic / screenshot / modal-overlay mods.
using Anomaly.Client.Api.Features;
// Cinematic tools: hide the entire HUD while the user is in photo mode.using (Hud.PushHide()){ yield return TakeScreenshot();}// HUD restored automatically on dispose.
// Long-lived: hold the handle as a field, dispose when the cinematic ends.private IDisposable _hudHide;
void OnCinematicStart() => _hudHide = Hud.PushHideElement(Hud.Element.Inventory);void OnCinematicEnd() => _hudHide?.Dispose();
// Direct toggle — useful for one-shot debugHud.SetVisible(false);Hud.SetElementVisible(Hud.Element.Console, true);API:
public enum Element { All, Inventory, Hints, Broadcasts, Console }
public static bool IsVisible { get; }public static bool SetVisible(bool visible);public static bool IsElementVisible(Element element);public static bool SetElementVisible(Element element, bool visible);public static IDisposable PushHide();public static IDisposable PushHideElement(Element element);Targeting:
| Element | Targets |
|---|---|
All | HideHUDController._showHUDElements — same path as the engine’s F2 screenshot toggle |
Inventory | InventoryGuiController.Singleton.gameObject — radial inventory + ammo display |
Hints | The local ReferenceHub’s HintDisplay.gameObject |
Broadcasts | Broadcast.Singleton.gameObject |
Console | Il2CppGameCore.Console.Singleton.gameObject (F1 dev console) |
The two models combine deterministically. Effective visibility is !(pushCounter > 0 || manualHide) — element is hidden if either the push stack or the direct toggle says hide. They don’t override each other.
Push handles are released automatically on ClientEvents.Disconnected. Don’t assume a handle survives a session boundary — the framework calls Hud.ResetAll() on disconnect.
Player motor state
Section titled “Player motor state”Anomaly.Client.Api.Features.Player exposes convenience properties that wrap FirstPersonMovementModule and the player’s room. All are null-safe — they return sensible defaults when the player has no FPC module (SCP-079, dead, spectator).
using Anomaly.Client.Api.Features;using PlayerRoles.FirstPersonControl;
if (player.MovementState == PlayerMovementState.Sprinting && player.IsGrounded) DoFootstep();
if (player.IsCrouching) HideHostileIndicator();
var zone = player.Zone; // FacilityZone.LightContainment, HeavyContainment, etc.API additions on Player:
public PlayerMovementState MovementState { get; }public bool IsCrouching { get; } // Crouching or Sneakingpublic bool IsSprinting { get; } // exact matchpublic bool IsWalking { get; } // exact matchpublic bool IsGrounded { get; }public FacilityZone Zone { get; } // FacilityZone.None when not in any tracked roomMovementState returns Walking as a safe default for non-FPC roles. MovementState is replicated via the engine, so reading from a remote Player is valid — these aren’t local-only.
Zone is a convenience alias for Room.Zone and returns FacilityZone.None when the player isn’t in any tracked room.
Stamina exposure
Section titled “Stamina exposure”Anomaly.Client.Api.Features.PlayerStatsWrapper adds Stamina and MaxStamina alongside the existing Health / MaxHealth / HumeShield / ArtificialHealth / etc.
var stats = player.Stats;float fraction = stats.Stamina / stats.MaxStamina;public float Stamina { get; }public float MaxStamina { get; }StaminaStat is replicated by the engine’s PlayerStats infrastructure, so these are valid for remote players too. MaxStamina is role-dependent.
The StatsEvents handler also gains StaminaChanged and HumeShieldChanged — both fire at game-tick resolution with Previous / Current / Delta. HumeShieldChanged.Broke flags the broken-zero transition. See Client API Tour — Events.
Round state
Section titled “Round state”Anomaly.Client.Api.Features.Round is a read-only static facade over the round’s lifecycle phase. Backed by the same RoundEvents.CurrentPhase field that feeds the event-based path — Round.* is the poll-style accessor.
using Anomaly.Client.Api.Features;
if (Round.IsLobby) ShowLobbyHud();else if (Round.IsInProgress) ShowGameplayHud();else if (Round.IsEnded) ShowEndScreen();API:
public static RoundPhase CurrentPhase { get; }public static bool IsLobby { get; } // WaitingForPlayerspublic static bool IsStarted { get; } // Starting through Endingpublic static bool IsInProgress { get; } // strictly the gameplay phasepublic static bool IsEnded { get; } // Ending or Endedpublic static bool IsRestarting { get; } // single-tick Restarted phaseThere is no Duration or StartTime. SCP:SL’s server only broadcasts phase changes, not absolute timestamps. If you need an approximate round-elapsed counter, track your own timer driven by RoundEvents.Started.
Camera resolution
Section titled “Camera resolution”Anomaly.Client.Api.Features.CameraManager resolves the active main camera with a multi-step fallback chain and caches the result. Use it instead of GameObject.Find("Main Camera Controller/...") on every access.
using Anomaly.Client.Api.Features;using UnityEngine;
var cam = CameraManager.Main; // cheap to call in hot loops; cachedif (cam != null) cam.fieldOfView = MyFov;
// Inject your own resolver if you swap the rig (third-person cam, free-cam, photo mode).// Returns IDisposable — disposing both deregisters and invalidates the cache.var fallback = CameraManager.RegisterFallback("mymod.thirdperson", () => myThirdPersonCamera);
// Notification when the rig swaps — re-attach overlays / VFX here.CameraManager.Changed += newCamera => ReattachOverlays(newCamera);API:
public static event Action<Camera> Changed;public static Camera Main { get; }public static void Invalidate();public static IDisposable RegisterFallback(string modId, Func<Camera> resolver);Resolution chain (first non-null wins): cached → MainCameraController.CurrentCamera → user-registered fallbacks (in registration order) → Camera.main → active scene root walk.
Auto-invalidates on (a) local-player role change, (b) scene load, and (c) the cached reference becoming null. The framework also calls CameraManager.Reset() on disconnect to drop fallbacks and the cache.
LocalPlayer.Camera delegates to CameraManager.Main; both are stable surfaces.
Map Features for facility-state wrappers and item / pickup / door subtypes.