Skip to content

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:

kotlin
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

kotlin
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:

DomainDefault ownerDefault prefixPurpose
INSTANCEqi_core / give / init / templateCore item data
LAYERstrengthen / forge / gem / enchant / socket / refine / embed / upgradesys_ / layer_Persistent modifications
RUNTIMEqi_admin / qi_ui / adminbuff_ / temp_ / ui_Temporary overrides / UI
kotlin
WriteDomainPolicy.validateLayerWrite(owner, layerId): Verdict
WriteDomainPolicy.validateRuntimeWrite(owner): Verdict

For 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

kotlin
// 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:

kotlin
build(definition, amount = 1, player = null, templateInstance = null): ItemStack?  // brand new
rebuild(item): ItemStack?                                                          // rebuild
applyLayerPatch(item, pack): Pair<ItemStack?, LayerWriteResult>
removeLayer(item, layerId): ItemStack

Assembly 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 in CompiledState.

Custom Layer Contributor

kotlin
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

kotlin
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

kotlin
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.


Next Steps