Note
This is my attempt to gather the key takeaways from all the various Vintage Story modding documentation that is spread far and wide on the wiki page. Feel free to create PRs with improvements to this.
Vintage Story mods can be created through C# programming (code mods) or by creating JSON assets and content files (content mods), or both. This document separates conventions into two main categories: Coding for C# development and Content for JSON assets, textures, shapes, and localization. Vintage Story has conventions that mod developers commonly follow for consistency and to reduce conflicts across mods.
- Blocks:
Block{Name}(example:BlockTrampoline) - Items:
Item{Name}(example:ItemThornsBlade) - Block entities:
BE{Name}(example:BEAnvil) - Entities:
Entity{Name}(example:EntityEarthworm) - Behaviors: descriptive name (optional
Behaviorsuffix), registered under a string code - Mod entry point: one class extending
ModSystem(often named after the mod)
This section covers C# programming conventions for Vintage Story code mods.
Some projects choose to deviate from common C# conventions where they harm readability. This is a project-level decision, and standard C# conventions (with "I" prefix for interfaces) are equally valid.
- Good:
Feature,Config,Handler - Avoid:
IFeature,IConfig,IHandler
An interface IS the thing, not "an interface of" the thing! A Feature interface represents what a feature is. The "I" prefix adds noise without adding meaning.
Implementation names become much cleaner! With IFeature, you end up with awkward names like FeatureImpl or FeatureBase. With Feature, implementations are naturally named: TrashCompactorFeature, TeleportFeature.
Vintage Story code commonly uses a {Type}{Name} style for classes, where the type communicates what the class represents.
-
Block classes: Use the prefix
Blockfollowed by the block name (example:BlockTrampoline). Each block class extends the baseBlockclass. -
Item classes: Use the prefix
Itemfollowed by the item name (example:ItemThornsBlade). Each item class extends the baseItemclass. -
Block entity classes: Use the prefix
BE(Block Entity) followed by the entity name (examples:BEAnvil,BEBed). In JSON, block entities are referenced without theBEprefix. For example, the block JSON might contain"entityClass": "Anvil", which corresponds to the code classBEAnvil. -
Entity classes: Use the prefix
Entityfor custom creatures or entities (example:EntityEarthworm). This matches vanilla patterns such asEntityWolfandEntityPlayer. Ensure you register your entity class in code. -
Behavior classes: If you create block behaviors or entity behaviors (classes extending
BlockBehavior,CollectibleBehavior, orEntityBehavior), use a descriptive name that reflects what it does. Many mods use a short name (example:Moving), while others add aBehaviorsuffix for clarity (example:ExplosionDropNerfBehavior). Behaviors are also registered under a string code, which is often similar to the class name. -
Mod system classes: Every code mod should include a class extending
ModSystemas the entry point. By convention, it is often named after the mod (example:TrampolineMod). It typically registers blocks, items, entities, behaviors, and other systems in itsStartmethod.
Vintage Story mod templates and common practice encourage organizing code by category to keep projects maintainable.
-
Folders and namespaces: Group classes in folders by type (for example,
Blocks,Items,Entities). Match this in namespaces. If your project is namedVSTutorial, a block class in aBlocksfolder commonly uses a namespace likeVSTutorial.Blocks. The top level namespace is usually your mod name or a capitalized form of your mod ID. -
One class per file: Keep one class per
.csfile, and name the file the same as the class (example:BlockTrampoline.cs,ItemThornsBlade.cs). -
Access modifiers: Mod classes do not need to be
publicunless they must be accessed from other assemblies. Many mods keep block and item classesinternal. YourModSystementry point is typicallypublicso the game can load it.
In your ModSystem.Start, register custom classes using a lowercase string that combines your mod ID and a short identifier, commonly based on the asset code. Example:
api.RegisterBlockClass(Mod.Info.ModID + ".trampoline", typeof(BlockTrampoline));The same idea applies to RegisterItemClass, RegisterEntityClass, RegisterBlockEntityClass, and similar APIs.
User-facing text in Vintage Story may be parsed as formatted text (VTML). This affects places like handbook content and can also affect other displayed messages. Because VTML uses angle brackets for tags, unescaped < and > can be interpreted as markup instead of literal characters.
- Treat messages as markup-aware: Chat output, command help, error messages, and other user-facing strings should be written as if a markup parser may read them.
- Avoid raw angle brackets: Do not use
<arg>style placeholders in messages. Prefer[arg],(arg), or{arg}. - Escape when needed: If you must show literal
<or>, escape them as<and>. - Use formatting intentionally: Only use VTML tags when you explicitly want formatting, and keep chat output simple and readable.
- Prefer lang keys: Do not hardcode player-facing strings when a lang key is practical (this also helps keep formatting consistent across the mod).
Examples:
- Good:
/spawnnear range [min] [max] - Good:
/spawnnear range <min> <max> - Avoid:
/spawnnear range <min> <max>
Player-facing notifications use consistent VTML formatting with the <font> tag:
Syntax reference:
<font size="num" color="hexcolor" weight="bold" lineheight="1.2" align="right" opacity="0.5">text</font>
Standard styles:
- Feature prefix (normal):
<font color="#88cc88" weight="bold">[FeatureName]</font>(soft green, bold) - Feature prefix (error):
<font color="#cc8888" weight="bold">[FeatureName]</font>(soft red, bold) - Key values (numbers, states):
<font weight="bold">{value}</font>(bold)
Example:
player.SendMessage(0, $"<font color=\"#88cc88\" weight=\"bold\">[SpawnNear]</font> Respawning <font weight=\"bold\">{distance:F0}</font> blocks from your death.", EnumChatType.Notification);This ensures mod messages are visually distinct from regular chat and maintain a consistent brand identity across all Better Survival features.
Vintage Story provides a built-in command help system accessed via /help. Leverage this system rather than creating custom help handlers.
-
Use
.WithDescription(): Every command and subcommand should have a clear description. Important: Descriptions only appear when using/help- they do NOT appear in the basic subcommand listing when you type/command. When you type/command, you only see "Choose a subcommand: [list]" without descriptions. -
Avoid custom help subcommands: As a rule of thumb, avoid creating
/mycommand helpsubcommands. Users expect/help mycommandto work, and the built-in system already provides this. -
Avoid redundant status handlers: Do not create handlers on intermediate nodes that just echo available subcommands. When a user types an incomplete command, the built-in system automatically shows "Choose a subcommand: [list]".
-
Status commands show actual state: Only create explicit status/info commands when showing complex state that isn't obvious from the command structure. Examples:
- Good:
/mycommand statusshowing current configuration, overrides, and policy settings - Good:
/mycommand infoshowing detailed behavior and explanations - Avoid:
/mycommand defaulthandler that just says "Default is ON. Use /mycommand default on|off"
- Good:
-
Context-aware handlers: If you need different behavior for different privilege levels, handle it in the same command handler:
public TextCommandResult HandleStatus(TextCommandCallingArgs args) { var player = args.Caller.Player as IServerPlayer; if (player.HasPrivilege(Privilege.controlserver)) return ShowAdminStatus(args); else return ShowPlayerStatus(player); }
-
Arguments vs subcommands: Choose between arguments and subcommands based on discoverability:
- Use subcommands when values are enumerable/predictable (policies, modes, on/off toggles)
- Use arguments only when values are dynamic/unpredictable (player names, numbers, free text)
Good (discoverable):
.BeginSubCommand("set") .BeginSubCommand("strict") .WithDescription("Fall back to world spawn immediately on death") .HandleWith(handler) .EndSubCommand() .BeginSubCommand("adaptive") .WithDescription("Try multiple strategies before falling back to world spawn on death") .HandleWith(handler) .EndSubCommand() .EndSubCommand()
Avoid (not discoverable):
.BeginSubCommand("set") .WithArgs(_api.ChatCommands.Parsers.Word("policy")) .HandleWith(handler) .EndSubCommand()
When users type
/help mycommand set, subcommands show all options with descriptions. String arguments only show<policy is a string without spaces>. -
Argument placement: Place arguments on the subcommand that uses them, not on parent nodes. This allows intermediate nodes to display helpful subcommand lists instead of "Argument X is missing" errors.
-
Multi-argument commands: For commands requiring multiple arguments, include the full usage syntax in the description. This helps users who use
/helpto discover commands:Good:
.BeginSubCommand("config") .WithDescription("Set configuration: /mycommand config [name] [value] [option]") .WithArgs(parser1, parser2, parser3) .HandleWith(handler) .EndSubCommand()
Avoid:
.BeginSubCommand("config") .WithDescription("Set custom configuration") .WithArgs(parser1, parser2, parser3) .HandleWith(handler) .EndSubCommand()
Limitation: The built-in system only shows missing arguments one at a time when typing commands directly. Descriptions (including usage syntax) only appear in
/helpoutput, not in the basic subcommand listing. Users typing/command subcommandwill still see "Argument X is missing" one at a time, but/help command subcommandwill show the full usage syntax.
Benefits of using the built-in system:
- More discoverable - users see available subcommands automatically
- Less code to maintain
- Consistent with game conventions
/helpprovides detailed usage information including argument parsers
This section covers JSON assets, textures, shapes, localization, and other content creation conventions.
Every mod has a mod ID (also called a domain) that namespaces its assets.
-
Mod ID format: The mod ID is defined in
modinfo.jsonand should be lowercase alphanumeric with no spaces or special characters. For example,"modId": "mycoolmod"is valid, while"MyCoolMod"and"my-cool-mod"are not. -
Domain usage: A domain is the prefix before the colon in an asset location such as
mycoolmod:trampoline. The base game commonly uses thegamedomain (and sometimes other domains in its own asset layout), while your mod uses your mod ID as its domain. Inside your own mod assets, you can often omit the domain because the game assumes your mod domain when none is provided. When referencing base game or other mod assets, include the domain explicitly. -
Unique naming: Use your mod domain consistently to avoid conflicts. Two mods can both have a block code
trampolinebecause they are distinct assets asalice:trampolineandbob:trampoline.
Much of Vintage Story content is defined via JSON assets (block types, item types, entity types, and more). The conventions below help keep assets discoverable and consistent.
-
Asset file structure: Under
assets/{modid}, organize assets by category. Common folders includeblocktypes/,itemtypes/,entities/,shapes/,textures/,lang/, andpatches/. You can also use subfolders for organization, similar to the base game. -
JSON file naming: The JSON filename typically matches the asset code. For example, a block with
"code": "trampoline"is commonly stored astrampoline.jsonunderassets/{modid}/blocktypes/. Use lowercase codes, and prefer hyphens for multi word codes (example:chest-labeled). -
JSON
codeproperty: Thecodefield defines the asset identifier within its domain.-
Implicit mod domain (inside your mod):
{ "code": "trampoline" } -
Explicit domain (referencing another domain):
{ "code": "game:granite" }
-
-
Class attachment in JSON: If an asset needs a custom C# class, add a
"class"property in the JSON. The value must match the name you registered in code. Conventionally, include your mod ID in that registry name. Example:{ "code": "trampoline", "class": "vstutorial.trampoline" }Place
"class"near the top (often directly after"code") so it is easy to spot. -
JSON patch file naming: Patch file names are flexible, but a common convention is to mirror the target path using hyphens. For example, a patch targeting
game:blocktypes/wood/bed.jsonmight be namedgame-blocktypes-wood-bed.jsonand placed underassets/{modid}/patches/. -
Asset referencing: When referencing shapes, textures, and similar assets, omit the domain for your own assets and include it for external assets.
- Mod asset reference (implicit domain):
"item/simplewand" - Base game reference (explicit domain):
"game:block/stone/granite"
- Mod asset reference (implicit domain):
Mods can provide localization by adding entries to a language file (for example, en.json) under assets/{modid}/lang/.
-
Auto generated keys: The game generates default localization keys for most assets based on type and code. The pattern is typically
{type}-{code}. Examples:block-simplyshinyblock,item-simplewand. In your lang file, map keys to display strings:{ "block-simplyshinyblock": "Simple Gold Block", "item-simplewand": "Simple Wand" } -
Other lang keys: For custom UI, behavior text, and messages, define your own keys with a consistent scheme. Many mods prefix by area or system (example:
gui-alloymixer-title) or include the mod ID in the key to reduce collisions. -
No hard coded strings: Prefer lang keys for user facing text instead of hard coding strings in code or JSON.
-
Maintaining multiple languages: If you provide multiple language files, keep keys identical across locales and only change values.
- Code Tutorial Simple Block: Creating a Block Class
- Code Tutorial Simple Item: Creating an Item Class
- Block Entity Classes (some data might be outdated)
- Adding Block Behavior
- Advanced Blocks: Creating a Trampoline
- Modinfo
- modinfo.json schema
- Asset System
- Asset System: Asset Folder Location and Structure
- Asset System: Domains (prefixes)
- Content Tutorial Basics: Shape Files
- Content Tutorial Basics: Texture Files
- Content Tutorial Basics: Language File
- JSON Patch Reference
- JSON Patch Reference: Operations (addMerge, add, addEach, remove, replace, move, copy)
- VTML: Vintagetext Markup Language