XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEServerManager

【Java】我的世界XFE服务器管理器

公开
关注 0 Fork 0 Star 0
UTF-8
package com.xfestudio.xfeservermanager.core.governance.claim;

import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;

/** Serializes claim mutations while retaining lock-free immutable reads. */
public final class ClaimService {
    public record TransferTicket(UUID token, UUID claimId, UUID fromOwner, UUID toOwner, Instant expiresAt) { }

    private final AtomicReference<ClaimSnapshot> snapshot;
    private final ClaimConfiguration configuration;
    private final ClaimRepositoryPort repository;
    private final Clock clock;
    private final Map<UUID, TransferTicket> transfers = new ConcurrentHashMap<>();

    public ClaimService(ClaimSnapshot initial, ClaimConfiguration configuration,
                        ClaimRepositoryPort repository, Clock clock) {
        this.snapshot = new AtomicReference<>(Objects.requireNonNull(initial, "initial"));
        this.configuration = Objects.requireNonNull(configuration, "configuration");
        this.repository = Objects.requireNonNull(repository, "repository");
        this.clock = Objects.requireNonNull(clock, "clock");
    }

    public ClaimSnapshot snapshot() {
        return snapshot.get();
    }

    public synchronized Claim create(UUID owner, String name, Set<ChunkKey> chunks, boolean administrator) {
        return create(owner, name, chunks, Map.of(), Set.of(), administrator);
    }

    public synchronized Claim create(UUID owner, String name, Set<ChunkKey> chunks,
                                     Map<UUID, ClaimRole> assignments, Set<ClaimFlag> flags,
                                     boolean administrator) {
        Claim candidate = new Claim(UUID.randomUUID(), owner, name, chunks, assignments, flags, 0);
        enforceAvailable(snapshot.get(), chunks, null);
        enforceQuota(snapshot.get(), candidate.ownerId(), candidate.dimensionId(), chunks.size(), administrator);
        return publish(candidate, null);
    }

    public synchronized Claim addChunk(UUID claimId, ChunkKey chunk, UUID actor, boolean administrator) {
        Claim current = requireClaim(claimId);
        requireManager(current, actor, administrator);
        if (!chunk.dimensionId().equals(current.dimensionId())) throw new IllegalArgumentException("dimension mismatch");
        var chunks = new HashSet<>(current.chunks());
        chunks.add(chunk);
        Claim replacement = current.withChunks(chunks);
        enforceAvailable(snapshot.get(), Set.of(chunk), claimId);
        enforceQuota(snapshot.get(), current.ownerId(), current.dimensionId(), 1, administrator);
        return publish(replacement, claimId);
    }

    public synchronized Claim removeChunk(UUID claimId, ChunkKey chunk, UUID actor, boolean administrator) {
        Claim current = requireClaim(claimId);
        requireManager(current, actor, administrator);
        var chunks = new HashSet<>(current.chunks());
        if (!chunks.remove(chunk)) throw new IllegalArgumentException("chunk is not in claim");
        if (chunks.isEmpty()) throw new IllegalArgumentException("cannot remove the last chunk");
        return publish(current.withChunks(chunks), claimId);
    }

    public synchronized Claim assign(UUID claimId, UUID player, ClaimRole role, UUID actor, boolean administrator) {
        Claim current = requireClaim(claimId);
        requireManager(current, actor, administrator);
        return publish(current.withAssignment(player, role), claimId);
    }

    public synchronized Claim unassign(UUID claimId, UUID player, UUID actor, boolean administrator) {
        Claim current = requireClaim(claimId);
        requireManager(current, actor, administrator);
        return publish(current.withoutAssignment(player), claimId);
    }

    public synchronized Claim setFlags(UUID claimId, Set<ClaimFlag> flags, UUID actor, boolean administrator) {
        Claim current = requireClaim(claimId);
        requireManager(current, actor, administrator);
        return publish(current.withFlags(flags), claimId);
    }

    /** Atomically replaces editable claim fields using an optimistic revision check. */
    public synchronized Claim update(UUID claimId, long expectedRevision, String name,
                                     Set<ChunkKey> chunks, Map<UUID, ClaimRole> assignments,
                                     Set<ClaimFlag> flags, UUID actor, boolean administrator) {
        Claim current = requireClaim(claimId);
        requireManager(current, actor, administrator);
        if (current.revision() != expectedRevision) {
            throw new IllegalStateException("claim revision conflict");
        }
        String nextName = name == null ? current.name() : name;
        Set<ChunkKey> nextChunks = chunks == null ? current.chunks() : Set.copyOf(chunks);
        Map<UUID, ClaimRole> nextAssignments = assignments == null
                ? current.assignments() : Map.copyOf(assignments);
        Set<ClaimFlag> nextFlags = flags == null ? current.publicFlags() : Set.copyOf(flags);
        Claim replacement = new Claim(current.claimId(), current.ownerId(), nextName, nextChunks,
                nextAssignments, nextFlags, current.revision() + 1);
        if (!replacement.dimensionId().equals(current.dimensionId())) {
            throw new IllegalArgumentException("claim dimension cannot be changed");
        }
        enforceAvailable(snapshot.get(), nextChunks, claimId);
        long existingOwnerChunks = current.chunks().size();
        long nextOwnerUsage = ownedChunks(snapshot.get(), current.ownerId(), current.dimensionId())
                - existingOwnerChunks + nextChunks.size();
        if (!administrator && nextOwnerUsage > configuration.quota(replacement.dimensionId())) {
            throw new IllegalStateException("claim quota exceeded");
        }
        return publish(replacement, claimId);
    }

    /** Deletes a claim after checking both authority and the caller's observed revision. */
    public synchronized Claim delete(UUID claimId, long expectedRevision, UUID actor, boolean administrator) {
        Claim current = requireClaim(claimId);
        requireManager(current, actor, administrator);
        if (current.revision() != expectedRevision) {
            throw new IllegalStateException("claim revision conflict");
        }
        ClaimSnapshot existing = snapshot.get();
        var remaining = new HashMap<>(existing.claims());
        remaining.remove(claimId);
        var index = new HashMap<ChunkKey, UUID>();
        remaining.values().forEach(claim -> claim.chunks().forEach(chunk -> {
            if (index.put(chunk, claim.claimId()) != null) throw new IllegalStateException("duplicate chunk ownership");
        }));
        var next = new ClaimSnapshot(existing.revision() + 1, remaining, index);
        repository.save(next);
        snapshot.set(next);
        return current;
    }

    public TransferTicket beginTransfer(UUID claimId, UUID actor, UUID newOwner, Duration validity) {
        Objects.requireNonNull(newOwner, "newOwner");
        Claim claim = requireClaim(claimId);
        if (!claim.ownerId().equals(actor)) throw new SecurityException("only the owner can transfer a claim");
        if (claim.ownerId().equals(newOwner)) throw new IllegalArgumentException("new owner must differ from current owner");
        if (validity.isZero() || validity.isNegative()) throw new IllegalArgumentException("validity must be positive");
        var ticket = new TransferTicket(UUID.randomUUID(), claimId, actor, newOwner, clock.instant().plus(validity));
        transfers.put(ticket.token(), ticket);
        return ticket;
    }

    public synchronized Claim confirmTransfer(UUID token, UUID acceptingPlayer) {
        TransferTicket ticket = transfers.get(token);
        if (ticket == null) throw new IllegalArgumentException("unknown transfer token");
        if (!ticket.toOwner().equals(acceptingPlayer)) throw new SecurityException("transfer must be accepted by its recipient");
        if (!clock.instant().isBefore(ticket.expiresAt())) {
            transfers.remove(token, ticket);
            throw new IllegalStateException("transfer token expired");
        }
        Claim current = requireClaim(ticket.claimId());
        if (!current.ownerId().equals(ticket.fromOwner())) throw new IllegalStateException("claim owner changed");
        long recipientUsage = ownedChunks(snapshot.get(), acceptingPlayer, current.dimensionId());
        if (recipientUsage + current.chunks().size() > configuration.quota(current.dimensionId())) {
            throw new IllegalStateException("recipient quota exceeded");
        }
        Claim transferred = publish(current.transferTo(acceptingPlayer), current.claimId());
        if (!transfers.remove(token, ticket)) throw new IllegalStateException("transfer token was already used");
        return transferred;
    }

    private Claim publish(Claim replacement, UUID replacingId) {
        ClaimSnapshot current = snapshot.get();
        var claims = new HashMap<>(current.claims());
        if (replacingId != null) claims.remove(replacingId);
        claims.put(replacement.claimId(), replacement);
        var index = new HashMap<ChunkKey, UUID>();
        claims.values().forEach(claim -> claim.chunks().forEach(chunk -> {
            if (index.put(chunk, claim.claimId()) != null) throw new IllegalStateException("duplicate chunk ownership");
        }));
        var next = new ClaimSnapshot(current.revision() + 1, claims, index);
        repository.save(next);
        snapshot.set(next);
        return replacement;
    }

    private void enforceAvailable(ClaimSnapshot current, Set<ChunkKey> chunks, UUID sameClaim) {
        for (ChunkKey chunk : chunks) {
            UUID occupied = current.chunkIndex().get(chunk);
            if (occupied != null && !occupied.equals(sameClaim)) throw new IllegalStateException("chunk already claimed: " + chunk);
        }
    }

    private void enforceQuota(ClaimSnapshot current, UUID owner, String dimension, int additional, boolean administrator) {
        if (administrator) return;
        int quota = configuration.quota(dimension);
        if (quota == 0) throw new SecurityException("self-service claiming is disabled in " + dimension);
        if (ownedChunks(current, owner, dimension) + additional > quota) throw new IllegalStateException("claim quota exceeded");
    }

    private static long ownedChunks(ClaimSnapshot current, UUID owner, String dimension) {
        return current.claims().values().stream().filter(claim -> claim.ownerId().equals(owner))
                .filter(claim -> claim.dimensionId().equals(dimension)).mapToLong(claim -> claim.chunks().size()).sum();
    }

    private Claim requireClaim(UUID id) {
        Claim claim = snapshot.get().claims().get(id);
        if (claim == null) throw new IllegalArgumentException("unknown claim " + id);
        return claim;
    }

    private static void requireManager(Claim claim, UUID actor, boolean administrator) {
        ClaimRole role = claim.roleOf(actor);
        if (!administrator && (role == null || !role.atLeast(ClaimRole.MANAGER))) {
            throw new SecurityException("manager role required");
        }
    }
}