Skip to content

来源、查询与事件 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,
}