外观
定义与绑定 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(),
)
}
}