-
Notifications
You must be signed in to change notification settings - Fork 0
Code First Definitions
A block, item or recipe does not have to live in a JSON file. ExpandedLib.Definitions lets you
build the same blocktype/itemtype/recipe JSON the vanilla object loader consumes, in C#, co-located
with the class it configures. Everything ends up as the same synthetic asset a hand-written file
would produce, so variant expansion, *ByType selection, the atlas, block-ID assignment and client
sync all run unchanged - no Block is ever constructed directly from a definition.
Three sibling interfaces, one per asset kind, each with a single static abstract factory:
public interface IExBlockDefProvider {
static abstract IEnumerable<ExBlockDef> Definitions(string domain);
}
public interface IExItemDefProvider {
static abstract IEnumerable<ExItemDef> Definitions(string domain);
}
public interface IExRecipeDefProvider {
static abstract IEnumerable<ExRecipeDef> Definitions(string domain);
}Implement IExBlockDefProvider on the block class itself (a class can back several blocktype assets -
same code, distinct asset paths - so it returns an IEnumerable). Items and recipes have no natural
class to hang a definition on, so their providers are usually stand-alone classes that implement
IExItemDefProvider / IExRecipeDefProvider and nothing else. domain is the mod id the def
registers into, which is what binds ExBlockDef.Create and Class<T>() to the right asset domain and
registered-class key.
EntityRegistry.RegisterAll(api, mod, asm) - the same call that registers your
[BlockRegister]/[ItemRegister]/[BlockEntityRegister] classes - also scans asm for provider
implementors and feeds each one's Definitions(domain) result into ExDefinitions. No separate call
is needed; a class that implements a provider interface is discovered the moment your ModSystem.Start
calls EntityRegistry.RegisterAll.
ExDefinitions is the process-wide registry the discovered defs land in:
Process-wide registry of code-first block definitions. A mod authors a block in C# with
ExBlockDefand registers it here (from itsModSystem.Start); the sharedExDefinitionModSystemserializes each and injects it as a syntheticblocktypes/asset on the server, before the object loader runs. Keyed by asset location so a re-register (or a deliberate override) replaces rather than duplicates.
ExDefinitionModSystem is the ModSystem that turns the registry into real assets:
public override bool ShouldLoad(EnumAppSide side) => side == EnumAppSide.Server;
public override double ExecuteOrder() => 0.04;It runs at AssetsLoaded: below the game's own JSON patch loader (0.05), so injected assets stay
patchable by other mods; well below the object loader (0.2) that consumes them; and above 0 so base
assets are already indexed. It is server-only, because blocktypes/itemtypes loading and the object
loader are both server-only stages - the client receives the resolved block and item types over the
network, the same way it receives any other asset the server built. See Lifecycle for
where this sits relative to everything else that happens at world load. A definition registered after
this deadline is never built; LateDefinitionCheck names it and the fix in the log - unless
it re-registers a location injection already covered, replacing the built content with something the
loader never sees; the check tests location membership only, so that case stays silent.
A provider's Definitions(domain) runs the moment EntityRegistry.RegisterAll discovers it, which
can be before every mod's own assets are readable - fine for a definition that is complete in
source, wrong for one built from a catalogue another mod's JSON contributes to. IExDefinitionContributor
is the interface for that case:
public interface IExDefinitionContributor {
void Contribute(ICoreAPI api);
}EntityRegistry.RegisterAll discovers an implementor the same way it discovers a provider;
ExDefinitions.RunContributors instantiates and runs each one at AssetsLoaded 0.04, right before
injection, regardless of which mod or module Start discovered it in - the one point in the phase
sequence guaranteed to run after every Start has returned, so a contributor can read a catalogue
AssetCatalogueLoader assembles from every domain's JSON and register definitions from it.
Server-only, the same as ExDefinitionModSystem itself. Industry's metal-family item emission is a
contributor for exactly this reason: the metals it builds items for come from config/metals/
across every domain. See Modules for a module's own use of it.
The block-side builder: a fluent chain that ends with Location (the synthetic asset path) and
ToJson() (the built blocktype). Minimal:
public static IEnumerable<ExBlockDef> Definitions(string domain) =>
[ExBlockDef.Create(domain, "pebble")
.Class<BlockPebble>()
.Shape("game:block/basic/cube")
.TextureAll("game:block/stone/rock/granite1")
.CreativeCommon("*")];A real call site, with everything a shipped block actually needs - the mpenergy transmission mega-block:
public static IEnumerable<ExBlockDef> Definitions(string domain) {
ExBlockDef def = ExBlockDef
.Create(domain, "mpenergy", "mpenergy/transmission")
.Class<BlockTransmission>()
.EntityClass<BlockEntityTransmission>()
.Material(EnumBlockMaterial.Metal)
.MiningTier(0)
.Resistance(4.5f)
.MaxStackSize(1)
.NoDrops()
.FillerOffsets([
new FillerCellSpec(1, 0, 0),
new FillerCellSpec(0, 1, 0),
new FillerCellSpec(1, 1, 0),
])
.Behavior<BlockBehaviorExOrientable>()
.Behavior("BlockEntityInteract")
.EntityBehavior("Animatable")
.Construction(c => c.Stage(s => s.AddElements("Base")) /* ... */);
// ...
}Create(domain, code) places the asset at blocktypes/{code}.json; the three-argument overload
Create(domain, code, assetName) lets several blocktype files share one code (a pipe class backing
pipe/straight, pipe/bend, ...).
Only the typed methods are listed; anything not here goes through Attribute/AttributeByType/
RootKey/RootKeyByType below.
| Section | Methods | JSON key(s) written |
|---|---|---|
| Shape | Shape |
shape.base |
ShapeRotateYByType, ShapeSpunPerOrientation
|
shape.rotateYByType.{wildcard} |
|
ShapeSelectiveElements |
shape.selectiveElements |
|
ShapeByType, ShapeByTypePerOrientation
|
shapebytype.{wildcard} |
|
| Textures |
Texture, TextureAll
|
textures.{key} |
TextureByType |
texturesByType.{wildcard}.{key} |
|
| Variants |
VariantGroup, VariantGroupFromProperties, SideVariant
|
variantgroups[] |
SkipVariants |
skipVariants |
|
| Behaviors |
Behavior(name), Behavior(name, props), Behavior<T>(), NetworkOriented
|
behaviors[] |
EntityBehavior(name), EntityBehavior(name, props), EntityBehavior<T>(), Construction
|
entityBehaviors[] |
|
| Attributes |
Attribute, Attributes
|
attributes.{key} |
Handbook |
attributes.handbook.groupBy |
|
FillerOffsets |
attributes.fillerOffsets |
|
FillerOffsetsByType |
attributesByType.{typeWildcard}.fillerOffsets |
|
AttributeByType |
attributes.{key}ByType.{wildcard} |
|
Multiblock, MultiblockLayout
|
attributes.multiblockStructure (+ multiblockFacings/multiblockRoles/multiblockConnectors siblings for MultiblockLayout) |
|
| Collision |
CollisionBox, SelectionBox
|
collisionboxes[] / selectionboxes[]
|
SingleCollisionBox, SingleSelectionBox
|
collisionbox / selectionbox
|
|
SideSolid, SideOpaque, SideAo, EmitSideAo, NonSolid, SolidNonOpaque
|
sidesolid / sideopaque / sideAo / emitSideAo
|
|
| Drops |
NoDrops, Drop
|
drops / drops[]
|
| Creative inventory |
CreativeTab, CreativeCommon
|
creativeinventory.{tab} |
| Handbook | HandbookExclude |
handbook.exclude (top-level, not under attributes) |
A handful of scalars have their own one-line methods too (Material, Resistance, MaxStackSize,
StorageFlags, Replaceable, MaterialDensity, the Held*/heldTp* animation setters,
WalkSpeedMultiplier, MiningTier, MineTool, RenderPass, FaceCullMode, DrawType,
LightAbsorption, sound setters, CombustibleProps) - each named after the blocktype key it writes.
RenderPass and DrawType also take the EnumChunkRenderPass/EnumDrawType enum directly, and
FaceCullMode the EnumFaceCullMode enum; all three still take the raw string too, writing the same
value either way.
Everything in the table above except the collision/side/render/sound/drop/entity/megablock rows and
ShapeRotateYByType/ShapeSpunPerOrientation/ShapeByTypePerOrientation/SideVariant/
NetworkOriented (placed-block orientation) also exists on ExItemDef, writing the same JSON key -
see the ExItemDef section below for the list of what does not carry over and why.
Two methods reach schema the typed API does not cover, and they write to different places:
-
Attribute(key, value)writesattributes.{key}- a key the block/behaviour code reads back throughAttributes["..."]at runtime. Anything is a valid key here; a typo just means your own code never finds it. -
RootKey(key, value)writes a top-level key next tocode,shape,behaviors, etc. This is read only if it is a real blocktype key the game's object loader itself understands (e.g.combustibleProps,guiTransform; seeKnownRootKeys). A key that is not one of those is written into the JSON but never read by anything -ExDefinitionModSystemlogs a Warning for it at injection, naming the def and the key, rather than letting the mistake sit silent.Raw/RawByTypeare the pre-Unreleased names, kept as[Obsolete]forwarders.
The item-side sibling of ExBlockDef, covering every blocktype method whose JSON key also exists on
an itemtype (same builder shape: Class<T>(), Shape, Texture/TextureByType, ShapeByType,
VariantGroup, Behavior/Behavior<T>(), Handbook/HandbookExclude, SkipVariants,
Attribute/AttributeByType, RootKey/RootKeyByType, CreativeTab/CreativeCommon, the four
model transforms, plus CombustibleProps/GrindingProps). A stand-alone provider, since a plain item
has no mod class to hang the definition on:
public class DiagramItemDefinitions : IExItemDefProvider {
public static IEnumerable<ExItemDef> Definitions(string domain) {
// ...
yield return ExItemDef
.Create(domain, "diagram")
.VariantGroup("type", [.. types])
.Texture("base", $"{domain}:item/diag-base")
.Behavior("GroundStorable", new { layout = "SingleCenter" })
.Handbook("diagram-*")
// ...
}
}Missing on purpose: anything that only makes sense for a placed block - world orientation
(ShapeRotateYByType, ShapeSpunPerOrientation, ShapeByTypePerOrientation, SideVariant,
NetworkOriented), the render/collision/face fields (RenderPass, FaceCullMode, DrawType,
CollisionBox/SelectionBox, SideSolid/SideOpaque/SideAo/EmitSideAo), block entities
(EntityClass<T>(), EntityBehavior), sounds (sounds.* is BlockType-only; an item's HeldSounds
is a separate, unrelated key), drops (NoDrops/Drop) and megablocks
(FillerOffsets/FillerOffsetsByType, Construction, Multiblock/MultiblockLayout). A reflection
test over ExBlockDef and ExItemDef (in the Definitions test suite) is the audit that keeps this
list honest as ExBlockDef grows.
ExRecipeDef is the recipe-file builder: one def produces one
recipes/{category}/{assetName}.json asset, either an array (Grid/Add, callable repeatedly) or a
single object (GridObject/Body, callable once). GridRecipeBuilder builds one grid-recipe object
({ name, ingredientPattern, ingredients, width, height, output }); IngredientBuilder builds one
ingredient slot. A real call site, the rolling mill's grid recipe:
private static ExRecipeDef RollingMill(string domain) =>
ExRecipeDef
.Create(domain, "grid", "rollingmill")
.Grid(r =>
r.Name("Rolling Mill")
.Pattern("PRP,PGP,PHP")
.Size(3, 3)
.Ingredient("P", i => i.Item($"{domain}:castplate-heavy").Quantity(1))
.Ingredient("R", Rod(2))
.Ingredient("G", i => i.Item($"{domain}:{SpurGearItemDefinitions.Code}").Quantity(1))
.Ingredient("H", Hammer)
.OutputBlock($"{domain}:forming-rollingmill-we", 1)
);(Rod and Hammer are ExIngredients factories, brought in via using static ExpandedLib.Definitions.ExIngredients; - see below.)
Builds a block's attributes.multiblockStructure ({ blockNumbers, offsets }) from explicit
Number/At/Fill calls, validating at build time that every offset's number is declared and that
no cell is duplicated. A real call site, the Bessemer converter control block:
.Multiblock(m =>
m.Number("siex:convertercontrol*", 1)
.Number("siex:convertertransmission*", 2)
.Number("siex:converterbessemer*", 3)
.Number(ExCodes.Filler, 8)
.At(0, 0, 0, 1)
.At(0, -1, 0, 2)
.Fill(-1, -1, 1, 1, 1, 1, 8)
// ...
)The higher-level alternative to MultiblockBuilder: draws the structure as ASCII diagrams instead
of listing offsets by hand. ExpandedLib.Structures.CellGrid is the grid core underneath it (also
usable directly for other ASCII-diagram DSLs, such as the filler footprint); MultiblockLayoutBuilder
adds the legend, role and connector vocabulary over it. Layer draws a floor plan, one grid per Y
level; Slice and Face draw a fixed-X or fixed-Z elevation instead, for a structure that stacks in
Y - a layout may mix all three. A real call site, the coke oven core:
.MultiblockLayout(s =>
s.Origin(-4, -2)
.Legend('#', VanillaCodes.FireBricks)
.Legend('c', IiexBlocks.FurnaceFirebox.Any)
.Legend('-', VanillaCodes.FireSlab(BlockFacing.UP))
.Legend('C', IiexBlocks.FurnaceCokeovencore.Any)
.Legend('f', ExCodes.Filler)
.Legend('a', VanillaCodes.Air)
.Role('c', FurnaceCellRoles.Firebox)
.Layer(0, """
# # # # # # # # #
...
""")
// ...
)A code carrying a whole horizontal side segment (north/south/east/west, or the letters) is
orientation-checked automatically: the required facing rotates with the structure. See
Multiblock Structures for the orientation-checking mechanism itself and the
filler system this builder's Legend/Layer vocabulary is shared with. A block's own
attributes.multiblockLayout is the JSON twin of this builder, for a machine that carries no C# at
all - see "From JSON only" on that page.
Not something you construct directly - it is the read model ExBlockDef.VariantGroups returns for
each VariantGroup(code, states...) / VariantGroupFromProperties(...) call, in declaration order
(the order states appear in the rendered code). ExBlockDef.Any and WithVariant(group, state) walk
this list to build wildcarded or pinned codes for you, so a layout legend or a recipe output never
hand-types a variant string that can drift from the definition:
.VariantGroup("tier", BlockPipe.CastTier)
.VariantGroup("type", "pressurevalve")
.VariantGroup("orientation", "ns", "we", "ud", "sn", "ew", "du")Two catalogues of block-code strings that multiblock layouts and recipes are drawn from, mirroring
each other's purpose but not their construction: ExCodes forwards to the generated ExlibBlocks
table, so a name can never drift from the code it stands for (ExCodes.Filler is
ExlibBlocks.Structurefiller.Code, the invisible per-cell filler). VanillaCodes cannot work that
way - the game declares those blocks, not a def in this codebase - so every member is hand-kept, at
varying strictness (VanillaCodes.RefractoryTier(3) for one exact tier,
VanillaCodes.Refractory for any tier, VanillaCodes.AnyBricks for any masonry at all). Each mod in
the family keeps the equivalent of ExCodes for its own blocks (IiexBlocks, SiexBlocks,
generated the same way).
A small catalogue of IngredientBuilder factories for vanilla game: ingredients shared across the
mods' recipe files - Hammer, Chisel, Plate(qty), Rod(qty), FireClay(qty), and their
steel-only siblings. After using static ExpandedLib.Definitions.ExIngredients;, pass one as a method
group (.Ingredient("H", Hammer)) or call the quantity factory (.Ingredient("P", Plate(1))). Only
mod-agnostic ingredients belong here; a mod's own item codes stay in its own ingredients helper.
Class-typed methods (Class<T>(), EntityClass<T>(), Behavior<T>(), EntityBehavior<T>()) resolve
T's registered key from T's own assembly, not from the definition's domain, so naming a class from
a dependency yields the key that mod actually registered. That resolution needs the dependency's
assembly to declare [assembly: ExDomain("itsmodid")] - see Getting Started,
"If other mods will name your types", for the full explanation and the failure mode when it is
missing.
DefinitionGoldens (in exlib.testing) is the oracle every migrated def is checked against: it
collects every def a mod assembly declares (without touching the live ExDefinitions registry) and
compares each one's emitted JSON to a committed golden file under goldens/{domain}/{Location.Path}.
EXLIB_WRITE_GOLDENS=1 reblesses every golden in a run; a narrower value
(EXLIB_WRITE_GOLDENS=iiex/blocktypes/furnace/blastcore) blesses one file at a time.
For a def migrated from an existing hand-written blocktype, DefinitionParity checks the two are
semantically the same JSON - numbers compare type-agnostically, multiblockStructure and
fillerOffsets compare as sets of cells rather than ordered arrays - so a def is free to reorder
what the schema treats as unordered without breaking parity. See Testing Harness
for wiring a parity/golden test project.
- A duplicate asset location (the same
{domain}:{blocktypes|itemtypes|recipes}/...path registered twice) replaces silently when both registrations come from the same assembly; when they come from different assemblies,ExDefinitionslogs a Notification naming the location and both assemblies - seeExDefinitions.Logger. - A mistyped root key passed to
RootKeyis written into the JSON but never read by the game, since only a real blocktype/itemtype key means anything to the object loader;ExDefinitionModSystemlogs a Warning for it at injection, naming the def and the key (KnownRootKeys). - A
MultiblockLayout's declaredOriginmust land the glyph markedCoreon the layout's own(0,0,0);Build()throws naming the layout, the declared origin and where the anchor actually landed when it does not. Unmarked layouts are not checked. - A missing
[assembly: ExDomain]breaks only cross-modClass<T>()/Behavior<T>()naming - a mod nobody else extends works fine without it, but the fallback resolves to the wrong domain andEntityRegistrylogs a Warning naming the assembly. - The injection (
ExDefinitionModSystem.AssetsLoaded) is server-only, so a client-only build of a mod never sees its own code-first defs - the client always receives them resolved from the server.
Expanded Library · framework mod for Vintage Story
exlib - Blocks
- Block Entities
- Block Networks
- Multiblock Structures
- Production Machines
- Construction (RCC)
- Migrations & Healing
exlib - Extending our mods
exlib - Registration
- Registries
- Modules
- Code-First Definitions
- Config System
- Commands
- Recipe Costs
- Source Generators
- Checks
exlib - Utilities
exlib.testing