-
Notifications
You must be signed in to change notification settings - Fork 0
Checks
ExpandedLib.Checks is the shared library behind the content guards that used to live only in
exlib.testing: dangling recipe/multiblock codes, missing lang coverage, pinned network nodes, a
prefix collision that widens a wildcard onto a foreign block, a network node or membership missing
part of its contract. The rule for each lives in one class, parameterised only through
ICheckSource - never a file path, an assembly or a test framework - so the same rule runs against
the live game, against a repository tree, or against anything else you can describe in terms of
codes, recipes, lang and definitions.
LateDefinitionCheck is the odd one out: it names every block, item or recipe definition registered
after ExDefinitionModSystem already injected (see Code-First-Definitions),
which the loader then never builds. It reads ExDefinitions directly rather than ICheckSource, and
reports nothing until injection has actually run once in the process - a dedicated multiplayer client
never sees it, and neither does an ICheckSource built without replaying injection. In singleplayer
the integrated server and the client share that process and its static state, so the client's own
check call reports too, once the server's pass has run.
LangCoverageCheck in this library only guards the en locale - an unresolved en key is the one
that renders raw on screen, since every other translation falls back to it. Parity across a mod's
other shipped locales (a missing Ukrainian key, say) is a repository-time concern instead: see
ExpandedLib.Testing.LangCoverage and each mod's own LangParityTests.
There are three rungs, in increasing order of control.
ExpandedLibModSystem.AssetsFinalize runs every check against the live game state and logs the
results, after the metal/fluid/process catalogues finish loading. Each check logs one summary line
naming itself, its domain and how many errors it found, followed by one line per error. A modder who
never opens xUnit still sees "your recipe names a code that does not exist" in the server log the
first time the world loads with the mistake in it.
Set RunChecksOnLoad to false in exlib's config (ex_values.json) to skip this pass - the one
reason to is the one-time scan costing something noticeable on a very large modpack's world load.
Run the same checks on demand:
/exmod verify # exlib itself plus every mod that depends on it
/exmod verify iiex # one domain only, named explicitly - any loaded mod, dependent or not
With no argument, only exlib and its dependents are checked - never a bystander mod with no exlib
dependency, and never vanilla's own game/survival/creative, which never declares one and whose
own incomplete locales are not this library's to police. Naming a domain explicitly checks it
regardless of whether exlib depends on it, so long as some loaded mod answers to that id.
Prints how many checks ran and how many errors they found, then the first ten error lines; the full
list always goes to the server log via the same ExlibChecks.Log call AssetsFinalize uses, so a
long list is never truncated where it matters.
This is exactly the command exmod smoke (see Testing Harness)
runs against a freshly-booted dedicated server before stopping it, so the smoke lane's pass/fail
includes whatever /exmod verify finds.
A JSON-only modder has no code to build and no reason to install xUnit, but still wants "does my
mod even load" before ever launching the game. exlib-verify, the ExpandedLib.Verify .NET tool
built from extools and installed with
dotnet tool install -g ExpandedLib.Verify, answers that from a mod folder or zip alone, against a
provisioned game install and any number of other mods:
exlib-verify <modpath> [--game <install>] [--mods <dir>...] [--json] [--strict]
<modpath> is a folder or a zip carrying modinfo.json. --game defaults to the same
VINTAGE_STORY-or-.game/<slug> resolution the test harness uses; --mods loads any number of
other mods (folder or zip) as additional asset domains, so a compatibility patch against a mod that
isn't the one under test can actually be checked. --json prints a stable
{level, check, file, line, message} array for CI; --strict also fails the run (exit 1) on an
informational finding, not only an error.
Errors (exit 1):
- a JSON file under the mod's own
assets/that does not parse, with line and column - a patch whose
filetarget exists in no loaded domain, once itsdependsOnmods are satisfied (a target in a mod that isn't loaded and isn't required is informational instead - see below) - a patch operation that does not apply against the real target document -
add/replace/remove/addmerge/addeach/move/copy, run through the game's ownTavis.JsonPatchengine exactly the wayModJsonPatchLoaderdrives it, against the real (and, by the time a check runs, already-patched) target JSON - a
title/textkey aconfig/handbook/*.jsonpage names that has no matching key in that key's own domain'slang/en.json - a recipe ingredient or output code -
{ "type": "item"|"block", "code": ... }, wherever it appears in a recipe's own JSON shape - that resolves to no block or item code declared anywhere across the mod, the game, and any--mods
Informational (exit 0 unless --strict):
- a patch's
dependsOnnaming a mod id this run has no--modsfor - the patch is not evaluated, since it may be entirely correct once that mod is actually loaded alongside it - a patch
condition.when- there is no live world config outside a running game to evaluate it against, so the patch is named but never evaluated - a
variantgroupsentry this tool cannot expand headlessly (loadFromProperties, which needs the loader's ownICoreServerAPI-bound world-property resolution) - a reference under that type's base code is assumed to resolve rather than risking a false error - a code whose domain isn't loaded at all (no
--modsfor it) - this run has no way to say whether it resolves - a locale other than
enmissing some ofen's keys, one line per locale naming the count - the same gapExpandedLib.Testing.LangCoveragetracks for a checked-in mod, since a missing non-enkey falls back to English in game rather than showing raw
A hybrid code+JSON mod (most third-party mods on the Mod DB) will still show real findings this way:
anything it registers from C# is invisible to a JSON-only scan, so a reference to it reads as
unresolved. That is a limitation of what a JSON-only pass can know, not a defect in the check - see
extools' verify/ExlibVerify.Tests's own run over .compat/_im and .compat/industrialstory for
what this looks like against two real mods.
using ExpandedLib.Checks;
IReadOnlyList<CheckResult> results = ExlibChecks.All(api);
foreach (CheckResult result in results.Where(r => r.Errors.Count > 0))
DoSomethingWith(result);All(ICoreAPI) is the in-game path, over a fresh AssetCheckSource. All(ICheckSource) runs the
same checks against any source you build yourself - useful for a build-time script, a CI job, or a
tool that reads from somewhere other than a running game.
The eight checks above are exlib's own; a mod's own content invariant - every machine's job table names a registered item, every diagram has a shape - gets the same three rungs with one line of its own, none of them a call.
Zero-config. Nothing changes about the shipped checks: they keep running as Rung 1, 2 and 3 above regardless of whether you add one of your own.
Declarative: [ExCheckRegister]. Write a class exposing static CheckResult Run(ICheckSource, string) and mark it. Declare it a plain (non-static) class - the scan that finds it skips
abstract types, and a C# static class compiles to one:
[ExCheckRegister]
public sealed class MyOwnCheck {
public static CheckResult Run(ICheckSource source, string domain) {
var errors = new List<string>();
// ... your rule against source.BlockCodes, source.Recipes(domain), etc.
return new CheckResult("MyOwn", domain, errors);
}
}ExCheckRegistry.RegisterAll scans for it the same way EntityRegistry.RegisterAll scans for
[BlockRegister], and runs automatically from a deriving ExModSystem's or a module's own Start -
there is nothing to call. A class carrying the attribute but not shaped exactly this way is warned
about and skipped; the same class scanned twice (a rejoined world, a module and its host sharing an
assembly) is registered once and every later scan is silently ignored.
Explicit: ExlibChecks.All. Once registered, your check is appended after the eight shipped
ones, in registration order, and runs at every rung above - the AssetsFinalize log line,
/exmod verify, and ExlibChecks.All from your own code - with no further wiring. A throw from
Run is caught and reported as one error naming your check, the way ExModuleHost.Isolate wraps a
module phase, so one bad rule does not take the other checks down with it.
Implement the five members - Domains, BlockCodes, ItemCodes, Recipes(domain),
Lang(domain), BlockDefinitions(domain) - over whatever you're validating, and every check runs
unmodified, with one exception: LateDefinitionCheck ignores the source it is handed and reads
ExDefinitions, the process-wide registry ExDefinitionModSystem injects from, directly. A custom
source not backed by a live game process - exlib-verify, a CI job reading a repository tree - can
never make it report; it still prints a "0 error(s)" line, which reads as a pass. AssetCheckSource
and the harness's RepoCheckSource are the two shipped implementations; reading either is the
fastest way to see what each member is expected to answer.
Seven of the eight checks above started life as a validator in exlib.testing's Checks/ folder,
used from each mod's xUnit suite. MultiblockCodesCheck, RecipeCodesCheck, LangCoverageCheck,
CodePrefixCollisionCheck, PinnedNetworkNodesCheck and DefinitionCatalogueCheck moved cleanly:
everything they need is expressible over ICheckSource. LateDefinitionCheck is the eighth: it
never lived in the harness, since there was nothing there to replay ExDefinitionModSystem.AssetsLoaded.
NetworkNodeContractCheck did not move in full. The harness's own NetworkNodeContract selects a
"network node" definition by C# class (BlockNetworkNode, BEBehaviorNetworkMember and their
subclasses), which needs an assembly to reflect over - something no ICheckSource can supply, in
game or in a repository tree read generically. The library version selects the same definitions by
the contract they declare in JSON instead (a behaviour named "ExOrientable" in network mode, and
the framework's own BEBehaviorNetworkMember key for a membership), which is everything every check
in this codebase
has needed so far but is a narrower rule than the harness's reflective one - see the class's own
remarks for exactly where the two can disagree. The harness's NetworkNodeContract stays as it was,
unchanged, for that reason; the wrappers over the other six now delegate into this library so the
rule is written once. See Testing-Harness for the harness side of this split.
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