Layer & Assembly Pipeline
Belongs to: Developer · Related: Core Concepts · API Reference
A layer is a patch appended after item creation (drilling / socketing / strengthening / adding affixes), and the Assembly Pipeline is the process of composing a Definition + Instance + layers into an ItemStack. This chapter is the developer reference for this core mechanism.
1. What Is a Layer
A layer (QinhLayerPatchPack) is a patch stacked on top of a compiled definition, letting you modify an item without recompiling the Definition:
data class QinhLayerPatchPack(
val id: String,
val priority: Int = 0, // ordering (lower applies first)
val owner: String, // write-domain owner (gem/enchant/forge…)
val variables: Map<String,String> = emptyMap(), // variable patches
val lockRequests: Map<String,String> = emptyMap(), // variable locks
val meta: Map<String,String> = emptyMap(),
) {
fun sanitized(): QinhLayerPatchPack // filters protected keys
}Layer Types and Order
enum class QinhItemLayerType(val namespace: String, val order: Int) {
BASE("base", 10),
VARIABLES("variables", 20),
SECTION("section", 35),
AFFIX("affix", 36),
EXTERNAL("external", 100),
}Layers are stacked in ascending order of order.
2. Semantic Guard (LayerSemanticGuard)
Layer patches cannot modify combat-critical keys (to prevent breaking balance). The reject list includes attack_damage / crit / defense / health / mana / skill / strength / attack_speed / lifesteal and so on, as well as keys matching suspicious patterns like *attack*. sanitized() filters out these keys and warns.
To change attributes, go through a Provider, not a layer patch.
3. Write-Domain Policy (WriteDomainPolicy)
There are three domains — layer / instance / runtime — restricting who can modify what:
| Domain | Default owner | Default prefix | Purpose |
|---|---|---|---|
| INSTANCE | qi_core / give / init / template | — | Core item data |
| LAYER | strengthen / forge / gem / enchant / socket / refine / embed / upgrade | sys_ / layer_ | Persistent modifications |
| RUNTIME | qi_admin / qi_ui / admin | buff_ / temp_ / ui_ | Temporary overrides / UI |
WriteDomainPolicy.validateLayerWrite(owner, layerId): Verdict
WriteDomainPolicy.validateRuntimeWrite(owner): VerdictFor configuration see config.yml → write-domains. Overstepping writes raise an error when strict: true.
LayerWriteResult: OK / NOT_QINH_ITEM / DOMAIN_VIOLATION / PROVIDER_PATCH_FORBIDDEN.
4. Layer API
// write a patch (auto-validates the write domain)
val (item, result) = QinhItemsAPI.assembly().applyLayerPatch(stack, QinhLayerPatchPack(
id = "gem_1",
owner = "gem",
priority = 50,
variables = mapOf("gem_slot_1" to "amethyst"),
lockRequests = mapOf("gem_slot_1" to "gem"),
))
// read
QinhItemsAPI.assembly().readLayerPack(stack, "gem_1"): QinhLayerPatchPack?
QinhItemsAPI.assembly().layerStack(stack): List<QinhLayerPatchPack>
// remove
QinhItemsAPI.assembly().removeLayer(stack, "gem_1")
// type-safe layer value reads
QinhItemsAPI.layers().read(stack, "gem_1"): QinhLayerState?
QinhItemsAPI.layers().int(stack, "gem_1", "level"): Int?NBT serialization is done by LayerPatchCodec (stored as a YAML string), and stack management by LayerStack.
5. Assembly Pipeline (QinhItemAssemblyService)
Entry points:
build(definition, amount = 1, player = null, templateInstance = null): ItemStack? // brand new
rebuild(item): ItemStack? // rebuild
applyLayerPatch(item, pack): Pair<ItemStack?, LayerWriteResult>
removeLayer(item, layerId): ItemStackAssembly Sequence
assemble(definition, amount, player, freshGenerate, instance?, layerStack?)
├─ build QinhItemAssemblyContext (when freshGenerate, allocate seed per InstanceSeedPolicy)
├─ event QinhItemGenerateEvent (when freshGenerate, pre)
├─ event QinhItemAssembleEvent (pre)
├─ run each layer contributor in turn:
│ BaseLayerContributor(10) build base ItemStack
│ VariablesLayerContributor(20) resolve variables, render, apply locks, fire QinhItemCompiledEvent
│ SectionLayerContributor(35) inject sections
│ AffixLayerContributor(36) inject affixes
│ External(100) third-party
│ (each contributor returns a QinhItemLayerSnapshot, merged into compiledState)
├─ add layer.summary.* aggregate variables
├─ QinhItemTags.stamp(stack, id, version, hash)
├─ write instance data to NBT
├─ event QinhItemAssembleEvent (post)
└─ return ItemStack- A random seed is allocated only on brand-new generation; rebuild preserves the seed / instance and only re-renders.
- Variables are resolved by
QinhVariableEngine.resolveWithTrace(), and the result is stored inCompiledState.
Custom Layer Contributor
interface QinhItemLayerContributor {
val namespace: String
val order: Int
fun contribute(context: QinhItemAssemblyContext): QinhItemLayerSnapshot?
}
QinhIntegrationRegistry.registerLayerContributor(myContributor)6. Definition System
TemplateCompiler compiles YAML into a QinhItemDefinition: merge fragments → override → root fields, inject tier variables, migrate legacy effects, compile base values into ap, read actionLore / gem sockets / sections / affixes / enchants / resource packs. For merge rules see Fragments & Definitions.
7. Compile Trace
data class CompiledState(
val variableTrace: Map<String, VariableTrace.Entry>,
val conflicts: List<VariableConflict>,
val resolvedVariables: Map<String, String>,
val layerSnapshots: List<QinhItemLayerSnapshot>,
val compileEpoch: Long,
)CompileEpoch.next(): a monotonic counter incremented on each assembly.CompileSessionStore: an LRU cache of compile results (up to 4096, keyed by item fingerprint).
8. Library Manifest
data class LibraryManifest(
val id: String, val displayName: String, val version: String,
val description: String, val category: String,
val types: List<String>, val capabilities: Set<TypeCapability>,
val dependencies: List<String>, val author: String,
)
LibraryManifestRegistry.get(id) / all() / reload()For the server-owner-facing explanation see Fragments & Definitions → Library Manifest.