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

XFEServerManager

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

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

import com.xfestudio.xfeservermanager.core.trigger.TriggerEvaluator;

import java.time.Instant;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;

/**
 * Version-neutral menu document edited by the Web console and rendered by a compatible client.
 * Coordinates are logical pixels in the configured canvas and are scaled as one unit.
 */
public record MenuDefinition(
        UUID id,
        String name,
        String description,
        int width,
        int height,
        String popupPosition,
        int offsetX,
        int offsetY,
        boolean pauseGame,
        String backgroundColor,
        List<Control> controls,
        long revision,
        String createdBy,
        Instant createdAt,
        Instant updatedAt) {

    public static final int MIN_WIDTH = 120;
    public static final int MAX_WIDTH = 1_920;
    public static final int MIN_HEIGHT = 80;
    public static final int MAX_HEIGHT = 1_080;
    public static final int MAX_CONTROLS = 128;
    public static final Set<String> POSITIONS = Set.of(
            "center", "top", "bottom", "left", "right",
            "top_left", "top_right", "bottom_left", "bottom_right", "custom");
    public static final Set<String> CONTROL_TYPES = Set.of(
            "text", "rich_text", "button", "icon_button", "image", "panel", "border",
            "progress", "input", "checkbox", "slider", "item_slot", "separator", "spacer");
    public static final Set<String> CONTROL_EVENTS = Set.of(
            "click", "double_click", "change", "submit", "toggle", "hover", "focus", "blur");

    public MenuDefinition {
        Objects.requireNonNull(id, "id");
        name = bounded(name, "menu name", 1, 96);
        description = bounded(description, "menu description", 0, 1_024);
        if (width < MIN_WIDTH || width > MAX_WIDTH) {
            throw new IllegalArgumentException("menu width must be between " + MIN_WIDTH + " and " + MAX_WIDTH);
        }
        if (height < MIN_HEIGHT || height > MAX_HEIGHT) {
            throw new IllegalArgumentException("menu height must be between " + MIN_HEIGHT + " and " + MAX_HEIGHT);
        }
        popupPosition = Objects.toString(popupPosition, "center").strip().toLowerCase(java.util.Locale.ROOT);
        if (!POSITIONS.contains(popupPosition)) throw new IllegalArgumentException("unsupported menu popup position");
        if (Math.abs(offsetX) > 8_192 || Math.abs(offsetY) > 8_192) {
            throw new IllegalArgumentException("menu offsets must be between -8192 and 8192");
        }
        backgroundColor = color(backgroundColor, "menu backgroundColor");
        controls = controls == null ? List.of() : List.copyOf(controls);
        if (controls.size() > MAX_CONTROLS) {
            throw new IllegalArgumentException("a menu cannot contain more than " + MAX_CONTROLS + " controls");
        }
        Set<String> ids = new HashSet<>();
        for (Control control : controls) {
            Objects.requireNonNull(control, "menu control");
            if (!ids.add(control.id())) throw new IllegalArgumentException("duplicate menu control id " + control.id());
            if ((long) control.x() + control.width() > width || (long) control.y() + control.height() > height) {
                throw new IllegalArgumentException("menu control lies outside the canvas: " + control.id());
            }
        }
        if (revision < 0) throw new IllegalArgumentException("menu revision must not be negative");
        createdBy = bounded(createdBy, "menu creator", 1, 128);
        Objects.requireNonNull(createdAt, "createdAt");
        Objects.requireNonNull(updatedAt, "updatedAt");
    }

    public record Control(
            String id,
            String type,
            int x,
            int y,
            int width,
            int height,
            int zIndex,
            String visibleWhen,
            Map<String, String> properties,
            List<EventBinding> events) {
        public Control {
            id = bounded(id, "control id", 1, 64);
            if (!id.matches("[A-Za-z_][A-Za-z0-9_.-]{0,63}")) {
                throw new IllegalArgumentException("control id must be an identifier");
            }
            if (id.equals("__xfesm_root__")) throw new IllegalArgumentException("control id is reserved");
            type = Objects.toString(type, "").strip().toLowerCase(java.util.Locale.ROOT);
            if (!CONTROL_TYPES.contains(type)) throw new IllegalArgumentException("unsupported menu control type " + type);
            if (x < 0 || y < 0 || width < 1 || height < 1 || width > MAX_WIDTH || height > MAX_HEIGHT) {
                throw new IllegalArgumentException("menu control geometry is invalid: " + id);
            }
            if (zIndex < -1_000 || zIndex > 1_000) throw new IllegalArgumentException("control zIndex is out of range");
            visibleWhen = bounded(visibleWhen, "control visibleWhen", 0, 512);
            if (!visibleWhen.isEmpty()) TriggerEvaluator.validateTemplateVariables(visibleWhen);
            Map<String, String> safeProperties = new LinkedHashMap<>();
            if (properties != null) {
                if (properties.size() > 48) throw new IllegalArgumentException("a control has too many properties");
                properties.forEach((key, value) -> {
                    String safeKey = bounded(key, "control property name", 1, 64);
                    if (!safeKey.matches("[A-Za-z][A-Za-z0-9_.-]{0,63}")) {
                        throw new IllegalArgumentException("invalid control property name " + safeKey);
                    }
                    String safeValue = propertyValue(value);
                    TriggerEvaluator.validateTemplateVariables(safeValue);
                    safeProperties.put(safeKey, safeValue);
                });
            }
            properties = Map.copyOf(safeProperties);
            events = events == null ? List.of() : List.copyOf(events);
            if (events.size() > CONTROL_EVENTS.size()) throw new IllegalArgumentException("a control has too many events");
            Set<String> eventNames = new HashSet<>();
            for (EventBinding event : events) {
                Objects.requireNonNull(event, "control event");
                if (!eventNames.add(event.event())) {
                    throw new IllegalArgumentException("duplicate " + event.event() + " binding on " + id);
                }
            }
        }
    }

    /** A control event may open a submenu, invoke a menu.control trigger, close, or combine them. */
    public record EventBinding(String event, String triggerId, String submenuId, boolean closeMenu) {
        public EventBinding {
            event = Objects.toString(event, "click").strip().toLowerCase(java.util.Locale.ROOT);
            if (!CONTROL_EVENTS.contains(event)) throw new IllegalArgumentException("unsupported control event " + event);
            triggerId = optionalUuid(triggerId, "triggerId");
            submenuId = optionalUuid(submenuId, "submenuId");
            if (triggerId.isEmpty() && submenuId.isEmpty() && !closeMenu) {
                throw new IllegalArgumentException("a control event needs a trigger, submenu, or close action");
            }
        }
    }

    private static String optionalUuid(String value, String name) {
        String normalized = Objects.toString(value, "").strip();
        if (normalized.isEmpty()) return "";
        try {
            return UUID.fromString(normalized).toString();
        } catch (IllegalArgumentException invalid) {
            throw new IllegalArgumentException(name + " must be a UUID", invalid);
        }
    }

    private static String color(String value, String name) {
        String normalized = Objects.toString(value, "#101713e8").strip().toLowerCase(java.util.Locale.ROOT);
        if (!normalized.matches("#[0-9a-f]{6}(?:[0-9a-f]{2})?")) {
            throw new IllegalArgumentException(name + " must be #RRGGBB or #RRGGBBAA");
        }
        return normalized;
    }

    private static String bounded(String value, String name, int minimum, int maximum) {
        String safe = Objects.toString(value, "").strip();
        if (safe.length() < minimum || safe.length() > maximum || safe.codePoints().anyMatch(Character::isISOControl)) {
            throw new IllegalArgumentException(name + " must contain " + minimum + "-" + maximum + " printable characters");
        }
        return safe;
    }

    private static String propertyValue(String value) {
        String safe = Objects.toString(value, "");
        if (safe.length() > 16_384 || safe.codePoints().anyMatch(codePoint ->
                Character.isISOControl(codePoint) && codePoint != '\n' && codePoint != '\r' && codePoint != '\t')) {
            throw new IllegalArgumentException("control property value is too long or contains control characters");
        }
        return safe;
    }
}