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

XFEServerManager

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

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

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;

/** Online-only inventory editor with slot revisions and compare-and-swap persistence. */
public final class PlayerInventoryService {
    private final InventoryPort port;
    private final Set<String> allowedStructuredKeys;
    private final java.util.function.BooleanSupplier rawEditingEnabled;

    public PlayerInventoryService(
            InventoryPort port, Set<String> allowedStructuredKeys, boolean rawEditingEnabled) {
        this(port, allowedStructuredKeys, () -> rawEditingEnabled);
    }

    public PlayerInventoryService(
            InventoryPort port, Set<String> allowedStructuredKeys,
            java.util.function.BooleanSupplier rawEditingEnabled) {
        this.port = Objects.requireNonNull(port, "port");
        this.allowedStructuredKeys = Set.copyOf(allowedStructuredKeys);
        this.rawEditingEnabled = Objects.requireNonNull(rawEditingEnabled, "rawEditingEnabled");
    }

    public InventoryUpdateResult update(
            UUID playerId,
            ContainerKind container,
            InventoryChangeRequest request,
            boolean owner) {
        Objects.requireNonNull(playerId, "playerId");
        Objects.requireNonNull(container, "container");
        Objects.requireNonNull(request, "request");
        if (request.reason().isBlank() || request.reason().length() > 1_000) {
            throw new PlayerOperationException("reason_required", "a concise operation reason is required");
        }
        InventorySnapshot current = port.read(playerId, container);
        if (!current.online()) {
            throw new PlayerOperationException("player_offline", "only online inventories may be edited");
        }
        if (current.revision() != request.expectedRevision()) {
            throw new PlayerOperationException(
                    "inventory_revision_conflict", "inventory changed after it was read");
        }
        Map<Integer, SlotState> next = new LinkedHashMap<>(current.slots());
        List<SlotDiff> diffs = new ArrayList<>();
        for (Map.Entry<Integer, SlotState> update : request.updates().entrySet()) {
            int slot = update.getKey();
            SlotState after = update.getValue();
            if (slot != after.slot()) {
                throw new PlayerOperationException("slot_mismatch", "slot map key and value disagree");
            }
            validateSlot(after, request.rawEditingRequested(), owner);
            SlotState before = next.put(slot, after);
            if (!Objects.equals(before, after)) {
                diffs.add(new SlotDiff(slot, before, after));
            }
        }
        if (diffs.isEmpty()) {
            return new InventoryUpdateResult(current.revision(), List.of());
        }
        CompareAndSetResult result = port.compareAndSet(
                playerId, container, current.revision(), Map.copyOf(next));
        if (result == CompareAndSetResult.OFFLINE) {
            throw new PlayerOperationException("player_offline", "player disconnected during inventory update");
        }
        if (result == CompareAndSetResult.REVISION_MISMATCH) {
            throw new PlayerOperationException(
                    "inventory_revision_conflict", "inventory changed during update");
        }
        return new InventoryUpdateResult(current.revision() + 1, diffs);
    }

    private void validateSlot(SlotState state, boolean rawRequested, boolean owner) {
        if (!allowedStructuredKeys.containsAll(state.structuredData().keySet())) {
            Set<String> unknown = new java.util.LinkedHashSet<>(state.structuredData().keySet());
            unknown.removeAll(allowedStructuredKeys);
            throw new PlayerOperationException(
                    "unsafe_item_component", "item contains unsupported structured keys " + unknown);
        }
        boolean hasRaw = state.rawData().length != 0;
        if (hasRaw && (!rawRequested || !rawEditingEnabled.getAsBoolean() || !owner)) {
            throw new PlayerOperationException(
                    "raw_item_editing_disabled", "raw NBT/data-component editing is owner-only and disabled by default");
        }
    }

    public interface InventoryPort {
        /** Must obtain the snapshot on the Minecraft server thread. */
        InventorySnapshot read(UUID playerId, ContainerKind container);

        /** Must run atomically on the Minecraft server thread. */
        CompareAndSetResult compareAndSet(
                UUID playerId,
                ContainerKind container,
                long expectedRevision,
                Map<Integer, SlotState> replacement);
    }

    public enum ContainerKind {
        INVENTORY,
        ENDER_CHEST
    }

    public enum CompareAndSetResult {
        APPLIED,
        REVISION_MISMATCH,
        OFFLINE
    }

    public record InventorySnapshot(
            UUID playerId,
            ContainerKind container,
            long revision,
            boolean online,
            Map<Integer, SlotState> slots) {
        public InventorySnapshot {
            Objects.requireNonNull(playerId, "playerId");
            Objects.requireNonNull(container, "container");
            if (revision < 0) throw new IllegalArgumentException("revision must be non-negative");
            slots = Map.copyOf(slots);
        }
    }

    public record SlotState(
            int slot,
            String itemId,
            int count,
            Map<String, String> structuredData,
            byte[] rawData) {
        public SlotState {
            if (slot < 0 || slot > 255) throw new IllegalArgumentException("slot must be between 0 and 255");
            if (itemId == null || itemId.isBlank()) throw new IllegalArgumentException("itemId is required");
            itemId = itemId.contains(":")
                    ? itemId.toLowerCase(java.util.Locale.ROOT)
                    : "minecraft:" + itemId.toLowerCase(java.util.Locale.ROOT);
            if (count < 0 || count > 127) throw new IllegalArgumentException("count must be between 0 and 127");
            structuredData = Map.copyOf(structuredData);
            rawData = rawData == null ? new byte[0] : rawData.clone();
        }

        @Override
        public byte[] rawData() {
            return rawData.clone();
        }

        @Override
        public boolean equals(Object other) {
            if (this == other) return true;
            if (!(other instanceof SlotState that)) return false;
            return slot == that.slot
                    && count == that.count
                    && itemId.equals(that.itemId)
                    && structuredData.equals(that.structuredData)
                    && java.util.Arrays.equals(rawData, that.rawData);
        }

        @Override
        public int hashCode() {
            int result = Objects.hash(slot, itemId, count, structuredData);
            return 31 * result + java.util.Arrays.hashCode(rawData);
        }
    }

    public record InventoryChangeRequest(
            long expectedRevision,
            Map<Integer, SlotState> updates,
            String reason,
            boolean rawEditingRequested) {
        public InventoryChangeRequest {
            if (expectedRevision < 0) throw new IllegalArgumentException("expectedRevision must be non-negative");
            updates = Map.copyOf(updates);
            if (updates.isEmpty()) throw new IllegalArgumentException("at least one slot update is required");
            reason = reason == null ? "" : reason.strip();
        }
    }

    public record SlotDiff(int slot, SlotState before, SlotState after) { }

    public record InventoryUpdateResult(long revision, List<SlotDiff> diffs) {
        public InventoryUpdateResult {
            if (revision < 0) throw new IllegalArgumentException("revision must be non-negative");
            diffs = List.copyOf(diffs);
        }
    }
}