Lifecycle

A Vintage Story world does not load in one step. The game walks a fixed sequence of phases and, in each phase, calls one method on every loaded mod before it moves on to the next. The class carrying those methods is a ModSystem: the entry point the game constructs for your mod, one instance per class you derive from it, with a virtual method per phase. The phases in order are StartPre, Start, AssetsLoaded, AssetsFinalize, then StartServerSide or StartClientSide depending on the side, and Dispose at shutdown. Within one phase, mods run in ExecuteOrder order, lowest number first; 0.1 is the vanilla default.

That ordering is the whole difficulty. Almost everything a mod does has one phase where it works and several where it silently does not. Register a network type later than Start, and a pipe in a chunk that is already streaming in reaches the graph before its type exists, so it joins no network. Declare a block definition after the phase that turns definitions into assets, and the block never reaches the game's registry. Read a catalogue file before the game's JSON-patch pass, and you read the unpatched copy, so another mod's patch is missing from whatever you validated. None of these throws an exception. What you get is a block absent from the handbook, a machine that never starts, or one line in a log nobody reads.

exlib pins its own systems to explicit ExecuteOrder rungs so each of those steps has exactly one place, and this page states them all. Because exlib is a runtime dependency of your mod, the game constructs its ModSystems before your mod's Start runs. The systems that must get ahead of the rung every mod shares are pinned below it: the module driver's AssetsLoaded and AssetsFinalize at 0.03, code-first definitions' AssetsLoaded at 0.04, exlib's own catalogue loads at AssetsFinalize 0.06, with the game's own JSON patch loader running at AssetsLoaded 0.05 in between. BlockNetworkModSystem, NetworkHighlightModSystem, ExConfigSyncModSystem, BlockMigrationModSystem and BlockEntityHealModSystem take the vendored 0.1 default like any consumer, since nothing they do needs to run ahead of it.

You schedule none of this by hand. Derive ExModSystem and the standard calls land on the right rung for you, with your own work in the On... hook of the matching name. Read on when you call something yourself, when you need to know whether a registry is populated yet, or when something you registered never showed up.

What ExModSystem does for you

A mod whose system derives ExModSystem gets all of the following without writing a line of it. Your own code goes in the hook named at the end of each item.

  • Start loads your config classes, registers every class you tagged with a registration attribute (see Registries), registers your content checks, and applies your Harmony patches if you override PatchHarmony to true. Then OnStart runs. Anything keyed by a Start-phase registry belongs there: a network type, a recipe profile, a broken-drops ratio.
  • StartServerSide registers your command classes, then runs OnStartServerSide.
  • StartClientSide registers your client preferences, then your commands, then runs OnStartClientSide. That order is load-bearing; see Ordering rules below.
  • AssetsFinalize runs OnAssetsFinalize after exlib's own catalogue loads at 0.06, so it is the one phase where the catalogues and every mod's JSON patches are final. Validate your own content against them here.
  • Dispose unpatches your Harmony instance if PatchHarmony is set. Nothing you registered through the registries above needs teardown of its own.

A phase you never override costs nothing. ExModSystem also drives your mod's own modules through each phase, so a module you ship joins the same sequence.

Ordering rules

Three rules the phase reference below proves, not conventions you could choose to ignore.

  • Preferences before commands, on the client. A sub-command resolves the IExPreference it names once, at registration time, into a local it holds from then on. Call PreferenceRegistry.RegisterAll before CommandRegistry.RegisterAll in your StartClientSide, or a command naming your own preference finds nothing.
  • Register your network types in Start, before any node can initialise. A node registers itself with the graph (BlockNetworkModSystem.AddNode) from block-entity initialization during chunk loading, which cannot happen before every mod's Start has returned. RegisterNetworkType called any later than Start risks a node reaching the graph before its type exists; an unregistered type logs an error and the block joins no network at all.
  • A def provider must be discovered in Start for the AssetsLoaded 0.04 injection to see it. EntityRegistry.RegisterAll (called from Start) is what feeds IExBlockDefProvider/ IExItemDefProvider/IExRecipeDefProvider implementors into ExDefinitions; ExDefinitionModSystem reads that registry at AssetsLoaded 0.04, strictly after every mod's Start has run. A provider whose owning mod never calls RegisterAll (or calls it only later) is never discovered. The same RegisterAll call also discovers IExDefinitionContributor implementors; a contributor discovered in any Start (main assembly or module, any host order) runs at AssetsLoaded 0.04, which is the one legal place to emit a definition that itself depends on assets loaded earlier in that same phase.

Phase reference

Read a row as a contract: by the time the game reaches that rung, exlib has done what the middle column says, and the right column is what is safe for you to call there. Rungs below 0.1 are exlib's own; a mod that leaves ExecuteOrder alone always runs after them.

PhaseWhat exlib has done by thenWhat you may call here
StartPre (ExecuteOrder 0.0)ExModsModSystem.StartPre sets ExMods.FlagKey(modId) to true in api.World.Config for every enabled mod, on both sides from each side's own local mod list, well ahead of the JSON patch loader's AssetsLoaded at 0.05. A patch can then test for a mod being present.Little beyond that: no blocks, items or recipes are registered, and no asset has been read.
StartPre 0.03ExModuleModSystem.StartPre wires ExDefinitions.Logger and EntityRegistry.Logger, loads ExlibValues, sets ExModules.FlagKey(id) to true in api.World.Config for every enabled module, then runs every framework module's own StartPre.Whatever a module of yours provides at this rung, and nothing more; blocks, items and recipes are still unregistered.
Start (ExecuteOrder 0.1, the consumer default - pinned explicitly on ExModSystem, inherited otherwise)ExModsModSystem.Start sets the same flags again, harmlessly, in case World.Config was not yet available at StartPre. ExpandedLibModSystem.Start has registered exlib's own [BlockRegister]/[BlockEntityRegister]/[BlockBehaviorRegister] classes (the structure filler, the multiblock-structure behaviour) via EntityRegistry.RegisterAll, and pointed StructureFillers.FillerCode at the shared filler block. BlockNetworkModSystem and BlockMigrationModSystem/BlockEntityHealModSystem exist as separate auto-loaded ModSystems but have not yet run their own StartServerSide.This is the phase that holds most of your registration. Your config load (*Values.Load(api)); EntityRegistry.RegisterAll(api, Mod, GetType().Assembly), which registers your tagged classes and discovers your IExBlockDefProvider/IExItemDefProvider/IExRecipeDefProvider types into ExDefinitions; ExRecipeProfiles.Register(...); ExRccSettings.RegisterBrokenDropsRatio(...); api.ModLoader.GetModSystem<BlockNetworkModSystem>().RegisterNetworkType(...); ExHarmony.PatchOnce(Mod, GetType().Assembly). Deriving ExModSystem runs the config load, EntityRegistry.RegisterAll and, if PatchHarmony is set, ExHarmony.PatchOnce here for you, then calls your OnStart.
AssetsLoaded 0.03 (module driver)ExModuleModSystem.AssetsLoaded runs every framework module's own IExModule.AssetsLoaded, ahead of the JSON patch loader below, so an asset a module reads here is unpatched JSON. Industry declines this rung for its metal catalogue; see Modules "Phases" for why.Nothing exlib-specific. A self-hosted module's own AssetsLoaded, at its own order, is documented on Modules.
AssetsLoaded 0.04ExDefinitionModSystem.AssetsLoaded (server-only: ShouldLoad returns side == EnumAppSide.Server) first runs ExDefinitions.RunContributors, which instantiates every IExDefinitionContributor discovered during any Start (main assembly or module, any host order) and lets it register its own definitions - the metal-family items (Industry.IndustryModule.Contribute, via MetalFamilyEmitter) run here, not in a module's own AssetsLoaded. It then reads everything registered into ExDefinitions and injects it as synthetic blocktypes//itemtypes//recipes/{category}/ assets, including the process-route stopping-point items (ProcessItemEmitter), read straight off the config/ catalogues since the registries that would normally serve them are still empty at this phase.Nothing: this phase is exlib's own injection step. Anything you need it to see must already be registered as a def provider, or discovered as a contributor, by the time your Start returned. See Ordering rules above.
AssetsLoaded 0.05The game's own JSON-patch loader runs, at its vendor default ExecuteOrder rather than an exlib rung. Patches can now target the assets exlib injected at 0.04, the same as any file asset.Nothing exlib-specific. Blocks and items are still unregistered below ExecuteOrder 0.2, recipes below 0.6.
AssetsFinalize 0.03The domain layer's own IExModule.AssetsFinalize, run through the module driver (ExModuleModSystem, ExecuteOrder 0.03): the Industry module clears and reloads the metal catalogue (MetalCatalogueLoader.Load) here, well ahead of exlib's own AssetsFinalize below.Nothing exlib-specific at this order. A consumer's own AssetsFinalize runs later, at its own ExecuteOrder.
AssetsFinalize 0.06ExpandedLibModSystem.AssetsFinalize clears and reloads the fluids catalogue (LiquidCatalogueLoader.Load), the material-role catalogue (MaterialRoleLoader.Load), the process-route catalogue (Processes.ProcessRouteLoader.Load), the process-job catalogue (Processes.ProcessJobLoader.Load) and the storage bay-occupancy catalogue (Storage.BayOccupancyLoader.Load) - all after the JSON patch pass, so every mod's merged config is what these registries hold from here on. Each Load runs its catalogue's code contributors (Catalogues.CatalogueContributors, one per registry) after its JSON read, then returns a Catalogues.CatalogueLoadReport that AssetsFinalize logs: files read, entries accepted, and one Error per malformed asset or clash, naming its file. See Extending-Processes "What the log tells you" and "From C#". It then runs every content check (Checks.ExlibChecks.All) over the live game state and logs the results, unless ExlibConfig.RunChecksOnLoad is off; /exmod verify runs the same checks on demand.Your own validations against the now-final catalogues. Iron Industry Expanded validates its casting patterns and roll sets here, since a bad one would otherwise fail silently the first time a player used it. Deriving ExModSystem calls your OnAssetsFinalize from here; there is nothing else to run at this phase.
StartServerSide / StartClientSideBlockNetworkModSystem.StartServerSide captures ServerWorld and registers its 1000ms tick listener. BlockMigrationModSystem/BlockEntityHealModSystem (via ChunkColumnSweeperModSystem.StartServerSide) capture _sapi and register the RunGame-phase startup sweep plus the ChunkColumnLoaded listener. ExConfigSyncModSystem registers its channel on both sides and, server-side, subscribes PlayerJoin. ExpandedLibModSystem.StartClientSide runs PreferenceRegistry.RegisterAll (its own metric/imperial preference), then ExPreferences.LoadConfig, the handbook-units Harmony patch, a LevelFinalize hook that applies the local player's saved preferences, CommandRegistry.RegisterAll (the shared .exmod root), and ExRecipeProfiles.ApplyAll(api). ExpandedLibModSystem.StartServerSide runs CommandRegistry.RegisterAll and ExRecipeProfiles.ApplyAll(api).On the client, PreferenceRegistry.RegisterAll(...) before CommandRegistry.RegisterAll(...), because a sub-command resolves the preference it names once, at registration time. On the server, CommandRegistry.RegisterAll(...). Neither side calls ExRecipeProfiles.ApplyAll itself: exlib already applies every registered mod's profile on both sides. Deriving ExModSystem runs exactly this on each side, preferences before commands on the client, before calling your OnStartServerSide/OnStartClientSide.
Network manager's server tick (every 1000ms, registered in BlockNetworkModSystem.StartServerSide)Resumes any connectivity review an unloaded chunk suspended (ResumeSuspendedReviews), then dispatches BlockNetwork.OnTick for every live network, with dt capped at 2 seconds so a stalled server cannot hand you an enormous step.Nothing directly. This tick is what drives your own BlockNetwork subclass's OnTick, which is where your network's gameplay logic runs.
PlayerJoin (config sync)ExConfigSyncModSystem sends one ConfigSyncPacket per section registered with ExConfigProfiles (every Manageable config) to the joining player, carrying the host's live values.Nothing: a Manageable config gets this for free. A config with its own transport calls IExConfigAccess.ImportJson directly instead. See Config-System "What the client sees".
Migration sweep and BE healer on chunk loadBlockMigrationModSystem and BlockEntityHealModSystem each sweep every already-loaded chunk once at EnumServerRunPhase.RunGame, then sweep each column again as it streams in (ChunkColumnLoaded). The migrator rewrites blocks and items matching any discovered IBlockCodeMigration/IItemCodeMigration/IBlockRemoval; the healer respawns a missing block entity for any block whose EntityClass resolves to a [BlockEntityRegister] type.Declare your own IBlockCodeMigration/IItemCodeMigration/IBlockRemoval/IBlockEntityMigration implementations anywhere in your assembly. Both systems discover them by reflection across every loaded assembly, with no registration call needed. See Migrations & Healing.
DisposeExpandedLibModSystem.Dispose unpatches its Harmony instance. BlockNetworkModSystem.Dispose clears the graph (_networks, _posToNetwork, _unreadableNodes).Unpatch your own Harmony instance, and unregister any listener you added directly. Anything registered through the registries above needs no explicit teardown.