Library packages: extraction and consumption
How to split reusable code out of a game into an s&box Library package, and how to consume one from a game, without silently losing content, binding the wrong types, or shipping divergent assets.
How to split reusable code out of a game into an s&box Library package, and how to consume
one from a game, without silently losing content, binding the wrong types, or shipping
divergent assets. Verified on engine 26.07.18 on a real library-inversion flip: a game
consuming Library packages, gated by an offline two-assembly dotnet build
(0 warnings 0 errors) plus content diffs.
Platform facts
- Libraries are SOURCE-distributed. A consumed library's C#/razor/assets compile inside the consumer's build; there is no prebuilt-binary distribution for game consumption.
- Consumed libraries land under the project's
Libraries/folder. - Each library folder must contain exactly ONE library-type
.sbprojor the editor silently skips it. Discovery scansLibraries/*at project load only; a folder with zero (the hand-vendored case) or multiple.sbprojfiles never becomes a library project, and the game compile fails CS0246 on every library type. Hand-vendored kits need a minimal{"Title", "Type": "library", "Org", "Ident", "Schema": 1}project file added, and a LIVE editor must be closed and reopened to pick it up (hot recompile does not rescan). Once discovered, local libraries are auto-referenced; noPackageReferencesentry needed. - No library-to-library references. A library cannot consume another library; anything shared across libraries must be duplicated or folded into one.
- Publishing goes through the editor's Library Manager (same package pipeline as games, library package type).
Vendored-copy canonicality
When a game vendors a library's source (instead of or in addition to package consumption), the vendored copy must stay BYTE-IDENTICAL to its canonical upstream. Never fix a consumption problem by editing vendored files; every technique below exists so the fix can live on the consumer side:
- Name collisions: pin with a
global usingalias in the game'sAssembly.cs, never a rename inside the library. Enclosing-namespace members beat compilation-unit aliases, so library files are untouched by the game's aliases. - Behavior differences: wrap the library's seam on the consumer side, never patch the library path (see seam inventory below).
- Asset differences: override paths on the game side (see byte-identity below).
Verify canonicality mechanically: content-diff the vendored tree against upstream at vendor time and on every sync (URL + commit of the upstream in the vendor manifest).
The extraction/consumption checklist
1. Split before you delete
A game file that defines a now-library-provided type often ALSO carries game-only content (a tuned data roster, constants, extension members in the same file). Deleting the file because "the library provides it" drops that content silently; the compile stays green when shapes match. Split game-only content into its own file FIRST, then delete only the true duplicate, and content-diff the deletion against what the library provides (every deleted hunk must exist in the library).
2. Pin every surviving name collision, even when the build is green
If the game keeps a type whose simple name collides with a library type of compatible
shape, both bindings are legal and the compiler warns about nothing; a green build does not
tell you which type your code is using. Add a global using X = Game.Namespace.X; alias
for every such name as a matter of policy. The loud variants of this trap (CS0104, CS0234
namespace-vs-class) at least fail; the same-shape variant is silent.
3. Inventory the seam's side effects
An extraction seam (a delegate that swaps one behavior, e.g. a body builder) usually carries only the headline behavior. List EVERY side effect of the replaced code path (visual mounting, data-driven transforms, perf timing, audit/log lines, registrations) and reproduce the missing ones in a consumer-side wrapper. Physics note: part colliders shift the inertia tensor, so assembly differences move telemetry — they are not cosmetic.
4. Byte-identity discipline for colliding asset paths
A vendored library shipping assets at the same bare resource path as the game's own copies
is behavior-neutral ONLY if every colliding file is byte-identical (cmp/hash the pairs,
including the whole reference chain: a .sound and the audio it points at). If any pair
differs, the game must override/re-point its paths. Re-run the sweep on every vendor sync.
5. Razor consumers need explicit @using
Razor component TAG resolution ignores global using: every .razor file rendering
library components needs explicit @using lines for those namespaces, and RZ10012 must
be treated as an error in review (it is a warning that silently renders a plain element).
The offline two-assembly compile gate
When no editor-generated csproj exists (fresh worktree) or the live editor cannot be touched, gate the whole stack headlessly:
- Write a scratch csproj per assembly (library and game), SDK
Microsoft.NET.Sdk.Razorso.razorfiles compile, referencing the editor install's prebuiltBase Library.dllby absolute path. <GenerateAssemblyInfo>false</GenerateAssemblyInfo>plus a FRESHobjdirectory per build (dodgesCS0579duplicate assembly attributes from stale generated info).- Build the LIBRARY assembly first; then build the GAME assembly referencing the produced kit dll. This mirrors the consumption topology the editor creates.
- Gate on 0 warnings 0 errors, and treat
RZ10012as a failure.
Limits (same as every headless gate): dotnet build compiles no SCSS, enforces no
whitelist, and proves compile-level truth only; binding intent (step 2 above) and seam
completeness (step 3) still need diffs and review. It also CANNOT catch library-discovery
failures: the scratch csproj compiles the library source directly, so a vendored kit
missing its .sbproj gates green offline and still fails in the editor (see platform
facts above).
- Published