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

MyDimension

【Java】我的维度模组

公开
关注 0 Fork 0 Star 0
UTF-8
package com.xfestudio.mydimension.client.builder;

import com.xfestudio.mydimension.builder.BuilderMode;
import com.xfestudio.mydimension.builder.SurfaceMatchMode;
import com.xfestudio.mydimension.client.builder.blueprint.BlueprintRenameScreen;
import com.xfestudio.mydimension.client.builder.blueprint.ClientBlueprintLibrary;
import net.minecraft.Util;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.gui.screens.controls.ControlsScreen;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.util.Mth;

import java.util.List;
import java.util.UUID;

/** Five-tab, non-pausing control surface for the Realmwright's Scepter. */
public class BuilderToolScreen extends Screen {
    private static final int PANEL_WIDTH = 540;
    private static final int PANEL_HEIGHT = 322;
    private static final int PAGE_ROWS = 6;
    private static final int WORKFLOW_ROW_TOP = 145;
    private static final int WORKFLOW_ROW_HEIGHT = 22;

    private final ClientBlueprintLibrary library = ClientBlueprintLibrary.get();
    private Tab tab = Tab.OPERATIONS;
    private BuilderClientSnapshot snapshot = BuilderClientSnapshot.EMPTY;
    private BuilderMode localMode = BuilderMode.BUILD;
    private SurfaceMatchMode localMatch = SurfaceMatchMode.SAME_BLOCK;
    private boolean localHistoryRecording;
    private boolean localAllowReplacement;
    private EditBox buildLimit;
    private EditBox demolishLimit;
    private EditBox aclPlayer;
    private Button missingWorkflowCancel;
    private Button selectionWorkflowCancel;
    private Button deploymentWorkflowCancel;
    private int anchorOffset;
    private int blueprintOffset;
    private int historyOffset;
    private UUID selectedLocalBlueprint;
    private UUID deleteConfirmation;
    private UUID selectedManagedAnchor;
    private String notice = "";
    private int requestTicker;

    public BuilderToolScreen() {
        super(Component.translatable("screen.mydimension.realmwright.title"));
    }

    @Override
    protected void init() {
        snapshot = BuilderClientServices.snapshot();
        missingWorkflowCancel = null;
        selectionWorkflowCancel = null;
        deploymentWorkflowCancel = null;
        if (!snapshot.enabled() && tab != Tab.BLUEPRINTS && tab != Tab.SETTINGS) tab = Tab.BLUEPRINTS;
        localMode = snapshot.mode();
        localMatch = snapshot.surfaceMatch();
        localHistoryRecording = snapshot.historyRecording();
        localAllowReplacement = snapshot.allowReplacement();
        addTabs();
        switch (tab) {
            case OPERATIONS -> initOperations();
            case SUPPLIES -> initSupplies();
            case BLUEPRINTS -> initBlueprints();
            case HISTORY -> initHistory();
            case SETTINGS -> initSettings();
        }
        addRenderableWidget(Button.builder(Component.translatable("gui.done"), button -> onClose())
                .bounds(panelRight() - 70, panelBottom() - 27, 58, 20).build());
        if (library.entries().isEmpty() && !library.refreshing()) {
            library.refreshAsync();
        }
    }

    private void addTabs() {
        int left = panelLeft() + 12;
        int width = (panelWidth() - 24) / Tab.values().length;
        for (int index = 0; index < Tab.values().length; index++) {
            Tab value = Tab.values()[index];
            Button button = Button.builder(Component.translatable(value.translationKey), clicked -> {
                        if (tab != value) {
                            tab = value;
                            rebuildWidgets();
                        }
                    }).bounds(left + index * width, panelTop() + 34, width - 3, 22).build();
            button.active = snapshot.enabled() ? tab != value
                    : (value == Tab.BLUEPRINTS || value == Tab.SETTINGS) && tab != value;
            addRenderableWidget(button);
        }
    }

    private void initOperations() {
        int left = contentLeft();
        int top = contentTop();
        Button mode = Button.builder(modeLabel(), button -> {
            localMode = localMode.toggle();
            button.setMessage(modeLabel());
            BuilderClientServices.send(new BuilderClientCommand.SetMode(localMode));
        }).bounds(left, top, 172, 24).build();
        addRenderableWidget(mode);

        Button match = Button.builder(matchLabel(), button -> {
            localMatch = localMatch.toggle();
            button.setMessage(matchLabel());
            BuilderClientServices.send(new BuilderClientCommand.SetSurfaceMatch(localMatch));
        }).bounds(left + 184, top, 184, 24).build();
        addRenderableWidget(match);

        buildLimit = numericBox(left, top + 55, snapshot.buildLimit(), 112);
        demolishLimit = numericBox(left + 132, top + 55, snapshot.demolishLimit(), 112);
        addRenderableWidget(buildLimit);
        addRenderableWidget(demolishLimit);
        addRenderableWidget(Button.builder(Component.translatable("screen.mydimension.realmwright.apply"), button -> applyLimits())
                .bounds(left + 264, top + 55, 104, 20).build());

        Button history = Button.builder(historyRecordingLabel(), button -> {
            localHistoryRecording = !localHistoryRecording;
            button.setMessage(historyRecordingLabel());
            BuilderClientServices.send(new BuilderClientCommand.SetHistoryRecording(localHistoryRecording));
        }).bounds(left, top + 82, 172, 20).build();
        addRenderableWidget(history);

        Button replacement = Button.builder(replacementLabel(), button -> {
            localAllowReplacement = !localAllowReplacement;
            button.setMessage(replacementLabel());
            BuilderClientServices.send(new BuilderClientCommand.SetAllowReplacement(localAllowReplacement));
        }).bounds(left + 184, top + 82, 184, 20).build();
        addRenderableWidget(replacement);

        Button undo = Button.builder(Component.translatable("screen.mydimension.realmwright.undo"),
                button -> BuilderClientServices.send(new BuilderClientCommand.Undo()))
                .bounds(left, top + 108, 86, 20).build();
        undo.active = snapshot.canUndo();
        addRenderableWidget(undo);
        Button redo = Button.builder(Component.translatable("screen.mydimension.realmwright.redo"),
                button -> BuilderClientServices.send(new BuilderClientCommand.Redo()))
                .bounds(left + 98, top + 108, 86, 20).build();
        redo.active = snapshot.canRedo();
        addRenderableWidget(redo);

        missingWorkflowCancel = addRenderableWidget(workflowCancelButton(left, top,
                WorkflowKind.MISSING));
        selectionWorkflowCancel = addRenderableWidget(workflowCancelButton(left, top,
                WorkflowKind.SELECTION));
        deploymentWorkflowCancel = addRenderableWidget(workflowCancelButton(left, top,
                WorkflowKind.DEPLOYMENT));
        updateWorkflowButtons();
    }

    private void initSupplies() {
        List<BuilderClientSnapshot.AnchorView> anchors = snapshot.anchors();
        anchorOffset = clampOffset(anchorOffset, anchors.size());
        int left = contentLeft();
        int top = contentTop();
        int shown = Math.min(PAGE_ROWS, anchors.size() - anchorOffset);
        for (int row = 0; row < shown; row++) {
            int index = anchorOffset + row;
            BuilderClientSnapshot.AnchorView anchor = anchors.get(index);
            int y = top + row * 30;
            Button anchorLine = Button.builder(anchorLabel(anchor), button -> {
                selectedManagedAnchor = anchor.id();
                rebuildWidgets();
            }).bounds(left, y, 250, 23).build();
            anchorLine.active = anchor.owner() && !anchor.id().equals(selectedManagedAnchor);
            addRenderableWidget(anchorLine);
            addRenderableWidget(Button.builder(Component.literal("↑"), button -> {
                BuilderClientServices.send(new BuilderClientCommand.MoveAnchor(anchor.id(), -1));
                BuilderClientServices.bridge().requestSnapshot();
            }).bounds(left + 256, y, 26, 23).build()).active = index > 0;
            addRenderableWidget(Button.builder(Component.literal("↓"), button -> {
                BuilderClientServices.send(new BuilderClientCommand.MoveAnchor(anchor.id(), 1));
                BuilderClientServices.bridge().requestSnapshot();
            }).bounds(left + 286, y, 26, 23).build()).active = index + 1 < anchors.size();
            addRenderableWidget(Button.builder(Component.literal("×"), button ->
                            BuilderClientServices.send(new BuilderClientCommand.UnbindAnchor(anchor.id())))
                    .bounds(left + 316, y, 26, 23).build());
            Button visibility = Button.builder(Component.literal(anchor.publicAccess() ? "🌐" : "🔒"), button ->
                            BuilderClientServices.send(new BuilderClientCommand.SetAnchorPublic(anchor.id(),
                                    !anchor.publicAccess())))
                    .bounds(left + 346, y, 28, 23).build();
            visibility.active = anchor.owner();
            addRenderableWidget(visibility);
        }
        BuilderClientSnapshot.AnchorView managed = anchors.stream()
                .filter(anchor -> anchor.id().equals(selectedManagedAnchor) && anchor.owner())
                .findFirst().orElse(null);
        int aclY = contentBottom() - 24;
        aclPlayer = new EditBox(font, left, aclY, 176, 20,
                Component.translatable("screen.mydimension.realmwright.anchor_player"));
        aclPlayer.setMaxLength(16);
        aclPlayer.setHint(Component.translatable("screen.mydimension.realmwright.anchor_player"));
        aclPlayer.setEditable(managed != null);
        addRenderableWidget(aclPlayer);
        Button allow = Button.builder(Component.translatable("screen.mydimension.realmwright.anchor_allow"), button ->
                        updateAnchorPlayer(true)).bounds(left + 184, aclY, 90, 20).build();
        allow.active = managed != null;
        addRenderableWidget(allow);
        Button revoke = Button.builder(Component.translatable("screen.mydimension.realmwright.anchor_revoke"), button ->
                        updateAnchorPlayer(false)).bounds(left + 282, aclY, 90, 20).build();
        revoke.active = managed != null;
        addRenderableWidget(revoke);
    }

    private void initBlueprints() {
        List<ClientBlueprintLibrary.Entry> entries = library.entries();
        blueprintOffset = clampOffset(blueprintOffset, entries.size());
        int left = contentLeft();
        int top = contentTop();
        int shown = Math.min(PAGE_ROWS, entries.size() - blueprintOffset);
        for (int row = 0; row < shown; row++) {
            ClientBlueprintLibrary.Entry entry = entries.get(blueprintOffset + row);
            int y = top + row * 28;
            Button button = Button.builder(blueprintLabel(entry), clicked -> {
                if (entry.valid()) {
                    selectedLocalBlueprint = entry.id();
                    BuilderClientServices.send(new BuilderClientCommand.SelectBlueprint(entry.id()));
                    rebuildWidgets();
                }
            }).bounds(left, y, 374, 22).build();
            button.active = entry.valid() && !entry.id().equals(selectedLocalBlueprint);
            addRenderableWidget(button);
        }

        int actionY = contentBottom() - 24;
        addRenderableWidget(Button.builder(Component.translatable("screen.mydimension.realmwright.blueprint.import"), button -> importBlueprint())
                .bounds(left, actionY, 62, 20).build());
        Button export = Button.builder(Component.translatable("screen.mydimension.realmwright.blueprint.export"), button -> exportBlueprint())
                .bounds(left + 66, actionY, 62, 20).build();
        export.active = selectedLocalBlueprint != null;
        addRenderableWidget(export);
        Button rename = Button.builder(Component.translatable("screen.mydimension.realmwright.blueprint.rename"), button -> renameBlueprint())
                .bounds(left + 132, actionY, 62, 20).build();
        rename.active = selectedLocalBlueprint != null;
        addRenderableWidget(rename);
        Button delete = Button.builder(Component.translatable("screen.mydimension.realmwright.blueprint.delete"), button -> deleteBlueprint())
                .bounds(left + 198, actionY, 62, 20).build();
        delete.active = selectedLocalBlueprint != null;
        addRenderableWidget(delete);
        addRenderableWidget(Button.builder(Component.translatable("screen.mydimension.realmwright.blueprint.refresh"), button ->
                        library.refreshAsync().thenRun(() -> executeOnClient(this::rebuildWidgets)))
                .bounds(left + 264, actionY, 62, 20).build());
        addRenderableWidget(Button.builder(Component.translatable("screen.mydimension.realmwright.blueprint.save_current"), button ->
                        BuilderClientServices.send(new BuilderClientCommand.ExecuteBlueprintAction(
                                BlueprintAltActionController.Action.SAVE)))
                .bounds(left + 330, actionY, 90, 20).build());
    }

    private void initHistory() {
        List<BuilderClientSnapshot.HistoryView> history = snapshot.history();
        historyOffset = clampOffset(historyOffset, history.size());
        int left = contentLeft();
        int top = contentTop();
        int shown = Math.min(PAGE_ROWS, history.size() - historyOffset);
        for (int row = 0; row < shown; row++) {
            BuilderClientSnapshot.HistoryView entry = history.get(historyOffset + row);
            int y = top + row * 28;
            Button line = Button.builder(historyLabel(entry), button -> {
            }).bounds(left, y, 374, 22).build();
            line.active = false;
            addRenderableWidget(line);
        }
        Button undo = Button.builder(Component.translatable("screen.mydimension.realmwright.undo"),
                button -> BuilderClientServices.send(new BuilderClientCommand.Undo()))
                .bounds(left, contentBottom() - 24, 96, 20).build();
        undo.active = snapshot.canUndo();
        addRenderableWidget(undo);
        Button redo = Button.builder(Component.translatable("screen.mydimension.realmwright.redo"),
                button -> BuilderClientServices.send(new BuilderClientCommand.Redo()))
                .bounds(left + 104, contentBottom() - 24, 96, 20).build();
        redo.active = snapshot.canRedo();
        addRenderableWidget(redo);
    }

    private void initSettings() {
        int left = contentLeft();
        int top = contentTop() + 116;
        addRenderableWidget(Button.builder(Component.translatable("screen.mydimension.realmwright.controls"), button -> {
                    if (minecraft != null) minecraft.setScreen(new ControlsScreen(this, minecraft.options));
                }).bounds(left, top, 178, 22).build());
        addRenderableWidget(Button.builder(Component.translatable("screen.mydimension.realmwright.open_library"), button -> {
                    try {
                        java.nio.file.Files.createDirectories(library.directory());
                        Util.getPlatform().openFile(library.directory().toFile());
                    } catch (java.io.IOException exception) {
                        notice = exception.getMessage();
                    }
                }).bounds(left + 190, top, 178, 22).build());
    }

    @Override
    public void tick() {
        super.tick();
        BuilderClientSnapshot latest = BuilderClientServices.snapshot();
        if (latest != snapshot) snapshot = latest;
        if (++requestTicker >= 20) {
            requestTicker = 0;
            BuilderClientServices.bridge().requestSnapshot();
        }
        if (buildLimit != null) buildLimit.tick();
        if (demolishLimit != null) demolishLimit.tick();
        if (aclPlayer != null) aclPlayer.tick();
        updateWorkflowButtons();
    }

    @Override
    public boolean mouseScrolled(double mouseX, double mouseY, double delta) {
        int direction = delta > 0 ? -1 : delta < 0 ? 1 : 0;
        if (direction != 0) {
            switch (tab) {
                case SUPPLIES -> anchorOffset = scroll(anchorOffset, direction, snapshot.anchors().size());
                case BLUEPRINTS -> blueprintOffset = scroll(blueprintOffset, direction, library.entries().size());
                case HISTORY -> historyOffset = scroll(historyOffset, direction, snapshot.history().size());
                default -> {
                    return super.mouseScrolled(mouseX, mouseY, delta);
                }
            }
            rebuildWidgets();
            return true;
        }
        return super.mouseScrolled(mouseX, mouseY, delta);
    }

    @Override
    public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
        renderBackground(graphics);
        graphics.fill(panelLeft(), panelTop(), panelRight(), panelBottom(), 0xE7121822);
        graphics.renderOutline(panelLeft(), panelTop(), panelWidth(), panelHeight(), 0xFF557A9F);
        graphics.drawString(font, title, panelLeft() + 14, panelTop() + 13, 0xFFFFFFFF, false);

        if (!snapshot.enabled()) {
            Component disabled = Component.translatable("screen.mydimension.realmwright.disabled");
            graphics.drawCenteredString(font, disabled, width / 2, contentTop() + 82, 0xFFFF6B63);
        } else {
            renderPageText(graphics);
        }
        if (!notice.isBlank()) {
            graphics.drawString(font, font.plainSubstrByWidth(notice, panelWidth() - 100),
                    panelLeft() + 14, panelBottom() - 22, 0xFFFFD66B, false);
        }
        super.render(graphics, mouseX, mouseY, partialTick);
    }

    private void renderPageText(GuiGraphics graphics) {
        int left = contentLeft();
        int top = contentTop();
        switch (tab) {
            case OPERATIONS -> {
                graphics.drawString(font, Component.translatable("screen.mydimension.realmwright.build_limit"),
                        left, top + 42, 0xFFBFC9D8, false);
                graphics.drawString(font, Component.translatable("screen.mydimension.realmwright.demolish_limit"),
                        left + 132, top + 42, 0xFFBFC9D8, false);
                renderWorkflowRows(graphics, left, top);
            }
            case SUPPLIES -> {
                if (snapshot.anchors().isEmpty()) {
                    graphics.drawString(font, Component.translatable("screen.mydimension.realmwright.no_anchors"),
                            left, top + 8, 0xFF9FAABC, false);
                }
            }
            case BLUEPRINTS -> {
                if (library.refreshing()) {
                    graphics.drawString(font, Component.translatable("screen.mydimension.realmwright.blueprint.loading"),
                            left, top + 8, 0xFF9FAABC, false);
                } else if (library.entries().isEmpty()) {
                    graphics.drawString(font, Component.translatable("screen.mydimension.realmwright.blueprint.empty"),
                            left, top + 8, 0xFF9FAABC, false);
                }
            }
            case HISTORY -> {
                if (snapshot.history().isEmpty()) {
                    graphics.drawString(font, Component.translatable("screen.mydimension.realmwright.history.empty"),
                            left, top + 8, 0xFF9FAABC, false);
                }
            }
            case SETTINGS -> {
                graphics.drawString(font, Component.translatable("screen.mydimension.realmwright.reach", snapshot.reach()),
                        left, top, 0xFFE7EDF7, false);
                graphics.drawString(font, BuilderKeyMappings.TOGGLE_MODE.getTranslatedKeyMessage(),
                        left, top + 24, 0xFFBFC9D8, false);
                graphics.drawString(font, BuilderKeyMappings.UNDO.getTranslatedKeyMessage(),
                        left, top + 44, 0xFFBFC9D8, false);
                graphics.drawString(font, BuilderKeyMappings.REDO.getTranslatedKeyMessage(),
                        left, top + 64, 0xFFBFC9D8, false);
                graphics.drawString(font, Component.translatable("screen.mydimension.realmwright.alt_help"),
                        left, top + 88, 0xFFBFC9D8, false);
            }
        }
    }

    private void applyLimits() {
        try {
            int build = Mth.clamp(Integer.parseInt(buildLimit.getValue()), 1, snapshot.maximumBuildLimit());
            int demolish = Mth.clamp(Integer.parseInt(demolishLimit.getValue()), 1, snapshot.maximumDemolishLimit());
            buildLimit.setValue(Integer.toString(build));
            demolishLimit.setValue(Integer.toString(demolish));
            BuilderClientServices.send(new BuilderClientCommand.SetLimits(build, demolish));
            notice = "";
        } catch (NumberFormatException exception) {
            notice = Component.translatable("screen.mydimension.realmwright.invalid_number").getString();
        }
    }

    private void updateAnchorPlayer(boolean authorize) {
        if (selectedManagedAnchor == null || aclPlayer == null || aclPlayer.getValue().isBlank()) return;
        BuilderClientServices.send(new BuilderClientCommand.SetAnchorPlayer(selectedManagedAnchor,
                aclPlayer.getValue(), authorize));
        aclPlayer.setValue("");
    }

    private void importBlueprint() {
        library.chooseImportFile().ifPresent(path -> {
            notice = Component.translatable("screen.mydimension.realmwright.blueprint.importing").getString();
            library.importAsync(path, ClientBlueprintLibrary.ConflictPolicy.FAIL)
                    .whenComplete((entry, failure) -> executeOnClient(() -> {
                        notice = failure == null ? entry.name() : rootMessage(failure);
                        library.refreshAsync().thenRun(() -> executeOnClient(this::rebuildWidgets));
                    }));
        });
    }

    private void exportBlueprint() {
        if (selectedLocalBlueprint == null) return;
        ClientBlueprintLibrary.Entry entry = library.entries().stream()
                .filter(value -> value.id().equals(selectedLocalBlueprint)).findFirst().orElse(null);
        if (entry == null) return;
        library.chooseExportFile(entry.name()).ifPresent(path ->
                library.exportAsync(entry.id(), path, false).whenComplete((output, failure) ->
                        executeOnClient(() -> notice = failure == null ? output.toString() : rootMessage(failure))));
    }

    private void renameBlueprint() {
        if (selectedLocalBlueprint == null || minecraft == null) return;
        ClientBlueprintLibrary.Entry entry = library.entries().stream()
                .filter(value -> value.valid() && value.id().equals(selectedLocalBlueprint))
                .findFirst().orElse(null);
        if (entry != null) minecraft.setScreen(new BlueprintRenameScreen(this, entry));
    }

    private void deleteBlueprint() {
        if (selectedLocalBlueprint == null) return;
        if (!selectedLocalBlueprint.equals(deleteConfirmation)) {
            deleteConfirmation = selectedLocalBlueprint;
            notice = Component.translatable("screen.mydimension.realmwright.blueprint.confirm_delete").getString();
            return;
        }
        UUID deleting = selectedLocalBlueprint;
        deleteConfirmation = null;
        library.deleteAsync(deleting).whenComplete((deleted, failure) -> executeOnClient(() -> {
            if (failure == null && Boolean.TRUE.equals(deleted)) selectedLocalBlueprint = null;
            notice = failure == null ? "" : rootMessage(failure);
            library.refreshAsync().thenRun(() -> executeOnClient(this::rebuildWidgets));
        }));
    }

    private EditBox numericBox(int x, int y, int value, int width) {
        EditBox box = new EditBox(font, x, y, width, 20, Component.empty());
        box.setFilter(text -> text.isEmpty() || text.chars().allMatch(Character::isDigit));
        box.setMaxLength(7);
        box.setValue(Integer.toString(value));
        return box;
    }

    private Component modeLabel() {
        return Component.translatable(localMode == BuilderMode.BUILD
                ? "screen.mydimension.realmwright.mode.build" : "screen.mydimension.realmwright.mode.demolish");
    }

    private Component matchLabel() {
        return Component.translatable(localMatch == SurfaceMatchMode.SAME_BLOCK
                ? "screen.mydimension.realmwright.match.same" : "screen.mydimension.realmwright.match.any");
    }

    private Component historyRecordingLabel() {
        return Component.translatable(localHistoryRecording
                ? "screen.mydimension.realmwright.history_recording.on"
                : "screen.mydimension.realmwright.history_recording.off");
    }

    private Component replacementLabel() {
        return Component.translatable(localAllowReplacement
                ? "screen.mydimension.realmwright.allow_replacement.on"
                : "screen.mydimension.realmwright.allow_replacement.off");
    }

    private Button workflowCancelButton(int left, int top, WorkflowKind kind) {
        Button button = Button.builder(Component.translatable("gui.cancel"), ignored -> cancelWorkflow(kind))
                .bounds(left + 318, top + WORKFLOW_ROW_TOP, 56, 18).build();
        button.visible = false;
        return button;
    }

    private void cancelWorkflow(WorkflowKind kind) {
        switch (kind) {
            case MISSING -> {
                BuilderPreviewState previewState = BuilderPreviewState.get();
                UUID jobId = previewState.activeJobId();
                if (jobId == null) jobId = BuilderClientServices.snapshot().activeJobId();
                if (jobId != null) {
                    BuilderClientServices.send(new BuilderClientCommand.CancelActive(jobId));
                    previewState.clearMissingPreview();
                }
            }
            case SELECTION -> BuilderClientNetworkBridge.cancelSourceSelection();
            case DEPLOYMENT -> BuilderClientNetworkBridge.cancelPlacementPreview();
        }
        updateWorkflowButtons();
    }

    private void renderWorkflowRows(GuiGraphics graphics, int left, int top) {
        List<WorkflowRow> rows = workflowRows(snapshot, BuilderPreviewState.get().snapshot());
        graphics.drawString(font, Component.translatable("screen.mydimension.realmwright.workflow.title"),
                left, top + 133, 0xFFBFC9D8, false);
        if (rows.isEmpty()) {
            graphics.drawString(font,
                    Component.translatable("screen.mydimension.realmwright.workflow.empty"),
                    left, top + WORKFLOW_ROW_TOP + 5, 0xFF8995A7, false);
            return;
        }
        for (int index = 0; index < rows.size(); index++) {
            int y = top + WORKFLOW_ROW_TOP + index * WORKFLOW_ROW_HEIGHT;
            graphics.fill(left, y, left + 310, y + 18, 0x7A1C2633);
            Component clipped = Component.literal(font.plainSubstrByWidth(
                    rows.get(index).label().getString(), 298));
            graphics.drawString(font, clipped, left + 6, y + 5, 0xFFE7EDF7, false);
        }
    }

    private void updateWorkflowButtons() {
        if (missingWorkflowCancel == null || selectionWorkflowCancel == null
                || deploymentWorkflowCancel == null) return;
        missingWorkflowCancel.visible = false;
        selectionWorkflowCancel.visible = false;
        deploymentWorkflowCancel.visible = false;
        List<WorkflowRow> rows = workflowRows(snapshot, BuilderPreviewState.get().snapshot());
        for (int index = 0; index < rows.size(); index++) {
            Button button = switch (rows.get(index).kind()) {
                case MISSING -> missingWorkflowCancel;
                case SELECTION -> selectionWorkflowCancel;
                case DEPLOYMENT -> deploymentWorkflowCancel;
            };
            button.setY(contentTop() + WORKFLOW_ROW_TOP + index * WORKFLOW_ROW_HEIGHT);
            button.visible = true;
            button.active = true;
        }
    }

    static List<WorkflowRow> workflowRows(BuilderClientSnapshot server,
                                          BuilderPreviewState.Snapshot preview) {
        java.util.ArrayList<WorkflowRow> rows = new java.util.ArrayList<>(3);
        UUID activeJobId = preview != null && preview.activeJobId() != null
                ? preview.activeJobId() : server.activeJobId();
        if (activeJobId != null) {
            int missing = preview == null ? 0 : (int) preview.cells().stream()
                    .filter(cell -> cell.kind() == BuilderPreviewState.Kind.MISSING).count();
            int completed = Math.max(0, server.completedBlocks());
            int total = Math.max(Math.max(0, server.totalBlocks()), completed + missing);
            int remaining = Math.max(missing, Math.max(0, total - completed));
            rows.add(new WorkflowRow(WorkflowKind.MISSING,
                    Component.translatable("screen.mydimension.realmwright.workflow.missing",
                            completed, total, remaining)));
        }
        if (preview != null && preview.selection() != null && preview.selection().active()) {
            BlockPos first = preview.selection().first();
            BlockPos second = preview.selection().second();
            Component label = second == null
                    ? Component.translatable("screen.mydimension.realmwright.workflow.selection_partial")
                    : Component.translatable("screen.mydimension.realmwright.workflow.selection",
                    Math.abs(first.getX() - second.getX()) + 1,
                    Math.abs(first.getY() - second.getY()) + 1,
                    Math.abs(first.getZ() - second.getZ()) + 1);
            rows.add(new WorkflowRow(WorkflowKind.SELECTION, label));
        }
        if (preview != null && preview.blueprintPreview()) {
            long blocks = preview.cells().stream()
                    .filter(cell -> cell.kind() != BuilderPreviewState.Kind.MISSING).count();
            rows.add(new WorkflowRow(WorkflowKind.DEPLOYMENT,
                    Component.translatable("screen.mydimension.realmwright.workflow.deployment", blocks)));
        }
        return List.copyOf(rows);
    }

    private Component anchorLabel(BuilderClientSnapshot.AnchorView anchor) {
        BlockPos pos = BlockPos.of(anchor.packedPos());
        return Component.literal(anchor.name() + "  " + anchor.status().name().toLowerCase(java.util.Locale.ROOT)
                + "  " + anchor.dimension() + " " + pos.toShortString());
    }

    private Component blueprintLabel(ClientBlueprintLibrary.Entry entry) {
        if (!entry.valid()) return Component.literal("! " + entry.name());
        String selected = entry.id().equals(selectedLocalBlueprint) ? "✓ " : "";
        return Component.literal(selected + entry.name() + "  " + entry.sizeX() + "×" + entry.sizeY() + "×"
                + entry.sizeZ() + "  [" + entry.blocks() + "]");
    }

    private Component historyLabel(BuilderClientSnapshot.HistoryView entry) {
        return Component.literal(entry.label() + "  " + entry.changedBlocks() + "  "
                + entry.status().name().toLowerCase(java.util.Locale.ROOT));
    }

    private int scroll(int offset, int direction, int size) {
        return Mth.clamp(offset + direction, 0, Math.max(0, size - PAGE_ROWS));
    }

    private int clampOffset(int offset, int size) {
        return Mth.clamp(offset, 0, Math.max(0, size - PAGE_ROWS));
    }

    private void executeOnClient(Runnable action) {
        Minecraft client = minecraft == null ? Minecraft.getInstance() : minecraft;
        client.execute(action);
    }

    private static String rootMessage(Throwable throwable) {
        Throwable current = throwable;
        while (current.getCause() != null) current = current.getCause();
        return current.getMessage() == null ? current.getClass().getSimpleName() : current.getMessage();
    }

    private int panelWidth() { return Math.min(PANEL_WIDTH, width - 16); }
    private int panelHeight() { return Math.min(PANEL_HEIGHT, height - 16); }
    private int panelLeft() { return (width - panelWidth()) / 2; }
    private int panelTop() { return (height - panelHeight()) / 2; }
    private int panelRight() { return panelLeft() + panelWidth(); }
    private int panelBottom() { return panelTop() + panelHeight(); }
    private int contentLeft() { return panelLeft() + Math.max(14, (panelWidth() - 400) / 2); }
    private int contentTop() { return panelTop() + 70; }
    private int contentBottom() { return panelBottom() - 38; }

    @Override
    public boolean isPauseScreen() {
        return false;
    }

    private enum Tab {
        OPERATIONS("screen.mydimension.realmwright.tab.operations"),
        SUPPLIES("screen.mydimension.realmwright.tab.supplies"),
        BLUEPRINTS("screen.mydimension.realmwright.tab.blueprints"),
        HISTORY("screen.mydimension.realmwright.tab.history"),
        SETTINGS("screen.mydimension.realmwright.tab.settings");

        private final String translationKey;

        Tab(String translationKey) {
            this.translationKey = translationKey;
        }
    }

    enum WorkflowKind {
        MISSING,
        SELECTION,
        DEPLOYMENT
    }

    record WorkflowRow(WorkflowKind kind, Component label) {
    }
}