A Godot 4 (C#) editor plugin that generates strongly-typed, compile-time-safe
StringName constants for your files — no more typo-prone magic strings for
asset paths, sound names, or resource keys.
Point it at a folder, and Anvil generates a nested static class full of constants, one per file, named after the file itself.
Instead of this:
GD.Print("main_theme"); // typo-prone, no autocomplete, breaks silentlyYou get this:
GD.Print(Audio.Music.MainTheme); // compile-time checked, autocompletesAnvil can generate constants in one of two shapes, controlled by ForgeMode.
Picking the right one for each folder matters — they solve different
problems:
| Mode | Constant holds | Use it for |
|---|---|---|
ForgeMode.Id (default) |
The file's short name, e.g. "main_theme" |
ID-based systems: audio bus names, animation names, save-data keys, PropertyHint.Enum dropdowns — anywhere you compare or store a name rather than load a file |
ForgeMode.FullPath |
The full loadable path, e.g. "res://audio/music/main_theme.ogg" |
Directly loading a resource: GD.Load<T>(...), [Export] preload paths — anywhere the string needs to point straight at the file |
If you're not sure which you need: if you're about to call GD.Load,
you want FullPath. If you're about to compare, store, or display a name,
you want Id.
- Copy the
addons/Anvilfolder into your project'saddons/directory. - In Godot, go to Project → Project Settings → Plugins and enable Anvil.
using Anvil;
public partial class Audio
{
// Id mode (default) — short names, for ID-based lookups
[Forge("res://audio/music")]
public static partial class Music
{
}
// FullPath mode — full res:// paths, ready to GD.Load directly
[Forge("res://audio/sfx", mode: ForgeMode.FullPath)]
public static partial class Sfx
{
}
}- The outer class (
Audio) must bepartial. - The inner class (
Music,Sfx) must bestatic partialand carry the[Forge]attribute. - The path passed to
[Forge]is ares://folder — every valid file inside it becomes a constant. mode(optional, defaultForgeMode.Id) — see the table above. Can be passed positionally ([Forge("res://audio/sfx", ForgeMode.FullPath)]) or named (mode: ForgeMode.FullPath).recursive(optional, defaultfalse) — whentrue, subfolders are scanned too and all files are flattened into the same class. If two files in different subfolders share a name, the first one found wins and the rest are skipped with a console warning naming the conflicting file. Works with either mode, in any argument order:[Forge("res://audio/sfx", recursive: true, mode: ForgeMode.FullPath)].
In the Godot editor, go to Project → Tools → Generate Anvil IDs.
Anvil scans your project for every [Forge] attribute, and for each one
writes a generated file at:
res://addons/Anvil/Generated/<OuterClassName>.anvilgen.cs
Re-running generation clears out old generated files first, so it's always safe to run again after adding or removing assets.
public partial class Audio
{
public static partial class Music
{
public static readonly StringName MainTheme = "main_theme";
public static readonly StringName BossFight = "boss_fight";
public const string EnumNames = "main_theme,boss_fight";
}
}These constants hold a name, not a path — use them wherever your code compares, stores, or looks something up by that name:
if (currentTrackName == Audio.Music.MainTheme) { /* ... */ }
saveData.LastPlayedTrack = Audio.Music.BossFight;Id mode also generates an EnumNames const: a comma-joined string of every
file name in that folder, ready to drop straight into PropertyHint.Enum:
[Export(PropertyHint.Enum, Audio.Music.EnumNames)]
public string SelectedTrack;This gives you an inspector dropdown of every file in the folder, kept in
sync automatically every time you regenerate. A file name containing a comma
is excluded from EnumNames (it would corrupt the hint string) but still
gets its own constant; a warning is printed naming the file.
public partial class Audio
{
public static partial class Sfx
{
public static readonly StringName Digging = "res://audio/sfx/digging.ogg";
public static readonly StringName Planted = "res://audio/sfx/planted.ogg";
}
}These constants hold a complete, loadable path — pass them straight into
GD.Load, no string-building required:
audioPlayer.Stream = GD.Load<AudioStream>(Audio.Sfx.Digging);There's no EnumNames in this mode — a full path isn't a meaningful value
for an inspector dropdown, so it's skipped.
By default, Anvil picks up these extensions:
.wav .ogg .mp3 .tscn .tres .glb .blend .fbx
You can change this list in Project Settings under:
anvil/generator/valid_extensions
Any file in a [Forge]-marked folder whose extension isn't in this list is
ignored. .import files are always ignored.
- Generated files live under
res://addons/Anvil/Generated/and are marked with a.gdignore, so Godot won't try to import them as resources. - Generated files are overwritten on every run — don't hand-edit them.
- File names are converted to PascalCase for the generated constant name
(e.g.
main-theme.ogg→MainTheme). - With
recursive: true, all matched files across every subfolder are flattened into one class. Duplicate names across subfolders are not both included — the first match wins, and later duplicates are skipped with a console warning. modeonly changes what a constant holds (a name vs. a full path) — it doesn't change how files are discovered, sorecursive, extension filtering, and duplicate handling all work the same way regardless of mode.
MIT