外观
来源、查询与事件 API 类型参考
本页是 API baseline 1.3 候选源码的公开签名参考。先阅读第三方插件接入教程,按其中的 provider 绑定、owner epoch、执行上下文和释放规则调用。
版本边界
正式交付 1.0.8 与 1.0.12 候选并不等价。依赖版本必须与服务器实际 JAR 配套,并重新编译、在目标服务端验收。
SubjectState.kt
kotlin
package cn.sealplugins.attributes.api.subject
/** Immutable lifecycle and revision tuple read atomically for one subject. */
public class SubjectState(
public val key: SubjectKey,
public val readiness: SubjectReadiness,
public val reason: ReadinessReason,
public val registryGeneration: Long,
public val runtimeRevision: Long,
public val snapshotRevision: Long,
) {
init {
require(registryGeneration > 0L) { "registryGeneration must be positive" }
require(runtimeRevision >= 0L) { "runtimeRevision must not be negative" }
require(snapshotRevision >= 0L) { "snapshotRevision must not be negative" }
if (readiness == SubjectReadiness.READY) {
require(reason == ReadinessReason.NONE) { "READY subjects must use reason NONE" }
} else {
require(reason != ReadinessReason.NONE) { "$readiness subjects must provide a reason" }
}
}
/** True only when entity combat may consume this state. */
public fun combatReady(): Boolean = readiness == SubjectReadiness.READY
override fun equals(other: Any?): Boolean = other is SubjectState &&
key == other.key &&
readiness == other.readiness &&
reason == other.reason &&
registryGeneration == other.registryGeneration &&
runtimeRevision == other.runtimeRevision &&
snapshotRevision == other.snapshotRevision
override fun hashCode(): Int = listOf(
key,
readiness,
reason,
registryGeneration,
runtimeRevision,
snapshotRevision,
).hashCode()
override fun toString(): String =
"SubjectState(key=$key, readiness=$readiness, snapshotRevision=$snapshotRevision)"
}SubjectReadiness.kt
kotlin
package cn.sealplugins.attributes.api.subject
/** Public lifecycle state used by queries, combat fail-closed checks and PAPI. */
public enum class SubjectReadiness {
NOT_READY,
READY,
QUIESCING,
CLOSED,
}
/** Typed explanation for a subject's current readiness state. */
public enum class ReadinessReason {
NONE,
PRELOADING,
JOIN_INITIALIZING,
ITEM_MODE_RECONFIGURING,
STORAGE_LOADING,
SHUTTING_DOWN,
SESSION_CLOSED,
}SubjectKey.kt
kotlin
package cn.sealplugins.attributes.api.subject
import java.util.UUID
/** Stable entity identity plus a positive session epoch. */
public class SubjectKey(
public val uniqueId: UUID,
public val epoch: Long,
) : Comparable<SubjectKey> {
init {
require(epoch > 0L) { "subject epoch must be positive" }
}
override fun compareTo(other: SubjectKey): Int {
val idOrder = uniqueId.toString().compareTo(other.uniqueId.toString())
return if (idOrder != 0) idOrder else epoch.compareTo(other.epoch)
}
override fun equals(other: Any?): Boolean =
other is SubjectKey && uniqueId == other.uniqueId && epoch == other.epoch
override fun hashCode(): Int = 31 * uniqueId.hashCode() + epoch.hashCode()
override fun toString(): String = "$uniqueId@$epoch"
}EffectiveHealthSnapshot.kt
kotlin
package cn.sealplugins.attributes.api.subject
import cn.sealplugins.attributes.api.internal.requireFinite
import java.time.Instant
/** Immutable Paper-proven health view; reads never access a live entity or database. */
public class EffectiveHealthSnapshot(
public val subject: SubjectKey,
currentHealth: Double,
effectiveMaxHealth: Double,
public val revision: Long,
public val observedAt: Instant,
) {
public val currentHealth: Double = requireFinite(currentHealth, "currentHealth")
public val effectiveMaxHealth: Double = requireFinite(effectiveMaxHealth, "effectiveMaxHealth")
init {
require(this.currentHealth >= 0.0) { "currentHealth must not be negative" }
require(this.effectiveMaxHealth > 0.0) { "effectiveMaxHealth must be positive" }
require(this.currentHealth <= this.effectiveMaxHealth) {
"currentHealth cannot exceed effectiveMaxHealth"
}
require(revision > 0L) { "health revision must be positive" }
}
override fun equals(other: Any?): Boolean = other is EffectiveHealthSnapshot &&
subject == other.subject &&
currentHealth.toBits() == other.currentHealth.toBits() &&
effectiveMaxHealth.toBits() == other.effectiveMaxHealth.toBits() &&
revision == other.revision &&
observedAt == other.observedAt
override fun hashCode(): Int {
var result = subject.hashCode()
result = 31 * result + currentHealth.hashCode()
result = 31 * result + effectiveMaxHealth.hashCode()
result = 31 * result + revision.hashCode()
return 31 * result + observedAt.hashCode()
}
}AttributeSnapshot.kt
kotlin
package cn.sealplugins.attributes.api.subject
import cn.sealplugins.attributes.api.ApiLimits
import cn.sealplugins.attributes.api.definition.AttributeId
import cn.sealplugins.attributes.api.internal.immutableMap
import cn.sealplugins.attributes.api.internal.requireFinite
import java.util.OptionalDouble
/**
* Atomically committed immutable attribute values for one subject state.
*
* The backing numeric array, registry indices and locks are never exposed. [value] is O(1).
*/
public class AttributeSnapshot(
public val state: SubjectState,
values: Map<AttributeId, Double>,
) {
private val committedValues: Map<AttributeId, Double> = immutableMap(
values,
ApiLimits.MAX_ATTRIBUTES_PER_SNAPSHOT,
"attribute snapshot",
).also { committed ->
committed.forEach { (id, value) -> requireFinite(value, "attribute value $id") }
}
public fun value(id: AttributeId): OptionalDouble {
val value = committedValues[id]
return if (value == null) OptionalDouble.empty() else OptionalDouble.of(value)
}
public fun valueOr(id: AttributeId, fallback: Double): Double =
committedValues[id] ?: requireFinite(fallback, "fallback")
/** Immutable map view intended for administrative UI and diagnostics, not combat hot paths. */
public fun entries(): Map<AttributeId, Double> = committedValues
public fun size(): Int = committedValues.size
override fun equals(other: Any?): Boolean =
other is AttributeSnapshot && state == other.state && committedValues == other.committedValues
override fun hashCode(): Int = 31 * state.hashCode() + committedValues.hashCode()
override fun toString(): String = "AttributeSnapshot(state=$state, size=${committedValues.size})"
}AttributeQuery.kt
kotlin
package cn.sealplugins.attributes.api.subject
import java.util.Optional
import java.util.UUID
/** Thread-safe committed-state queries; implementations must not trigger entity or database reads. */
public interface AttributeQuery {
/** Resolves the current session epoch from a platform entity UUID without exposing Bukkit. */
public fun currentSubject(uniqueId: UUID): QueryResult<SubjectKey>
public fun state(subject: SubjectKey): QueryResult<SubjectState>
public fun snapshot(subject: SubjectKey): QueryResult<AttributeSnapshot>
/** Last Paper-proven current/effective maximum health; never reads a live entity. */
public fun effectiveHealth(subject: SubjectKey): QueryResult<EffectiveHealthSnapshot>
}
/** Typed lookup outcome that distinguishes absence from an expired subject epoch. */
public class QueryResult<T : Any> private constructor(
public val status: QueryStatus,
value: Optional<T>,
) {
public val value: Optional<T> = value
init {
require((status == QueryStatus.FOUND) == value.isPresent) {
"only FOUND query results may contain a value"
}
}
public fun found(): Boolean = status == QueryStatus.FOUND
public companion object {
@JvmStatic
public fun <T : Any> found(value: T): QueryResult<T> =
QueryResult(QueryStatus.FOUND, Optional.of(value))
@JvmStatic
public fun <T : Any> missing(status: QueryStatus): QueryResult<T> {
require(status != QueryStatus.FOUND) { "missing status cannot be FOUND" }
return QueryResult(status, Optional.empty())
}
}
}
public enum class QueryStatus {
FOUND,
NOT_FOUND,
STALE_SUBJECT,
STALE_OWNER,
QUIESCING,
CLOSED,
}SourceSnapshot.kt
kotlin
package cn.sealplugins.attributes.api.source
import cn.sealplugins.attributes.api.internal.immutableList
import java.time.Instant
import java.util.Optional
/** Complete immutable replacement for one [SourceKey]. */
public class SourceSnapshot(
public val key: SourceKey,
public val revision: Long,
contributions: Collection<SourceContribution>,
public val persistence: SourcePersistence,
expiresAt: Optional<Instant>,
) {
public val contributions: List<SourceContribution> = immutableList(contributions, "source contributions")
public val expiresAt: Optional<Instant> = expiresAt
init {
require(revision > 0L) { "source revision must be positive" }
require(this.contributions.isNotEmpty()) { "source contributions must not be empty" }
val uniqueOperations = this.contributions.map { it.attributeId to it.operation }.toSet()
require(uniqueOperations.size == this.contributions.size) {
"a source cannot repeat the same attribute and operation"
}
when (persistence) {
SourcePersistence.EXPIRES_AT -> require(this.expiresAt.isPresent) {
"EXPIRES_AT sources require an expiry instant"
}
SourcePersistence.RUNTIME,
SourcePersistence.PERMANENT,
-> require(this.expiresAt.isEmpty) {
"$persistence sources cannot provide an expiry instant"
}
}
}
override fun equals(other: Any?): Boolean = other is SourceSnapshot &&
key == other.key &&
revision == other.revision &&
contributions == other.contributions &&
persistence == other.persistence &&
expiresAt == other.expiresAt
override fun hashCode(): Int = listOf(key, revision, contributions, persistence, expiresAt).hashCode()
override fun toString(): String = "SourceSnapshot(key=$key, revision=$revision, size=${contributions.size})"
}
public enum class SourcePersistence {
RUNTIME,
PERMANENT,
EXPIRES_AT,
}SourcePublisher.kt
kotlin
package cn.sealplugins.attributes.api.source
import cn.sealplugins.attributes.api.subject.SubjectKey
/**
* Provider-bound in-memory source publisher.
*
* Calls are atomic and may be made from any thread with immutable input. Implementations must
* recheck subject epoch, owner epoch, registry generation and runtime revision before commit.
*/
public interface SourcePublisher {
public fun providerId(): String
public fun ownerEpoch(): Long
public fun publish(subject: SubjectKey, snapshot: SourceSnapshot): PublishResult
public fun withdraw(subject: SubjectKey, key: SourceKey, revision: Long): PublishResult
/**
* Applies all runtime replacements and withdrawals with one subject snapshot commit.
* If any mutation is invalid or stale, none of the mutations are applied.
*/
public fun mutate(subject: SubjectKey, mutations: Collection<SourceMutation>): BatchPublishResult
}SourceMutation.kt
kotlin
package cn.sealplugins.attributes.api.source
import cn.sealplugins.attributes.api.subject.SubjectKey
import java.util.Optional
/** One replacement or withdrawal inside an atomic runtime-source batch. */
public class SourceMutation private constructor(
public val key: SourceKey,
public val revision: Long,
replacement: Optional<SourceSnapshot>,
) {
public val replacement: Optional<SourceSnapshot> = replacement
init {
require(revision > 0L) { "source mutation revision must be positive" }
require(this.replacement.map { it.key == key && it.revision == revision }.orElse(true)) {
"replacement key and revision must match the mutation"
}
require(this.replacement.map { it.persistence == SourcePersistence.RUNTIME }.orElse(true)) {
"atomic batches accept RUNTIME sources only"
}
}
public companion object {
@JvmStatic
public fun replace(snapshot: SourceSnapshot): SourceMutation =
SourceMutation(snapshot.key, snapshot.revision, Optional.of(snapshot))
@JvmStatic
public fun withdraw(key: SourceKey, revision: Long): SourceMutation =
SourceMutation(key, revision, Optional.empty())
}
}
/** Result of one all-or-nothing runtime-source batch for a single subject. */
public class BatchPublishResult(
public val status: PublishStatus,
public val subject: SubjectKey,
public val ownerEpoch: Long,
public val mutationCount: Int,
public val snapshotRevision: Long,
) {
init {
require(ownerEpoch > 0L) { "ownerEpoch must be positive" }
require(mutationCount >= 0) { "mutationCount must not be negative" }
require(snapshotRevision >= 0L) { "snapshotRevision must not be negative" }
}
public fun subjectEpoch(): Long = subject.epoch
public fun successful(): Boolean =
status == PublishStatus.APPLIED || status == PublishStatus.UNCHANGED
}SourceKey.kt
kotlin
package cn.sealplugins.attributes.api.source
import cn.sealplugins.attributes.api.internal.requireNamespace
import cn.sealplugins.attributes.api.internal.requireNamespacedId
import cn.sealplugins.attributes.api.internal.requirePath
/** Provider-owned source identity; a newer snapshot with the same key replaces the older one. */
public class SourceKey(
providerId: String,
sourceId: String,
) : Comparable<SourceKey> {
public val providerId: String = requireNamespace(providerId, "source providerId")
public val sourceId: String = requirePath(sourceId, "source sourceId")
public val value: String = requireNamespacedId(
"${this.providerId}:${this.sourceId}",
"source key",
)
override fun compareTo(other: SourceKey): Int = value.compareTo(other.value)
override fun equals(other: Any?): Boolean = other is SourceKey && value == other.value
override fun hashCode(): Int = value.hashCode()
override fun toString(): String = value
}SourceContribution.kt
kotlin
package cn.sealplugins.attributes.api.source
import cn.sealplugins.attributes.api.definition.AttributeId
import cn.sealplugins.attributes.api.internal.requireFinite
/** One immutable contribution within a complete source replacement snapshot. */
public class SourceContribution(
public val attributeId: AttributeId,
public val operation: ContributionOperation,
value: Double,
) {
public val value: Double = requireFinite(value, "source contribution")
override fun equals(other: Any?): Boolean = other is SourceContribution &&
attributeId == other.attributeId &&
operation == other.operation &&
value.toBits() == other.value.toBits()
override fun hashCode(): Int = listOf(attributeId, operation, value.toBits()).hashCode()
override fun toString(): String = "$attributeId $operation $value"
}
/** V1 contribution operations; TOTAL_PERCENT is valid only for AMOUNT definitions. */
public enum class ContributionOperation {
ADD,
TOTAL_PERCENT,
}PublishResult.kt
kotlin
package cn.sealplugins.attributes.api.source
import cn.sealplugins.attributes.api.subject.SubjectKey
/** Typed result of a runtime or durable source mutation. */
public class PublishResult(
public val status: PublishStatus,
public val subject: SubjectKey,
public val source: SourceKey,
public val ownerEpoch: Long,
public val sourceRevision: Long,
public val snapshotRevision: Long,
) {
init {
require(ownerEpoch > 0L) { "ownerEpoch must be positive" }
require(sourceRevision >= 0L) { "sourceRevision must not be negative" }
require(snapshotRevision >= 0L) { "snapshotRevision must not be negative" }
}
public fun subjectEpoch(): Long = subject.epoch
public fun successful(): Boolean = when (status) {
PublishStatus.APPLIED,
PublishStatus.REMOVED,
PublishStatus.UNCHANGED,
PublishStatus.PERSISTED_SESSION_DEFERRED,
-> true
else -> false
}
}
/** Stable source mutation outcomes; storage errors never masquerade as success. */
public enum class PublishStatus {
APPLIED,
REMOVED,
UNCHANGED,
/**
* The durable mutation committed, but the requested live subject session could no longer
* accept it. Callers must treat the mutation as successful and must not retry the same
* business action; the durable value is materialized by a later current session.
*/
PERSISTED_SESSION_DEFERRED,
NOT_READY,
STALE_SUBJECT,
STALE_OWNER,
STALE_REVISION,
NAMESPACE_MISMATCH,
INVALID_SOURCE,
STORAGE_FAILURE,
QUIESCING,
CLOSED,
LIMIT_EXCEEDED,
}PersistentSourcePublisher.kt
kotlin
package cn.sealplugins.attributes.api.source
import cn.sealplugins.attributes.api.subject.SubjectKey
import java.util.concurrent.CompletionStage
/**
* Provider-bound durable source publisher.
*
* The returned stage completes after the database outcome and the best-effort publication into the
* requested live session are both known. [PublishStatus.PERSISTED_SESSION_DEFERRED] means durability
* succeeded after that session became unavailable; it is successful and must not be retried as a
* failed business action. The caller is never blocked on database I/O, and storage failures preserve
* the previously committed source.
*/
public interface PersistentSourcePublisher {
public fun providerId(): String
public fun ownerEpoch(): Long
public fun publish(subject: SubjectKey, snapshot: SourceSnapshot): CompletionStage<PublishResult>
public fun withdraw(subject: SubjectKey, key: SourceKey, revision: Long): CompletionStage<PublishResult>
}SealAttributesEvents.kt
kotlin
package cn.sealplugins.attributes.api.event
/**
* Provider-owned event subscriptions.
*
* Listeners run after commit on the owning subject execution context and exceptions are isolated.
* Snapshot-listener changes are queued for a later change round; combat/healing submissions made
* recursively from their own completion callback are rejected fail-closed.
*/
public interface SealAttributesEvents {
public fun onAttributeSnapshotChanged(
listener: SealAttributesEventListener<AttributeSnapshotChanged>,
): EventSubscription
public fun onCombatCompleted(
listener: SealAttributesEventListener<CombatCompleted>,
): EventSubscription
public fun onHealingCompleted(
listener: SealAttributesEventListener<HealingCompleted>,
): EventSubscription
public fun onResourceChanged(
listener: SealAttributesEventListener<ResourceChanged>,
): EventSubscription
}
public fun interface SealAttributesEventListener<T : Any> {
public fun onEvent(event: T)
}
/** Idempotent listener handle invalidated automatically with its provider owner epoch. */
public interface EventSubscription : AutoCloseable {
public fun active(): Boolean
override fun close()
}ResourceChanged.kt
kotlin
package cn.sealplugins.attributes.api.event
import cn.sealplugins.attributes.api.resource.ResourceSnapshot
import cn.sealplugins.attributes.api.subject.SubjectKey
import java.time.Instant
/** Read-only post-commit resource change event. */
public class ResourceChanged(
public val subject: SubjectKey,
public val previous: ResourceSnapshot,
public val current: ResourceSnapshot,
public val reason: ResourceChangeReason,
public val occurredAt: Instant,
) {
init {
require(previous.subject == subject && current.subject == subject) {
"resource event values must belong to subject"
}
require(previous.resource == current.resource) { "resource event values must use one resource" }
require(current.revision > previous.revision) { "resource event revisions must advance" }
}
}
public enum class ResourceChangeReason {
INITIALIZED,
REGENERATED,
CONSUMED,
RESTORED,
MAXIMUM_CHANGED,
}HealingCompleted.kt
kotlin
package cn.sealplugins.attributes.api.event
import cn.sealplugins.attributes.api.healing.HealingResult
import java.time.Instant
/** Read-only post-commit healing result event; it is not cancellable or mutable. */
public class HealingCompleted(
public val result: HealingResult,
public val occurredAt: Instant,
)CombatCompleted.kt
kotlin
package cn.sealplugins.attributes.api.event
import cn.sealplugins.attributes.api.combat.CombatResult
import java.time.Instant
/** Read-only post-commit combat result event; it is not cancellable or mutable. */
public class CombatCompleted(
public val result: CombatResult,
public val occurredAt: Instant,
)AttributeSnapshotChanged.kt
kotlin
package cn.sealplugins.attributes.api.event
import cn.sealplugins.attributes.api.ApiLimits
import cn.sealplugins.attributes.api.definition.AttributeId
import cn.sealplugins.attributes.api.internal.immutableSet
import cn.sealplugins.attributes.api.subject.AttributeSnapshot
import cn.sealplugins.attributes.api.subject.SubjectKey
import java.time.Instant
/** Read-only, merged post-commit snapshot change event. */
public class AttributeSnapshotChanged(
public val subject: SubjectKey,
public val previous: AttributeSnapshot,
public val current: AttributeSnapshot,
changedAttributes: Collection<AttributeId>,
public val reason: SnapshotChangeReason,
public val occurredAt: Instant,
) {
public val changedAttributes: Set<AttributeId> = immutableSet(
changedAttributes,
ApiLimits.MAX_ATTRIBUTES_PER_SNAPSHOT,
"changed attributes",
)
init {
require(previous.state.key == subject && current.state.key == subject) {
"snapshot event values must belong to subject"
}
require(current.state.snapshotRevision > previous.state.snapshotRevision) {
"snapshot event revisions must advance"
}
}
}
public enum class SnapshotChangeReason {
SOURCE_PUBLISHED,
SOURCE_WITHDRAWN,
BASE_PROFILE_CHANGED,
ITEM_MODE_RECONFIGURED,
REGISTRY_REBUILT,
SESSION_READY,
}