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

XFEServerManager

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

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

import com.xfestudio.xfeservermanager.api.world.WorldChangeRecord;
import com.xfestudio.xfeservermanager.api.world.WorldStatePayload;
import java.time.Clock;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;

/** Builds deterministic fixed-watermark previews and performs no mutations. */
public final class RollbackPlanner {
    public static final int MAX_TEMPORARY_CHUNKS = 128;

    private final RollbackJournalPort journal;
    private final WorldAccessPort world;
    private final Clock clock;

    public RollbackPlanner(RollbackJournalPort journal, WorldAccessPort world, Clock clock) {
        this.journal = Objects.requireNonNull(journal, "journal");
        this.world = Objects.requireNonNull(world, "world");
        this.clock = Objects.requireNonNull(clock, "clock");
    }

    public RollbackPlan preview(RollbackQuery query, boolean force, boolean loadUnloadedChunks,
                                boolean ownerAuthorized) {
        if ((force || loadUnloadedChunks) && !ownerAuthorized) {
            throw new SecurityException("force and temporary chunk loading are owner-only");
        }
        JournalSlice slice = journal.capture(query);
        var ordered = slice.entries().stream().sorted(Comparator.comparingLong(JournalEntry::sequence).reversed()).toList();
        var temporaryChunks = new HashSet<ChunkCoordinate>();
        var virtualStates = new HashMap<ObjectCoordinate, WorldStatePayload>();
        var items = new ArrayList<RollbackPlanItem>();
        for (JournalEntry entry : ordered) {
            items.add(assess(entry, force, loadUnloadedChunks, ownerAuthorized, temporaryChunks, virtualStates));
        }
        return new RollbackPlan(UUID.randomUUID(), slice.watermark(), clock.instant(), RollbackDirection.ROLLBACK,
                force, loadUnloadedChunks, ownerAuthorized, items, slice.gaps());
    }

    public RollbackPlan previewRedo(RollbackOperation completedRollback, boolean force,
                                    boolean loadUnloadedChunks, boolean ownerAuthorized) {
        if (completedRollback.status() != RollbackStatus.COMPLETED
                || completedRollback.plan().direction() != RollbackDirection.ROLLBACK) {
            throw new IllegalArgumentException("redo requires a completed rollback");
        }
        if ((force || loadUnloadedChunks) && !ownerAuthorized) throw new SecurityException("owner authorization required");
        Set<UUID> applied = Set.copyOf(completedRollback.appliedEventIds());
        var temporaryChunks = new HashSet<ChunkCoordinate>();
        var virtualStates = new HashMap<ObjectCoordinate, WorldStatePayload>();
        var items = completedRollback.plan().items().stream()
                .filter(item -> applied.contains(item.entry().change().eventId()))
                .sorted(Comparator.comparingLong(item -> item.entry().sequence()))
                .map(item -> assessRedo(item.entry(), force, loadUnloadedChunks, ownerAuthorized,
                        temporaryChunks, virtualStates)).toList();
        return new RollbackPlan(UUID.randomUUID(), completedRollback.plan().watermark(), clock.instant(),
                RollbackDirection.REDO, force, loadUnloadedChunks, ownerAuthorized,
                items, completedRollback.plan().gaps());
    }

    private RollbackPlanItem assess(JournalEntry entry, boolean force, boolean load, boolean ownerAuthorized,
                                    Set<ChunkCoordinate> loads,
                                    Map<ObjectCoordinate, WorldStatePayload> virtualStates) {
        WorldChangeRecord change = entry.change();
        return assessStates(entry, change.afterState(), change.beforeState(), force, load, ownerAuthorized,
                loads, virtualStates);
    }

    private RollbackPlanItem assessRedo(JournalEntry entry, boolean force, boolean load, boolean ownerAuthorized,
                                        Set<ChunkCoordinate> loads,
                                        Map<ObjectCoordinate, WorldStatePayload> virtualStates) {
        WorldChangeRecord change = entry.change();
        return assessStates(entry, change.beforeState(), change.afterState(), force, load, ownerAuthorized,
                loads, virtualStates);
    }

    private RollbackPlanItem assessStates(JournalEntry entry, WorldStatePayload expected, WorldStatePayload target,
                                          boolean force, boolean load, boolean ownerAuthorized,
                                          Set<ChunkCoordinate> loads,
                                          Map<ObjectCoordinate, WorldStatePayload> virtualStates) {
        WorldChangeRecord change = entry.change();
        if (!change.restorable()) return item(entry, expected, target, PlanDisposition.NOT_RESTORABLE, "event was not captured as restorable");
        if (requiresOwnerAuthorization(change) && !ownerAuthorized) {
            return item(entry, expected, target, PlanDisposition.OWNER_AUTHORIZATION_REQUIRED,
                    "entity and item restoration requires owner authorization");
        }
        var compatibility = world.compatibility(change, target);
        if (!compatibility.compatible()) return item(entry, expected, target, PlanDisposition.INCOMPATIBLE, compatibility.detail());
        ChunkCoordinate chunk = ChunkCoordinate.of(change);
        if (!world.isChunkLoaded(chunk.dimensionId(), chunk.x(), chunk.z())) {
            if (!load) return item(entry, expected, target, PlanDisposition.UNLOADED, "chunk is not loaded");
            loads.add(chunk);
            if (loads.size() > MAX_TEMPORARY_CHUNKS) {
                return item(entry, expected, target, PlanDisposition.CHUNK_LOAD_LIMIT, "temporary chunk limit exceeded");
            }
            virtualStates.put(ObjectCoordinate.of(change), target);
            return item(entry, expected, target, PlanDisposition.READY_REQUIRES_CHUNK_LOAD, "temporary load required");
        }
        ObjectCoordinate object = ObjectCoordinate.of(change);
        WorldStatePayload actual;
        if (virtualStates.containsKey(object)) {
            actual = virtualStates.get(object);
        } else {
            actual = world.currentState(change);
        }
        if (!Objects.equals(actual, expected)) {
            if (force) virtualStates.put(object, target);
            return item(entry, expected, target, PlanDisposition.CONFLICT,
                    force ? "current state differs; owner force is eligible to override" : "current state differs from journal after state");
        }
        virtualStates.put(object, target);
        return item(entry, expected, target, PlanDisposition.READY, "");
    }

    private static boolean requiresOwnerAuthorization(WorldChangeRecord change) {
        return switch (change.kind()) {
            case ENTITY_SPAWN, ENTITY_DEATH, ENTITY_REMOVE, ITEM_DROP, ITEM_PICKUP, ITEM_DESPAWN -> true;
            default -> false;
        };
    }

    private static RollbackPlanItem item(JournalEntry entry, WorldStatePayload expected, WorldStatePayload target,
                                         PlanDisposition disposition, String detail) {
        return new RollbackPlanItem(entry, expected, target, disposition, detail);
    }

    record ChunkCoordinate(String dimensionId, int x, int z) {
        static ChunkCoordinate of(WorldChangeRecord change) {
            return new ChunkCoordinate(change.dimensionId(), Math.floorDiv(change.x(), 16), Math.floorDiv(change.z(), 16));
        }
    }

    record ObjectCoordinate(String dimensionId, int x, int y, int z, String category) {
        static ObjectCoordinate of(WorldChangeRecord change) {
            String category = switch (change.kind()) {
                case BLOCK_PLACE, BLOCK_BREAK, BLOCK_CHANGE, EXPLOSION -> "block";
                case CONTAINER_CHANGE -> "container";
                case ENTITY_SPAWN, ENTITY_DEATH, ENTITY_REMOVE -> "entity";
                case ITEM_DROP, ITEM_PICKUP, ITEM_DESPAWN -> "item";
                case GAP, ROLLBACK, REDO -> change.kind().name();
            };
            return new ObjectCoordinate(change.dimensionId(), change.x(), change.y(), change.z(), category);
        }
    }
}