Skip to content

Assets and Localization

This chapter covers two client-side systems: assets and localization. Assets let mods use files provided by an Anomaly server or placed in local override folders. Localization lets mods register translated strings under their own namespace.

  • The server advertises available assets when a client connects.
  • Anomaly downloads missing assets into a shared cache and verifies them.
  • Mods load assets through AnomalyResources, not by reading cache files directly.
  • Treat asset IDs as opaque values supplied by the server or your server companion plugin.
using Anomaly.Client.Api.Assets;
if (AnomalyResources.TryGetTexture(textureHash, out Texture2D tex))
{
myMaterial.mainTexture = tex;
}

TryGetTexture is lazy: the texture is created on first request and cached in memory afterward. It returns false if the asset is not available locally.

if (AnomalyResources.TryLoadFromBundle<GameObject>(bundleHash, "Prefabs/MySpawn", out var prefab))
{
Instantiate(prefab, parent);
}

Specify the type of asset inside the bundle, such as GameObject, Material, or AudioClip.

using Anomaly.Client.Api.Events.Handlers;
AssetEvents.Spawned += ev => HandleSpawn(ev.SpawnId, ev.Hash);
AssetEvents.Despawned += ev => HandleDespawn(ev.SpawnId);
AssetEvents.OverrideApplied += ev => ReapplyTexture(ev.NetId, ev.Hash);
AssetEvents.OverrideCleared += ev => ClearOverride(ev.NetId);
AssetEvents.Reloaded += ev => RebindTexture(ev.OldHash, ev.NewHash);

Reloaded fires when the server replaces an asset during a session. If your mod caches Unity object references, handle this event and rebind as needed.

Hook FileTransferEvents only if your mod needs custom progress UI:

FileTransferEvents.Progress += ev => Hud.SetProgress(ev.Ratio);
FileTransferEvents.Complete += ev => Hud.Hide();

Anomaly already provides default transfer feedback during join.

Common patterns:

  • Use a server companion plugin to announce which asset ID represents each feature.
  • Use Anomaly’s server asset registry to resolve a configured file path, then send that result to clients.
  • For fixed private servers, keep a shared config file or constants assembly in sync across client and server releases.

Prefer server-announced IDs for anything a server operator may customize.

Players can place supported local replacement files under:

<MelonLoader UserData>/Anomaly/Overrides/
├── AudioClip/
├── Texture2D/
├── Sprite/
└── Mesh/

Audio overrides are supported today. Texture and mesh override catalogs may include reserved paths for future support.

Local overrides are a player preference. Server-provided assets can still take priority when the server needs a consistent experience.

See the asset catalog pages for supported paths:

  • Every mod picks a namespace such as mymod or acme.wargames.
  • Reserved prefixes are anomaly. and launcher..
  • Translation keys start with your namespace, such as mymod.greeting.
  • Per-locale YAML files live under <MelonLoader UserData>/i18n/<namespace>/.
  • en.yaml is the required fallback source.

Anomaly’s own translations live under <MelonLoader UserData>/i18n/anomaly/. Mod translations live beside them under i18n/<namespace>/.

Use a flat YAML mapping. Quote keys so dots are treated as literal key text rather than nesting. Placeholders use string.Format positional syntax.

# {0} = username
"mymod.greeting": "Hello, {0}!"
"mymod.button.open_settings": "Open Settings"
# {0} = server name
"mymod.error.connection_lost": "Lost connection to server ({0})"

TextMeshPro rich text tags are allowed when your UI expects them.

using Anomaly.Client.Api.Localization;
public override void OnInitializeMelon()
{
// Loads <MelonLoader UserData>/i18n/mymod/
Tr.RegisterMod("mymod");
}

Use Tr.RegisterMod("mymod", customDirectory) only when your translations intentionally live outside the conventional UserData/i18n/<namespace>/ path.

Declare [assembly: MelonAdditionalDependencies("Anomaly")] so Anomaly’s localization service is ready before registration.

string greeting = Tr.Get("mymod.greeting", "Player1");
string btn = Tr.Get("mymod.button.open_settings");

Missing keys return the key itself and log a warning. Never ship with placeholder keys visible to players.

Anomaly chooses the active locale from:

  1. SCP:SL’s registry language setting.
  2. MelonPreferences category anomaly, entry locale.
  3. The system culture.
  4. English fallback.

The SCP:SL registry language is authoritative when present. Regional locale variants fall back to language-only files and then English. For example, pt_BR tries pt_BR.yaml, then pt.yaml, then en.yaml.

  • Lowercase letters, digits, underscores, and dots.
  • Must start with a letter.
  • Must not be anomaly / launcher or start with anomaly. / launcher..
  • One namespace per mod.
  • Cross-namespace reads are allowed, but only the namespace owner should write files for that prefix.

Anomaly validates loaded keys against the registered namespace. A file under i18n/mymod/ that contains othermod.key is rejected. Duplicate or colliding translations log warnings before overwrite.

  • Missing-key warnings: add the key to en.yaml.
  • Key renders as literal text: registration failed or the file was not found.
  • Wrong language: confirm SCP:SL’s in-game language and any Anomaly MelonPreferences locale override.
  • Placeholders render incorrectly: match Tr.Get(...) arguments to the placeholders in the string.

Packaging and MelonGameVersion.