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

XFEServerManager

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

公开
关注 0 Fork 0 Star 0
UTF-8
import type { TriggerEventBinding, TriggerExpression, TriggerFunctionDeclaration, TriggerProgramV2,
  TriggerStatement, TriggerVariableDeclarationV2 } from '../types';

export type TriggerProgramNode = { category: 'event' | 'declaration' | 'function' | 'statement';
  value: TriggerEventBinding | TriggerVariableDeclarationV2 | TriggerFunctionDeclaration | TriggerStatement };
export type TriggerPasteTarget = { parentId: string; branch: 'statements' | 'elseStatements' | 'cases'; caseId?: string };
const marker = 'xfesm-trigger-node-v2';
const maxClipboardCharacters = 1_048_576;

const object = (value: unknown): value is Record<string, unknown> => !!value && typeof value === 'object' && !Array.isArray(value);
const text = (value: unknown): value is string => typeof value === 'string';

// Clipboard JSON is untrusted. Check its entire AST before it can reach the recursive editor.
function checker(maxNodes = 4096, maxDepth = 32) {
  let nodes = 0;
  const node = (value: unknown): value is Record<string, unknown> => object(value)
    && text(value.nodeId) && !!value.nodeId && ++nodes <= maxNodes;
  const array = (value: unknown, check: (entry: unknown) => boolean) => Array.isArray(value) && value.every(check);
  const expression = (value: unknown, depth = 0): boolean => node(value) && depth <= maxDepth && text(value.name)
    && text(value.valueType) && ['LITERAL', 'REFERENCE', 'UNARY', 'BINARY', 'FUNCTION', 'INDEX', 'COALESCE', 'CONVERT'].includes(String(value.kind))
    && array(value.arguments, (entry) => expression(entry, depth + 1));
  const declaration = (value: unknown): boolean => node(value) && text(value.name) && text(value.valueType)
    && ['trigger', 'global'].includes(String(value.visibility)) && text(value.storage)
    && ['SESSION', 'TTL', 'PERSISTENT'].includes(String(value.lifetime))
    && (value.ttlSeconds == null || typeof value.ttlSeconds === 'number' && Number.isFinite(value.ttlSeconds))
    && (value.initialValue == null || expression(value.initialValue));
  const statement = (value: unknown, depth = 0): boolean => node(value) && depth <= maxDepth && text(value.name)
    && ['ACTION', 'SET', 'IF', 'SWITCH', 'REPEAT', 'WHILE', 'FOREACH', 'BREAK', 'CONTINUE', 'CALL', 'RETURN', 'TRY'].includes(String(value.kind))
    && object(value.inputs) && Object.values(value.inputs).every((entry) => expression(entry))
    && (value.expression == null || expression(value.expression))
    && array(value.statements, (entry) => statement(entry, depth + 1))
    && array(value.elseStatements, (entry) => statement(entry, depth + 1))
    && array(value.cases, (entry) => node(entry) && expression(entry.match)
      && array(entry.statements, (child) => statement(child, depth + 1)));
  const event = (value: unknown): boolean => node(value) && text(value.type) && object(value.configuration)
    && Object.values(value.configuration).every(text);
  const fn = (value: unknown): boolean => node(value) && text(value.name) && text(value.returnType)
    && array(value.parameters, (entry) => object(entry) && text(entry.name) && text(entry.valueType)
      && ['IN', 'OUT', 'INOUT'].includes(String(entry.mode)))
    && array(value.locals, declaration) && array(value.statements, (entry) => statement(entry));
  return { event, declaration, function: fn, statement };
}

export function serializeTriggerNode(node: TriggerProgramNode): string {
  return JSON.stringify({ marker, ...node });
}

export function parseTriggerNode(source: string): TriggerProgramNode | undefined {
  if (!source || source.length > maxClipboardCharacters) return undefined;
  try {
    const value: unknown = JSON.parse(source);
    if (!object(value) || value.marker !== marker || !['event', 'declaration', 'function', 'statement'].includes(String(value.category))) return undefined;
    const category = value.category as TriggerProgramNode['category'];
    return checker()[category](value.value) ? { category, value: value.value as TriggerProgramNode['value'] } : undefined;
  } catch { return undefined; }
}

export function triggerProgramFitsLimits(program: TriggerProgramV2, maxNodes = 4096, maxDepth = 32): boolean {
  const check = checker(maxNodes, maxDepth);
  return program.events.length > 0 && program.events.length <= 32 && program.events.every(check.event)
    && program.declarations.every(check.declaration) && program.functions.every(check.function)
    && program.statements.every((entry) => check.statement(entry));
}

function freshId(): string {
  return globalThis.crypto?.randomUUID?.() ?? 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (character) => {
    const value = Math.floor(Math.random() * 16); return (character === 'x' ? value : value & 3 | 8).toString(16);
  });
}

function cloneNode(node: TriggerProgramNode): TriggerProgramNode {
  const clone = structuredClone(node);
  const expression = (value?: TriggerExpression) => {
    if (!value) return;
    value.nodeId = freshId(); value.arguments.forEach(expression);
    // Literal records may contain an ordinary field named nodeId; that is user data, not an AST ID.
  };
  const declaration = (value: TriggerVariableDeclarationV2) => { value.nodeId = freshId(); expression(value.initialValue); };
  const statement = (value: TriggerStatement) => {
    value.nodeId = freshId(); expression(value.expression); Object.values(value.inputs).forEach(expression);
    value.statements.forEach(statement); value.elseStatements.forEach(statement);
    value.cases.forEach((branch) => { branch.nodeId = freshId(); expression(branch.match); branch.statements.forEach(statement); });
  };
  if (clone.category === 'statement') statement(clone.value as TriggerStatement);
  else if (clone.category === 'declaration') declaration(clone.value as TriggerVariableDeclarationV2);
  else if (clone.category === 'function') {
    const fn = clone.value as TriggerFunctionDeclaration;
    fn.nodeId = freshId(); fn.locals.forEach(declaration); fn.statements.forEach(statement);
  } else clone.value.nodeId = freshId();
  return clone;
}

function insertAfter<T extends { nodeId: string }>(items: T[], selectedId: string, value: T): T[] {
  const index = items.findIndex((entry) => entry.nodeId === selectedId);
  return index < 0 ? [...items, value] : [...items.slice(0, index + 1), value, ...items.slice(index + 1)];
}

function availableName(name: string, existing: string[], maxLength: number): string {
  if (!existing.includes(name)) return name;
  const stem = name.slice(0, maxLength - 12);
  let next = `${stem}_copy`; let index = 2;
  while (existing.includes(next)) next = `${stem}_copy${index++}`;
  return next;
}

export function insertTriggerNode(program: TriggerProgramV2, source: TriggerProgramNode, selectedId: string,
  target?: TriggerPasteTarget): { program: TriggerProgramV2; nodeId: string } {
  const copy = cloneNode(source);
  let next = program;
  if (copy.category === 'event') next = { ...program, events: insertAfter(program.events, selectedId, copy.value as TriggerEventBinding) };
  if (copy.category === 'declaration') {
    const value = copy.value as TriggerVariableDeclarationV2;
    value.name = availableName(value.name, program.declarations.map((entry) => entry.name), 48);
    next = { ...program, declarations: insertAfter(program.declarations, selectedId, value) };
  }
  if (copy.category === 'function') {
    const value = copy.value as TriggerFunctionDeclaration;
    const previousName = value.name;
    value.name = availableName(value.name, program.functions.map((entry) => entry.name), 128);
    const renameSelfCalls = (statements: TriggerStatement[]) => statements.forEach((statement) => {
      if (statement.kind === 'CALL' && statement.name === previousName) statement.name = value.name;
      renameSelfCalls(statement.statements); renameSelfCalls(statement.elseStatements);
      statement.cases.forEach((branch) => renameSelfCalls(branch.statements));
    });
    renameSelfCalls(value.statements);
    next = { ...program, functions: insertAfter(program.functions, selectedId, value) };
  }
  if (copy.category === 'statement') {
    const value = copy.value as TriggerStatement;
    let inserted = false;
    const insert = (statements: TriggerStatement[]): TriggerStatement[] => statements.flatMap((entry) => {
      if (!target && entry.nodeId === selectedId) { inserted = true; return [entry, value]; }
      const parent = target?.parentId === entry.nodeId;
      const branch = (key: 'statements' | 'elseStatements') => {
        if (parent && target.branch === key) { inserted = true; return [...entry[key], value]; }
        return insert(entry[key]);
      };
      return [{ ...entry, statements: branch('statements'), elseStatements: branch('elseStatements'),
        cases: entry.cases.map((caseBranch) => {
          if (parent && target.branch === 'cases' && target.caseId === caseBranch.nodeId) {
            inserted = true; return { ...caseBranch, statements: [...caseBranch.statements, value] };
          }
          return { ...caseBranch, statements: insert(caseBranch.statements) };
        }) }];
    });
    const statements = insert(program.statements);
    const functions = program.functions.map((fn) => {
      if (!target && fn.nodeId === selectedId) { inserted = true; return { ...fn, statements: [...fn.statements, value] }; }
      return { ...fn, statements: insert(fn.statements) };
    });
    next = { ...program, statements: inserted ? statements : [...statements, value], functions };
  }
  return { program: next, nodeId: copy.value.nodeId };
}