Skip to content

定义与绑定 API 类型参考

本页是 API baseline 1.3 候选源码的公开签名参考。先阅读第三方插件接入教程,按其中的 provider 绑定、owner epoch、执行上下文和释放规则调用。

版本边界

正式交付 1.0.8 与 1.0.12 候选并不等价。依赖版本必须与服务器实际 JAR 配套,并重新编译、在目标服务端验收。

SealAttributesApiProvider.kt

kotlin
package cn.sealplugins.attributes.api

import cn.sealplugins.attributes.api.internal.requireNamespace
import java.util.Optional

/**
 * Platform service used to acquire a provider-bound [SealAttributesApi].
 *
 * Paper registers one implementation in its service manager. The caller supplies a class loaded
 * by its own plugin class loader; the Paper adapter resolves that class to the real plugin owner,
 * binds the canonical namespace and creates the owner epoch. No platform type enters this artifact.
 */
public interface SealAttributesApiProvider {
    public fun bind(providerId: String, ownerAnchor: Class<*>): ProviderBindingResult
}

/** Typed provider binding outcome. */
public class ProviderBindingResult(
    public val status: ProviderBindingStatus,
    providerId: Optional<String>,
    public val ownerEpoch: Long,
    api: Optional<SealAttributesApi>,
) {
    public val providerId: Optional<String> = providerId.map { requireNamespace(it, "providerId") }
    public val api: Optional<SealAttributesApi> = api

    init {
        require(ownerEpoch >= 0L) { "ownerEpoch must not be negative" }
        require((status == ProviderBindingStatus.BOUND) == this.api.isPresent) {
            "only BOUND results may contain an API facade"
        }
        if (status == ProviderBindingStatus.BOUND) {
            require(ownerEpoch > 0L) { "bound providers require a positive ownerEpoch" }
            require(this.providerId.isPresent) { "bound providers require a canonical providerId" }
            require(this.api.orElseThrow().providerId() == this.providerId.orElseThrow()) {
                "bound facade providerId does not match the result"
            }
            require(this.api.orElseThrow().ownerEpoch() == ownerEpoch) {
                "bound facade ownerEpoch does not match the result"
            }
        }
    }

    public fun bound(): Boolean = status == ProviderBindingStatus.BOUND
}

public enum class ProviderBindingStatus {
    BOUND,
    INVALID_NAMESPACE,
    OWNER_NOT_FOUND,
    NAMESPACE_CONFLICT,
    PLATFORM_NOT_READY,
    CLOSED,
}

SealAttributesApiMetadata.kt

kotlin
package cn.sealplugins.attributes.api

/** Version metadata for the separately published developer API artifact. */
public object SealAttributesApiMetadata {

    public const val ARTIFACT_ID: String = "sealattributes-api"

    public const val BASELINE: String = "1.3"
}

SealAttributesApi.kt

kotlin
package cn.sealplugins.attributes.api

import cn.sealplugins.attributes.api.combat.CombatService
import cn.sealplugins.attributes.api.definition.DefinitionRegistry
import cn.sealplugins.attributes.api.event.SealAttributesEvents
import cn.sealplugins.attributes.api.healing.HealingService
import cn.sealplugins.attributes.api.resource.ResourceService
import cn.sealplugins.attributes.api.source.PersistentSourcePublisher
import cn.sealplugins.attributes.api.source.SourcePublisher
import cn.sealplugins.attributes.api.subject.AttributeQuery
import cn.sealplugins.attributes.api.subject.QueryResult

/**
 * Provider-bound entry point exposed by the platform adapter.
 *
 * A facade is valid only while [ownerEpoch] is current. Implementations must reject calls made
 * through an expired facade with a typed result; they must never silently adopt a new owner epoch.
 * Acquiring a facade is platform-specific and intentionally not implemented as a global singleton.
 */
public interface SealAttributesApi {

    /** Semantic API baseline implemented by this facade. */
    public fun apiVersion(): String

    /** Canonical provider namespace bound by the platform adapter. */
    public fun providerId(): String

    /** Positive lease epoch that invalidates every handle when the provider is reloaded. */
    public fun ownerEpoch(): Long

    /**
     * Live state of the built-in reader for the main hand, off hand and four armor slots.
     *
     * External equipment owners may publish slot attributes only when this query is [QueryResult.found]
     * and its value is `false`. Every missing or stale result must be treated as enabled so callers
     * fail closed into read-only mode.
     */
    public fun builtInEquipmentReaderEnabled(): QueryResult<Boolean>

    /** Startup-only definition and extension registration surface. */
    public fun definitions(): DefinitionRegistry

    /** Synchronous, in-memory runtime source publisher. */
    public fun sources(): SourcePublisher

    /** Durable publisher whose methods never block the caller on database I/O. */
    public fun persistentSources(): PersistentSourcePublisher

    /** Synchronous deterministic combat entry point for a legal entity execution context. */
    public fun combat(): CombatService

    /** Synchronous active-healing entry point for a legal entity execution context. */
    public fun healing(): HealingService

    /** Thread-safe O(1) reads of committed immutable subject state. */
    public fun queries(): AttributeQuery

    /** Synchronous, atomic access to committed dynamic resources. */
    public fun resources(): ResourceService

    /** Read-only post-commit event subscriptions owned by this provider lease. */
    public fun events(): SealAttributesEvents
}

ApiLimits.kt

kotlin
package cn.sealplugins.attributes.api

/** Hard safety limits shared by all V1 API implementations. */
public object ApiLimits {

    public const val MAX_CANONICAL_ID_LENGTH: Int = 128
    public const val MAX_DISPLAY_NAME_LENGTH: Int = 64
    public const val MAX_ATTRIBUTES_PER_SNAPSHOT: Int = 512
    public const val MAX_REGISTERED_FORMULAS: Int = 128
    public const val MAX_REGISTERED_COMBAT_EXTENSIONS: Int = 128
    public const val MAX_REGISTERED_PLATFORM_EXTENSIONS: Int = 128
    public const val MAX_REGISTERED_COMBAT_GATES: Int = 128
    public const val MAX_REGISTERED_COMBAT_PLANS: Int = 256
    public const val MAX_REGISTERED_RESOURCES: Int = 64
    /** Legacy ABI field; [SourceSnapshot][cn.sealplugins.attributes.api.source.SourceSnapshot] no longer enforces it. */
    @Deprecated("SourceSnapshot no longer has an independent contribution-count limit")
    public const val MAX_CONTRIBUTIONS_PER_SOURCE: Int = 64
    public const val MAX_SOURCES_PER_SUBJECT: Int = 256
    public const val MAX_AGGREGATION_FAILURES: Int = 16
    public const val MAX_COMBAT_MODIFIERS_PER_EXTENSION: Int = 16
    public const val MAX_COMBAT_DIAGNOSTICS: Int = 16
    public const val MAX_PLATFORM_INTENTS_PER_EXTENSION: Int = 16
}

ResourceId.kt

kotlin
package cn.sealplugins.attributes.api.definition

import cn.sealplugins.attributes.api.internal.requireNamespace
import cn.sealplugins.attributes.api.internal.requireNamespacedId
import cn.sealplugins.attributes.api.internal.requirePath

/** Canonical, case-sensitive `namespace:path` identifier for a dynamic resource. */
public class ResourceId private constructor(
    public val namespace: String,
    public val path: String,
) : Comparable<ResourceId> {
    public val value: String = "$namespace:$path"

    override fun compareTo(other: ResourceId): Int = value.compareTo(other.value)
    override fun equals(other: Any?): Boolean = other is ResourceId && value == other.value
    override fun hashCode(): Int = value.hashCode()
    override fun toString(): String = value

    public companion object {
        @JvmStatic
        public fun of(namespace: String, path: String): ResourceId {
            val canonicalNamespace = requireNamespace(namespace, "resource namespace")
            val canonicalPath = requirePath(path, "resource path")
            requireNamespacedId("$canonicalNamespace:$canonicalPath", "resourceId")
            return ResourceId(canonicalNamespace, canonicalPath)
        }

        @JvmStatic
        public fun parse(value: String): ResourceId {
            requireNamespacedId(value, "resourceId")
            val separator = value.indexOf(':')
            return ResourceId(value.substring(0, separator), value.substring(separator + 1))
        }
    }
}

ResourceDefinition.kt

kotlin
package cn.sealplugins.attributes.api.definition

import cn.sealplugins.attributes.api.internal.requireDisplayName
import java.util.Optional

/** Immutable startup definition linking one current resource to its aggregate attributes. */
public class ResourceDefinition(
    public val id: ResourceId,
    displayName: String,
    public val maximumAttribute: AttributeId,
    flatRegenerationAttribute: Optional<AttributeId>,
    percentRegenerationAttribute: Optional<AttributeId>,
    costReductionAttribute: Optional<AttributeId>,
) {
    public val displayName: String = requireDisplayName(displayName)
    public val flatRegenerationAttribute: Optional<AttributeId> = flatRegenerationAttribute
    public val percentRegenerationAttribute: Optional<AttributeId> = percentRegenerationAttribute
    public val costReductionAttribute: Optional<AttributeId> = costReductionAttribute

    override fun equals(other: Any?): Boolean = other is ResourceDefinition &&
        id == other.id &&
        displayName == other.displayName &&
        maximumAttribute == other.maximumAttribute &&
        flatRegenerationAttribute == other.flatRegenerationAttribute &&
        percentRegenerationAttribute == other.percentRegenerationAttribute &&
        costReductionAttribute == other.costReductionAttribute

    override fun hashCode(): Int = listOf(
        id,
        displayName,
        maximumAttribute,
        flatRegenerationAttribute,
        percentRegenerationAttribute,
        costReductionAttribute,
    ).hashCode()

    override fun toString(): String = "ResourceDefinition(id=$id, maximum=$maximumAttribute)"
}

RegistrationResult.kt

kotlin
package cn.sealplugins.attributes.api.definition

import cn.sealplugins.attributes.api.internal.requireNamespacedId
import java.util.Optional

/** Typed outcome for every startup registration attempt. */
public class RegistrationResult(
    public val status: RegistrationStatus,
    registrationId: Optional<String>,
) {

    /** Canonical attempted ID when one could be obtained safely; invalid callbacks leave it empty. */
    public val registrationId: Optional<String> = registrationId.map {
        requireNamespacedId(it, "registrationId")
    }

    init {
        require(status != RegistrationStatus.ACCEPTED || this.registrationId.isPresent) {
            "accepted registrations require a canonical registrationId"
        }
    }

    public fun accepted(): Boolean = status == RegistrationStatus.ACCEPTED

    override fun toString(): String = "RegistrationResult(status=$status, registrationId=$registrationId)"
}

/** Registration failures are stable codes and never need to be parsed from log text. */
public enum class RegistrationStatus {
    ACCEPTED,
    WINDOW_CLOSED,
    DUPLICATE_ID,
    INVALID_DEFINITION,
    NAMESPACE_MISMATCH,
    MISSING_DEPENDENCY,
    KIND_MISMATCH,
    OWNER_EXPIRED,
    LIMIT_EXCEEDED,
}

/** Structural registration state; only [OPEN] accepts new definitions or extensions. */
public enum class RegistrationWindow {
    OPEN,
    FROZEN,
    CLOSED,
}

FormulaProvider.kt

kotlin
package cn.sealplugins.attributes.api.definition

import cn.sealplugins.attributes.api.internal.requireFinite
import cn.sealplugins.attributes.api.internal.requireNamespacedId
import cn.sealplugins.attributes.api.internal.requirePriority as requireContractPriority
import cn.sealplugins.attributes.api.subject.AttributeSnapshot
import java.util.Optional

/**
 * Startup-registered deterministic formula SPI.
 *
 * Evaluation is synchronous on the same platform-approved subject execution context as the
 * calculation that requested it. Providers are ordered by `priority`, provider namespace,
 * provider ID and attribute ID. Implementations receive immutable values only and must not
 * perform platform access, I/O, mutable global reads or reentrant SealAttributes mutations.
 * An exception, null return from Java or non-finite result discards only that invocation, records
 * a typed combat diagnostic and continues with the value that existed before the provider ran.
 */
public interface FormulaProvider {
    public fun id(): String
    public fun priority(): Int
    public fun evaluate(context: FormulaContext): FormulaResult
}

/** Immutable input available to a custom formula provider. */
public class FormulaContext(
    public val attributeId: AttributeId,
    public val subject: AttributeSnapshot,
    target: Optional<AttributeSnapshot>,
    baseValue: Double,
) {
    public val target: Optional<AttributeSnapshot> = target
    public val baseValue: Double = requireFinite(baseValue, "baseValue")
}

/** A formula result is a single deterministic finite scalar. */
public class FormulaResult(value: Double) {
    public val value: Double = requireFinite(value, "formula result")
}

/** Shared validation helpers for implementations that register formula providers. */
public object FormulaProviderContract {
    @JvmStatic
    public fun requireId(value: String): String = requireNamespacedId(value, "formula provider id")

    @JvmStatic
    public fun requirePriority(value: Int): Int = requireContractPriority(value)
}

ExtensionKind.kt

kotlin
package cn.sealplugins.attributes.api.definition

/** Declares whether a custom attribute has controlled behavior beyond storage and query. */
public enum class ExtensionKind {
    DATA_ONLY,
    COMBAT_EXTENSION,
    PAPER_EXTENSION,
}

DefinitionRegistry.kt

kotlin
package cn.sealplugins.attributes.api.definition

import cn.sealplugins.attributes.api.combat.CombatExtension
import cn.sealplugins.attributes.api.platform.PlatformExtension
import java.util.Optional

/**
 * Provider-bound structural registry.
 *
 * Registration is legal only during [RegistrationWindow.OPEN]. Reads are thread-safe after the
 * window freezes; returned collections and definitions are immutable snapshots.
 */
public interface DefinitionRegistry {
    public fun registrationWindow(): RegistrationWindow
    public fun register(definition: AttributeDefinition): RegistrationResult
    public fun registerResource(definition: ResourceDefinition): RegistrationResult
    public fun registerFormula(provider: FormulaProvider): RegistrationResult
    public fun registerCombatExtension(extension: CombatExtension): RegistrationResult
    public fun registerPlatformExtension(extension: PlatformExtension): RegistrationResult
    public fun find(id: AttributeId): Optional<AttributeDefinition>
    public fun all(): List<AttributeDefinition>
    public fun findResource(id: ResourceId): Optional<ResourceDefinition>
    public fun allResources(): List<ResourceDefinition>
}

AttributeType.kt

kotlin
package cn.sealplugins.attributes.api.definition

/** Business meaning of a scalar attribute value. */
public enum class AttributeType {
    /** Fixed amount such as health, attack, defense or regeneration per second. */
    AMOUNT,

    /** Percentage-point value represented as a decimal, where `0.25` means 25%. */
    PERCENT,

    /** Rating points converted by a deterministic formula at combat time. */
    RATING,
}

AttributeId.kt

kotlin
package cn.sealplugins.attributes.api.definition

import cn.sealplugins.attributes.api.internal.requireNamespace
import cn.sealplugins.attributes.api.internal.requireNamespacedId
import cn.sealplugins.attributes.api.internal.requirePath

/** Canonical, case-sensitive `namespace:path` identifier for an attribute definition. */
public class AttributeId private constructor(
    public val namespace: String,
    public val path: String,
) : Comparable<AttributeId> {

    public val value: String = "$namespace:$path"

    override fun compareTo(other: AttributeId): Int = value.compareTo(other.value)

    override fun equals(other: Any?): Boolean = other is AttributeId && value == other.value

    override fun hashCode(): Int = value.hashCode()

    override fun toString(): String = value

    public companion object {

        /** Creates an ID without silently lowercasing or trimming either component. */
        @JvmStatic
        public fun of(namespace: String, path: String): AttributeId {
            val canonicalNamespace = requireNamespace(namespace, "attribute namespace")
            val canonicalPath = requirePath(path, "attribute path")
            requireNamespacedId("$canonicalNamespace:$canonicalPath", "attributeId")
            return AttributeId(canonicalNamespace, canonicalPath)
        }

        /** Parses an already canonical ID; malformed or non-normalized input is rejected. */
        @JvmStatic
        public fun parse(value: String): AttributeId {
            requireNamespacedId(value, "attributeId")
            val separator = value.indexOf(':')
            return AttributeId(value.substring(0, separator), value.substring(separator + 1))
        }
    }
}

AttributeDefinition.kt

kotlin
package cn.sealplugins.attributes.api.definition

import cn.sealplugins.attributes.api.internal.requireDisplayName
import cn.sealplugins.attributes.api.internal.requireFinite
import cn.sealplugins.attributes.api.internal.requireNamespacedId
import java.util.Optional

/** Immutable startup-time definition of one scalar attribute. */
public class AttributeDefinition(
    public val id: AttributeId,
    displayName: String,
    public val type: AttributeType,
    defaultValue: Double,
    public val extensionKind: ExtensionKind,
    behaviorId: Optional<String>,
    formulaProviderId: Optional<String>,
) {

    public val displayName: String = requireDisplayName(displayName)
    public val defaultValue: Double = requireFinite(defaultValue, "defaultValue")
    public val behaviorId: Optional<String> = behaviorId.map { requireNamespacedId(it, "behaviorId") }
    public val formulaProviderId: Optional<String> =
        formulaProviderId.map { requireNamespacedId(it, "formulaProviderId") }

    init {
        when (extensionKind) {
            ExtensionKind.DATA_ONLY -> require(this.behaviorId.isEmpty && this.formulaProviderId.isEmpty) {
                "DATA_ONLY definitions cannot name behavior or formula providers"
            }

            ExtensionKind.COMBAT_EXTENSION,
            ExtensionKind.PAPER_EXTENSION,
            -> require(this.behaviorId.isPresent) {
                "$extensionKind definitions must name a behaviorId"
            }
        }
    }

    override fun equals(other: Any?): Boolean = other is AttributeDefinition &&
        id == other.id &&
        displayName == other.displayName &&
        type == other.type &&
        defaultValue.toBits() == other.defaultValue.toBits() &&
        extensionKind == other.extensionKind &&
        behaviorId == other.behaviorId &&
        formulaProviderId == other.formulaProviderId

    override fun hashCode(): Int = listOf(
        id,
        displayName,
        type,
        defaultValue.toBits(),
        extensionKind,
        behaviorId,
        formulaProviderId,
    ).hashCode()

    override fun toString(): String = "AttributeDefinition(id=$id, type=$type, kind=$extensionKind)"

    public companion object {
        /** Java/Kotlin friendly path for an attribute with no declared behavior. */
        @JvmStatic
        public fun dataOnly(
            id: AttributeId,
            displayName: String,
            type: AttributeType,
            defaultValue: Double,
        ): AttributeDefinition = AttributeDefinition(
            id,
            displayName,
            type,
            defaultValue,
            ExtensionKind.DATA_ONLY,
            Optional.empty(),
            Optional.empty(),
        )
    }
}