Skip to content

Assets and Localization

Authenticated remote manifests accept only:

  • PNG, used by custom-item icons and supported texture consumers.
  • OGG, used by supported audio consumers.

Each file is content-addressed; the transfer manifest permits 64 MiB per file and 256 MiB in aggregate. Decode admission is stricter: PNG is capped at 16 MiB encoded and OGG at 32 MiB encoded, with dimension, pixel, channel, sample-rate, duration, decoded-memory, and aggregate-memory checks. The client requests only approved missing blobs and exposes successfully verified resources through Anomaly.Client.Api.Assets.AnomalyResources.TryGetTexture and .TryGetAudioClip.

Those resource lookups fail closed unless the active connection has completed the approved manifest containing that exact hash and matching asset kind. A hash present in the shared cache from another server or catalog is not sufficient authorization. Supported server features apply the same per-recipient manifest gate before sending an override or vocabulary reference.

Call resource lookups on the Unity main thread. Returned textures and clips are shared, borrowed objects for the active connection: do not destroy them or retain them across teardown. Custom-item materialization resolves its icon automatically. Use these lookups for authorized resources rather than reading the shared cache.

if (AnomalyResources.TryGetAudioClip(audioHash, out var clip))
{
audioSource.clip = clip;
audioSource.Play();
}

Anomaly.Client.Api.Assets.AssetEvents exposes OverrideApplied and OverrideCleared with AssetOverrideAppliedEventArgs and AssetOverrideClearedEventArgs from Anomaly.Client.Api.Events.Args. Remove event subscriptions when your feature shuts down.

Anomaly.Server.Api.Assets.AssetRegistry reads the advertised catalog: GetHashByPath resolves a relative path, TryGetKind and TryGetRelativePath inspect a hash, and All enumerates path/hash/kind entries. Files live under the operator’s configured AssetDirectory; Anomaly owns scanning and catalog lifecycle.

AssetOverrides.SetTexture(netId, componentPath, materialIdx, textureHash) targets a material on an existing network object. The hash must identify a PNG in the current advertised manifest. SetPlayerTexture, SetRoomTexture, and SetDoorTexture accept native LabAPI wrappers; rooms and doors require a network identity. Call these APIs on the server game thread.

Overrides are limited to 32 addresses per object and 1,024 in total. Invalid addresses, materials, hashes, or capacity are rejected before state changes. Each recipient must have approved the exact asset; overrides are replayed after approved transfer completion. ClearTexture restores one address; ClearAll, ClearPlayer, ClearRoom, and ClearDoor restore the relevant object’s textures. Round restart clears server override state.

The selected material’s shader must expose a main texture property, _BaseColorMap, _BaseMap, or _MainTex. Unsupported shaders are skipped. Clearing restores the original texture on the same material property.

Anomaly.Server.Api.Features.Announcer.AddWord registers a CASSIE vocabulary entry using an advertised OGG path or SHA-256 hash, its duration, and an optional CassieClipCategory. Names are normalized to uppercase and cannot contain whitespace; use a unique name that does not collide with stock vocabulary. The catalog accepts at most 128 words, names of at most 128 UTF-8 bytes, and finite durations greater than zero and at most 30 seconds. Announcer.Words exposes registered entries. Use normal game or LabAPI announcement playback; recipients receive a word only after approving its exact OGG resource.

The generated audio catalog documents player-local CASSIE audio paths. CASSIE overrides accept the documented OGG and WAV forms and are scanned at startup. Server texture changes use explicit network-object addresses as described above.

Remote policy and local override policy are different: the remote allowlist remains PNG and OGG only.

Each mod owns a lowercase dotted namespace. Put flat YAML files in:

<MelonLoader UserData>/i18n/<modNamespace>/
+-- en.yaml
+-- fr.yaml
+-- pt_BR.yaml

en.yaml is the required fallback catalog. Registration fails atomically when it is missing or invalid; active and regional locale files are optional.

Tr.RegisterMod(modNamespace, directoryPath) accepts an explicit locale directory. The anomaly and launcher namespace prefixes are reserved. Every key must begin with the registered namespace:

"mymod.greeting": "Hello, {0}!"
"mymod.settings.enabled": "Enabled"

Register during initialization and check the return value. false means the namespace could not be registered:

using Anomaly.Client.Api.Localization;
if (!Tr.RegisterMod("mymod"))
MelonLoader.MelonLogger.Warning("MyMod translations could not be registered.");
string text = Tr.Get("mymod.greeting", playerName);

The active locale is resolved in this order:

  1. SCP:SL registry language.
  2. Anomaly’s MelonPreferences locale override.
  3. System culture.
  4. English.

For a regional locale such as pt_BR, Anomaly tries pt_BR.yaml, then pt.yaml, then the registered English fallback. A missing key returns the key. Mod registration stages all translations first: invalid YAML, an out-of-namespace or null entry, or a collision with an already registered key rejects the whole registration without publishing partial entries or claiming the namespace.

Translation values support positional string.Format placeholders. Keep placeholder counts and types consistent across locales.

Continue to Packaging and Compatibility.