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.
Assets
Section titled “Assets”The model
Section titled “The model”- 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.
Loading a texture
Section titled “Loading a texture”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.
Loading from an asset bundle
Section titled “Loading from an asset bundle”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.
Reacting to asset lifecycle
Section titled “Reacting to asset lifecycle”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.
File transfer progress
Section titled “File transfer progress”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.
How asset IDs get to you
Section titled “How asset IDs get to you”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.
Local overrides
Section titled “Local overrides”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:
Localization
Section titled “Localization”The model
Section titled “The model”- Every mod picks a namespace such as
mymodoracme.wargames. - Reserved prefixes are
anomaly.andlauncher.. - Translation keys start with your namespace, such as
mymod.greeting. - Per-locale YAML files live under
<MelonLoader UserData>/i18n/<namespace>/. en.yamlis the required fallback source.
Anomaly’s own translations live under <MelonLoader UserData>/i18n/anomaly/. Mod translations live beside them under i18n/<namespace>/.
File format
Section titled “File format”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.
Registering
Section titled “Registering”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.
Looking up a string
Section titled “Looking up a string”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.
Locale resolution
Section titled “Locale resolution”Anomaly chooses the active locale from:
- SCP:SL’s registry language setting.
- MelonPreferences category
anomaly, entrylocale. - The system culture.
- 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.
Namespace rules
Section titled “Namespace rules”- Lowercase letters, digits, underscores, and dots.
- Must start with a letter.
- Must not be
anomaly/launcheror start withanomaly./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.
Troubleshooting
Section titled “Troubleshooting”- 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.